From 5b4392894f22658bab2a53df9d9efe345f55490f Mon Sep 17 00:00:00 2001 From: liangzai <2440983361@qq.com> Date: Wed, 2 Jul 2025 16:17:59 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=9E=B6=E6=9E=84):=20=E5=AE=8C=E5=96=84?= =?UTF-8?q?=E5=9F=BA=E7=A1=80=E6=9E=B6=E6=9E=84=E8=AE=BE=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cursor/rules/api.mdc | 2527 ++++++++++++++ .cursor/rules/global.mdc | 76 + .cursor/rules/start.mdc | 21 + .dockerignore | 47 + .env.production | 53 + .gitignore | 2 +- COMPLETE_ARCHITECTURE_PLAN.md | 811 ----- Dockerfile | 68 + Makefile | 450 ++- README.md | 347 +- api.md | 2953 +++++++++++++++++ cmd/api/main.go | 150 +- config.prod.yaml | 91 - config.yaml | 31 +- configs/env.development.yaml | 20 + configs/env.production.yaml | 32 + configs/env.testing.yaml | 39 + .../provisioning/dashboards/dashboards.yml | 11 + .../dashboards/tracing-dashboard.json | 206 ++ .../provisioning/datasources/datasources.yml | 36 + deployments/docker/jaeger-sampling-prod.json | 109 + deployments/docker/jaeger-sampling.json | 109 + deployments/docker/jaeger-ui-config.json | 46 + deployments/docker/nginx.conf | 234 ++ deployments/docker/pgadmin-passfile | 1 + deployments/docker/pgadmin-servers.json | 15 + deployments/docker/prometheus.yml | 39 + docker-compose.dev.yml | 325 +- docker-compose.prod.yml | 442 +++ docs/Makefile使用指南.md | 668 ++++ docs/docs.go | 8 + docs/swagger/docs.go | 592 ++++ docs/swagger/swagger.json | 568 ++++ docs/swagger/swagger.yaml | 397 +++ docs/{ => 开始指南}/开发指南.md | 0 docs/{ => 开始指南}/快速开始指南.md | 0 docs/{ => 开始指南}/故障排除指南.md | 15 +- docs/开始指南/文档索引.md | 145 + docs/{ => 开始指南}/最佳实践指南.md | 0 .../架构设计文档.md} | 0 docs/{ => 开始指南}/环境搭建指南.md | 0 docs/开始指南/生产环境部署指南.md | 338 ++ docs/{ => 开始指南}/部署指南.md | 0 docs/开始指南/链路追踪指南.md | 338 ++ docs/文档索引.md | 102 - env.example | 137 - go.mod | 43 +- go.sum | 185 +- internal/app/app.go | 29 +- internal/config/config.go | 53 +- internal/config/loader.go | 330 +- internal/container/container.go | 455 ++- internal/domains/user/dto/sms_dto.go | 72 + internal/domains/user/dto/user_dto.go | 177 +- internal/domains/user/entities/sms_code.go | 88 + internal/domains/user/entities/user.go | 97 +- internal/domains/user/events/user_events.go | 150 +- .../domains/user/handlers/user_handler.go | 524 +-- .../user/repositories/sms_code_repository.go | 120 + .../user/repositories/user_repository.go | 320 +- internal/domains/user/routes/user_routes.go | 136 +- .../domains/user/services/sms_code_service.go | 187 ++ .../domains/user/services/user_service.go | 438 +-- internal/shared/health/health_checker.go | 34 +- internal/shared/hooks/hook_system.go | 587 ++++ internal/shared/http/response.go | 48 +- internal/shared/http/router.go | 58 +- internal/shared/http/validator.go | 74 +- internal/shared/http/validator_zh.go | 294 ++ internal/shared/interfaces/http.go | 8 + internal/shared/interfaces/service.go | 25 + internal/shared/logger/enhanced_logger.go | 214 ++ internal/shared/metrics/business_metrics.go | 263 ++ internal/shared/metrics/prometheus_metrics.go | 353 ++ internal/shared/middleware/auth.go | 12 +- internal/shared/middleware/ratelimit.go | 14 +- internal/shared/middleware/request_logger.go | 303 +- internal/shared/resilience/circuit_breaker.go | 389 +++ internal/shared/resilience/retry.go | 467 +++ internal/shared/saga/saga.go | 612 ++++ internal/shared/sms/sms_service.go | 130 + internal/shared/tracing/decorators.go | 292 ++ internal/shared/tracing/gorm_plugin.go | 320 ++ internal/shared/tracing/redis_wrapper.go | 407 +++ internal/shared/tracing/service_wrapper.go | 189 ++ internal/shared/tracing/tracer.go | 474 +++ scripts/deploy.ps1 | 255 ++ scripts/deploy.sh | 221 ++ scripts/init.sql | 30 +- 89 files changed, 18555 insertions(+), 3521 deletions(-) create mode 100644 .cursor/rules/api.mdc create mode 100644 .cursor/rules/global.mdc create mode 100644 .cursor/rules/start.mdc create mode 100644 .dockerignore create mode 100644 .env.production delete mode 100644 COMPLETE_ARCHITECTURE_PLAN.md create mode 100644 Dockerfile create mode 100644 api.md delete mode 100644 config.prod.yaml create mode 100644 configs/env.development.yaml create mode 100644 configs/env.production.yaml create mode 100644 configs/env.testing.yaml create mode 100644 deployments/docker/grafana/provisioning/dashboards/dashboards.yml create mode 100644 deployments/docker/grafana/provisioning/dashboards/tracing-dashboard.json create mode 100644 deployments/docker/grafana/provisioning/datasources/datasources.yml create mode 100644 deployments/docker/jaeger-sampling-prod.json create mode 100644 deployments/docker/jaeger-sampling.json create mode 100644 deployments/docker/jaeger-ui-config.json create mode 100644 deployments/docker/nginx.conf create mode 100644 deployments/docker/pgadmin-passfile create mode 100644 deployments/docker/pgadmin-servers.json create mode 100644 deployments/docker/prometheus.yml create mode 100644 docker-compose.prod.yml create mode 100644 docs/Makefile使用指南.md create mode 100644 docs/docs.go create mode 100644 docs/swagger/docs.go create mode 100644 docs/swagger/swagger.json create mode 100644 docs/swagger/swagger.yaml rename docs/{ => 开始指南}/开发指南.md (100%) rename docs/{ => 开始指南}/快速开始指南.md (100%) rename docs/{ => 开始指南}/故障排除指南.md (97%) create mode 100644 docs/开始指南/文档索引.md rename docs/{ => 开始指南}/最佳实践指南.md (100%) rename docs/{ARCHITECTURE.md => 开始指南/架构设计文档.md} (100%) rename docs/{ => 开始指南}/环境搭建指南.md (100%) create mode 100644 docs/开始指南/生产环境部署指南.md rename docs/{ => 开始指南}/部署指南.md (100%) create mode 100644 docs/开始指南/链路追踪指南.md delete mode 100644 docs/文档索引.md delete mode 100644 env.example create mode 100644 internal/domains/user/dto/sms_dto.go create mode 100644 internal/domains/user/entities/sms_code.go create mode 100644 internal/domains/user/repositories/sms_code_repository.go create mode 100644 internal/domains/user/services/sms_code_service.go create mode 100644 internal/shared/hooks/hook_system.go create mode 100644 internal/shared/http/validator_zh.go create mode 100644 internal/shared/logger/enhanced_logger.go create mode 100644 internal/shared/metrics/business_metrics.go create mode 100644 internal/shared/metrics/prometheus_metrics.go create mode 100644 internal/shared/resilience/circuit_breaker.go create mode 100644 internal/shared/resilience/retry.go create mode 100644 internal/shared/saga/saga.go create mode 100644 internal/shared/sms/sms_service.go create mode 100644 internal/shared/tracing/decorators.go create mode 100644 internal/shared/tracing/gorm_plugin.go create mode 100644 internal/shared/tracing/redis_wrapper.go create mode 100644 internal/shared/tracing/service_wrapper.go create mode 100644 internal/shared/tracing/tracer.go create mode 100644 scripts/deploy.ps1 create mode 100644 scripts/deploy.sh diff --git a/.cursor/rules/api.mdc b/.cursor/rules/api.mdc new file mode 100644 index 0000000..f56538b --- /dev/null +++ b/.cursor/rules/api.mdc @@ -0,0 +1,2527 @@ +--- +description: +globs: +alwaysApply: false +--- +# 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 请求频率限制 +- 📱 **短信限流**: 验证码发送频率限制 +- 🔐 **登录限流**: 防止暴力破解 +- 📧 **邮件限流**: 邮件发送频率限制 +- �� **搜索限流**: 防止恶意搜索 + +### 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 " + +# 响应示例 +{ + "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 " \ + -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 开发的一致性、可维护性和扩展性。 diff --git a/.cursor/rules/global.mdc b/.cursor/rules/global.mdc new file mode 100644 index 0000000..f5c5132 --- /dev/null +++ b/.cursor/rules/global.mdc @@ -0,0 +1,76 @@ +--- +description: +globs: +alwaysApply: true +--- +# 语言规范 +-- 对于用户端输出和前端响应看到的文字内容部分,尽量使用中文 + +## 中文化规则 + +### 1. 面向用户的内容 +- 所有面向用户的响应消息必须使用中文 +- HTTP API 响应中的消息字段(`message`)必须使用中文 +- 错误提示信息必须使用中文 +- 用户界面上的所有文本必须使用中文 + +### 2. 错误和验证信息 +- 所有验证错误提示必须使用中文 +- 字段验证规则的错误消息必须使用中文 +- 业务逻辑错误提示必须使用中文 +- HTTP状态码对应的消息必须使用中文 + +### 3. 日志信息 +- 业务操作相关的日志消息应使用中文 +- 系统状态和生命周期相关的日志应使用中文 +- 错误和警告日志消息应使用中文 +- 日志中的上下文信息(字段名)可使用英文 + +### 4. 代码规范 +- 代码注释可以使用中文,提高可读性 +- 变量名、函数名、类名等标识符仍使用英文 +- 中文字符串应使用双引号,不使用反引号 +- 避免在代码中使用特殊中文标点符号 + +### 5. 中文规范示例 + +#### HTTP 响应示例: +```go +// 成功响应 +h.response.Success(c, data, "操作成功") +h.response.Created(c, user, "用户创建成功") + +// 错误响应 +h.response.BadRequest(c, "请求参数错误") +h.response.Unauthorized(c, "用户未认证或登录已过期") +h.response.NotFound(c, "请求的资源不存在") +h.response.TooManyRequests(c, "请求过于频繁,请稍后再试") +``` + +#### 验证错误示例: +```go +// 字段验证错误 +"手机号格式不正确" +"密码强度不足,必须包含大小写字母和数字" +"两次输入的密码不一致" + +// 注册自定义验证器 +v.RegisterTranslation("phone", trans, func(ut ut.Translator) error { + return ut.Add("phone", "{0}必须是有效的手机号", true) +}) +``` + +#### 日志消息示例: +```go +// 成功日志 +logger.Info("用户注册成功", zap.String("user_id", user.ID)) +logger.Info("支付订单已完成", zap.String("order_id", order.ID)) + +// 错误日志 +logger.Error("创建用户失败", zap.Error(err)) +logger.Error("数据库连接失败", zap.Error(err)) + +// 警告日志 +logger.Warn("用户多次登录失败", zap.String("phone", phone)) +logger.Warn("接口调用频率异常", zap.String("client_ip", clientIP)) +``` diff --git a/.cursor/rules/start.mdc b/.cursor/rules/start.mdc new file mode 100644 index 0000000..ce914e9 --- /dev/null +++ b/.cursor/rules/start.mdc @@ -0,0 +1,21 @@ +--- +description: +globs: +alwaysApply: true +--- +# 开发环境和偏好设置 + +## 开发环境 +- **操作系统**: Windows +- **Shell环境**: PowerShell +- **项目路径**: D:\Code\tyapi-project\tyapi-server-gin + +## 开发偏好 +- **代码测试**: 不需要AI在编写完代码后自动运行项目进行测试 +- **测试责任**: 用户自己负责测试和运行项目 +- **AI角色**: 专注于代码编写、修改和解决方案提供,不进行实际的项目运行测试 + +## 注意事项 +- 在Windows PowerShell环境中,某些命令可能与Linux/Mac不同 +- 避免使用需要用户交互的命令 +- 如需运行命令,优先考虑PowerShell兼容的语法 diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d0bbae9 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,47 @@ +# 开发环境文件 +.env* +*.log +logs/ +tmp/ + +# 构建产物 +bin/ +dist/ +build/ + +# 测试文件 +*_test.go +coverage.out +coverage.html + +# 开发工具 +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS 文件 +.DS_Store +Thumbs.db + +# Git +.git/ +.gitignore + +# Docker 相关 +Dockerfile* +docker-compose*.yml +.dockerignore + +# 文档 +README.md +docs/ +*.md + +# 开发依赖 +node_modules/ + +# 临时文件 +*.tmp +*.temp \ No newline at end of file diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..896cb58 --- /dev/null +++ b/.env.production @@ -0,0 +1,53 @@ +# 生产环境配置模板 +# 复制此文件到服务器并重命名为 .env,然后修改相应的值 + +# 应用配置 +APP_VERSION=latest +APP_PORT=8080 + +# 数据库配置 (必须修改) +DB_USER=tyapi_user +DB_PASSWORD=your_secure_database_password_here +DB_NAME=tyapi_prod +DB_SSLMODE=require + +# Redis配置 (必须修改) +REDIS_PASSWORD=your_secure_redis_password_here + +# JWT配置 (必须修改) +JWT_SECRET=your_super_secure_jwt_secret_key_for_production_at_least_32_chars + +# 日志级别 +LOG_LEVEL=info + +# 端口配置 +NGINX_HTTP_PORT=80 +NGINX_HTTPS_PORT=443 +JAEGER_UI_PORT=16686 +PROMETHEUS_PORT=9090 +GRAFANA_PORT=3000 +MINIO_API_PORT=9000 +MINIO_CONSOLE_PORT=9001 +PGADMIN_PORT=5050 + +# Grafana 配置 +GRAFANA_ADMIN_USER=admin +GRAFANA_ADMIN_PASSWORD=your_secure_grafana_password_here + +# MinIO 配置 +MINIO_ROOT_USER=minioadmin +MINIO_ROOT_PASSWORD=your_secure_minio_password_here + +# pgAdmin 配置 +PGADMIN_EMAIL=admin@tyapi.com +PGADMIN_PASSWORD=your_secure_pgadmin_password_here + +# 短信服务配置 (必须修改) +SMS_ACCESS_KEY_ID=your_sms_access_key_id +SMS_ACCESS_KEY_SECRET=your_sms_access_key_secret +SMS_SIGN_NAME=your_sms_sign_name +SMS_TEMPLATE_CODE=your_sms_template_code + +# SSL证书路径 (如果使用HTTPS) +# SSL_CERT_PATH=/path/to/cert.pem +# SSL_KEY_PATH=/path/to/key.pem diff --git a/.gitignore b/.gitignore index b80c85c..ebefbf8 100644 --- a/.gitignore +++ b/.gitignore @@ -55,7 +55,7 @@ temp/ build/ dist/ -# Air live reload +# Temporary directories tmp/ # Compiled binary diff --git a/COMPLETE_ARCHITECTURE_PLAN.md b/COMPLETE_ARCHITECTURE_PLAN.md deleted file mode 100644 index 992db91..0000000 --- a/COMPLETE_ARCHITECTURE_PLAN.md +++ /dev/null @@ -1,811 +0,0 @@ -# 🏗️ 2025 年最佳 Gin Web 架构完整实施计划 - -## 📋 项目概述 - -构建一个基于 Clean Architecture + DDD 的高性能、模块化 Gin Web 应用架构,支持快速开发、易维护、高扩展性。 - -## 🎯 架构目标 - -- ✅ **高度解耦**: 清晰的分层架构,依赖倒置 -- ✅ **模块化**: 支持模块间快速复制和一键接入 -- ✅ **高性能**: 优化的并发处理和自动缓存策略 -- ✅ **易测试**: 完整的单元测试和集成测试 -- ✅ **易维护**: 标准化的代码结构和文档 -- ✅ **可扩展**: 支持微服务演进 -- ✅ **生产就绪**: 完整的安全、监控、容错机制 - -## 📁 完整项目目录结构 - -``` -tyapi-server-gin/ -├── cmd/ -│ └── api/ -│ └── main.go # 应用程序入口 -├── internal/ -│ ├── config/ # 全局配置 -│ │ ├── config.go # 配置结构体 -│ │ ├── database.go # 数据库配置 -│ │ ├── server.go # 服务器配置 -│ │ └── loader.go # 配置加载器 -│ ├── container/ # 依赖注入容器 -│ │ ├── container.go # FX容器 -│ │ ├── providers.go # 全局依赖提供者 -│ │ └── module_registry.go # 模块注册器 -│ ├── shared/ # 共享基础设施 -│ │ ├── database/ -│ │ │ ├── connection.go # 数据库连接 -│ │ │ ├── base_repository.go # 通用仓储基类 -│ │ │ └── pool_manager.go # 连接池管理器 -│ │ ├── cache/ -│ │ │ ├── redis.go # Redis缓存实现 -│ │ │ ├── cache_wrapper.go # 查询缓存包装器 -│ │ │ └── cache_manager.go # 缓存管理器 -│ │ ├── logger/ -│ │ │ └── logger.go # 结构化日志 -│ │ ├── middleware/ # 共享中间件 -│ │ │ ├── auth.go # 简单认证 -│ │ │ ├── cors.go # 跨域处理 -│ │ │ ├── logger.go # 日志中间件 -│ │ │ ├── recovery.go # 异常恢复 -│ │ │ └── security.go # 安全中间件栈 -│ │ ├── events/ # 事件总线 -│ │ │ ├── event_bus.go -│ │ │ └── event_handler.go -│ │ ├── saga/ # 分布式事务 -│ │ │ ├── saga.go -│ │ │ └── saga_executor.go -│ │ ├── metrics/ # 指标收集 -│ │ │ ├── simple_metrics.go -│ │ │ └── business_metrics.go -│ │ ├── tracing/ # 链路追踪 -│ │ │ └── simple_tracer.go -│ │ ├── resilience/ # 容错机制 -│ │ │ ├── circuit_breaker.go -│ │ │ └── retry.go -│ │ ├── hooks/ # 钩子系统 -│ │ │ └── hook_system.go -│ │ ├── health/ # 健康检查 -│ │ │ └── health_checker.go -│ │ └── interfaces/ -│ │ └── module.go # 模块接口定义 -│ └── domains/ # 业务域 -│ ├── user/ # 用户域 -│ │ ├── entities/ -│ │ │ ├── user.go -│ │ │ ├── profile.go -│ │ │ └── auth_token.go -│ │ ├── repositories/ -│ │ │ ├── user_repository.go # 接口 -│ │ │ └── user_repository_impl.go # 实现 -│ │ ├── services/ -│ │ │ ├── user_service.go # 接口 -│ │ │ ├── user_service_impl.go # 实现 -│ │ │ └── auth_service.go -│ │ ├── dto/ -│ │ │ ├── user_dto.go -│ │ │ └── auth_dto.go -│ │ ├── handlers/ -│ │ │ ├── user_handler.go -│ │ │ └── auth_handler.go -│ │ ├── routes/ -│ │ │ └── user_routes.go -│ │ ├── validators/ -│ │ │ └── user_validator.go -│ │ ├── migrations/ -│ │ │ └── 001_create_users_table.sql -│ │ ├── events/ -│ │ │ └── user_events.go -│ │ └── module.go # 模块定义 -│ ├── product/ # 产品域 -│ │ ├── entities/ -│ │ │ ├── product.go -│ │ │ ├── category.go -│ │ │ └── inventory.go -│ │ ├── repositories/ -│ │ │ ├── product_repository.go -│ │ │ ├── product_repository_impl.go -│ │ │ └── category_repository.go -│ │ ├── services/ -│ │ │ ├── product_service.go -│ │ │ ├── product_service_impl.go -│ │ │ └── inventory_service.go -│ │ ├── dto/ -│ │ │ ├── product_dto.go -│ │ │ └── inventory_dto.go -│ │ ├── handlers/ -│ │ │ ├── product_handler.go -│ │ │ └── category_handler.go -│ │ ├── routes/ -│ │ │ └── product_routes.go -│ │ ├── validators/ -│ │ │ └── product_validator.go -│ │ ├── migrations/ -│ │ │ └── 002_create_products_table.sql -│ │ ├── events/ -│ │ │ └── product_events.go -│ │ └── module.go -│ ├── finance/ # 财务域 -│ │ ├── entities/ -│ │ │ ├── order.go -│ │ │ ├── payment.go -│ │ │ └── invoice.go -│ │ ├── repositories/ -│ │ │ ├── order_repository.go -│ │ │ ├── order_repository_impl.go -│ │ │ └── payment_repository.go -│ │ ├── services/ -│ │ │ ├── order_service.go -│ │ │ ├── order_service_impl.go -│ │ │ └── payment_service.go -│ │ ├── dto/ -│ │ │ ├── order_dto.go -│ │ │ └── payment_dto.go -│ │ ├── handlers/ -│ │ │ ├── order_handler.go -│ │ │ └── payment_handler.go -│ │ ├── routes/ -│ │ │ └── finance_routes.go -│ │ ├── validators/ -│ │ │ └── order_validator.go -│ │ ├── migrations/ -│ │ │ └── 003_create_orders_table.sql -│ │ ├── events/ -│ │ │ └── finance_events.go -│ │ └── module.go -│ └── analytics/ # 数据业务域 -│ ├── entities/ -│ │ ├── report.go -│ │ ├── metric.go -│ │ └── dashboard.go -│ ├── repositories/ -│ │ ├── analytics_repository.go -│ │ └── analytics_repository_impl.go -│ ├── services/ -│ │ ├── analytics_service.go -│ │ ├── analytics_service_impl.go -│ │ └── report_service.go -│ ├── dto/ -│ │ ├── report_dto.go -│ │ └── metric_dto.go -│ ├── handlers/ -│ │ ├── analytics_handler.go -│ │ └── report_handler.go -│ ├── routes/ -│ │ └── analytics_routes.go -│ ├── validators/ -│ │ └── report_validator.go -│ ├── migrations/ -│ │ └── 004_create_analytics_table.sql -│ ├── events/ -│ │ └── analytics_events.go -│ └── module.go -├── pkg/ # 公共工具包 -│ ├── utils/ -│ │ ├── converter.go # 类型转换 -│ │ ├── validator.go # 验证工具 -│ │ └── response.go # 响应工具 -│ ├── constants/ -│ │ ├── errors.go # 错误常量 -│ │ └── status.go # 状态常量 -│ └── errors/ -│ └── errors.go # 自定义错误 -├── scripts/ # 脚本工具 -│ ├── generate-domain.sh # 域生成器 -│ ├── generate-module.sh # 模块生成器 -│ ├── register-module.sh # 模块注册器 -│ ├── build.sh # 构建脚本 -│ └── migrate.sh # 数据库迁移脚本 -├── deployments/ # 部署配置 -│ ├── docker/ -│ │ ├── Dockerfile # 多阶段构建 -│ │ └── docker-compose.yml # 生产环境 -│ └── docker-compose.dev.yml # 开发环境 -├── docs/ # 文档 -│ ├── DEVELOPMENT.md # 开发指南 -│ ├── DEPLOYMENT.md # 部署指南 -│ └── API.md # API使用说明 -├── test/ # 测试 -│ ├── integration/ # 集成测试 -│ └── fixtures/ # 测试数据 -├── .env.example # 环境变量示例 -├── .gitignore -├── Makefile # 构建命令 -├── go.mod -└── go.sum -``` - -## 🔧 技术栈选择 - -### 核心框架 - -- **Web 框架**: Gin v1.10+ -- **ORM**: GORM v2 (支持泛型) -- **依赖注入**: Uber FX -- **配置管理**: Viper -- **验证**: go-playground/validator v10 - -### 数据存储 - -- **主数据库**: PostgreSQL 15+ -- **缓存**: Redis 7+ (自动缓存数据库查询) - -### 监控和日志 - -- **日志**: Zap + OpenTelemetry -- **指标收集**: 简化的业务指标系统 -- **链路追踪**: 轻量级请求追踪 -- **健康检查**: 数据库和 Redis 状态监控 - -### 开发工具 - -- **代码生成**: Wire (可选) -- **测试**: Testify + Testcontainers -- **Linting**: golangci-lint - -## 📅 分步骤实施计划 - -### 🚀 阶段 1: 基础架构搭建 (预计 2-3 小时) - -#### Step 1.1: 项目初始化 - -- [ ] 初始化 Go 模块 -- [ ] 创建基础目录结构 -- [ ] 配置.gitignore 和基础文件 -- [ ] 安装核心依赖 - -#### Step 1.2: 配置系统 - -- [ ] 实现配置加载器 (Viper) -- [ ] 创建环境变量管理 -- [ ] 设置不同环境配置 (dev/staging/prod) - -#### Step 1.3: 日志系统 - -- [ ] 集成 Zap 日志库 -- [ ] 配置结构化日志 -- [ ] 实现日志中间件 - -#### Step 1.4: 数据库连接 - -- [ ] 配置 PostgreSQL 连接 -- [ ] 实现连接池管理 -- [ ] 集成 GORM -- [ ] 创建数据库迁移机制 - -### 🏗️ 阶段 2: 架构核心层 (预计 3-4 小时) - -#### Step 2.1: 领域层设计 - -- [ ] 定义基础实体接口 -- [ ] 创建用户实体示例 -- [ ] 实现仓储接口定义 -- [ ] 设计业务服务接口 - -#### Step 2.2: 基础设施层 - -- [ ] 实现通用仓储基类 (支持自动缓存) -- [ ] 创建缓存包装器和管理器 -- [ ] 创建用户仓储实现 (集成缓存) -- [ ] 集成 Redis 缓存 -- [ ] 实现监控指标收集 - -#### Step 2.3: 应用层 - -- [ ] 定义 DTO 结构 -- [ ] 实现业务服务 -- [ ] 创建用例处理器 -- [ ] 实现数据转换器 - -#### Step 2.4: 依赖注入 - -- [ ] 配置 FX 容器 -- [ ] 实现依赖提供者 -- [ ] 创建模块注册机制 - -### 🌐 阶段 3: Web 层实现 (预计 2-3 小时) - -#### Step 3.1: HTTP 层基础 - -- [ ] 创建 Gin 路由器 -- [ ] 实现基础中间件 (CORS, Recovery, Logger) -- [ ] 配置路由组织结构 - -#### Step 3.2: 处理器实现 - -- [ ] 实现用户 CRUD 处理器 -- [ ] 创建健康检查处理器 -- [ ] 实现统一响应格式 -- [ ] 添加请求验证 - -#### Step 3.3: 中间件系统 - -- [ ] 实现简单认证中间件 -- [ ] 创建限流中间件 -- [ ] 添加请求 ID 追踪 -- [ ] 实现错误处理中间件 -- [ ] 集成安全头中间件 - -### 🔧 阶段 4: 高级特性实现 (预计 3-4 小时) - -#### Step 4.1: 自动缓存系统 - -- [ ] 实现缓存包装器 -- [ ] 集成查询结果自动缓存 -- [ ] 实现智能缓存失效策略 -- [ ] 添加缓存穿透防护 - -#### Step 4.2: 跨域事务处理 - -- [ ] 实现 Saga 模式事务协调器 -- [ ] 创建事件驱动架构 -- [ ] 实现补偿机制 -- [ ] 添加视图聚合模式 - -#### Step 4.3: 可观测性 - -- [ ] 集成简单指标收集 -- [ ] 实现链路追踪 -- [ ] 添加业务指标监控 -- [ ] 创建性能监控面板 - -#### Step 4.4: 容错机制 - -- [ ] 实现简化熔断器 -- [ ] 添加智能重试机制 -- [ ] 集成钩子系统 -- [ ] 实现优雅降级 - -### 🚀 阶段 5: 部署和工具 (预计 2-3 小时) - -#### Step 5.1: 容器化 (Docker Compose) - -- [ ] 创建多阶段 Dockerfile -- [ ] 配置生产环境 docker-compose.yml -- [ ] 配置开发环境 docker-compose.dev.yml -- [ ] 集成 PostgreSQL 和 Redis 容器 -- [ ] 实现健康检查 -- [ ] 配置数据卷持久化 - -#### Step 5.2: 开发工具 - -- [ ] 创建 Makefile 命令 -- [ ] 实现模块生成器脚本 -- [ ] 配置代码质量检查 -- [ ] 创建数据库迁移脚本 - -#### Step 5.3: 生产就绪 - -- [ ] 实现优雅关闭 -- [ ] 配置信号处理 -- [ ] 添加安全性增强 -- [ ] 创建监控和告警 - -## 🔄 自动缓存架构设计 - -### 缓存包装器实现 - -```go -// 缓存包装器接口 -type CacheableRepository[T any] interface { - GetWithCache(ctx context.Context, key string, finder func() (*T, error)) (*T, error) - ListWithCache(ctx context.Context, key string, finder func() ([]*T, error)) ([]*T, error) - InvalidateCache(ctx context.Context, pattern string) error -} - -// 自动缓存的仓储实现 -type UserRepository struct { - db *gorm.DB - cache cache.Manager -} - -func (r *UserRepository) GetByID(ctx context.Context, id uint) (*User, error) { - cacheKey := fmt.Sprintf("user:id:%d", id) - return r.cache.GetWithCache(ctx, cacheKey, func() (*User, error) { - var user User - err := r.db.First(&user, id).Error - return &user, err - }) -} -``` - -### 缓存策略 - -- **自动查询缓存**: 数据库查询结果自动缓存 -- **智能缓存失效**: 基于数据变更的缓存失效 -- **多级缓存架构**: 内存 + Redis 组合 -- **缓存穿透防护**: 空结果缓存和布隆过滤器 -- **缓存预热**: 启动时预加载热点数据 - -## 🔄 跨域事务处理 - -### Saga 模式实现 - -```go -// Saga事务协调器 -type Saga struct { - ID string - Steps []SagaStep - Data interface{} - Status SagaStatus - executor SagaExecutor -} - -type SagaStep struct { - Name string - Action func(ctx context.Context, data interface{}) error - Compensate func(ctx context.Context, data interface{}) error -} -``` - -### 跨域查询处理 - -#### 视图聚合模式 - -```go -// OrderView 订单视图聚合 -type OrderView struct { - // 订单基本信息 (来自finance域) - OrderID uint `json:"order_id"` - Amount float64 `json:"amount"` - Status string `json:"status"` - CreatedAt time.Time `json:"created_at"` - - // 用户信息 (来自user域) - UserID uint `json:"user_id"` - UserName string `json:"user_name"` - UserEmail string `json:"user_email"` - - // 产品信息 (来自product域) - ProductID uint `json:"product_id"` - ProductName string `json:"product_name"` - ProductPrice float64 `json:"product_price"` -} -``` - -## 🔄 模块化特性 - -### 快速添加新模块 - -```bash -# 使用脚本快速生成新模块 -make add-domain name=inventory - -# 自动生成: -# - 完整的目录结构和代码模板 -# - 自动缓存支持的仓储层 -# - 标准化的API接口 -# - 依赖注入自动配置 -# - 路由自动注册 - -# 立即可用的API: -# GET /api/v1/inventorys -# POST /api/v1/inventorys -# GET /api/v1/inventorys/:id -# PUT /api/v1/inventorys/:id -# DELETE /api/v1/inventorys/:id -# GET /api/v1/inventorys/search -``` - -### 模块间解耦 - -- 通过接口定义模块边界 -- 使用事件驱动通信 -- 独立的数据模型 -- 可插拔的模块系统 - -## 🐳 Docker Compose 部署架构 - -### 生产环境配置 - -```yaml -# docker-compose.yml -version: "3.8" -services: - api: - build: . - ports: - - "8080:8080" - environment: - - ENV=production - - DB_HOST=postgres - - REDIS_HOST=redis - depends_on: - - postgres - - redis - - postgres: - image: postgres:15-alpine - environment: - POSTGRES_DB: tyapi - POSTGRES_USER: tyapi - POSTGRES_PASSWORD: ${DB_PASSWORD} - volumes: - - postgres_data:/var/lib/postgresql/data - ports: - - "5432:5432" - - redis: - image: redis:7-alpine - volumes: - - redis_data:/data - ports: - - "6379:6379" - -volumes: - postgres_data: - redis_data: -``` - -### 多阶段 Dockerfile - -```dockerfile -# Build stage -FROM golang:1.23.4-alpine AS builder -WORKDIR /app -COPY go.mod go.sum ./ -RUN go mod download -COPY . . -RUN CGO_ENABLED=0 GOOS=linux go build -o main cmd/api/main.go - -# Development stage -FROM golang:1.23.4-alpine AS development -WORKDIR /app -COPY go.mod go.sum ./ -RUN go mod download -CMD ["go", "run", "cmd/api/main.go"] - -# Production stage -FROM alpine:latest AS production -RUN apk --no-cache add ca-certificates -WORKDIR /root/ -COPY --from=builder /app/main . -CMD ["./main"] -``` - -## 🛡️ 安全和监控特性 - -### 轻量安全中间件栈 - -```go -// SecurityMiddleware 安全中间件集合 -type SecurityMiddleware struct { - jwtSecret string - rateLimiter *rate.Limiter - trustedIPs map[string]bool -} - -// Chain 安全中间件链 -func (s *SecurityMiddleware) Chain() []gin.HandlerFunc { - return []gin.HandlerFunc{ - s.RateLimit(), - s.SecurityHeaders(), - s.IPWhitelist(), - s.RequestID(), - } -} -``` - -### 简化的可观测性 - -```go -// SimpleMetrics 简化的指标收集器 -type SimpleMetrics struct { - counters map[string]int64 - gauges map[string]float64 - mu sync.RWMutex -} - -// BusinessMetrics 业务指标 -type BusinessMetrics struct { - metrics *SimpleMetrics -} - -func (b *BusinessMetrics) RecordOrderCreated(amount float64) { - b.metrics.IncCounter("orders.created.count", 1) - b.metrics.IncCounter("orders.created.amount", int64(amount*100)) -} -``` - -### 容错机制 - -```go -// SimpleCircuitBreaker 简化的熔断器 -type SimpleCircuitBreaker struct { - maxFailures int - resetTimeout time.Duration - state CircuitState -} - -// SimpleRetry 智能重试机制 -func SimpleRetry(ctx context.Context, config RetryConfig, fn func() error) error { - // 指数退避重试实现 -} -``` - -### 健康检查 - -```go -// HealthChecker 健康检查器 -type HealthChecker struct { - db *gorm.DB - redis *redis.Client -} - -type HealthStatus struct { - Status string `json:"status"` - Timestamp time.Time `json:"timestamp"` - Checks map[string]CheckResult `json:"checks"` -} -``` - -## 📊 性能优化策略 - -### 数据库优化 - -- 连接池动态调优 -- 查询结果自动缓存 -- 索引策略优化 -- 批量操作优化 - -### 缓存策略 - -- **自动查询缓存**: 透明的查询结果缓存 -- **智能失效**: 数据变更时自动清理相关缓存 -- **多级架构**: L1(内存) + L2(Redis) 缓存 -- **穿透防护**: 空值缓存和布隆过滤器 - -### 并发优化 - -- Goroutine 池管理 -- Context 超时控制 -- 异步任务处理 -- 批量数据加载 - -## 🔌 扩展性设计 - -### 钩子系统 - -```go -// SimpleHookSystem 简化的钩子系统 -type SimpleHookSystem struct { - hooks map[string][]HookFunc -} - -// 使用示例 -hookSystem.Register("user.created", func(ctx context.Context, data interface{}) error { - user := data.(*entities.User) - return sendWelcomeEmail(user.Email) -}) -``` - -### 事件驱动架构 - -```go -// 跨域事件通信 -type UserCreatedEvent struct { - UserID uint `json:"user_id"` - Email string `json:"email"` - CreatedAt time.Time `json:"created_at"` -} - -// 产品域监听用户创建事件 -func (s *ProductService) OnUserCreated(event UserCreatedEvent) error { - return s.CreateUserRecommendations(event.UserID) -} -``` - -## 🛠️ 开发工具 - -### Makefile 快速命令 - -```makefile -# 添加新的业务域 -add-domain: - @./scripts/generate-domain.sh $(name) - @./scripts/register-module.sh $(name) - -# 开发环境运行 -run-dev: - @go run cmd/api/main.go - -# Docker开发环境 -docker-dev: - @docker-compose -f docker-compose.dev.yml up -d - -# 数据库迁移 -migrate: - @go run cmd/migrate/main.go -``` - -### 域生成器脚本 - -```bash -#!/bin/bash -# scripts/generate-domain.sh - -# 自动生成: -# - 实体定义和表结构 -# - 仓储接口和实现(支持自动缓存) -# - 业务服务层 -# - HTTP处理器 -# - DTO和验证器 -# - 路由注册 -# - 模块定义和依赖注入 -``` - -## 🎯 预期收益 - -1. **开发效率**: 模块生成器将新功能开发时间减少 70% -2. **代码质量**: 统一的架构模式和代码规范 -3. **维护性**: 清晰的分层和依赖关系 -4. **可测试性**: 依赖注入支持 100%的单元测试覆盖 -5. **性能**: 自动缓存和并发优化策略 -6. **扩展性**: 支持平滑的微服务演进 -7. **稳定性**: 完整的容错和监控机制 -8. **安全性**: 轻量但完备的安全防护 - -## ✅ 架构特色总结 - -### 🎯 核心优势 - -1. **自动缓存系统**: - - - 数据库查询自动缓存,性能提升显著 - - 智能缓存失效策略 - - 多级缓存架构(内存 + Redis) - -2. **模块化设计**: - - - Clean Architecture + DDD 设计 - - 5 分钟快速生成完整业务域 - - 依赖注入容器(Uber FX) - -3. **跨域事务处理**: - - - Saga 模式处理分布式事务 - - 视图聚合解决跨域查询 - - 事件驱动架构 - -4. **Docker Compose 部署**: - - - 开发和生产环境分离 - - 多阶段构建优化 - - 数据持久化和网络隔离 - -5. **生产就绪特性**: - - 轻量安全中间件栈 - - 简化的可观测性 - - 容错和稳定性保障 - - 健康检查和监控 - -### 📋 技术决策 - -✅ **数据库**: PostgreSQL 15+ 主库 + Redis 7+ 缓存 -✅ **缓存策略**: 自动查询缓存 + 智能失效 -✅ **部署方式**: Docker Compose(生产/开发分离) -✅ **开发工具**: 移除热重载,保留核心工具 -✅ **认证方案**: 简化 JWT 认证,无复杂 RBAC -✅ **测试策略**: 手动编写,无自动生成 -✅ **文档方案**: 手动维护,无自动 API 文档 - -## 🚀 开始实施 - -架构设计已完成!总预计实施时间:**12-15 小时**,分 5 个阶段完成。 - -### 实施顺序: - -1. **基础架构搭建** (2-3 小时) -2. **架构核心层** (3-4 小时) -3. **Web 层实现** (2-3 小时) -4. **高级特性** (3-4 小时) -5. **部署和工具** (2-3 小时) - -### 关键特性: - -- 🚀 **5 分钟生成新业务域** - 完整的模块化开发 -- ⚡ **自动缓存系统** - 透明的性能优化 -- 🔄 **跨域事务处理** - 企业级数据一致性 -- 🐳 **容器化部署** - 生产就绪的部署方案 -- 🛡️ **安全和监控** - 轻量但完备的保障体系 - -**准备好开始实施了吗?** 🎯 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a447568 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,68 @@ +# 第一阶段:构建阶段 +FROM golang:1.23-alpine AS builder + +# 设置工作目录 +WORKDIR /app + +# 安装必要的包 +RUN apk add --no-cache git ca-certificates tzdata + +# 复制模块文件 +COPY go.mod go.sum ./ + +# 下载依赖 +RUN go mod download + +# 复制源代码 +COPY . . + +# 构建应用程序 +ARG VERSION=1.0.0 +ARG COMMIT=dev +ARG BUILD_TIME +RUN BUILD_TIME=${BUILD_TIME:-$(date -u +"%Y-%m-%dT%H:%M:%SZ")} && \ + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ + -ldflags "-X main.version=${VERSION} -X main.commit=${COMMIT} -X main.date=${BUILD_TIME} -w -s" \ + -a -installsuffix cgo \ + -o tyapi-server \ + cmd/api/main.go + +# 第二阶段:运行阶段 +FROM alpine:3.19 + +# 安装必要的包 +RUN apk --no-cache add ca-certificates tzdata curl && \ + update-ca-certificates + +# 设置时区 +ENV TZ=Asia/Shanghai + +# 创建非root用户 +RUN addgroup -g 1001 tyapi && \ + adduser -D -s /bin/sh -u 1001 -G tyapi tyapi + +# 设置工作目录 +WORKDIR /app + +# 从构建阶段复制二进制文件 +COPY --from=builder /app/tyapi-server . + +# 复制配置文件 +COPY --chown=tyapi:tyapi config.yaml . +COPY --chown=tyapi:tyapi configs/ ./configs/ + +# 创建日志目录 +RUN mkdir -p logs && chown -R tyapi:tyapi logs + +# 切换到非root用户 +USER tyapi + +# 暴露端口 +EXPOSE 8080 + +# 健康检查 +HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ + CMD curl -f http://localhost:8080/health || exit 1 + +# 启动命令 +CMD ["./tyapi-server", "-env=production"] \ No newline at end of file diff --git a/Makefile b/Makefile index f173bbb..983887e 100644 --- a/Makefile +++ b/Makefile @@ -3,9 +3,25 @@ # 应用信息 APP_NAME := tyapi-server VERSION := 1.0.0 -BUILD_TIME := $(shell date -u +"%Y-%m-%dT%H:%M:%SZ") -GIT_COMMIT := $(shell git rev-parse --short HEAD) -GO_VERSION := $(shell go version | awk '{print $$3}') + +# 检测操作系统 +ifeq ($(OS),Windows_NT) + # Windows 环境 + BUILD_TIME := $(shell powershell -Command "Get-Date -Format 'yyyy-MM-ddTHH:mm:ssZ'") + GIT_COMMIT := $(shell powershell -Command "try { git rev-parse --short HEAD } catch { 'dev' }") + GO_VERSION := $(shell go version) + MKDIR := mkdir + RM := del /f /q + RMDIR := rmdir /s /q +else + # Unix 环境 + BUILD_TIME := $(shell date -u +"%Y-%m-%dT%H:%M:%SZ") + GIT_COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo 'dev') + GO_VERSION := $(shell go version | awk '{print $$3}') + MKDIR := mkdir -p + RM := rm -f + RMDIR := rm -rf +endif # 构建参数 LDFLAGS := -ldflags "-X main.version=$(VERSION) -X main.commit=$(GIT_COMMIT) -X main.date=$(BUILD_TIME)" @@ -32,58 +48,132 @@ DOCKER_LATEST := $(APP_NAME):latest help: @echo "TYAPI Server Makefile" @echo "" - @echo "使用方法: make [目标]" + @echo "Usage: make [target]" @echo "" - @echo "可用目标:" - @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " %-20s %s\n", $$1, $$2}' $(MAKEFILE_LIST) + @echo "Development Basics:" + @echo " help Show this help message" + @echo " setup Setup development environment" + @echo " deps Install dependencies" + @echo " fmt Format code" + @echo " lint Lint code" + @echo " test Run tests" + @echo " coverage Generate test coverage report" + @echo "" + @echo "Build & Compile:" + @echo " build Build application" + @echo " build-prod Build production version" + @echo " build-all Cross compile for all platforms" + @echo " clean Clean build files" + @echo "" + @echo "Run & Manage:" + @echo " dev Run in development mode" + @echo " run Run compiled application" + @echo " migrate Run database migration" + @echo " version Show version info" + @echo " health Run health check" + @echo "" + @echo "Docker Containers:" + @echo " docker-build Build Docker image" + @echo " docker-build-prod Build production Docker image" + @echo " docker-push-prod Push image to registry" + @echo " docker-run Run Docker container" + @echo " docker-stop Stop Docker container" + @echo "" + @echo "Production Environment:" + @echo " deploy-prod Deploy to production" + @echo " prod-up Start production services" + @echo " prod-down Stop production services" + @echo " prod-logs View production logs" + @echo " prod-status Check production status" + @echo "" + @echo "Development Environment:" + @echo " services-up Start dev dependencies" + @echo " services-down Stop dev dependencies" + @echo " services-update Update dev dependencies (rebuild & restart)" + @echo " dev-up Alias for services-up" + @echo " dev-down Alias for services-down" + @echo " dev-update Alias for services-update" + @echo "" + @echo "Tools & Utilities:" + @echo " env Create .env file from template" + @echo " logs View application logs" + @echo " docs Generate API documentation" + @echo " bench Run performance benchmark" + @echo " race Run race condition detection" + @echo " security Run security scan" + @echo " mock Generate mock data" + @echo "" + @echo "CI/CD Pipeline:" + @echo " ci Run complete CI pipeline" + @echo " release Run complete release pipeline" -## 安装依赖 +## Install dependencies deps: - @echo "安装依赖..." + @echo "Installing dependencies..." $(GOMOD) download $(GOMOD) tidy -## 代码格式化 +## Format code fmt: - @echo "格式化代码..." + @echo "Formatting code..." $(GOFMT) ./... -## 代码检查 +## Lint code lint: - @echo "代码检查..." + @echo "Linting code..." +ifeq ($(OS),Windows_NT) + @where golangci-lint >nul 2>&1 && golangci-lint run || echo "golangci-lint not installed, skipping lint check" +else @if command -v golangci-lint >/dev/null 2>&1; then \ golangci-lint run; \ else \ - echo "golangci-lint 未安装,跳过代码检查"; \ + echo "golangci-lint not installed, skipping lint check"; \ fi +endif -## 运行测试 +## Run tests test: - @echo "运行测试..." + @echo "Running tests..." +ifeq ($(OS),Windows_NT) + $(GOTEST) -v -coverprofile=coverage.out ./... +else $(GOTEST) -v -race -coverprofile=coverage.out ./... +endif -## 生成测试覆盖率报告 +## Generate test coverage report coverage: test - @echo "生成覆盖率报告..." + @echo "Generating coverage report..." $(GOCMD) tool cover -html=coverage.out -o coverage.html - @echo "覆盖率报告已生成: coverage.html" + @echo "Coverage report generated: coverage.html" -## 构建应用 (开发环境) +## Build application (development) build: - @echo "构建应用..." + @echo "Building application..." +ifeq ($(OS),Windows_NT) + @if not exist "$(BUILD_DIR)" mkdir "$(BUILD_DIR)" +else @mkdir -p $(BUILD_DIR) +endif $(GOBUILD) $(LDFLAGS) -o $(BUILD_DIR)/$(APP_NAME) $(MAIN_PATH) -## 构建生产版本 +## Build production version build-prod: - @echo "构建生产版本..." + @echo "Building production version..." +ifeq ($(OS),Windows_NT) + @if not exist "$(BUILD_DIR)" mkdir "$(BUILD_DIR)" +else @mkdir -p $(BUILD_DIR) +endif CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GOBUILD) $(LDFLAGS) -a -installsuffix cgo -o $(BUILD_DIR)/$(APP_NAME)-linux-amd64 $(MAIN_PATH) -## 交叉编译 +## Cross compile build-all: - @echo "交叉编译..." + @echo "Cross compiling..." +ifeq ($(OS),Windows_NT) + @if not exist "$(BUILD_DIR)" mkdir "$(BUILD_DIR)" +else @mkdir -p $(BUILD_DIR) +endif # Linux AMD64 CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GOBUILD) $(LDFLAGS) -o $(BUILD_DIR)/$(APP_NAME)-linux-amd64 $(MAIN_PATH) # Linux ARM64 @@ -95,147 +185,323 @@ build-all: # Windows AMD64 CGO_ENABLED=0 GOOS=windows GOARCH=amd64 $(GOBUILD) $(LDFLAGS) -o $(BUILD_DIR)/$(APP_NAME)-windows-amd64.exe $(MAIN_PATH) -## 运行应用 +## Run application run: build - @echo "启动应用..." + @echo "Starting application..." ./$(BUILD_DIR)/$(APP_NAME) -## 开发模式运行 (热重载) +## Run in development mode dev: - @echo "开发模式启动..." - @if command -v air >/dev/null 2>&1; then \ - air; \ - else \ - echo "air 未安装,使用普通模式运行..."; \ - $(GOCMD) run $(MAIN_PATH); \ - fi + @echo "Starting development mode..." + $(GOCMD) run $(MAIN_PATH) -## 运行数据库迁移 +## Run database migration migrate: build - @echo "运行数据库迁移..." + @echo "Running database migration..." ./$(BUILD_DIR)/$(APP_NAME) -migrate -## 显示版本信息 +## Show version info version: build - @echo "版本信息:" + @echo "Version info:" ./$(BUILD_DIR)/$(APP_NAME) -version -## 健康检查 +## Health check health: build - @echo "执行健康检查..." + @echo "Running health check..." ./$(BUILD_DIR)/$(APP_NAME) -health -## 清理构建文件 +## Clean build files clean: - @echo "清理构建文件..." + @echo "Cleaning build files..." $(GOCLEAN) - rm -rf $(BUILD_DIR) - rm -f coverage.out coverage.html +ifeq ($(OS),Windows_NT) + @if exist "$(BUILD_DIR)" $(RMDIR) "$(BUILD_DIR)" 2>nul || echo "" + @if exist "coverage.out" $(RM) "coverage.out" 2>nul || echo "" + @if exist "coverage.html" $(RM) "coverage.html" 2>nul || echo "" +else + $(RMDIR) $(BUILD_DIR) 2>/dev/null || true + $(RM) coverage.out coverage.html 2>/dev/null || true +endif -## 创建 .env 文件 +## Create .env file env: +ifeq ($(OS),Windows_NT) + @if not exist ".env" ( \ + echo Creating .env file from production template... && \ + copy .env.production .env && \ + echo .env file created, please modify configuration as needed \ + ) else ( \ + echo .env file already exists \ + ) +else @if [ ! -f .env ]; then \ - echo "创建 .env 文件..."; \ - cp env.example .env; \ - echo ".env 文件已创建,请根据需要修改配置"; \ + echo "Creating .env file from production template..."; \ + cp .env.production .env; \ + echo ".env file created, please modify configuration as needed"; \ else \ - echo ".env 文件已存在"; \ + echo ".env file already exists"; \ fi +endif -## 设置开发环境 +## Setup development environment setup: deps env - @echo "设置开发环境..." - @echo "1. 依赖已安装" - @echo "2. .env 文件已创建" - @echo "3. 请确保 PostgreSQL 和 Redis 正在运行" - @echo "4. 运行 'make migrate' 创建数据库表" - @echo "5. 运行 'make dev' 启动开发服务器" + @echo "Setting up development environment..." + @echo "1. [OK] Dependencies installed" + @echo "2. [OK] .env file created from production template" + @echo "3. [TODO] Please edit .env file and set your configuration" + @echo "4. [NEXT] Run 'make services-up' to start PostgreSQL + Redis" + @echo "5. [NEXT] Run 'make migrate' to create database tables" + @echo "6. [NEXT] Run 'make dev' to start development server" + @echo "" + @echo "Tip: Use 'make help' to see all available commands" -## 构建 Docker 镜像 +## Build Docker image docker-build: - @echo "构建 Docker 镜像..." + @echo "Building Docker image..." docker build -t $(DOCKER_IMAGE) -t $(DOCKER_LATEST) . -## 运行 Docker 容器 +## Build production Docker image with registry +docker-build-prod: + @echo "Building production Docker image..." + docker build \ + --build-arg VERSION=$(VERSION) \ + --build-arg COMMIT=$(GIT_COMMIT) \ + --build-arg BUILD_TIME=$(BUILD_TIME) \ + -t docker-registry.tianyuanapi.com/tyapi-server:$(VERSION) \ + -t docker-registry.tianyuanapi.com/tyapi-server:latest \ + . + +## Push Docker image to registry +docker-push-prod: + @echo "Pushing Docker image to production registry..." + docker push docker-registry.tianyuanapi.com/tyapi-server:$(VERSION) + docker push docker-registry.tianyuanapi.com/tyapi-server:latest + +## Deploy to production +deploy-prod: + @echo "Deploying to production environment..." +ifeq ($(OS),Windows_NT) + @if exist "scripts\\deploy.sh" ( \ + bash scripts/deploy.sh $(VERSION) \ + ) else ( \ + echo "Deploy script not found" \ + ) +else + @if [ -f scripts/deploy.sh ]; then \ + ./scripts/deploy.sh $(VERSION); \ + else \ + echo "Deploy script not found"; \ + fi +endif + +## Start production services +prod-up: + @echo "Starting production services..." +ifeq ($(OS),Windows_NT) + @if exist "docker-compose.prod.yml" ( \ + docker-compose -f docker-compose.prod.yml up -d \ + ) else ( \ + echo docker-compose.prod.yml not found \ + ) +else + @if [ -f docker-compose.prod.yml ]; then \ + docker-compose -f docker-compose.prod.yml up -d; \ + else \ + echo "docker-compose.prod.yml not found"; \ + fi +endif + +## Stop production services +prod-down: + @echo "Stopping production services..." +ifeq ($(OS),Windows_NT) + @if exist "docker-compose.prod.yml" ( \ + docker-compose -f docker-compose.prod.yml down \ + ) else ( \ + echo docker-compose.prod.yml not found \ + ) +else + @if [ -f docker-compose.prod.yml ]; then \ + docker-compose -f docker-compose.prod.yml down; \ + else \ + echo "docker-compose.prod.yml not found"; \ + fi +endif + +## View production logs +prod-logs: + @echo "Viewing production logs..." +ifeq ($(OS),Windows_NT) + @if exist "docker-compose.prod.yml" ( \ + docker-compose -f docker-compose.prod.yml logs -f \ + ) else ( \ + echo docker-compose.prod.yml not found \ + ) +else + @if [ -f docker-compose.prod.yml ]; then \ + docker-compose -f docker-compose.prod.yml logs -f; \ + else \ + echo "docker-compose.prod.yml not found"; \ + fi +endif + +## Check production status +prod-status: + @echo "Checking production status..." +ifeq ($(OS),Windows_NT) + @if exist "docker-compose.prod.yml" ( \ + docker-compose -f docker-compose.prod.yml ps \ + ) else ( \ + echo docker-compose.prod.yml not found \ + ) +else + @if [ -f docker-compose.prod.yml ]; then \ + docker-compose -f docker-compose.prod.yml ps; \ + else \ + echo "docker-compose.prod.yml not found"; \ + fi +endif + +## Run Docker container docker-run: - @echo "运行 Docker 容器..." + @echo "Running Docker container..." docker run -d --name $(APP_NAME) -p 8080:8080 --env-file .env $(DOCKER_LATEST) -## 停止 Docker 容器 +## Stop Docker container docker-stop: - @echo "停止 Docker 容器..." + @echo "Stopping Docker container..." docker stop $(APP_NAME) || true docker rm $(APP_NAME) || true -## 推送 Docker 镜像 -docker-push: - @echo "推送 Docker 镜像..." - docker push $(DOCKER_IMAGE) - docker push $(DOCKER_LATEST) - -## 启动开发依赖服务 (Docker Compose) +## Start development dependencies (Docker Compose) services-up: - @echo "启动开发依赖服务..." + @echo "Starting development dependencies..." +ifeq ($(OS),Windows_NT) + @if exist "docker-compose.dev.yml" ( \ + docker-compose -f docker-compose.dev.yml up -d \ + ) else ( \ + echo docker-compose.dev.yml not found \ + ) +else @if [ -f docker-compose.dev.yml ]; then \ docker-compose -f docker-compose.dev.yml up -d; \ else \ - echo "docker-compose.dev.yml 不存在"; \ + echo "docker-compose.dev.yml not found"; \ fi +endif -## 停止开发依赖服务 +## Stop development dependencies services-down: - @echo "停止开发依赖服务..." + @echo "Stopping development dependencies..." +ifeq ($(OS),Windows_NT) + @if exist "docker-compose.dev.yml" ( \ + docker-compose -f docker-compose.dev.yml down \ + ) else ( \ + echo docker-compose.dev.yml not found \ + ) +else @if [ -f docker-compose.dev.yml ]; then \ docker-compose -f docker-compose.dev.yml down; \ else \ - echo "docker-compose.dev.yml 不存在"; \ + echo "docker-compose.dev.yml not found"; \ fi +endif -## 查看服务日志 +## Alias for dev-up (start development dependencies) +dev-up: services-up + +## Alias for dev-down (stop development dependencies) +dev-down: services-down + +## Update development dependencies (rebuild and restart) +services-update: + @echo "Updating development dependencies..." +ifeq ($(OS),Windows_NT) + @if exist "docker-compose.dev.yml" ( \ + docker-compose -f docker-compose.dev.yml down && \ + docker-compose -f docker-compose.dev.yml pull && \ + docker-compose -f docker-compose.dev.yml up -d --build \ + ) else ( \ + echo docker-compose.dev.yml not found \ + ) +else + @if [ -f docker-compose.dev.yml ]; then \ + docker-compose -f docker-compose.dev.yml down && \ + docker-compose -f docker-compose.dev.yml pull && \ + docker-compose -f docker-compose.dev.yml up -d --build; \ + else \ + echo "docker-compose.dev.yml not found"; \ + fi +endif + +## Alias for services-update +dev-update: services-update + +## View application logs logs: - @echo "查看应用日志..." + @echo "Viewing application logs..." +ifeq ($(OS),Windows_NT) + @if exist "logs\\app.log" ( \ + powershell -Command "Get-Content logs\\app.log -Wait" \ + ) else ( \ + echo "Log file does not exist" \ + ) +else @if [ -f logs/app.log ]; then \ tail -f logs/app.log; \ else \ - echo "日志文件不存在"; \ + echo "Log file does not exist"; \ fi +endif -## 生成 API 文档 +## Generate API documentation docs: - @echo "生成 API 文档..." + @echo "Generating API documentation..." +ifeq ($(OS),Windows_NT) + @where swag >nul 2>&1 && swag init -g $(MAIN_PATH) -o docs/swagger || echo "swag not installed, skipping documentation generation" +else @if command -v swag >/dev/null 2>&1; then \ swag init -g $(MAIN_PATH) -o docs/swagger; \ else \ - echo "swag 未安装,跳过文档生成"; \ + echo "swag not installed, skipping documentation generation"; \ fi +endif -## 性能测试 +## Performance benchmark bench: - @echo "运行性能测试..." + @echo "Running performance benchmark..." $(GOTEST) -bench=. -benchmem ./... -## 内存泄漏检测 +## Race condition detection race: - @echo "运行竞态条件检测..." + @echo "Running race condition detection..." $(GOTEST) -race ./... -## 安全扫描 +## Security scan security: - @echo "运行安全扫描..." + @echo "Running security scan..." +ifeq ($(OS),Windows_NT) + @where gosec >nul 2>&1 && gosec ./... || echo "gosec not installed, skipping security scan" +else @if command -v gosec >/dev/null 2>&1; then \ gosec ./...; \ else \ - echo "gosec 未安装,跳过安全扫描"; \ + echo "gosec not installed, skipping security scan"; \ fi +endif -## 生成模拟数据 +## Generate mock data mock: - @echo "生成模拟数据..." + @echo "Generating mock data..." +ifeq ($(OS),Windows_NT) + @where mockgen >nul 2>&1 && echo "Generating mock data..." || echo "mockgen not installed, please install: go install github.com/golang/mock/mockgen@latest" +else @if command -v mockgen >/dev/null 2>&1; then \ - echo "生成模拟数据..."; \ + echo "Generating mock data..."; \ else \ - echo "mockgen 未安装,请先安装: go install github.com/golang/mock/mockgen@latest"; \ + echo "mockgen not installed, please install: go install github.com/golang/mock/mockgen@latest"; \ fi +endif ## 完整的 CI 流程 ci: deps fmt lint test build @@ -243,4 +509,4 @@ ci: deps fmt lint test build ## 完整的发布流程 release: ci build-all docker-build -.PHONY: help deps fmt lint test coverage build build-prod build-all run dev migrate version health clean env setup docker-build docker-run docker-stop docker-push services-up services-down logs docs bench race security mock ci release \ No newline at end of file +.PHONY: help deps fmt lint test coverage build build-prod build-all run dev migrate version health clean env setup docker-build docker-run docker-stop docker-push docker-build-prod docker-push-prod deploy-prod prod-up prod-down prod-logs prod-status services-up services-down services-update dev-up dev-down dev-update logs docs bench race security mock ci release \ No newline at end of file diff --git a/README.md b/README.md index b679965..001be96 100644 --- a/README.md +++ b/README.md @@ -1,334 +1,107 @@ -# TYAPI Server +# TYAPI 服务端配置系统 -## 🚀 2025 年最前沿的 Go Web 架构系统 +## 配置系统设计 -TYAPI Server 是一个基于 Go 语言和 Gin 框架构建的现代化、高性能、模块化的 Web API 服务器。采用领域驱动设计(DDD)、CQRS、事件驱动架构等先进设计模式,为企业级应用提供坚实的技术基础。 +TYAPI 服务端采用分层配置系统,使配置管理更加灵活和清晰: -## ✨ 核心特性 +1. **基础配置文件** (`config.yaml`): 包含所有配置项和默认值 +2. **环境特定配置文件** (`configs/env.<环境>.yaml`): 包含特定环境需要覆盖的配置项 +3. **环境变量**: 可以覆盖任何配置项,优先级最高 -### 🏗️ 架构特性 +## 配置文件格式 -- **领域驱动设计(DDD)**: 清晰的业务边界和模型隔离 -- **CQRS 模式**: 命令查询责任分离,优化读写性能 -- **事件驱动**: 基于事件的异步处理和系统解耦 -- **依赖注入**: 基于 Uber FX 的完整 IoC 容器 -- **模块化设计**: 高内聚、低耦合的组件架构 +所有配置文件采用 YAML 格式,保持相同的结构层次。 -### 🔧 技术栈 +### 基础配置文件 (config.yaml) -- **Web 框架**: Gin (高性能 HTTP 路由) -- **ORM**: GORM (功能强大的对象关系映射) -- **数据库**: PostgreSQL (主数据库) + Redis (缓存) -- **日志**: Zap (结构化高性能日志) -- **配置**: Viper (多格式配置管理) -- **监控**: Prometheus + Grafana + Jaeger -- **依赖注入**: Uber FX +包含所有配置项和默认值,作为配置的基础。 -### 🛡️ 生产就绪特性 +### 环境配置文件 -- **安全性**: JWT 认证、CORS、安全头部、输入验证 -- **性能**: 智能缓存、连接池、限流、压缩 -- **可观测性**: 链路追踪、指标监控、结构化日志 -- **容错性**: 熔断器、重试机制、优雅降级 -- **运维**: 健康检查、优雅关闭、Docker 化部署 +环境配置文件只需包含需要覆盖的配置项,保持与基础配置相同的层次结构: -## 📁 项目结构 +- `configs/env.development.yaml`: 开发环境配置 +- `configs/env.testing.yaml`: 测试环境配置 +- `configs/env.production.yaml`: 生产环境配置 -``` -tyapi-server-gin/ -├── cmd/ # 应用程序入口 -│ └── api/ -│ └── main.go # 主程序入口 -├── internal/ # 内部代码 -│ ├── app/ # 应用启动器 -│ ├── config/ # 配置管理 -│ ├── container/ # 依赖注入容器 -│ ├── domains/ # 业务域 -│ │ └── user/ # 用户域 -│ │ ├── dto/ # 数据传输对象 -│ │ ├── entities/ # 实体 -│ │ ├── events/ # 域事件 -│ │ ├── handlers/ # HTTP处理器 -│ │ ├── repositories/ # 仓储层 -│ │ ├── routes/ # 路由定义 -│ │ └── services/ # 业务服务 -│ └── shared/ # 共享组件 -│ ├── cache/ # 缓存服务 -│ ├── database/ # 数据库连接 -│ ├── events/ # 事件总线 -│ ├── health/ # 健康检查 -│ ├── http/ # HTTP组件 -│ ├── interfaces/ # 接口定义 -│ ├── logger/ # 日志服务 -│ └── middleware/ # 中间件 -├── deployments/ # 部署相关 -├── docs/ # 文档 -├── scripts/ # 脚本文件 -├── test/ # 测试文件 -├── config.yaml # 配置文件 -├── docker-compose.dev.yml # 开发环境 -├── Makefile # 构建脚本 -└── README.md # 项目说明 -``` +## 配置加载顺序 -## 🚀 快速开始 +系统按以下顺序加载配置,后加载的会覆盖先加载的: -### 环境要求 +1. 首先加载基础配置文件 `config.yaml` +2. 然后加载环境特定配置文件 `configs/env.<环境>.yaml` +3. 最后应用环境变量覆盖 -- Go 1.23.4+ -- PostgreSQL 12+ -- Redis 6+ -- Docker & Docker Compose (可选) +## 环境确定方式 -### 1. 克隆项目 +系统按以下优先级确定当前环境: -```bash -git clone -cd tyapi-server-gin -``` +1. `CONFIG_ENV` 环境变量 +2. `ENV` 环境变量 +3. `APP_ENV` 环境变量 +4. 默认值 `development` -### 2. 安装依赖 +## 统一配置项 -```bash -make deps -``` +某些配置项在所有环境中保持一致,直接在基础配置文件中设置: -### 3. 配置环境 +1. **短信配置**: 所有环境使用相同的短信服务配置 +2. **基础服务地址**: 如第三方服务端点等 -```bash -# 创建环境变量文件 -make env +## 使用示例 -# 编辑 .env 文件,配置数据库连接等信息 -vim .env -``` - -### 4. 启动依赖服务(Docker) - -```bash -# 启动 PostgreSQL, Redis 等服务 -make services-up -``` - -### 5. 数据库迁移 - -```bash -# 运行数据库迁移 -make migrate -``` - -### 6. 启动应用 - -```bash -# 开发模式 -make dev - -# 或构建后运行 -make build -make run -``` - -## 🛠️ 开发指南 - -### Make 命令 - -```bash -# 开发相关 -make setup # 设置开发环境 -make dev # 开发模式运行 -make build # 构建应用 -make test # 运行测试 -make lint # 代码检查 - -# 数据库相关 -make migrate # 运行迁移 -make services-up # 启动依赖服务 -make services-down # 停止依赖服务 - -# Docker相关 -make docker-build # 构建Docker镜像 -make docker-run # 运行Docker容器 - -# 其他 -make clean # 清理构建文件 -make help # 显示帮助信息 -``` - -### API 端点 - -#### 认证相关 - -- `POST /api/v1/auth/login` - 用户登录 -- `POST /api/v1/auth/register` - 用户注册 - -#### 用户管理 - -- `GET /api/v1/users` - 获取用户列表 -- `GET /api/v1/users/:id` - 获取用户详情 -- `POST /api/v1/users` - 创建用户 -- `PUT /api/v1/users/:id` - 更新用户 -- `DELETE /api/v1/users/:id` - 删除用户 - -#### 个人资料 - -- `GET /api/v1/profile` - 获取个人资料 -- `PUT /api/v1/profile` - 更新个人资料 -- `POST /api/v1/profile/change-password` - 修改密码 - -#### 系统 - -- `GET /health` - 健康检查 -- `GET /info` - 系统信息 - -### 配置说明 - -主要配置项说明: +### 基础配置 (config.yaml) ```yaml -# 应用配置 app: name: "TYAPI Server" version: "1.0.0" env: "development" -# 服务器配置 -server: - host: "0.0.0.0" - port: "8080" - -# 数据库配置 database: host: "localhost" port: "5432" user: "postgres" - password: "password" - name: "tyapi_dev" + password: "default_password" -# JWT配置 -jwt: - secret: "your-secret-key" - expires_in: 24h +# 统一的短信配置 +sms: + access_key_id: "LTAI5tKGB3TVJbMHSoZN3yr9" + access_key_secret: "OCQ30GWp4yENMjmfOAaagksE18bp65" + endpoint_url: "dysmsapi.aliyuncs.com" ``` -## 🏗️ 架构说明 +### 环境配置 (configs/env.production.yaml) -### 领域驱动设计 +```yaml +app: + env: "production" -项目采用 DDD 架构模式,每个业务域包含: +database: + host: "prod-db.example.com" + password: "prod_secure_password" +``` -- **Entities**: 业务实体,包含业务逻辑 -- **DTOs**: 数据传输对象,用于 API 交互 -- **Services**: 业务服务,协调实体完成业务操作 -- **Repositories**: 仓储模式,抽象数据访问 -- **Events**: 域事件,实现模块间解耦 - -### 事件驱动架构 - -- **事件总线**: 异步事件分发机制 -- **事件处理器**: 响应特定事件的处理逻辑 -- **事件存储**: 事件溯源和审计日志 - -### 中间件系统 - -- **认证中间件**: JWT token 验证 -- **限流中间件**: API 调用频率控制 -- **日志中间件**: 请求/响应日志记录 -- **CORS 中间件**: 跨域请求处理 -- **安全中间件**: 安全头部设置 - -## 📊 监控和运维 - -### 健康检查 +### 运行时 ```bash -# 应用健康检查 -curl http://localhost:8080/health +# 使用开发环境配置 +go run cmd/api/main.go -# 系统信息 -curl http://localhost:8080/info +# 使用生产环境配置 +ENV=production go run cmd/api/main.go + +# 使用环境变量覆盖特定配置 +ENV=production DB_PASSWORD=custom_password go run cmd/api/main.go ``` -### 指标监控 +## 敏感信息处理 -- **Prometheus**: `http://localhost:9090` -- **Grafana**: `http://localhost:3000` (admin/admin) -- **Jaeger**: `http://localhost:16686` +对于敏感信息(如密码、密钥等): -### 日志管理 +1. 开发环境:可以放在环境配置文件中 +2. 生产环境:应通过环境变量注入,不应出现在配置文件中 -结构化 JSON 日志,支持不同级别: +## 配置验证 -```bash -# 查看实时日志 -make logs - -# 或直接查看文件 -tail -f logs/app.log -``` - -## 🧪 测试 - -### 运行测试 - -```bash -# 所有测试 -make test - -# 生成覆盖率报告 -make coverage - -# 性能测试 -make bench - -# 竞态条件检测 -make race -``` - -### 测试结构 - -- **单元测试**: 业务逻辑测试 -- **集成测试**: 数据库集成测试 -- **API 测试**: HTTP 接口测试 - -## 🚢 部署 - -### Docker 部署 - -```bash -# 构建镜像 -make docker-build - -# 运行容器 -make docker-run -``` - -### 生产环境 - -1. 配置生产环境变量 -2. 使用 `config.prod.yaml` -3. 设置适当的资源限制 -4. 配置负载均衡和反向代理 - -## 🤝 贡献指南 - -1. Fork 项目 -2. 创建特性分支 (`git checkout -b feature/AmazingFeature`) -3. 提交更改 (`git commit -m 'Add some AmazingFeature'`) -4. 推送到分支 (`git push origin feature/AmazingFeature`) -5. 创建 Pull Request - -## 📝 许可证 - -本项目采用 MIT 许可证。详情请参阅 [LICENSE](LICENSE) 文件。 - -## 🆘 支持 - -如有问题或建议,请: - -1. 查看 [文档](docs/) -2. 创建 [Issue](issues) -3. 参与 [讨论](discussions) - ---- - -**TYAPI Server** - 构建下一代 Web 应用的理想选择 🚀 +系统在启动时会验证必要的配置项,确保应用能够正常运行。如果缺少关键配置,系统将拒绝启动并提供明确的错误信息。 diff --git a/api.md b/api.md new file mode 100644 index 0000000..69954e3 --- /dev/null +++ b/api.md @@ -0,0 +1,2953 @@ +# 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 请求频率限制 +- 📱 **短信限流**: 验证码发送频率限制 +- 🔐 **登录限流**: 防止暴力破解 +- 📧 **邮件限流**: 邮件发送频率限制 +- �� **搜索限流**: 防止恶意搜索 + +### 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 " + +# 响应示例 +{ + "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 " \ + -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 开发的一致性、可维护性和扩展性。 + +# 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 已经具备了企业级产品的所有核心监控和弹性能力!🚀 diff --git a/cmd/api/main.go b/cmd/api/main.go index ef9acbb..caaab36 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -6,30 +6,72 @@ import ( "log" "os" + _ "tyapi-server/docs" // docs is generated by Swag CLI, you have to import it. "tyapi-server/internal/app" ) +// @title TYAPI Server API +// @version 1.0 +// @description 基于DDD和Clean Architecture的企业级后端API服务 +// @description 采用Gin框架构建,支持用户管理、JWT认证、事件驱动等功能 + +// @contact.name API Support +// @contact.url https://github.com/your-org/tyapi-server-gin +// @contact.email support@example.com + +// @license.name Apache 2.0 +// @license.url http://www.apache.org/licenses/LICENSE-2.0.html + +// @host localhost:8080 +// @BasePath /api/v1 + +// @securityDefinitions.apikey Bearer +// @in header +// @name Authorization +// @description Type "Bearer" followed by a space and JWT token. + +// 构建时注入的变量 var ( - // 版本信息 - version = "1.0.0" - commit = "unknown" + version = "dev" + commit = "none" date = "unknown" ) func main() { - // 解析命令行参数 + // 命令行参数 var ( showVersion = flag.Bool("version", false, "显示版本信息") - runMigrate = flag.Bool("migrate", false, "运行数据库迁移") - healthCheck = flag.Bool("health", false, "执行健康检查") - command = flag.String("cmd", "", "执行特定命令 (version|migrate|health)") + migrate = flag.Bool("migrate", false, "运行数据库迁移") + health = flag.Bool("health", false, "执行健康检查") + env = flag.String("env", "", "指定运行环境 (development|production|testing)") ) flag.Parse() - // 显示版本信息 + // 处理版本信息显示 (不需要初始化完整应用) if *showVersion { - printVersion() - return + fmt.Printf("TYAPI Server\n") + fmt.Printf("Version: %s\n", version) + fmt.Printf("Commit: %s\n", commit) + fmt.Printf("Build Date: %s\n", date) + os.Exit(0) + } + + // 设置环境变量(如果通过命令行指定) + if *env != "" { + if err := validateEnvironment(*env); err != nil { + log.Fatalf("无效的环境参数: %v", err) + } + os.Setenv("ENV", *env) + fmt.Printf("🌍 通过命令行设置环境: %s\n", *env) + } + + // 显示当前环境 + currentEnv := getCurrentEnvironment() + fmt.Printf("🔧 当前运行环境: %s\n", currentEnv) + + // 生产环境安全提示 + if currentEnv == "production" { + fmt.Printf("⚠️ 生产环境模式 - 请确保配置正确\n") } // 创建应用程序实例 @@ -38,73 +80,53 @@ func main() { log.Fatalf("Failed to create application: %v", err) } - // 处理命令行命令 - if *command != "" { - if err := application.RunCommand(*command); err != nil { - log.Fatalf("Command '%s' failed: %v", *command, err) - } - return - } - - // 运行数据库迁移 - if *runMigrate { - if err := application.RunMigrations(); err != nil { + // 处理命令行参数 + if *migrate { + fmt.Println("Running database migrations...") + if err := application.RunCommand("migrate"); err != nil { log.Fatalf("Migration failed: %v", err) } - fmt.Println("Migration completed successfully") - return + fmt.Println("Database migrations completed successfully") + os.Exit(0) } - // 执行健康检查 - if *healthCheck { - if err := application.HealthCheck(); err != nil { + if *health { + fmt.Println("Performing health check...") + if err := application.RunCommand("health"); err != nil { log.Fatalf("Health check failed: %v", err) } fmt.Println("Health check passed") - return + os.Exit(0) } - // 默认:启动应用程序服务器 - logger := application.GetLogger() - logger.Info("Starting TYAPI Server...") - + // 启动应用程序 (使用完整的架构) + fmt.Printf("🚀 Starting TYAPI Server v%s (%s)\n", version, commit) if err := application.Run(); err != nil { log.Fatalf("Application failed to start: %v", err) } } -// printVersion 打印版本信息 -func printVersion() { - fmt.Printf("TYAPI Server\n") - fmt.Printf("Version: %s\n", version) - fmt.Printf("Commit: %s\n", commit) - fmt.Printf("Built: %s\n", date) - fmt.Printf("Go Version: %s\n", getGoVersion()) -} - -// getGoVersion 获取Go版本 -func getGoVersion() string { - return fmt.Sprintf("%s %s/%s", - os.Getenv("GO_VERSION"), - os.Getenv("GOOS"), - os.Getenv("GOARCH")) -} - -// 信号处理相关的辅助函数 - -// handleSignals 处理系统信号(这个函数在app包中已经实现,这里只是示例) -func handleSignals() { - // 信号处理逻辑已经在 app.Application 中实现 - // 这里保留作为参考 -} - -// init 初始化函数 -func init() { - // 设置日志格式 - log.SetFlags(log.LstdFlags | log.Lshortfile) - - // 环境变量检查 - if os.Getenv("APP_ENV") == "" { - os.Setenv("APP_ENV", "development") +// validateEnvironment 验证环境参数 +func validateEnvironment(env string) error { + validEnvs := []string{"development", "production", "testing"} + for _, validEnv := range validEnvs { + if env == validEnv { + return nil + } } + return fmt.Errorf("环境必须是以下之一: %v", validEnvs) +} + +// getCurrentEnvironment 获取当前环境(与config包中的逻辑保持一致) +func getCurrentEnvironment() string { + if env := os.Getenv("CONFIG_ENV"); env != "" { + return env + } + if env := os.Getenv("ENV"); env != "" { + return env + } + if env := os.Getenv("APP_ENV"); env != "" { + return env + } + return "development" } diff --git a/config.prod.yaml b/config.prod.yaml deleted file mode 100644 index bdf2572..0000000 --- a/config.prod.yaml +++ /dev/null @@ -1,91 +0,0 @@ -# TYAPI Server Production Configuration - -app: - name: "TYAPI Server" - version: "1.0.0" - env: "production" - -server: - host: "0.0.0.0" - port: "8080" - mode: "release" - read_timeout: 60s - write_timeout: 60s - idle_timeout: 300s - -database: - host: "${DB_HOST}" - port: "${DB_PORT}" - user: "${DB_USER}" - password: "Pg9mX4kL8nW2rT5y" - name: "${DB_NAME}" - sslmode: "require" - timezone: "UTC" - max_open_conns: 50 - max_idle_conns: 25 - conn_max_lifetime: 600s - -redis: - host: "${REDIS_HOST}" - port: "${REDIS_PORT}" - password: "${REDIS_PASSWORD}" - db: 0 - pool_size: 20 - min_idle_conns: 5 - max_retries: 3 - dial_timeout: 10s - read_timeout: 5s - write_timeout: 5s - -cache: - default_ttl: 7200s - cleanup_interval: 300s - max_size: 10000 - -logger: - level: "warn" - format: "json" - output: "stdout" - file_path: "/var/log/tyapi/app.log" - max_size: 500 - max_backups: 10 - max_age: 30 - compress: true - -jwt: - secret: "JwT8xR4mN9vP2sL7kH3oB6yC1zA5uF0qE9tW" - expires_in: 6h - refresh_expires_in: 72h # 3 days - -ratelimit: - requests: 1000 - window: 60s - burst: 200 - -monitoring: - metrics_enabled: true - metrics_port: "9090" - tracing_enabled: true - tracing_endpoint: "${JAEGER_ENDPOINT}" - sample_rate: 0.01 - -health: - enabled: true - interval: 60s - timeout: 30s - -resilience: - circuit_breaker_enabled: true - circuit_breaker_threshold: 10 - circuit_breaker_timeout: 300s - retry_max_attempts: 5 - retry_initial_delay: 200ms - retry_max_delay: 30s - -development: - debug: false - enable_profiler: false - enable_cors: true - cors_allowed_origins: "${CORS_ALLOWED_ORIGINS}" - cors_allowed_methods: "GET,POST,PUT,PATCH,DELETE,OPTIONS" - cors_allowed_headers: "Origin,Content-Type,Accept,Authorization,X-Requested-With" diff --git a/config.yaml b/config.yaml index 6a2cece..c033f94 100644 --- a/config.yaml +++ b/config.yaml @@ -1,4 +1,5 @@ # TYAPI Server Configuration +# 🎯 统一配置文件,包含所有默认配置值 app: name: "TYAPI Server" @@ -24,6 +25,7 @@ database: max_open_conns: 25 max_idle_conns: 10 conn_max_lifetime: 300s + auto_migrate: true redis: host: "localhost" @@ -44,29 +46,44 @@ cache: logger: level: "info" - format: "json" + format: "console" output: "stdout" file_path: "logs/app.log" max_size: 100 max_backups: 3 max_age: 7 compress: true + use_color: true jwt: - secret: "JwT8xR4mN9vP2sL7kH3oB6yC1zA5uF0qE9tW" + secret: "default-jwt-secret-key-change-in-env-config" expires_in: 24h - refresh_expires_in: 168h # 7 days + refresh_expires_in: 168h + +sms: + access_key_id: "LTAI5tKGB3TVJbMHSoZN3yr9" + access_key_secret: "OCQ30GWp4yENMjmfOAaagksE18bp65" + endpoint_url: "dysmsapi.aliyuncs.com" + sign_name: "天远数据" + template_code: "SMS_474525324" + code_length: 6 + expire_time: 5m + mock_enabled: false + rate_limit: + daily_limit: 10 + hourly_limit: 5 + min_interval: 60s ratelimit: - requests: 100 + requests: 5000 window: 60s - burst: 50 + burst: 100 monitoring: metrics_enabled: true metrics_port: "9090" - tracing_enabled: false - tracing_endpoint: "http://localhost:14268/api/traces" + tracing_enabled: true + tracing_endpoint: "http://localhost:4317" sample_rate: 0.1 health: diff --git a/configs/env.development.yaml b/configs/env.development.yaml new file mode 100644 index 0000000..b4b3e88 --- /dev/null +++ b/configs/env.development.yaml @@ -0,0 +1,20 @@ +# 🔧 开发环境配置 +# 只包含与默认配置不同的配置项 + +# =========================================== +# 🌍 环境标识 +# =========================================== +app: + env: development + +# =========================================== +# 🗄️ 数据库配置 +# =========================================== +database: + password: Pg9mX4kL8nW2rT5y + +# =========================================== +# 🔐 JWT配置 +# =========================================== +jwt: + secret: JwT8xR4mN9vP2sL7kH3oB6yC1zA5uF0qE9tW diff --git a/configs/env.production.yaml b/configs/env.production.yaml new file mode 100644 index 0000000..c5db7a8 --- /dev/null +++ b/configs/env.production.yaml @@ -0,0 +1,32 @@ +# 🏭 生产环境配置 +# 只包含与默认配置不同的配置项 + +# =========================================== +# 🌍 环境标识 +# =========================================== +app: + env: production + +# =========================================== +# 🌐 服务器配置 +# =========================================== +server: + mode: release + +# =========================================== +# 🗄️ 数据库配置 +# =========================================== +# 敏感信息通过外部环境变量注入 +database: + sslmode: require + +# =========================================== +# 📝 日志配置 +# =========================================== +logger: + level: warn + format: json +# =========================================== +# 🔐 JWT配置 +# =========================================== +# JWT_SECRET 必须通过外部环境变量注入 diff --git a/configs/env.testing.yaml b/configs/env.testing.yaml new file mode 100644 index 0000000..c785228 --- /dev/null +++ b/configs/env.testing.yaml @@ -0,0 +1,39 @@ +# 🧪 测试环境配置 +# 只包含与默认配置不同的配置项 + +# =========================================== +# 🌍 环境标识 +# =========================================== +app: + env: testing + +# =========================================== +# 🌐 服务器配置 +# =========================================== +server: + mode: test + +# =========================================== +# 🗄️ 数据库配置 +# =========================================== +database: + password: test_password + name: tyapi_test + +# =========================================== +# 📦 Redis配置 +# =========================================== +redis: + db: 15 + +# =========================================== +# 📝 日志配置 +# =========================================== +logger: + level: debug + +# =========================================== +# 🔐 JWT配置 +# =========================================== +jwt: + secret: test-jwt-secret-key-for-testing-only diff --git a/deployments/docker/grafana/provisioning/dashboards/dashboards.yml b/deployments/docker/grafana/provisioning/dashboards/dashboards.yml new file mode 100644 index 0000000..a7e97b1 --- /dev/null +++ b/deployments/docker/grafana/provisioning/dashboards/dashboards.yml @@ -0,0 +1,11 @@ +apiVersion: 1 + +providers: + - name: "default" + orgId: 1 + folder: "" + type: file + disableDeletion: false + editable: true + options: + path: /etc/grafana/provisioning/dashboards diff --git a/deployments/docker/grafana/provisioning/dashboards/tracing-dashboard.json b/deployments/docker/grafana/provisioning/dashboards/tracing-dashboard.json new file mode 100644 index 0000000..e997be0 --- /dev/null +++ b/deployments/docker/grafana/provisioning/dashboards/tracing-dashboard.json @@ -0,0 +1,206 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": null, + "links": [], + "panels": [ + { + "datasource": "Jaeger", + "fieldConfig": { + "defaults": { + "custom": {}, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "pluginVersion": "7.5.0", + "targets": [ + { + "query": "tyapi-server", + "refId": "A" + } + ], + "title": "TYAPI服务链路追踪", + "type": "jaeger" + }, + { + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": { + "align": null, + "filterable": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 2, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "values": false, + "calcs": [ + "lastNotNull" + ], + "fields": "" + }, + "textMode": "auto" + }, + "pluginVersion": "7.5.0", + "targets": [ + { + "expr": "rate(http_requests_total[5m])", + "interval": "", + "legendFormat": "{{method}} {{path}}", + "refId": "A" + } + ], + "title": "HTTP请求速率", + "type": "stat" + }, + { + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": {}, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 0.5 + }, + { + "color": "red", + "value": 1 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 8 + }, + "id": 3, + "options": { + "displayMode": "list", + "orientation": "horizontal", + "reduceOptions": { + "values": false, + "calcs": [ + "lastNotNull" + ], + "fields": "" + }, + "showUnfilled": true + }, + "pluginVersion": "7.5.0", + "targets": [ + { + "expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))", + "interval": "", + "legendFormat": "95th percentile - {{method}} {{path}}", + "refId": "A" + }, + { + "expr": "histogram_quantile(0.50, rate(http_request_duration_seconds_bucket[5m]))", + "interval": "", + "legendFormat": "50th percentile - {{method}} {{path}}", + "refId": "B" + } + ], + "title": "HTTP请求延迟分布", + "type": "bargauge" + } + ], + "schemaVersion": 27, + "style": "dark", + "tags": [ + "jaeger", + "tracing", + "tyapi" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "TYAPI链路追踪监控", + "uid": "tyapi-tracing", + "version": 1 +} \ No newline at end of file diff --git a/deployments/docker/grafana/provisioning/datasources/datasources.yml b/deployments/docker/grafana/provisioning/datasources/datasources.yml new file mode 100644 index 0000000..bd75905 --- /dev/null +++ b/deployments/docker/grafana/provisioning/datasources/datasources.yml @@ -0,0 +1,36 @@ +apiVersion: 1 + +datasources: + - name: Prometheus + type: prometheus + access: proxy + url: http://prometheus:9090 + isDefault: true + editable: true + + - name: Jaeger + type: jaeger + access: proxy + url: http://jaeger:16686 + isDefault: false + editable: true + jsonData: + httpMethod: GET + # 启用节点图功能 + nodeGraph: + enabled: true + # 启用追踪链路图 + traceQuery: + timeShiftEnabled: true + spanStartTimeShift: "1h" + spanEndTimeShift: "1h" + # 配置标签 + tracesToLogs: + datasourceUid: "loki" + tags: ["job", "instance", "pod", "namespace"] + mappedTags: [{ key: "service.name", value: "service" }] + mapTagNamesEnabled: false + spanStartTimeShift: "1h" + spanEndTimeShift: "1h" + filterByTraceID: false + filterBySpanID: false diff --git a/deployments/docker/jaeger-sampling-prod.json b/deployments/docker/jaeger-sampling-prod.json new file mode 100644 index 0000000..8b4be32 --- /dev/null +++ b/deployments/docker/jaeger-sampling-prod.json @@ -0,0 +1,109 @@ +{ + "service_strategies": [ + { + "service": "tyapi-server", + "type": "probabilistic", + "param": 0.01, + "max_traces_per_second": 50, + "operation_strategies": [ + { + "operation": "GET /health", + "type": "probabilistic", + "param": 0.001 + }, + { + "operation": "GET /metrics", + "type": "probabilistic", + "param": 0.001 + }, + { + "operation": "GET /api/v1/health", + "type": "probabilistic", + "param": 0.001 + }, + { + "operation": "POST /api/v1/users/register", + "type": "probabilistic", + "param": 0.2 + }, + { + "operation": "POST /api/v1/users/login", + "type": "probabilistic", + "param": 0.1 + }, + { + "operation": "POST /api/v1/users/logout", + "type": "probabilistic", + "param": 0.05 + }, + { + "operation": "POST /api/v1/users/refresh", + "type": "probabilistic", + "param": 0.05 + }, + { + "operation": "GET /api/v1/users/profile", + "type": "probabilistic", + "param": 0.02 + }, + { + "operation": "PUT /api/v1/users/profile", + "type": "probabilistic", + "param": 0.1 + }, + { + "operation": "POST /api/v1/sms/send", + "type": "probabilistic", + "param": 0.3 + }, + { + "operation": "POST /api/v1/sms/verify", + "type": "probabilistic", + "param": 0.3 + }, + { + "operation": "error", + "type": "probabilistic", + "param": 1.0 + } + ] + } + ], + "default_strategy": { + "type": "probabilistic", + "param": 0.01, + "max_traces_per_second": 50 + }, + "per_operation_strategies": [ + { + "operation": "health_check", + "type": "probabilistic", + "param": 0.001 + }, + { + "operation": "metrics", + "type": "probabilistic", + "param": 0.001 + }, + { + "operation": "database_query", + "type": "probabilistic", + "param": 0.01 + }, + { + "operation": "redis_operation", + "type": "probabilistic", + "param": 0.005 + }, + { + "operation": "external_api_call", + "type": "probabilistic", + "param": 0.2 + }, + { + "operation": "error", + "type": "probabilistic", + "param": 1.0 + } + ] +} \ No newline at end of file diff --git a/deployments/docker/jaeger-sampling.json b/deployments/docker/jaeger-sampling.json new file mode 100644 index 0000000..160d07e --- /dev/null +++ b/deployments/docker/jaeger-sampling.json @@ -0,0 +1,109 @@ +{ + "service_strategies": [ + { + "service": "tyapi-server", + "type": "probabilistic", + "param": 0.1, + "max_traces_per_second": 200, + "operation_strategies": [ + { + "operation": "GET /health", + "type": "probabilistic", + "param": 0.01 + }, + { + "operation": "GET /metrics", + "type": "probabilistic", + "param": 0.01 + }, + { + "operation": "GET /api/v1/health", + "type": "probabilistic", + "param": 0.01 + }, + { + "operation": "POST /api/v1/users/register", + "type": "probabilistic", + "param": 0.8 + }, + { + "operation": "POST /api/v1/users/login", + "type": "probabilistic", + "param": 0.8 + }, + { + "operation": "POST /api/v1/users/logout", + "type": "probabilistic", + "param": 0.3 + }, + { + "operation": "POST /api/v1/users/refresh", + "type": "probabilistic", + "param": 0.3 + }, + { + "operation": "GET /api/v1/users/profile", + "type": "probabilistic", + "param": 0.2 + }, + { + "operation": "PUT /api/v1/users/profile", + "type": "probabilistic", + "param": 0.6 + }, + { + "operation": "POST /api/v1/sms/send", + "type": "probabilistic", + "param": 0.9 + }, + { + "operation": "POST /api/v1/sms/verify", + "type": "probabilistic", + "param": 0.9 + }, + { + "operation": "error", + "type": "probabilistic", + "param": 1.0 + } + ] + } + ], + "default_strategy": { + "type": "probabilistic", + "param": 0.1, + "max_traces_per_second": 200 + }, + "per_operation_strategies": [ + { + "operation": "health_check", + "type": "probabilistic", + "param": 0.01 + }, + { + "operation": "metrics", + "type": "probabilistic", + "param": 0.01 + }, + { + "operation": "database_query", + "type": "probabilistic", + "param": 0.1 + }, + { + "operation": "redis_operation", + "type": "probabilistic", + "param": 0.05 + }, + { + "operation": "external_api_call", + "type": "probabilistic", + "param": 0.8 + }, + { + "operation": "error", + "type": "probabilistic", + "param": 1.0 + } + ] +} \ No newline at end of file diff --git a/deployments/docker/jaeger-ui-config.json b/deployments/docker/jaeger-ui-config.json new file mode 100644 index 0000000..3788a68 --- /dev/null +++ b/deployments/docker/jaeger-ui-config.json @@ -0,0 +1,46 @@ +{ + "monitor": { + "menuEnabled": true + }, + "dependencies": { + "menuEnabled": true + }, + "archiveEnabled": true, + "tracking": { + "gaID": null, + "trackErrors": false + }, + "menu": [ + { + "label": "TYAPI 文档", + "url": "http://localhost:3000/docs", + "anchorTarget": "_blank" + }, + { + "label": "Grafana 监控", + "url": "http://localhost:3000", + "anchorTarget": "_blank" + }, + { + "label": "Prometheus 指标", + "url": "http://localhost:9090", + "anchorTarget": "_blank" + } + ], + "search": { + "maxLookback": { + "label": "2 days", + "value": "2d" + }, + "maxLimit": 1500 + }, + "scripts": [], + "linkPatterns": [ + { + "type": "process", + "key": "jaeger.version", + "url": "https://github.com/jaegertracing/jaeger/releases/tag/#{jaeger.version}", + "text": "#{jaeger.version} release notes" + } + ] +} \ No newline at end of file diff --git a/deployments/docker/nginx.conf b/deployments/docker/nginx.conf new file mode 100644 index 0000000..4651306 --- /dev/null +++ b/deployments/docker/nginx.conf @@ -0,0 +1,234 @@ +user nginx; +worker_processes auto; +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; + use epoll; + multi_accept on; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # 日志格式 + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for" ' + 'rt=$request_time uct="$upstream_connect_time" ' + 'uht="$upstream_header_time" urt="$upstream_response_time"'; + + access_log /var/log/nginx/access.log main; + + # 基本设置 + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + server_tokens off; + + # 客户端设置 + client_max_body_size 10M; + client_body_timeout 60s; + client_header_timeout 60s; + + # Gzip 压缩 + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_types + text/plain + text/css + text/xml + text/javascript + application/json + application/javascript + application/xml+rss + application/atom+xml; + + # 上游服务器配置 + upstream tyapi_backend { + server tyapi-app:8080; + keepalive 32; + } + + upstream grafana_backend { + server grafana:3000; + keepalive 16; + } + + upstream prometheus_backend { + server prometheus:9090; + keepalive 16; + } + + upstream minio_backend { + server minio:9000; + keepalive 16; + } + + upstream minio_console_backend { + server minio:9001; + keepalive 16; + } + + upstream jaeger_backend { + server jaeger:16686; + keepalive 16; + } + + upstream pgadmin_backend { + server pgadmin:80; + keepalive 16; + } + + # HTTP 服务器配置 + server { + listen 80; + server_name _; + + # 健康检查端点 + location /health { + proxy_pass http://tyapi_backend/health; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # API 路由 + location /api/ { + proxy_pass http://tyapi_backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # 超时设置 + proxy_connect_timeout 30s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + + # 缓冲设置 + proxy_buffering on; + proxy_buffer_size 4k; + proxy_buffers 8 4k; + } + + # Swagger 文档 + location /swagger/ { + proxy_pass http://tyapi_backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # 根路径重定向到API文档 + location = / { + return 301 /swagger/index.html; + } + + # Grafana 仪表盘 + location /grafana/ { + proxy_pass http://grafana_backend/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # WebSocket 支持 + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + # Prometheus 监控 + location /prometheus/ { + proxy_pass http://prometheus_backend/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Jaeger 链路追踪 + location /jaeger/ { + proxy_pass http://jaeger_backend/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # MinIO 对象存储 API + location /minio/ { + proxy_pass http://minio_backend/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # MinIO 需要的特殊头 + proxy_set_header X-Forwarded-Host $host; + client_max_body_size 1000M; + } + + # MinIO 控制台 + location /minio-console/ { + proxy_pass http://minio_console_backend/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # WebSocket 支持 + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + # pgAdmin 数据库管理 + location /pgadmin/ { + proxy_pass http://pgadmin_backend/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Script-Name /pgadmin; + } + + # 限制某些路径的访问 + location ~* \.(git|env|log)$ { + deny all; + return 404; + } + } + + # HTTPS 服务器配置 (可选,需要SSL证书) + # server { + # listen 443 ssl http2; + # server_name your-domain.com; + + # ssl_certificate /etc/nginx/ssl/server.crt; + # ssl_certificate_key /etc/nginx/ssl/server.key; + # ssl_protocols TLSv1.2 TLSv1.3; + # ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384; + # ssl_prefer_server_ciphers off; + + # # HSTS + # add_header Strict-Transport-Security "max-age=63072000" always; + + # location / { + # proxy_pass http://tyapi_backend; + # proxy_set_header Host $host; + # proxy_set_header X-Real-IP $remote_addr; + # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + # proxy_set_header X-Forwarded-Proto $scheme; + # } + # } +} \ No newline at end of file diff --git a/deployments/docker/pgadmin-passfile b/deployments/docker/pgadmin-passfile new file mode 100644 index 0000000..c4f56f8 --- /dev/null +++ b/deployments/docker/pgadmin-passfile @@ -0,0 +1 @@ +postgres:5432:tyapi_dev:postgres:Pg9mX4kL8nW2rT5y \ No newline at end of file diff --git a/deployments/docker/pgadmin-servers.json b/deployments/docker/pgadmin-servers.json new file mode 100644 index 0000000..561b614 --- /dev/null +++ b/deployments/docker/pgadmin-servers.json @@ -0,0 +1,15 @@ +{ + "Servers": { + "1": { + "Name": "TYAPI PostgreSQL", + "Group": "Development Servers", + "Host": "postgres", + "Port": 5432, + "MaintenanceDB": "tyapi_dev", + "Username": "postgres", + "PassFile": "/var/lib/pgadmin/passfile", + "SSLMode": "prefer", + "Comment": "TYAPI Development Database" + } + } +} \ No newline at end of file diff --git a/deployments/docker/prometheus.yml b/deployments/docker/prometheus.yml new file mode 100644 index 0000000..a17d4c7 --- /dev/null +++ b/deployments/docker/prometheus.yml @@ -0,0 +1,39 @@ +global: + scrape_interval: 15s + evaluation_interval: 15s + +rule_files: + # - "first_rules.yml" + # - "second_rules.yml" + +scrape_configs: + # Prometheus 自身监控 + - job_name: "prometheus" + static_configs: + - targets: ["localhost:9090"] + + # TYAPI 应用监控 + - job_name: "tyapi-server" + static_configs: + - targets: ["host.docker.internal:8080"] + metrics_path: "/metrics" + scrape_interval: 10s + + # PostgreSQL 监控 (如果启用了 postgres_exporter) + - job_name: "postgres" + static_configs: + - targets: ["postgres:5432"] + scrape_interval: 30s + + # Redis 监控 (如果启用了 redis_exporter) + - job_name: "redis" + static_configs: + - targets: ["redis:6379"] + scrape_interval: 30s + + # Docker 容器监控 (如果启用了 cadvisor) + - job_name: "docker" + static_configs: + - targets: ["host.docker.internal:8080"] + metrics_path: "/docker/metrics" + scrape_interval: 30s diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index ead963c..83b3b04 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -1,152 +1,195 @@ -version: '3.8' - services: - # PostgreSQL 数据库 - postgres: - image: postgres:15-alpine - container_name: tyapi-postgres - environment: - POSTGRES_DB: tyapi_dev - POSTGRES_USER: postgres - POSTGRES_PASSWORD: Pg9mX4kL8nW2rT5y - POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C" - ports: - - "5432:5432" - volumes: - - postgres_data:/var/lib/postgresql/data - - ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql - networks: - - tyapi-network - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] - interval: 10s - timeout: 5s - retries: 5 + # PostgreSQL 数据库 + postgres: + image: postgres:16.9 + container_name: tyapi-postgres + environment: + POSTGRES_DB: tyapi_dev + POSTGRES_USER: postgres + POSTGRES_PASSWORD: Pg9mX4kL8nW2rT5y + POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C" + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + - ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql + networks: + - tyapi-network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 10s + timeout: 5s + retries: 5 + # Redis 缓存 + redis: + image: redis:8.0.2 + container_name: tyapi-redis + ports: + - "6379:6379" + volumes: + - redis_data:/data + - ./deployments/docker/redis.conf:/usr/local/etc/redis/redis.conf + command: redis-server /usr/local/etc/redis/redis.conf + networks: + - tyapi-network + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 3s + retries: 5 - # Redis 缓存 - redis: - image: redis:7-alpine - container_name: tyapi-redis - ports: - - "6379:6379" - volumes: - - redis_data:/data - - ./deployments/docker/redis.conf:/usr/local/etc/redis/redis.conf - command: redis-server /usr/local/etc/redis/redis.conf - networks: - - tyapi-network - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 10s - timeout: 3s - retries: 5 + # Jaeger 链路追踪 + jaeger: + image: jaegertracing/all-in-one:1.70.0 + container_name: tyapi-jaeger + ports: + - "16686:16686" # Jaeger UI + - "14268:14268" # Jaeger HTTP collector (传统) + - "14250:14250" # Jaeger gRPC collector + - "4317:4317" # OTLP gRPC receiver + - "4318:4318" # OTLP HTTP receiver + environment: + # 启用OTLP接收器 + COLLECTOR_OTLP_ENABLED: true + # 配置内存存储 + SPAN_STORAGE_TYPE: memory + # 设置日志级别 + LOG_LEVEL: info + # 配置采样策略 + SAMPLING_STRATEGIES_FILE: /etc/jaeger/sampling_strategies.json + # 内存存储配置 + MEMORY_MAX_TRACES: 50000 + # 查询服务配置 + QUERY_MAX_CLOCK_SKEW_ADJUSTMENT: 0 + # 收集器配置 + COLLECTOR_QUEUE_SIZE: 2000 + COLLECTOR_NUM_WORKERS: 50 + # gRPC服务器配置 + COLLECTOR_GRPC_SERVER_MAX_RECEIVE_MESSAGE_LENGTH: 4194304 + COLLECTOR_GRPC_SERVER_MAX_CONNECTION_AGE: 60s + # HTTP服务器配置 + COLLECTOR_HTTP_SERVER_HOST_PORT: :14268 + # UI配置 + QUERY_UI_CONFIG: /etc/jaeger/ui-config.json + volumes: + - ./deployments/docker/jaeger-sampling.json:/etc/jaeger/sampling_strategies.json + - ./deployments/docker/jaeger-ui-config.json:/etc/jaeger/ui-config.json + networks: + - tyapi-network + healthcheck: + test: + [ + "CMD", + "wget", + "--no-verbose", + "--tries=1", + "--spider", + "http://localhost:14269/health", + ] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s - # Jaeger 链路追踪 - jaeger: - image: jaegertracing/all-in-one:latest - container_name: tyapi-jaeger - ports: - - "16686:16686" # Jaeger UI - - "14268:14268" # Jaeger HTTP collector - environment: - COLLECTOR_OTLP_ENABLED: true - networks: - - tyapi-network + # Prometheus 监控 + prometheus: + image: prom/prometheus:main + container_name: tyapi-prometheus + ports: + - "9090:9090" + volumes: + - ./deployments/docker/prometheus.yml:/etc/prometheus/prometheus.yml + - prometheus_data:/prometheus + command: + - "--config.file=/etc/prometheus/prometheus.yml" + - "--storage.tsdb.path=/prometheus" + - "--web.console.libraries=/etc/prometheus/console_libraries" + - "--web.console.templates=/etc/prometheus/consoles" + - "--web.enable-lifecycle" + networks: + - tyapi-network - # Prometheus 监控 - prometheus: - image: prom/prometheus:latest - container_name: tyapi-prometheus - ports: - - "9090:9090" - volumes: - - ./deployments/docker/prometheus.yml:/etc/prometheus/prometheus.yml - - prometheus_data:/prometheus - command: - - '--config.file=/etc/prometheus/prometheus.yml' - - '--storage.tsdb.path=/prometheus' - - '--web.console.libraries=/etc/prometheus/console_libraries' - - '--web.console.templates=/etc/prometheus/consoles' - - '--web.enable-lifecycle' - networks: - - tyapi-network + # Grafana 仪表盘 + grafana: + image: grafana/grafana:12.0.2 + container_name: tyapi-grafana + ports: + - "3000:3000" + environment: + GF_SECURITY_ADMIN_PASSWORD: Gf7nB3xM9cV6pQ2w + volumes: + - grafana_data:/var/lib/grafana + - ./deployments/docker/grafana/provisioning:/etc/grafana/provisioning + networks: + - tyapi-network - # Grafana 仪表盘 - grafana: - image: grafana/grafana:latest - container_name: tyapi-grafana - ports: - - "3000:3000" - environment: - GF_SECURITY_ADMIN_PASSWORD: Gf7nB3xM9cV6pQ2w - volumes: - - grafana_data:/var/lib/grafana - - ./deployments/docker/grafana/provisioning:/etc/grafana/provisioning - networks: - - tyapi-network + # MinIO 对象存储 (S3兼容) + minio: + image: minio/minio:RELEASE.2025-06-13T11-33-47Z-cpuv1 + container_name: tyapi-minio + ports: + - "9000:9000" + - "9001:9001" + environment: + MINIO_ROOT_USER: minioadmin + MINIO_ROOT_PASSWORD: Mn5oH8yK3bR7vX1z + volumes: + - minio_data:/data + command: server /data --console-address ":9001" + networks: + - tyapi-network + healthcheck: + test: + ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] + interval: 30s + timeout: 20s + retries: 3 - # MinIO 对象存储 (S3兼容) - minio: - image: minio/minio:latest - container_name: tyapi-minio - ports: - - "9000:9000" - - "9001:9001" - environment: - MINIO_ROOT_USER: minioadmin - MINIO_ROOT_PASSWORD: Mn5oH8yK3bR7vX1z - volumes: - - minio_data:/data - command: server /data --console-address ":9001" - networks: - - tyapi-network - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] - interval: 30s - timeout: 20s - retries: 3 + # Mailhog 邮件测试服务 + mailhog: + image: mailhog/mailhog:v1.0.1 + container_name: tyapi-mailhog + ports: + - "1025:1025" # SMTP + - "8025:8025" # Web UI + networks: + - tyapi-network - # Mailhog 邮件测试服务 - mailhog: - image: mailhog/mailhog:latest - container_name: tyapi-mailhog - ports: - - "1025:1025" # SMTP - - "8025:8025" # Web UI - networks: - - tyapi-network - - # pgAdmin 数据库管理 - pgadmin: - image: dpage/pgadmin4:latest - container_name: tyapi-pgadmin - environment: - PGADMIN_DEFAULT_EMAIL: admin@tyapi.com - PGADMIN_DEFAULT_PASSWORD: Pa4dG9wF2sL6tN8u - PGADMIN_CONFIG_SERVER_MODE: 'False' - ports: - - "5050:80" - volumes: - - pgadmin_data:/var/lib/pgadmin - networks: - - tyapi-network - depends_on: - - postgres + # pgAdmin 数据库管理 + pgadmin: + image: dpage/pgadmin4:snapshot + container_name: tyapi-pgadmin + environment: + PGADMIN_DEFAULT_EMAIL: admin@tyapi.com + PGADMIN_DEFAULT_PASSWORD: Pa4dG9wF2sL6tN8u + PGADMIN_CONFIG_SERVER_MODE: "True" + PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: "False" + PGADMIN_CONFIG_UPGRADE_CHECK_ENABLED: "False" + ports: + - "5050:80" + volumes: + - pgadmin_data:/var/lib/pgadmin + - ./deployments/docker/pgadmin-servers.json:/pgadmin4/servers.json + - ./deployments/docker/pgadmin-passfile:/var/lib/pgadmin/passfile + networks: + - tyapi-network + depends_on: + - postgres volumes: - postgres_data: - driver: local - redis_data: - driver: local - prometheus_data: - driver: local - grafana_data: - driver: local - minio_data: - driver: local - pgadmin_data: - driver: local + postgres_data: + driver: local + redis_data: + driver: local + prometheus_data: + driver: local + grafana_data: + driver: local + minio_data: + driver: local + pgadmin_data: + driver: local networks: - tyapi-network: - driver: bridge \ No newline at end of file + tyapi-network: + driver: bridge diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..71594ba --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,442 @@ +version: "3.8" + +services: + # PostgreSQL 数据库 (生产环境) + postgres: + image: postgres:16.9 + container_name: tyapi-postgres-prod + environment: + POSTGRES_DB: ${DB_NAME:-tyapi_prod} + POSTGRES_USER: ${DB_USER:-tyapi_user} + POSTGRES_PASSWORD: ${DB_PASSWORD} + POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C" + # 性能优化配置 + POSTGRES_SHARED_PRELOAD_LIBRARIES: pg_stat_statements + volumes: + - postgres_data:/var/lib/postgresql/data + - ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql + networks: + - tyapi-network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-tyapi_user}"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 60s + restart: unless-stopped + deploy: + resources: + limits: + memory: 2G + cpus: "1.0" + reservations: + memory: 512M + cpus: "0.5" + # 生产环境不暴露端口到主机 + # ports: + # - "5432:5432" + + # Redis 缓存 (生产环境) + redis: + image: redis:8.0.2 + container_name: tyapi-redis-prod + environment: + REDIS_PASSWORD: ${REDIS_PASSWORD} + volumes: + - redis_data:/data + - ./deployments/docker/redis.conf:/usr/local/etc/redis/redis.conf + command: > + sh -c " + if [ ! -z '${REDIS_PASSWORD}' ]; then + redis-server /usr/local/etc/redis/redis.conf --requirepass ${REDIS_PASSWORD} + else + redis-server /usr/local/etc/redis/redis.conf + fi + " + networks: + - tyapi-network + healthcheck: + test: > + sh -c " + if [ ! -z '${REDIS_PASSWORD}' ]; then + redis-cli -a ${REDIS_PASSWORD} ping + else + redis-cli ping + fi + " + interval: 30s + timeout: 10s + retries: 5 + restart: unless-stopped + deploy: + resources: + limits: + memory: 1G + cpus: "0.5" + reservations: + memory: 256M + cpus: "0.2" + # 生产环境不暴露端口到主机 + # ports: + # - "6379:6379" + + # TYAPI 应用程序 + tyapi-app: + image: docker-registry.tianyuanapi.com/tyapi-server:${APP_VERSION:-latest} + container_name: tyapi-app-prod + environment: + # 环境设置 + ENV: production + + # 服务器配置 + SERVER_PORT: ${SERVER_PORT:-8080} + SERVER_MODE: release + + # 数据库配置 + DB_HOST: postgres + DB_PORT: 5432 + DB_USER: ${DB_USER:-tyapi_user} + DB_PASSWORD: ${DB_PASSWORD} + DB_NAME: ${DB_NAME:-tyapi_prod} + DB_SSLMODE: ${DB_SSLMODE:-require} + + # Redis配置 + REDIS_HOST: redis + REDIS_PORT: 6379 + REDIS_PASSWORD: ${REDIS_PASSWORD} + + # JWT配置 + JWT_SECRET: ${JWT_SECRET} + + # 监控配置 + TRACING_ENABLED: true + TRACING_ENDPOINT: http://jaeger:4317 + METRICS_ENABLED: true + + # 日志配置 + LOG_LEVEL: ${LOG_LEVEL:-info} + LOG_FORMAT: json + + # 短信配置 + SMS_ACCESS_KEY_ID: ${SMS_ACCESS_KEY_ID} + SMS_ACCESS_KEY_SECRET: ${SMS_ACCESS_KEY_SECRET} + SMS_SIGN_NAME: ${SMS_SIGN_NAME} + SMS_TEMPLATE_CODE: ${SMS_TEMPLATE_CODE} + ports: + - "${APP_PORT:-8080}:8080" + volumes: + - app_logs:/app/logs + networks: + - tyapi-network + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/health"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 60s + restart: unless-stopped + deploy: + resources: + limits: + memory: 1G + cpus: "1.0" + reservations: + memory: 256M + cpus: "0.3" + + # Jaeger 链路追踪 (生产环境配置) + jaeger: + image: jaegertracing/all-in-one:1.70.0 + container_name: tyapi-jaeger-prod + ports: + - "${JAEGER_UI_PORT:-16686}:16686" # Jaeger UI + - "4317:4317" # OTLP gRPC receiver + - "4318:4318" # OTLP HTTP receiver + environment: + # 启用OTLP接收器 + COLLECTOR_OTLP_ENABLED: true + # 配置持久化存储 (生产环境建议使用Elasticsearch/Cassandra) + SPAN_STORAGE_TYPE: memory + # 设置日志级别 + LOG_LEVEL: warn + # 配置采样策略 + SAMPLING_STRATEGIES_FILE: /etc/jaeger/sampling_strategies.json + # 内存存储配置 (生产环境应增加) + MEMORY_MAX_TRACES: 100000 + # 查询服务配置 + QUERY_MAX_CLOCK_SKEW_ADJUSTMENT: 0 + # 收集器配置 (生产环境优化) + COLLECTOR_QUEUE_SIZE: 5000 + COLLECTOR_NUM_WORKERS: 100 + # gRPC服务器配置 + COLLECTOR_GRPC_SERVER_MAX_RECEIVE_MESSAGE_LENGTH: 8388608 + COLLECTOR_GRPC_SERVER_MAX_CONNECTION_AGE: 120s + COLLECTOR_GRPC_SERVER_MAX_CONNECTION_IDLE: 60s + # HTTP服务器配置 + COLLECTOR_HTTP_SERVER_HOST_PORT: :14268 + COLLECTOR_HTTP_SERVER_READ_TIMEOUT: 30s + COLLECTOR_HTTP_SERVER_WRITE_TIMEOUT: 30s + # UI配置 + QUERY_UI_CONFIG: /etc/jaeger/ui-config.json + # 安全配置 + QUERY_BASE_PATH: / + volumes: + - ./deployments/docker/jaeger-sampling-prod.json:/etc/jaeger/sampling_strategies.json + - ./deployments/docker/jaeger-ui-config.json:/etc/jaeger/ui-config.json + networks: + - tyapi-network + healthcheck: + test: + [ + "CMD", + "wget", + "--no-verbose", + "--tries=1", + "--spider", + "http://localhost:14269/health", + ] + interval: 60s + timeout: 30s + retries: 3 + start_period: 60s + restart: unless-stopped + deploy: + resources: + limits: + memory: 1G + cpus: "0.5" + reservations: + memory: 512M + cpus: "0.2" + + # Nginx 反向代理 (可选) + nginx: + image: nginx:1.27.3-alpine + container_name: tyapi-nginx-prod + ports: + - "${NGINX_HTTP_PORT:-80}:80" + - "${NGINX_HTTPS_PORT:-443}:443" + volumes: + - ./deployments/docker/nginx.conf:/etc/nginx/nginx.conf + - ./deployments/docker/ssl:/etc/nginx/ssl + - nginx_logs:/var/log/nginx + networks: + - tyapi-network + depends_on: + - tyapi-app + healthcheck: + test: + [ + "CMD", + "wget", + "--quiet", + "--tries=1", + "--spider", + "http://localhost/health", + ] + interval: 30s + timeout: 10s + retries: 3 + restart: unless-stopped + deploy: + resources: + limits: + memory: 256M + cpus: "0.3" + reservations: + memory: 64M + cpus: "0.1" + + # Prometheus 监控 (生产环境) + prometheus: + image: prom/prometheus:v2.55.1 + container_name: tyapi-prometheus-prod + ports: + - "${PROMETHEUS_PORT:-9090}:9090" + volumes: + - ./deployments/docker/prometheus.yml:/etc/prometheus/prometheus.yml + - prometheus_data:/prometheus + command: + - "--config.file=/etc/prometheus/prometheus.yml" + - "--storage.tsdb.path=/prometheus" + - "--web.console.libraries=/etc/prometheus/console_libraries" + - "--web.console.templates=/etc/prometheus/consoles" + - "--web.enable-lifecycle" + - "--storage.tsdb.retention.time=30d" + - "--storage.tsdb.retention.size=10GB" + - "--web.enable-admin-api" + networks: + - tyapi-network + healthcheck: + test: + [ + "CMD", + "wget", + "--quiet", + "--tries=1", + "--spider", + "http://localhost:9090/-/healthy", + ] + interval: 30s + timeout: 10s + retries: 3 + restart: unless-stopped + deploy: + resources: + limits: + memory: 2G + cpus: "1.0" + reservations: + memory: 512M + cpus: "0.3" + + # Grafana 仪表盘 (生产环境) + grafana: + image: grafana/grafana:11.4.0 + container_name: tyapi-grafana-prod + ports: + - "${GRAFANA_PORT:-3000}:3000" + environment: + GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD:-Gf7nB3xM9cV6pQ2w} + GF_SECURITY_ADMIN_USER: ${GRAFANA_ADMIN_USER:-admin} + GF_INSTALL_PLUGINS: "grafana-clock-panel,grafana-simple-json-datasource" + GF_ANALYTICS_REPORTING_ENABLED: "false" + GF_ANALYTICS_CHECK_FOR_UPDATES: "false" + GF_USERS_ALLOW_SIGN_UP: "false" + GF_SERVER_ROOT_URL: "http://localhost:3000" + volumes: + - grafana_data:/var/lib/grafana + - ./deployments/docker/grafana/provisioning:/etc/grafana/provisioning + networks: + - tyapi-network + depends_on: + - prometheus + healthcheck: + test: + [ + "CMD", + "wget", + "--quiet", + "--tries=1", + "--spider", + "http://localhost:3000/api/health", + ] + interval: 30s + timeout: 10s + retries: 3 + restart: unless-stopped + deploy: + resources: + limits: + memory: 1G + cpus: "0.5" + reservations: + memory: 256M + cpus: "0.2" + + # MinIO 对象存储 (生产环境) + minio: + image: minio/minio:RELEASE.2024-12-18T13-15-44Z + container_name: tyapi-minio-prod + ports: + - "${MINIO_API_PORT:-9000}:9000" + - "${MINIO_CONSOLE_PORT:-9001}:9001" + environment: + MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minioadmin} + MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-Mn5oH8yK3bR7vX1z} + MINIO_BROWSER_REDIRECT_URL: "http://localhost:9001" + volumes: + - minio_data:/data + command: server /data --console-address ":9001" + networks: + - tyapi-network + healthcheck: + test: + ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] + interval: 30s + timeout: 20s + retries: 3 + restart: unless-stopped + deploy: + resources: + limits: + memory: 1G + cpus: "0.5" + reservations: + memory: 256M + cpus: "0.2" + + # pgAdmin 数据库管理 (生产环境) + pgadmin: + image: dpage/pgadmin4:8.15 + container_name: tyapi-pgadmin-prod + environment: + PGADMIN_DEFAULT_EMAIL: ${PGADMIN_EMAIL:-admin@tyapi.com} + PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_PASSWORD:-Pa4dG9wF2sL6tN8u} + PGADMIN_CONFIG_SERVER_MODE: "True" + PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: "False" + PGADMIN_CONFIG_UPGRADE_CHECK_ENABLED: "False" + PGADMIN_CONFIG_ENHANCED_COOKIE_PROTECTION: "False" + ports: + - "${PGADMIN_PORT:-5050}:80" + volumes: + - pgadmin_data:/var/lib/pgadmin + - ./deployments/docker/pgadmin-servers.json:/pgadmin4/servers.json + - ./deployments/docker/pgadmin-passfile:/var/lib/pgadmin/passfile + networks: + - tyapi-network + depends_on: + postgres: + condition: service_healthy + healthcheck: + test: + [ + "CMD", + "wget", + "--quiet", + "--tries=1", + "--spider", + "http://localhost/misc/ping", + ] + interval: 30s + timeout: 10s + retries: 3 + restart: unless-stopped + deploy: + resources: + limits: + memory: 512M + cpus: "0.3" + reservations: + memory: 128M + cpus: "0.1" + +volumes: + postgres_data: + driver: local + redis_data: + driver: local + app_logs: + driver: local + nginx_logs: + driver: local + prometheus_data: + driver: local + grafana_data: + driver: local + minio_data: + driver: local + pgadmin_data: + driver: local + +networks: + tyapi-network: + driver: bridge + ipam: + config: + - subnet: 172.20.0.0/16 diff --git a/docs/Makefile使用指南.md b/docs/Makefile使用指南.md new file mode 100644 index 0000000..45b930d --- /dev/null +++ b/docs/Makefile使用指南.md @@ -0,0 +1,668 @@ +# 📋 Makefile 命令详细执行逻辑指南 + +本文档详细说明了 TYAPI 项目中每个 Make 命令的**执行逻辑、具体步骤和背后原理**。 + +## 🚀 快速开始 + +```bash +# 查看所有可用命令 +make help + +# 设置开发环境 +make setup + +# 启动开发依赖服务 +make dev-up + +# 开发模式运行应用 +make dev +``` + +--- + +## 📖 命令详细执行逻辑 + +### 🔍 **信息查看命令** + +#### `make help` + +**执行逻辑**: + +```bash +# 1. 检测操作系统类型 +# 2. 输出预定义的帮助信息 +@echo "TYAPI Server Makefile" +@echo "Usage: make [target]" +@echo "Main targets:" +@echo " help Show this help message" +# ... 更多帮助信息 +``` + +**实际效果**: + +- 📄 直接打印硬编码的帮助文本 +- 🚫 不执行任何文件操作 +- ⚡ 瞬间完成,无依赖 + +#### `make version` + +**执行逻辑**: + +```bash +# 步骤1: 自动执行 make build (依赖检查) +# 步骤2: 执行构建好的二进制文件 +./bin/tyapi-server -version +``` + +**详细步骤**: + +1. **依赖检查**: 检查是否需要重新构建 (通过 make build) +2. **参数传递**: 向应用程序传递 `-version` 标志 +3. **读取构建信息**: 应用程序输出编译时注入的版本信息 + - `main.version` (来自 Makefile 的 VERSION 变量) + - `main.commit` (来自 git rev-parse --short HEAD) + - `main.date` (来自构建时间) + +**相关文件**: `bin/tyapi-server`, `cmd/api/main.go` + +--- + +### 🏗️ **构建相关命令** + +#### `make build` + +**执行逻辑**: + +```bash +# 步骤1: 检测操作系统 +ifeq ($(OS),Windows_NT) + # Windows: 创建bin目录 (如果不存在) + @if not exist "bin" mkdir "bin" +else + # Unix: 创建bin目录 + @mkdir -p bin +endif + +# 步骤2: 构建Go应用程序 +go build -ldflags "-X main.version=1.0.0 -X main.commit=abc123 -X main.date=2025-01-01T00:00:00Z" -o bin/tyapi-server cmd/api/main.go +``` + +**详细步骤**: + +1. **环境检测**: 通过 `$(OS)` 变量检测操作系统 +2. **目录创建**: + - Windows: `if not exist "bin" mkdir "bin"` + - Unix: `mkdir -p bin` +3. **版本信息收集**: + - `BUILD_TIME`: PowerShell/date 命令获取当前时间 + - `GIT_COMMIT`: git 命令获取当前 commit hash + - `VERSION`: 硬编码版本号 1.0.0 +4. **编译执行**: + - 使用 `go build` 命令 + - `-ldflags` 注入版本信息到可执行文件 + - 输出文件到 `bin/tyapi-server` + +**生成文件**: `bin/tyapi-server` (Windows 下为 `.exe`) + +#### `make build-prod` + +**执行逻辑**: + +```bash +# 步骤1: 创建bin目录 (同build) +# 步骤2: 生产环境构建 +CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "..." -a -installsuffix cgo -o bin/tyapi-server-linux-amd64 cmd/api/main.go +``` + +**关键参数解析**: + +- `CGO_ENABLED=0`: 禁用 CGO,生成纯静态二进制 +- `GOOS=linux GOARCH=amd64`: 强制 Linux 64 位平台 +- `-a`: 重新构建所有包 +- `-installsuffix cgo`: 避免缓存冲突 +- **结果**: 生成可在任何 Linux x64 环境运行的静态二进制文件 + +#### `make build-all` + +**执行逻辑**: + +```bash +# 步骤1: 创建bin目录 +# 步骤2: 循环构建5个平台 +# Linux AMD64 +CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build ... -o bin/tyapi-server-linux-amd64 +# Linux ARM64 +CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build ... -o bin/tyapi-server-linux-arm64 +# macOS Intel +CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build ... -o bin/tyapi-server-darwin-amd64 +# macOS Apple Silicon +CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build ... -o bin/tyapi-server-darwin-arm64 +# Windows +CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build ... -o bin/tyapi-server-windows-amd64.exe +``` + +**生成的 5 个文件**: + +- `bin/tyapi-server-linux-amd64` +- `bin/tyapi-server-linux-arm64` +- `bin/tyapi-server-darwin-amd64` +- `bin/tyapi-server-darwin-arm64` +- `bin/tyapi-server-windows-amd64.exe` + +--- + +### ▶️ **运行相关命令** + +#### `make run` + +**执行逻辑**: + +```bash +# 步骤1: 执行make build (依赖) +# 步骤2: 直接运行构建好的二进制文件 +./bin/tyapi-server +``` + +**详细流程**: + +1. **依赖检查**: Make 自动检查 build 目标是否需要重新执行 +2. **文件存在性检查**: 确认 `bin/tyapi-server` 存在 +3. **权限检查**: Unix 系统检查执行权限 +4. **进程启动**: 启动 Gin HTTP 服务器 +5. **端口监听**: 默认监听 8080 端口 + +**运行环境**: 需要 PostgreSQL 和 Redis 服务可用 + +#### `make dev` + +**执行逻辑**: + +```bash +# 直接执行 (已去掉air检查) +go run cmd/api/main.go +``` + +**详细步骤**: + +1. **源码编译**: Go 编译器实时编译 main.go 及其依赖 +2. **内存运行**: 不生成磁盘文件,直接在内存中运行 +3. **依赖加载**: 自动下载并编译所有 import 的包 +4. **服务启动**: 启动 HTTP 服务器监听 8080 端口 + +**与 build 的区别**: + +- ✅ 无需预先构建 +- ✅ 代码变更后需手动重启 +- ❌ 每次启动都需要重新编译 + +--- + +### 🛠️ **开发工具命令** + +#### `make deps` + +**执行逻辑**: + +```bash +# 步骤1: 下载依赖 +go mod download +# 步骤2: 整理依赖 +go mod tidy +``` + +**详细步骤**: + +1. **go mod download**: + - 读取 `go.mod` 文件 + - 下载所有依赖包到 `$GOPATH/pkg/mod/` + - 验证包的 checksum (通过 go.sum) + - 不修改 go.mod 文件 +2. **go mod tidy**: + - 扫描所有.go 文件中的 import 语句 + - 添加缺失的依赖到 go.mod + - 移除未使用的依赖 + - 更新 go.sum 文件 + +**影响的文件**: `go.mod`, `go.sum`, `$GOPATH/pkg/mod/` + +#### `make fmt` + +**执行逻辑**: + +```bash +go fmt ./... +``` + +**详细步骤**: + +1. **递归扫描**: 扫描当前目录及所有子目录的.go 文件 +2. **格式化规则应用**: + - 统一缩进 (tab) + - 统一换行 + - 移除行尾空格 + - 规范化大括号位置 +3. **文件修改**: 直接修改源文件 (in-place) +4. **报告**: 输出被修改的文件列表 + +**影响的文件**: 所有.go 源码文件 + +#### `make lint` + +**执行逻辑**: + +```bash +# 检查golangci-lint是否安装 +@if command -v golangci-lint >/dev/null 2>&1; then \ + golangci-lint run; \ +else \ + echo "golangci-lint not installed, skipping lint check"; \ +fi +``` + +**详细步骤**: + +1. **工具检查**: 使用 `command -v` 检查 golangci-lint 是否在 PATH 中 +2. **如果已安装**: + - 读取 `.golangci.yml` 配置 (如果存在) + - 运行多个 linter 检查 (默认包括: errcheck, gosimple, govet, ineffassign 等) + - 分析所有.go 文件 + - 输出问题报告 +3. **如果未安装**: 显示提示信息并跳过 + +**检查项目**: 代码质量、潜在 bug、性能问题、安全问题等 + +--- + +### 🧪 **测试相关命令** + +#### `make test` + +**执行逻辑**: + +```bash +go test -v -race -coverprofile=coverage.out ./... +``` + +**参数详解**: + +- `-v`: 详细输出,显示每个测试的名称和结果 +- `-race`: 启用竞态条件检测器 +- `-coverprofile=coverage.out`: 生成覆盖率数据文件 +- `./...`: 递归测试所有包 + +**详细步骤**: + +1. **包发现**: 递归扫描所有包含\*\_test.go 的目录 +2. **编译测试**: 为每个包编译测试二进制文件 +3. **竞态检测**: 启用 Go race detector +4. **执行测试**: 逐个运行 Test\*函数 +5. **覆盖率收集**: 记录每行代码是否被执行 +6. **生成报告**: 输出到 coverage.out 文件 + +**生成文件**: `coverage.out` + +#### `make coverage` + +**执行逻辑**: + +```bash +# 步骤1: 执行make test (依赖) +# 步骤2: 生成HTML报告 +go tool cover -html=coverage.out -o coverage.html +# 步骤3: 提示用户 +@echo "Coverage report generated: coverage.html" +``` + +**详细步骤**: + +1. **依赖检查**: 确保 coverage.out 文件存在 +2. **HTML 生成**: + - 读取 coverage.out 二进制数据 + - 生成带颜色标记的 HTML 页面 + - 绿色 = 已覆盖,红色 = 未覆盖 +3. **文件输出**: 生成 coverage.html 文件 + +**生成文件**: `coverage.html` (可在浏览器中打开) + +--- + +### 🗂️ **环境管理命令** + +#### `make env` + +**执行逻辑**: + +```bash +# Windows环境 +ifeq ($(OS),Windows_NT) + @if not exist ".env" ( \ + echo Creating .env file... && \ + copy env.example .env && \ + echo .env file created, please modify configuration as needed \ + ) else ( \ + echo .env file already exists \ + ) +# Unix环境 +else + @if [ ! -f .env ]; then \ + echo "Creating .env file..."; \ + cp env.example .env; \ + echo ".env file created, please modify configuration as needed"; \ + else \ + echo ".env file already exists"; \ + fi +endif +``` + +**详细步骤**: + +1. **文件检查**: 检查.env 文件是否已存在 +2. **如果不存在**: + - Windows: 使用 `copy` 命令复制 + - Unix: 使用 `cp` 命令复制 + - 复制 `env.example` → `.env` +3. **如果已存在**: 显示提示信息,不覆盖现有文件 + +**相关文件**: `env.example` → `.env` + +#### `make setup` + +**执行逻辑**: + +```bash +# 依赖: make deps env +# 然后执行: +@echo "Setting up development environment..." +@echo "1. Dependencies installed" +@echo "2. .env file created" +@echo "3. Please ensure PostgreSQL and Redis are running" +@echo "4. Run 'make migrate' to create database tables" +@echo "5. Run 'make dev' to start development server" +``` + +**详细步骤**: + +1. **执行 make deps**: 安装 Go 依赖 +2. **执行 make env**: 创建环境配置文件 +3. **输出设置指南**: 显示后续步骤提示 + +**完成后状态**: 开发环境基本就绪,需要手动启动数据库服务 + +--- + +### 🐳 **Docker 相关命令** + +#### `make docker-build` + +**执行逻辑**: + +```bash +docker build -t tyapi-server:1.0.0 -t tyapi-server:latest . +``` + +**详细步骤**: + +1. **读取 Dockerfile**: 从当前目录读取 Dockerfile +2. **构建上下文**: 将当前目录作为构建上下文发送给 Docker daemon +3. **镜像构建**: + - 执行 Dockerfile 中的每个指令 + - 逐层构建镜像 + - 缓存中间层以提高构建速度 +4. **标签应用**: 同时打上版本标签和 latest 标签 + +**生成镜像**: `tyapi-server:1.0.0`, `tyapi-server:latest` + +#### `make docker-run` + +**执行逻辑**: + +```bash +docker run -d --name tyapi-server -p 8080:8080 --env-file .env tyapi-server:latest +``` + +**详细步骤**: + +1. **环境检查**: 确认.env 文件存在 +2. **容器创建**: 基于 latest 镜像创建容器 +3. **参数应用**: + - `-d`: 后台运行 (detached mode) + - `--name tyapi-server`: 设置容器名称 + - `-p 8080:8080`: 端口映射 (主机:容器) + - `--env-file .env`: 加载环境变量文件 +4. **容器启动**: 启动应用程序进程 + +**结果**: 后台运行的 Docker 容器,端口 8080 可访问 + +--- + +### 🔧 **服务管理命令** + +#### `make services-up` / `make dev-up` + +**执行逻辑**: + +```bash +# 检查docker-compose.dev.yml文件 +# Windows: +@if exist "docker-compose.dev.yml" ( \ + docker-compose -f docker-compose.dev.yml up -d \ +) else ( \ + echo docker-compose.dev.yml not found \ +) +``` + +**详细步骤**: + +1. **文件检查**: 验证 docker-compose.dev.yml 文件存在 +2. **Docker Compose 启动**: + - 读取 docker-compose.dev.yml 配置 + - 拉取所需的 Docker 镜像 (如果本地不存在) + - 创建 Docker 网络 (tyapi-network) + - 创建数据卷 (postgres_data, redis_data 等) + - 按依赖顺序启动服务: + - PostgreSQL (端口 5432) + - Redis (端口 6379) + - pgAdmin (端口 5050) + - Prometheus (端口 9090) + - Grafana (端口 3000) + - Jaeger (端口 16686) + - MinIO (端口 9000/9001) + - MailHog (端口 8025) + +**启动的 8 个服务**: 数据库、缓存、监控、管理工具等完整开发环境 + +#### `make services-down` / `make dev-down` + +**执行逻辑**: + +```bash +docker-compose -f docker-compose.dev.yml down +``` + +**详细步骤**: + +1. **容器停止**: 优雅停止所有服务容器 (发送 SIGTERM) +2. **容器删除**: 删除所有相关容器 +3. **网络清理**: 删除自定义网络 +4. **数据保留**: 保留数据卷 (数据不丢失) + +**保留的资源**: 数据卷、镜像 +**删除的资源**: 容器、网络 + +--- + +### 🗃️ **数据库相关命令** + +#### `make migrate` + +**执行逻辑**: + +```bash +# 步骤1: 执行make build (依赖) +# 步骤2: 运行迁移 +./bin/tyapi-server -migrate +``` + +**详细步骤**: + +1. **应用程序启动**: 以迁移模式启动应用 +2. **数据库连接**: 连接到 PostgreSQL 数据库 +3. **迁移文件扫描**: 扫描 migrations 目录下的 SQL 文件 +4. **版本检查**: 检查数据库中的迁移版本表 +5. **增量执行**: 只执行未应用的迁移文件 +6. **版本更新**: 更新迁移版本记录 +7. **应用退出**: 迁移完成后程序退出 + +**相关文件**: `internal/domains/user/migrations/`, PostgreSQL 数据库 + +#### `make health` + +**执行逻辑**: + +```bash +# 步骤1: 执行make build (依赖) +# 步骤2: 运行健康检查 +./bin/tyapi-server -health +``` + +**详细步骤**: + +1. **健康检查启动**: 以健康检查模式启动应用 +2. **组件检查**: + - PostgreSQL 数据库连接 + - Redis 缓存连接 + - 关键配置项验证 + - 必要文件存在性检查 +3. **状态报告**: 输出每个组件的健康状态 +4. **退出码**: 成功返回 0,失败返回非零 + +**检查项目**: 数据库、缓存、配置、权限等 + +--- + +### 🧹 **清理命令** + +#### `make clean` + +**执行逻辑**: + +```bash +# 步骤1: Go缓存清理 +go clean +# 步骤2: 文件删除 (根据操作系统) +# Windows: +@if exist "bin" rmdir /s /q "bin" 2>nul || echo "" +@if exist "coverage.out" del /f /q "coverage.out" 2>nul || echo "" +@if exist "coverage.html" del /f /q "coverage.html" 2>nul || echo "" +``` + +**详细步骤**: + +1. **Go 清理**: + - 清理编译缓存 + - 删除临时构建文件 + - 清理测试缓存 +2. **目录删除**: + - 删除整个 bin 目录及内容 + - Windows: `rmdir /s /q` + - Unix: `rm -rf` +3. **文件删除**: + - 删除 coverage.out 测试覆盖率文件 + - 删除 coverage.html 覆盖率报告 +4. **错误抑制**: 使用 `2>nul` 或 `2>/dev/null` 忽略"文件不存在"错误 + +**删除的内容**: `bin/`, `coverage.out`, `coverage.html`, Go 构建缓存 + +--- + +### 🚀 **流水线命令** + +#### `make ci` + +**执行逻辑**: + +```bash +# 顺序执行5个步骤: +make deps # 安装依赖 +make fmt # 代码格式化 +make lint # 代码检查 +make test # 运行测试 +make build # 构建应用 +``` + +**详细流程**: + +1. **deps**: 确保所有依赖最新且完整 +2. **fmt**: 统一代码格式,确保可读性 +3. **lint**: 静态代码分析,发现潜在问题 +4. **test**: 运行所有测试,确保功能正确 +5. **build**: 验证代码可以成功编译 + +**失败策略**: 任何一步失败,立即停止后续步骤 + +#### `make release` + +**执行逻辑**: + +```bash +# 顺序执行3个步骤: +make ci # 完整CI检查 +make build-all # 交叉编译所有平台 +make docker-build # 构建Docker镜像 +``` + +**详细流程**: + +1. **CI 检查**: 确保代码质量和功能正确性 +2. **多平台构建**: 生成 5 个平台的可执行文件 +3. **Docker 镜像**: 构建容器化版本 + +**输出产物**: + +- 5 个平台的二进制文件 +- Docker 镜像 (2 个标签) +- 测试覆盖率报告 + +--- + +## 💡 **命令执行顺序和依赖关系** + +### 🔗 **依赖关系图** + +``` +version ─────► build +health ──────► build +run ─────────► build +migrate ─────► build + +coverage ────► test +ci ──────────► deps → fmt → lint → test → build +release ─────► ci → build-all → docker-build +setup ───────► deps → env +``` + +### ⚡ **执行时机建议** + +#### **每次开发前** + +```bash +make setup # 首次使用 +make dev-up # 启动依赖服务 +make dev # 开始开发 +``` + +#### **提交代码前** + +```bash +make ci # 完整检查 +``` + +#### **发布版本前** + +```bash +make release # 完整构建 +``` + +--- + +**这个指南详细说明了每个命令背后的具体操作逻辑,帮助您完全理解 Makefile 的工作原理!** 🎯 diff --git a/docs/docs.go b/docs/docs.go new file mode 100644 index 0000000..f85bda7 --- /dev/null +++ b/docs/docs.go @@ -0,0 +1,8 @@ +// Package docs 生成的API文档包 +// 这个包导入了自动生成的Swagger文档 +package docs + +import ( + // 导入生成的swagger文档 + _ "tyapi-server/docs/swagger" +) diff --git a/docs/swagger/docs.go b/docs/swagger/docs.go new file mode 100644 index 0000000..7e4039b --- /dev/null +++ b/docs/swagger/docs.go @@ -0,0 +1,592 @@ +// Package swagger Code generated by swaggo/swag. DO NOT EDIT +package swagger + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": { + "name": "API Support", + "url": "https://github.com/your-org/tyapi-server-gin", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/users/login-password": { + "post": { + "description": "使用手机号和密码进行用户登录,返回JWT令牌", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户认证" + ], + "summary": "用户密码登录", + "parameters": [ + { + "description": "密码登录请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.LoginWithPasswordRequest" + } + } + ], + "responses": { + "200": { + "description": "登录成功", + "schema": { + "$ref": "#/definitions/dto.LoginResponse" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "401": { + "description": "认证失败", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/users/login-sms": { + "post": { + "description": "使用手机号和短信验证码进行用户登录,返回JWT令牌", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户认证" + ], + "summary": "用户短信验证码登录", + "parameters": [ + { + "description": "短信登录请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.LoginWithSMSRequest" + } + } + ], + "responses": { + "200": { + "description": "登录成功", + "schema": { + "$ref": "#/definitions/dto.LoginResponse" + } + }, + "400": { + "description": "请求参数错误或验证码无效", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "401": { + "description": "认证失败", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/users/me": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "根据JWT令牌获取当前登录用户的详细信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "获取当前用户信息", + "responses": { + "200": { + "description": "用户信息", + "schema": { + "$ref": "#/definitions/dto.UserResponse" + } + }, + "401": { + "description": "未认证", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "用户不存在", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/users/me/password": { + "put": { + "security": [ + { + "Bearer": [] + } + ], + "description": "使用旧密码、新密码确认和验证码修改当前用户的密码", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "修改密码", + "parameters": [ + { + "description": "修改密码请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ChangePasswordRequest" + } + } + ], + "responses": { + "200": { + "description": "密码修改成功", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "请求参数错误或验证码无效", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "401": { + "description": "未认证", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/users/register": { + "post": { + "description": "使用手机号、密码和验证码进行用户注册,需要确认密码", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户认证" + ], + "summary": "用户注册", + "parameters": [ + { + "description": "用户注册请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.RegisterRequest" + } + } + ], + "responses": { + "201": { + "description": "注册成功", + "schema": { + "$ref": "#/definitions/dto.UserResponse" + } + }, + "400": { + "description": "请求参数错误或验证码无效", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "409": { + "description": "手机号已存在", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/users/send-code": { + "post": { + "description": "向指定手机号发送验证码,支持注册、登录、修改密码等场景", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户认证" + ], + "summary": "发送短信验证码", + "parameters": [ + { + "description": "发送验证码请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SendCodeRequest" + } + } + ], + "responses": { + "200": { + "description": "验证码发送成功", + "schema": { + "$ref": "#/definitions/dto.SendCodeResponse" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "429": { + "description": "请求频率限制", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + } + }, + "definitions": { + "dto.ChangePasswordRequest": { + "type": "object", + "required": [ + "code", + "confirm_new_password", + "new_password", + "old_password" + ], + "properties": { + "code": { + "type": "string", + "example": "123456" + }, + "confirm_new_password": { + "type": "string", + "example": "newpassword123" + }, + "new_password": { + "type": "string", + "maxLength": 128, + "minLength": 6, + "example": "newpassword123" + }, + "old_password": { + "type": "string", + "example": "oldpassword123" + } + } + }, + "dto.LoginResponse": { + "type": "object", + "properties": { + "access_token": { + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + }, + "expires_in": { + "type": "integer", + "example": 86400 + }, + "login_method": { + "description": "password 或 sms", + "type": "string", + "example": "password" + }, + "token_type": { + "type": "string", + "example": "Bearer" + }, + "user": { + "$ref": "#/definitions/dto.UserResponse" + } + } + }, + "dto.LoginWithPasswordRequest": { + "type": "object", + "required": [ + "password", + "phone" + ], + "properties": { + "password": { + "type": "string", + "example": "password123" + }, + "phone": { + "type": "string", + "example": "13800138000" + } + } + }, + "dto.LoginWithSMSRequest": { + "type": "object", + "required": [ + "code", + "phone" + ], + "properties": { + "code": { + "type": "string", + "example": "123456" + }, + "phone": { + "type": "string", + "example": "13800138000" + } + } + }, + "dto.RegisterRequest": { + "type": "object", + "required": [ + "code", + "confirm_password", + "password", + "phone" + ], + "properties": { + "code": { + "type": "string", + "example": "123456" + }, + "confirm_password": { + "type": "string", + "example": "password123" + }, + "password": { + "type": "string", + "maxLength": 128, + "minLength": 6, + "example": "password123" + }, + "phone": { + "type": "string", + "example": "13800138000" + } + } + }, + "dto.SendCodeRequest": { + "type": "object", + "required": [ + "phone", + "scene" + ], + "properties": { + "phone": { + "type": "string", + "example": "13800138000" + }, + "scene": { + "enum": [ + "register", + "login", + "change_password", + "reset_password", + "bind", + "unbind" + ], + "allOf": [ + { + "$ref": "#/definitions/entities.SMSScene" + } + ], + "example": "register" + } + } + }, + "dto.SendCodeResponse": { + "type": "object", + "properties": { + "expires_at": { + "type": "string", + "example": "2024-01-01T00:05:00Z" + }, + "message": { + "type": "string", + "example": "验证码发送成功" + } + } + }, + "dto.UserResponse": { + "type": "object", + "properties": { + "created_at": { + "type": "string", + "example": "2024-01-01T00:00:00Z" + }, + "id": { + "type": "string", + "example": "123e4567-e89b-12d3-a456-426614174000" + }, + "phone": { + "type": "string", + "example": "13800138000" + }, + "updated_at": { + "type": "string", + "example": "2024-01-01T00:00:00Z" + } + } + }, + "entities.SMSScene": { + "type": "string", + "enum": [ + "register", + "login", + "change_password", + "reset_password", + "bind", + "unbind" + ], + "x-enum-comments": { + "SMSSceneBind": "绑定手机号", + "SMSSceneChangePassword": "修改密码", + "SMSSceneLogin": "登录", + "SMSSceneRegister": "注册", + "SMSSceneResetPassword": "重置密码", + "SMSSceneUnbind": "解绑手机号" + }, + "x-enum-varnames": [ + "SMSSceneRegister", + "SMSSceneLogin", + "SMSSceneChangePassword", + "SMSSceneResetPassword", + "SMSSceneBind", + "SMSSceneUnbind" + ] + } + }, + "securityDefinitions": { + "Bearer": { + "description": "Type \"Bearer\" followed by a space and JWT token.", + "type": "apiKey", + "name": "Authorization", + "in": "header" + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "1.0", + Host: "localhost:8080", + BasePath: "/api/v1", + Schemes: []string{}, + Title: "TYAPI Server API", + Description: "基于DDD和Clean Architecture的企业级后端API服务\n采用Gin框架构建,支持用户管理、JWT认证、事件驱动等功能", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json new file mode 100644 index 0000000..a1ee0cb --- /dev/null +++ b/docs/swagger/swagger.json @@ -0,0 +1,568 @@ +{ + "swagger": "2.0", + "info": { + "description": "基于DDD和Clean Architecture的企业级后端API服务\n采用Gin框架构建,支持用户管理、JWT认证、事件驱动等功能", + "title": "TYAPI Server API", + "contact": { + "name": "API Support", + "url": "https://github.com/your-org/tyapi-server-gin", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0" + }, + "host": "localhost:8080", + "basePath": "/api/v1", + "paths": { + "/users/login-password": { + "post": { + "description": "使用手机号和密码进行用户登录,返回JWT令牌", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户认证" + ], + "summary": "用户密码登录", + "parameters": [ + { + "description": "密码登录请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.LoginWithPasswordRequest" + } + } + ], + "responses": { + "200": { + "description": "登录成功", + "schema": { + "$ref": "#/definitions/dto.LoginResponse" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "401": { + "description": "认证失败", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/users/login-sms": { + "post": { + "description": "使用手机号和短信验证码进行用户登录,返回JWT令牌", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户认证" + ], + "summary": "用户短信验证码登录", + "parameters": [ + { + "description": "短信登录请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.LoginWithSMSRequest" + } + } + ], + "responses": { + "200": { + "description": "登录成功", + "schema": { + "$ref": "#/definitions/dto.LoginResponse" + } + }, + "400": { + "description": "请求参数错误或验证码无效", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "401": { + "description": "认证失败", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/users/me": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "根据JWT令牌获取当前登录用户的详细信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "获取当前用户信息", + "responses": { + "200": { + "description": "用户信息", + "schema": { + "$ref": "#/definitions/dto.UserResponse" + } + }, + "401": { + "description": "未认证", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "用户不存在", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/users/me/password": { + "put": { + "security": [ + { + "Bearer": [] + } + ], + "description": "使用旧密码、新密码确认和验证码修改当前用户的密码", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "修改密码", + "parameters": [ + { + "description": "修改密码请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ChangePasswordRequest" + } + } + ], + "responses": { + "200": { + "description": "密码修改成功", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "请求参数错误或验证码无效", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "401": { + "description": "未认证", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/users/register": { + "post": { + "description": "使用手机号、密码和验证码进行用户注册,需要确认密码", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户认证" + ], + "summary": "用户注册", + "parameters": [ + { + "description": "用户注册请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.RegisterRequest" + } + } + ], + "responses": { + "201": { + "description": "注册成功", + "schema": { + "$ref": "#/definitions/dto.UserResponse" + } + }, + "400": { + "description": "请求参数错误或验证码无效", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "409": { + "description": "手机号已存在", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/users/send-code": { + "post": { + "description": "向指定手机号发送验证码,支持注册、登录、修改密码等场景", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户认证" + ], + "summary": "发送短信验证码", + "parameters": [ + { + "description": "发送验证码请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SendCodeRequest" + } + } + ], + "responses": { + "200": { + "description": "验证码发送成功", + "schema": { + "$ref": "#/definitions/dto.SendCodeResponse" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "429": { + "description": "请求频率限制", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + } + }, + "definitions": { + "dto.ChangePasswordRequest": { + "type": "object", + "required": [ + "code", + "confirm_new_password", + "new_password", + "old_password" + ], + "properties": { + "code": { + "type": "string", + "example": "123456" + }, + "confirm_new_password": { + "type": "string", + "example": "newpassword123" + }, + "new_password": { + "type": "string", + "maxLength": 128, + "minLength": 6, + "example": "newpassword123" + }, + "old_password": { + "type": "string", + "example": "oldpassword123" + } + } + }, + "dto.LoginResponse": { + "type": "object", + "properties": { + "access_token": { + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + }, + "expires_in": { + "type": "integer", + "example": 86400 + }, + "login_method": { + "description": "password 或 sms", + "type": "string", + "example": "password" + }, + "token_type": { + "type": "string", + "example": "Bearer" + }, + "user": { + "$ref": "#/definitions/dto.UserResponse" + } + } + }, + "dto.LoginWithPasswordRequest": { + "type": "object", + "required": [ + "password", + "phone" + ], + "properties": { + "password": { + "type": "string", + "example": "password123" + }, + "phone": { + "type": "string", + "example": "13800138000" + } + } + }, + "dto.LoginWithSMSRequest": { + "type": "object", + "required": [ + "code", + "phone" + ], + "properties": { + "code": { + "type": "string", + "example": "123456" + }, + "phone": { + "type": "string", + "example": "13800138000" + } + } + }, + "dto.RegisterRequest": { + "type": "object", + "required": [ + "code", + "confirm_password", + "password", + "phone" + ], + "properties": { + "code": { + "type": "string", + "example": "123456" + }, + "confirm_password": { + "type": "string", + "example": "password123" + }, + "password": { + "type": "string", + "maxLength": 128, + "minLength": 6, + "example": "password123" + }, + "phone": { + "type": "string", + "example": "13800138000" + } + } + }, + "dto.SendCodeRequest": { + "type": "object", + "required": [ + "phone", + "scene" + ], + "properties": { + "phone": { + "type": "string", + "example": "13800138000" + }, + "scene": { + "enum": [ + "register", + "login", + "change_password", + "reset_password", + "bind", + "unbind" + ], + "allOf": [ + { + "$ref": "#/definitions/entities.SMSScene" + } + ], + "example": "register" + } + } + }, + "dto.SendCodeResponse": { + "type": "object", + "properties": { + "expires_at": { + "type": "string", + "example": "2024-01-01T00:05:00Z" + }, + "message": { + "type": "string", + "example": "验证码发送成功" + } + } + }, + "dto.UserResponse": { + "type": "object", + "properties": { + "created_at": { + "type": "string", + "example": "2024-01-01T00:00:00Z" + }, + "id": { + "type": "string", + "example": "123e4567-e89b-12d3-a456-426614174000" + }, + "phone": { + "type": "string", + "example": "13800138000" + }, + "updated_at": { + "type": "string", + "example": "2024-01-01T00:00:00Z" + } + } + }, + "entities.SMSScene": { + "type": "string", + "enum": [ + "register", + "login", + "change_password", + "reset_password", + "bind", + "unbind" + ], + "x-enum-comments": { + "SMSSceneBind": "绑定手机号", + "SMSSceneChangePassword": "修改密码", + "SMSSceneLogin": "登录", + "SMSSceneRegister": "注册", + "SMSSceneResetPassword": "重置密码", + "SMSSceneUnbind": "解绑手机号" + }, + "x-enum-varnames": [ + "SMSSceneRegister", + "SMSSceneLogin", + "SMSSceneChangePassword", + "SMSSceneResetPassword", + "SMSSceneBind", + "SMSSceneUnbind" + ] + } + }, + "securityDefinitions": { + "Bearer": { + "description": "Type \"Bearer\" followed by a space and JWT token.", + "type": "apiKey", + "name": "Authorization", + "in": "header" + } + } +} \ No newline at end of file diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml new file mode 100644 index 0000000..e385e8f --- /dev/null +++ b/docs/swagger/swagger.yaml @@ -0,0 +1,397 @@ +basePath: /api/v1 +definitions: + dto.ChangePasswordRequest: + properties: + code: + example: "123456" + type: string + confirm_new_password: + example: newpassword123 + type: string + new_password: + example: newpassword123 + maxLength: 128 + minLength: 6 + type: string + old_password: + example: oldpassword123 + type: string + required: + - code + - confirm_new_password + - new_password + - old_password + type: object + dto.LoginResponse: + properties: + access_token: + example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... + type: string + expires_in: + example: 86400 + type: integer + login_method: + description: password 或 sms + example: password + type: string + token_type: + example: Bearer + type: string + user: + $ref: '#/definitions/dto.UserResponse' + type: object + dto.LoginWithPasswordRequest: + properties: + password: + example: password123 + type: string + phone: + example: "13800138000" + type: string + required: + - password + - phone + type: object + dto.LoginWithSMSRequest: + properties: + code: + example: "123456" + type: string + phone: + example: "13800138000" + type: string + required: + - code + - phone + type: object + dto.RegisterRequest: + properties: + code: + example: "123456" + type: string + confirm_password: + example: password123 + type: string + password: + example: password123 + maxLength: 128 + minLength: 6 + type: string + phone: + example: "13800138000" + type: string + required: + - code + - confirm_password + - password + - phone + type: object + dto.SendCodeRequest: + properties: + phone: + example: "13800138000" + type: string + scene: + allOf: + - $ref: '#/definitions/entities.SMSScene' + enum: + - register + - login + - change_password + - reset_password + - bind + - unbind + example: register + required: + - phone + - scene + type: object + dto.SendCodeResponse: + properties: + expires_at: + example: "2024-01-01T00:05:00Z" + type: string + message: + example: 验证码发送成功 + type: string + type: object + dto.UserResponse: + properties: + created_at: + example: "2024-01-01T00:00:00Z" + type: string + id: + example: 123e4567-e89b-12d3-a456-426614174000 + type: string + phone: + example: "13800138000" + type: string + updated_at: + example: "2024-01-01T00:00:00Z" + type: string + type: object + entities.SMSScene: + enum: + - register + - login + - change_password + - reset_password + - bind + - unbind + type: string + x-enum-comments: + SMSSceneBind: 绑定手机号 + SMSSceneChangePassword: 修改密码 + SMSSceneLogin: 登录 + SMSSceneRegister: 注册 + SMSSceneResetPassword: 重置密码 + SMSSceneUnbind: 解绑手机号 + x-enum-varnames: + - SMSSceneRegister + - SMSSceneLogin + - SMSSceneChangePassword + - SMSSceneResetPassword + - SMSSceneBind + - SMSSceneUnbind +host: localhost:8080 +info: + contact: + email: support@example.com + name: API Support + url: https://github.com/your-org/tyapi-server-gin + description: |- + 基于DDD和Clean Architecture的企业级后端API服务 + 采用Gin框架构建,支持用户管理、JWT认证、事件驱动等功能 + license: + name: Apache 2.0 + url: http://www.apache.org/licenses/LICENSE-2.0.html + title: TYAPI Server API + version: "1.0" +paths: + /users/login-password: + post: + consumes: + - application/json + description: 使用手机号和密码进行用户登录,返回JWT令牌 + parameters: + - description: 密码登录请求 + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.LoginWithPasswordRequest' + produces: + - application/json + responses: + "200": + description: 登录成功 + schema: + $ref: '#/definitions/dto.LoginResponse' + "400": + description: 请求参数错误 + schema: + additionalProperties: true + type: object + "401": + description: 认证失败 + schema: + additionalProperties: true + type: object + "500": + description: 服务器内部错误 + schema: + additionalProperties: true + type: object + summary: 用户密码登录 + tags: + - 用户认证 + /users/login-sms: + post: + consumes: + - application/json + description: 使用手机号和短信验证码进行用户登录,返回JWT令牌 + parameters: + - description: 短信登录请求 + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.LoginWithSMSRequest' + produces: + - application/json + responses: + "200": + description: 登录成功 + schema: + $ref: '#/definitions/dto.LoginResponse' + "400": + description: 请求参数错误或验证码无效 + schema: + additionalProperties: true + type: object + "401": + description: 认证失败 + schema: + additionalProperties: true + type: object + "500": + description: 服务器内部错误 + schema: + additionalProperties: true + type: object + summary: 用户短信验证码登录 + tags: + - 用户认证 + /users/me: + get: + consumes: + - application/json + description: 根据JWT令牌获取当前登录用户的详细信息 + produces: + - application/json + responses: + "200": + description: 用户信息 + schema: + $ref: '#/definitions/dto.UserResponse' + "401": + description: 未认证 + schema: + additionalProperties: true + type: object + "404": + description: 用户不存在 + schema: + additionalProperties: true + type: object + "500": + description: 服务器内部错误 + schema: + additionalProperties: true + type: object + security: + - Bearer: [] + summary: 获取当前用户信息 + tags: + - 用户管理 + /users/me/password: + put: + consumes: + - application/json + description: 使用旧密码、新密码确认和验证码修改当前用户的密码 + parameters: + - description: 修改密码请求 + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.ChangePasswordRequest' + produces: + - application/json + responses: + "200": + description: 密码修改成功 + schema: + additionalProperties: true + type: object + "400": + description: 请求参数错误或验证码无效 + schema: + additionalProperties: true + type: object + "401": + description: 未认证 + schema: + additionalProperties: true + type: object + "500": + description: 服务器内部错误 + schema: + additionalProperties: true + type: object + security: + - Bearer: [] + summary: 修改密码 + tags: + - 用户管理 + /users/register: + post: + consumes: + - application/json + description: 使用手机号、密码和验证码进行用户注册,需要确认密码 + parameters: + - description: 用户注册请求 + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.RegisterRequest' + produces: + - application/json + responses: + "201": + description: 注册成功 + schema: + $ref: '#/definitions/dto.UserResponse' + "400": + description: 请求参数错误或验证码无效 + schema: + additionalProperties: true + type: object + "409": + description: 手机号已存在 + schema: + additionalProperties: true + type: object + "500": + description: 服务器内部错误 + schema: + additionalProperties: true + type: object + summary: 用户注册 + tags: + - 用户认证 + /users/send-code: + post: + consumes: + - application/json + description: 向指定手机号发送验证码,支持注册、登录、修改密码等场景 + parameters: + - description: 发送验证码请求 + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.SendCodeRequest' + produces: + - application/json + responses: + "200": + description: 验证码发送成功 + schema: + $ref: '#/definitions/dto.SendCodeResponse' + "400": + description: 请求参数错误 + schema: + additionalProperties: true + type: object + "429": + description: 请求频率限制 + schema: + additionalProperties: true + type: object + "500": + description: 服务器内部错误 + schema: + additionalProperties: true + type: object + summary: 发送短信验证码 + tags: + - 用户认证 +securityDefinitions: + Bearer: + description: Type "Bearer" followed by a space and JWT token. + in: header + name: Authorization + type: apiKey +swagger: "2.0" diff --git a/docs/开发指南.md b/docs/开始指南/开发指南.md similarity index 100% rename from docs/开发指南.md rename to docs/开始指南/开发指南.md diff --git a/docs/快速开始指南.md b/docs/开始指南/快速开始指南.md similarity index 100% rename from docs/快速开始指南.md rename to docs/开始指南/快速开始指南.md diff --git a/docs/故障排除指南.md b/docs/开始指南/故障排除指南.md similarity index 97% rename from docs/故障排除指南.md rename to docs/开始指南/故障排除指南.md index 7e03964..9cf74d7 100644 --- a/docs/故障排除指南.md +++ b/docs/开始指南/故障排除指南.md @@ -221,17 +221,18 @@ iostat 1 5 ## 开发环境问题 -### 1. 热重载不工作 +### 1. 开发服务器问题 ```bash -# 检查文件监控 -ls -la .air.toml +# 停止当前开发服务器 +Ctrl+C -# 重启开发服务器 -make dev-restart +# 重新启动开发服务器 +make dev -# 检查文件权限 -chmod +x scripts/dev.sh +# 检查Go模块状态 +go mod tidy +go mod download ``` ### 2. 测试失败 diff --git a/docs/开始指南/文档索引.md b/docs/开始指南/文档索引.md new file mode 100644 index 0000000..6f7b860 --- /dev/null +++ b/docs/开始指南/文档索引.md @@ -0,0 +1,145 @@ +# 📚 TYAPI Server 文档中心 + +欢迎使用 TYAPI Server 文档中心!我们已将原本的使用指南拆分为多个专题文档,方便您按需查阅。 + +## 📋 文档导航 + +### 🚀 [快速开始指南](./快速开始指南.md) + +- 前置要求 +- 一键启动 +- 验证安装 +- 访问管理界面 + +### 🔧 [环境搭建指南](./环境搭建指南.md) + +- 开发环境配置 +- 生产环境配置 +- 服务配置说明 +- 常见配置问题 + +### 📋 [Makefile 命令指南](./MAKEFILE_GUIDE.md) + +- 所有 Make 命令详细说明 +- 常用工作流程 +- 构建和部署命令 +- 开发工具命令 +- 故障排除技巧 + +### 👨‍💻 [开发指南](./开发指南.md) + +- 项目结构理解 +- 开发流程 +- 测试编写 +- 调试技巧 +- 代码规范 + +### 🌐 [API 使用指南](./API使用指南.md) + +- 认证机制 +- 用户管理 API +- 响应格式 +- HTTP 状态码 +- API 测试 + +### 🚀 [部署指南](./部署指南.md) + +- Docker 部署 +- Kubernetes 部署 +- 云平台部署 +- 负载均衡配置 +- 监控部署 + +### 📦 [生产环境部署指南](./生产环境部署指南.md) + +- Docker + 私有 Registry 完整部署方案 +- 多阶段构建生产级镜像 +- 安全配置和资源限制 +- 自动化部署脚本 +- 监控和故障排除 + +### 🔍 [故障排除指南](./故障排除指南.md) + +- 常见问题 +- 日志分析 +- 性能问题 +- 紧急响应流程 + +### 📋 [最佳实践指南](./最佳实践指南.md) + +- 开发最佳实践 +- 安全最佳实践 +- 性能最佳实践 +- 运维最佳实践 +- 团队协作 + +### 🔍 [链路追踪指南](./链路追踪指南.md) + +- Jaeger 配置和使用 +- OpenTelemetry 集成 +- Grafana 可视化 +- 性能监控和优化 +- 故障排查技巧 + +## 🎯 快速索引 + +### 新手入门 + +1. [快速开始指南](./快速开始指南.md) - 5 分钟快速体验 +2. [环境搭建指南](./环境搭建指南.md) - 配置开发环境 +3. [Makefile 命令指南](./MAKEFILE_GUIDE.md) - 掌握所有开发命令 +4. [开发指南](./开发指南.md) - 开始第一个功能 + +### 日常开发 + +- [Makefile 命令指南](./MAKEFILE_GUIDE.md) - 构建、测试、部署命令 +- [API 使用指南](./API使用指南.md) - API 调用参考 +- [开发指南](./开发指南.md) - 开发流程和规范 +- [链路追踪指南](./链路追踪指南.md) - 性能监控和问题排查 +- [故障排除指南](./故障排除指南.md) - 解决常见问题 + +### 生产部署 + +- [部署指南](./部署指南.md) - 生产环境部署 +- [生产环境部署指南](./生产环境部署指南.md) - Docker + 私有 Registry 完整方案 +- [最佳实践指南](./最佳实践指南.md) - 运维最佳实践 +- [故障排除指南](./故障排除指南.md) - 生产问题排查 + +## 🔗 相关文档 + +### 技术文档 + +- [架构文档](./ARCHITECTURE.md) - 系统架构设计 +- [API 规范](http://localhost:8080/swagger/) - 在线 API 文档 + +### 项目文档 + +- [README](../README.md) - 项目介绍 +- [更新日志](../CHANGELOG.md) - 版本变更记录 + +## 📞 获取帮助 + +### 在线资源 + +- **Swagger UI**: http://localhost:8080/swagger/ +- **健康检查**: http://localhost:8080/api/v1/health +- **监控面板**: http://localhost:3000 (Grafana) +- **链路追踪**: http://localhost:16686 (Jaeger) + +### 社区支持 + +- **GitHub Issues**: 提交问题和建议 +- **Wiki**: 查看详细技术文档 +- **讨论区**: 参与技术讨论 + +## 🔄 文档更新 + +本文档会持续更新,如果您发现任何问题或有改进建议,请: + +1. 提交 GitHub Issue +2. 发起 Pull Request +3. 联系维护团队 + +--- + +**提示**:建议将此页面加入书签,方便随时查阅相关文档。 diff --git a/docs/最佳实践指南.md b/docs/开始指南/最佳实践指南.md similarity index 100% rename from docs/最佳实践指南.md rename to docs/开始指南/最佳实践指南.md diff --git a/docs/ARCHITECTURE.md b/docs/开始指南/架构设计文档.md similarity index 100% rename from docs/ARCHITECTURE.md rename to docs/开始指南/架构设计文档.md diff --git a/docs/环境搭建指南.md b/docs/开始指南/环境搭建指南.md similarity index 100% rename from docs/环境搭建指南.md rename to docs/开始指南/环境搭建指南.md diff --git a/docs/开始指南/生产环境部署指南.md b/docs/开始指南/生产环境部署指南.md new file mode 100644 index 0000000..18b5274 --- /dev/null +++ b/docs/开始指南/生产环境部署指南.md @@ -0,0 +1,338 @@ +# TYAPI 生产环境部署指南 + +## 🎯 **部署架构概览** + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 生产环境架构 │ +├─────────────────────────────────────────────────────────────┤ +│ Nginx (80/443) ──► TYAPI App (8080) ──► PostgreSQL (5432) │ +│ │ │ │ │ +│ │ └──► Redis (6379) │ │ +│ │ └──► Jaeger (4317) │ │ +│ └──► Jaeger UI (16686) │ │ +└─────────────────────────────────────────────────────────────┘ + +私有镜像仓库: docker-registry.tianyuanapi.com +``` + +## 📋 **部署清单** + +### ✅ **已创建的文件** + +- `Dockerfile` - 多阶段构建的生产级镜像 +- `docker-compose.prod.yml` - 生产环境服务编排 +- `.dockerignore` - Docker 构建忽略文件 +- `deployments/docker/nginx.conf` - Nginx 反向代理配置 +- `.env.production` - 生产环境配置模板 +- `scripts/deploy.sh` - Linux/macOS 部署脚本 +- `scripts/deploy.ps1` - Windows PowerShell 部署脚本 +- `Makefile` - 新增生产环境相关命令 + +### 🛠 **服务组件** + +1. **PostgreSQL** - 主数据库 (生产优化配置) +2. **Redis** - 缓存和会话存储 (密码保护) +3. **TYAPI App** - 主应用程序 (生产模式) +4. **Jaeger** - 链路追踪 (生产级配置) +5. **Nginx** - 反向代理和负载均衡 +6. **Prometheus** - 监控数据收集和存储 +7. **Grafana** - 监控数据可视化仪表盘 +8. **MinIO** - S3 兼容对象存储服务 +9. **pgAdmin** - PostgreSQL 数据库管理工具 + +## 🚀 **快速部署步骤** + +### 1️⃣ **环境准备** + +```bash +# 确保服务器已安装 +- Docker 20.10+ +- Docker Compose 2.0+ +- Git (可选) + +# 检查版本 +docker --version +docker-compose --version +``` + +### 2️⃣ **获取代码** + +```bash +# 克隆项目到服务器 +git clone tyapi-server +cd tyapi-server + +# 或直接上传项目文件 +``` + +### 3️⃣ **配置环境变量** + +```bash +# 复制配置模板 +cp .env.production .env + +# 编辑配置文件 +nano .env +``` + +**必须修改的关键配置:** + +```bash +# 数据库配置 +DB_PASSWORD=your_secure_database_password_here + +# Redis配置 +REDIS_PASSWORD=your_secure_redis_password_here + +# JWT密钥 (至少32位) +JWT_SECRET=your_super_secure_jwt_secret_key_for_production_at_least_32_chars + +# Grafana管理员配置 +GRAFANA_ADMIN_PASSWORD=your_secure_grafana_password_here + +# MinIO对象存储配置 +MINIO_ROOT_PASSWORD=your_secure_minio_password_here + +# pgAdmin数据库管理配置 +PGADMIN_PASSWORD=your_secure_pgadmin_password_here + +# 短信服务配置 +SMS_ACCESS_KEY_ID=your_sms_access_key_id +SMS_ACCESS_KEY_SECRET=your_sms_access_key_secret +SMS_SIGN_NAME=your_sms_sign_name +SMS_TEMPLATE_CODE=your_sms_template_code +``` + +### 4️⃣ **执行部署** + +#### **Linux/macOS:** + +```bash +# 给脚本执行权限 +chmod +x scripts/deploy.sh + +# 部署指定版本 +./scripts/deploy.sh v1.0.0 + +# 或部署最新版本 +./scripts/deploy.sh +``` + +#### **Windows:** + +```powershell +# 执行部署脚本 +.\scripts\deploy.ps1 -Version "v1.0.0" + +# 或使用Makefile +make docker-build-prod +make docker-push-prod +make prod-up +``` + +## 📊 **部署脚本功能** + +### 🔄 **自动化流程** + +1. **环境检查** - 验证 Docker、docker-compose 等工具 +2. **配置验证** - 检查关键配置项的安全性 +3. **镜像构建** - 构建生产级 Docker 镜像 +4. **镜像推送** - 推送到私有 Registry +5. **服务部署** - 启动所有生产服务 +6. **健康检查** - 验证服务运行状态 +7. **信息展示** - 显示访问地址和管理命令 + +### 🛡 **安全特性** + +- **非 root 用户运行** - 容器内使用专用用户 +- **资源限制** - CPU 和内存使用限制 +- **健康检查** - 自动重启异常服务 +- **网络隔离** - 独立的 Docker 网络 +- **密码保护** - 数据库和 Redis 强制密码 +- **SSL 就绪** - Nginx HTTPS 配置模板 + +## 🎛 **管理命令** + +### **通过 Makefile 管理:** + +```bash +# 构建生产镜像 +make docker-build-prod + +# 推送到Registry +make docker-push-prod + +# 启动生产服务 +make prod-up + +# 停止生产服务 +make prod-down + +# 查看服务状态 +make prod-status + +# 查看实时日志 +make prod-logs +``` + +### **通过 docker-compose 管理:** + +```bash +# 启动所有服务 +docker-compose -f docker-compose.prod.yml up -d + +# 停止所有服务 +docker-compose -f docker-compose.prod.yml down + +# 查看服务状态 +docker-compose -f docker-compose.prod.yml ps + +# 查看日志 +docker-compose -f docker-compose.prod.yml logs -f + +# 重启特定服务 +docker-compose -f docker-compose.prod.yml restart tyapi-app +``` + +## 🌐 **服务访问地址** + +部署成功后,可以通过以下地址访问服务: + +### **核心服务** + +- **API 服务**: `http://your-server:8080` +- **API 文档**: `http://your-server:8080/swagger/index.html` +- **健康检查**: `http://your-server:8080/health` + +### **监控和追踪** + +- **Grafana 仪表盘**: `http://your-server:3000` +- **Prometheus 监控**: `http://your-server:9090` +- **Jaeger 链路追踪**: `http://your-server:16686` + +### **管理工具** + +- **pgAdmin 数据库管理**: `http://your-server:5050` +- **MinIO 对象存储**: `http://your-server:9000` (API) +- **MinIO 控制台**: `http://your-server:9001` (管理界面) + +### **通过 Nginx 代理访问** + +如果启用了 Nginx,也可以通过以下路径访问: + +- **根目录**: `http://your-server/` → 重定向到 API 文档 +- **API 服务**: `http://your-server/api/` +- **Grafana**: `http://your-server/grafana/` +- **Prometheus**: `http://your-server/prometheus/` +- **Jaeger**: `http://your-server/jaeger/` +- **MinIO API**: `http://your-server/minio/` +- **MinIO 控制台**: `http://your-server/minio-console/` +- **pgAdmin**: `http://your-server/pgadmin/` + +## 🔍 **监控和故障排除** + +### **查看日志:** + +```bash +# 查看应用日志 +docker-compose -f docker-compose.prod.yml logs tyapi-app + +# 查看数据库日志 +docker-compose -f docker-compose.prod.yml logs postgres + +# 查看所有服务日志 +docker-compose -f docker-compose.prod.yml logs +``` + +### **健康检查:** + +```bash +# 检查服务状态 +curl -f http://localhost:8080/health + +# 检查Jaeger +curl -f http://localhost:16686 + +# 查看容器状态 +docker ps +``` + +### **常见问题:** + +1. **镜像拉取失败** + + ```bash + # 检查Registry连接 + docker pull docker-registry.tianyuanapi.com/tyapi-server:latest + ``` + +2. **数据库连接失败** + + ```bash + # 检查数据库配置 + docker-compose -f docker-compose.prod.yml logs postgres + ``` + +3. **应用启动失败** + ```bash + # 查看应用日志 + docker-compose -f docker-compose.prod.yml logs tyapi-app + ``` + +## 🔧 **配置优化** + +### **性能调优:** + +1. **数据库优化** - 根据服务器配置调整 PostgreSQL 参数 +2. **Redis 优化** - 配置内存和持久化策略 +3. **应用调优** - 调整连接池大小和超时时间 +4. **Nginx 优化** - 配置缓存和压缩 + +### **扩展配置:** + +1. **HTTPS 配置** - 添加 SSL 证书支持 +2. **域名配置** - 绑定自定义域名 +3. **备份策略** - 配置数据库自动备份 +4. **日志收集** - 集成 ELK 或其他日志系统 + +## 🔄 **版本更新** + +### **零停机更新:** + +```bash +# 构建新版本 +./scripts/deploy.sh v1.1.0 + +# 或渐进式更新 +docker-compose -f docker-compose.prod.yml pull tyapi-app +docker-compose -f docker-compose.prod.yml up -d --no-deps tyapi-app +``` + +### **回滚操作:** + +```bash +# 回滚到指定版本 +docker tag docker-registry.tianyuanapi.com/tyapi-server:v1.0.0 \ + docker-registry.tianyuanapi.com/tyapi-server:latest + +docker-compose -f docker-compose.prod.yml up -d --no-deps tyapi-app +``` + +## 📞 **技术支持** + +如果在部署过程中遇到问题,请: + +1. 检查本文档的故障排除部分 +2. 查看服务日志定位问题 +3. 确认配置文件的正确性 +4. 验证网络和防火墙设置 + +--- + +**部署前请务必:** + +- ✅ 测试配置文件 +- ✅ 备份现有数据 +- ✅ 验证 Registry 访问 +- ✅ 确认服务器资源充足 diff --git a/docs/部署指南.md b/docs/开始指南/部署指南.md similarity index 100% rename from docs/部署指南.md rename to docs/开始指南/部署指南.md diff --git a/docs/开始指南/链路追踪指南.md b/docs/开始指南/链路追踪指南.md new file mode 100644 index 0000000..bb4ad3e --- /dev/null +++ b/docs/开始指南/链路追踪指南.md @@ -0,0 +1,338 @@ +# TYAPI 项目链路追踪指南 + +## 概述 + +本项目使用 **Jaeger** 进行分布式链路追踪,通过 **OpenTelemetry** 标准实现数据收集,并在 **Grafana** 中进行可视化展示。 + +## 架构说明 + +``` +应用程序 -> OpenTelemetry -> Jaeger -> Grafana 可视化 +``` + +- **应用程序**:使用 OpenTelemetry Go SDK 生成链路追踪数据 +- **Jaeger**:收集、存储和查询链路追踪数据 +- **Grafana**:提供链路追踪数据的可视化界面 + +## 快速启动 + +### 1. 启动基础设施服务 + +```bash +# 启动所有服务(包括Jaeger) +docker-compose -f docker-compose.dev.yml up -d + +# 检查服务状态 +docker-compose -f docker-compose.dev.yml ps +``` + +### 2. 验证服务启动 + +- **Jaeger UI**: http://localhost:16686 +- **Grafana**: http://localhost:3000 (admin/Gf7nB3xM9cV6pQ2w) +- **应用程序**: http://localhost:8080 + +### 3. 启动应用程序 + +```bash +# 确保配置正确 +make run +``` + +## 配置说明 + +### 应用配置(config.yaml) + +```yaml +monitoring: + metrics_enabled: true + metrics_port: "9090" + tracing_enabled: true # 启用链路追踪 + tracing_endpoint: "http://localhost:4317" # OTLP gRPC 端点 + sample_rate: 0.1 # 采样率:10% +``` + +### Jaeger 配置 + +- **UI 端口**: 16686 +- **OTLP gRPC**: 4317 +- **OTLP HTTP**: 4318 +- **传统 gRPC**: 14250 +- **传统 HTTP**: 14268 + +### 采样策略 + +项目使用智能采样策略(配置在 `deployments/docker/jaeger-sampling.json`): + +- **默认采样率**: 10% +- **健康检查接口**: 1%(减少噪音) +- **关键业务接口**: 50%(如注册、登录) +- **错误请求**: 100%(所有 4xx 和 5xx 错误) + +#### 错误优先采样 + +系统实现了错误优先采样策略,确保所有出现错误的请求都被 100%采样记录,即使它们不在高采样率的关键业务接口中。这包括: + +- 所有返回 4xx 状态码的客户端错误(如 404、400、403 等) +- 所有返回 5xx 状态码的服务器错误(如 500、503 等) +- 所有抛出异常的数据库操作 +- 所有失败的缓存操作 +- 所有失败的外部 API 调用 + +这种策略确保了在出现问题时,相关的链路追踪数据始终可用,便于问题排查和根因分析。 + +## 使用指南 + +### 在 Grafana 中查看链路追踪 + +1. **访问 Grafana**: http://localhost:3000 +2. **登录**: admin / Gf7nB3xM9cV6pQ2w +3. **导航**: Dashboard → TYAPI 链路追踪监控 +4. **数据源**: + - Jaeger 数据源已自动配置 + - URL: http://jaeger:16686 + +### 在 Jaeger UI 中查看链路追踪 + +1. **访问 Jaeger**: http://localhost:16686 +2. **选择服务**: TYAPI Server +3. **查询追踪**: + - 按时间范围筛选 + - 按操作类型筛选 + - 按标签筛选 + - 按错误状态筛选(使用标签`error=true`) + +### 生成测试数据 + +```bash +# 注册用户(会生成链路追踪数据) +curl -X POST http://localhost:8080/api/v1/users/send-sms \ + -H "Content-Type: application/json" \ + -d '{"phone": "13800138000"}' + +# 用户注册 +curl -X POST http://localhost:8080/api/v1/users/register \ + -H "Content-Type: application/json" \ + -d '{ + "phone": "13800138000", + "password": "Test123456", + "sms_code": "123456" + }' + +# 用户登录 +curl -X POST http://localhost:8080/api/v1/users/login \ + -H "Content-Type: application/json" \ + -d '{ + "phone": "13800138000", + "password": "Test123456" + }' + +# 生成错误请求(测试错误采样) +curl -X GET http://localhost:8080/api/v1/not-exist-path +``` + +## 链路追踪功能特性 + +### 自动追踪的操作 + +1. **HTTP 请求**: 所有入站 HTTP 请求 +2. **数据库查询**: GORM 操作 +3. **缓存操作**: Redis 读写 +4. **外部调用**: 短信服务等 +5. **业务逻辑**: 用户注册、登录等 + +### 追踪数据包含的信息 + +- **请求信息**: URL、HTTP 方法、状态码 +- **时间信息**: 开始时间、持续时间 +- **错误信息**: 异常堆栈和错误消息 +- **上下文信息**: TraceID、SpanID +- **自定义标签**: 服务名、操作类型等 + +### TraceID 传播 + +应用程序会在 HTTP 响应头中返回 TraceID: + +``` +X-Trace-ID: 4bf92f3577b34da6a3ce929d0e0e4736 +``` + +通过这个 ID,可以在日志系统和 Jaeger UI 中关联同一请求的所有信息。 + +## 错误追踪与分析 + +### 错误链路的查询 + +在 Jaeger UI 中,可以通过以下方式查询错误链路: + +1. 在查询界面选择"Tags"标签 +2. 添加条件:`error=true`或`operation.type=error` +3. 点击"Find Traces"按钮 + +这将显示所有被标记为错误的链路追踪数据,包括: + +- 所有 HTTP 4xx/5xx 错误 +- 所有数据库操作错误 +- 所有缓存操作错误 +- 所有外部 API 调用错误 + +### 错误根因分析 + +链路追踪系统记录了错误发生的完整上下文,包括: + +- 错误发生的具体操作 +- 错误的详细信息和堆栈 +- 错误发生前的所有操作序列 +- 相关的请求参数和环境信息 + +通过这些信息,可以快速定位问题根源,而不需要在多个日志文件中搜索。 + +## 性能优化建议 + +### 采样率配置 + +- **开发环境**: 10-50%(便于调试) +- **测试环境**: 5-10% +- **生产环境**: 1-5%(减少性能影响) +- **错误请求**: 始终保持 100%(所有环境) + +### 批处理配置 + +生产环境建议使用批处理导出器: + +```yaml +monitoring: + tracing_enabled: true + tracing_endpoint: "http://jaeger:4317" + sample_rate: 0.01 # 生产环境1%采样率 +``` + +## 故障排除 + +### 常见问题 + +1. **链路追踪数据未显示** + + - 检查应用配置中 `tracing_enabled: true` + - 确认 Jaeger 服务正常运行 + - 检查网络连接和端口 + +2. **Grafana 无法连接 Jaeger** + + - 确认 Jaeger 数据源配置正确 + - 检查容器网络连接 + - 验证 Jaeger UI 可访问 + +3. **性能影响过大** + + - 降低采样率 + - 检查批处理配置 + - 监控内存和 CPU 使用率 + +4. **错误请求未被 100%采样** + - 检查 Jaeger 采样配置中是否包含`"operation": "error"`的配置 + - 确认中间件正确设置了错误标记 + - 验证错误处理逻辑是否正确调用了`SetSpanError`方法 + +### 调试命令 + +```bash +# 检查Jaeger健康状态 +curl http://localhost:14269/health + +# 检查容器日志 +docker logs tyapi-jaeger + +# 检查应用追踪配置 +curl http://localhost:8080/health +``` + +## 监控仪表板 + +### 默认仪表板 + +项目提供了预配置的 Grafana 仪表板: + +- **TYAPI 链路追踪监控**: 展示追踪概览和关键指标 +- **HTTP 请求分析**: 请求速率和延迟分布 +- **服务依赖图**: 服务间调用关系 +- **错误分析**: 错误率和错误类型分布 + +### 自定义仪表板 + +可以根据业务需求创建自定义仪表板: + +1. 在 Grafana 中创建新仪表板 +2. 添加 Jaeger 查询面板 +3. 配置告警规则 +4. 导出仪表板配置 + +## 生产环境部署 + +### 环境变量配置 + +```bash +export JAEGER_ENDPOINT="http://jaeger:4317" +export TRACING_SAMPLE_RATE="0.01" +``` + +### Kubernetes 部署 + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: app-config +data: + config.yaml: | + monitoring: + tracing_enabled: true + tracing_endpoint: "http://jaeger-collector:4317" + sample_rate: 0.01 +``` + +## 扩展功能 + +### 自定义追踪 + +```go +// 在业务代码中添加自定义追踪 +ctx, span := tracer.StartSpan(ctx, "custom-operation") +defer span.End() + +// 添加自定义属性 +tracer.AddSpanAttributes(span, + attribute.String("user.id", userID), + attribute.String("operation.type", "business"), +) + +// 记录错误 +if err != nil { + tracer.SetSpanError(span, err) +} +``` + +### 业务指标集成 + +链路追踪数据可以与业务指标结合: + +- 用户行为分析 +- 性能瓶颈定位 +- 错误率监控 +- 服务依赖分析 + +## 最佳实践 + +1. **合理设置采样率**: 平衡数据完整性和性能影响 +2. **添加有意义的标签**: 便于后续查询和分析 +3. **处理敏感信息**: 避免在追踪数据中记录密码等敏感信息 +4. **监控存储空间**: 定期清理过期的追踪数据 +5. **设置告警规则**: 对异常追踪模式设置告警 +6. **错误优先采样**: 确保所有错误请求都被记录,无论采样率如何 +7. **关联日志系统**: 在日志中包含 TraceID,便于关联查询 + +## 参考资料 + +- [OpenTelemetry Go 文档](https://opentelemetry.io/docs/instrumentation/go/) +- [Jaeger 官方文档](https://www.jaegertracing.io/docs/) +- [Grafana Jaeger 数据源](https://grafana.com/docs/grafana/latest/datasources/jaeger/) diff --git a/docs/文档索引.md b/docs/文档索引.md deleted file mode 100644 index 6782e29..0000000 --- a/docs/文档索引.md +++ /dev/null @@ -1,102 +0,0 @@ -# 📚 TYAPI Server 文档中心 - -欢迎使用 TYAPI Server 文档中心!我们已将原本的使用指南拆分为多个专题文档,方便您按需查阅。 - -## 📋 文档导航 - -### 🚀 [快速开始指南](./快速开始指南.md) -- 前置要求 -- 一键启动 -- 验证安装 -- 访问管理界面 - -### 🔧 [环境搭建指南](./环境搭建指南.md) -- 开发环境配置 -- 生产环境配置 -- 服务配置说明 -- 常见配置问题 - -### 👨‍💻 [开发指南](./开发指南.md) -- 项目结构理解 -- 开发流程 -- 测试编写 -- 调试技巧 -- 代码规范 - -### 🌐 [API使用指南](./API使用指南.md) -- 认证机制 -- 用户管理 API -- 响应格式 -- HTTP 状态码 -- API 测试 - -### 🚀 [部署指南](./部署指南.md) -- Docker 部署 -- Kubernetes 部署 -- 云平台部署 -- 负载均衡配置 -- 监控部署 - -### 🔍 [故障排除指南](./故障排除指南.md) -- 常见问题 -- 日志分析 -- 性能问题 -- 紧急响应流程 - -### 📋 [最佳实践指南](./最佳实践指南.md) -- 开发最佳实践 -- 安全最佳实践 -- 性能最佳实践 -- 运维最佳实践 -- 团队协作 - -## 🎯 快速索引 - -### 新手入门 -1. [快速开始指南](./快速开始指南.md) - 5分钟快速体验 -2. [环境搭建指南](./环境搭建指南.md) - 配置开发环境 -3. [开发指南](./开发指南.md) - 开始第一个功能 - -### 日常开发 -- [API使用指南](./API使用指南.md) - API 调用参考 -- [开发指南](./开发指南.md) - 开发流程和规范 -- [故障排除指南](./故障排除指南.md) - 解决常见问题 - -### 生产部署 -- [部署指南](./部署指南.md) - 生产环境部署 -- [最佳实践指南](./最佳实践指南.md) - 运维最佳实践 -- [故障排除指南](./故障排除指南.md) - 生产问题排查 - -## 🔗 相关文档 - -### 技术文档 -- [架构文档](./ARCHITECTURE.md) - 系统架构设计 -- [API 规范](http://localhost:8080/swagger/) - 在线 API 文档 - -### 项目文档 -- [README](../README.md) - 项目介绍 -- [更新日志](../CHANGELOG.md) - 版本变更记录 - -## 📞 获取帮助 - -### 在线资源 -- **Swagger UI**: http://localhost:8080/swagger/ -- **健康检查**: http://localhost:8080/api/v1/health -- **监控面板**: http://localhost:3000 (Grafana) - -### 社区支持 -- **GitHub Issues**: 提交问题和建议 -- **Wiki**: 查看详细技术文档 -- **讨论区**: 参与技术讨论 - -## 🔄 文档更新 - -本文档会持续更新,如果您发现任何问题或有改进建议,请: - -1. 提交 GitHub Issue -2. 发起 Pull Request -3. 联系维护团队 - ---- - -**提示**:建议将此页面加入书签,方便随时查阅相关文档。 \ No newline at end of file diff --git a/env.example b/env.example deleted file mode 100644 index 4722f9f..0000000 --- a/env.example +++ /dev/null @@ -1,137 +0,0 @@ -# =========================================== -# 服务配置 -# =========================================== -SERVER_PORT=8080 -SERVER_MODE=debug -SERVER_HOST=0.0.0.0 -SERVER_READ_TIMEOUT=30s -SERVER_WRITE_TIMEOUT=30s -SERVER_IDLE_TIMEOUT=120s - -# =========================================== -# 数据库配置 (PostgreSQL) -# =========================================== -DB_HOST=localhost -DB_PORT=5432 -DB_USER=postgres -DB_PASSWORD=password -DB_NAME=tyapi_dev -DB_SSLMODE=disable -DB_TIMEZONE=Asia/Shanghai -DB_MAX_OPEN_CONNS=100 -DB_MAX_IDLE_CONNS=10 -DB_CONN_MAX_LIFETIME=300s - -# =========================================== -# Redis配置 -# =========================================== -REDIS_HOST=localhost -REDIS_PORT=6379 -REDIS_PASSWORD= -REDIS_DB=0 -REDIS_POOL_SIZE=10 -REDIS_MIN_IDLE_CONNS=5 -REDIS_MAX_RETRIES=3 -REDIS_DIAL_TIMEOUT=5s -REDIS_READ_TIMEOUT=3s -REDIS_WRITE_TIMEOUT=3s - -# =========================================== -# 缓存配置 -# =========================================== -CACHE_DEFAULT_TTL=300s -CACHE_CLEANUP_INTERVAL=600s -CACHE_MAX_SIZE=1000 - -# =========================================== -# 日志配置 -# =========================================== -LOG_LEVEL=info -LOG_FORMAT=json -LOG_OUTPUT=stdout -LOG_FILE_PATH=logs/app.log -LOG_MAX_SIZE=100 -LOG_MAX_BACKUPS=5 -LOG_MAX_AGE=30 -LOG_COMPRESS=true - -# =========================================== -# JWT 认证配置 -# =========================================== -JWT_SECRET=your-super-secret-jwt-key-change-this-in-production -JWT_EXPIRES_IN=24h -JWT_REFRESH_EXPIRES_IN=168h - -# =========================================== -# 限流配置 -# =========================================== -RATE_LIMIT_REQUESTS=100 -RATE_LIMIT_WINDOW=60s -RATE_LIMIT_BURST=50 - -# =========================================== -# 监控和追踪配置 -# =========================================== -METRICS_ENABLED=true -METRICS_PORT=9090 -TRACING_ENABLED=true -TRACING_ENDPOINT=http://localhost:14268/api/traces -TRACING_SAMPLE_RATE=0.1 - -# =========================================== -# 健康检查配置 -# =========================================== -HEALTH_CHECK_ENABLED=true -HEALTH_CHECK_INTERVAL=30s -HEALTH_CHECK_TIMEOUT=10s - -# =========================================== -# 容错配置 -# =========================================== -CIRCUIT_BREAKER_ENABLED=true -CIRCUIT_BREAKER_THRESHOLD=5 -CIRCUIT_BREAKER_TIMEOUT=60s -RETRY_MAX_ATTEMPTS=3 -RETRY_INITIAL_DELAY=100ms -RETRY_MAX_DELAY=2s - -# =========================================== -# 开发模式配置 -# =========================================== -DEBUG=true -ENABLE_PROFILER=true -ENABLE_CORS=true -CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:3001 -CORS_ALLOWED_METHODS=GET,POST,PUT,PATCH,DELETE,OPTIONS -CORS_ALLOWED_HEADERS=Origin,Content-Type,Accept,Authorization,X-Requested-With - -# =========================================== -# 环境标识 -# =========================================== -ENV=development -APP_NAME=TYAPI Server -APP_VERSION=1.0.0 - -# =========================================== -# 监控和追踪配置 -# =========================================== -JAEGER_ENDPOINT=http://localhost:14268/api/traces - -# =========================================== -# 健康检查配置 -# =========================================== -HEALTH_ENABLED=true -HEALTH_INTERVAL=30s -HEALTH_TIMEOUT=10s - -# =========================================== -# 配置文件路径 -# =========================================== -CONFIG_FILE=config.yaml - -# =========================================== -# Go 环境变量 -# =========================================== -GO_VERSION=go1.23.4 -GOOS=linux -GOARCH=amd64 \ No newline at end of file diff --git a/go.mod b/go.mod index d6aaf1c..2618d26 100644 --- a/go.mod +++ b/go.mod @@ -3,13 +3,24 @@ module tyapi-server go 1.23.4 require ( + github.com/aliyun/alibaba-cloud-sdk-go v1.63.107 github.com/gin-contrib/cors v1.7.6 github.com/gin-gonic/gin v1.10.1 + github.com/go-playground/locales v0.14.1 + github.com/go-playground/universal-translator v0.18.1 github.com/go-playground/validator/v10 v10.26.0 github.com/golang-jwt/jwt/v5 v5.2.2 github.com/google/uuid v1.6.0 + github.com/prometheus/client_golang v1.22.0 github.com/redis/go-redis/v9 v9.11.0 github.com/spf13/viper v1.20.1 + github.com/swaggo/files v1.0.1 + github.com/swaggo/gin-swagger v1.6.0 + github.com/swaggo/swag v1.16.4 + go.opentelemetry.io/otel v1.37.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 + go.opentelemetry.io/otel/sdk v1.37.0 + go.opentelemetry.io/otel/trace v1.37.0 go.uber.org/fx v1.24.0 go.uber.org/zap v1.27.0 golang.org/x/crypto v0.39.0 @@ -19,31 +30,49 @@ require ( ) require ( + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/PuerkitoBio/purell v1.1.1 // indirect + github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect + github.com/beorn7/perks v1.0.1 // indirect github.com/bytedance/sonic v1.13.3 // indirect github.com/bytedance/sonic/loader v0.2.4 // indirect + github.com/cenkalti/backoff/v5 v5.0.2 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudwego/base64x v0.1.5 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/gabriel-vasile/mimetype v1.4.9 // indirect github.com/gin-contrib/sse v1.1.0 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.19.6 // indirect + github.com/go-openapi/spec v0.20.4 // indirect + github.com/go-openapi/swag v0.19.15 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/goccy/go-json v0.10.5 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgx/v5 v5.6.0 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/leodido/go-urn v1.4.0 // indirect + github.com/mailru/easyjson v0.7.6 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.62.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.12.0 // indirect @@ -52,6 +81,10 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect + go.opentelemetry.io/otel/metric v1.37.0 // indirect + go.opentelemetry.io/proto/otlp v1.7.0 // indirect go.uber.org/dig v1.19.0 // indirect go.uber.org/multierr v1.10.0 // indirect golang.org/x/arch v0.18.0 // indirect @@ -59,6 +92,12 @@ require ( golang.org/x/sync v0.15.0 // indirect golang.org/x/sys v0.33.0 // indirect golang.org/x/text v0.26.0 // indirect + golang.org/x/tools v0.33.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/grpc v1.73.0 // indirect google.golang.org/protobuf v1.36.6 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 05f3452..50d9b56 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,17 @@ +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= +github.com/aliyun/alibaba-cloud-sdk-go v1.63.107 h1:qagvUyrgOnBIlVRQWOyCZGVKUIYbMBdGdJ104vBpRFU= +github.com/aliyun/alibaba-cloud-sdk-go v1.63.107/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= @@ -7,16 +21,20 @@ github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1 github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= +github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= @@ -25,10 +43,28 @@ github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBv github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY= github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk= +github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= +github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= +github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= +github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -41,13 +77,20 @@ github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIx github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= @@ -60,18 +103,36 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -79,14 +140,29 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A= +github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/redis/go-redis/v9 v9.11.0 h1:E3S08Gl/nJNn5vkxd2i78wZxWAPNZgUNTp8WIJUAiIs= github.com/redis/go-redis/v9 v9.11.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= @@ -103,6 +179,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -111,10 +188,41 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= +github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= +github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M= +github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo= +github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A= +github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= +github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= +github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= +github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 h1:EtFWSnwW9hGObjkIdmlnWSydO+Qs8OwzfzXLUPg4xOc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h1:QjUEoiGCPkvFZ/MjK6ZZfNOS6mfVEVKYE99dFhuN2LI= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= +go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os= +go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4= go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg= @@ -127,25 +235,97 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc= golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= +golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= +gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= +google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= @@ -153,3 +333,4 @@ gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXD gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs= gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/internal/app/app.go b/internal/app/app.go index 1bb3c23..1eae4b7 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -50,6 +50,16 @@ func (a *Application) Run() error { // 打印启动信息 a.printBanner() + // 检查是否需要自动迁移 + if a.config.Database.AutoMigrate { + a.logger.Info("Auto migration is enabled, running database migrations...") + if err := a.RunMigrations(); err != nil { + a.logger.Error("Auto migration failed", zap.Error(err)) + return fmt.Errorf("auto migration failed: %w", err) + } + a.logger.Info("Auto migration completed successfully") + } + // 启动容器 a.logger.Info("Starting application container...") if err := a.container.Start(); err != nil { @@ -92,10 +102,10 @@ func (a *Application) RunMigrations() error { func (a *Application) printBanner() { banner := fmt.Sprintf(` ╔══════════════════════════════════════════════════════════════╗ - ║ %s ║ - ║ Version: %s ║ - ║ Environment: %s ║ - ║ Port: %s ║ + ║ %s + ║ Version: %s + ║ Environment: %s + ║ Port: %s ╚══════════════════════════════════════════════════════════════╝ `, a.config.App.Name, @@ -151,9 +161,20 @@ func (a *Application) createDatabaseConnection() (*gorm.DB, error) { // autoMigrate 自动迁移 func (a *Application) autoMigrate(db *gorm.DB) error { + // 如果需要删除某些表,可以在这里手动删除 + // 注意:这会永久删除数据,请谨慎使用! + /* + // 删除不再需要的表(示例,请根据实际情况使用) + if err := db.Migrator().DropTable(&entities.FavoriteItem{}); err != nil { + a.logger.Warn("Failed to drop table", zap.Error(err)) + // 继续执行,不阻断迁移 + } + */ + // 迁移用户相关表 return db.AutoMigrate( &entities.User{}, + &entities.SMSCode{}, // 后续可以添加其他实体 ) } diff --git a/internal/config/config.go b/internal/config/config.go index 4efc283..ec7c371 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -12,6 +12,7 @@ type Config struct { Cache CacheConfig `mapstructure:"cache"` Logger LoggerConfig `mapstructure:"logger"` JWT JWTConfig `mapstructure:"jwt"` + SMS SMSConfig `mapstructure:"sms"` RateLimit RateLimitConfig `mapstructure:"ratelimit"` Monitoring MonitoringConfig `mapstructure:"monitoring"` Health HealthConfig `mapstructure:"health"` @@ -42,6 +43,7 @@ type DatabaseConfig struct { MaxOpenConns int `mapstructure:"max_open_conns"` MaxIdleConns int `mapstructure:"max_idle_conns"` ConnMaxLifetime time.Duration `mapstructure:"conn_max_lifetime"` + AutoMigrate bool `mapstructure:"auto_migrate"` } // RedisConfig Redis配置 @@ -67,14 +69,15 @@ type CacheConfig struct { // LoggerConfig 日志配置 type LoggerConfig struct { - Level string `mapstructure:"level"` - Format string `mapstructure:"format"` - Output string `mapstructure:"output"` - FilePath string `mapstructure:"file_path"` - MaxSize int `mapstructure:"max_size"` - MaxBackups int `mapstructure:"max_backups"` - MaxAge int `mapstructure:"max_age"` - Compress bool `mapstructure:"compress"` + Level string `mapstructure:"level"` + Format string `mapstructure:"format"` + Output string `mapstructure:"output"` + FilePath string `mapstructure:"file_path"` + MaxSize int `mapstructure:"max_size"` + MaxBackups int `mapstructure:"max_backups"` + MaxAge int `mapstructure:"max_age"` + Compress bool `mapstructure:"compress"` + UseColor bool `mapstructure:"use_color"` } // JWTConfig JWT配置 @@ -119,12 +122,12 @@ type ResilienceConfig struct { // DevelopmentConfig 开发配置 type DevelopmentConfig struct { - Debug bool `mapstructure:"debug"` - EnableProfiler bool `mapstructure:"enable_profiler"` - EnableCors bool `mapstructure:"enable_cors"` - CorsOrigins string `mapstructure:"cors_allowed_origins"` - CorsMethods string `mapstructure:"cors_allowed_methods"` - CorsHeaders string `mapstructure:"cors_allowed_headers"` + Debug bool `mapstructure:"debug"` + EnableProfiler bool `mapstructure:"enable_profiler"` + EnableCors bool `mapstructure:"enable_cors"` + CorsOrigins string `mapstructure:"cors_allowed_origins"` + CorsMethods string `mapstructure:"cors_allowed_methods"` + CorsHeaders string `mapstructure:"cors_allowed_headers"` } // AppConfig 应用程序配置 @@ -134,6 +137,26 @@ type AppConfig struct { Env string `mapstructure:"env"` } +// SMSConfig 短信配置 +type SMSConfig struct { + AccessKeyID string `mapstructure:"access_key_id"` + AccessKeySecret string `mapstructure:"access_key_secret"` + EndpointURL string `mapstructure:"endpoint_url"` + SignName string `mapstructure:"sign_name"` + TemplateCode string `mapstructure:"template_code"` + CodeLength int `mapstructure:"code_length"` + ExpireTime time.Duration `mapstructure:"expire_time"` + RateLimit SMSRateLimit `mapstructure:"rate_limit"` + MockEnabled bool `mapstructure:"mock_enabled"` // 是否启用模拟短信服务 +} + +// SMSRateLimit 短信限流配置 +type SMSRateLimit struct { + DailyLimit int `mapstructure:"daily_limit"` // 每日发送限制 + HourlyLimit int `mapstructure:"hourly_limit"` // 每小时发送限制 + MinInterval time.Duration `mapstructure:"min_interval"` // 最小发送间隔 +} + // GetDSN 获取数据库DSN连接字符串 func (d DatabaseConfig) GetDSN() string { return "host=" + d.Host + @@ -163,4 +186,4 @@ func (a AppConfig) IsDevelopment() bool { // IsStaging 检查是否为测试环境 func (a AppConfig) IsStaging() bool { return a.Env == "staging" -} \ No newline at end of file +} diff --git a/internal/config/loader.go b/internal/config/loader.go index 7b1ff43..786b8f2 100644 --- a/internal/config/loader.go +++ b/internal/config/loader.go @@ -3,6 +3,7 @@ package config import ( "fmt" "os" + "path/filepath" "strings" "time" @@ -11,228 +12,173 @@ import ( // LoadConfig 加载应用程序配置 func LoadConfig() (*Config, error) { - // 设置配置文件名和路径 - viper.SetConfigName("config") - viper.SetConfigType("yaml") - viper.AddConfigPath(".") - viper.AddConfigPath("./configs") - viper.AddConfigPath("$HOME/.tyapi") + // 1️⃣ 获取环境变量决定配置文件 + env := getEnvironment() + fmt.Printf("🔧 当前运行环境: %s\n", env) - // 设置环境变量前缀 - viper.SetEnvPrefix("") - viper.AutomaticEnv() + // 2️⃣ 加载基础配置文件 + baseConfig := viper.New() + baseConfig.SetConfigName("config") + baseConfig.SetConfigType("yaml") + baseConfig.AddConfigPath(".") + baseConfig.AddConfigPath("./configs") + baseConfig.AddConfigPath("$HOME/.tyapi") - // 配置环境变量键名映射 - setupEnvKeyMapping() - - // 设置默认值 - setDefaults() - - // 尝试读取配置文件(可选) - if err := viper.ReadInConfig(); err != nil { + // 读取基础配置文件 + if err := baseConfig.ReadInConfig(); err != nil { if _, ok := err.(viper.ConfigFileNotFoundError); !ok { - return nil, fmt.Errorf("读取配置文件失败: %w", err) + return nil, fmt.Errorf("读取基础配置文件失败: %w", err) } - // 配置文件不存在时使用环境变量和默认值 + return nil, fmt.Errorf("未找到 config.yaml 文件,请确保配置文件存在") + } + fmt.Printf("✅ 已加载配置文件: %s\n", baseConfig.ConfigFileUsed()) + + // 3️⃣ 加载环境特定配置文件 + envConfigFile := findEnvConfigFile(env) + if envConfigFile != "" { + // 创建一个新的viper实例来读取环境配置 + envConfig := viper.New() + envConfig.SetConfigFile(envConfigFile) + + if err := envConfig.ReadInConfig(); err != nil { + fmt.Printf("⚠️ 环境配置文件加载警告: %v\n", err) + } else { + fmt.Printf("✅ 已加载环境配置: %s\n", envConfigFile) + + // 将环境配置合并到基础配置中 + if err := mergeConfigs(baseConfig, envConfig.AllSettings()); err != nil { + return nil, fmt.Errorf("合并配置失败: %w", err) + } + } + } else { + fmt.Printf("⚠️ 未找到环境配置文件 env.%s.yaml\n", env) } + // 4️⃣ 设置环境变量前缀和自动读取 + baseConfig.SetEnvPrefix("") + baseConfig.AutomaticEnv() + baseConfig.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + + // 5️⃣ 解析配置到结构体 var config Config - if err := viper.Unmarshal(&config); err != nil { + if err := baseConfig.Unmarshal(&config); err != nil { return nil, fmt.Errorf("解析配置失败: %w", err) } - // 验证配置 + // 6️⃣ 验证配置 if err := validateConfig(&config); err != nil { return nil, fmt.Errorf("配置验证失败: %w", err) } + // 7️⃣ 输出配置摘要 + printConfigSummary(&config, env) + return &config, nil } -// setupEnvKeyMapping 设置环境变量到配置键的映射 -func setupEnvKeyMapping() { - // 服务器配置 - viper.BindEnv("server.port", "SERVER_PORT") - viper.BindEnv("server.mode", "SERVER_MODE") - viper.BindEnv("server.host", "SERVER_HOST") - viper.BindEnv("server.read_timeout", "SERVER_READ_TIMEOUT") - viper.BindEnv("server.write_timeout", "SERVER_WRITE_TIMEOUT") - viper.BindEnv("server.idle_timeout", "SERVER_IDLE_TIMEOUT") +// mergeConfigs 递归合并配置 +func mergeConfigs(baseConfig *viper.Viper, overrideSettings map[string]interface{}) error { + for key, val := range overrideSettings { + // 如果值是一个嵌套的map,则递归合并 + if subMap, ok := val.(map[string]interface{}); ok { + // 创建子键路径 + subKey := key - // 数据库配置 - viper.BindEnv("database.host", "DB_HOST") - viper.BindEnv("database.port", "DB_PORT") - viper.BindEnv("database.user", "DB_USER") - viper.BindEnv("database.password", "DB_PASSWORD") - viper.BindEnv("database.name", "DB_NAME") - viper.BindEnv("database.sslmode", "DB_SSLMODE") - viper.BindEnv("database.timezone", "DB_TIMEZONE") - viper.BindEnv("database.max_open_conns", "DB_MAX_OPEN_CONNS") - viper.BindEnv("database.max_idle_conns", "DB_MAX_IDLE_CONNS") - viper.BindEnv("database.conn_max_lifetime", "DB_CONN_MAX_LIFETIME") - - // Redis配置 - viper.BindEnv("redis.host", "REDIS_HOST") - viper.BindEnv("redis.port", "REDIS_PORT") - viper.BindEnv("redis.password", "REDIS_PASSWORD") - viper.BindEnv("redis.db", "REDIS_DB") - viper.BindEnv("redis.pool_size", "REDIS_POOL_SIZE") - viper.BindEnv("redis.min_idle_conns", "REDIS_MIN_IDLE_CONNS") - viper.BindEnv("redis.max_retries", "REDIS_MAX_RETRIES") - viper.BindEnv("redis.dial_timeout", "REDIS_DIAL_TIMEOUT") - viper.BindEnv("redis.read_timeout", "REDIS_READ_TIMEOUT") - viper.BindEnv("redis.write_timeout", "REDIS_WRITE_TIMEOUT") - - // 缓存配置 - viper.BindEnv("cache.default_ttl", "CACHE_DEFAULT_TTL") - viper.BindEnv("cache.cleanup_interval", "CACHE_CLEANUP_INTERVAL") - viper.BindEnv("cache.max_size", "CACHE_MAX_SIZE") - - // 日志配置 - viper.BindEnv("logger.level", "LOG_LEVEL") - viper.BindEnv("logger.format", "LOG_FORMAT") - viper.BindEnv("logger.output", "LOG_OUTPUT") - viper.BindEnv("logger.file_path", "LOG_FILE_PATH") - viper.BindEnv("logger.max_size", "LOG_MAX_SIZE") - viper.BindEnv("logger.max_backups", "LOG_MAX_BACKUPS") - viper.BindEnv("logger.max_age", "LOG_MAX_AGE") - viper.BindEnv("logger.compress", "LOG_COMPRESS") - - // JWT配置 - viper.BindEnv("jwt.secret", "JWT_SECRET") - viper.BindEnv("jwt.expires_in", "JWT_EXPIRES_IN") - viper.BindEnv("jwt.refresh_expires_in", "JWT_REFRESH_EXPIRES_IN") - - // 限流配置 - viper.BindEnv("ratelimit.requests", "RATE_LIMIT_REQUESTS") - viper.BindEnv("ratelimit.window", "RATE_LIMIT_WINDOW") - viper.BindEnv("ratelimit.burst", "RATE_LIMIT_BURST") - - // 监控配置 - viper.BindEnv("monitoring.metrics_enabled", "METRICS_ENABLED") - viper.BindEnv("monitoring.metrics_port", "METRICS_PORT") - viper.BindEnv("monitoring.tracing_enabled", "TRACING_ENABLED") - viper.BindEnv("monitoring.tracing_endpoint", "TRACING_ENDPOINT") - viper.BindEnv("monitoring.sample_rate", "TRACING_SAMPLE_RATE") - - // 健康检查配置 - viper.BindEnv("health.enabled", "HEALTH_CHECK_ENABLED") - viper.BindEnv("health.interval", "HEALTH_CHECK_INTERVAL") - viper.BindEnv("health.timeout", "HEALTH_CHECK_TIMEOUT") - - // 容错配置 - viper.BindEnv("resilience.circuit_breaker_enabled", "CIRCUIT_BREAKER_ENABLED") - viper.BindEnv("resilience.circuit_breaker_threshold", "CIRCUIT_BREAKER_THRESHOLD") - viper.BindEnv("resilience.circuit_breaker_timeout", "CIRCUIT_BREAKER_TIMEOUT") - viper.BindEnv("resilience.retry_max_attempts", "RETRY_MAX_ATTEMPTS") - viper.BindEnv("resilience.retry_initial_delay", "RETRY_INITIAL_DELAY") - viper.BindEnv("resilience.retry_max_delay", "RETRY_MAX_DELAY") - - // 开发配置 - viper.BindEnv("development.debug", "DEBUG") - viper.BindEnv("development.enable_profiler", "ENABLE_PROFILER") - viper.BindEnv("development.enable_cors", "ENABLE_CORS") - viper.BindEnv("development.cors_allowed_origins", "CORS_ALLOWED_ORIGINS") - viper.BindEnv("development.cors_allowed_methods", "CORS_ALLOWED_METHODS") - viper.BindEnv("development.cors_allowed_headers", "CORS_ALLOWED_HEADERS") - - // 应用程序配置 - viper.BindEnv("app.name", "APP_NAME") - viper.BindEnv("app.version", "APP_VERSION") - viper.BindEnv("app.env", "ENV") + // 递归合并子配置 + for subK, subV := range subMap { + fullKey := fmt.Sprintf("%s.%s", subKey, subK) + baseConfig.Set(fullKey, subV) + } + } else { + // 直接设置值 + baseConfig.Set(key, val) + } + } + return nil } -// setDefaults 设置默认配置值 -func setDefaults() { - // 服务器默认值 - viper.SetDefault("server.port", "8080") - viper.SetDefault("server.mode", "debug") - viper.SetDefault("server.host", "0.0.0.0") - viper.SetDefault("server.read_timeout", "30s") - viper.SetDefault("server.write_timeout", "30s") - viper.SetDefault("server.idle_timeout", "120s") +// findEnvConfigFile 查找环境特定的配置文件 +func findEnvConfigFile(env string) string { + // 尝试查找的配置文件路径 + possiblePaths := []string{ + fmt.Sprintf("configs/env.%s.yaml", env), + fmt.Sprintf("configs/env.%s.yml", env), + fmt.Sprintf("configs/env.%s", env), + fmt.Sprintf("env.%s.yaml", env), + fmt.Sprintf("env.%s.yml", env), + fmt.Sprintf("env.%s", env), + } - // 数据库默认值 - viper.SetDefault("database.host", "localhost") - viper.SetDefault("database.port", "5432") - viper.SetDefault("database.user", "postgres") - viper.SetDefault("database.password", "password") - viper.SetDefault("database.name", "tyapi_db") - viper.SetDefault("database.sslmode", "disable") - viper.SetDefault("database.timezone", "Asia/Shanghai") - viper.SetDefault("database.max_open_conns", 100) - viper.SetDefault("database.max_idle_conns", 10) - viper.SetDefault("database.conn_max_lifetime", "300s") + // 如果有自定义环境文件路径 + if customEnvFile := os.Getenv("ENV_FILE"); customEnvFile != "" { + possiblePaths = append([]string{customEnvFile}, possiblePaths...) + } - // Redis默认值 - viper.SetDefault("redis.host", "localhost") - viper.SetDefault("redis.port", "6379") - viper.SetDefault("redis.password", "") - viper.SetDefault("redis.db", 0) - viper.SetDefault("redis.pool_size", 10) - viper.SetDefault("redis.min_idle_conns", 5) - viper.SetDefault("redis.max_retries", 3) - viper.SetDefault("redis.dial_timeout", "5s") - viper.SetDefault("redis.read_timeout", "3s") - viper.SetDefault("redis.write_timeout", "3s") + for _, path := range possiblePaths { + if _, err := os.Stat(path); err == nil { + absPath, _ := filepath.Abs(path) + return absPath + } + } - // 缓存默认值 - viper.SetDefault("cache.default_ttl", "300s") - viper.SetDefault("cache.cleanup_interval", "600s") - viper.SetDefault("cache.max_size", 1000) + return "" +} - // 日志默认值 - viper.SetDefault("logger.level", "info") - viper.SetDefault("logger.format", "json") - viper.SetDefault("logger.output", "stdout") - viper.SetDefault("logger.file_path", "logs/app.log") - viper.SetDefault("logger.max_size", 100) - viper.SetDefault("logger.max_backups", 5) - viper.SetDefault("logger.max_age", 30) - viper.SetDefault("logger.compress", true) +// getEnvironment 获取当前环境 +func getEnvironment() string { + var env string + var source string - // JWT默认值 - viper.SetDefault("jwt.secret", "your-super-secret-jwt-key-change-this-in-production") - viper.SetDefault("jwt.expires_in", "24h") - viper.SetDefault("jwt.refresh_expires_in", "168h") + // 优先级:CONFIG_ENV > ENV > APP_ENV > 默认值 + if env = os.Getenv("CONFIG_ENV"); env != "" { + source = "CONFIG_ENV" + } else if env = os.Getenv("ENV"); env != "" { + source = "ENV" + } else if env = os.Getenv("APP_ENV"); env != "" { + source = "APP_ENV" + } else { + env = "development" + source = "默认值" + } - // 限流默认值 - viper.SetDefault("ratelimit.requests", 100) - viper.SetDefault("ratelimit.window", "60s") - viper.SetDefault("ratelimit.burst", 10) + fmt.Printf("🌍 环境检测: %s (来源: %s)\n", env, source) - // 监控默认值 - viper.SetDefault("monitoring.metrics_enabled", true) - viper.SetDefault("monitoring.metrics_port", "9090") - viper.SetDefault("monitoring.tracing_enabled", false) - viper.SetDefault("monitoring.tracing_endpoint", "http://localhost:14268/api/traces") - viper.SetDefault("monitoring.sample_rate", 0.1) + // 验证环境值 + validEnvs := []string{"development", "production", "testing"} + isValid := false + for _, validEnv := range validEnvs { + if env == validEnv { + isValid = true + break + } + } - // 健康检查默认值 - viper.SetDefault("health.enabled", true) - viper.SetDefault("health.interval", "30s") - viper.SetDefault("health.timeout", "5s") + if !isValid { + fmt.Printf("⚠️ 警告: 未识别的环境 '%s',将使用默认环境 'development'\n", env) + return "development" + } - // 容错默认值 - viper.SetDefault("resilience.circuit_breaker_enabled", true) - viper.SetDefault("resilience.circuit_breaker_threshold", 5) - viper.SetDefault("resilience.circuit_breaker_timeout", "60s") - viper.SetDefault("resilience.retry_max_attempts", 3) - viper.SetDefault("resilience.retry_initial_delay", "100ms") - viper.SetDefault("resilience.retry_max_delay", "2s") + return env +} - // 开发默认值 - viper.SetDefault("development.debug", true) - viper.SetDefault("development.enable_profiler", false) - viper.SetDefault("development.enable_cors", true) - viper.SetDefault("development.cors_allowed_origins", "*") - viper.SetDefault("development.cors_allowed_methods", "GET,POST,PUT,DELETE,OPTIONS") - viper.SetDefault("development.cors_allowed_headers", "*") - - // 应用程序默认值 - viper.SetDefault("app.name", "tyapi-server") - viper.SetDefault("app.version", "1.0.0") - viper.SetDefault("app.env", "development") +// printConfigSummary 打印配置摘要 +func printConfigSummary(config *Config, env string) { + fmt.Printf("\n🔧 配置摘要:\n") + fmt.Printf(" 🌍 环境: %s\n", env) + fmt.Printf(" 📄 配置模板: config.yaml\n") + fmt.Printf(" 📱 应用名称: %s\n", config.App.Name) + fmt.Printf(" 🔖 版本: %s\n", config.App.Version) + fmt.Printf(" 🌐 服务端口: %s\n", config.Server.Port) + fmt.Printf(" 🗄️ 数据库: %s@%s:%s/%s\n", + config.Database.User, + config.Database.Host, + config.Database.Port, + config.Database.Name) + fmt.Printf(" 📊 追踪状态: %v (端点: %s)\n", + config.Monitoring.TracingEnabled, + config.Monitoring.TracingEndpoint) + fmt.Printf(" 📈 采样率: %.1f%%\n", config.Monitoring.SampleRate*100) + fmt.Printf("\n") } // validateConfig 验证配置 diff --git a/internal/container/container.go b/internal/container/container.go index 7ed3475..89405c3 100644 --- a/internal/container/container.go +++ b/internal/container/container.go @@ -3,11 +3,14 @@ package container import ( "context" "fmt" + nethttp "net/http" "time" + "github.com/gin-gonic/gin" "github.com/redis/go-redis/v9" "go.uber.org/fx" "go.uber.org/zap" + "go.uber.org/zap/zapcore" "gorm.io/gorm" "tyapi-server/internal/config" @@ -19,9 +22,15 @@ import ( "tyapi-server/internal/shared/database" "tyapi-server/internal/shared/events" "tyapi-server/internal/shared/health" - "tyapi-server/internal/shared/http" + "tyapi-server/internal/shared/hooks" + sharedhttp "tyapi-server/internal/shared/http" "tyapi-server/internal/shared/interfaces" + "tyapi-server/internal/shared/metrics" "tyapi-server/internal/shared/middleware" + "tyapi-server/internal/shared/resilience" + "tyapi-server/internal/shared/saga" + "tyapi-server/internal/shared/sms" + "tyapi-server/internal/shared/tracing" ) // Container 应用容器 @@ -40,11 +49,24 @@ func NewContainer() *Container { // 基础设施模块 fx.Provide( NewLogger, - NewDatabase, + // 使用带追踪的组件 + NewTracedDatabase, NewRedisClient, - NewRedisCache, + fx.Annotate(NewTracedRedisCache, fx.As(new(interfaces.CacheService))), NewEventBus, NewHealthChecker, + NewSMSService, + ), + + // 高级特性模块 + fx.Provide( + NewTracer, + NewPrometheusMetrics, + NewBusinessMetrics, + NewCircuitBreakerWrapper, + NewRetryerWrapper, + NewSagaManager, + NewHookSystem, ), // HTTP基础组件 @@ -64,14 +86,22 @@ func NewContainer() *Container { NewRequestLoggerMiddleware, NewJWTAuthMiddleware, NewOptionalAuthMiddleware, + NewTracingMiddleware, + NewMetricsMiddleware, + NewTraceIDMiddleware, + NewErrorTrackingMiddleware, + NewRequestBodyLoggerMiddleware, ), // 用户域组件 fx.Provide( NewUserRepository, + NewSMSCodeRepository, + NewSMSCodeService, NewUserService, + // 使用带自动追踪的用户服务 + fx.Annotate(NewTracedUserService, fx.As(new(interfaces.UserService))), NewUserHandler, - NewUserRoutes, ), // 应用生命周期 @@ -101,7 +131,7 @@ func (c *Container) Stop() error { return c.App.Stop(ctx) } -// 基础设施构造函数 +// ================ 基础设施构造函数 ================ // NewLogger 创建日志器 func NewLogger(cfg *config.Config) (*zap.Logger, error) { @@ -110,18 +140,27 @@ func NewLogger(cfg *config.Config) (*zap.Logger, error) { level = zap.NewAtomicLevelAt(zap.InfoLevel) } - config := zap.Config{ - Level: level, - Development: cfg.App.IsDevelopment(), - Encoding: cfg.Logger.Format, - EncoderConfig: zap.NewProductionEncoderConfig(), - OutputPaths: []string{cfg.Logger.Output}, - ErrorOutputPaths: []string{"stderr"}, + var config zap.Config + + if cfg.App.IsDevelopment() { + config = zap.NewDevelopmentConfig() + config.Level = level + config.Encoding = "console" + if cfg.Logger.UseColor { + config.EncoderConfig.EncodeLevel = zapcore.LowercaseColorLevelEncoder + } + } else { + config = zap.NewProductionConfig() + config.Level = level + config.Encoding = cfg.Logger.Format + if config.Encoding == "" { + config.Encoding = "json" + } } - if cfg.Logger.Format == "" { - config.Encoding = "json" - } + config.OutputPaths = []string{cfg.Logger.Output} + config.ErrorOutputPaths = []string{"stderr"} + if cfg.Logger.Output == "" { config.OutputPaths = []string{"stdout"} } @@ -152,6 +191,25 @@ func NewDatabase(cfg *config.Config, logger *zap.Logger) (*gorm.DB, error) { return db.DB, nil } +// NewTracedDatabase 创建带追踪的数据库连接 +func NewTracedDatabase(cfg *config.Config, tracer *tracing.Tracer, logger *zap.Logger) (*gorm.DB, error) { + // 先创建基础数据库连接 + db, err := NewDatabase(cfg, logger) + if err != nil { + return nil, err + } + + // 创建并注册GORM追踪插件 + tracingPlugin := tracing.NewGormTracingPlugin(tracer, logger) + if err := db.Use(tracingPlugin); err != nil { + logger.Error("注册GORM追踪插件失败", zap.Error(err)) + return nil, err + } + + logger.Info("GORM自动追踪已启用") + return db, nil +} + // NewRedisClient 创建Redis客户端 func NewRedisClient(cfg *config.Config, logger *zap.Logger) (*redis.Client, error) { client := redis.NewClient(&redis.Options{ @@ -165,17 +223,16 @@ func NewRedisClient(cfg *config.Config, logger *zap.Logger) (*redis.Client, erro WriteTimeout: cfg.Redis.WriteTimeout, }) - // 测试连接 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() _, err := client.Ping(ctx).Result() if err != nil { - logger.Error("Failed to connect to Redis", zap.Error(err)) + logger.Error("Redis连接失败", zap.Error(err)) return nil, err } - logger.Info("Redis connection established") + logger.Info("Redis连接已建立") return client, nil } @@ -184,9 +241,14 @@ func NewRedisCache(client *redis.Client, logger *zap.Logger, cfg *config.Config) return cache.NewRedisCache(client, logger, "app") } +// NewTracedRedisCache 创建带追踪的Redis缓存服务 +func NewTracedRedisCache(client *redis.Client, tracer *tracing.Tracer, logger *zap.Logger, cfg *config.Config) interfaces.CacheService { + return tracing.NewTracedRedisCache(client, tracer, logger, "app") +} + // NewEventBus 创建事件总线 func NewEventBus(logger *zap.Logger, cfg *config.Config) interfaces.EventBus { - return events.NewMemoryEventBus(logger, 5) // 默认5个工作协程 + return events.NewMemoryEventBus(logger, 5) } // NewHealthChecker 创建健康检查器 @@ -194,142 +256,293 @@ func NewHealthChecker(logger *zap.Logger) *health.HealthChecker { return health.NewHealthChecker(logger) } -// HTTP组件构造函数 +// NewSMSService 创建短信服务 +func NewSMSService(cfg *config.Config, logger *zap.Logger) (sms.Service, error) { + if cfg.SMS.MockEnabled { + logger.Info("使用模拟短信服务 (mock_enabled=true)") + return sms.NewMockSMSService(logger), nil + } + + logger.Info("使用阿里云短信服务") + return sms.NewAliSMSService(cfg.SMS, logger) +} + +// ================ HTTP组件构造函数 ================ // NewResponseBuilder 创建响应构建器 func NewResponseBuilder() interfaces.ResponseBuilder { - return http.NewResponseBuilder() + return sharedhttp.NewResponseBuilder() } -// NewRequestValidator 创建请求验证器 +// NewRequestValidator 创建中文请求验证器 func NewRequestValidator(response interfaces.ResponseBuilder) interfaces.RequestValidator { - return http.NewRequestValidator(response) + return sharedhttp.NewRequestValidatorZh(response) } // NewGinRouter 创建Gin路由器 -func NewGinRouter(cfg *config.Config, logger *zap.Logger) *http.GinRouter { - return http.NewGinRouter(cfg, logger) +func NewGinRouter(cfg *config.Config, logger *zap.Logger) *sharedhttp.GinRouter { + return sharedhttp.NewGinRouter(cfg, logger) } -// 中间件构造函数 +// ================ 中间件构造函数 ================ -// NewRequestIDMiddleware 创建请求ID中间件 func NewRequestIDMiddleware() *middleware.RequestIDMiddleware { return middleware.NewRequestIDMiddleware() } -// NewSecurityHeadersMiddleware 创建安全头部中间件 func NewSecurityHeadersMiddleware() *middleware.SecurityHeadersMiddleware { return middleware.NewSecurityHeadersMiddleware() } -// NewResponseTimeMiddleware 创建响应时间中间件 func NewResponseTimeMiddleware() *middleware.ResponseTimeMiddleware { return middleware.NewResponseTimeMiddleware() } -// NewCORSMiddleware 创建CORS中间件 func NewCORSMiddleware(cfg *config.Config) *middleware.CORSMiddleware { return middleware.NewCORSMiddleware(cfg) } -// NewRateLimitMiddleware 创建限流中间件 -func NewRateLimitMiddleware(cfg *config.Config) *middleware.RateLimitMiddleware { - return middleware.NewRateLimitMiddleware(cfg) +func NewRateLimitMiddleware(cfg *config.Config, response interfaces.ResponseBuilder) *middleware.RateLimitMiddleware { + return middleware.NewRateLimitMiddleware(cfg, response) } -// NewRequestLoggerMiddleware 创建请求日志中间件 -func NewRequestLoggerMiddleware(logger *zap.Logger) *middleware.RequestLoggerMiddleware { - return middleware.NewRequestLoggerMiddleware(logger) +func NewRequestLoggerMiddleware(logger *zap.Logger, cfg *config.Config, tracer *tracing.Tracer) *middleware.RequestLoggerMiddleware { + return middleware.NewRequestLoggerMiddleware(logger, cfg.App.IsDevelopment(), tracer) } -// NewJWTAuthMiddleware 创建JWT认证中间件 func NewJWTAuthMiddleware(cfg *config.Config, logger *zap.Logger) *middleware.JWTAuthMiddleware { return middleware.NewJWTAuthMiddleware(cfg, logger) } -// NewOptionalAuthMiddleware 创建可选认证中间件 func NewOptionalAuthMiddleware(jwtAuth *middleware.JWTAuthMiddleware) *middleware.OptionalAuthMiddleware { return middleware.NewOptionalAuthMiddleware(jwtAuth) } -// 用户域构造函数 +func NewTraceIDMiddleware(tracer *tracing.Tracer) *middleware.TraceIDMiddleware { + return middleware.NewTraceIDMiddleware(tracer) +} + +func NewErrorTrackingMiddleware(logger *zap.Logger, tracer *tracing.Tracer) *middleware.ErrorTrackingMiddleware { + return middleware.NewErrorTrackingMiddleware(logger, tracer) +} + +func NewRequestBodyLoggerMiddleware(logger *zap.Logger, cfg *config.Config, tracer *tracing.Tracer) *middleware.RequestBodyLoggerMiddleware { + return middleware.NewRequestBodyLoggerMiddleware(logger, cfg.App.IsDevelopment(), tracer) +} + +// ================ 高级特性构造函数 ================ + +// NewTracer 创建链路追踪器 +func NewTracer(cfg *config.Config, logger *zap.Logger) *tracing.Tracer { + tracingConfig := tracing.TracerConfig{ + ServiceName: cfg.App.Name, + ServiceVersion: cfg.App.Version, + Environment: cfg.App.Env, + Endpoint: cfg.Monitoring.TracingEndpoint, + SampleRate: cfg.Monitoring.SampleRate, + Enabled: cfg.Monitoring.TracingEnabled, + } + return tracing.NewTracer(tracingConfig, logger) +} + +// NewPrometheusMetrics 创建Prometheus指标收集器 +func NewPrometheusMetrics(logger *zap.Logger, cfg *config.Config) interfaces.MetricsCollector { + if !cfg.Monitoring.MetricsEnabled { + return &NoopMetricsCollector{} + } + return metrics.NewPrometheusMetrics(logger) +} + +// NewBusinessMetrics 创建业务指标收集器 +func NewBusinessMetrics(prometheusMetrics interfaces.MetricsCollector, logger *zap.Logger) *metrics.BusinessMetrics { + return metrics.NewBusinessMetrics(prometheusMetrics, logger) +} + +// NewCircuitBreakerWrapper 创建熔断器包装器 +func NewCircuitBreakerWrapper(logger *zap.Logger, cfg *config.Config) *resilience.Wrapper { + return resilience.NewWrapper(logger) +} + +// NewRetryerWrapper 创建重试器包装器 +func NewRetryerWrapper(logger *zap.Logger) *resilience.RetryerWrapper { + return resilience.NewRetryerWrapper(logger) +} + +// NewSagaManager 创建Saga管理器 +func NewSagaManager(cfg *config.Config, logger *zap.Logger) *saga.SagaManager { + sagaConfig := saga.SagaConfig{ + DefaultTimeout: 30 * time.Second, + DefaultMaxRetries: 3, + Parallel: false, + } + return saga.NewSagaManager(sagaConfig, logger) +} + +// NewHookSystem 创建钩子系统 +func NewHookSystem(logger *zap.Logger) *hooks.HookSystem { + hookConfig := hooks.HookConfig{ + DefaultTimeout: 30 * time.Second, + TrackDuration: true, + ErrorStrategy: hooks.ContinueOnError, + } + return hooks.NewHookSystem(hookConfig, logger) +} + +// NewTracingMiddleware 创建追踪中间件 +func NewTracingMiddleware(tracer *tracing.Tracer) *TracingMiddleware { + return &TracingMiddleware{tracer: tracer} +} + +// NewMetricsMiddleware 创建指标中间件 +func NewMetricsMiddleware(metricsCollector interfaces.MetricsCollector) *MetricsMiddleware { + return &MetricsMiddleware{metrics: metricsCollector} +} + +// ================ 用户域构造函数 ================ -// NewUserRepository 创建用户仓储 func NewUserRepository(db *gorm.DB, cache interfaces.CacheService, logger *zap.Logger) *repositories.UserRepository { return repositories.NewUserRepository(db, cache, logger) } -// NewUserService 创建用户服务 +func NewSMSCodeRepository(db *gorm.DB, cache interfaces.CacheService, logger *zap.Logger) *repositories.SMSCodeRepository { + return repositories.NewSMSCodeRepository(db, cache, logger) +} + +func NewSMSCodeService( + repo *repositories.SMSCodeRepository, + smsClient sms.Service, + cache interfaces.CacheService, + cfg *config.Config, + logger *zap.Logger, +) *services.SMSCodeService { + return services.NewSMSCodeService(repo, smsClient, cache, cfg.SMS, logger) +} + func NewUserService( repo *repositories.UserRepository, + smsCodeService *services.SMSCodeService, eventBus interfaces.EventBus, logger *zap.Logger, ) *services.UserService { - return services.NewUserService(repo, eventBus, logger) + return services.NewUserService(repo, smsCodeService, eventBus, logger) +} + +// NewTracedUserService 创建带自动追踪的用户服务 +func NewTracedUserService( + baseService *services.UserService, + tracer *tracing.Tracer, + logger *zap.Logger, +) interfaces.UserService { + serviceWrapper := tracing.NewServiceWrapper(tracer, logger) + return tracing.NewTracedUserService(baseService, serviceWrapper) } -// NewUserHandler 创建用户处理器 func NewUserHandler( - userService *services.UserService, + userService interfaces.UserService, + smsCodeService *services.SMSCodeService, response interfaces.ResponseBuilder, validator interfaces.RequestValidator, logger *zap.Logger, jwtAuth *middleware.JWTAuthMiddleware, ) *handlers.UserHandler { - return handlers.NewUserHandler(userService, response, validator, logger, jwtAuth) + return handlers.NewUserHandler(userService, smsCodeService, response, validator, logger, jwtAuth) } -// NewUserRoutes 创建用户路由 -func NewUserRoutes( - handler *handlers.UserHandler, - jwtAuth *middleware.JWTAuthMiddleware, - optionalAuth *middleware.OptionalAuthMiddleware, -) *routes.UserRoutes { - return routes.NewUserRoutes(handler, jwtAuth, optionalAuth) +// ================ 中间件定义 ================ + +// TracingMiddleware 追踪中间件 +type TracingMiddleware struct { + tracer *tracing.Tracer } -// 注册函数 +func (tm *TracingMiddleware) GetName() string { return "tracing" } +func (tm *TracingMiddleware) GetPriority() int { return 1 } +func (tm *TracingMiddleware) IsGlobal() bool { return true } +func (tm *TracingMiddleware) Handle() gin.HandlerFunc { + return tm.tracer.TraceMiddleware() +} + +// MetricsMiddleware 指标中间件 +type MetricsMiddleware struct { + metrics interfaces.MetricsCollector +} + +func (mm *MetricsMiddleware) GetName() string { return "metrics" } +func (mm *MetricsMiddleware) GetPriority() int { return 2 } +func (mm *MetricsMiddleware) IsGlobal() bool { return true } +func (mm *MetricsMiddleware) Handle() gin.HandlerFunc { + return func(c *gin.Context) { + start := time.Now() + c.Next() + duration := time.Since(start).Seconds() + mm.metrics.RecordHTTPRequest(c.Request.Method, c.FullPath(), c.Writer.Status(), duration) + mm.metrics.RecordHTTPDuration(c.Request.Method, c.FullPath(), duration) + } +} + +// NoopMetricsCollector 空的指标收集器实现 +type NoopMetricsCollector struct{} + +func (n *NoopMetricsCollector) RecordHTTPRequest(method, path string, status int, duration float64) {} +func (n *NoopMetricsCollector) RecordHTTPDuration(method, path string, duration float64) {} +func (n *NoopMetricsCollector) IncrementCounter(name string, labels map[string]string) {} +func (n *NoopMetricsCollector) RecordGauge(name string, value float64, labels map[string]string) {} +func (n *NoopMetricsCollector) RecordHistogram(name string, value float64, labels map[string]string) { +} +func (n *NoopMetricsCollector) RegisterCounter(name, help string, labels []string) error { return nil } +func (n *NoopMetricsCollector) RegisterGauge(name, help string, labels []string) error { return nil } +func (n *NoopMetricsCollector) RegisterHistogram(name, help string, labels []string, buckets []float64) error { + return nil +} +func (n *NoopMetricsCollector) GetHandler() nethttp.Handler { return nil } + +// ================ 注册函数 ================ // RegisterMiddlewares 注册中间件 func RegisterMiddlewares( - router *http.GinRouter, + router *sharedhttp.GinRouter, requestID *middleware.RequestIDMiddleware, security *middleware.SecurityHeadersMiddleware, responseTime *middleware.ResponseTimeMiddleware, cors *middleware.CORSMiddleware, rateLimit *middleware.RateLimitMiddleware, requestLogger *middleware.RequestLoggerMiddleware, + tracingMiddleware *TracingMiddleware, + metricsMiddleware *MetricsMiddleware, + traceIDMiddleware *middleware.TraceIDMiddleware, + errorTrackingMiddleware *middleware.ErrorTrackingMiddleware, + requestBodyLogger *middleware.RequestBodyLoggerMiddleware, ) { - // 注册全局中间件 router.RegisterMiddleware(requestID) router.RegisterMiddleware(security) router.RegisterMiddleware(responseTime) router.RegisterMiddleware(cors) router.RegisterMiddleware(rateLimit) router.RegisterMiddleware(requestLogger) + router.RegisterMiddleware(tracingMiddleware) + router.RegisterMiddleware(metricsMiddleware) + router.RegisterMiddleware(traceIDMiddleware) + router.RegisterMiddleware(errorTrackingMiddleware) + router.RegisterMiddleware(requestBodyLogger) } // RegisterRoutes 注册路由 func RegisterRoutes( - router *http.GinRouter, - userRoutes *routes.UserRoutes, + router *sharedhttp.GinRouter, + userHandler *handlers.UserHandler, + jwtAuth *middleware.JWTAuthMiddleware, + metricsCollector interfaces.MetricsCollector, ) { - // 设置默认路由 router.SetupDefaultRoutes() - // 注册用户路由 - userRoutes.RegisterRoutes(router.GetEngine()) - userRoutes.RegisterPublicRoutes(router.GetEngine()) - userRoutes.RegisterAdminRoutes(router.GetEngine()) - userRoutes.RegisterHealthRoutes(router.GetEngine()) + if handler := metricsCollector.GetHandler(); handler != nil { + router.GetEngine().GET("/metrics", gin.WrapH(handler)) + } - // 打印路由信息 + routes.UserRoutes(router.GetEngine(), userHandler, jwtAuth) router.PrintRoutes() } -// 生命周期钩子 - // RegisterLifecycleHooks 注册生命周期钩子 func RegisterLifecycleHooks( lc fx.Lifecycle, @@ -339,29 +552,60 @@ func RegisterLifecycleHooks( cache interfaces.CacheService, eventBus interfaces.EventBus, healthChecker *health.HealthChecker, - router *http.GinRouter, + router *sharedhttp.GinRouter, userService *services.UserService, + tracer *tracing.Tracer, + prometheusMetrics interfaces.MetricsCollector, + businessMetrics *metrics.BusinessMetrics, + circuitBreakerWrapper *resilience.Wrapper, + retryerWrapper *resilience.RetryerWrapper, + sagaManager *saga.SagaManager, + hookSystem *hooks.HookSystem, ) { lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { - logger.Info("Starting application services...") + logger.Info("正在启动应用服务...") + + // 初始化高级特性 + if err := tracer.Initialize(ctx); err != nil { + logger.Error("初始化追踪器失败", zap.Error(err)) + return err + } + + if err := hookSystem.Initialize(ctx); err != nil { + logger.Error("初始化钩子系统失败", zap.Error(err)) + return err + } + + if err := sagaManager.Initialize(ctx); err != nil { + logger.Error("初始化事务管理器失败", zap.Error(err)) + return err + } // 注册服务到健康检查器 healthChecker.RegisterService(userService) + healthChecker.RegisterService(sagaManager) - // 初始化缓存服务 + // 启动基础服务 if err := cache.Initialize(ctx); err != nil { - logger.Error("Failed to initialize cache", zap.Error(err)) + logger.Error("初始化缓存失败", zap.Error(err)) return err } - // 启动事件总线 if err := eventBus.Start(ctx); err != nil { - logger.Error("Failed to start event bus", zap.Error(err)) + logger.Error("启动事件总线失败", zap.Error(err)) return err } - // 启动健康检查(如果启用) + if err := hookSystem.Start(ctx); err != nil { + logger.Error("启动钩子系统失败", zap.Error(err)) + return err + } + + // 注册应用钩子 + registerApplicationHooks(hookSystem, businessMetrics, logger) + + // 启动健康检查 if cfg.Health.Enabled { go healthChecker.StartPeriodicCheck(ctx, cfg.Health.Interval) } @@ -370,44 +614,65 @@ func RegisterLifecycleHooks( go func() { addr := fmt.Sprintf("%s:%s", cfg.Server.Host, cfg.Server.Port) if err := router.Start(addr); err != nil { - logger.Error("Failed to start HTTP server", zap.Error(err)) + logger.Error("启动HTTP服务器失败", zap.Error(err)) } }() - logger.Info("All services started successfully") + logger.Info("所有服务已成功启动") return nil }, OnStop: func(ctx context.Context) error { - logger.Info("Stopping application services...") + logger.Info("正在停止应用服务...") - // 停止HTTP服务器 - if err := router.Stop(ctx); err != nil { - logger.Error("Failed to stop HTTP server", zap.Error(err)) - } + // 按顺序关闭服务 + router.Stop(ctx) + hookSystem.Shutdown(ctx) + sagaManager.Shutdown(ctx) + eventBus.Stop(ctx) + tracer.Shutdown(ctx) + cache.Shutdown(ctx) - // 停止事件总线 - if err := eventBus.Stop(ctx); err != nil { - logger.Error("Failed to stop event bus", zap.Error(err)) - } - - // 关闭缓存服务 - if err := cache.Shutdown(ctx); err != nil { - logger.Error("Failed to shutdown cache service", zap.Error(err)) - } - - // 关闭数据库连接 if sqlDB, err := db.DB(); err == nil { - if err := sqlDB.Close(); err != nil { - logger.Error("Failed to close database", zap.Error(err)) - } + sqlDB.Close() } - logger.Info("All services stopped") + logger.Info("所有服务已停止") return nil }, }) } +// registerApplicationHooks 注册应用钩子 +func registerApplicationHooks(hookSystem *hooks.HookSystem, businessMetrics *metrics.BusinessMetrics, logger *zap.Logger) { + userCreatedHook := &hooks.Hook{ + Name: "metrics.user_created", + Priority: 1, + Async: false, + Timeout: 5 * time.Second, + Func: func(ctx context.Context, data interface{}) error { + businessMetrics.RecordUserCreated("register") + logger.Debug("记录用户创建指标") + return nil + }, + } + hookSystem.Register("user.created", userCreatedHook) + + userLoginHook := &hooks.Hook{ + Name: "metrics.user_login", + Priority: 1, + Async: false, + Timeout: 5 * time.Second, + Func: func(ctx context.Context, data interface{}) error { + businessMetrics.RecordUserLogin("web", "success") + logger.Debug("记录用户登录指标") + return nil + }, + } + hookSystem.Register("user.logged_in", userLoginHook) + + logger.Info("应用钩子已成功注册") +} + // ServiceRegistrar 服务注册器接口 type ServiceRegistrar interface { RegisterServices() fx.Option diff --git a/internal/domains/user/dto/sms_dto.go b/internal/domains/user/dto/sms_dto.go new file mode 100644 index 0000000..79781a5 --- /dev/null +++ b/internal/domains/user/dto/sms_dto.go @@ -0,0 +1,72 @@ +package dto + +import ( + "time" + + "tyapi-server/internal/domains/user/entities" +) + +// SendCodeRequest 发送验证码请求 +type SendCodeRequest struct { + Phone string `json:"phone" binding:"required,len=11" example:"13800138000"` + Scene entities.SMSScene `json:"scene" binding:"required,oneof=register login change_password reset_password bind unbind" example:"register"` +} + +// SendCodeResponse 发送验证码响应 +type SendCodeResponse struct { + Message string `json:"message" example:"验证码发送成功"` + ExpiresAt time.Time `json:"expires_at" example:"2024-01-01T00:05:00Z"` +} + +// VerifyCodeRequest 验证验证码请求 +type VerifyCodeRequest struct { + Phone string `json:"phone" binding:"required,len=11" example:"13800138000"` + Code string `json:"code" binding:"required,len=6" example:"123456"` + Scene entities.SMSScene `json:"scene" binding:"required,oneof=register login change_password reset_password bind unbind" example:"register"` +} + +// SMSCodeResponse SMS验证码记录响应 +type SMSCodeResponse struct { + ID string `json:"id" example:"123e4567-e89b-12d3-a456-426614174000"` + Phone string `json:"phone" example:"13800138000"` + Scene entities.SMSScene `json:"scene" example:"register"` + Used bool `json:"used" example:"false"` + ExpiresAt time.Time `json:"expires_at" example:"2024-01-01T00:05:00Z"` + CreatedAt time.Time `json:"created_at" example:"2024-01-01T00:00:00Z"` +} + +// SMSCodeListRequest SMS验证码列表请求 +type SMSCodeListRequest struct { + Phone string `form:"phone" binding:"omitempty,len=11" example:"13800138000"` + Scene entities.SMSScene `form:"scene" binding:"omitempty,oneof=register login change_password reset_password bind unbind" example:"register"` + Page int `form:"page" binding:"omitempty,min=1" example:"1"` + PageSize int `form:"page_size" binding:"omitempty,min=1,max=100" example:"20"` +} + +// 转换方法 +func FromSMSCodeEntity(smsCode *entities.SMSCode) *SMSCodeResponse { + if smsCode == nil { + return nil + } + + return &SMSCodeResponse{ + ID: smsCode.ID, + Phone: smsCode.Phone, + Scene: smsCode.Scene, + Used: smsCode.Used, + ExpiresAt: smsCode.ExpiresAt, + CreatedAt: smsCode.CreatedAt, + } +} + +func FromSMSCodeEntities(smsCodes []*entities.SMSCode) []*SMSCodeResponse { + if smsCodes == nil { + return []*SMSCodeResponse{} + } + + responses := make([]*SMSCodeResponse, len(smsCodes)) + for i, smsCode := range smsCodes { + responses[i] = FromSMSCodeEntity(smsCode) + } + return responses +} diff --git a/internal/domains/user/dto/user_dto.go b/internal/domains/user/dto/user_dto.go index e452593..36460dc 100644 --- a/internal/domains/user/dto/user_dto.go +++ b/internal/domains/user/dto/user_dto.go @@ -6,88 +6,40 @@ import ( "tyapi-server/internal/domains/user/entities" ) -// CreateUserRequest 创建用户请求 -type CreateUserRequest struct { - Username string `json:"username" binding:"required,min=3,max=50" example:"john_doe"` - Email string `json:"email" binding:"required,email" example:"john@example.com"` - Password string `json:"password" binding:"required,min=6,max=128" example:"password123"` - FirstName string `json:"first_name" binding:"max=50" example:"John"` - LastName string `json:"last_name" binding:"max=50" example:"Doe"` - Phone string `json:"phone" binding:"omitempty,max=20" example:"+86-13800138000"` +// RegisterRequest 用户注册请求 +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"` } -// UpdateUserRequest 更新用户请求 -type UpdateUserRequest struct { - FirstName *string `json:"first_name,omitempty" binding:"omitempty,max=50" example:"John"` - LastName *string `json:"last_name,omitempty" binding:"omitempty,max=50" example:"Doe"` - Phone *string `json:"phone,omitempty" binding:"omitempty,max=20" example:"+86-13800138000"` - Avatar *string `json:"avatar,omitempty" binding:"omitempty,url" example:"https://example.com/avatar.jpg"` +// LoginWithPasswordRequest 密码登录请求 +type LoginWithPasswordRequest struct { + Phone string `json:"phone" binding:"required,len=11" example:"13800138000"` + Password string `json:"password" binding:"required" example:"password123"` +} + +// LoginWithSMSRequest 短信验证码登录请求 +type LoginWithSMSRequest struct { + Phone string `json:"phone" binding:"required,len=11" example:"13800138000"` + Code string `json:"code" binding:"required,len=6" example:"123456"` } // ChangePasswordRequest 修改密码请求 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"` + 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"` } // UserResponse 用户响应 type UserResponse struct { - ID string `json:"id" example:"123e4567-e89b-12d3-a456-426614174000"` - Username string `json:"username" example:"john_doe"` - Email string `json:"email" example:"john@example.com"` - FirstName string `json:"first_name" example:"John"` - LastName string `json:"last_name" example:"Doe"` - Phone string `json:"phone" example:"+86-13800138000"` - Avatar string `json:"avatar" example:"https://example.com/avatar.jpg"` - Status entities.UserStatus `json:"status" example:"active"` - LastLoginAt *time.Time `json:"last_login_at,omitempty" example:"2024-01-01T00:00:00Z"` - CreatedAt time.Time `json:"created_at" example:"2024-01-01T00:00:00Z"` - UpdatedAt time.Time `json:"updated_at" example:"2024-01-01T00:00:00Z"` - Profile *UserProfileResponse `json:"profile,omitempty"` -} - -// UserProfileResponse 用户档案响应 -type UserProfileResponse struct { - Bio string `json:"bio,omitempty" example:"Software Developer"` - Location string `json:"location,omitempty" example:"Beijing, China"` - Website string `json:"website,omitempty" example:"https://johndoe.com"` - Birthday *time.Time `json:"birthday,omitempty" example:"1990-01-01T00:00:00Z"` - Gender string `json:"gender,omitempty" example:"male"` - Timezone string `json:"timezone,omitempty" example:"Asia/Shanghai"` - Language string `json:"language,omitempty" example:"zh-CN"` -} - -// UserListRequest 用户列表请求 -type UserListRequest struct { - Page int `form:"page" binding:"omitempty,min=1" example:"1"` - PageSize int `form:"page_size" binding:"omitempty,min=1,max=100" example:"20"` - Sort string `form:"sort" binding:"omitempty,oneof=created_at updated_at username email" example:"created_at"` - Order string `form:"order" binding:"omitempty,oneof=asc desc" example:"desc"` - Status entities.UserStatus `form:"status" binding:"omitempty,oneof=active inactive suspended pending" example:"active"` - Search string `form:"search" binding:"omitempty,max=100" example:"john"` - Filters map[string]interface{} `form:"-"` -} - -// UserListResponse 用户列表响应 -type UserListResponse struct { - Users []*UserResponse `json:"users"` - Pagination PaginationMeta `json:"pagination"` -} - -// PaginationMeta 分页元数据 -type PaginationMeta struct { - Page int `json:"page" example:"1"` - PageSize int `json:"page_size" example:"20"` - Total int64 `json:"total" example:"100"` - TotalPages int `json:"total_pages" example:"5"` - HasNext bool `json:"has_next" example:"true"` - HasPrev bool `json:"has_prev" example:"false"` -} - -// LoginRequest 登录请求 -type LoginRequest struct { - Login string `json:"login" binding:"required" example:"john_doe"` - Password string `json:"password" binding:"required" example:"password123"` + ID string `json:"id" example:"123e4567-e89b-12d3-a456-426614174000"` + Phone string `json:"phone" example:"13800138000"` + CreatedAt time.Time `json:"created_at" example:"2024-01-01T00:00:00Z"` + UpdatedAt time.Time `json:"updated_at" example:"2024-01-01T00:00:00Z"` } // LoginResponse 登录响应 @@ -96,47 +48,27 @@ type LoginResponse struct { AccessToken string `json:"access_token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."` TokenType string `json:"token_type" example:"Bearer"` ExpiresIn int64 `json:"expires_in" example:"86400"` -} - -// UpdateProfileRequest 更新用户档案请求 -type UpdateProfileRequest struct { - Bio *string `json:"bio,omitempty" binding:"omitempty,max=500" example:"Software Developer"` - Location *string `json:"location,omitempty" binding:"omitempty,max=100" example:"Beijing, China"` - Website *string `json:"website,omitempty" binding:"omitempty,url" example:"https://johndoe.com"` - Birthday *time.Time `json:"birthday,omitempty" example:"1990-01-01T00:00:00Z"` - Gender *string `json:"gender,omitempty" binding:"omitempty,oneof=male female other" example:"male"` - Timezone *string `json:"timezone,omitempty" binding:"omitempty,max=50" example:"Asia/Shanghai"` - Language *string `json:"language,omitempty" binding:"omitempty,max=10" example:"zh-CN"` -} - -// UserStatsResponse 用户统计响应 -type UserStatsResponse struct { - TotalUsers int64 `json:"total_users" example:"1000"` - ActiveUsers int64 `json:"active_users" example:"950"` - InactiveUsers int64 `json:"inactive_users" example:"30"` - SuspendedUsers int64 `json:"suspended_users" example:"20"` - NewUsersToday int64 `json:"new_users_today" example:"5"` - NewUsersWeek int64 `json:"new_users_week" example:"25"` - NewUsersMonth int64 `json:"new_users_month" example:"120"` -} - -// UserSearchRequest 用户搜索请求 -type UserSearchRequest struct { - Query string `form:"q" binding:"required,min=1,max=100" example:"john"` - Page int `form:"page" binding:"omitempty,min=1" example:"1"` - PageSize int `form:"page_size" binding:"omitempty,min=1,max=50" example:"10"` + LoginMethod string `json:"login_method" example:"password"` // password 或 sms } // 转换方法 -func (r *CreateUserRequest) ToEntity() *entities.User { +func (r *RegisterRequest) ToEntity() *entities.User { return &entities.User{ - Username: r.Username, - Email: r.Email, - Password: r.Password, - FirstName: r.FirstName, - LastName: r.LastName, - Phone: r.Phone, - Status: entities.UserStatusActive, + Phone: r.Phone, + Password: r.Password, + } +} + +func (r *LoginWithPasswordRequest) ToEntity() *entities.User { + return &entities.User{ + Phone: r.Phone, + Password: r.Password, + } +} + +func (r *LoginWithSMSRequest) ToEntity() *entities.User { + return &entities.User{ + Phone: r.Phone, } } @@ -146,28 +78,9 @@ func FromEntity(user *entities.User) *UserResponse { } return &UserResponse{ - ID: user.ID, - Username: user.Username, - Email: user.Email, - FirstName: user.FirstName, - LastName: user.LastName, - Phone: user.Phone, - Avatar: user.Avatar, - Status: user.Status, - LastLoginAt: user.LastLoginAt, - CreatedAt: user.CreatedAt, - UpdatedAt: user.UpdatedAt, + ID: user.ID, + Phone: user.Phone, + CreatedAt: user.CreatedAt, + UpdatedAt: user.UpdatedAt, } } - -func FromEntities(users []*entities.User) []*UserResponse { - if users == nil { - return []*UserResponse{} - } - - responses := make([]*UserResponse, len(users)) - for i, user := range users { - responses[i] = FromEntity(user) - } - return responses -} diff --git a/internal/domains/user/entities/sms_code.go b/internal/domains/user/entities/sms_code.go new file mode 100644 index 0000000..d28896b --- /dev/null +++ b/internal/domains/user/entities/sms_code.go @@ -0,0 +1,88 @@ +package entities + +import ( + "time" + + "gorm.io/gorm" +) + +// SMSCode 短信验证码记录 +type SMSCode struct { + ID string `gorm:"primaryKey;type:varchar(36)" json:"id"` + Phone string `gorm:"type:varchar(20);not null;index" json:"phone"` + Code string `gorm:"type:varchar(10);not null" json:"-"` // 不返回给前端 + Scene SMSScene `gorm:"type:varchar(20);not null" json:"scene"` + Used bool `gorm:"default:false" json:"used"` + ExpiresAt time.Time `gorm:"not null" json:"expires_at"` + UsedAt *time.Time `json:"used_at,omitempty"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + + // 额外信息 + IP string `gorm:"type:varchar(45)" json:"ip"` + UserAgent string `gorm:"type:varchar(500)" json:"user_agent"` +} + +// SMSScene 短信验证码使用场景 +type SMSScene string + +const ( + SMSSceneRegister SMSScene = "register" // 注册 + SMSSceneLogin SMSScene = "login" // 登录 + SMSSceneChangePassword SMSScene = "change_password" // 修改密码 + SMSSceneResetPassword SMSScene = "reset_password" // 重置密码 + SMSSceneBind SMSScene = "bind" // 绑定手机号 + SMSSceneUnbind SMSScene = "unbind" // 解绑手机号 +) + +// 实现 Entity 接口 +func (s *SMSCode) GetID() string { + return s.ID +} + +func (s *SMSCode) GetCreatedAt() time.Time { + return s.CreatedAt +} + +func (s *SMSCode) GetUpdatedAt() time.Time { + return s.UpdatedAt +} + +// Validate 验证短信验证码 +func (s *SMSCode) Validate() error { + if s.Phone == "" { + return &ValidationError{Message: "手机号不能为空"} + } + if s.Code == "" { + return &ValidationError{Message: "验证码不能为空"} + } + if s.Scene == "" { + return &ValidationError{Message: "使用场景不能为空"} + } + if s.ExpiresAt.IsZero() { + return &ValidationError{Message: "过期时间不能为空"} + } + + return nil +} + +// 业务方法 +func (s *SMSCode) IsExpired() bool { + return time.Now().After(s.ExpiresAt) +} + +func (s *SMSCode) IsValid() bool { + return !s.Used && !s.IsExpired() +} + +func (s *SMSCode) MarkAsUsed() { + s.Used = true + now := time.Now() + s.UsedAt = &now +} + +// TableName 指定表名 +func (SMSCode) TableName() string { + return "sms_codes" +} diff --git a/internal/domains/user/entities/user.go b/internal/domains/user/entities/user.go index 015aa75..4a25437 100644 --- a/internal/domains/user/entities/user.go +++ b/internal/domains/user/entities/user.go @@ -8,37 +8,14 @@ import ( // User 用户实体 type User struct { - ID string `gorm:"primaryKey;type:varchar(36)" json:"id"` - Username string `gorm:"uniqueIndex;type:varchar(50);not null" json:"username"` - Email string `gorm:"uniqueIndex;type:varchar(100);not null" json:"email"` - Password string `gorm:"type:varchar(255);not null" json:"-"` - FirstName string `gorm:"type:varchar(50)" json:"first_name"` - LastName string `gorm:"type:varchar(50)" json:"last_name"` - Phone string `gorm:"type:varchar(20)" json:"phone"` - Avatar string `gorm:"type:varchar(255)" json:"avatar"` - Status UserStatus `gorm:"type:varchar(20);default:'active'" json:"status"` - LastLoginAt *time.Time `json:"last_login_at"` - CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` - UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - - // 软删除字段 - IsDeleted bool `gorm:"default:false" json:"is_deleted"` - - // 版本控制 - Version int `gorm:"default:1" json:"version"` + ID string `gorm:"primaryKey;type:varchar(36)" json:"id"` + Phone string `gorm:"uniqueIndex;type:varchar(20);not null" json:"phone"` + Password string `gorm:"type:varchar(255);not null" json:"-"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` } -// UserStatus 用户状态枚举 -type UserStatus string - -const ( - UserStatusActive UserStatus = "active" - UserStatusInactive UserStatus = "inactive" - UserStatusSuspended UserStatus = "suspended" - UserStatusPending UserStatus = "pending" -) - // 实现 Entity 接口 func (u *User) GetID() string { return u.ID @@ -52,47 +29,13 @@ func (u *User) GetUpdatedAt() time.Time { return u.UpdatedAt } -// 业务方法 -func (u *User) IsActive() bool { - return u.Status == UserStatusActive && !u.IsDeleted -} - -func (u *User) GetFullName() string { - if u.FirstName == "" && u.LastName == "" { - return u.Username - } - return u.FirstName + " " + u.LastName -} - -func (u *User) CanLogin() bool { - return u.IsActive() && u.Status != UserStatusSuspended -} - -func (u *User) MarkAsDeleted() { - u.IsDeleted = true - u.Status = UserStatusInactive -} - -func (u *User) Restore() { - u.IsDeleted = false - u.Status = UserStatusActive -} - -func (u *User) UpdateLastLogin() { - now := time.Now() - u.LastLoginAt = &now -} - // 验证方法 func (u *User) Validate() error { - if u.Username == "" { - return NewValidationError("username is required") - } - if u.Email == "" { - return NewValidationError("email is required") + if u.Phone == "" { + return NewValidationError("手机号不能为空") } if u.Password == "" { - return NewValidationError("password is required") + return NewValidationError("密码不能为空") } return nil } @@ -114,25 +57,3 @@ func (e *ValidationError) Error() string { func NewValidationError(message string) *ValidationError { return &ValidationError{Message: message} } - -// UserProfile 用户档案(扩展信息) -type UserProfile struct { - ID string `gorm:"primaryKey;type:varchar(36)" json:"id"` - UserID string `gorm:"type:varchar(36);not null;index" json:"user_id"` - Bio string `gorm:"type:text" json:"bio"` - Location string `gorm:"type:varchar(100)" json:"location"` - Website string `gorm:"type:varchar(255)" json:"website"` - Birthday *time.Time `json:"birthday"` - Gender string `gorm:"type:varchar(10)" json:"gender"` - Timezone string `gorm:"type:varchar(50)" json:"timezone"` - Language string `gorm:"type:varchar(10);default:'zh-CN'" json:"language"` - CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` - UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` - - // 关联关系 - User *User `gorm:"foreignKey:UserID;references:ID" json:"user,omitempty"` -} - -func (UserProfile) TableName() string { - return "user_profiles" -} diff --git a/internal/domains/user/events/user_events.go b/internal/domains/user/events/user_events.go index b09a987..39f9d9c 100644 --- a/internal/domains/user/events/user_events.go +++ b/internal/domains/user/events/user_events.go @@ -13,15 +13,9 @@ import ( type UserEventType string const ( - UserCreatedEvent UserEventType = "user.created" - UserUpdatedEvent UserEventType = "user.updated" - UserDeletedEvent UserEventType = "user.deleted" - UserRestoredEvent UserEventType = "user.restored" + UserRegisteredEvent UserEventType = "user.registered" UserLoggedInEvent UserEventType = "user.logged_in" - UserLoggedOutEvent UserEventType = "user.logged_out" UserPasswordChangedEvent UserEventType = "user.password_changed" - UserStatusChangedEvent UserEventType = "user.status_changed" - UserProfileUpdatedEvent UserEventType = "user.profile_updated" ) // BaseUserEvent 用户事件基础结构 @@ -99,17 +93,17 @@ func (e *BaseUserEvent) Unmarshal(data []byte) error { return json.Unmarshal(data, e) } -// UserCreated 用户创建事件 -type UserCreated struct { +// UserRegistered 用户注册事件 +type UserRegistered struct { *BaseUserEvent User *entities.User `json:"user"` } -func NewUserCreatedEvent(user *entities.User, correlationID string) *UserCreated { - return &UserCreated{ +func NewUserRegisteredEvent(user *entities.User, correlationID string) *UserRegistered { + return &UserRegistered{ BaseUserEvent: &BaseUserEvent{ ID: uuid.New().String(), - Type: string(UserCreatedEvent), + Type: string(UserRegisteredEvent), Version: "1.0", Timestamp: time.Now(), Source: "user-service", @@ -118,97 +112,28 @@ func NewUserCreatedEvent(user *entities.User, correlationID string) *UserCreated DomainVersion: "1.0", CorrelationID: correlationID, Metadata: map[string]interface{}{ - "user_id": user.ID, - "username": user.Username, - "email": user.Email, + "user_id": user.ID, + "phone": user.Phone, }, }, User: user, } } -func (e *UserCreated) GetPayload() interface{} { +func (e *UserRegistered) GetPayload() interface{} { return e.User } -// UserUpdated 用户更新事件 -type UserUpdated struct { - *BaseUserEvent - UserID string `json:"user_id"` - Changes map[string]interface{} `json:"changes"` - OldValues map[string]interface{} `json:"old_values"` - NewValues map[string]interface{} `json:"new_values"` -} - -func NewUserUpdatedEvent(userID string, changes, oldValues, newValues map[string]interface{}, correlationID string) *UserUpdated { - return &UserUpdated{ - BaseUserEvent: &BaseUserEvent{ - ID: uuid.New().String(), - Type: string(UserUpdatedEvent), - Version: "1.0", - Timestamp: time.Now(), - Source: "user-service", - AggregateID: userID, - AggregateType: "User", - DomainVersion: "1.0", - CorrelationID: correlationID, - Metadata: map[string]interface{}{ - "user_id": userID, - "changed_fields": len(changes), - }, - }, - UserID: userID, - Changes: changes, - OldValues: oldValues, - NewValues: newValues, - } -} - -// UserDeleted 用户删除事件 -type UserDeleted struct { - *BaseUserEvent - UserID string `json:"user_id"` - Username string `json:"username"` - Email string `json:"email"` - SoftDelete bool `json:"soft_delete"` -} - -func NewUserDeletedEvent(userID, username, email string, softDelete bool, correlationID string) *UserDeleted { - return &UserDeleted{ - BaseUserEvent: &BaseUserEvent{ - ID: uuid.New().String(), - Type: string(UserDeletedEvent), - Version: "1.0", - Timestamp: time.Now(), - Source: "user-service", - AggregateID: userID, - AggregateType: "User", - DomainVersion: "1.0", - CorrelationID: correlationID, - Metadata: map[string]interface{}{ - "user_id": userID, - "username": username, - "email": email, - "soft_delete": softDelete, - }, - }, - UserID: userID, - Username: username, - Email: email, - SoftDelete: softDelete, - } -} - // UserLoggedIn 用户登录事件 type UserLoggedIn struct { *BaseUserEvent UserID string `json:"user_id"` - Username string `json:"username"` + Phone string `json:"phone"` IPAddress string `json:"ip_address"` UserAgent string `json:"user_agent"` } -func NewUserLoggedInEvent(userID, username, ipAddress, userAgent, correlationID string) *UserLoggedIn { +func NewUserLoggedInEvent(userID, phone, ipAddress, userAgent, correlationID string) *UserLoggedIn { return &UserLoggedIn{ BaseUserEvent: &BaseUserEvent{ ID: uuid.New().String(), @@ -222,13 +147,13 @@ func NewUserLoggedInEvent(userID, username, ipAddress, userAgent, correlationID CorrelationID: correlationID, Metadata: map[string]interface{}{ "user_id": userID, - "username": username, + "phone": phone, "ip_address": ipAddress, "user_agent": userAgent, }, }, UserID: userID, - Username: username, + Phone: phone, IPAddress: ipAddress, UserAgent: userAgent, } @@ -237,11 +162,11 @@ func NewUserLoggedInEvent(userID, username, ipAddress, userAgent, correlationID // UserPasswordChanged 用户密码修改事件 type UserPasswordChanged struct { *BaseUserEvent - UserID string `json:"user_id"` - Username string `json:"username"` + UserID string `json:"user_id"` + Phone string `json:"phone"` } -func NewUserPasswordChangedEvent(userID, username, correlationID string) *UserPasswordChanged { +func NewUserPasswordChangedEvent(userID, phone, correlationID string) *UserPasswordChanged { return &UserPasswordChanged{ BaseUserEvent: &BaseUserEvent{ ID: uuid.New().String(), @@ -254,46 +179,11 @@ func NewUserPasswordChangedEvent(userID, username, correlationID string) *UserPa DomainVersion: "1.0", CorrelationID: correlationID, Metadata: map[string]interface{}{ - "user_id": userID, - "username": username, + "user_id": userID, + "phone": phone, }, }, - UserID: userID, - Username: username, - } -} - -// UserStatusChanged 用户状态变更事件 -type UserStatusChanged struct { - *BaseUserEvent - UserID string `json:"user_id"` - Username string `json:"username"` - OldStatus entities.UserStatus `json:"old_status"` - NewStatus entities.UserStatus `json:"new_status"` -} - -func NewUserStatusChangedEvent(userID, username string, oldStatus, newStatus entities.UserStatus, correlationID string) *UserStatusChanged { - return &UserStatusChanged{ - BaseUserEvent: &BaseUserEvent{ - ID: uuid.New().String(), - Type: string(UserStatusChangedEvent), - Version: "1.0", - Timestamp: time.Now(), - Source: "user-service", - AggregateID: userID, - AggregateType: "User", - DomainVersion: "1.0", - CorrelationID: correlationID, - Metadata: map[string]interface{}{ - "user_id": userID, - "username": username, - "old_status": oldStatus, - "new_status": newStatus, - }, - }, - UserID: userID, - Username: username, - OldStatus: oldStatus, - NewStatus: newStatus, + UserID: userID, + Phone: phone, } } diff --git a/internal/domains/user/handlers/user_handler.go b/internal/domains/user/handlers/user_handler.go index 8c9bb85..90f5862 100644 --- a/internal/domains/user/handlers/user_handler.go +++ b/internal/domains/user/handlers/user_handler.go @@ -1,7 +1,7 @@ package handlers import ( - "strconv" + "time" "github.com/gin-gonic/gin" "go.uber.org/zap" @@ -14,211 +14,123 @@ import ( // UserHandler 用户HTTP处理器 type UserHandler struct { - userService *services.UserService - response interfaces.ResponseBuilder - validator interfaces.RequestValidator - logger *zap.Logger - jwtAuth *middleware.JWTAuthMiddleware + userService interfaces.UserService + smsCodeService *services.SMSCodeService + response interfaces.ResponseBuilder + validator interfaces.RequestValidator + logger *zap.Logger + jwtAuth *middleware.JWTAuthMiddleware } // NewUserHandler 创建用户处理器 func NewUserHandler( - userService *services.UserService, + userService interfaces.UserService, + smsCodeService *services.SMSCodeService, response interfaces.ResponseBuilder, validator interfaces.RequestValidator, logger *zap.Logger, jwtAuth *middleware.JWTAuthMiddleware, ) *UserHandler { return &UserHandler{ - userService: userService, - response: response, - validator: validator, - logger: logger, - jwtAuth: jwtAuth, + userService: userService, + smsCodeService: smsCodeService, + response: response, + validator: validator, + logger: logger, + jwtAuth: jwtAuth, } } -// GetPath 返回处理器路径 -func (h *UserHandler) GetPath() string { - return "/users" -} - -// GetMethod 返回HTTP方法 -func (h *UserHandler) GetMethod() string { - return "GET" // 主要用于列表,具体方法在路由注册时指定 -} - -// GetMiddlewares 返回中间件 -func (h *UserHandler) GetMiddlewares() []gin.HandlerFunc { - return []gin.HandlerFunc{ - // 这里可以添加特定的中间件 - } -} - -// Handle 主处理函数(用于列表) -func (h *UserHandler) Handle(c *gin.Context) { - h.List(c) -} - -// RequiresAuth 是否需要认证 -func (h *UserHandler) RequiresAuth() bool { - return true -} - -// GetPermissions 获取所需权限 -func (h *UserHandler) GetPermissions() []string { - return []string{"user:read"} -} - -// REST操作实现 - -// Create 创建用户 -func (h *UserHandler) Create(c *gin.Context) { - var req dto.CreateUserRequest +// SendCode 发送验证码 +// @Summary 发送短信验证码 +// @Description 向指定手机号发送验证码,支持注册、登录、修改密码等场景 +// @Tags 用户认证 +// @Accept json +// @Produce json +// @Param request body dto.SendCodeRequest true "发送验证码请求" +// @Success 200 {object} dto.SendCodeResponse "验证码发送成功" +// @Failure 400 {object} map[string]interface{} "请求参数错误" +// @Failure 429 {object} map[string]interface{} "请求频率限制" +// @Failure 500 {object} map[string]interface{} "服务器内部错误" +// @Router /users/send-code [post] +func (h *UserHandler) SendCode(c *gin.Context) { + var req dto.SendCodeRequest // 验证请求体 if err := h.validator.BindAndValidate(c, &req); err != nil { return // 响应已在验证器中处理 } - // 创建用户 - user, err := h.userService.Create(c.Request.Context(), &req) - if err != nil { - h.logger.Error("Failed to create user", zap.Error(err)) + // 获取客户端信息 + clientIP := c.ClientIP() + userAgent := c.GetHeader("User-Agent") + + // 发送验证码 + if err := h.smsCodeService.SendCode(c.Request.Context(), req.Phone, req.Scene, clientIP, userAgent); err != nil { + h.logger.Error("发送验证码失败", + zap.String("phone", req.Phone), + zap.String("scene", string(req.Scene)), + zap.Error(err)) h.response.BadRequest(c, err.Error()) return } - // 返回响应 - response := dto.FromEntity(user) - h.response.Created(c, response, "User created successfully") -} - -// GetByID 根据ID获取用户 -func (h *UserHandler) GetByID(c *gin.Context) { - id := c.Param("id") - if id == "" { - h.response.BadRequest(c, "User ID is required") - return - } - - // 获取用户 - user, err := h.userService.GetByID(c.Request.Context(), id) - if err != nil { - h.logger.Error("Failed to get user", zap.Error(err)) - h.response.NotFound(c, "User not found") - return - } - - // 返回响应 - response := dto.FromEntity(user) - h.response.Success(c, response) -} - -// Update 更新用户 -func (h *UserHandler) Update(c *gin.Context) { - id := c.Param("id") - if id == "" { - h.response.BadRequest(c, "User ID is required") - return - } - - var req dto.UpdateUserRequest - - // 验证请求体 - if err := h.validator.BindAndValidate(c, &req); err != nil { - return - } - - // 更新用户 - user, err := h.userService.Update(c.Request.Context(), id, &req) - if err != nil { - h.logger.Error("Failed to update user", zap.Error(err)) - h.response.BadRequest(c, err.Error()) - return - } - - // 返回响应 - response := dto.FromEntity(user) - h.response.Success(c, response, "User updated successfully") -} - -// Delete 删除用户 -func (h *UserHandler) Delete(c *gin.Context) { - id := c.Param("id") - if id == "" { - h.response.BadRequest(c, "User ID is required") - return - } - - // 删除用户 - if err := h.userService.Delete(c.Request.Context(), id); err != nil { - h.logger.Error("Failed to delete user", zap.Error(err)) - h.response.BadRequest(c, err.Error()) - return - } - - // 返回响应 - h.response.Success(c, nil, "User deleted successfully") -} - -// List 获取用户列表 -func (h *UserHandler) List(c *gin.Context) { - var req dto.UserListRequest - - // 验证查询参数 - if err := h.validator.ValidateQuery(c, &req); err != nil { - return - } - - // 设置默认值 - if req.Page <= 0 { - req.Page = 1 - } - if req.PageSize <= 0 { - req.PageSize = 20 - } - - // 构建查询选项 - options := interfaces.ListOptions{ - Page: req.Page, - PageSize: req.PageSize, - Sort: req.Sort, - Order: req.Order, - Search: req.Search, - Filters: req.Filters, - } - - // 获取用户列表 - users, err := h.userService.List(c.Request.Context(), options) - if err != nil { - h.logger.Error("Failed to get user list", zap.Error(err)) - h.response.InternalError(c, "Failed to get user list") - return - } - - // 获取总数 - countOptions := interfaces.CountOptions{ - Search: req.Search, - Filters: req.Filters, - } - total, err := h.userService.Count(c.Request.Context(), countOptions) - if err != nil { - h.logger.Error("Failed to count users", zap.Error(err)) - h.response.InternalError(c, "Failed to count users") - return - } - // 构建响应 - userResponses := dto.FromEntities(users) - pagination := buildPagination(req.Page, req.PageSize, total) + response := &dto.SendCodeResponse{ + Message: "验证码发送成功", + ExpiresAt: time.Now().Add(5 * time.Minute), // 5分钟过期 + } - h.response.Paginated(c, userResponses, pagination) + h.response.Success(c, response, "验证码发送成功") } -// Login 用户登录 -func (h *UserHandler) Login(c *gin.Context) { - var req dto.LoginRequest +// Register 用户注册 +// @Summary 用户注册 +// @Description 使用手机号、密码和验证码进行用户注册,需要确认密码 +// @Tags 用户认证 +// @Accept json +// @Produce json +// @Param request body dto.RegisterRequest true "用户注册请求" +// @Success 201 {object} dto.UserResponse "注册成功" +// @Failure 400 {object} map[string]interface{} "请求参数错误或验证码无效" +// @Failure 409 {object} map[string]interface{} "手机号已存在" +// @Failure 500 {object} map[string]interface{} "服务器内部错误" +// @Router /users/register [post] +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)) + h.response.BadRequest(c, err.Error()) + return + } + + // 返回响应 + response := dto.FromEntity(user) + h.response.Created(c, response, "用户注册成功") +} + +// LoginWithPassword 密码登录 +// @Summary 用户密码登录 +// @Description 使用手机号和密码进行用户登录,返回JWT令牌 +// @Tags 用户认证 +// @Accept json +// @Produce json +// @Param request body dto.LoginWithPasswordRequest true "密码登录请求" +// @Success 200 {object} dto.LoginResponse "登录成功" +// @Failure 400 {object} map[string]interface{} "请求参数错误" +// @Failure 401 {object} map[string]interface{} "认证失败" +// @Failure 500 {object} map[string]interface{} "服务器内部错误" +// @Router /users/login-password [post] +func (h *UserHandler) LoginWithPassword(c *gin.Context) { + var req dto.LoginWithPasswordRequest // 验证请求体 if err := h.validator.BindAndValidate(c, &req); err != nil { @@ -226,18 +138,18 @@ func (h *UserHandler) Login(c *gin.Context) { } // 用户登录 - user, err := h.userService.Login(c.Request.Context(), &req) + user, err := h.userService.LoginWithPassword(c.Request.Context(), &req) if err != nil { - h.logger.Error("Login failed", zap.Error(err)) - h.response.Unauthorized(c, "Invalid credentials") + h.logger.Error("密码登录失败", zap.Error(err)) + h.response.Unauthorized(c, "用户名或密码错误") return } // 生成JWT token - accessToken, err := h.jwtAuth.GenerateToken(user.ID, user.Username, user.Email) + accessToken, err := h.jwtAuth.GenerateToken(user.ID, user.Phone, user.Phone) if err != nil { - h.logger.Error("Failed to generate token", zap.Error(err)) - h.response.InternalError(c, "Failed to generate access token") + h.logger.Error("生成令牌失败", zap.Error(err)) + h.response.InternalError(c, "生成访问令牌失败") return } @@ -247,72 +159,109 @@ func (h *UserHandler) Login(c *gin.Context) { AccessToken: accessToken, TokenType: "Bearer", ExpiresIn: 86400, // 24小时,从配置获取 + LoginMethod: "password", } - h.response.Success(c, loginResponse, "Login successful") + h.response.Success(c, loginResponse, "登录成功") } -// Logout 用户登出 -func (h *UserHandler) Logout(c *gin.Context) { - // 简单实现,客户端删除token即可 - // 如果需要服务端黑名单,可以在这里实现 - h.response.Success(c, nil, "Logout successful") -} - -// GetProfile 获取当前用户信息 -func (h *UserHandler) GetProfile(c *gin.Context) { - userID := h.getCurrentUserID(c) - if userID == "" { - h.response.Unauthorized(c, "User not authenticated") - return - } - - // 获取用户信息 - user, err := h.userService.GetByID(c.Request.Context(), userID) - if err != nil { - h.logger.Error("Failed to get user profile", zap.Error(err)) - h.response.NotFound(c, "User not found") - return - } - - // 返回响应 - response := dto.FromEntity(user) - h.response.Success(c, response) -} - -// UpdateProfile 更新当前用户信息 -func (h *UserHandler) UpdateProfile(c *gin.Context) { - userID := h.getCurrentUserID(c) - if userID == "" { - h.response.Unauthorized(c, "User not authenticated") - return - } - - var req dto.UpdateUserRequest +// LoginWithSMS 短信验证码登录 +// @Summary 用户短信验证码登录 +// @Description 使用手机号和短信验证码进行用户登录,返回JWT令牌 +// @Tags 用户认证 +// @Accept json +// @Produce json +// @Param request body dto.LoginWithSMSRequest true "短信登录请求" +// @Success 200 {object} dto.LoginResponse "登录成功" +// @Failure 400 {object} map[string]interface{} "请求参数错误或验证码无效" +// @Failure 401 {object} map[string]interface{} "认证失败" +// @Failure 500 {object} map[string]interface{} "服务器内部错误" +// @Router /users/login-sms [post] +func (h *UserHandler) LoginWithSMS(c *gin.Context) { + var req dto.LoginWithSMSRequest // 验证请求体 if err := h.validator.BindAndValidate(c, &req); err != nil { return } - // 更新用户 - user, err := h.userService.Update(c.Request.Context(), userID, &req) + // 用户登录 + user, err := h.userService.LoginWithSMS(c.Request.Context(), &req) if err != nil { - h.logger.Error("Failed to update profile", zap.Error(err)) - h.response.BadRequest(c, err.Error()) + h.logger.Error("短信登录失败", zap.Error(err)) + h.response.Unauthorized(c, err.Error()) return } - // 返回响应 + // 生成JWT token + accessToken, err := h.jwtAuth.GenerateToken(user.ID, user.Phone, user.Phone) + if err != nil { + h.logger.Error("生成令牌失败", zap.Error(err)) + h.response.InternalError(c, "生成访问令牌失败") + return + } + + // 构建登录响应 + loginResponse := &dto.LoginResponse{ + User: dto.FromEntity(user), + AccessToken: accessToken, + TokenType: "Bearer", + ExpiresIn: 86400, // 24小时,从配置获取 + LoginMethod: "sms", + } + + h.response.Success(c, loginResponse, "登录成功") +} + +// GetProfile 获取当前用户信息 +// @Summary 获取当前用户信息 +// @Description 根据JWT令牌获取当前登录用户的详细信息 +// @Tags 用户管理 +// @Accept json +// @Produce json +// @Security Bearer +// @Success 200 {object} dto.UserResponse "用户信息" +// @Failure 401 {object} map[string]interface{} "未认证" +// @Failure 404 {object} map[string]interface{} "用户不存在" +// @Failure 500 {object} map[string]interface{} "服务器内部错误" +// @Router /users/me [get] +func (h *UserHandler) GetProfile(c *gin.Context) { + userID := h.getCurrentUserID(c) + if userID == "" { + h.response.Unauthorized(c, "用户未认证") + return + } + + // 获取用户信息 + user, err := h.userService.GetByID(c.Request.Context(), userID) + if err != nil { + h.logger.Error("获取用户资料失败", zap.Error(err)) + h.response.NotFound(c, "用户不存在") + return + } + + // 返回用户信息 response := dto.FromEntity(user) - h.response.Success(c, response, "Profile updated successfully") + h.response.Success(c, response, "获取用户资料成功") } // ChangePassword 修改密码 +// @Summary 修改密码 +// @Description 使用旧密码、新密码确认和验证码修改当前用户的密码 +// @Tags 用户管理 +// @Accept json +// @Produce json +// @Security Bearer +// @Param request body dto.ChangePasswordRequest true "修改密码请求" +// @Success 200 {object} map[string]interface{} "密码修改成功" +// @Failure 400 {object} map[string]interface{} "请求参数错误或验证码无效" +// @Failure 401 {object} map[string]interface{} "未认证" +// @Failure 500 {object} map[string]interface{} "服务器内部错误" +// @Router /users/me/password [put] func (h *UserHandler) ChangePassword(c *gin.Context) { userID := h.getCurrentUserID(c) if userID == "" { - h.response.Unauthorized(c, "User not authenticated") + h.response.Unauthorized(c, "用户未认证") return } @@ -325,78 +274,14 @@ func (h *UserHandler) ChangePassword(c *gin.Context) { // 修改密码 if err := h.userService.ChangePassword(c.Request.Context(), userID, &req); err != nil { - h.logger.Error("Failed to change password", zap.Error(err)) + h.logger.Error("修改密码失败", zap.Error(err)) h.response.BadRequest(c, err.Error()) return } - h.response.Success(c, nil, "Password changed successfully") + h.response.Success(c, nil, "密码修改成功") } -// Search 搜索用户 -func (h *UserHandler) Search(c *gin.Context) { - var req dto.UserSearchRequest - - // 验证查询参数 - if err := h.validator.ValidateQuery(c, &req); err != nil { - return - } - - // 设置默认值 - if req.Page <= 0 { - req.Page = 1 - } - if req.PageSize <= 0 { - req.PageSize = 10 - } - - // 构建查询选项 - options := interfaces.ListOptions{ - Page: req.Page, - PageSize: req.PageSize, - Search: req.Query, - } - - // 搜索用户 - users, err := h.userService.Search(c.Request.Context(), req.Query, options) - if err != nil { - h.logger.Error("Failed to search users", zap.Error(err)) - h.response.InternalError(c, "Failed to search users") - return - } - - // 获取搜索结果总数 - countOptions := interfaces.CountOptions{ - Search: req.Query, - } - total, err := h.userService.Count(c.Request.Context(), countOptions) - if err != nil { - h.logger.Error("Failed to count search results", zap.Error(err)) - h.response.InternalError(c, "Failed to count search results") - return - } - - // 构建响应 - userResponses := dto.FromEntities(users) - pagination := buildPagination(req.Page, req.PageSize, total) - - h.response.Paginated(c, userResponses, pagination) -} - -// GetStats 获取用户统计 -func (h *UserHandler) GetStats(c *gin.Context) { - stats, err := h.userService.GetStats(c.Request.Context()) - if err != nil { - h.logger.Error("Failed to get user stats", zap.Error(err)) - h.response.InternalError(c, "Failed to get user statistics") - return - } - - h.response.Success(c, stats) -} - -// 私有方法 - // getCurrentUserID 获取当前用户ID func (h *UserHandler) getCurrentUserID(c *gin.Context) string { if userID, exists := c.Get("user_id"); exists { @@ -406,50 +291,3 @@ func (h *UserHandler) getCurrentUserID(c *gin.Context) string { } return "" } - -// parsePageSize 解析页面大小 -func (h *UserHandler) parsePageSize(str string, defaultValue int) int { - if str == "" { - return defaultValue - } - - if size, err := strconv.Atoi(str); err == nil && size > 0 && size <= 100 { - return size - } - - return defaultValue -} - -// parsePage 解析页码 -func (h *UserHandler) parsePage(str string, defaultValue int) int { - if str == "" { - return defaultValue - } - - if page, err := strconv.Atoi(str); err == nil && page > 0 { - return page - } - - return defaultValue -} - -// buildPagination 构建分页元数据 -func buildPagination(page, pageSize int, total int64) interfaces.PaginationMeta { - totalPages := int(float64(total) / float64(pageSize)) - if float64(total)/float64(pageSize) > float64(totalPages) { - totalPages++ - } - - if totalPages < 1 { - totalPages = 1 - } - - return interfaces.PaginationMeta{ - Page: page, - PageSize: pageSize, - Total: total, - TotalPages: totalPages, - HasNext: page < totalPages, - HasPrev: page > 1, - } -} diff --git a/internal/domains/user/repositories/sms_code_repository.go b/internal/domains/user/repositories/sms_code_repository.go new file mode 100644 index 0000000..f23b20d --- /dev/null +++ b/internal/domains/user/repositories/sms_code_repository.go @@ -0,0 +1,120 @@ +package repositories + +import ( + "context" + "fmt" + "time" + + "go.uber.org/zap" + "gorm.io/gorm" + + "tyapi-server/internal/domains/user/entities" + "tyapi-server/internal/shared/interfaces" +) + +// SMSCodeRepository 短信验证码仓储 +type SMSCodeRepository struct { + db *gorm.DB + cache interfaces.CacheService + logger *zap.Logger +} + +// NewSMSCodeRepository 创建短信验证码仓储 +func NewSMSCodeRepository(db *gorm.DB, cache interfaces.CacheService, logger *zap.Logger) *SMSCodeRepository { + return &SMSCodeRepository{ + db: db, + cache: cache, + logger: logger, + } +} + +// Create 创建短信验证码记录 +func (r *SMSCodeRepository) Create(ctx context.Context, smsCode *entities.SMSCode) error { + if err := r.db.WithContext(ctx).Create(smsCode).Error; err != nil { + r.logger.Error("创建短信验证码失败", zap.Error(err)) + return err + } + + // 缓存验证码 + cacheKey := r.buildCacheKey(smsCode.Phone, smsCode.Scene) + r.cache.Set(ctx, cacheKey, smsCode, 5*time.Minute) + + return nil +} + +// GetValidCode 获取有效的验证码 +func (r *SMSCodeRepository) GetValidCode(ctx context.Context, phone string, scene entities.SMSScene) (*entities.SMSCode, error) { + // 先从缓存查找 + cacheKey := r.buildCacheKey(phone, scene) + var smsCode entities.SMSCode + if err := r.cache.Get(ctx, cacheKey, &smsCode); err == nil { + return &smsCode, nil + } + + // 从数据库查找最新的有效验证码 + if err := r.db.WithContext(ctx). + Where("phone = ? AND scene = ? AND expires_at > ? AND used_at IS NULL", + phone, scene, time.Now()). + Order("created_at DESC"). + First(&smsCode).Error; err != nil { + return nil, err + } + + // 缓存结果 + r.cache.Set(ctx, cacheKey, &smsCode, 5*time.Minute) + + return &smsCode, nil +} + +// MarkAsUsed 标记验证码为已使用 +func (r *SMSCodeRepository) MarkAsUsed(ctx context.Context, id string) error { + now := time.Now() + if err := r.db.WithContext(ctx). + Model(&entities.SMSCode{}). + Where("id = ?", id). + Update("used_at", now).Error; err != nil { + r.logger.Error("标记验证码为已使用失败", zap.Error(err)) + return err + } + + r.logger.Info("验证码已标记为使用", zap.String("code_id", id)) + return nil +} + +// CleanupExpired 清理过期的验证码 +func (r *SMSCodeRepository) CleanupExpired(ctx context.Context) error { + result := r.db.WithContext(ctx). + Where("expires_at < ?", time.Now()). + Delete(&entities.SMSCode{}) + + if result.Error != nil { + r.logger.Error("清理过期验证码失败", zap.Error(result.Error)) + return result.Error + } + + if result.RowsAffected > 0 { + r.logger.Info("清理过期验证码完成", zap.Int64("count", result.RowsAffected)) + } + + return nil +} + +// CountRecentCodes 统计最近发送的验证码数量 +func (r *SMSCodeRepository) CountRecentCodes(ctx context.Context, phone string, scene entities.SMSScene, duration time.Duration) (int64, error) { + var count int64 + if err := r.db.WithContext(ctx). + Model(&entities.SMSCode{}). + Where("phone = ? AND scene = ? AND created_at > ?", + phone, scene, time.Now().Add(-duration)). + Count(&count).Error; err != nil { + r.logger.Error("统计最近验证码数量失败", zap.Error(err)) + return 0, err + } + + return count, nil +} + +// buildCacheKey 构建缓存键 +func (r *SMSCodeRepository) buildCacheKey(phone string, scene entities.SMSScene) string { + return fmt.Sprintf("sms_code:%s:%s", phone, string(scene)) +} diff --git a/internal/domains/user/repositories/user_repository.go b/internal/domains/user/repositories/user_repository.go index 4cf3bf1..2fd7374 100644 --- a/internal/domains/user/repositories/user_repository.go +++ b/internal/domains/user/repositories/user_repository.go @@ -2,6 +2,7 @@ package repositories import ( "context" + "errors" "fmt" "time" @@ -12,6 +13,12 @@ import ( "tyapi-server/internal/shared/interfaces" ) +// 定义错误常量 +var ( + // ErrUserNotFound 用户不存在错误 + ErrUserNotFound = errors.New("用户不存在") +) + // UserRepository 用户仓储实现 type UserRepository struct { db *gorm.DB @@ -29,311 +36,150 @@ func NewUserRepository(db *gorm.DB, cache interfaces.CacheService, logger *zap.L } // Create 创建用户 -func (r *UserRepository) Create(ctx context.Context, entity *entities.User) error { - if err := r.db.WithContext(ctx).Create(entity).Error; err != nil { - r.logger.Error("Failed to create user", zap.Error(err)) +func (r *UserRepository) Create(ctx context.Context, user *entities.User) error { + if err := r.db.WithContext(ctx).Create(user).Error; err != nil { + r.logger.Error("创建用户失败", zap.Error(err)) return err } // 清除相关缓存 - r.invalidateUserCaches(ctx, entity.ID) + r.deleteCacheByPhone(ctx, user.Phone) + r.logger.Info("用户创建成功", zap.String("user_id", user.ID)) return nil } // GetByID 根据ID获取用户 func (r *UserRepository) GetByID(ctx context.Context, id string) (*entities.User, error) { - // 先尝试从缓存获取 - cacheKey := r.GetCacheKey(id) + // 尝试从缓存获取 + cacheKey := fmt.Sprintf("user:id:%s", id) var user entities.User - if err := r.cache.Get(ctx, cacheKey, &user); err == nil { return &user, nil } - // 从数据库获取 - if err := r.db.WithContext(ctx).Where("id = ? AND is_deleted = false", id).First(&user).Error; err != nil { - if err == gorm.ErrRecordNotFound { - return nil, fmt.Errorf("user not found") + // 从数据库查询 + if err := r.db.WithContext(ctx).Where("id = ?", id).First(&user).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrUserNotFound } + r.logger.Error("根据ID查询用户失败", zap.Error(err)) return nil, err } // 缓存结果 - r.cache.Set(ctx, cacheKey, &user, 1*time.Hour) + r.cache.Set(ctx, cacheKey, &user, 10*time.Minute) + + return &user, nil +} + +// FindByPhone 根据手机号查找用户 +func (r *UserRepository) FindByPhone(ctx context.Context, phone string) (*entities.User, error) { + // 尝试从缓存获取 + cacheKey := fmt.Sprintf("user:phone:%s", phone) + var user entities.User + if err := r.cache.Get(ctx, cacheKey, &user); err == nil { + return &user, nil + } + + // 从数据库查询 + if err := r.db.WithContext(ctx).Where("phone = ?", phone).First(&user).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrUserNotFound + } + r.logger.Error("根据手机号查询用户失败", zap.Error(err)) + return nil, err + } + + // 缓存结果 + r.cache.Set(ctx, cacheKey, &user, 10*time.Minute) return &user, nil } // Update 更新用户 -func (r *UserRepository) Update(ctx context.Context, entity *entities.User) error { - if err := r.db.WithContext(ctx).Save(entity).Error; err != nil { - r.logger.Error("Failed to update user", zap.Error(err)) +func (r *UserRepository) Update(ctx context.Context, user *entities.User) error { + if err := r.db.WithContext(ctx).Save(user).Error; err != nil { + r.logger.Error("更新用户失败", zap.Error(err)) return err } // 清除相关缓存 - r.invalidateUserCaches(ctx, entity.ID) + r.deleteCacheByID(ctx, user.ID) + r.deleteCacheByPhone(ctx, user.Phone) + r.logger.Info("用户更新成功", zap.String("user_id", user.ID)) return nil } // Delete 删除用户 func (r *UserRepository) Delete(ctx context.Context, id string) error { + // 先获取用户信息用于清除缓存 + user, err := r.GetByID(ctx, id) + if err != nil { + return err + } + if err := r.db.WithContext(ctx).Delete(&entities.User{}, "id = ?", id).Error; err != nil { - r.logger.Error("Failed to delete user", zap.Error(err)) + r.logger.Error("删除用户失败", zap.Error(err)) return err } // 清除相关缓存 - r.invalidateUserCaches(ctx, id) + r.deleteCacheByID(ctx, id) + r.deleteCacheByPhone(ctx, user.Phone) + r.logger.Info("用户删除成功", zap.String("user_id", id)) return nil } -// CreateBatch 批量创建用户 -func (r *UserRepository) CreateBatch(ctx context.Context, entities []*entities.User) error { - if err := r.db.WithContext(ctx).CreateInBatches(entities, 100).Error; err != nil { - r.logger.Error("Failed to create users in batch", zap.Error(err)) - return err - } - - // 清除列表缓存 - r.cache.DeletePattern(ctx, "users:list:*") - - return nil -} - -// GetByIDs 根据ID列表获取用户 -func (r *UserRepository) GetByIDs(ctx context.Context, ids []string) ([]*entities.User, error) { - var users []entities.User - - if err := r.db.WithContext(ctx). - Where("id IN ? AND is_deleted = false", ids). - Find(&users).Error; err != nil { - return nil, err - } - - // 转换为指针切片 - result := make([]*entities.User, len(users)) - for i := range users { - result[i] = &users[i] - } - - return result, nil -} - -// UpdateBatch 批量更新用户 -func (r *UserRepository) UpdateBatch(ctx context.Context, entities []*entities.User) error { - return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { - for _, entity := range entities { - if err := tx.Save(entity).Error; err != nil { - return err - } - } - return nil - }) -} - -// DeleteBatch 批量删除用户 -func (r *UserRepository) DeleteBatch(ctx context.Context, ids []string) error { - if err := r.db.WithContext(ctx). - Where("id IN ?", ids). - Delete(&entities.User{}).Error; err != nil { - return err - } - - // 清除相关缓存 - for _, id := range ids { - r.invalidateUserCaches(ctx, id) - } - - return nil -} - -// List 获取用户列表 -func (r *UserRepository) List(ctx context.Context, options interfaces.ListOptions) ([]*entities.User, error) { - // 尝试从缓存获取 - cacheKey := fmt.Sprintf("users:list:%d:%d:%s", options.Page, options.PageSize, options.Sort) +// List 分页获取用户列表 +func (r *UserRepository) List(ctx context.Context, offset, limit int) ([]*entities.User, error) { var users []*entities.User - - if err := r.cache.Get(ctx, cacheKey, &users); err == nil { - return users, nil - } - - // 从数据库查询 - query := r.db.WithContext(ctx).Where("is_deleted = false") - - // 应用过滤条件 - if options.Search != "" { - query = query.Where("username ILIKE ? OR email ILIKE ? OR first_name ILIKE ? OR last_name ILIKE ?", - "%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%") - } - - // 应用排序 - if options.Sort != "" { - order := options.Order - if order == "" { - order = "asc" - } - query = query.Order(fmt.Sprintf("%s %s", options.Sort, order)) - } else { - query = query.Order("created_at desc") - } - - // 应用分页 - if options.Page > 0 && options.PageSize > 0 { - offset := (options.Page - 1) * options.PageSize - query = query.Offset(offset).Limit(options.PageSize) - } - - var userEntities []entities.User - if err := query.Find(&userEntities).Error; err != nil { + if err := r.db.WithContext(ctx).Offset(offset).Limit(limit).Find(&users).Error; err != nil { + r.logger.Error("查询用户列表失败", zap.Error(err)) return nil, err } - // 转换为指针切片 - users = make([]*entities.User, len(userEntities)) - for i := range userEntities { - users[i] = &userEntities[i] - } - - // 缓存结果 - r.cache.Set(ctx, cacheKey, users, 30*time.Minute) - return users, nil } -// Count 统计用户数量 -func (r *UserRepository) Count(ctx context.Context, options interfaces.CountOptions) (int64, error) { - query := r.db.WithContext(ctx).Model(&entities.User{}).Where("is_deleted = false") - - // 应用过滤条件 - if options.Search != "" { - query = query.Where("username ILIKE ? OR email ILIKE ? OR first_name ILIKE ? OR last_name ILIKE ?", - "%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%") - } - +// Count 获取用户总数 +func (r *UserRepository) Count(ctx context.Context) (int64, error) { var count int64 - if err := query.Count(&count).Error; err != nil { + if err := r.db.WithContext(ctx).Model(&entities.User{}).Count(&count).Error; err != nil { + r.logger.Error("统计用户数量失败", zap.Error(err)) return 0, err } return count, nil } -// Exists 检查用户是否存在 -func (r *UserRepository) Exists(ctx context.Context, id string) (bool, error) { +// ExistsByPhone 检查手机号是否存在 +func (r *UserRepository) ExistsByPhone(ctx context.Context, phone string) (bool, error) { var count int64 - if err := r.db.WithContext(ctx). - Model(&entities.User{}). - Where("id = ? AND is_deleted = false", id). - Count(&count).Error; err != nil { + if err := r.db.WithContext(ctx).Model(&entities.User{}).Where("phone = ?", phone).Count(&count).Error; err != nil { + r.logger.Error("检查手机号是否存在失败", zap.Error(err)) return false, err } return count > 0, nil } -// SoftDelete 软删除用户 -func (r *UserRepository) SoftDelete(ctx context.Context, id string) error { - if err := r.db.WithContext(ctx). - Model(&entities.User{}). - Where("id = ?", id). - Update("is_deleted", true).Error; err != nil { - return err - } +// 私有辅助方法 - // 清除相关缓存 - r.invalidateUserCaches(ctx, id) - - return nil -} - -// Restore 恢复用户 -func (r *UserRepository) Restore(ctx context.Context, id string) error { - if err := r.db.WithContext(ctx). - Model(&entities.User{}). - Where("id = ?", id). - Update("is_deleted", false).Error; err != nil { - return err - } - - // 清除相关缓存 - r.invalidateUserCaches(ctx, id) - - return nil -} - -// WithTx 使用事务 -func (r *UserRepository) WithTx(tx interface{}) interfaces.Repository[*entities.User] { - gormTx, ok := tx.(*gorm.DB) - if !ok { - return r - } - - return &UserRepository{ - db: gormTx, - cache: r.cache, - logger: r.logger, +// deleteCacheByID 根据ID删除缓存 +func (r *UserRepository) deleteCacheByID(ctx context.Context, id string) { + cacheKey := fmt.Sprintf("user:id:%s", id) + if err := r.cache.Delete(ctx, cacheKey); err != nil { + r.logger.Warn("删除用户ID缓存失败", zap.String("cache_key", cacheKey), zap.Error(err)) } } -// InvalidateCache 清除缓存 -func (r *UserRepository) InvalidateCache(ctx context.Context, keys ...string) error { - return r.cache.Delete(ctx, keys...) -} - -// WarmupCache 预热缓存 -func (r *UserRepository) WarmupCache(ctx context.Context) error { - // 预热热门用户数据 - // 这里可以实现具体的预热逻辑 - return nil -} - -// GetCacheKey 获取缓存键 -func (r *UserRepository) GetCacheKey(id string) string { - return fmt.Sprintf("user:%s", id) -} - -// FindByUsername 根据用户名查找用户 -func (r *UserRepository) FindByUsername(ctx context.Context, username string) (*entities.User, error) { - var user entities.User - - if err := r.db.WithContext(ctx). - Where("username = ? AND is_deleted = false", username). - First(&user).Error; err != nil { - if err == gorm.ErrRecordNotFound { - return nil, fmt.Errorf("user not found") - } - return nil, err +// deleteCacheByPhone 根据手机号删除缓存 +func (r *UserRepository) deleteCacheByPhone(ctx context.Context, phone string) { + cacheKey := fmt.Sprintf("user:phone:%s", phone) + if err := r.cache.Delete(ctx, cacheKey); err != nil { + r.logger.Warn("删除用户手机号缓存失败", zap.String("cache_key", cacheKey), zap.Error(err)) } - - return &user, nil -} - -// FindByEmail 根据邮箱查找用户 -func (r *UserRepository) FindByEmail(ctx context.Context, email string) (*entities.User, error) { - var user entities.User - - if err := r.db.WithContext(ctx). - Where("email = ? AND is_deleted = false", email). - First(&user).Error; err != nil { - if err == gorm.ErrRecordNotFound { - return nil, fmt.Errorf("user not found") - } - return nil, err - } - - return &user, nil -} - -// invalidateUserCaches 清除用户相关缓存 -func (r *UserRepository) invalidateUserCaches(ctx context.Context, userID string) { - keys := []string{ - r.GetCacheKey(userID), - } - - r.cache.Delete(ctx, keys...) - r.cache.DeletePattern(ctx, "users:list:*") } diff --git a/internal/domains/user/routes/user_routes.go b/internal/domains/user/routes/user_routes.go index 32bd510..5a71a79 100644 --- a/internal/domains/user/routes/user_routes.go +++ b/internal/domains/user/routes/user_routes.go @@ -7,127 +7,23 @@ import ( "github.com/gin-gonic/gin" ) -// UserRoutes 用户路由注册器 -type UserRoutes struct { - handler *handlers.UserHandler - jwtAuth *middleware.JWTAuthMiddleware - optionalAuth *middleware.OptionalAuthMiddleware -} - -// NewUserRoutes 创建用户路由注册器 -func NewUserRoutes( - handler *handlers.UserHandler, - jwtAuth *middleware.JWTAuthMiddleware, - optionalAuth *middleware.OptionalAuthMiddleware, -) *UserRoutes { - return &UserRoutes{ - handler: handler, - jwtAuth: jwtAuth, - optionalAuth: optionalAuth, - } -} - -// RegisterRoutes 注册用户路由 -func (r *UserRoutes) RegisterRoutes(router *gin.Engine) { - // API版本组 - v1 := router.Group("/api/v1") - - // 公开路由(不需要认证) - public := v1.Group("/auth") +// UserRoutes 注册用户相关路由 +func UserRoutes(router *gin.Engine, handler *handlers.UserHandler, authMiddleware *middleware.JWTAuthMiddleware) { + // 用户域路由组 + usersGroup := router.Group("/api/v1/users") { - public.POST("/login", r.handler.Login) - public.POST("/register", r.handler.Create) - } + // 公开路由(不需要认证) + usersGroup.POST("/send-code", handler.SendCode) // 发送验证码 + usersGroup.POST("/register", handler.Register) // 用户注册 + usersGroup.POST("/login-password", handler.LoginWithPassword) // 密码登录 + usersGroup.POST("/login-sms", handler.LoginWithSMS) // 短信验证码登录 - // 需要认证的路由 - protected := v1.Group("/users") - protected.Use(r.jwtAuth.Handle()) - { - // 用户管理(管理员) - protected.GET("", r.handler.List) - protected.POST("", r.handler.Create) - protected.GET("/:id", r.handler.GetByID) - protected.PUT("/:id", r.handler.Update) - protected.DELETE("/:id", r.handler.Delete) - - // 用户搜索 - protected.GET("/search", r.handler.Search) - - // 用户统计 - protected.GET("/stats", r.handler.GetStats) - } - - // 用户个人操作路由 - profile := v1.Group("/profile") - profile.Use(r.jwtAuth.Handle()) - { - profile.GET("", r.handler.GetProfile) - profile.PUT("", r.handler.UpdateProfile) - profile.POST("/change-password", r.handler.ChangePassword) - profile.POST("/logout", r.handler.Logout) - } -} - -// RegisterPublicRoutes 注册公开路由 -func (r *UserRoutes) RegisterPublicRoutes(router *gin.Engine) { - v1 := router.Group("/api/v1") - - // 公开的用户相关路由 - public := v1.Group("/public") - { - // 可选认证的路由(用户可能登录也可能未登录) - public.Use(r.optionalAuth.Handle()) - - // 这里可以添加一些公开的用户信息查询接口 - // 比如根据用户名查看公开信息(如果用户设置为公开) - } -} - -// RegisterAdminRoutes 注册管理员路由 -func (r *UserRoutes) RegisterAdminRoutes(router *gin.Engine) { - admin := router.Group("/admin/v1") - admin.Use(r.jwtAuth.Handle()) - // 这里可以添加管理员权限检查中间件 - - // 管理员用户管理 - users := admin.Group("/users") - { - users.GET("", r.handler.List) - users.GET("/:id", r.handler.GetByID) - users.PUT("/:id", r.handler.Update) - users.DELETE("/:id", r.handler.Delete) - users.GET("/stats", r.handler.GetStats) - users.GET("/search", r.handler.Search) - - // 批量操作 - users.POST("/batch-delete", r.handleBatchDelete) - users.POST("/batch-update", r.handleBatchUpdate) - } -} - -// 批量删除处理器 -func (r *UserRoutes) handleBatchDelete(c *gin.Context) { - // 实现批量删除逻辑 - // 这里可以接收用户ID列表并调用服务进行批量删除 - c.JSON(200, gin.H{"message": "Batch delete not implemented yet"}) -} - -// 批量更新处理器 -func (r *UserRoutes) handleBatchUpdate(c *gin.Context) { - // 实现批量更新逻辑 - c.JSON(200, gin.H{"message": "Batch update not implemented yet"}) -} - -// RegisterHealthRoutes 注册健康检查路由 -func (r *UserRoutes) RegisterHealthRoutes(router *gin.Engine) { - health := router.Group("/health") - { - health.GET("/users", func(c *gin.Context) { - // 用户服务健康检查 - c.JSON(200, gin.H{ - "service": "users", - "status": "healthy", - }) - }) + // 需要认证的路由 + authenticated := usersGroup.Group("") + authenticated.Use(authMiddleware.Handle()) + { + authenticated.GET("/me", handler.GetProfile) // 获取当前用户信息 + authenticated.PUT("/me/password", handler.ChangePassword) // 修改密码 + } } } diff --git a/internal/domains/user/services/sms_code_service.go b/internal/domains/user/services/sms_code_service.go new file mode 100644 index 0000000..3d3d4cf --- /dev/null +++ b/internal/domains/user/services/sms_code_service.go @@ -0,0 +1,187 @@ +package services + +import ( + "context" + "fmt" + "time" + + "github.com/google/uuid" + "go.uber.org/zap" + + "tyapi-server/internal/config" + "tyapi-server/internal/domains/user/entities" + "tyapi-server/internal/domains/user/repositories" + "tyapi-server/internal/shared/interfaces" + "tyapi-server/internal/shared/sms" +) + +// SMSCodeService 短信验证码服务 +type SMSCodeService struct { + repo *repositories.SMSCodeRepository + smsClient sms.Service + cache interfaces.CacheService + config config.SMSConfig + logger *zap.Logger +} + +// NewSMSCodeService 创建短信验证码服务 +func NewSMSCodeService( + repo *repositories.SMSCodeRepository, + smsClient sms.Service, + cache interfaces.CacheService, + config config.SMSConfig, + logger *zap.Logger, +) *SMSCodeService { + return &SMSCodeService{ + repo: repo, + smsClient: smsClient, + cache: cache, + config: config, + logger: logger, + } +} + +// SendCode 发送验证码 +func (s *SMSCodeService) SendCode(ctx context.Context, phone string, scene entities.SMSScene, clientIP, userAgent string) error { + // 检查频率限制 + if err := s.checkRateLimit(ctx, phone); err != nil { + return err + } + + // 生成验证码 + code := s.smsClient.GenerateCode(s.config.CodeLength) + + // 创建SMS验证码记录 + smsCode := &entities.SMSCode{ + ID: uuid.New().String(), + Phone: phone, + Code: code, + Scene: scene, + IP: clientIP, + UserAgent: userAgent, + Used: false, + ExpiresAt: time.Now().Add(s.config.ExpireTime), + } + + // 保存验证码 + if err := s.repo.Create(ctx, smsCode); err != nil { + s.logger.Error("保存短信验证码失败", + zap.String("phone", phone), + zap.String("scene", string(scene)), + zap.Error(err)) + return fmt.Errorf("保存验证码失败: %w", err) + } + + // 发送短信 + if err := s.smsClient.SendVerificationCode(ctx, phone, code); err != nil { + // 记录发送失败但不删除验证码记录,让其自然过期 + s.logger.Error("发送短信验证码失败", + zap.String("phone", phone), + zap.String("code", code), + zap.Error(err)) + return fmt.Errorf("短信发送失败: %w", err) + } + + // 更新发送记录缓存 + s.updateSendRecord(ctx, phone) + + s.logger.Info("短信验证码发送成功", + zap.String("phone", phone), + zap.String("scene", string(scene))) + + return nil +} + +// VerifyCode 验证验证码 +func (s *SMSCodeService) VerifyCode(ctx context.Context, phone, code string, scene entities.SMSScene) error { + // 根据手机号和场景获取有效的验证码记录 + smsCode, err := s.repo.GetValidCode(ctx, phone, scene) + if err != nil { + return fmt.Errorf("验证码无效或已过期") + } + + // 验证验证码是否匹配 + if smsCode.Code != code { + return fmt.Errorf("验证码无效或已过期") + } + + // 标记验证码为已使用 + if err := s.repo.MarkAsUsed(ctx, smsCode.ID); err != nil { + s.logger.Error("标记验证码为已使用失败", + zap.String("code_id", smsCode.ID), + zap.Error(err)) + return fmt.Errorf("验证码状态更新失败") + } + + s.logger.Info("短信验证码验证成功", + zap.String("phone", phone), + zap.String("scene", string(scene))) + + return nil +} + +// checkRateLimit 检查发送频率限制 +func (s *SMSCodeService) checkRateLimit(ctx context.Context, phone string) error { + now := time.Now() + + // 检查最小发送间隔 + lastSentKey := fmt.Sprintf("sms:last_sent:%s", phone) + var lastSent time.Time + if err := s.cache.Get(ctx, lastSentKey, &lastSent); err == nil { + if 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")) + var hourlyCount int + if err := s.cache.Get(ctx, hourlyKey, &hourlyCount); err == nil { + if hourlyCount >= s.config.RateLimit.HourlyLimit { + return fmt.Errorf("每小时最多发送 %d 条短信", s.config.RateLimit.HourlyLimit) + } + } + + // 检查每日发送限制 + dailyKey := fmt.Sprintf("sms:daily:%s:%s", phone, now.Format("20060102")) + var dailyCount int + if err := s.cache.Get(ctx, dailyKey, &dailyCount); err == nil { + if dailyCount >= s.config.RateLimit.DailyLimit { + return fmt.Errorf("每日最多发送 %d 条短信", s.config.RateLimit.DailyLimit) + } + } + + return nil +} + +// updateSendRecord 更新发送记录 +func (s *SMSCodeService) updateSendRecord(ctx context.Context, phone string) { + now := time.Now() + + // 更新最后发送时间 + lastSentKey := fmt.Sprintf("sms:last_sent:%s", phone) + s.cache.Set(ctx, lastSentKey, now, s.config.RateLimit.MinInterval) + + // 更新每小时计数 + hourlyKey := fmt.Sprintf("sms:hourly:%s:%s", phone, now.Format("2006010215")) + var hourlyCount int + if err := s.cache.Get(ctx, hourlyKey, &hourlyCount); err == nil { + s.cache.Set(ctx, hourlyKey, hourlyCount+1, time.Hour) + } else { + s.cache.Set(ctx, hourlyKey, 1, time.Hour) + } + + // 更新每日计数 + dailyKey := fmt.Sprintf("sms:daily:%s:%s", phone, now.Format("20060102")) + var dailyCount int + if err := s.cache.Get(ctx, dailyKey, &dailyCount); err == nil { + s.cache.Set(ctx, dailyKey, dailyCount+1, 24*time.Hour) + } else { + s.cache.Set(ctx, dailyKey, 1, 24*time.Hour) + } +} + +// CleanExpiredCodes 清理过期验证码 +func (s *SMSCodeService) CleanExpiredCodes(ctx context.Context) error { + return s.repo.CleanupExpired(ctx) +} diff --git a/internal/domains/user/services/user_service.go b/internal/domains/user/services/user_service.go index 771614f..3f86152 100644 --- a/internal/domains/user/services/user_service.go +++ b/internal/domains/user/services/user_service.go @@ -3,7 +3,7 @@ package services import ( "context" "fmt" - "time" + "regexp" "github.com/google/uuid" "go.uber.org/zap" @@ -18,21 +18,24 @@ import ( // UserService 用户服务实现 type UserService struct { - repo *repositories.UserRepository - eventBus interfaces.EventBus - logger *zap.Logger + repo *repositories.UserRepository + smsCodeService *SMSCodeService + eventBus interfaces.EventBus + logger *zap.Logger } // NewUserService 创建用户服务 func NewUserService( repo *repositories.UserRepository, + smsCodeService *SMSCodeService, eventBus interfaces.EventBus, logger *zap.Logger, ) *UserService { return &UserService{ - repo: repo, - eventBus: eventBus, - logger: logger, + repo: repo, + smsCodeService: smsCodeService, + eventBus: eventBus, + logger: logger, } } @@ -43,341 +46,209 @@ func (s *UserService) Name() string { // Initialize 初始化服务 func (s *UserService) Initialize(ctx context.Context) error { - s.logger.Info("User service initialized") + s.logger.Info("用户服务已初始化") return nil } // HealthCheck 健康检查 func (s *UserService) HealthCheck(ctx context.Context) error { - // 简单检查:尝试查询用户数量 - _, err := s.repo.Count(ctx, interfaces.CountOptions{}) - return err + // 简单的健康检查 + return nil } // Shutdown 关闭服务 func (s *UserService) Shutdown(ctx context.Context) error { - s.logger.Info("User service shutdown") + s.logger.Info("用户服务已关闭") return nil } -// Create 创建用户 -func (s *UserService) Create(ctx context.Context, createDTO interface{}) (*entities.User, error) { - req, ok := createDTO.(*dto.CreateUserRequest) - if !ok { - return nil, fmt.Errorf("invalid DTO type for user creation") +// Register 用户注册 +func (s *UserService) Register(ctx context.Context, registerReq *dto.RegisterRequest) (*entities.User, error) { + // 验证手机号格式 + if !s.isValidPhone(registerReq.Phone) { + return nil, fmt.Errorf("手机号格式无效") } - // 验证业务规则 - if err := s.ValidateCreate(ctx, req); err != nil { - return nil, err + // 验证密码确认 + if registerReq.Password != registerReq.ConfirmPassword { + return nil, fmt.Errorf("密码和确认密码不匹配") } - // 检查用户名和邮箱是否已存在 - if err := s.checkDuplicates(ctx, req.Username, req.Email); err != nil { + // 验证短信验证码 + if err := s.smsCodeService.VerifyCode(ctx, registerReq.Phone, registerReq.Code, entities.SMSSceneRegister); err != nil { + return nil, fmt.Errorf("验证码验证失败: %w", err) + } + + // 检查手机号是否已存在 + if err := s.checkPhoneDuplicate(ctx, registerReq.Phone); err != nil { return nil, err } // 创建用户实体 - user := req.ToEntity() + user := registerReq.ToEntity() user.ID = uuid.New().String() - // 加密密码 - hashedPassword, err := s.hashPassword(req.Password) + // 哈希密码 + hashedPassword, err := s.hashPassword(registerReq.Password) if err != nil { - return nil, fmt.Errorf("failed to hash password: %w", err) + return nil, fmt.Errorf("密码加密失败: %w", err) } user.Password = hashedPassword // 保存用户 if err := s.repo.Create(ctx, user); err != nil { - s.logger.Error("Failed to create user", zap.Error(err)) - return nil, fmt.Errorf("failed to create user: %w", err) + s.logger.Error("创建用户失败", zap.Error(err)) + return nil, fmt.Errorf("创建用户失败: %w", err) } - // 发布用户创建事件 - event := events.NewUserCreatedEvent(user, s.getCorrelationID(ctx)) + // 发布用户注册事件 + event := events.NewUserRegisteredEvent(user, s.getCorrelationID(ctx)) if err := s.eventBus.Publish(ctx, event); err != nil { - s.logger.Warn("Failed to publish user created event", zap.Error(err)) + s.logger.Warn("发布用户注册事件失败", zap.Error(err)) } - s.logger.Info("User created successfully", + s.logger.Info("用户注册成功", zap.String("user_id", user.ID), - zap.String("username", user.Username)) + zap.String("phone", user.Phone)) return user, nil } -// GetByID 根据ID获取用户 -func (s *UserService) GetByID(ctx context.Context, id string) (*entities.User, error) { - if id == "" { - return nil, fmt.Errorf("user ID is required") - } - - user, err := s.repo.GetByID(ctx, id) +// LoginWithPassword 密码登录 +func (s *UserService) LoginWithPassword(ctx context.Context, loginReq *dto.LoginWithPasswordRequest) (*entities.User, error) { + // 根据手机号查找用户 + user, err := s.repo.FindByPhone(ctx, loginReq.Phone) if err != nil { - return nil, fmt.Errorf("user not found: %w", err) - } - - return user, nil -} - -// Update 更新用户 -func (s *UserService) Update(ctx context.Context, id string, updateDTO interface{}) (*entities.User, error) { - req, ok := updateDTO.(*dto.UpdateUserRequest) - if !ok { - return nil, fmt.Errorf("invalid DTO type for user update") - } - - // 验证业务规则 - if err := s.ValidateUpdate(ctx, id, req); err != nil { - return nil, err - } - - // 获取现有用户 - user, err := s.repo.GetByID(ctx, id) - if err != nil { - return nil, fmt.Errorf("user not found: %w", err) - } - - // 记录变更前的值 - oldValues := s.captureUserValues(user) - - // 应用更新 - s.applyUserUpdates(user, req) - - // 保存更新 - if err := s.repo.Update(ctx, user); err != nil { - s.logger.Error("Failed to update user", zap.Error(err)) - return nil, fmt.Errorf("failed to update user: %w", err) - } - - // 发布用户更新事件 - newValues := s.captureUserValues(user) - changes := s.findChanges(oldValues, newValues) - if len(changes) > 0 { - event := events.NewUserUpdatedEvent(user.ID, changes, oldValues, newValues, s.getCorrelationID(ctx)) - if err := s.eventBus.Publish(ctx, event); err != nil { - s.logger.Warn("Failed to publish user updated event", zap.Error(err)) - } - } - - s.logger.Info("User updated successfully", - zap.String("user_id", user.ID), - zap.Int("changes", len(changes))) - - return user, nil -} - -// Delete 删除用户 -func (s *UserService) Delete(ctx context.Context, id string) error { - if id == "" { - return fmt.Errorf("user ID is required") - } - - // 获取用户信息用于事件 - user, err := s.repo.GetByID(ctx, id) - if err != nil { - return fmt.Errorf("user not found: %w", err) - } - - // 软删除用户 - if err := s.repo.SoftDelete(ctx, id); err != nil { - s.logger.Error("Failed to delete user", zap.Error(err)) - return fmt.Errorf("failed to delete user: %w", err) - } - - // 发布用户删除事件 - event := events.NewUserDeletedEvent(user.ID, user.Username, user.Email, true, s.getCorrelationID(ctx)) - if err := s.eventBus.Publish(ctx, event); err != nil { - s.logger.Warn("Failed to publish user deleted event", zap.Error(err)) - } - - s.logger.Info("User deleted successfully", zap.String("user_id", id)) - return nil -} - -// List 获取用户列表 -func (s *UserService) List(ctx context.Context, options interfaces.ListOptions) ([]*entities.User, error) { - return s.repo.List(ctx, options) -} - -// Search 搜索用户 -func (s *UserService) Search(ctx context.Context, query string, options interfaces.ListOptions) ([]*entities.User, error) { - // 设置搜索关键字 - searchOptions := options - searchOptions.Search = query - - return s.repo.List(ctx, searchOptions) -} - -// Count 统计用户数量 -func (s *UserService) Count(ctx context.Context, options interfaces.CountOptions) (int64, error) { - return s.repo.Count(ctx, options) -} - -// Validate 验证用户实体 -func (s *UserService) Validate(ctx context.Context, entity *entities.User) error { - return entity.Validate() -} - -// ValidateCreate 验证创建请求 -func (s *UserService) ValidateCreate(ctx context.Context, createDTO interface{}) error { - req, ok := createDTO.(*dto.CreateUserRequest) - if !ok { - return fmt.Errorf("invalid DTO type") - } - - // 基础验证已经由binding标签处理,这里添加业务规则验证 - if req.Username == "admin" || req.Username == "root" { - return fmt.Errorf("username '%s' is reserved", req.Username) - } - - return nil -} - -// ValidateUpdate 验证更新请求 -func (s *UserService) ValidateUpdate(ctx context.Context, id string, updateDTO interface{}) error { - _, ok := updateDTO.(*dto.UpdateUserRequest) - if !ok { - return fmt.Errorf("invalid DTO type") - } - - if id == "" { - return fmt.Errorf("user ID is required") - } - - return nil -} - -// 业务方法 - -// Login 用户登录 -func (s *UserService) Login(ctx context.Context, loginReq *dto.LoginRequest) (*entities.User, error) { - // 根据用户名或邮箱查找用户 - var user *entities.User - var err error - - if s.isEmail(loginReq.Login) { - user, err = s.repo.FindByEmail(ctx, loginReq.Login) - } else { - user, err = s.repo.FindByUsername(ctx, loginReq.Login) - } - - if err != nil { - return nil, fmt.Errorf("invalid credentials") + return nil, fmt.Errorf("用户名或密码错误") } // 验证密码 if !s.checkPassword(loginReq.Password, user.Password) { - return nil, fmt.Errorf("invalid credentials") + return nil, fmt.Errorf("用户名或密码错误") } - // 检查用户状态 - if !user.CanLogin() { - return nil, fmt.Errorf("account is disabled or suspended") - } - - // 更新最后登录时间 - user.UpdateLastLogin() - if err := s.repo.Update(ctx, user); err != nil { - s.logger.Warn("Failed to update last login time", zap.Error(err)) - } - - // 发布登录事件 + // 发布用户登录事件 event := events.NewUserLoggedInEvent( - user.ID, user.Username, + user.ID, user.Phone, s.getClientIP(ctx), s.getUserAgent(ctx), s.getCorrelationID(ctx)) if err := s.eventBus.Publish(ctx, event); err != nil { - s.logger.Warn("Failed to publish user logged in event", zap.Error(err)) + s.logger.Warn("发布用户登录事件失败", zap.Error(err)) } - s.logger.Info("User logged in successfully", + s.logger.Info("用户密码登录成功", zap.String("user_id", user.ID), - zap.String("username", user.Username)) + zap.String("phone", user.Phone)) + + return user, nil +} + +// LoginWithSMS 短信验证码登录 +func (s *UserService) LoginWithSMS(ctx context.Context, loginReq *dto.LoginWithSMSRequest) (*entities.User, error) { + // 验证短信验证码 + if err := s.smsCodeService.VerifyCode(ctx, loginReq.Phone, loginReq.Code, entities.SMSSceneLogin); err != nil { + return nil, fmt.Errorf("验证码验证失败: %w", err) + } + + // 根据手机号查找用户 + user, err := s.repo.FindByPhone(ctx, loginReq.Phone) + if err != nil { + return nil, fmt.Errorf("用户不存在") + } + + // 发布用户登录事件 + event := events.NewUserLoggedInEvent( + user.ID, user.Phone, + s.getClientIP(ctx), s.getUserAgent(ctx), + s.getCorrelationID(ctx)) + if err := s.eventBus.Publish(ctx, event); err != nil { + s.logger.Warn("发布用户登录事件失败", zap.Error(err)) + } + + s.logger.Info("用户短信登录成功", + zap.String("user_id", user.ID), + zap.String("phone", user.Phone)) return user, nil } // ChangePassword 修改密码 func (s *UserService) ChangePassword(ctx context.Context, userID string, req *dto.ChangePasswordRequest) error { - // 获取用户 + // 验证新密码确认 + if req.NewPassword != req.ConfirmNewPassword { + return fmt.Errorf("新密码和确认新密码不匹配") + } + + // 获取用户信息 user, err := s.repo.GetByID(ctx, userID) if err != nil { - return fmt.Errorf("user not found: %w", err) + return fmt.Errorf("用户不存在: %w", err) } - // 验证旧密码 + // 验证短信验证码 + if err := s.smsCodeService.VerifyCode(ctx, user.Phone, req.Code, entities.SMSSceneChangePassword); err != nil { + return fmt.Errorf("验证码验证失败: %w", err) + } + + // 验证当前密码 if !s.checkPassword(req.OldPassword, user.Password) { - return fmt.Errorf("current password is incorrect") + return fmt.Errorf("当前密码错误") } - // 加密新密码 + // 哈希新密码 hashedPassword, err := s.hashPassword(req.NewPassword) if err != nil { - return fmt.Errorf("failed to hash new password: %w", err) + return fmt.Errorf("新密码加密失败: %w", err) } // 更新密码 user.Password = hashedPassword if err := s.repo.Update(ctx, user); err != nil { - return fmt.Errorf("failed to update password: %w", err) + return fmt.Errorf("密码更新失败: %w", err) } // 发布密码修改事件 - event := events.NewUserPasswordChangedEvent(user.ID, user.Username, s.getCorrelationID(ctx)) + event := events.NewUserPasswordChangedEvent(user.ID, user.Phone, s.getCorrelationID(ctx)) if err := s.eventBus.Publish(ctx, event); err != nil { - s.logger.Warn("Failed to publish password changed event", zap.Error(err)) + s.logger.Warn("发布密码修改事件失败", zap.Error(err)) } - s.logger.Info("Password changed successfully", zap.String("user_id", userID)) + s.logger.Info("密码修改成功", zap.String("user_id", userID)) + return nil } -// GetStats 获取用户统计 -func (s *UserService) GetStats(ctx context.Context) (*dto.UserStatsResponse, error) { - total, err := s.repo.Count(ctx, interfaces.CountOptions{}) - if err != nil { - return nil, err +// GetByID 根据ID获取用户 +func (s *UserService) GetByID(ctx context.Context, id string) (*entities.User, error) { + if id == "" { + return nil, fmt.Errorf("用户ID不能为空") } - // 这里可以并行查询不同状态的用户数量 - // 简化实现,返回基础统计 - return &dto.UserStatsResponse{ - TotalUsers: total, - ActiveUsers: total, // 简化 - InactiveUsers: 0, - SuspendedUsers: 0, - NewUsersToday: 0, - NewUsersWeek: 0, - NewUsersMonth: 0, - }, nil + user, err := s.repo.GetByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("用户不存在: %w", err) + } + + return user, nil } -// 私有方法 +// 工具方法 -// checkDuplicates 检查重复的用户名和邮箱 -func (s *UserService) checkDuplicates(ctx context.Context, username, email string) error { - // 检查用户名 - if existingUser, err := s.repo.FindByUsername(ctx, username); err == nil && existingUser != nil { - return fmt.Errorf("username already exists") +// checkPhoneDuplicate 检查手机号重复 +func (s *UserService) checkPhoneDuplicate(ctx context.Context, phone string) error { + if _, err := s.repo.FindByPhone(ctx, phone); err == nil { + return fmt.Errorf("手机号已存在") } - - // 检查邮箱 - if existingUser, err := s.repo.FindByEmail(ctx, email); err == nil && existingUser != nil { - return fmt.Errorf("email already exists") - } - return nil } // hashPassword 加密密码 func (s *UserService) hashPassword(password string) (string, error) { - hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + hashedBytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return "", err } - return string(hash), nil + return string(hashedBytes), nil } // checkPassword 验证密码 @@ -386,63 +257,24 @@ func (s *UserService) checkPassword(password, hash string) bool { return err == nil } -// isEmail 检查是否为邮箱格式 -func (s *UserService) isEmail(str string) bool { - return len(str) > 0 && len(str) < 255 && - len(str) > 5 && - str[len(str)-4:] != ".." && - (len(str) > 6 && str[len(str)-4:] == ".com") || - (len(str) > 5 && str[len(str)-3:] == ".cn") || - (len(str) > 6 && str[len(str)-4:] == ".org") || - (len(str) > 6 && str[len(str)-4:] == ".net") - // 简化的邮箱检查,实际应该使用正则表达式 +// isValidPhone 验证手机号格式 +func (s *UserService) isValidPhone(phone string) bool { + // 简单的中国手机号验证(11位数字,以1开头) + pattern := `^1[3-9]\d{9}$` + matched, _ := regexp.MatchString(pattern, phone) + return matched } -// applyUserUpdates 应用用户更新 -func (s *UserService) applyUserUpdates(user *entities.User, req *dto.UpdateUserRequest) { - if req.FirstName != nil { - user.FirstName = *req.FirstName - } - if req.LastName != nil { - user.LastName = *req.LastName - } - if req.Phone != nil { - user.Phone = *req.Phone - } - if req.Avatar != nil { - user.Avatar = *req.Avatar - } - user.UpdatedAt = time.Now() -} - -// captureUserValues 捕获用户值用于变更比较 -func (s *UserService) captureUserValues(user *entities.User) map[string]interface{} { - return map[string]interface{}{ - "first_name": user.FirstName, - "last_name": user.LastName, - "phone": user.Phone, - "avatar": user.Avatar, - } -} - -// findChanges 找出变更的字段 -func (s *UserService) findChanges(oldValues, newValues map[string]interface{}) map[string]interface{} { - changes := make(map[string]interface{}) - - for key, newValue := range newValues { - if oldValue, exists := oldValues[key]; !exists || oldValue != newValue { - changes[key] = newValue - } - } - - return changes +// generateUserID 生成用户ID +func (s *UserService) generateUserID() string { + return uuid.New().String() } // getCorrelationID 获取关联ID func (s *UserService) getCorrelationID(ctx context.Context) string { if id := ctx.Value("correlation_id"); id != nil { - if correlationID, ok := id.(string); ok { - return correlationID + if strID, ok := id.(string); ok { + return strID } } return uuid.New().String() @@ -451,19 +283,19 @@ func (s *UserService) getCorrelationID(ctx context.Context) string { // getClientIP 获取客户端IP func (s *UserService) getClientIP(ctx context.Context) string { if ip := ctx.Value("client_ip"); ip != nil { - if clientIP, ok := ip.(string); ok { - return clientIP + if strIP, ok := ip.(string); ok { + return strIP } } - return "unknown" + return "" } // getUserAgent 获取用户代理 func (s *UserService) getUserAgent(ctx context.Context) string { if ua := ctx.Value("user_agent"); ua != nil { - if userAgent, ok := ua.(string); ok { - return userAgent + if strUA, ok := ua.(string); ok { + return strUA } } - return "unknown" + return "" } diff --git a/internal/shared/health/health_checker.go b/internal/shared/health/health_checker.go index 64c8e88..eca78ad 100644 --- a/internal/shared/health/health_checker.go +++ b/internal/shared/health/health_checker.go @@ -36,7 +36,7 @@ func (h *HealthChecker) RegisterService(service interfaces.Service) { defer h.mutex.Unlock() h.services[service.Name()] = service - h.logger.Info("Registered service for health check", zap.String("service", service.Name())) + h.logger.Info("服务已注册健康检查", zap.String("service", service.Name())) } // CheckHealth 检查单个服务健康状态 @@ -47,8 +47,8 @@ func (h *HealthChecker) CheckHealth(ctx context.Context, serviceName string) *in h.mutex.RUnlock() return &interfaces.HealthStatus{ Status: "DOWN", - Message: "Service not found", - Details: map[string]interface{}{"error": "service not registered"}, + Message: "服务未找到", + Details: map[string]interface{}{"error": "服务未注册"}, CheckedAt: time.Now().Unix(), ResponseTime: 0, } @@ -79,24 +79,24 @@ func (h *HealthChecker) CheckHealth(ctx context.Context, serviceName string) *in if err != nil { status.Status = "DOWN" - status.Message = "Health check failed" + status.Message = "健康检查失败" status.Details = map[string]interface{}{ "error": err.Error(), "service_name": serviceName, "check_time": start.Format(time.RFC3339), } - h.logger.Warn("Service health check failed", + h.logger.Warn("服务健康检查失败", zap.String("service", serviceName), zap.Error(err), zap.Int64("response_time_ms", responseTime)) } else { status.Status = "UP" - status.Message = "Service is healthy" + status.Message = "服务运行正常" status.Details = map[string]interface{}{ "service_name": serviceName, "check_time": start.Format(time.RFC3339), } - h.logger.Debug("Service health check passed", + h.logger.Debug("服务健康检查通过", zap.String("service", serviceName), zap.Int64("response_time_ms", responseTime)) } @@ -173,13 +173,13 @@ func (h *HealthChecker) GetOverallStatus(ctx context.Context) *interfaces.Health // 确定整体状态 if healthyCount == totalCount { overall.Status = "UP" - overall.Message = "All services are healthy" + overall.Message = "所有服务运行正常" } else if healthyCount == 0 { overall.Status = "DOWN" - overall.Message = "All services are down" + overall.Message = "所有服务均已下线" } else { overall.Status = "DEGRADED" - overall.Message = fmt.Sprintf("%d of %d services are healthy", healthyCount, totalCount) + overall.Message = fmt.Sprintf("%d/%d 个服务运行正常", healthyCount, totalCount) } return overall @@ -205,7 +205,7 @@ func (h *HealthChecker) RemoveService(serviceName string) { delete(h.services, serviceName) delete(h.cache, serviceName) - h.logger.Info("Removed service from health check", zap.String("service", serviceName)) + h.logger.Info("服务已从健康检查中移除", zap.String("service", serviceName)) } // ClearCache 清除缓存 @@ -214,7 +214,7 @@ func (h *HealthChecker) ClearCache() { defer h.mutex.Unlock() h.cache = make(map[string]*interfaces.HealthStatus) - h.logger.Debug("Health check cache cleared") + h.logger.Debug("健康检查缓存已清除") } // GetCacheStats 获取缓存统计 @@ -243,7 +243,7 @@ func (h *HealthChecker) SetCacheTTL(ttl time.Duration) { defer h.mutex.Unlock() h.cacheTTL = ttl - h.logger.Info("Updated health check cache TTL", zap.Duration("ttl", ttl)) + h.logger.Info("健康检查缓存TTL已更新", zap.Duration("ttl", ttl)) } // StartPeriodicCheck 启动定期健康检查 @@ -251,12 +251,12 @@ func (h *HealthChecker) StartPeriodicCheck(ctx context.Context, interval time.Du ticker := time.NewTicker(interval) defer ticker.Stop() - h.logger.Info("Started periodic health check", zap.Duration("interval", interval)) + h.logger.Info("已启动定期健康检查", zap.Duration("interval", interval)) for { select { case <-ctx.Done(): - h.logger.Info("Stopped periodic health check") + h.logger.Info("已停止定期健康检查") return case <-ticker.C: h.performPeriodicCheck(ctx) @@ -268,14 +268,14 @@ func (h *HealthChecker) StartPeriodicCheck(ctx context.Context, interval time.Du func (h *HealthChecker) performPeriodicCheck(ctx context.Context) { overall := h.GetOverallStatus(ctx) - h.logger.Info("Periodic health check completed", + h.logger.Info("定期健康检查已完成", zap.String("overall_status", overall.Status), zap.String("message", overall.Message), zap.Int64("response_time_ms", overall.ResponseTime)) // 如果有服务下线,记录警告 if overall.Status != "UP" { - h.logger.Warn("Some services are not healthy", + h.logger.Warn("部分服务不健康", zap.String("status", overall.Status), zap.Any("details", overall.Details)) } diff --git a/internal/shared/hooks/hook_system.go b/internal/shared/hooks/hook_system.go new file mode 100644 index 0000000..d09ea65 --- /dev/null +++ b/internal/shared/hooks/hook_system.go @@ -0,0 +1,587 @@ +package hooks + +import ( + "context" + "fmt" + "reflect" + "sort" + "sync" + "time" + + "go.uber.org/zap" +) + +// HookPriority 钩子优先级 +type HookPriority int + +const ( + // PriorityLowest 最低优先级 + PriorityLowest HookPriority = 0 + // PriorityLow 低优先级 + PriorityLow HookPriority = 25 + // PriorityNormal 普通优先级 + PriorityNormal HookPriority = 50 + // PriorityHigh 高优先级 + PriorityHigh HookPriority = 75 + // PriorityHighest 最高优先级 + PriorityHighest HookPriority = 100 +) + +// HookFunc 钩子函数类型 +type HookFunc func(ctx context.Context, data interface{}) error + +// Hook 钩子定义 +type Hook struct { + Name string + Func HookFunc + Priority HookPriority + Async bool + Timeout time.Duration +} + +// HookResult 钩子执行结果 +type HookResult struct { + HookName string `json:"hook_name"` + Success bool `json:"success"` + Duration time.Duration `json:"duration"` + Error string `json:"error,omitempty"` +} + +// HookConfig 钩子配置 +type HookConfig struct { + // 默认超时时间 + DefaultTimeout time.Duration + // 是否记录执行时间 + TrackDuration bool + // 错误处理策略 + ErrorStrategy ErrorStrategy +} + +// ErrorStrategy 错误处理策略 +type ErrorStrategy int + +const ( + // ContinueOnError 遇到错误继续执行 + ContinueOnError ErrorStrategy = iota + // StopOnError 遇到错误停止执行 + StopOnError + // CollectErrors 收集所有错误 + CollectErrors +) + +// DefaultHookConfig 默认钩子配置 +func DefaultHookConfig() HookConfig { + return HookConfig{ + DefaultTimeout: 30 * time.Second, + TrackDuration: true, + ErrorStrategy: ContinueOnError, + } +} + +// HookSystem 钩子系统 +type HookSystem struct { + hooks map[string][]*Hook + config HookConfig + logger *zap.Logger + mutex sync.RWMutex + stats map[string]*HookStats +} + +// HookStats 钩子统计 +type HookStats struct { + TotalExecutions int `json:"total_executions"` + Successes int `json:"successes"` + Failures int `json:"failures"` + TotalDuration time.Duration `json:"total_duration"` + AverageDuration time.Duration `json:"average_duration"` + LastExecution time.Time `json:"last_execution"` + LastError string `json:"last_error,omitempty"` +} + +// NewHookSystem 创建钩子系统 +func NewHookSystem(config HookConfig, logger *zap.Logger) *HookSystem { + return &HookSystem{ + hooks: make(map[string][]*Hook), + config: config, + logger: logger, + stats: make(map[string]*HookStats), + } +} + +// Register 注册钩子 +func (hs *HookSystem) Register(event string, hook *Hook) error { + if hook.Name == "" { + return fmt.Errorf("hook name cannot be empty") + } + + if hook.Func == nil { + return fmt.Errorf("hook function cannot be nil") + } + + if hook.Timeout == 0 { + hook.Timeout = hs.config.DefaultTimeout + } + + hs.mutex.Lock() + defer hs.mutex.Unlock() + + // 检查是否已经注册了同名钩子 + for _, existingHook := range hs.hooks[event] { + if existingHook.Name == hook.Name { + return fmt.Errorf("hook %s already registered for event %s", hook.Name, event) + } + } + + hs.hooks[event] = append(hs.hooks[event], hook) + + // 按优先级排序 + sort.Slice(hs.hooks[event], func(i, j int) bool { + return hs.hooks[event][i].Priority > hs.hooks[event][j].Priority + }) + + // 初始化统计 + hookKey := fmt.Sprintf("%s.%s", event, hook.Name) + hs.stats[hookKey] = &HookStats{} + + hs.logger.Info("Registered hook", + zap.String("event", event), + zap.String("hook_name", hook.Name), + zap.Int("priority", int(hook.Priority)), + zap.Bool("async", hook.Async)) + + return nil +} + +// RegisterFunc 注册钩子函数(简化版) +func (hs *HookSystem) RegisterFunc(event, name string, priority HookPriority, fn HookFunc) error { + hook := &Hook{ + Name: name, + Func: fn, + Priority: priority, + Async: false, + Timeout: hs.config.DefaultTimeout, + } + + return hs.Register(event, hook) +} + +// RegisterAsyncFunc 注册异步钩子函数 +func (hs *HookSystem) RegisterAsyncFunc(event, name string, priority HookPriority, fn HookFunc) error { + hook := &Hook{ + Name: name, + Func: fn, + Priority: priority, + Async: true, + Timeout: hs.config.DefaultTimeout, + } + + return hs.Register(event, hook) +} + +// Unregister 取消注册钩子 +func (hs *HookSystem) Unregister(event, hookName string) error { + hs.mutex.Lock() + defer hs.mutex.Unlock() + + hooks := hs.hooks[event] + for i, hook := range hooks { + if hook.Name == hookName { + // 删除钩子 + hs.hooks[event] = append(hooks[:i], hooks[i+1:]...) + + // 删除统计 + hookKey := fmt.Sprintf("%s.%s", event, hookName) + delete(hs.stats, hookKey) + + hs.logger.Info("Unregistered hook", + zap.String("event", event), + zap.String("hook_name", hookName)) + + return nil + } + } + + return fmt.Errorf("hook %s not found for event %s", hookName, event) +} + +// Trigger 触发事件 +func (hs *HookSystem) Trigger(ctx context.Context, event string, data interface{}) ([]HookResult, error) { + hs.mutex.RLock() + hooks := make([]*Hook, len(hs.hooks[event])) + copy(hooks, hs.hooks[event]) + hs.mutex.RUnlock() + + if len(hooks) == 0 { + hs.logger.Debug("No hooks registered for event", zap.String("event", event)) + return nil, nil + } + + hs.logger.Debug("Triggering event", + zap.String("event", event), + zap.Int("hook_count", len(hooks))) + + results := make([]HookResult, 0, len(hooks)) + var errors []error + + for _, hook := range hooks { + result := hs.executeHook(ctx, event, hook, data) + results = append(results, result) + + if !result.Success { + err := fmt.Errorf("hook %s failed: %s", hook.Name, result.Error) + errors = append(errors, err) + + // 根据错误策略决定是否继续 + if hs.config.ErrorStrategy == StopOnError { + break + } + } + } + + // 处理错误 + if len(errors) > 0 { + switch hs.config.ErrorStrategy { + case StopOnError: + return results, errors[0] + case CollectErrors: + return results, fmt.Errorf("multiple hook errors: %v", errors) + case ContinueOnError: + // 继续执行,但记录错误 + hs.logger.Warn("Some hooks failed but continuing execution", + zap.String("event", event), + zap.Int("error_count", len(errors))) + } + } + + return results, nil +} + +// executeHook 执行单个钩子 +func (hs *HookSystem) executeHook(ctx context.Context, event string, hook *Hook, data interface{}) HookResult { + hookKey := fmt.Sprintf("%s.%s", event, hook.Name) + start := time.Now() + + result := HookResult{ + HookName: hook.Name, + Success: false, + } + + // 更新统计 + defer func() { + result.Duration = time.Since(start) + hs.updateStats(hookKey, result) + }() + + if hook.Async { + // 异步执行 + go func() { + hs.doExecuteHook(ctx, hook, data) + }() + result.Success = true // 异步执行总是认为成功 + return result + } + + // 同步执行 + err := hs.doExecuteHook(ctx, hook, data) + if err != nil { + result.Error = err.Error() + hs.logger.Error("Hook execution failed", + zap.String("event", event), + zap.String("hook_name", hook.Name), + zap.Error(err)) + } else { + result.Success = true + hs.logger.Debug("Hook executed successfully", + zap.String("event", event), + zap.String("hook_name", hook.Name)) + } + + return result +} + +// doExecuteHook 实际执行钩子 +func (hs *HookSystem) doExecuteHook(ctx context.Context, hook *Hook, data interface{}) error { + // 设置超时上下文 + hookCtx, cancel := context.WithTimeout(ctx, hook.Timeout) + defer cancel() + + // 在goroutine中执行,以便处理超时 + errChan := make(chan error, 1) + go func() { + defer func() { + if r := recover(); r != nil { + errChan <- fmt.Errorf("hook panicked: %v", r) + } + }() + + errChan <- hook.Func(hookCtx, data) + }() + + select { + case err := <-errChan: + return err + case <-hookCtx.Done(): + return fmt.Errorf("hook execution timeout after %v", hook.Timeout) + } +} + +// updateStats 更新统计信息 +func (hs *HookSystem) updateStats(hookKey string, result HookResult) { + hs.mutex.Lock() + defer hs.mutex.Unlock() + + stats, exists := hs.stats[hookKey] + if !exists { + stats = &HookStats{} + hs.stats[hookKey] = stats + } + + stats.TotalExecutions++ + stats.LastExecution = time.Now() + + if result.Success { + stats.Successes++ + } else { + stats.Failures++ + stats.LastError = result.Error + } + + if hs.config.TrackDuration { + stats.TotalDuration += result.Duration + stats.AverageDuration = stats.TotalDuration / time.Duration(stats.TotalExecutions) + } +} + +// GetHooks 获取事件的所有钩子 +func (hs *HookSystem) GetHooks(event string) []*Hook { + hs.mutex.RLock() + defer hs.mutex.RUnlock() + + hooks := make([]*Hook, len(hs.hooks[event])) + copy(hooks, hs.hooks[event]) + return hooks +} + +// GetEvents 获取所有注册的事件 +func (hs *HookSystem) GetEvents() []string { + hs.mutex.RLock() + defer hs.mutex.RUnlock() + + events := make([]string, 0, len(hs.hooks)) + for event := range hs.hooks { + events = append(events, event) + } + + sort.Strings(events) + return events +} + +// GetStats 获取钩子统计信息 +func (hs *HookSystem) GetStats() map[string]*HookStats { + hs.mutex.RLock() + defer hs.mutex.RUnlock() + + stats := make(map[string]*HookStats) + for key, stat := range hs.stats { + statCopy := *stat + stats[key] = &statCopy + } + + return stats +} + +// GetEventStats 获取特定事件的统计信息 +func (hs *HookSystem) GetEventStats(event string) map[string]*HookStats { + allStats := hs.GetStats() + eventStats := make(map[string]*HookStats) + + prefix := event + "." + for key, stat := range allStats { + if len(key) > len(prefix) && key[:len(prefix)] == prefix { + hookName := key[len(prefix):] + eventStats[hookName] = stat + } + } + + return eventStats +} + +// Clear 清除所有钩子 +func (hs *HookSystem) Clear() { + hs.mutex.Lock() + defer hs.mutex.Unlock() + + hs.hooks = make(map[string][]*Hook) + hs.stats = make(map[string]*HookStats) + + hs.logger.Info("Cleared all hooks") +} + +// ClearEvent 清除特定事件的所有钩子 +func (hs *HookSystem) ClearEvent(event string) { + hs.mutex.Lock() + defer hs.mutex.Unlock() + + // 删除钩子 + delete(hs.hooks, event) + + // 删除统计 + prefix := event + "." + for key := range hs.stats { + if len(key) > len(prefix) && key[:len(prefix)] == prefix { + delete(hs.stats, key) + } + } + + hs.logger.Info("Cleared hooks for event", zap.String("event", event)) +} + +// Count 获取钩子总数 +func (hs *HookSystem) Count() int { + hs.mutex.RLock() + defer hs.mutex.RUnlock() + + total := 0 + for _, hooks := range hs.hooks { + total += len(hooks) + } + + return total +} + +// EventCount 获取特定事件的钩子数量 +func (hs *HookSystem) EventCount(event string) int { + hs.mutex.RLock() + defer hs.mutex.RUnlock() + + return len(hs.hooks[event]) +} + +// 实现Service接口 + +// Name 返回服务名称 +func (hs *HookSystem) Name() string { + return "hook-system" +} + +// Initialize 初始化钩子系统 +func (hs *HookSystem) Initialize(ctx context.Context) error { + hs.logger.Info("Hook system initialized") + return nil +} + +// Start 启动钩子系统 +func (hs *HookSystem) Start(ctx context.Context) error { + hs.logger.Info("Hook system started") + return nil +} + +// HealthCheck 健康检查 +func (hs *HookSystem) HealthCheck(ctx context.Context) error { + return nil +} + +// Shutdown 关闭钩子系统 +func (hs *HookSystem) Shutdown(ctx context.Context) error { + hs.logger.Info("Hook system shutdown") + return nil +} + +// 便捷方法 + +// OnUserCreated 用户创建事件钩子 +func (hs *HookSystem) OnUserCreated(name string, priority HookPriority, fn HookFunc) error { + return hs.RegisterFunc("user.created", name, priority, fn) +} + +// OnUserUpdated 用户更新事件钩子 +func (hs *HookSystem) OnUserUpdated(name string, priority HookPriority, fn HookFunc) error { + return hs.RegisterFunc("user.updated", name, priority, fn) +} + +// OnUserDeleted 用户删除事件钩子 +func (hs *HookSystem) OnUserDeleted(name string, priority HookPriority, fn HookFunc) error { + return hs.RegisterFunc("user.deleted", name, priority, fn) +} + +// OnOrderCreated 订单创建事件钩子 +func (hs *HookSystem) OnOrderCreated(name string, priority HookPriority, fn HookFunc) error { + return hs.RegisterFunc("order.created", name, priority, fn) +} + +// OnOrderCompleted 订单完成事件钩子 +func (hs *HookSystem) OnOrderCompleted(name string, priority HookPriority, fn HookFunc) error { + return hs.RegisterFunc("order.completed", name, priority, fn) +} + +// TriggerUserCreated 触发用户创建事件 +func (hs *HookSystem) TriggerUserCreated(ctx context.Context, user interface{}) ([]HookResult, error) { + return hs.Trigger(ctx, "user.created", user) +} + +// TriggerUserUpdated 触发用户更新事件 +func (hs *HookSystem) TriggerUserUpdated(ctx context.Context, user interface{}) ([]HookResult, error) { + return hs.Trigger(ctx, "user.updated", user) +} + +// TriggerUserDeleted 触发用户删除事件 +func (hs *HookSystem) TriggerUserDeleted(ctx context.Context, user interface{}) ([]HookResult, error) { + return hs.Trigger(ctx, "user.deleted", user) +} + +// HookBuilder 钩子构建器 +type HookBuilder struct { + hook *Hook +} + +// NewHookBuilder 创建钩子构建器 +func NewHookBuilder(name string, fn HookFunc) *HookBuilder { + return &HookBuilder{ + hook: &Hook{ + Name: name, + Func: fn, + Priority: PriorityNormal, + Async: false, + Timeout: 30 * time.Second, + }, + } +} + +// WithPriority 设置优先级 +func (hb *HookBuilder) WithPriority(priority HookPriority) *HookBuilder { + hb.hook.Priority = priority + return hb +} + +// WithTimeout 设置超时时间 +func (hb *HookBuilder) WithTimeout(timeout time.Duration) *HookBuilder { + hb.hook.Timeout = timeout + return hb +} + +// Async 设置为异步执行 +func (hb *HookBuilder) Async() *HookBuilder { + hb.hook.Async = true + return hb +} + +// Build 构建钩子 +func (hb *HookBuilder) Build() *Hook { + return hb.hook +} + +// TypedHookFunc 类型化钩子函数 +type TypedHookFunc[T any] func(ctx context.Context, data T) error + +// RegisterTypedFunc 注册类型化钩子函数 +func RegisterTypedFunc[T any](hs *HookSystem, event, name string, priority HookPriority, fn TypedHookFunc[T]) error { + hookFunc := func(ctx context.Context, data interface{}) error { + typedData, ok := data.(T) + if !ok { + return fmt.Errorf("invalid data type for hook %s, expected %s", name, reflect.TypeOf((*T)(nil)).Elem().Name()) + } + return fn(ctx, typedData) + } + + return hs.RegisterFunc(event, name, priority, hookFunc) +} diff --git a/internal/shared/http/response.go b/internal/shared/http/response.go index 53ae802..18927e8 100644 --- a/internal/shared/http/response.go +++ b/internal/shared/http/response.go @@ -20,7 +20,7 @@ func NewResponseBuilder() interfaces.ResponseBuilder { // Success 成功响应 func (r *ResponseBuilder) Success(c *gin.Context, data interface{}, message ...string) { - msg := "Success" + msg := "操作成功" if len(message) > 0 && message[0] != "" { msg = message[0] } @@ -38,7 +38,7 @@ func (r *ResponseBuilder) Success(c *gin.Context, data interface{}, message ...s // Created 创建成功响应 func (r *ResponseBuilder) Created(c *gin.Context, data interface{}, message ...string) { - msg := "Created successfully" + msg := "创建成功" if len(message) > 0 && message[0] != "" { msg = message[0] } @@ -58,7 +58,7 @@ func (r *ResponseBuilder) Created(c *gin.Context, data interface{}, message ...s func (r *ResponseBuilder) Error(c *gin.Context, err error) { // 根据错误类型确定状态码 statusCode := http.StatusInternalServerError - message := "Internal server error" + message := "服务器内部错误" errorDetail := err.Error() // 这里可以根据不同的错误类型设置不同的状态码 @@ -93,7 +93,7 @@ func (r *ResponseBuilder) BadRequest(c *gin.Context, message string, errors ...i // Unauthorized 401错误响应 func (r *ResponseBuilder) Unauthorized(c *gin.Context, message ...string) { - msg := "Unauthorized" + msg := "用户未登录或认证已过期" if len(message) > 0 && message[0] != "" { msg = message[0] } @@ -110,7 +110,7 @@ func (r *ResponseBuilder) Unauthorized(c *gin.Context, message ...string) { // Forbidden 403错误响应 func (r *ResponseBuilder) Forbidden(c *gin.Context, message ...string) { - msg := "Forbidden" + msg := "权限不足,无法访问此资源" if len(message) > 0 && message[0] != "" { msg = message[0] } @@ -127,7 +127,7 @@ func (r *ResponseBuilder) Forbidden(c *gin.Context, message ...string) { // NotFound 404错误响应 func (r *ResponseBuilder) NotFound(c *gin.Context, message ...string) { - msg := "Resource not found" + msg := "请求的资源不存在" if len(message) > 0 && message[0] != "" { msg = message[0] } @@ -156,7 +156,7 @@ func (r *ResponseBuilder) Conflict(c *gin.Context, message string) { // InternalError 500错误响应 func (r *ResponseBuilder) InternalError(c *gin.Context, message ...string) { - msg := "Internal server error" + msg := "服务器内部错误" if len(message) > 0 && message[0] != "" { msg = message[0] } @@ -175,7 +175,7 @@ func (r *ResponseBuilder) InternalError(c *gin.Context, message ...string) { func (r *ResponseBuilder) Paginated(c *gin.Context, data interface{}, pagination interfaces.PaginationMeta) { response := interfaces.APIResponse{ Success: true, - Message: "Success", + Message: "查询成功", Data: data, Pagination: &pagination, RequestID: r.getRequestID(c), @@ -215,9 +215,35 @@ func BuildPagination(page, pageSize int, total int64) interfaces.PaginationMeta // CustomResponse 自定义响应 func (r *ResponseBuilder) CustomResponse(c *gin.Context, statusCode int, data interface{}) { + var message string + switch statusCode { + case http.StatusOK: + message = "请求成功" + case http.StatusCreated: + message = "创建成功" + case http.StatusNoContent: + message = "无内容" + case http.StatusBadRequest: + message = "请求参数错误" + case http.StatusUnauthorized: + message = "认证失败" + case http.StatusForbidden: + message = "权限不足" + case http.StatusNotFound: + message = "资源不存在" + case http.StatusConflict: + message = "资源冲突" + case http.StatusTooManyRequests: + message = "请求过于频繁" + case http.StatusInternalServerError: + message = "服务器内部错误" + default: + message = "未知状态" + } + response := interfaces.APIResponse{ Success: statusCode >= 200 && statusCode < 300, - Message: http.StatusText(statusCode), + Message: message, Data: data, RequestID: r.getRequestID(c), Timestamp: time.Now().Unix(), @@ -230,7 +256,7 @@ func (r *ResponseBuilder) CustomResponse(c *gin.Context, statusCode int, data in func (r *ResponseBuilder) ValidationError(c *gin.Context, errors interface{}) { response := interfaces.APIResponse{ Success: false, - Message: "Validation failed", + Message: "请求参数验证失败", Errors: errors, RequestID: r.getRequestID(c), Timestamp: time.Now().Unix(), @@ -241,7 +267,7 @@ func (r *ResponseBuilder) ValidationError(c *gin.Context, errors interface{}) { // TooManyRequests 限流错误响应 func (r *ResponseBuilder) TooManyRequests(c *gin.Context, message ...string) { - msg := "Too many requests" + msg := "请求过于频繁,请稍后再试" if len(message) > 0 && message[0] != "" { msg = message[0] } diff --git a/internal/shared/http/router.go b/internal/shared/http/router.go index 12559d8..771323c 100644 --- a/internal/shared/http/router.go +++ b/internal/shared/http/router.go @@ -8,6 +8,8 @@ import ( "time" "github.com/gin-gonic/gin" + swaggerFiles "github.com/swaggo/files" + ginSwagger "github.com/swaggo/gin-swagger" "go.uber.org/zap" "tyapi-server/internal/config" @@ -51,7 +53,7 @@ func (r *GinRouter) RegisterHandler(handler interfaces.HTTPHandler) error { // 注册路由 r.engine.Handle(handler.GetMethod(), handler.GetPath(), append(middlewares, handler.Handle)...) - r.logger.Info("Registered HTTP handler", + r.logger.Info("已注册HTTP处理器", zap.String("method", handler.GetMethod()), zap.String("path", handler.GetPath())) @@ -62,7 +64,7 @@ func (r *GinRouter) RegisterHandler(handler interfaces.HTTPHandler) error { func (r *GinRouter) RegisterMiddleware(middleware interfaces.Middleware) error { r.middlewares = append(r.middlewares, middleware) - r.logger.Info("Registered middleware", + r.logger.Info("已注册中间件", zap.String("name", middleware.GetName()), zap.Int("priority", middleware.GetPriority())) @@ -93,7 +95,7 @@ func (r *GinRouter) Start(addr string) error { IdleTimeout: r.config.Server.IdleTimeout, } - r.logger.Info("Starting HTTP server", zap.String("addr", addr)) + r.logger.Info("正在启动HTTP服务器", zap.String("addr", addr)) // 启动服务器 if err := r.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { @@ -109,15 +111,15 @@ func (r *GinRouter) Stop(ctx context.Context) error { return nil } - r.logger.Info("Stopping HTTP server...") + r.logger.Info("正在关闭HTTP服务器...") // 优雅关闭服务器 if err := r.server.Shutdown(ctx); err != nil { - r.logger.Error("Failed to shutdown server gracefully", zap.Error(err)) + r.logger.Error("优雅关闭服务器失败", zap.Error(err)) return err } - r.logger.Info("HTTP server stopped") + r.logger.Info("HTTP服务器已关闭") return nil } @@ -137,7 +139,7 @@ func (r *GinRouter) applyMiddlewares() { for _, middleware := range r.middlewares { if middleware.IsGlobal() { r.engine.Use(middleware.Handle()) - r.logger.Debug("Applied global middleware", + r.logger.Debug("已应用全局中间件", zap.String("name", middleware.GetName()), zap.Int("priority", middleware.GetPriority())) } @@ -156,6 +158,18 @@ func (r *GinRouter) SetupDefaultRoutes() { }) }) + // 详细健康检查 + r.engine.GET("/health/detailed", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "status": "healthy", + "timestamp": time.Now().Unix(), + "service": r.config.App.Name, + "version": r.config.App.Version, + "uptime": time.Now().Unix(), + "environment": r.config.App.Env, + }) + }) + // API信息 r.engine.GET("/info", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ @@ -166,11 +180,37 @@ func (r *GinRouter) SetupDefaultRoutes() { }) }) + // Swagger文档路由 (仅在开发环境启用) + if !r.config.App.IsProduction() { + // Swagger UI + r.engine.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) + + // API文档重定向 + r.engine.GET("/docs", func(c *gin.Context) { + c.Redirect(http.StatusMovedPermanently, "/swagger/index.html") + }) + + // API文档信息 + r.engine.GET("/api/docs", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "swagger_ui": fmt.Sprintf("http://%s/swagger/index.html", c.Request.Host), + "openapi_json": fmt.Sprintf("http://%s/swagger/doc.json", c.Request.Host), + "redoc": fmt.Sprintf("http://%s/redoc", c.Request.Host), + "message": "API文档已可用", + }) + }) + + r.logger.Info("Swagger documentation enabled", + zap.String("swagger_url", "/swagger/index.html"), + zap.String("docs_url", "/docs"), + zap.String("api_docs_url", "/api/docs")) + } + // 404处理 r.engine.NoRoute(func(c *gin.Context) { c.JSON(http.StatusNotFound, gin.H{ "success": false, - "message": "Route not found", + "message": "路由未找到", "path": c.Request.URL.Path, "method": c.Request.Method, "timestamp": time.Now().Unix(), @@ -181,7 +221,7 @@ func (r *GinRouter) SetupDefaultRoutes() { r.engine.NoMethod(func(c *gin.Context) { c.JSON(http.StatusMethodNotAllowed, gin.H{ "success": false, - "message": "Method not allowed", + "message": "请求方法不允许", "path": c.Request.URL.Path, "method": c.Request.Method, "timestamp": time.Now().Unix(), diff --git a/internal/shared/http/validator.go b/internal/shared/http/validator.go index 3fdc46b..ef7b8a0 100644 --- a/internal/shared/http/validator.go +++ b/internal/shared/http/validator.go @@ -42,13 +42,13 @@ func (v *RequestValidator) Validate(c *gin.Context, dto interface{}) error { // ValidateQuery 验证查询参数 func (v *RequestValidator) ValidateQuery(c *gin.Context, dto interface{}) error { if err := c.ShouldBindQuery(dto); err != nil { - v.response.BadRequest(c, "Invalid query parameters", err.Error()) + v.response.BadRequest(c, "查询参数格式错误", err.Error()) return err } if err := v.validator.Struct(dto); err != nil { validationErrors := v.formatValidationErrors(err) - v.response.BadRequest(c, "Validation failed", validationErrors) + v.response.ValidationError(c, validationErrors) return err } return nil @@ -57,13 +57,13 @@ func (v *RequestValidator) ValidateQuery(c *gin.Context, dto interface{}) error // ValidateParam 验证路径参数 func (v *RequestValidator) ValidateParam(c *gin.Context, dto interface{}) error { if err := c.ShouldBindUri(dto); err != nil { - v.response.BadRequest(c, "Invalid path parameters", err.Error()) + v.response.BadRequest(c, "路径参数格式错误", err.Error()) return err } if err := v.validator.Struct(dto); err != nil { validationErrors := v.formatValidationErrors(err) - v.response.BadRequest(c, "Validation failed", validationErrors) + v.response.ValidationError(c, validationErrors) return err } return nil @@ -73,7 +73,7 @@ func (v *RequestValidator) ValidateParam(c *gin.Context, dto interface{}) error func (v *RequestValidator) BindAndValidate(c *gin.Context, dto interface{}) error { // 绑定请求体 if err := c.ShouldBindJSON(dto); err != nil { - v.response.BadRequest(c, "Invalid request body", err.Error()) + v.response.BadRequest(c, "请求体格式错误", err.Error()) return err } @@ -115,44 +115,74 @@ func (v *RequestValidator) getErrorMessage(fieldError validator.FieldError) stri tag := fieldError.Tag() param := fieldError.Param() + fieldDisplayName := v.getFieldDisplayName(field) + switch tag { case "required": - return fmt.Sprintf("%s is required", field) + return fmt.Sprintf("%s 不能为空", fieldDisplayName) case "email": - return fmt.Sprintf("%s must be a valid email address", field) + return fmt.Sprintf("%s 必须是有效的邮箱地址", fieldDisplayName) case "min": - return fmt.Sprintf("%s must be at least %s characters", field, param) + return fmt.Sprintf("%s 长度不能少于 %s 位", fieldDisplayName, param) case "max": - return fmt.Sprintf("%s must be at most %s characters", field, param) + return fmt.Sprintf("%s 长度不能超过 %s 位", fieldDisplayName, param) case "len": - return fmt.Sprintf("%s must be exactly %s characters", field, param) + return fmt.Sprintf("%s 长度必须为 %s 位", fieldDisplayName, param) case "gt": - return fmt.Sprintf("%s must be greater than %s", field, param) + return fmt.Sprintf("%s 必须大于 %s", fieldDisplayName, param) case "gte": - return fmt.Sprintf("%s must be greater than or equal to %s", field, param) + return fmt.Sprintf("%s 必须大于等于 %s", fieldDisplayName, param) case "lt": - return fmt.Sprintf("%s must be less than %s", field, param) + return fmt.Sprintf("%s 必须小于 %s", fieldDisplayName, param) case "lte": - return fmt.Sprintf("%s must be less than or equal to %s", field, param) + return fmt.Sprintf("%s 必须小于等于 %s", fieldDisplayName, param) case "oneof": - return fmt.Sprintf("%s must be one of [%s]", field, param) + return fmt.Sprintf("%s 必须是以下值之一:[%s]", fieldDisplayName, param) case "url": - return fmt.Sprintf("%s must be a valid URL", field) + return fmt.Sprintf("%s 必须是有效的URL地址", fieldDisplayName) case "alpha": - return fmt.Sprintf("%s must contain only alphabetic characters", field) + return fmt.Sprintf("%s 只能包含字母", fieldDisplayName) case "alphanum": - return fmt.Sprintf("%s must contain only alphanumeric characters", field) + return fmt.Sprintf("%s 只能包含字母和数字", fieldDisplayName) case "numeric": - return fmt.Sprintf("%s must be numeric", field) + return fmt.Sprintf("%s 必须是数字", fieldDisplayName) case "phone": - return fmt.Sprintf("%s must be a valid phone number", field) + return fmt.Sprintf("%s 必须是有效的手机号", fieldDisplayName) case "username": - return fmt.Sprintf("%s must be a valid username", field) + return fmt.Sprintf("%s 格式不正确,只能包含字母、数字、下划线,且不能以数字开头", fieldDisplayName) + case "strong_password": + return fmt.Sprintf("%s 强度不足,必须包含大小写字母和数字,且不少于8位", fieldDisplayName) + case "eqfield": + return fmt.Sprintf("%s 必须与 %s 一致", fieldDisplayName, v.getFieldDisplayName(param)) default: - return fmt.Sprintf("%s is invalid", field) + return fmt.Sprintf("%s 格式不正确", fieldDisplayName) } } +// getFieldDisplayName 获取字段显示名称(中文) +func (v *RequestValidator) getFieldDisplayName(field string) string { + fieldNames := map[string]string{ + "phone": "手机号", + "password": "密码", + "confirm_password": "确认密码", + "old_password": "原密码", + "new_password": "新密码", + "confirm_new_password": "确认新密码", + "code": "验证码", + "username": "用户名", + "email": "邮箱", + "display_name": "显示名称", + "scene": "使用场景", + "Password": "密码", + "NewPassword": "新密码", + } + + if displayName, exists := fieldNames[field]; exists { + return displayName + } + return field +} + // toSnakeCase 转换为snake_case func (v *RequestValidator) toSnakeCase(str string) string { var result strings.Builder diff --git a/internal/shared/http/validator_zh.go b/internal/shared/http/validator_zh.go new file mode 100644 index 0000000..404ba36 --- /dev/null +++ b/internal/shared/http/validator_zh.go @@ -0,0 +1,294 @@ +package http + +import ( + "strings" + + "tyapi-server/internal/shared/interfaces" + + "github.com/gin-gonic/gin" + "github.com/go-playground/locales/zh" + ut "github.com/go-playground/universal-translator" + "github.com/go-playground/validator/v10" + zh_translations "github.com/go-playground/validator/v10/translations/zh" +) + +// RequestValidatorZh 中文验证器实现 +type RequestValidatorZh struct { + validator *validator.Validate + translator ut.Translator + response interfaces.ResponseBuilder +} + +// NewRequestValidatorZh 创建支持中文翻译的请求验证器 +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, + } +} + +// Validate 验证请求体 +func (v *RequestValidatorZh) Validate(c *gin.Context, dto interface{}) error { + if err := v.validator.Struct(dto); err != nil { + validationErrors := v.formatValidationErrorsZh(err) + v.response.ValidationError(c, validationErrors) + return err + } + return nil +} + +// ValidateQuery 验证查询参数 +func (v *RequestValidatorZh) ValidateQuery(c *gin.Context, dto interface{}) error { + if err := c.ShouldBindQuery(dto); err != nil { + v.response.BadRequest(c, "查询参数格式错误", err.Error()) + return err + } + + if err := v.validator.Struct(dto); err != nil { + validationErrors := v.formatValidationErrorsZh(err) + v.response.ValidationError(c, validationErrors) + return err + } + return nil +} + +// ValidateParam 验证路径参数 +func (v *RequestValidatorZh) ValidateParam(c *gin.Context, dto interface{}) error { + if err := c.ShouldBindUri(dto); err != nil { + v.response.BadRequest(c, "路径参数格式错误", err.Error()) + return err + } + + if err := v.validator.Struct(dto); err != nil { + validationErrors := v.formatValidationErrorsZh(err) + v.response.ValidationError(c, validationErrors) + return err + } + return nil +} + +// BindAndValidate 绑定并验证请求 +func (v *RequestValidatorZh) BindAndValidate(c *gin.Context, dto interface{}) error { + // 绑定请求体 + if err := c.ShouldBindJSON(dto); err != nil { + v.response.BadRequest(c, "请求体格式错误", err.Error()) + return err + } + + // 验证数据 + return v.Validate(c, dto) +} + +// formatValidationErrorsZh 格式化验证错误(中文翻译版) +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 +} + +// getFieldNameZh 获取字段名(JSON标签优先) +func (v *RequestValidatorZh) getFieldNameZh(fieldError validator.FieldError) string { + fieldName := fieldError.Field() + return v.toSnakeCase(fieldName) +} + +// getFieldDisplayName 获取字段显示名称(中文) +func (v *RequestValidatorZh) getFieldDisplayName(field string) string { + fieldNames := map[string]string{ + "phone": "手机号", + "password": "密码", + "confirm_password": "确认密码", + "old_password": "原密码", + "new_password": "新密码", + "confirm_new_password": "确认新密码", + "code": "验证码", + "username": "用户名", + "email": "邮箱", + "display_name": "显示名称", + "scene": "使用场景", + "Password": "密码", + "NewPassword": "新密码", + "ConfirmPassword": "确认密码", + } + + if displayName, exists := fieldNames[field]; exists { + return displayName + } + return field +} + +// toSnakeCase 转换为snake_case +func (v *RequestValidatorZh) toSnakeCase(str string) string { + var result strings.Builder + for i, r := range str { + if i > 0 && (r >= 'A' && r <= 'Z') { + result.WriteRune('_') + } + result.WriteRune(r) + } + return strings.ToLower(result.String()) +} + +// registerCustomValidatorsZh 注册自定义验证器和中文翻译 +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 + }) + + // 自定义eqfield翻译 + v.RegisterTranslation("eqfield", trans, func(ut ut.Translator) error { + return ut.Add("eqfield", "{0}必须等于{1}", true) + }, func(ut ut.Translator, fe validator.FieldError) string { + t, _ := ut.T("eqfield", fe.Field(), fe.Param()) + return t + }) +} + +// validatePhoneZh 验证手机号 +func validatePhoneZh(fl validator.FieldLevel) bool { + phone := fl.Field().String() + if phone == "" { + return true // 空值由required标签处理 + } + + // 中国手机号验证:11位,以1开头 + if len(phone) != 11 { + return false + } + + if !strings.HasPrefix(phone, "1") { + return false + } + + // 检查是否全是数字 + for _, r := range phone { + if r < '0' || r > '9' { + return false + } + } + + return true +} + +// validateUsernameZh 验证用户名 +func validateUsernameZh(fl validator.FieldLevel) bool { + username := fl.Field().String() + if username == "" { + return true // 空值由required标签处理 + } + + // 用户名规则:3-30个字符,只能包含字母、数字、下划线,不能以数字开头 + if len(username) < 3 || len(username) > 30 { + return false + } + + // 不能以数字开头 + if username[0] >= '0' && username[0] <= '9' { + return false + } + + // 只能包含字母、数字、下划线 + for _, r := range username { + if !((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_') { + return false + } + } + + return true +} + +// validateStrongPasswordZh 验证密码强度 +func validateStrongPasswordZh(fl validator.FieldLevel) bool { + password := fl.Field().String() + if password == "" { + return true // 空值由required标签处理 + } + + // 密码强度规则:至少8个字符,包含大小写字母、数字 + if len(password) < 8 { + return false + } + + hasUpper := false + hasLower := false + hasDigit := false + + for _, r := range password { + switch { + case r >= 'A' && r <= 'Z': + hasUpper = true + case r >= 'a' && r <= 'z': + hasLower = true + case r >= '0' && r <= '9': + hasDigit = true + } + } + + return hasUpper && hasLower && hasDigit +} + +// ValidateStruct 直接验证结构体 +func (v *RequestValidatorZh) ValidateStruct(dto interface{}) error { + return v.validator.Struct(dto) +} diff --git a/internal/shared/interfaces/http.go b/internal/shared/interfaces/http.go index 043b0e1..00eb4ab 100644 --- a/internal/shared/interfaces/http.go +++ b/internal/shared/interfaces/http.go @@ -76,9 +76,14 @@ type ResponseBuilder interface { NotFound(c *gin.Context, message ...string) Conflict(c *gin.Context, message string) InternalError(c *gin.Context, message ...string) + ValidationError(c *gin.Context, errors interface{}) + TooManyRequests(c *gin.Context, message ...string) // 分页响应 Paginated(c *gin.Context, data interface{}, pagination PaginationMeta) + + // 自定义响应 + CustomResponse(c *gin.Context, statusCode int, data interface{}) } // RequestValidator 请求验证器接口 @@ -90,6 +95,9 @@ type RequestValidator interface { // 绑定和验证 BindAndValidate(c *gin.Context, dto interface{}) error + + // 直接验证结构体 + ValidateStruct(dto interface{}) error } // PaginationMeta 分页元数据 diff --git a/internal/shared/interfaces/service.go b/internal/shared/interfaces/service.go index 0e8647e..d6d11c6 100644 --- a/internal/shared/interfaces/service.go +++ b/internal/shared/interfaces/service.go @@ -2,6 +2,15 @@ package interfaces import ( "context" + "errors" + + "tyapi-server/internal/domains/user/dto" + "tyapi-server/internal/domains/user/entities" +) + +// 常见错误定义 +var ( + ErrCacheMiss = errors.New("cache miss") ) // Service 通用服务接口 @@ -16,6 +25,22 @@ type Service interface { Shutdown(ctx context.Context) error } +// UserService 用户服务接口 +type UserService interface { + Service + + // 用户注册 + Register(ctx context.Context, req *dto.RegisterRequest) (*entities.User, error) + // 密码登录 + LoginWithPassword(ctx context.Context, req *dto.LoginWithPasswordRequest) (*entities.User, error) + // 短信验证码登录 + LoginWithSMS(ctx context.Context, req *dto.LoginWithSMSRequest) (*entities.User, error) + // 修改密码 + ChangePassword(ctx context.Context, userID string, req *dto.ChangePasswordRequest) error + // 根据ID获取用户 + GetByID(ctx context.Context, id string) (*entities.User, error) +} + // DomainService 领域服务接口,支持泛型 type DomainService[T Entity] interface { Service diff --git a/internal/shared/logger/enhanced_logger.go b/internal/shared/logger/enhanced_logger.go new file mode 100644 index 0000000..bb08fd4 --- /dev/null +++ b/internal/shared/logger/enhanced_logger.go @@ -0,0 +1,214 @@ +package logger + +import ( + "context" + "strings" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// LogLevel 日志级别 +type LogLevel string + +const ( + DebugLevel LogLevel = "debug" + InfoLevel LogLevel = "info" + WarnLevel LogLevel = "warn" + ErrorLevel LogLevel = "error" +) + +// LogContext 日志上下文 +type LogContext struct { + RequestID string + UserID string + TraceID string + OperationName string + Layer string // repository/service/handler + Component string +} + +// ContextualLogger 上下文感知的日志器 +type ContextualLogger struct { + logger *zap.Logger + ctx LogContext +} + +// NewContextualLogger 创建上下文日志器 +func NewContextualLogger(logger *zap.Logger) *ContextualLogger { + return &ContextualLogger{ + logger: logger, + } +} + +// WithContext 添加上下文信息 +func (l *ContextualLogger) WithContext(ctx context.Context) *ContextualLogger { + logCtx := LogContext{} + + // 从context中提取常用字段 + if requestID := getStringFromContext(ctx, "request_id"); requestID != "" { + logCtx.RequestID = requestID + } + if userID := getStringFromContext(ctx, "user_id"); userID != "" { + logCtx.UserID = userID + } + if traceID := getStringFromContext(ctx, "trace_id"); traceID != "" { + logCtx.TraceID = traceID + } + + return &ContextualLogger{ + logger: l.logger, + ctx: logCtx, + } +} + +// WithLayer 设置层级信息 +func (l *ContextualLogger) WithLayer(layer string) *ContextualLogger { + newCtx := l.ctx + newCtx.Layer = layer + return &ContextualLogger{ + logger: l.logger, + ctx: newCtx, + } +} + +// WithComponent 设置组件信息 +func (l *ContextualLogger) WithComponent(component string) *ContextualLogger { + newCtx := l.ctx + newCtx.Component = component + return &ContextualLogger{ + logger: l.logger, + ctx: newCtx, + } +} + +// WithOperation 设置操作名称 +func (l *ContextualLogger) WithOperation(operation string) *ContextualLogger { + newCtx := l.ctx + newCtx.OperationName = operation + return &ContextualLogger{ + logger: l.logger, + ctx: newCtx, + } +} + +// 构建基础字段 +func (l *ContextualLogger) buildBaseFields() []zapcore.Field { + fields := []zapcore.Field{} + + if l.ctx.RequestID != "" { + fields = append(fields, zap.String("request_id", l.ctx.RequestID)) + } + if l.ctx.UserID != "" { + fields = append(fields, zap.String("user_id", l.ctx.UserID)) + } + if l.ctx.TraceID != "" { + fields = append(fields, zap.String("trace_id", l.ctx.TraceID)) + } + if l.ctx.Layer != "" { + fields = append(fields, zap.String("layer", l.ctx.Layer)) + } + if l.ctx.Component != "" { + fields = append(fields, zap.String("component", l.ctx.Component)) + } + if l.ctx.OperationName != "" { + fields = append(fields, zap.String("operation", l.ctx.OperationName)) + } + + return fields +} + +// LogTechnicalError 记录技术性错误(Repository层) +func (l *ContextualLogger) LogTechnicalError(msg string, err error, fields ...zapcore.Field) { + allFields := l.buildBaseFields() + allFields = append(allFields, zap.Error(err)) + allFields = append(allFields, zap.String("error_type", "technical")) + allFields = append(allFields, fields...) + + l.logger.Error(msg, allFields...) +} + +// LogBusinessWarn 记录业务警告(Service层) +func (l *ContextualLogger) LogBusinessWarn(msg string, fields ...zapcore.Field) { + allFields := l.buildBaseFields() + allFields = append(allFields, zap.String("log_type", "business")) + allFields = append(allFields, fields...) + + l.logger.Warn(msg, allFields...) +} + +// LogBusinessInfo 记录业务信息(Service层) +func (l *ContextualLogger) LogBusinessInfo(msg string, fields ...zapcore.Field) { + allFields := l.buildBaseFields() + allFields = append(allFields, zap.String("log_type", "business")) + allFields = append(allFields, fields...) + + l.logger.Info(msg, allFields...) +} + +// LogUserAction 记录用户行为(Handler层) +func (l *ContextualLogger) LogUserAction(msg string, fields ...zapcore.Field) { + allFields := l.buildBaseFields() + allFields = append(allFields, zap.String("log_type", "user_action")) + allFields = append(allFields, fields...) + + l.logger.Info(msg, allFields...) +} + +// LogRequestFailed 记录请求失败(Handler层) +func (l *ContextualLogger) LogRequestFailed(msg string, errorType string, fields ...zapcore.Field) { + allFields := l.buildBaseFields() + allFields = append(allFields, zap.String("log_type", "request_failed")) + allFields = append(allFields, zap.String("error_category", errorType)) + allFields = append(allFields, fields...) + + l.logger.Info(msg, allFields...) +} + +// getStringFromContext 从上下文获取字符串值 +func getStringFromContext(ctx context.Context, key string) string { + if value := ctx.Value(key); value != nil { + if str, ok := value.(string); ok { + return str + } + } + return "" +} + +// ErrorCategory 错误分类 +type ErrorCategory string + +const ( + DatabaseError ErrorCategory = "database" + NetworkError ErrorCategory = "network" + ValidationError ErrorCategory = "validation" + BusinessError ErrorCategory = "business" + AuthError ErrorCategory = "auth" + ExternalAPIError ErrorCategory = "external_api" +) + +// CategorizeError 错误分类 +func CategorizeError(err error) ErrorCategory { + errMsg := strings.ToLower(err.Error()) + + switch { + case strings.Contains(errMsg, "database") || + strings.Contains(errMsg, "sql") || + strings.Contains(errMsg, "gorm"): + return DatabaseError + case strings.Contains(errMsg, "network") || + strings.Contains(errMsg, "connection") || + strings.Contains(errMsg, "timeout"): + return NetworkError + case strings.Contains(errMsg, "validation") || + strings.Contains(errMsg, "invalid") || + strings.Contains(errMsg, "format"): + return ValidationError + case strings.Contains(errMsg, "unauthorized") || + strings.Contains(errMsg, "forbidden") || + strings.Contains(errMsg, "token"): + return AuthError + default: + return BusinessError + } +} diff --git a/internal/shared/metrics/business_metrics.go b/internal/shared/metrics/business_metrics.go new file mode 100644 index 0000000..9983a03 --- /dev/null +++ b/internal/shared/metrics/business_metrics.go @@ -0,0 +1,263 @@ +package metrics + +import ( + "context" + "sync" + + "go.uber.org/zap" + + "tyapi-server/internal/shared/interfaces" +) + +// BusinessMetrics 业务指标收集器 +type BusinessMetrics struct { + metrics interfaces.MetricsCollector + logger *zap.Logger + mutex sync.RWMutex + + // 业务指标缓存 + userMetrics map[string]int64 + orderMetrics map[string]int64 +} + +// NewBusinessMetrics 创建业务指标收集器 +func NewBusinessMetrics(metrics interfaces.MetricsCollector, logger *zap.Logger) *BusinessMetrics { + bm := &BusinessMetrics{ + metrics: metrics, + logger: logger, + userMetrics: make(map[string]int64), + orderMetrics: make(map[string]int64), + } + + // 注册业务指标 + bm.registerBusinessMetrics() + + return bm +} + +// registerBusinessMetrics 注册业务指标 +func (bm *BusinessMetrics) registerBusinessMetrics() { + // 用户相关指标 + bm.metrics.RegisterCounter("users_created_total", "Total number of users created", []string{"source"}) + bm.metrics.RegisterCounter("users_login_total", "Total number of user logins", []string{"method", "status"}) + bm.metrics.RegisterGauge("users_active_sessions", "Current number of active user sessions", nil) + + // 订单相关指标 + bm.metrics.RegisterCounter("orders_created_total", "Total number of orders created", []string{"status"}) + bm.metrics.RegisterCounter("orders_amount_total", "Total order amount in cents", []string{"currency"}) + bm.metrics.RegisterHistogram("orders_processing_duration_seconds", "Order processing duration", []string{"status"}, []float64{0.1, 0.5, 1, 2, 5, 10, 30}) + + // API相关指标 + bm.metrics.RegisterCounter("api_errors_total", "Total number of API errors", []string{"endpoint", "error_type"}) + bm.metrics.RegisterHistogram("api_response_size_bytes", "API response size in bytes", []string{"endpoint"}, []float64{100, 1000, 10000, 100000}) + + // 缓存相关指标 + bm.metrics.RegisterCounter("cache_operations_total", "Total number of cache operations", []string{"operation", "result"}) + bm.metrics.RegisterGauge("cache_memory_usage_bytes", "Cache memory usage in bytes", []string{"cache_type"}) + + // 数据库相关指标 + bm.metrics.RegisterHistogram("database_query_duration_seconds", "Database query duration", []string{"operation", "table"}, []float64{0.001, 0.01, 0.1, 1, 10}) + bm.metrics.RegisterCounter("database_errors_total", "Total number of database errors", []string{"operation", "error_type"}) + + bm.logger.Info("Business metrics registered successfully") +} + +// User相关指标 + +// RecordUserCreated 记录用户创建 +func (bm *BusinessMetrics) RecordUserCreated(source string) { + bm.metrics.IncrementCounter("users_created_total", map[string]string{ + "source": source, + }) + + bm.mutex.Lock() + bm.userMetrics["created"]++ + bm.mutex.Unlock() + + bm.logger.Debug("Recorded user created", zap.String("source", source)) +} + +// RecordUserLogin 记录用户登录 +func (bm *BusinessMetrics) RecordUserLogin(method, status string) { + bm.metrics.IncrementCounter("users_login_total", map[string]string{ + "method": method, + "status": status, + }) + + bm.logger.Debug("Recorded user login", zap.String("method", method), zap.String("status", status)) +} + +// UpdateActiveUserSessions 更新活跃用户会话数 +func (bm *BusinessMetrics) UpdateActiveUserSessions(count float64) { + bm.metrics.RecordGauge("users_active_sessions", count, nil) +} + +// Order相关指标 + +// RecordOrderCreated 记录订单创建 +func (bm *BusinessMetrics) RecordOrderCreated(status string, amount float64, currency string) { + bm.metrics.IncrementCounter("orders_created_total", map[string]string{ + "status": status, + }) + + // 记录订单金额(以分为单位) + amountCents := int64(amount * 100) + bm.metrics.IncrementCounter("orders_amount_total", map[string]string{ + "currency": currency, + }) + + bm.mutex.Lock() + bm.orderMetrics["created"]++ + bm.orderMetrics["amount"] += amountCents + bm.mutex.Unlock() + + bm.logger.Debug("Recorded order created", + zap.String("status", status), + zap.Float64("amount", amount), + zap.String("currency", currency)) +} + +// RecordOrderProcessingDuration 记录订单处理时长 +func (bm *BusinessMetrics) RecordOrderProcessingDuration(status string, duration float64) { + bm.metrics.RecordHistogram("orders_processing_duration_seconds", duration, map[string]string{ + "status": status, + }) +} + +// API相关指标 + +// RecordAPIError 记录API错误 +func (bm *BusinessMetrics) RecordAPIError(endpoint, errorType string) { + bm.metrics.IncrementCounter("api_errors_total", map[string]string{ + "endpoint": endpoint, + "error_type": errorType, + }) + + bm.logger.Debug("Recorded API error", + zap.String("endpoint", endpoint), + zap.String("error_type", errorType)) +} + +// RecordAPIResponseSize 记录API响应大小 +func (bm *BusinessMetrics) RecordAPIResponseSize(endpoint string, sizeBytes float64) { + bm.metrics.RecordHistogram("api_response_size_bytes", sizeBytes, map[string]string{ + "endpoint": endpoint, + }) +} + +// Cache相关指标 + +// RecordCacheOperation 记录缓存操作 +func (bm *BusinessMetrics) RecordCacheOperation(operation, result string) { + bm.metrics.IncrementCounter("cache_operations_total", map[string]string{ + "operation": operation, + "result": result, + }) +} + +// UpdateCacheMemoryUsage 更新缓存内存使用量 +func (bm *BusinessMetrics) UpdateCacheMemoryUsage(cacheType string, usageBytes float64) { + bm.metrics.RecordGauge("cache_memory_usage_bytes", usageBytes, map[string]string{ + "cache_type": cacheType, + }) +} + +// Database相关指标 + +// RecordDatabaseQuery 记录数据库查询 +func (bm *BusinessMetrics) RecordDatabaseQuery(operation, table string, duration float64) { + bm.metrics.RecordHistogram("database_query_duration_seconds", duration, map[string]string{ + "operation": operation, + "table": table, + }) +} + +// RecordDatabaseError 记录数据库错误 +func (bm *BusinessMetrics) RecordDatabaseError(operation, errorType string) { + bm.metrics.IncrementCounter("database_errors_total", map[string]string{ + "operation": operation, + "error_type": errorType, + }) + + bm.logger.Debug("Recorded database error", + zap.String("operation", operation), + zap.String("error_type", errorType)) +} + +// 获取统计信息 + +// GetUserStats 获取用户统计 +func (bm *BusinessMetrics) GetUserStats() map[string]int64 { + bm.mutex.RLock() + defer bm.mutex.RUnlock() + + stats := make(map[string]int64) + for k, v := range bm.userMetrics { + stats[k] = v + } + return stats +} + +// GetOrderStats 获取订单统计 +func (bm *BusinessMetrics) GetOrderStats() map[string]int64 { + bm.mutex.RLock() + defer bm.mutex.RUnlock() + + stats := make(map[string]int64) + for k, v := range bm.orderMetrics { + stats[k] = v + } + return stats +} + +// GetOverallStats 获取整体统计 +func (bm *BusinessMetrics) GetOverallStats() map[string]interface{} { + return map[string]interface{}{ + "user_stats": bm.GetUserStats(), + "order_stats": bm.GetOrderStats(), + } +} + +// Reset 重置统计数据 +func (bm *BusinessMetrics) Reset() { + bm.mutex.Lock() + defer bm.mutex.Unlock() + + bm.userMetrics = make(map[string]int64) + bm.orderMetrics = make(map[string]int64) + + bm.logger.Info("Business metrics reset") +} + +// Context相关方法 + +// WithContext 创建带上下文的业务指标收集器 +func (bm *BusinessMetrics) WithContext(ctx context.Context) *BusinessMetrics { + // 这里可以从context中提取追踪信息,关联指标 + return bm +} + +// 实现Service接口(如果需要) + +// Name 返回服务名称 +func (bm *BusinessMetrics) Name() string { + return "business-metrics" +} + +// Initialize 初始化服务 +func (bm *BusinessMetrics) Initialize(ctx context.Context) error { + bm.logger.Info("Business metrics service initialized") + return nil +} + +// HealthCheck 健康检查 +func (bm *BusinessMetrics) HealthCheck(ctx context.Context) error { + // 检查指标收集器是否正常 + return nil +} + +// Shutdown 关闭服务 +func (bm *BusinessMetrics) Shutdown(ctx context.Context) error { + bm.logger.Info("Business metrics service shutdown") + return nil +} diff --git a/internal/shared/metrics/prometheus_metrics.go b/internal/shared/metrics/prometheus_metrics.go new file mode 100644 index 0000000..7a52a44 --- /dev/null +++ b/internal/shared/metrics/prometheus_metrics.go @@ -0,0 +1,353 @@ +package metrics + +import ( + "net/http" + "strconv" + "sync" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" + "go.uber.org/zap" +) + +// PrometheusMetrics Prometheus指标收集器 +type PrometheusMetrics struct { + logger *zap.Logger + registry *prometheus.Registry + mutex sync.RWMutex + + // 预定义指标 + httpRequests *prometheus.CounterVec + httpDuration *prometheus.HistogramVec + activeUsers prometheus.Gauge + dbConnections prometheus.Gauge + cacheHits *prometheus.CounterVec + businessMetrics map[string]prometheus.Collector +} + +// NewPrometheusMetrics 创建Prometheus指标收集器 +func NewPrometheusMetrics(logger *zap.Logger) *PrometheusMetrics { + registry := prometheus.NewRegistry() + + // HTTP请求计数器 + httpRequests := prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "http_requests_total", + Help: "Total number of HTTP requests", + }, + []string{"method", "path", "status"}, + ) + + // HTTP请求耗时直方图 + httpDuration := prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "http_request_duration_seconds", + Help: "HTTP request duration in seconds", + Buckets: prometheus.DefBuckets, + }, + []string{"method", "path"}, + ) + + // 活跃用户数 + activeUsers := prometheus.NewGauge( + prometheus.GaugeOpts{ + Name: "active_users_total", + Help: "Current number of active users", + }, + ) + + // 数据库连接数 + dbConnections := prometheus.NewGauge( + prometheus.GaugeOpts{ + Name: "database_connections_active", + Help: "Current number of active database connections", + }, + ) + + // 缓存命中率 + cacheHits := prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "cache_operations_total", + Help: "Total number of cache operations", + }, + []string{"operation", "result"}, + ) + + // 注册指标 + registry.MustRegister(httpRequests) + registry.MustRegister(httpDuration) + registry.MustRegister(activeUsers) + registry.MustRegister(dbConnections) + registry.MustRegister(cacheHits) + + return &PrometheusMetrics{ + logger: logger, + registry: registry, + httpRequests: httpRequests, + httpDuration: httpDuration, + activeUsers: activeUsers, + dbConnections: dbConnections, + cacheHits: cacheHits, + businessMetrics: make(map[string]prometheus.Collector), + } +} + +// RecordHTTPRequest 记录HTTP请求指标 +func (m *PrometheusMetrics) RecordHTTPRequest(method, path string, statusCode int, duration float64) { + status := strconv.Itoa(statusCode) + + m.httpRequests.WithLabelValues(method, path, status).Inc() + m.httpDuration.WithLabelValues(method, path).Observe(duration) + + m.logger.Debug("Recorded HTTP request metric", + zap.String("method", method), + zap.String("path", path), + zap.String("status", status), + zap.Float64("duration", duration)) +} + +// RecordHTTPDuration 记录HTTP请求耗时 +func (m *PrometheusMetrics) RecordHTTPDuration(method, path string, duration float64) { + m.httpDuration.WithLabelValues(method, path).Observe(duration) + + m.logger.Debug("Recorded HTTP duration metric", + zap.String("method", method), + zap.String("path", path), + zap.Float64("duration", duration)) +} + +// IncrementCounter 增加计数器 +func (m *PrometheusMetrics) IncrementCounter(name string, labels map[string]string) { + if counter, exists := m.getOrCreateCounter(name, labels); exists { + if vec, ok := counter.(*prometheus.CounterVec); ok { + vec.With(labels).Inc() + } + } +} + +// RecordGauge 记录仪表盘值 +func (m *PrometheusMetrics) RecordGauge(name string, value float64, labels map[string]string) { + if gauge, exists := m.getOrCreateGauge(name, labels); exists { + if vec, ok := gauge.(*prometheus.GaugeVec); ok { + vec.With(labels).Set(value) + } else if g, ok := gauge.(prometheus.Gauge); ok { + g.Set(value) + } + } +} + +// RecordHistogram 记录直方图值 +func (m *PrometheusMetrics) RecordHistogram(name string, value float64, labels map[string]string) { + if histogram, exists := m.getOrCreateHistogram(name, labels); exists { + if vec, ok := histogram.(*prometheus.HistogramVec); ok { + vec.With(labels).Observe(value) + } + } +} + +// RegisterCounter 注册计数器 +func (m *PrometheusMetrics) RegisterCounter(name, help string, labels []string) error { + m.mutex.Lock() + defer m.mutex.Unlock() + + if _, exists := m.businessMetrics[name]; exists { + return nil // 已存在 + } + + var counter prometheus.Collector + if len(labels) > 0 { + counter = prometheus.NewCounterVec( + prometheus.CounterOpts{Name: name, Help: help}, + labels, + ) + } else { + counter = prometheus.NewCounter( + prometheus.CounterOpts{Name: name, Help: help}, + ) + } + + if err := m.registry.Register(counter); err != nil { + return err + } + + m.businessMetrics[name] = counter + m.logger.Info("Registered counter metric", zap.String("name", name)) + return nil +} + +// RegisterGauge 注册仪表盘 +func (m *PrometheusMetrics) RegisterGauge(name, help string, labels []string) error { + m.mutex.Lock() + defer m.mutex.Unlock() + + if _, exists := m.businessMetrics[name]; exists { + return nil + } + + var gauge prometheus.Collector + if len(labels) > 0 { + gauge = prometheus.NewGaugeVec( + prometheus.GaugeOpts{Name: name, Help: help}, + labels, + ) + } else { + gauge = prometheus.NewGauge( + prometheus.GaugeOpts{Name: name, Help: help}, + ) + } + + if err := m.registry.Register(gauge); err != nil { + return err + } + + m.businessMetrics[name] = gauge + m.logger.Info("Registered gauge metric", zap.String("name", name)) + return nil +} + +// RegisterHistogram 注册直方图 +func (m *PrometheusMetrics) RegisterHistogram(name, help string, labels []string, buckets []float64) error { + m.mutex.Lock() + defer m.mutex.Unlock() + + if _, exists := m.businessMetrics[name]; exists { + return nil + } + + if buckets == nil { + buckets = prometheus.DefBuckets + } + + var histogram prometheus.Collector + if len(labels) > 0 { + histogram = prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: name, + Help: help, + Buckets: buckets, + }, + labels, + ) + } else { + histogram = prometheus.NewHistogram( + prometheus.HistogramOpts{ + Name: name, + Help: help, + Buckets: buckets, + }, + ) + } + + if err := m.registry.Register(histogram); err != nil { + return err + } + + m.businessMetrics[name] = histogram + m.logger.Info("Registered histogram metric", zap.String("name", name)) + return nil +} + +// GetHandler 获取HTTP处理器 +func (m *PrometheusMetrics) GetHandler() http.Handler { + return promhttp.HandlerFor(m.registry, promhttp.HandlerOpts{}) +} + +// 内部辅助方法 + +func (m *PrometheusMetrics) getOrCreateCounter(name string, labels map[string]string) (prometheus.Collector, bool) { + m.mutex.RLock() + counter, exists := m.businessMetrics[name] + m.mutex.RUnlock() + + if !exists { + // 自动创建计数器 + labelNames := make([]string, 0, len(labels)) + for k := range labels { + labelNames = append(labelNames, k) + } + + if err := m.RegisterCounter(name, "Auto-created counter", labelNames); err != nil { + m.logger.Error("Failed to auto-create counter", zap.String("name", name), zap.Error(err)) + return nil, false + } + + m.mutex.RLock() + counter, exists = m.businessMetrics[name] + m.mutex.RUnlock() + } + + return counter, exists +} + +func (m *PrometheusMetrics) getOrCreateGauge(name string, labels map[string]string) (prometheus.Collector, bool) { + m.mutex.RLock() + gauge, exists := m.businessMetrics[name] + m.mutex.RUnlock() + + if !exists { + labelNames := make([]string, 0, len(labels)) + for k := range labels { + labelNames = append(labelNames, k) + } + + if err := m.RegisterGauge(name, "Auto-created gauge", labelNames); err != nil { + m.logger.Error("Failed to auto-create gauge", zap.String("name", name), zap.Error(err)) + return nil, false + } + + m.mutex.RLock() + gauge, exists = m.businessMetrics[name] + m.mutex.RUnlock() + } + + return gauge, exists +} + +func (m *PrometheusMetrics) getOrCreateHistogram(name string, labels map[string]string) (prometheus.Collector, bool) { + m.mutex.RLock() + histogram, exists := m.businessMetrics[name] + m.mutex.RUnlock() + + if !exists { + labelNames := make([]string, 0, len(labels)) + for k := range labels { + labelNames = append(labelNames, k) + } + + if err := m.RegisterHistogram(name, "Auto-created histogram", labelNames, nil); err != nil { + m.logger.Error("Failed to auto-create histogram", zap.String("name", name), zap.Error(err)) + return nil, false + } + + m.mutex.RLock() + histogram, exists = m.businessMetrics[name] + m.mutex.RUnlock() + } + + return histogram, exists +} + +// UpdateActiveUsers 更新活跃用户数 +func (m *PrometheusMetrics) UpdateActiveUsers(count float64) { + m.activeUsers.Set(count) +} + +// UpdateDBConnections 更新数据库连接数 +func (m *PrometheusMetrics) UpdateDBConnections(count float64) { + m.dbConnections.Set(count) +} + +// RecordCacheOperation 记录缓存操作 +func (m *PrometheusMetrics) RecordCacheOperation(operation, result string) { + m.cacheHits.WithLabelValues(operation, result).Inc() +} + +// GetStats 获取指标统计 +func (m *PrometheusMetrics) GetStats() map[string]interface{} { + m.mutex.RLock() + defer m.mutex.RUnlock() + + return map[string]interface{}{ + "registered_metrics": len(m.businessMetrics), + } +} diff --git a/internal/shared/middleware/auth.go b/internal/shared/middleware/auth.go index b96e283..b5331cc 100644 --- a/internal/shared/middleware/auth.go +++ b/internal/shared/middleware/auth.go @@ -42,31 +42,31 @@ func (m *JWTAuthMiddleware) Handle() gin.HandlerFunc { // 获取Authorization头部 authHeader := c.GetHeader("Authorization") if authHeader == "" { - m.respondUnauthorized(c, "Missing authorization header") + m.respondUnauthorized(c, "缺少认证头部") return } // 检查Bearer前缀 const bearerPrefix = "Bearer " if !strings.HasPrefix(authHeader, bearerPrefix) { - m.respondUnauthorized(c, "Invalid authorization header format") + m.respondUnauthorized(c, "认证头部格式无效") return } // 提取token tokenString := authHeader[len(bearerPrefix):] if tokenString == "" { - m.respondUnauthorized(c, "Missing token") + m.respondUnauthorized(c, "缺少认证令牌") return } // 验证token claims, err := m.validateToken(tokenString) if err != nil { - m.logger.Warn("Invalid token", + m.logger.Warn("无效的认证令牌", zap.Error(err), zap.String("request_id", c.GetString("request_id"))) - m.respondUnauthorized(c, "Invalid token") + m.respondUnauthorized(c, "认证令牌无效") return } @@ -119,7 +119,7 @@ func (m *JWTAuthMiddleware) validateToken(tokenString string) (*JWTClaims, error func (m *JWTAuthMiddleware) respondUnauthorized(c *gin.Context, message string) { c.JSON(http.StatusUnauthorized, gin.H{ "success": false, - "message": "Unauthorized", + "message": "认证失败", "error": message, "request_id": c.GetString("request_id"), "timestamp": time.Now().Unix(), diff --git a/internal/shared/middleware/ratelimit.go b/internal/shared/middleware/ratelimit.go index 433e05b..b544b0d 100644 --- a/internal/shared/middleware/ratelimit.go +++ b/internal/shared/middleware/ratelimit.go @@ -2,11 +2,11 @@ package middleware import ( "fmt" - "net/http" "sync" "time" "tyapi-server/internal/config" + "tyapi-server/internal/shared/interfaces" "github.com/gin-gonic/gin" "golang.org/x/time/rate" @@ -15,14 +15,16 @@ import ( // RateLimitMiddleware 限流中间件 type RateLimitMiddleware struct { config *config.Config + response interfaces.ResponseBuilder limiters map[string]*rate.Limiter mutex sync.RWMutex } // NewRateLimitMiddleware 创建限流中间件 -func NewRateLimitMiddleware(cfg *config.Config) *RateLimitMiddleware { +func NewRateLimitMiddleware(cfg *config.Config, response interfaces.ResponseBuilder) *RateLimitMiddleware { return &RateLimitMiddleware{ config: cfg, + response: response, limiters: make(map[string]*rate.Limiter), } } @@ -48,15 +50,13 @@ func (m *RateLimitMiddleware) Handle() gin.HandlerFunc { // 检查是否允许请求 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") - c.JSON(http.StatusTooManyRequests, gin.H{ - "success": false, - "message": "Rate limit exceeded", - "error": "Too many requests", - }) + // 使用统一的响应格式 + m.response.TooManyRequests(c, "请求过于频繁,请稍后再试") c.Abort() return } diff --git a/internal/shared/middleware/request_logger.go b/internal/shared/middleware/request_logger.go index a8125f1..8d3f3da 100644 --- a/internal/shared/middleware/request_logger.go +++ b/internal/shared/middleware/request_logger.go @@ -2,23 +2,35 @@ package middleware import ( "bytes" + "context" + "fmt" "io" "time" "github.com/gin-gonic/gin" "github.com/google/uuid" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" "go.uber.org/zap" + + "tyapi-server/internal/shared/tracing" ) // RequestLoggerMiddleware 请求日志中间件 type RequestLoggerMiddleware struct { - logger *zap.Logger + logger *zap.Logger + useColoredLog bool + isDevelopment bool + tracer *tracing.Tracer } // NewRequestLoggerMiddleware 创建请求日志中间件 -func NewRequestLoggerMiddleware(logger *zap.Logger) *RequestLoggerMiddleware { +func NewRequestLoggerMiddleware(logger *zap.Logger, isDevelopment bool, tracer *tracing.Tracer) *RequestLoggerMiddleware { return &RequestLoggerMiddleware{ - logger: logger, + logger: logger, + useColoredLog: isDevelopment, // 开发环境使用彩色日志 + isDevelopment: isDevelopment, + tracer: tracer, } } @@ -34,24 +46,110 @@ func (m *RequestLoggerMiddleware) GetPriority() int { // Handle 返回中间件处理函数 func (m *RequestLoggerMiddleware) Handle() gin.HandlerFunc { - return gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string { - // 使用zap logger记录请求信息 - m.logger.Info("HTTP Request", - zap.String("client_ip", param.ClientIP), - zap.String("method", param.Method), - zap.String("path", param.Path), - zap.String("protocol", param.Request.Proto), - zap.Int("status_code", param.StatusCode), - zap.Duration("latency", param.Latency), - zap.String("user_agent", param.Request.UserAgent()), - zap.Int("body_size", param.BodySize), - zap.String("referer", param.Request.Referer()), - zap.String("request_id", param.Request.Header.Get("X-Request-ID")), - ) + if m.useColoredLog { + // 开发环境:使用Gin默认的彩色日志格式 + return gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string { + var statusColor, methodColor, resetColor string + if param.IsOutputColor() { + statusColor = param.StatusCodeColor() + methodColor = param.MethodColor() + resetColor = param.ResetColor() + } - // 返回空字符串,因为我们已经用zap记录了 - return "" - }) + if param.Latency > time.Minute { + param.Latency = param.Latency.Truncate(time.Second) + } + + // 获取TraceID + traceID := param.Request.Header.Get("X-Trace-ID") + if traceID == "" && m.tracer != nil { + traceID = m.tracer.GetTraceID(param.Request.Context()) + } + + // 检查是否为错误响应 + if param.StatusCode >= 400 && m.tracer != nil { + span := trace.SpanFromContext(param.Request.Context()) + if span.IsRecording() { + // 标记为错误操作,确保100%采样 + span.SetAttributes( + attribute.String("error.operation", "true"), + attribute.String("operation.type", "error"), + ) + } + } + + traceInfo := "" + if traceID != "" { + traceInfo = fmt.Sprintf(" | TraceID: %s", traceID) + } + + return fmt.Sprintf("[GIN] %v |%s %3d %s| %13v | %15s |%s %-7s %s %#v%s\n%s", + param.TimeStamp.Format("2006/01/02 - 15:04:05"), + statusColor, param.StatusCode, resetColor, + param.Latency, + param.ClientIP, + methodColor, param.Method, resetColor, + param.Path, + traceInfo, + param.ErrorMessage, + ) + }) + } else { + // 生产环境:使用结构化JSON日志 + return gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string { + // 获取TraceID + traceID := param.Request.Header.Get("X-Trace-ID") + if traceID == "" && m.tracer != nil { + traceID = m.tracer.GetTraceID(param.Request.Context()) + } + + // 检查是否为错误响应 + if param.StatusCode >= 400 && m.tracer != nil { + span := trace.SpanFromContext(param.Request.Context()) + if span.IsRecording() { + // 标记为错误操作,确保100%采样 + span.SetAttributes( + attribute.String("error.operation", "true"), + attribute.String("operation.type", "error"), + ) + + // 对于服务器错误,记录更详细的日志 + if param.StatusCode >= 500 { + m.logger.Error("服务器错误", + zap.Int("status_code", param.StatusCode), + zap.String("method", param.Method), + zap.String("path", param.Path), + zap.Duration("latency", param.Latency), + zap.String("client_ip", param.ClientIP), + zap.String("trace_id", traceID), + ) + } + } + } + + // 记录请求日志 + logFields := []zap.Field{ + zap.String("client_ip", param.ClientIP), + zap.String("method", param.Method), + zap.String("path", param.Path), + zap.String("protocol", param.Request.Proto), + zap.Int("status_code", param.StatusCode), + zap.Duration("latency", param.Latency), + zap.String("user_agent", param.Request.UserAgent()), + zap.Int("body_size", param.BodySize), + zap.String("referer", param.Request.Referer()), + zap.String("request_id", param.Request.Header.Get("X-Request-ID")), + } + + // 添加TraceID + if traceID != "" { + logFields = append(logFields, zap.String("trace_id", traceID)) + } + + m.logger.Info("HTTP请求", logFields...) + return "" + }) + } } // IsGlobal 是否为全局中间件 @@ -102,6 +200,70 @@ func (m *RequestIDMiddleware) IsGlobal() bool { return true } +// TraceIDMiddleware 追踪ID中间件 +type TraceIDMiddleware struct { + tracer *tracing.Tracer +} + +// NewTraceIDMiddleware 创建追踪ID中间件 +func NewTraceIDMiddleware(tracer *tracing.Tracer) *TraceIDMiddleware { + return &TraceIDMiddleware{ + tracer: tracer, + } +} + +// GetName 返回中间件名称 +func (m *TraceIDMiddleware) GetName() string { + return "trace_id" +} + +// GetPriority 返回中间件优先级 +func (m *TraceIDMiddleware) GetPriority() int { + return 94 // 仅次于请求ID中间件 +} + +// Handle 返回中间件处理函数 +func (m *TraceIDMiddleware) Handle() gin.HandlerFunc { + return func(c *gin.Context) { + // 获取或生成追踪ID + traceID := m.tracer.GetTraceID(c.Request.Context()) + if traceID != "" { + // 设置追踪ID到响应头 + c.Header("X-Trace-ID", traceID) + // 添加到上下文 + c.Set("trace_id", traceID) + } + + // 检查是否为错误请求(例如URL不存在) + c.Next() + + // 请求完成后检查状态码 + if c.Writer.Status() >= 400 { + // 获取当前span + span := trace.SpanFromContext(c.Request.Context()) + if span.IsRecording() { + // 标记为错误操作,确保100%采样 + span.SetAttributes( + attribute.String("error.operation", "true"), + attribute.String("operation.type", "error"), + ) + + // 设置错误上下文,以便后续span可以识别 + c.Request = c.Request.WithContext(context.WithValue( + c.Request.Context(), + "otel_error_request", + true, + )) + } + } + } +} + +// IsGlobal 是否为全局中间件 +func (m *TraceIDMiddleware) IsGlobal() bool { + return true +} + // SecurityHeadersMiddleware 安全头部中间件 type SecurityHeadersMiddleware struct{} @@ -183,13 +345,15 @@ func (m *ResponseTimeMiddleware) IsGlobal() bool { type RequestBodyLoggerMiddleware struct { logger *zap.Logger enable bool + tracer *tracing.Tracer } // NewRequestBodyLoggerMiddleware 创建请求体日志中间件 -func NewRequestBodyLoggerMiddleware(logger *zap.Logger, enable bool) *RequestBodyLoggerMiddleware { +func NewRequestBodyLoggerMiddleware(logger *zap.Logger, enable bool, tracer *tracing.Tracer) *RequestBodyLoggerMiddleware { return &RequestBodyLoggerMiddleware{ logger: logger, enable: enable, + tracer: tracer, } } @@ -220,13 +384,26 @@ func (m *RequestBodyLoggerMiddleware) Handle() gin.HandlerFunc { // 重新设置body供后续处理使用 c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) + // 获取追踪ID + traceID := "" + if m.tracer != nil { + traceID = m.tracer.GetTraceID(c.Request.Context()) + } + // 记录请求体(注意:生产环境中应该谨慎记录敏感信息) - m.logger.Debug("Request Body", + logFields := []zap.Field{ zap.String("method", c.Request.Method), zap.String("path", c.Request.URL.Path), zap.String("body", string(bodyBytes)), zap.String("request_id", c.GetString("request_id")), - ) + } + + // 添加追踪ID + if traceID != "" { + logFields = append(logFields, zap.String("trace_id", traceID)) + } + + m.logger.Debug("请求体", logFields...) } } } @@ -239,3 +416,83 @@ func (m *RequestBodyLoggerMiddleware) Handle() gin.HandlerFunc { func (m *RequestBodyLoggerMiddleware) IsGlobal() bool { return false // 可选中间件,不是全局的 } + +// ErrorTrackingMiddleware 错误追踪中间件 +type ErrorTrackingMiddleware struct { + logger *zap.Logger + tracer *tracing.Tracer +} + +// NewErrorTrackingMiddleware 创建错误追踪中间件 +func NewErrorTrackingMiddleware(logger *zap.Logger, tracer *tracing.Tracer) *ErrorTrackingMiddleware { + return &ErrorTrackingMiddleware{ + logger: logger, + tracer: tracer, + } +} + +// GetName 返回中间件名称 +func (m *ErrorTrackingMiddleware) GetName() string { + return "error_tracking" +} + +// GetPriority 返回中间件优先级 +func (m *ErrorTrackingMiddleware) GetPriority() int { + return 60 // 低优先级,在大多数中间件之后执行 +} + +// Handle 返回中间件处理函数 +func (m *ErrorTrackingMiddleware) Handle() gin.HandlerFunc { + return func(c *gin.Context) { + c.Next() + + // 检查是否有错误 + if len(c.Errors) > 0 || c.Writer.Status() >= 400 { + // 获取当前span + span := trace.SpanFromContext(c.Request.Context()) + if span.IsRecording() { + // 标记为错误操作,确保100%采样 + span.SetAttributes( + attribute.String("error.operation", "true"), + attribute.String("operation.type", "error"), + ) + + // 记录错误日志 + traceID := m.tracer.GetTraceID(c.Request.Context()) + spanID := m.tracer.GetSpanID(c.Request.Context()) + + logFields := []zap.Field{ + zap.Int("status_code", c.Writer.Status()), + zap.String("method", c.Request.Method), + zap.String("path", c.FullPath()), + zap.String("client_ip", c.ClientIP()), + } + + // 添加追踪信息 + if traceID != "" { + logFields = append(logFields, zap.String("trace_id", traceID)) + } + if spanID != "" { + logFields = append(logFields, zap.String("span_id", spanID)) + } + + // 添加错误信息 + if len(c.Errors) > 0 { + logFields = append(logFields, zap.String("errors", c.Errors.String())) + } + + // 根据状态码决定日志级别 + if c.Writer.Status() >= 500 { + m.logger.Error("服务器错误", logFields...) + } else { + m.logger.Warn("客户端错误", logFields...) + } + } + } + } +} + +// IsGlobal 是否为全局中间件 +func (m *ErrorTrackingMiddleware) IsGlobal() bool { + return true +} diff --git a/internal/shared/resilience/circuit_breaker.go b/internal/shared/resilience/circuit_breaker.go new file mode 100644 index 0000000..05533c3 --- /dev/null +++ b/internal/shared/resilience/circuit_breaker.go @@ -0,0 +1,389 @@ +package resilience + +import ( + "context" + "errors" + "sync" + "time" + + "go.uber.org/zap" +) + +// CircuitState 熔断器状态 +type CircuitState int + +const ( + // StateClosed 关闭状态(正常) + StateClosed CircuitState = iota + // StateOpen 开启状态(熔断) + StateOpen + // StateHalfOpen 半开状态(测试) + StateHalfOpen +) + +func (s CircuitState) String() string { + switch s { + case StateClosed: + return "CLOSED" + case StateOpen: + return "OPEN" + case StateHalfOpen: + return "HALF_OPEN" + default: + return "UNKNOWN" + } +} + +// CircuitBreakerConfig 熔断器配置 +type CircuitBreakerConfig struct { + // 故障阈值 + FailureThreshold int + // 重置超时时间 + ResetTimeout time.Duration + // 检测窗口大小 + WindowSize int + // 半开状态允许的请求数 + HalfOpenMaxRequests int + // 成功阈值(半开->关闭) + SuccessThreshold int +} + +// DefaultCircuitBreakerConfig 默认熔断器配置 +func DefaultCircuitBreakerConfig() CircuitBreakerConfig { + return CircuitBreakerConfig{ + FailureThreshold: 5, + ResetTimeout: 60 * time.Second, + WindowSize: 10, + HalfOpenMaxRequests: 3, + SuccessThreshold: 2, + } +} + +// CircuitBreaker 熔断器 +type CircuitBreaker struct { + config CircuitBreakerConfig + logger *zap.Logger + mutex sync.RWMutex + + // 状态 + state CircuitState + + // 计数器 + failures int + successes int + requests int + consecutiveFailures int + + // 时间记录 + lastFailTime time.Time + lastStateChange time.Time + + // 统计窗口 + window []bool // true=success, false=failure + windowIndex int + windowFull bool + + // 事件回调 + onStateChange func(from, to CircuitState) +} + +// NewCircuitBreaker 创建熔断器 +func NewCircuitBreaker(config CircuitBreakerConfig, logger *zap.Logger) *CircuitBreaker { + cb := &CircuitBreaker{ + config: config, + logger: logger, + state: StateClosed, + window: make([]bool, config.WindowSize), + lastStateChange: time.Now(), + } + + return cb +} + +// Execute 执行函数,如果熔断器开启则快速失败 +func (cb *CircuitBreaker) Execute(ctx context.Context, fn func() error) error { + // 检查是否允许执行 + if !cb.allowRequest() { + return ErrCircuitBreakerOpen + } + + // 执行函数 + start := time.Now() + err := fn() + duration := time.Since(start) + + // 记录结果 + cb.recordResult(err == nil, duration) + + return err +} + +// allowRequest 检查是否允许请求 +func (cb *CircuitBreaker) allowRequest() bool { + cb.mutex.Lock() + defer cb.mutex.Unlock() + + now := time.Now() + + switch cb.state { + case StateClosed: + return true + + case StateOpen: + // 检查是否到了重置时间 + if now.Sub(cb.lastStateChange) > cb.config.ResetTimeout { + cb.setState(StateHalfOpen) + return true + } + return false + + case StateHalfOpen: + // 半开状态下限制请求数 + return cb.requests < cb.config.HalfOpenMaxRequests + + default: + return false + } +} + +// recordResult 记录执行结果 +func (cb *CircuitBreaker) recordResult(success bool, duration time.Duration) { + cb.mutex.Lock() + defer cb.mutex.Unlock() + + cb.requests++ + + // 更新滑动窗口 + cb.updateWindow(success) + + if success { + cb.successes++ + cb.consecutiveFailures = 0 + cb.onSuccess() + } else { + cb.failures++ + cb.consecutiveFailures++ + cb.lastFailTime = time.Now() + cb.onFailure() + } + + cb.logger.Debug("Circuit breaker recorded result", + zap.Bool("success", success), + zap.Duration("duration", duration), + zap.String("state", cb.state.String()), + zap.Int("failures", cb.failures), + zap.Int("successes", cb.successes)) +} + +// updateWindow 更新滑动窗口 +func (cb *CircuitBreaker) updateWindow(success bool) { + cb.window[cb.windowIndex] = success + cb.windowIndex = (cb.windowIndex + 1) % cb.config.WindowSize + + if cb.windowIndex == 0 { + cb.windowFull = true + } +} + +// onSuccess 成功时的处理 +func (cb *CircuitBreaker) onSuccess() { + if cb.state == StateHalfOpen { + // 半开状态下,如果成功次数达到阈值,则关闭熔断器 + if cb.successes >= cb.config.SuccessThreshold { + cb.setState(StateClosed) + } + } +} + +// onFailure 失败时的处理 +func (cb *CircuitBreaker) onFailure() { + if cb.state == StateClosed { + // 关闭状态下,检查是否需要开启熔断器 + if cb.shouldTrip() { + cb.setState(StateOpen) + } + } else if cb.state == StateHalfOpen { + // 半开状态下,如果失败则立即开启熔断器 + cb.setState(StateOpen) + } +} + +// shouldTrip 检查是否应该触发熔断 +func (cb *CircuitBreaker) shouldTrip() bool { + // 基于连续失败次数 + if cb.consecutiveFailures >= cb.config.FailureThreshold { + return true + } + + // 基于滑动窗口的失败率 + if cb.windowFull { + failures := 0 + for _, success := range cb.window { + if !success { + failures++ + } + } + + failureRate := float64(failures) / float64(cb.config.WindowSize) + return failureRate >= 0.5 // 50%失败率 + } + + return false +} + +// setState 设置状态 +func (cb *CircuitBreaker) setState(newState CircuitState) { + if cb.state == newState { + return + } + + oldState := cb.state + cb.state = newState + cb.lastStateChange = time.Now() + + // 重置计数器 + if newState == StateClosed { + cb.requests = 0 + cb.failures = 0 + cb.successes = 0 + cb.consecutiveFailures = 0 + } else if newState == StateHalfOpen { + cb.requests = 0 + cb.successes = 0 + } + + cb.logger.Info("Circuit breaker state changed", + zap.String("from", oldState.String()), + zap.String("to", newState.String()), + zap.Int("failures", cb.failures), + zap.Int("successes", cb.successes)) + + // 触发状态变更回调 + if cb.onStateChange != nil { + cb.onStateChange(oldState, newState) + } +} + +// GetState 获取当前状态 +func (cb *CircuitBreaker) GetState() CircuitState { + cb.mutex.RLock() + defer cb.mutex.RUnlock() + return cb.state +} + +// GetStats 获取统计信息 +func (cb *CircuitBreaker) GetStats() CircuitBreakerStats { + cb.mutex.RLock() + defer cb.mutex.RUnlock() + + return CircuitBreakerStats{ + State: cb.state.String(), + Failures: cb.failures, + Successes: cb.successes, + Requests: cb.requests, + ConsecutiveFailures: cb.consecutiveFailures, + LastFailTime: cb.lastFailTime, + LastStateChange: cb.lastStateChange, + FailureThreshold: cb.config.FailureThreshold, + ResetTimeout: cb.config.ResetTimeout, + } +} + +// Reset 重置熔断器 +func (cb *CircuitBreaker) Reset() { + cb.mutex.Lock() + defer cb.mutex.Unlock() + + cb.setState(StateClosed) + cb.window = make([]bool, cb.config.WindowSize) + cb.windowIndex = 0 + cb.windowFull = false + + cb.logger.Info("Circuit breaker reset") +} + +// SetStateChangeCallback 设置状态变更回调 +func (cb *CircuitBreaker) SetStateChangeCallback(callback func(from, to CircuitState)) { + cb.mutex.Lock() + defer cb.mutex.Unlock() + cb.onStateChange = callback +} + +// CircuitBreakerStats 熔断器统计信息 +type CircuitBreakerStats struct { + State string `json:"state"` + Failures int `json:"failures"` + Successes int `json:"successes"` + Requests int `json:"requests"` + ConsecutiveFailures int `json:"consecutive_failures"` + LastFailTime time.Time `json:"last_fail_time"` + LastStateChange time.Time `json:"last_state_change"` + FailureThreshold int `json:"failure_threshold"` + ResetTimeout time.Duration `json:"reset_timeout"` +} + +// 预定义错误 +var ( + ErrCircuitBreakerOpen = errors.New("circuit breaker is open") +) + +// Wrapper 熔断器包装器 +type Wrapper struct { + breakers map[string]*CircuitBreaker + logger *zap.Logger + mutex sync.RWMutex +} + +// NewWrapper 创建熔断器包装器 +func NewWrapper(logger *zap.Logger) *Wrapper { + return &Wrapper{ + breakers: make(map[string]*CircuitBreaker), + logger: logger, + } +} + +// GetOrCreate 获取或创建熔断器 +func (w *Wrapper) GetOrCreate(name string, config CircuitBreakerConfig) *CircuitBreaker { + w.mutex.Lock() + defer w.mutex.Unlock() + + if cb, exists := w.breakers[name]; exists { + return cb + } + + cb := NewCircuitBreaker(config, w.logger.Named(name)) + w.breakers[name] = cb + + w.logger.Info("Created circuit breaker", zap.String("name", name)) + return cb +} + +// Execute 执行带熔断器的函数 +func (w *Wrapper) Execute(ctx context.Context, name string, fn func() error) error { + cb := w.GetOrCreate(name, DefaultCircuitBreakerConfig()) + return cb.Execute(ctx, fn) +} + +// GetStats 获取所有熔断器统计 +func (w *Wrapper) GetStats() map[string]CircuitBreakerStats { + w.mutex.RLock() + defer w.mutex.RUnlock() + + stats := make(map[string]CircuitBreakerStats) + for name, cb := range w.breakers { + stats[name] = cb.GetStats() + } + + return stats +} + +// ResetAll 重置所有熔断器 +func (w *Wrapper) ResetAll() { + w.mutex.RLock() + defer w.mutex.RUnlock() + + for name, cb := range w.breakers { + cb.Reset() + w.logger.Info("Reset circuit breaker", zap.String("name", name)) + } +} diff --git a/internal/shared/resilience/retry.go b/internal/shared/resilience/retry.go new file mode 100644 index 0000000..d78a8fd --- /dev/null +++ b/internal/shared/resilience/retry.go @@ -0,0 +1,467 @@ +package resilience + +import ( + "context" + "fmt" + "math/rand" + "sync" + "time" + + "go.uber.org/zap" +) + +// RetryConfig 重试配置 +type RetryConfig struct { + // 最大重试次数 + MaxAttempts int + // 初始延迟 + InitialDelay time.Duration + // 最大延迟 + MaxDelay time.Duration + // 退避倍数 + BackoffMultiplier float64 + // 抖动系数 + JitterFactor float64 + // 重试条件 + RetryCondition func(error) bool + // 延迟函数 + DelayFunc func(attempt int, config RetryConfig) time.Duration +} + +// DefaultRetryConfig 默认重试配置 +func DefaultRetryConfig() RetryConfig { + return RetryConfig{ + MaxAttempts: 3, + InitialDelay: 100 * time.Millisecond, + MaxDelay: 5 * time.Second, + BackoffMultiplier: 2.0, + JitterFactor: 0.1, + RetryCondition: DefaultRetryCondition, + DelayFunc: ExponentialBackoffWithJitter, + } +} + +// RetryableError 可重试错误接口 +type RetryableError interface { + error + IsRetryable() bool +} + +// DefaultRetryCondition 默认重试条件 +func DefaultRetryCondition(err error) bool { + if err == nil { + return false + } + + // 检查是否实现了RetryableError接口 + if retryable, ok := err.(RetryableError); ok { + return retryable.IsRetryable() + } + + // 默认所有错误都重试 + return true +} + +// IsRetryableHTTPError HTTP错误重试条件 +func IsRetryableHTTPError(statusCode int) bool { + // 5xx错误通常可以重试 + // 429(Too Many Requests)也可以重试 + return statusCode >= 500 || statusCode == 429 +} + +// DelayFunction 延迟函数类型 +type DelayFunction func(attempt int, config RetryConfig) time.Duration + +// FixedDelay 固定延迟 +func FixedDelay(attempt int, config RetryConfig) time.Duration { + return config.InitialDelay +} + +// LinearBackoff 线性退避 +func LinearBackoff(attempt int, config RetryConfig) time.Duration { + delay := time.Duration(attempt) * config.InitialDelay + if delay > config.MaxDelay { + delay = config.MaxDelay + } + return delay +} + +// ExponentialBackoff 指数退避 +func ExponentialBackoff(attempt int, config RetryConfig) time.Duration { + delay := config.InitialDelay + for i := 0; i < attempt; i++ { + delay = time.Duration(float64(delay) * config.BackoffMultiplier) + } + + if delay > config.MaxDelay { + delay = config.MaxDelay + } + + return delay +} + +// ExponentialBackoffWithJitter 带抖动的指数退避 +func ExponentialBackoffWithJitter(attempt int, config RetryConfig) time.Duration { + delay := ExponentialBackoff(attempt, config) + + // 添加抖动 + jitter := config.JitterFactor + if jitter > 0 { + jitterRange := float64(delay) * jitter + jitterOffset := (rand.Float64() - 0.5) * 2 * jitterRange + delay = time.Duration(float64(delay) + jitterOffset) + } + + if delay < 0 { + delay = config.InitialDelay + } + + return delay +} + +// RetryStats 重试统计 +type RetryStats struct { + TotalAttempts int `json:"total_attempts"` + Successes int `json:"successes"` + Failures int `json:"failures"` + TotalRetries int `json:"total_retries"` + AverageAttempts float64 `json:"average_attempts"` + TotalDelay time.Duration `json:"total_delay"` + LastError string `json:"last_error,omitempty"` +} + +// Retryer 重试器 +type Retryer struct { + config RetryConfig + logger *zap.Logger + stats RetryStats +} + +// NewRetryer 创建重试器 +func NewRetryer(config RetryConfig, logger *zap.Logger) *Retryer { + if config.DelayFunc == nil { + config.DelayFunc = ExponentialBackoffWithJitter + } + if config.RetryCondition == nil { + config.RetryCondition = DefaultRetryCondition + } + + return &Retryer{ + config: config, + logger: logger, + } +} + +// Execute 执行带重试的函数 +func (r *Retryer) Execute(ctx context.Context, operation func() error) error { + return r.ExecuteWithResult(ctx, func() (interface{}, error) { + return nil, operation() + }) +} + +// ExecuteWithResult 执行带重试和返回值的函数 +func (r *Retryer) ExecuteWithResult(ctx context.Context, operation func() (interface{}, error)) error { + var lastErr error + startTime := time.Now() + + for attempt := 0; attempt < r.config.MaxAttempts; attempt++ { + // 检查上下文是否被取消 + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + + // 执行操作 + attemptStart := time.Now() + _, err := operation() + attemptDuration := time.Since(attemptStart) + + // 更新统计 + r.stats.TotalAttempts++ + if err == nil { + r.stats.Successes++ + r.logger.Debug("Operation succeeded", + zap.Int("attempt", attempt+1), + zap.Duration("duration", attemptDuration)) + return nil + } + + lastErr = err + r.stats.Failures++ + if attempt > 0 { + r.stats.TotalRetries++ + } + + // 检查是否应该重试 + if !r.config.RetryCondition(err) { + r.logger.Debug("Error is not retryable", + zap.Error(err), + zap.Int("attempt", attempt+1)) + break + } + + // 如果这是最后一次尝试,不需要延迟 + if attempt == r.config.MaxAttempts-1 { + r.logger.Debug("Reached max attempts", + zap.Error(err), + zap.Int("max_attempts", r.config.MaxAttempts)) + break + } + + // 计算延迟 + delay := r.config.DelayFunc(attempt, r.config) + r.stats.TotalDelay += delay + + r.logger.Debug("Operation failed, retrying", + zap.Error(err), + zap.Int("attempt", attempt+1), + zap.Duration("delay", delay), + zap.Duration("attempt_duration", attemptDuration)) + + // 等待重试 + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(delay): + } + } + + // 更新最终统计 + totalDuration := time.Since(startTime) + if r.stats.TotalAttempts > 0 { + r.stats.AverageAttempts = float64(r.stats.TotalRetries) / float64(r.stats.Successes+r.stats.Failures) + } + if lastErr != nil { + r.stats.LastError = lastErr.Error() + } + + r.logger.Warn("Operation failed after all retries", + zap.Error(lastErr), + zap.Int("total_attempts", r.stats.TotalAttempts), + zap.Duration("total_duration", totalDuration)) + + return fmt.Errorf("operation failed after %d attempts: %w", r.config.MaxAttempts, lastErr) +} + +// GetStats 获取重试统计 +func (r *Retryer) GetStats() RetryStats { + return r.stats +} + +// Reset 重置统计 +func (r *Retryer) Reset() { + r.stats = RetryStats{} + r.logger.Debug("Retry stats reset") +} + +// Retry 简单重试函数 +func Retry(ctx context.Context, config RetryConfig, operation func() error) error { + retryer := NewRetryer(config, zap.NewNop()) + return retryer.Execute(ctx, operation) +} + +// RetryWithResult 带返回值的重试函数 +func RetryWithResult[T any](ctx context.Context, config RetryConfig, operation func() (T, error)) (T, error) { + var result T + var finalErr error + + retryer := NewRetryer(config, zap.NewNop()) + err := retryer.ExecuteWithResult(ctx, func() (interface{}, error) { + r, e := operation() + result = r + return r, e + }) + + if err != nil { + finalErr = err + } + + return result, finalErr +} + +// 预定义的重试配置 + +// QuickRetry 快速重试(适用于轻量级操作) +func QuickRetry() RetryConfig { + return RetryConfig{ + MaxAttempts: 3, + InitialDelay: 50 * time.Millisecond, + MaxDelay: 500 * time.Millisecond, + BackoffMultiplier: 2.0, + JitterFactor: 0.1, + RetryCondition: DefaultRetryCondition, + DelayFunc: ExponentialBackoffWithJitter, + } +} + +// StandardRetry 标准重试(适用于一般操作) +func StandardRetry() RetryConfig { + return DefaultRetryConfig() +} + +// PatientRetry 耐心重试(适用于重要操作) +func PatientRetry() RetryConfig { + return RetryConfig{ + MaxAttempts: 5, + InitialDelay: 200 * time.Millisecond, + MaxDelay: 10 * time.Second, + BackoffMultiplier: 2.0, + JitterFactor: 0.2, + RetryCondition: DefaultRetryCondition, + DelayFunc: ExponentialBackoffWithJitter, + } +} + +// DatabaseRetry 数据库重试配置 +func DatabaseRetry() RetryConfig { + return RetryConfig{ + MaxAttempts: 3, + InitialDelay: 100 * time.Millisecond, + MaxDelay: 2 * time.Second, + BackoffMultiplier: 1.5, + JitterFactor: 0.1, + RetryCondition: func(err error) bool { + // 这里可以根据具体的数据库错误类型判断 + // 例如:连接超时、临时网络错误等 + return DefaultRetryCondition(err) + }, + DelayFunc: ExponentialBackoffWithJitter, + } +} + +// HTTPRetry HTTP重试配置 +func HTTPRetry() RetryConfig { + return RetryConfig{ + MaxAttempts: 3, + InitialDelay: 200 * time.Millisecond, + MaxDelay: 5 * time.Second, + BackoffMultiplier: 2.0, + JitterFactor: 0.15, + RetryCondition: func(err error) bool { + // HTTP相关的重试条件 + return DefaultRetryCondition(err) + }, + DelayFunc: ExponentialBackoffWithJitter, + } +} + +// RetryManager 重试管理器 +type RetryManager struct { + retryers map[string]*Retryer + logger *zap.Logger + mutex sync.RWMutex +} + +// NewRetryManager 创建重试管理器 +func NewRetryManager(logger *zap.Logger) *RetryManager { + return &RetryManager{ + retryers: make(map[string]*Retryer), + logger: logger, + } +} + +// GetOrCreate 获取或创建重试器 +func (rm *RetryManager) GetOrCreate(name string, config RetryConfig) *Retryer { + rm.mutex.Lock() + defer rm.mutex.Unlock() + + if retryer, exists := rm.retryers[name]; exists { + return retryer + } + + retryer := NewRetryer(config, rm.logger.Named(name)) + rm.retryers[name] = retryer + + rm.logger.Info("Created retryer", zap.String("name", name)) + return retryer +} + +// Execute 执行带重试的操作 +func (rm *RetryManager) Execute(ctx context.Context, name string, operation func() error) error { + retryer := rm.GetOrCreate(name, DefaultRetryConfig()) + return retryer.Execute(ctx, operation) +} + +// GetStats 获取所有重试器统计 +func (rm *RetryManager) GetStats() map[string]RetryStats { + rm.mutex.RLock() + defer rm.mutex.RUnlock() + + stats := make(map[string]RetryStats) + for name, retryer := range rm.retryers { + stats[name] = retryer.GetStats() + } + + return stats +} + +// ResetAll 重置所有重试器统计 +func (rm *RetryManager) ResetAll() { + rm.mutex.RLock() + defer rm.mutex.RUnlock() + + for name, retryer := range rm.retryers { + retryer.Reset() + rm.logger.Info("Reset retryer stats", zap.String("name", name)) + } +} + +// RetryerWrapper 重试器包装器 +type RetryerWrapper struct { + manager *RetryManager + logger *zap.Logger +} + +// NewRetryerWrapper 创建重试器包装器 +func NewRetryerWrapper(logger *zap.Logger) *RetryerWrapper { + return &RetryerWrapper{ + manager: NewRetryManager(logger), + logger: logger, + } +} + +// ExecuteWithQuickRetry 执行快速重试 +func (rw *RetryerWrapper) ExecuteWithQuickRetry(ctx context.Context, name string, operation func() error) error { + retryer := rw.manager.GetOrCreate(name+".quick", QuickRetry()) + return retryer.Execute(ctx, operation) +} + +// ExecuteWithStandardRetry 执行标准重试 +func (rw *RetryerWrapper) ExecuteWithStandardRetry(ctx context.Context, name string, operation func() error) error { + retryer := rw.manager.GetOrCreate(name+".standard", StandardRetry()) + return retryer.Execute(ctx, operation) +} + +// ExecuteWithDatabaseRetry 执行数据库重试 +func (rw *RetryerWrapper) ExecuteWithDatabaseRetry(ctx context.Context, name string, operation func() error) error { + retryer := rw.manager.GetOrCreate(name+".database", DatabaseRetry()) + return retryer.Execute(ctx, operation) +} + +// ExecuteWithHTTPRetry 执行HTTP重试 +func (rw *RetryerWrapper) ExecuteWithHTTPRetry(ctx context.Context, name string, operation func() error) error { + retryer := rw.manager.GetOrCreate(name+".http", HTTPRetry()) + return retryer.Execute(ctx, operation) +} + +// ExecuteWithCustomRetry 执行自定义重试 +func (rw *RetryerWrapper) ExecuteWithCustomRetry(ctx context.Context, name string, config RetryConfig, operation func() error) error { + retryer := rw.manager.GetOrCreate(name+".custom", config) + return retryer.Execute(ctx, operation) +} + +// GetManager 获取重试管理器 +func (rw *RetryerWrapper) GetManager() *RetryManager { + return rw.manager +} + +// GetAllStats 获取所有统计信息 +func (rw *RetryerWrapper) GetAllStats() map[string]RetryStats { + return rw.manager.GetStats() +} + +// ResetAllStats 重置所有统计信息 +func (rw *RetryerWrapper) ResetAllStats() { + rw.manager.ResetAll() +} diff --git a/internal/shared/saga/saga.go b/internal/shared/saga/saga.go new file mode 100644 index 0000000..a451f4a --- /dev/null +++ b/internal/shared/saga/saga.go @@ -0,0 +1,612 @@ +package saga + +import ( + "context" + "fmt" + "sync" + "time" + + "go.uber.org/zap" + + "tyapi-server/internal/shared/interfaces" +) + +// SagaStatus Saga状态 +type SagaStatus int + +const ( + // StatusPending 等待中 + StatusPending SagaStatus = iota + // StatusRunning 执行中 + StatusRunning + // StatusCompleted 已完成 + StatusCompleted + // StatusFailed 失败 + StatusFailed + // StatusCompensating 补偿中 + StatusCompensating + // StatusCompensated 已补偿 + StatusCompensated + // StatusAborted 已中止 + StatusAborted +) + +func (s SagaStatus) String() string { + switch s { + case StatusPending: + return "PENDING" + case StatusRunning: + return "RUNNING" + case StatusCompleted: + return "COMPLETED" + case StatusFailed: + return "FAILED" + case StatusCompensating: + return "COMPENSATING" + case StatusCompensated: + return "COMPENSATED" + case StatusAborted: + return "ABORTED" + default: + return "UNKNOWN" + } +} + +// StepStatus 步骤状态 +type StepStatus int + +const ( + // StepPending 等待执行 + StepPending StepStatus = iota + // StepRunning 执行中 + StepRunning + // StepCompleted 完成 + StepCompleted + // StepFailed 失败 + StepFailed + // StepCompensated 已补偿 + StepCompensated + // StepSkipped 跳过 + StepSkipped +) + +func (s StepStatus) String() string { + switch s { + case StepPending: + return "PENDING" + case StepRunning: + return "RUNNING" + case StepCompleted: + return "COMPLETED" + case StepFailed: + return "FAILED" + case StepCompensated: + return "COMPENSATED" + case StepSkipped: + return "SKIPPED" + default: + return "UNKNOWN" + } +} + +// SagaStep Saga步骤 +type SagaStep struct { + Name string + Action func(ctx context.Context, data interface{}) error + Compensate func(ctx context.Context, data interface{}) error + Status StepStatus + Error error + StartTime time.Time + EndTime time.Time + RetryCount int + MaxRetries int + Timeout time.Duration +} + +// SagaConfig Saga配置 +type SagaConfig struct { + // 默认超时时间 + DefaultTimeout time.Duration + // 默认重试次数 + DefaultMaxRetries int + // 是否并行执行(当前只支持串行) + Parallel bool + // 事件发布器 + EventBus interfaces.EventBus +} + +// DefaultSagaConfig 默认Saga配置 +func DefaultSagaConfig() SagaConfig { + return SagaConfig{ + DefaultTimeout: 30 * time.Second, + DefaultMaxRetries: 3, + Parallel: false, + } +} + +// Saga 分布式事务 +type Saga struct { + ID string + Name string + Steps []*SagaStep + Status SagaStatus + Data interface{} + StartTime time.Time + EndTime time.Time + Error error + Config SagaConfig + logger *zap.Logger + mutex sync.RWMutex + currentStep int + result interface{} +} + +// NewSaga 创建新的Saga +func NewSaga(id, name string, config SagaConfig, logger *zap.Logger) *Saga { + return &Saga{ + ID: id, + Name: name, + Steps: make([]*SagaStep, 0), + Status: StatusPending, + Config: config, + logger: logger, + currentStep: -1, + } +} + +// AddStep 添加步骤 +func (s *Saga) AddStep(name string, action, compensate func(ctx context.Context, data interface{}) error) *Saga { + step := &SagaStep{ + Name: name, + Action: action, + Compensate: compensate, + Status: StepPending, + MaxRetries: s.Config.DefaultMaxRetries, + Timeout: s.Config.DefaultTimeout, + } + + s.mutex.Lock() + s.Steps = append(s.Steps, step) + s.mutex.Unlock() + + s.logger.Debug("Added step to saga", + zap.String("saga_id", s.ID), + zap.String("step_name", name)) + + return s +} + +// AddStepWithConfig 添加带配置的步骤 +func (s *Saga) AddStepWithConfig(name string, action, compensate func(ctx context.Context, data interface{}) error, maxRetries int, timeout time.Duration) *Saga { + step := &SagaStep{ + Name: name, + Action: action, + Compensate: compensate, + Status: StepPending, + MaxRetries: maxRetries, + Timeout: timeout, + } + + s.mutex.Lock() + s.Steps = append(s.Steps, step) + s.mutex.Unlock() + + s.logger.Debug("Added step with config to saga", + zap.String("saga_id", s.ID), + zap.String("step_name", name), + zap.Int("max_retries", maxRetries), + zap.Duration("timeout", timeout)) + + return s +} + +// Execute 执行Saga +func (s *Saga) Execute(ctx context.Context, data interface{}) error { + s.mutex.Lock() + if s.Status != StatusPending { + s.mutex.Unlock() + return fmt.Errorf("saga %s is not in pending status", s.ID) + } + + s.Status = StatusRunning + s.Data = data + s.StartTime = time.Now() + s.mutex.Unlock() + + s.logger.Info("Starting saga execution", + zap.String("saga_id", s.ID), + zap.String("saga_name", s.Name), + zap.Int("total_steps", len(s.Steps))) + + // 发布Saga开始事件 + s.publishEvent(ctx, "saga.started") + + // 执行所有步骤 + for i, step := range s.Steps { + s.mutex.Lock() + s.currentStep = i + s.mutex.Unlock() + + if err := s.executeStep(ctx, step, data); err != nil { + s.logger.Error("Step execution failed", + zap.String("saga_id", s.ID), + zap.String("step_name", step.Name), + zap.Error(err)) + + // 执行补偿 + if compensateErr := s.compensate(ctx, i-1); compensateErr != nil { + s.logger.Error("Compensation failed", + zap.String("saga_id", s.ID), + zap.Error(compensateErr)) + + s.setStatus(StatusAborted) + s.publishEvent(ctx, "saga.aborted") + return fmt.Errorf("saga execution failed and compensation failed: %w", compensateErr) + } + + s.setStatus(StatusCompensated) + s.publishEvent(ctx, "saga.compensated") + return fmt.Errorf("saga execution failed: %w", err) + } + } + + // 所有步骤成功完成 + s.setStatus(StatusCompleted) + s.EndTime = time.Now() + + s.logger.Info("Saga completed successfully", + zap.String("saga_id", s.ID), + zap.Duration("duration", s.EndTime.Sub(s.StartTime))) + + s.publishEvent(ctx, "saga.completed") + return nil +} + +// executeStep 执行单个步骤 +func (s *Saga) executeStep(ctx context.Context, step *SagaStep, data interface{}) error { + step.Status = StepRunning + step.StartTime = time.Now() + + s.logger.Debug("Executing step", + zap.String("saga_id", s.ID), + zap.String("step_name", step.Name)) + + // 设置超时上下文 + stepCtx, cancel := context.WithTimeout(ctx, step.Timeout) + defer cancel() + + // 重试逻辑 + var lastErr error + for attempt := 0; attempt <= step.MaxRetries; attempt++ { + if attempt > 0 { + s.logger.Debug("Retrying step", + zap.String("saga_id", s.ID), + zap.String("step_name", step.Name), + zap.Int("attempt", attempt)) + } + + err := step.Action(stepCtx, data) + if err == nil { + step.Status = StepCompleted + step.EndTime = time.Now() + + s.logger.Debug("Step completed successfully", + zap.String("saga_id", s.ID), + zap.String("step_name", step.Name), + zap.Duration("duration", step.EndTime.Sub(step.StartTime))) + + return nil + } + + lastErr = err + step.RetryCount = attempt + + // 检查是否应该重试 + if attempt < step.MaxRetries { + select { + case <-stepCtx.Done(): + // 上下文被取消,停止重试 + break + case <-time.After(time.Duration(attempt+1) * 100 * time.Millisecond): + // 等待一段时间后重试 + } + } + } + + // 所有重试都失败了 + step.Status = StepFailed + step.Error = lastErr + step.EndTime = time.Now() + + return lastErr +} + +// compensate 执行补偿 +func (s *Saga) compensate(ctx context.Context, fromStep int) error { + s.setStatus(StatusCompensating) + + s.logger.Info("Starting compensation", + zap.String("saga_id", s.ID), + zap.Int("from_step", fromStep)) + + // 逆序执行补偿 + for i := fromStep; i >= 0; i-- { + step := s.Steps[i] + + // 只补偿已完成的步骤 + if step.Status != StepCompleted { + step.Status = StepSkipped + continue + } + + if step.Compensate == nil { + s.logger.Warn("No compensation function for step", + zap.String("saga_id", s.ID), + zap.String("step_name", step.Name)) + continue + } + + s.logger.Debug("Compensating step", + zap.String("saga_id", s.ID), + zap.String("step_name", step.Name)) + + // 设置超时上下文 + compensateCtx, cancel := context.WithTimeout(ctx, step.Timeout) + + err := step.Compensate(compensateCtx, s.Data) + cancel() + + if err != nil { + s.logger.Error("Compensation failed for step", + zap.String("saga_id", s.ID), + zap.String("step_name", step.Name), + zap.Error(err)) + return err + } + + step.Status = StepCompensated + + s.logger.Debug("Step compensated successfully", + zap.String("saga_id", s.ID), + zap.String("step_name", step.Name)) + } + + s.logger.Info("Compensation completed", + zap.String("saga_id", s.ID)) + + return nil +} + +// setStatus 设置状态 +func (s *Saga) setStatus(status SagaStatus) { + s.mutex.Lock() + defer s.mutex.Unlock() + s.Status = status +} + +// GetStatus 获取状态 +func (s *Saga) GetStatus() SagaStatus { + s.mutex.RLock() + defer s.mutex.RUnlock() + return s.Status +} + +// GetProgress 获取进度 +func (s *Saga) GetProgress() SagaProgress { + s.mutex.RLock() + defer s.mutex.RUnlock() + + completed := 0 + for _, step := range s.Steps { + if step.Status == StepCompleted { + completed++ + } + } + + var percentage float64 + if len(s.Steps) > 0 { + percentage = float64(completed) / float64(len(s.Steps)) * 100 + } + + return SagaProgress{ + SagaID: s.ID, + Status: s.Status.String(), + TotalSteps: len(s.Steps), + CompletedSteps: completed, + CurrentStep: s.currentStep + 1, + PercentComplete: percentage, + StartTime: s.StartTime, + Duration: time.Since(s.StartTime), + } +} + +// GetStepStatus 获取所有步骤状态 +func (s *Saga) GetStepStatus() []StepProgress { + s.mutex.RLock() + defer s.mutex.RUnlock() + + progress := make([]StepProgress, len(s.Steps)) + for i, step := range s.Steps { + progress[i] = StepProgress{ + Name: step.Name, + Status: step.Status.String(), + RetryCount: step.RetryCount, + StartTime: step.StartTime, + EndTime: step.EndTime, + Duration: step.EndTime.Sub(step.StartTime), + Error: "", + } + + if step.Error != nil { + progress[i].Error = step.Error.Error() + } + } + + return progress +} + +// publishEvent 发布事件 +func (s *Saga) publishEvent(ctx context.Context, eventType string) { + if s.Config.EventBus == nil { + return + } + + event := &SagaEvent{ + SagaID: s.ID, + SagaName: s.Name, + EventType: eventType, + Status: s.Status.String(), + Timestamp: time.Now(), + Data: s.Data, + } + + // 这里应该实现Event接口,简化处理 + _ = event +} + +// SagaProgress Saga进度 +type SagaProgress struct { + SagaID string `json:"saga_id"` + Status string `json:"status"` + TotalSteps int `json:"total_steps"` + CompletedSteps int `json:"completed_steps"` + CurrentStep int `json:"current_step"` + PercentComplete float64 `json:"percent_complete"` + StartTime time.Time `json:"start_time"` + Duration time.Duration `json:"duration"` +} + +// StepProgress 步骤进度 +type StepProgress struct { + Name string `json:"name"` + Status string `json:"status"` + RetryCount int `json:"retry_count"` + StartTime time.Time `json:"start_time"` + EndTime time.Time `json:"end_time"` + Duration time.Duration `json:"duration"` + Error string `json:"error,omitempty"` +} + +// SagaEvent Saga事件 +type SagaEvent struct { + SagaID string `json:"saga_id"` + SagaName string `json:"saga_name"` + EventType string `json:"event_type"` + Status string `json:"status"` + Timestamp time.Time `json:"timestamp"` + Data interface{} `json:"data,omitempty"` +} + +// SagaManager Saga管理器 +type SagaManager struct { + sagas map[string]*Saga + logger *zap.Logger + mutex sync.RWMutex + config SagaConfig +} + +// NewSagaManager 创建Saga管理器 +func NewSagaManager(config SagaConfig, logger *zap.Logger) *SagaManager { + return &SagaManager{ + sagas: make(map[string]*Saga), + logger: logger, + config: config, + } +} + +// CreateSaga 创建Saga +func (sm *SagaManager) CreateSaga(id, name string) *Saga { + saga := NewSaga(id, name, sm.config, sm.logger.Named("saga")) + + sm.mutex.Lock() + sm.sagas[id] = saga + sm.mutex.Unlock() + + sm.logger.Info("Created saga", + zap.String("saga_id", id), + zap.String("saga_name", name)) + + return saga +} + +// GetSaga 获取Saga +func (sm *SagaManager) GetSaga(id string) (*Saga, bool) { + sm.mutex.RLock() + defer sm.mutex.RUnlock() + + saga, exists := sm.sagas[id] + return saga, exists +} + +// ListSagas 列出所有Saga +func (sm *SagaManager) ListSagas() []*Saga { + sm.mutex.RLock() + defer sm.mutex.RUnlock() + + sagas := make([]*Saga, 0, len(sm.sagas)) + for _, saga := range sm.sagas { + sagas = append(sagas, saga) + } + + return sagas +} + +// GetSagaProgress 获取Saga进度 +func (sm *SagaManager) GetSagaProgress(id string) (SagaProgress, bool) { + saga, exists := sm.GetSaga(id) + if !exists { + return SagaProgress{}, false + } + + return saga.GetProgress(), true +} + +// RemoveSaga 移除Saga +func (sm *SagaManager) RemoveSaga(id string) { + sm.mutex.Lock() + defer sm.mutex.Unlock() + + delete(sm.sagas, id) + sm.logger.Debug("Removed saga", zap.String("saga_id", id)) +} + +// GetStats 获取统计信息 +func (sm *SagaManager) GetStats() map[string]interface{} { + sm.mutex.RLock() + defer sm.mutex.RUnlock() + + statusCount := make(map[string]int) + for _, saga := range sm.sagas { + status := saga.GetStatus().String() + statusCount[status]++ + } + + return map[string]interface{}{ + "total_sagas": len(sm.sagas), + "status_count": statusCount, + } +} + +// 实现Service接口 + +// Name 返回服务名称 +func (sm *SagaManager) Name() string { + return "saga-manager" +} + +// Initialize 初始化服务 +func (sm *SagaManager) Initialize(ctx context.Context) error { + sm.logger.Info("Saga manager service initialized") + return nil +} + +// HealthCheck 健康检查 +func (sm *SagaManager) HealthCheck(ctx context.Context) error { + return nil +} + +// Shutdown 关闭服务 +func (sm *SagaManager) Shutdown(ctx context.Context) error { + sm.logger.Info("Saga manager service shutdown") + return nil +} diff --git a/internal/shared/sms/sms_service.go b/internal/shared/sms/sms_service.go new file mode 100644 index 0000000..97103b0 --- /dev/null +++ b/internal/shared/sms/sms_service.go @@ -0,0 +1,130 @@ +package sms + +import ( + "context" + "crypto/rand" + "fmt" + "math/big" + + "github.com/aliyun/alibaba-cloud-sdk-go/services/dysmsapi" + "go.uber.org/zap" + + "tyapi-server/internal/config" +) + +// Service 短信服务接口 +type Service interface { + SendVerificationCode(ctx context.Context, phone string, code string) error + GenerateCode(length int) string +} + +// AliSMSService 阿里云短信服务实现 +type AliSMSService struct { + client *dysmsapi.Client + config config.SMSConfig + logger *zap.Logger +} + +// NewAliSMSService 创建阿里云短信服务 +func NewAliSMSService(cfg config.SMSConfig, logger *zap.Logger) (*AliSMSService, error) { + client, err := dysmsapi.NewClientWithAccessKey("cn-hangzhou", cfg.AccessKeyID, cfg.AccessKeySecret) + if err != nil { + return nil, fmt.Errorf("创建短信客户端失败: %w", err) + } + + return &AliSMSService{ + client: client, + config: cfg, + logger: logger, + }, nil +} + +// SendVerificationCode 发送验证码 +func (s *AliSMSService) SendVerificationCode(ctx context.Context, phone string, code string) error { + request := dysmsapi.CreateSendSmsRequest() + request.Scheme = "https" + request.PhoneNumbers = phone + request.SignName = s.config.SignName + request.TemplateCode = s.config.TemplateCode + request.TemplateParam = fmt.Sprintf(`{"code":"%s"}`, code) + + response, err := s.client.SendSms(request) + if err != nil { + s.logger.Error("Failed to send SMS", + zap.String("phone", phone), + zap.Error(err)) + return fmt.Errorf("短信发送失败: %w", err) + } + + if response.Code != "OK" { + s.logger.Error("SMS send failed", + zap.String("phone", phone), + zap.String("code", response.Code), + zap.String("message", response.Message)) + return fmt.Errorf("短信发送失败: %s - %s", response.Code, response.Message) + } + + s.logger.Info("SMS sent successfully", + zap.String("phone", phone), + zap.String("bizId", response.BizId)) + + return nil +} + +// GenerateCode 生成验证码 +func (s *AliSMSService) GenerateCode(length int) string { + if length <= 0 { + length = 6 + } + + // 生成指定长度的数字验证码 + max := big.NewInt(int64(pow10(length))) + n, _ := rand.Int(rand.Reader, max) + + // 格式化为指定长度,不足时前面补0 + format := fmt.Sprintf("%%0%dd", length) + return fmt.Sprintf(format, n.Int64()) +} + +// pow10 计算10的n次方 +func pow10(n int) int { + result := 1 + for i := 0; i < n; i++ { + result *= 10 + } + return result +} + +// MockSMSService 模拟短信服务(用于开发和测试) +type MockSMSService struct { + logger *zap.Logger +} + +// NewMockSMSService 创建模拟短信服务 +func NewMockSMSService(logger *zap.Logger) *MockSMSService { + return &MockSMSService{ + logger: logger, + } +} + +// SendVerificationCode 模拟发送验证码 +func (s *MockSMSService) SendVerificationCode(ctx context.Context, phone string, code string) error { + s.logger.Info("Mock SMS sent", + zap.String("phone", phone), + zap.String("code", code)) + return nil +} + +// GenerateCode 生成验证码 +func (s *MockSMSService) GenerateCode(length int) string { + if length <= 0 { + length = 6 + } + + // 开发环境使用固定验证码便于测试 + result := "" + for i := 0; i < length; i++ { + result += "1" + } + return result +} diff --git a/internal/shared/tracing/decorators.go b/internal/shared/tracing/decorators.go new file mode 100644 index 0000000..b085d0b --- /dev/null +++ b/internal/shared/tracing/decorators.go @@ -0,0 +1,292 @@ +package tracing + +import ( + "context" + "fmt" + "reflect" + "runtime" + "strings" + "time" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + "go.uber.org/zap" +) + +// TracableService 可追踪的服务接口 +type TracableService interface { + Name() string +} + +// ServiceDecorator 服务装饰器 +type ServiceDecorator struct { + tracer *Tracer + logger *zap.Logger + config DecoratorConfig +} + +// DecoratorConfig 装饰器配置 +type DecoratorConfig struct { + EnableMethodTracing bool + ExcludePatterns []string + IncludeArguments bool + IncludeResults bool + SlowMethodThreshold time.Duration +} + +// DefaultDecoratorConfig 默认装饰器配置 +func DefaultDecoratorConfig() DecoratorConfig { + return DecoratorConfig{ + EnableMethodTracing: true, + ExcludePatterns: []string{"Health", "Ping", "Name"}, + IncludeArguments: true, + IncludeResults: false, + SlowMethodThreshold: 100 * time.Millisecond, + } +} + +// NewServiceDecorator 创建服务装饰器 +func NewServiceDecorator(tracer *Tracer, logger *zap.Logger) *ServiceDecorator { + return &ServiceDecorator{ + tracer: tracer, + logger: logger, + config: DefaultDecoratorConfig(), + } +} + +// WrapService 自动包装服务,为所有方法添加链路追踪 +func (d *ServiceDecorator) WrapService(service interface{}) interface{} { + serviceValue := reflect.ValueOf(service) + serviceType := reflect.TypeOf(service) + + if serviceType.Kind() == reflect.Ptr { + serviceType = serviceType.Elem() + serviceValue = serviceValue.Elem() + } + + // 创建代理结构 + proxyType := d.createProxyType(serviceType) + proxyValue := reflect.New(proxyType).Elem() + + // 设置原始服务字段 + proxyValue.FieldByName("target").Set(reflect.ValueOf(service)) + proxyValue.FieldByName("decorator").Set(reflect.ValueOf(d)) + + return proxyValue.Addr().Interface() +} + +// createProxyType 创建代理类型 +func (d *ServiceDecorator) createProxyType(serviceType reflect.Type) reflect.Type { + // 获取服务名称 + serviceName := d.getServiceName(serviceType) + + // 创建代理结构字段 + fields := []reflect.StructField{ + { + Name: "target", + Type: reflect.PtrTo(serviceType), + }, + { + Name: "decorator", + Type: reflect.TypeOf(d), + }, + } + + // 为每个方法创建包装器方法 + for i := 0; i < serviceType.NumMethod(); i++ { + method := serviceType.Method(i) + if d.shouldTraceMethod(method.Name) { + // 创建方法字段(用于存储方法实现) + fields = append(fields, reflect.StructField{ + Name: method.Name, + Type: method.Type, + }) + } + } + + // 创建新的结构类型 + proxyType := reflect.StructOf(fields) + + // 实现接口方法 + d.implementMethods(proxyType, serviceType, serviceName) + + return proxyType +} + +// shouldTraceMethod 判断是否应该追踪方法 +func (d *ServiceDecorator) shouldTraceMethod(methodName string) bool { + if !d.config.EnableMethodTracing { + return false + } + + for _, pattern := range d.config.ExcludePatterns { + if strings.Contains(methodName, pattern) { + return false + } + } + + return true +} + +// getServiceName 获取服务名称 +func (d *ServiceDecorator) getServiceName(serviceType reflect.Type) string { + serviceName := serviceType.Name() + // 移除Service后缀 + if strings.HasSuffix(serviceName, "Service") { + serviceName = strings.TrimSuffix(serviceName, "Service") + } + return strings.ToLower(serviceName) +} + +// TraceMethodCall 追踪方法调用 +func (d *ServiceDecorator) TraceMethodCall( + ctx context.Context, + serviceName, methodName string, + fn func(context.Context) ([]reflect.Value, error), + args []reflect.Value, +) ([]reflect.Value, error) { + // 创建span名称 + spanName := fmt.Sprintf("%s.%s", serviceName, methodName) + + // 开始追踪 + ctx, span := d.tracer.StartSpan(ctx, spanName) + defer span.End() + + // 添加基础属性 + d.tracer.AddSpanAttributes(span, + attribute.String("service.name", serviceName), + attribute.String("service.method", methodName), + attribute.String("service.type", "business"), + ) + + // 添加参数信息(如果启用) + if d.config.IncludeArguments { + d.addArgumentAttributes(span, args) + } + + // 记录开始时间 + startTime := time.Now() + + // 执行原始方法 + results, err := fn(ctx) + + // 计算执行时间 + duration := time.Since(startTime) + d.tracer.AddSpanAttributes(span, + attribute.Int64("service.duration_ms", duration.Milliseconds()), + ) + + // 标记慢方法 + if duration > d.config.SlowMethodThreshold { + d.tracer.AddSpanAttributes(span, + attribute.Bool("service.slow_method", true), + ) + d.logger.Warn("慢方法检测", + zap.String("service", serviceName), + zap.String("method", methodName), + zap.Duration("duration", duration), + zap.String("trace_id", d.tracer.GetTraceID(ctx)), + ) + } + + // 处理错误 + if err != nil { + d.tracer.SetSpanError(span, err) + d.logger.Error("服务方法执行失败", + zap.String("service", serviceName), + zap.String("method", methodName), + zap.Error(err), + zap.String("trace_id", d.tracer.GetTraceID(ctx)), + ) + } else { + d.tracer.SetSpanSuccess(span) + + // 添加结果信息(如果启用) + if d.config.IncludeResults { + d.addResultAttributes(span, results) + } + } + + return results, err +} + +// addArgumentAttributes 添加参数属性 +func (d *ServiceDecorator) addArgumentAttributes(span trace.Span, args []reflect.Value) { + for i, arg := range args { + if i == 0 && arg.Type().String() == "context.Context" { + continue // 跳过context参数 + } + + argName := fmt.Sprintf("service.arg_%d", i) + argValue := d.extractValue(arg) + + if argValue != "" && len(argValue) < 1000 { // 限制长度避免性能问题 + d.tracer.AddSpanAttributes(span, + attribute.String(argName, argValue), + ) + } + } +} + +// addResultAttributes 添加结果属性 +func (d *ServiceDecorator) addResultAttributes(span trace.Span, results []reflect.Value) { + for i, result := range results { + if result.Type().String() == "error" { + continue // 错误在其他地方处理 + } + + resultName := fmt.Sprintf("service.result_%d", i) + resultValue := d.extractValue(result) + + if resultValue != "" && len(resultValue) < 1000 { + d.tracer.AddSpanAttributes(span, + attribute.String(resultName, resultValue), + ) + } + } +} + +// extractValue 提取值的字符串表示 +func (d *ServiceDecorator) extractValue(value reflect.Value) string { + if !value.IsValid() { + return "" + } + + switch value.Kind() { + case reflect.String: + return value.String() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return fmt.Sprintf("%d", value.Int()) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return fmt.Sprintf("%d", value.Uint()) + case reflect.Float32, reflect.Float64: + return fmt.Sprintf("%.2f", value.Float()) + case reflect.Bool: + return fmt.Sprintf("%t", value.Bool()) + case reflect.Ptr: + if value.IsNil() { + return "nil" + } + return d.extractValue(value.Elem()) + case reflect.Struct: + // 对于结构体,只返回类型名 + return value.Type().Name() + case reflect.Slice, reflect.Array: + return fmt.Sprintf("[%d items]", value.Len()) + default: + return value.Type().Name() + } +} + +// implementMethods 实现接口方法(占位符,实际需要运行时代理) +func (d *ServiceDecorator) implementMethods(proxyType, serviceType reflect.Type, serviceName string) { + // 这里是运行时方法实现的占位符 + // 实际实现需要使用reflect.MakeFunc或其他运行时代理技术 +} + +// GetFunctionName 获取函数名称 +func GetFunctionName(fn interface{}) string { + name := runtime.FuncForPC(reflect.ValueOf(fn).Pointer()).Name() + parts := strings.Split(name, ".") + return parts[len(parts)-1] +} diff --git a/internal/shared/tracing/gorm_plugin.go b/internal/shared/tracing/gorm_plugin.go new file mode 100644 index 0000000..f7159a4 --- /dev/null +++ b/internal/shared/tracing/gorm_plugin.go @@ -0,0 +1,320 @@ +package tracing + +import ( + "context" + "fmt" + "strings" + "time" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" + "go.uber.org/zap" + "gorm.io/gorm" +) + +const ( + gormSpanKey = "otel:span" + gormOperationKey = "otel:operation" + gormTableNameKey = "otel:table_name" + gormStartTimeKey = "otel:start_time" +) + +// GormTracingPlugin GORM链路追踪插件 +type GormTracingPlugin struct { + tracer *Tracer + logger *zap.Logger + config GormPluginConfig +} + +// GormPluginConfig GORM插件配置 +type GormPluginConfig struct { + IncludeSQL bool + IncludeValues bool + SlowThreshold time.Duration + ExcludeTables []string + SanitizeSQL bool +} + +// DefaultGormPluginConfig 默认GORM插件配置 +func DefaultGormPluginConfig() GormPluginConfig { + return GormPluginConfig{ + IncludeSQL: true, + IncludeValues: false, // 生产环境建议设为false避免记录敏感数据 + SlowThreshold: 200 * time.Millisecond, + ExcludeTables: []string{"migrations", "schema_migrations"}, + SanitizeSQL: true, + } +} + +// NewGormTracingPlugin 创建GORM追踪插件 +func NewGormTracingPlugin(tracer *Tracer, logger *zap.Logger) *GormTracingPlugin { + return &GormTracingPlugin{ + tracer: tracer, + logger: logger, + config: DefaultGormPluginConfig(), + } +} + +// Name 返回插件名称 +func (p *GormTracingPlugin) Name() string { + return "gorm-otel-tracing" +} + +// Initialize 初始化插件 +func (p *GormTracingPlugin) Initialize(db *gorm.DB) error { + // 注册各种操作的回调 + callbacks := []string{"create", "query", "update", "delete", "raw"} + + for _, operation := range callbacks { + switch operation { + case "create": + err := db.Callback().Create().Before("gorm:create"). + Register(p.Name()+":before_create", p.beforeOperation) + if err != nil { + return fmt.Errorf("failed to register before create callback: %w", err) + } + err = db.Callback().Create().After("gorm:create"). + Register(p.Name()+":after_create", p.afterOperation) + if err != nil { + return fmt.Errorf("failed to register after create callback: %w", err) + } + case "query": + err := db.Callback().Query().Before("gorm:query"). + Register(p.Name()+":before_query", p.beforeOperation) + if err != nil { + return fmt.Errorf("failed to register before query callback: %w", err) + } + err = db.Callback().Query().After("gorm:query"). + Register(p.Name()+":after_query", p.afterOperation) + if err != nil { + return fmt.Errorf("failed to register after query callback: %w", err) + } + case "update": + err := db.Callback().Update().Before("gorm:update"). + Register(p.Name()+":before_update", p.beforeOperation) + if err != nil { + return fmt.Errorf("failed to register before update callback: %w", err) + } + err = db.Callback().Update().After("gorm:update"). + Register(p.Name()+":after_update", p.afterOperation) + if err != nil { + return fmt.Errorf("failed to register after update callback: %w", err) + } + case "delete": + err := db.Callback().Delete().Before("gorm:delete"). + Register(p.Name()+":before_delete", p.beforeOperation) + if err != nil { + return fmt.Errorf("failed to register before delete callback: %w", err) + } + err = db.Callback().Delete().After("gorm:delete"). + Register(p.Name()+":after_delete", p.afterOperation) + if err != nil { + return fmt.Errorf("failed to register after delete callback: %w", err) + } + case "raw": + err := db.Callback().Raw().Before("gorm:raw"). + Register(p.Name()+":before_raw", p.beforeOperation) + if err != nil { + return fmt.Errorf("failed to register before raw callback: %w", err) + } + err = db.Callback().Raw().After("gorm:raw"). + Register(p.Name()+":after_raw", p.afterOperation) + if err != nil { + return fmt.Errorf("failed to register after raw callback: %w", err) + } + } + } + + p.logger.Info("GORM追踪插件已初始化") + return nil +} + +// beforeOperation 操作前回调 +func (p *GormTracingPlugin) beforeOperation(db *gorm.DB) { + // 检查是否应该跳过追踪 + if p.shouldSkipTracing(db) { + return + } + + ctx := db.Statement.Context + if ctx == nil { + ctx = context.Background() + } + + // 获取操作信息 + operation := p.getOperationType(db) + tableName := p.getTableName(db) + + // 检查是否应该排除此表 + if p.isExcludedTable(tableName) { + return + } + + // 开始追踪 + ctx, span := p.tracer.StartDBSpan(ctx, operation, tableName) + + // 添加基础属性 + p.tracer.AddSpanAttributes(span, + attribute.String("db.system", "postgresql"), + attribute.String("db.operation", operation), + ) + + if tableName != "" { + p.tracer.AddSpanAttributes(span, attribute.String("db.table", tableName)) + } + + // 保存追踪信息到GORM context + db.Set(gormSpanKey, span) + db.Set(gormOperationKey, operation) + db.Set(gormTableNameKey, tableName) + db.Set(gormStartTimeKey, time.Now()) + + // 更新statement context + db.Statement.Context = ctx +} + +// afterOperation 操作后回调 +func (p *GormTracingPlugin) afterOperation(db *gorm.DB) { + // 获取span + spanValue, exists := db.Get(gormSpanKey) + if !exists { + return + } + + span, ok := spanValue.(trace.Span) + if !ok { + return + } + defer span.End() + + // 获取操作信息 + operation, _ := db.Get(gormOperationKey) + tableName, _ := db.Get(gormTableNameKey) + startTime, _ := db.Get(gormStartTimeKey) + + // 计算执行时间 + var duration time.Duration + if st, ok := startTime.(time.Time); ok { + duration = time.Since(st) + p.tracer.AddSpanAttributes(span, + attribute.Int64("db.duration_ms", duration.Milliseconds()), + ) + } + + // 添加SQL信息 + if p.config.IncludeSQL && db.Statement.SQL.String() != "" { + sql := db.Statement.SQL.String() + if p.config.SanitizeSQL { + sql = p.sanitizeSQL(sql) + } + p.tracer.AddSpanAttributes(span, attribute.String("db.statement", sql)) + } + + // 添加影响行数 + if db.Statement.RowsAffected >= 0 { + p.tracer.AddSpanAttributes(span, + attribute.Int64("db.rows_affected", db.Statement.RowsAffected), + ) + } + + // 处理错误 + if db.Error != nil { + p.tracer.SetSpanError(span, db.Error) + span.SetStatus(codes.Error, db.Error.Error()) + + p.logger.Error("数据库操作失败", + zap.String("operation", fmt.Sprintf("%v", operation)), + zap.String("table", fmt.Sprintf("%v", tableName)), + zap.Error(db.Error), + zap.String("trace_id", p.tracer.GetTraceID(db.Statement.Context)), + ) + } else { + p.tracer.SetSpanSuccess(span) + span.SetStatus(codes.Ok, "success") + + // 检查慢查询 + if duration > p.config.SlowThreshold { + p.tracer.AddSpanAttributes(span, + attribute.Bool("db.slow_query", true), + ) + + p.logger.Warn("慢SQL查询检测", + zap.String("operation", fmt.Sprintf("%v", operation)), + zap.String("table", fmt.Sprintf("%v", tableName)), + zap.Duration("duration", duration), + zap.String("sql", db.Statement.SQL.String()), + zap.String("trace_id", p.tracer.GetTraceID(db.Statement.Context)), + ) + } + } +} + +// shouldSkipTracing 检查是否应该跳过追踪 +func (p *GormTracingPlugin) shouldSkipTracing(db *gorm.DB) bool { + // 检查是否已有span(避免重复追踪) + if _, exists := db.Get(gormSpanKey); exists { + return true + } + + return false +} + +// getOperationType 获取操作类型 +func (p *GormTracingPlugin) getOperationType(db *gorm.DB) string { + switch db.Statement.ReflectValue.Kind() { + default: + sql := strings.ToUpper(strings.TrimSpace(db.Statement.SQL.String())) + if sql == "" { + return "unknown" + } + + if strings.HasPrefix(sql, "SELECT") { + return "select" + } else if strings.HasPrefix(sql, "INSERT") { + return "insert" + } else if strings.HasPrefix(sql, "UPDATE") { + return "update" + } else if strings.HasPrefix(sql, "DELETE") { + return "delete" + } else if strings.HasPrefix(sql, "CREATE") { + return "create" + } else if strings.HasPrefix(sql, "DROP") { + return "drop" + } else if strings.HasPrefix(sql, "ALTER") { + return "alter" + } + + return "query" + } +} + +// getTableName 获取表名 +func (p *GormTracingPlugin) getTableName(db *gorm.DB) string { + if db.Statement.Table != "" { + return db.Statement.Table + } + + if db.Statement.Schema != nil && db.Statement.Schema.Table != "" { + return db.Statement.Schema.Table + } + + return "" +} + +// isExcludedTable 检查是否为排除的表 +func (p *GormTracingPlugin) isExcludedTable(tableName string) bool { + for _, excluded := range p.config.ExcludeTables { + if tableName == excluded { + return true + } + } + return false +} + +// sanitizeSQL 清理SQL语句,移除敏感信息 +func (p *GormTracingPlugin) sanitizeSQL(sql string) string { + // 简单的SQL清理,将参数替换为占位符 + // 在生产环境中,您可能需要更复杂的清理逻辑 + return strings.ReplaceAll(sql, "'", "?") +} diff --git a/internal/shared/tracing/redis_wrapper.go b/internal/shared/tracing/redis_wrapper.go new file mode 100644 index 0000000..12a47ac --- /dev/null +++ b/internal/shared/tracing/redis_wrapper.go @@ -0,0 +1,407 @@ +package tracing + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/redis/go-redis/v9" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + "go.uber.org/zap" + + "tyapi-server/internal/shared/interfaces" +) + +// TracedRedisCache Redis缓存自动追踪包装器 +type TracedRedisCache struct { + client redis.UniversalClient + tracer *Tracer + logger *zap.Logger + prefix string + config RedisTracingConfig +} + +// RedisTracingConfig Redis追踪配置 +type RedisTracingConfig struct { + IncludeKeys bool + IncludeValues bool + MaxKeyLength int + MaxValueLength int + SlowThreshold time.Duration + SanitizeValues bool +} + +// DefaultRedisTracingConfig 默认Redis追踪配置 +func DefaultRedisTracingConfig() RedisTracingConfig { + return RedisTracingConfig{ + IncludeKeys: true, + IncludeValues: false, // 生产环境建议设为false保护敏感数据 + MaxKeyLength: 100, + MaxValueLength: 1000, + SlowThreshold: 50 * time.Millisecond, + SanitizeValues: true, + } +} + +// NewTracedRedisCache 创建带追踪的Redis缓存 +func NewTracedRedisCache(client redis.UniversalClient, tracer *Tracer, logger *zap.Logger, prefix string) interfaces.CacheService { + return &TracedRedisCache{ + client: client, + tracer: tracer, + logger: logger, + prefix: prefix, + config: DefaultRedisTracingConfig(), + } +} + +// Name 返回服务名称 +func (c *TracedRedisCache) Name() string { + return "redis-cache" +} + +// Initialize 初始化服务 +func (c *TracedRedisCache) Initialize(ctx context.Context) error { + c.logger.Info("Redis缓存服务已初始化") + return nil +} + +// HealthCheck 健康检查 +func (c *TracedRedisCache) HealthCheck(ctx context.Context) error { + _, err := c.client.Ping(ctx).Result() + return err +} + +// Shutdown 关闭服务 +func (c *TracedRedisCache) Shutdown(ctx context.Context) error { + c.logger.Info("Redis缓存服务已关闭") + return c.client.Close() +} + +// Get 获取缓存值 +func (c *TracedRedisCache) Get(ctx context.Context, key string, dest interface{}) error { + // 开始追踪 + ctx, span := c.tracer.StartCacheSpan(ctx, "get", key) + defer span.End() + + // 添加基础属性 + c.addBaseAttributes(span, "get", key) + + // 记录开始时间 + startTime := time.Now() + + // 构建完整键名 + fullKey := c.buildKey(key) + + // 执行Redis操作 + result, err := c.client.Get(ctx, fullKey).Result() + + // 计算执行时间 + duration := time.Since(startTime) + c.tracer.AddSpanAttributes(span, + attribute.Int64("redis.duration_ms", duration.Milliseconds()), + ) + + // 检查慢操作 + if duration > c.config.SlowThreshold { + c.tracer.AddSpanAttributes(span, + attribute.Bool("redis.slow_operation", true), + ) + c.logger.Warn("Redis慢操作检测", + zap.String("operation", "get"), + zap.String("key", c.sanitizeKey(key)), + zap.Duration("duration", duration), + zap.String("trace_id", c.tracer.GetTraceID(ctx)), + ) + } + + // 处理结果 + if err != nil { + if err == redis.Nil { + // 缓存未命中 + c.tracer.AddSpanAttributes(span, + attribute.Bool("redis.hit", false), + attribute.String("redis.result", "miss"), + ) + c.tracer.SetSpanSuccess(span) + return interfaces.ErrCacheMiss + } else { + // Redis错误 + c.tracer.SetSpanError(span, err) + c.logger.Error("Redis GET操作失败", + zap.String("key", c.sanitizeKey(key)), + zap.Error(err), + zap.String("trace_id", c.tracer.GetTraceID(ctx)), + ) + return err + } + } + + // 缓存命中 + c.tracer.AddSpanAttributes(span, + attribute.Bool("redis.hit", true), + attribute.String("redis.result", "hit"), + attribute.Int("redis.value_size", len(result)), + ) + + // 反序列化 + if err := c.deserialize(result, dest); err != nil { + c.tracer.SetSpanError(span, err) + return err + } + + c.tracer.SetSpanSuccess(span) + return nil +} + +// Set 设置缓存值 +func (c *TracedRedisCache) Set(ctx context.Context, key string, value interface{}, ttl ...interface{}) error { + // 开始追踪 + ctx, span := c.tracer.StartCacheSpan(ctx, "set", key) + defer span.End() + + // 添加基础属性 + c.addBaseAttributes(span, "set", key) + + // 处理TTL + var expiration time.Duration + if len(ttl) > 0 { + if duration, ok := ttl[0].(time.Duration); ok { + expiration = duration + c.tracer.AddSpanAttributes(span, + attribute.Int64("redis.ttl_seconds", int64(expiration.Seconds())), + ) + } + } + + // 记录开始时间 + startTime := time.Now() + + // 序列化值 + serialized, err := c.serialize(value) + if err != nil { + c.tracer.SetSpanError(span, err) + return err + } + + // 构建完整键名 + fullKey := c.buildKey(key) + + // 执行Redis操作 + err = c.client.Set(ctx, fullKey, serialized, expiration).Err() + + // 计算执行时间 + duration := time.Since(startTime) + c.tracer.AddSpanAttributes(span, + attribute.Int64("redis.duration_ms", duration.Milliseconds()), + attribute.Int("redis.value_size", len(serialized)), + ) + + // 检查慢操作 + if duration > c.config.SlowThreshold { + c.tracer.AddSpanAttributes(span, + attribute.Bool("redis.slow_operation", true), + ) + c.logger.Warn("Redis慢操作检测", + zap.String("operation", "set"), + zap.String("key", c.sanitizeKey(key)), + zap.Duration("duration", duration), + zap.String("trace_id", c.tracer.GetTraceID(ctx)), + ) + } + + // 处理错误 + if err != nil { + c.tracer.SetSpanError(span, err) + c.logger.Error("Redis SET操作失败", + zap.String("key", c.sanitizeKey(key)), + zap.Error(err), + zap.String("trace_id", c.tracer.GetTraceID(ctx)), + ) + return err + } + + c.tracer.SetSpanSuccess(span) + return nil +} + +// Delete 删除缓存 +func (c *TracedRedisCache) Delete(ctx context.Context, keys ...string) error { + // 开始追踪 + ctx, span := c.tracer.StartCacheSpan(ctx, "delete", strings.Join(keys, ",")) + defer span.End() + + // 添加基础属性 + c.tracer.AddSpanAttributes(span, + attribute.String("redis.operation", "delete"), + attribute.Int("redis.key_count", len(keys)), + ) + + // 记录开始时间 + startTime := time.Now() + + // 构建完整键名 + fullKeys := make([]string, len(keys)) + for i, key := range keys { + fullKeys[i] = c.buildKey(key) + } + + // 执行Redis操作 + deleted, err := c.client.Del(ctx, fullKeys...).Result() + + // 计算执行时间 + duration := time.Since(startTime) + c.tracer.AddSpanAttributes(span, + attribute.Int64("redis.duration_ms", duration.Milliseconds()), + attribute.Int64("redis.deleted_count", deleted), + ) + + // 处理错误 + if err != nil { + c.tracer.SetSpanError(span, err) + c.logger.Error("Redis DELETE操作失败", + zap.Strings("keys", c.sanitizeKeys(keys)), + zap.Error(err), + zap.String("trace_id", c.tracer.GetTraceID(ctx)), + ) + return err + } + + c.tracer.SetSpanSuccess(span) + return nil +} + +// Exists 检查键是否存在 +func (c *TracedRedisCache) Exists(ctx context.Context, key string) (bool, error) { + // 开始追踪 + ctx, span := c.tracer.StartCacheSpan(ctx, "exists", key) + defer span.End() + + // 添加基础属性 + c.addBaseAttributes(span, "exists", key) + + // 记录开始时间 + startTime := time.Now() + + // 构建完整键名 + fullKey := c.buildKey(key) + + // 执行Redis操作 + count, err := c.client.Exists(ctx, fullKey).Result() + + // 计算执行时间 + duration := time.Since(startTime) + c.tracer.AddSpanAttributes(span, + attribute.Int64("redis.duration_ms", duration.Milliseconds()), + attribute.Bool("redis.exists", count > 0), + ) + + // 处理错误 + if err != nil { + c.tracer.SetSpanError(span, err) + return false, err + } + + c.tracer.SetSpanSuccess(span) + return count > 0, nil +} + +// GetMultiple 批量获取(基础实现) +func (c *TracedRedisCache) GetMultiple(ctx context.Context, keys []string) (map[string]interface{}, error) { + result := make(map[string]interface{}) + + // 简单实现:逐个获取(实际应用中可以使用MGET优化) + for _, key := range keys { + var value interface{} + if err := c.Get(ctx, key, &value); err == nil { + result[key] = value + } + } + + return result, nil +} + +// SetMultiple 批量设置(基础实现) +func (c *TracedRedisCache) SetMultiple(ctx context.Context, data map[string]interface{}, ttl ...interface{}) error { + // 简单实现:逐个设置(实际应用中可以使用pipeline优化) + for key, value := range data { + if err := c.Set(ctx, key, value, ttl...); err != nil { + return err + } + } + return nil +} + +// DeletePattern 按模式删除(基础实现) +func (c *TracedRedisCache) DeletePattern(ctx context.Context, pattern string) error { + // 这里需要实现模式删除逻辑 + return fmt.Errorf("DeletePattern not implemented") +} + +// Keys 获取匹配的键(基础实现) +func (c *TracedRedisCache) Keys(ctx context.Context, pattern string) ([]string, error) { + // 这里需要实现键匹配逻辑 + return nil, fmt.Errorf("Keys not implemented") +} + +// Stats 获取缓存统计(基础实现) +func (c *TracedRedisCache) Stats(ctx context.Context) (interfaces.CacheStats, error) { + return interfaces.CacheStats{}, fmt.Errorf("Stats not implemented") +} + +// 辅助方法 + +// addBaseAttributes 添加基础属性 +func (c *TracedRedisCache) addBaseAttributes(span trace.Span, operation, key string) { + c.tracer.AddSpanAttributes(span, + attribute.String("redis.operation", operation), + attribute.String("db.system", "redis"), + ) + + if c.config.IncludeKeys { + sanitizedKey := c.sanitizeKey(key) + if len(sanitizedKey) <= c.config.MaxKeyLength { + c.tracer.AddSpanAttributes(span, + attribute.String("redis.key", sanitizedKey), + ) + } + } +} + +// buildKey 构建完整的Redis键名 +func (c *TracedRedisCache) buildKey(key string) string { + if c.prefix == "" { + return key + } + return fmt.Sprintf("%s:%s", c.prefix, key) +} + +// sanitizeKey 清理键名用于日志记录 +func (c *TracedRedisCache) sanitizeKey(key string) string { + if len(key) <= c.config.MaxKeyLength { + return key + } + return key[:c.config.MaxKeyLength] + "..." +} + +// sanitizeKeys 批量清理键名 +func (c *TracedRedisCache) sanitizeKeys(keys []string) []string { + result := make([]string, len(keys)) + for i, key := range keys { + result[i] = c.sanitizeKey(key) + } + return result +} + +// serialize 序列化值(简单实现) +func (c *TracedRedisCache) serialize(value interface{}) (string, error) { + // 这里应该使用JSON或其他序列化方法 + return fmt.Sprintf("%v", value), nil +} + +// deserialize 反序列化值(简单实现) +func (c *TracedRedisCache) deserialize(data string, dest interface{}) error { + // 这里应该实现真正的反序列化逻辑 + return fmt.Errorf("deserialize not fully implemented") +} diff --git a/internal/shared/tracing/service_wrapper.go b/internal/shared/tracing/service_wrapper.go new file mode 100644 index 0000000..327aa8e --- /dev/null +++ b/internal/shared/tracing/service_wrapper.go @@ -0,0 +1,189 @@ +package tracing + +import ( + "context" + "fmt" + "time" + + "go.opentelemetry.io/otel/attribute" + "go.uber.org/zap" + + "tyapi-server/internal/domains/user/dto" + "tyapi-server/internal/domains/user/entities" + "tyapi-server/internal/shared/interfaces" +) + +// ServiceWrapper 服务包装器,提供自动追踪能力 +type ServiceWrapper struct { + tracer *Tracer + logger *zap.Logger +} + +// NewServiceWrapper 创建服务包装器 +func NewServiceWrapper(tracer *Tracer, logger *zap.Logger) *ServiceWrapper { + return &ServiceWrapper{ + tracer: tracer, + logger: logger, + } +} + +// TraceServiceCall 追踪服务调用的通用方法 +func (w *ServiceWrapper) TraceServiceCall( + ctx context.Context, + serviceName, methodName string, + fn func(context.Context) error, +) error { + // 创建span名称 + spanName := fmt.Sprintf("%s.%s", serviceName, methodName) + + // 开始追踪 + ctx, span := w.tracer.StartSpan(ctx, spanName) + defer span.End() + + // 添加基础属性 + w.tracer.AddSpanAttributes(span, + attribute.String("service.name", serviceName), + attribute.String("service.method", methodName), + attribute.String("service.type", "business"), + ) + + // 记录开始时间 + startTime := time.Now() + + // 执行原始方法 + err := fn(ctx) + + // 计算执行时间 + duration := time.Since(startTime) + w.tracer.AddSpanAttributes(span, + attribute.Int64("service.duration_ms", duration.Milliseconds()), + ) + + // 标记慢方法 + if duration > 100*time.Millisecond { + w.tracer.AddSpanAttributes(span, + attribute.Bool("service.slow_method", true), + ) + w.logger.Warn("慢方法检测", + zap.String("service", serviceName), + zap.String("method", methodName), + zap.Duration("duration", duration), + zap.String("trace_id", w.tracer.GetTraceID(ctx)), + ) + } + + // 处理错误 + if err != nil { + w.tracer.SetSpanError(span, err) + w.logger.Error("服务方法执行失败", + zap.String("service", serviceName), + zap.String("method", methodName), + zap.Error(err), + zap.String("trace_id", w.tracer.GetTraceID(ctx)), + ) + } else { + w.tracer.SetSpanSuccess(span) + } + + return err +} + +// TracedUserService 自动追踪的用户服务包装器 +type TracedUserService struct { + service interfaces.UserService + wrapper *ServiceWrapper +} + +// NewTracedUserService 创建带追踪的用户服务 +func NewTracedUserService(service interfaces.UserService, wrapper *ServiceWrapper) interfaces.UserService { + return &TracedUserService{ + service: service, + wrapper: wrapper, + } +} + +func (t *TracedUserService) Name() string { + return "user-service" +} + +func (t *TracedUserService) Initialize(ctx context.Context) error { + return t.wrapper.TraceServiceCall(ctx, "user", "initialize", t.service.Initialize) +} + +func (t *TracedUserService) HealthCheck(ctx context.Context) error { + return t.service.HealthCheck(ctx) // 不追踪健康检查 +} + +func (t *TracedUserService) Shutdown(ctx context.Context) error { + return t.wrapper.TraceServiceCall(ctx, "user", "shutdown", t.service.Shutdown) +} + +func (t *TracedUserService) Register(ctx context.Context, req *dto.RegisterRequest) (*entities.User, error) { + var result *entities.User + var err error + + traceErr := t.wrapper.TraceServiceCall(ctx, "user", "register", func(ctx context.Context) error { + result, err = t.service.Register(ctx, req) + return err + }) + + if traceErr != nil { + return nil, traceErr + } + + return result, err +} + +func (t *TracedUserService) LoginWithPassword(ctx context.Context, req *dto.LoginWithPasswordRequest) (*entities.User, error) { + var result *entities.User + var err error + + traceErr := t.wrapper.TraceServiceCall(ctx, "user", "login_password", func(ctx context.Context) error { + result, err = t.service.LoginWithPassword(ctx, req) + return err + }) + + if traceErr != nil { + return nil, traceErr + } + + return result, err +} + +func (t *TracedUserService) LoginWithSMS(ctx context.Context, req *dto.LoginWithSMSRequest) (*entities.User, error) { + var result *entities.User + var err error + + traceErr := t.wrapper.TraceServiceCall(ctx, "user", "login_sms", func(ctx context.Context) error { + result, err = t.service.LoginWithSMS(ctx, req) + return err + }) + + if traceErr != nil { + return nil, traceErr + } + + return result, err +} + +func (t *TracedUserService) ChangePassword(ctx context.Context, userID string, req *dto.ChangePasswordRequest) error { + return t.wrapper.TraceServiceCall(ctx, "user", "change_password", func(ctx context.Context) error { + return t.service.ChangePassword(ctx, userID, req) + }) +} + +func (t *TracedUserService) GetByID(ctx context.Context, id string) (*entities.User, error) { + var result *entities.User + var err error + + traceErr := t.wrapper.TraceServiceCall(ctx, "user", "get_by_id", func(ctx context.Context) error { + result, err = t.service.GetByID(ctx, id) + return err + }) + + if traceErr != nil { + return nil, traceErr + } + + return result, err +} diff --git a/internal/shared/tracing/tracer.go b/internal/shared/tracing/tracer.go new file mode 100644 index 0000000..cb9344b --- /dev/null +++ b/internal/shared/tracing/tracer.go @@ -0,0 +1,474 @@ +package tracing + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/gin-gonic/gin" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/trace" + "go.uber.org/zap" +) + +// TracerConfig 追踪器配置 +type TracerConfig struct { + ServiceName string + ServiceVersion string + Environment string + Endpoint string + SampleRate float64 + Enabled bool +} + +// DefaultTracerConfig 默认追踪器配置 +func DefaultTracerConfig() TracerConfig { + return TracerConfig{ + ServiceName: "tyapi-server", + ServiceVersion: "1.0.0", + Environment: "development", + Endpoint: "http://localhost:4317", + SampleRate: 0.1, + Enabled: true, + } +} + +// Tracer 链路追踪器 +type Tracer struct { + config TracerConfig + logger *zap.Logger + provider *sdktrace.TracerProvider + tracer trace.Tracer + mutex sync.RWMutex + initialized bool + shutdown func(context.Context) error +} + +// NewTracer 创建链路追踪器 +func NewTracer(config TracerConfig, logger *zap.Logger) *Tracer { + return &Tracer{ + config: config, + logger: logger, + } +} + +// Initialize 初始化追踪器 +func (t *Tracer) Initialize(ctx context.Context) error { + t.mutex.Lock() + defer t.mutex.Unlock() + + if t.initialized { + return nil + } + + if !t.config.Enabled { + t.logger.Info("Tracing is disabled") + return nil + } + + // 创建资源 + res, err := resource.New(ctx, + resource.WithAttributes( + attribute.String("service.name", t.config.ServiceName), + attribute.String("service.version", t.config.ServiceVersion), + attribute.String("environment", t.config.Environment), + ), + ) + if err != nil { + return fmt.Errorf("failed to create resource: %w", err) + } + + // 创建采样器 + sampler := sdktrace.TraceIDRatioBased(t.config.SampleRate) + + // 创建导出器 + var spanProcessor sdktrace.SpanProcessor + if t.config.Endpoint != "" { + // 使用OTLP gRPC导出器(支持Jaeger、Tempo等) + exporter, err := otlptracegrpc.New(ctx, + otlptracegrpc.WithEndpoint(t.config.Endpoint), + otlptracegrpc.WithInsecure(), // 开发环境使用,生产环境应配置TLS + otlptracegrpc.WithTimeout(time.Second*10), + otlptracegrpc.WithRetry(otlptracegrpc.RetryConfig{ + Enabled: true, + InitialInterval: time.Millisecond * 100, + MaxInterval: time.Second * 5, + MaxElapsedTime: time.Second * 30, + }), + ) + if err != nil { + t.logger.Warn("Failed to create OTLP exporter, using noop exporter", + zap.Error(err), + zap.String("endpoint", t.config.Endpoint)) + spanProcessor = sdktrace.NewSimpleSpanProcessor(&noopExporter{}) + } else { + // 在生产环境中使用批处理器以提高性能 + spanProcessor = sdktrace.NewBatchSpanProcessor(exporter, + sdktrace.WithBatchTimeout(time.Second*5), + sdktrace.WithMaxExportBatchSize(512), + sdktrace.WithMaxQueueSize(2048), + sdktrace.WithExportTimeout(time.Second*30), + ) + t.logger.Info("OTLP exporter initialized successfully", + zap.String("endpoint", t.config.Endpoint)) + } + } else { + // 如果没有配置端点,使用空导出器 + spanProcessor = sdktrace.NewSimpleSpanProcessor(&noopExporter{}) + t.logger.Info("Using noop exporter (no endpoint configured)") + } + + // 创建TracerProvider + provider := sdktrace.NewTracerProvider( + sdktrace.WithResource(res), + sdktrace.WithSampler(sampler), + sdktrace.WithSpanProcessor(spanProcessor), + ) + + // 设置全局TracerProvider + otel.SetTracerProvider(provider) + + // 创建Tracer + tracer := provider.Tracer(t.config.ServiceName) + + t.provider = provider + t.tracer = tracer + t.shutdown = func(ctx context.Context) error { + return provider.Shutdown(ctx) + } + t.initialized = true + + t.logger.Info("Tracing initialized successfully", + zap.String("service", t.config.ServiceName), + zap.Float64("sample_rate", t.config.SampleRate)) + + return nil +} + +// StartSpan 开始一个新的span +func (t *Tracer) StartSpan(ctx context.Context, name string, opts ...trace.SpanStartOption) (context.Context, trace.Span) { + if !t.initialized || !t.config.Enabled { + return ctx, trace.SpanFromContext(ctx) + } + + return t.tracer.Start(ctx, name, opts...) +} + +// StartHTTPSpan 开始一个HTTP span +func (t *Tracer) StartHTTPSpan(ctx context.Context, method, path string) (context.Context, trace.Span) { + spanName := fmt.Sprintf("%s %s", method, path) + + // 检查是否已有错误标记,如果有则使用"error"作为操作名 + // 这样可以匹配Jaeger采样配置中的错误操作策略 + if ctx.Value("otel_error_request") != nil { + spanName = "error" + } + + ctx, span := t.StartSpan(ctx, spanName, + trace.WithSpanKind(trace.SpanKindServer), + trace.WithAttributes( + attribute.String("http.method", method), + attribute.String("http.route", path), + ), + ) + + // 保存原始操作名,以便在错误发生时可以更新 + if ctx.Value("otel_error_request") == nil { + ctx = context.WithValue(ctx, "otel_original_operation", spanName) + } + + return ctx, span +} + +// StartDBSpan 开始一个数据库span +func (t *Tracer) StartDBSpan(ctx context.Context, operation, table string) (context.Context, trace.Span) { + spanName := fmt.Sprintf("db.%s.%s", operation, table) + + return t.StartSpan(ctx, spanName, + trace.WithSpanKind(trace.SpanKindClient), + trace.WithAttributes( + attribute.String("db.operation", operation), + attribute.String("db.table", table), + attribute.String("db.system", "postgresql"), + ), + ) +} + +// StartCacheSpan 开始一个缓存span +func (t *Tracer) StartCacheSpan(ctx context.Context, operation, key string) (context.Context, trace.Span) { + spanName := fmt.Sprintf("cache.%s", operation) + + return t.StartSpan(ctx, spanName, + trace.WithSpanKind(trace.SpanKindClient), + trace.WithAttributes( + attribute.String("cache.operation", operation), + attribute.String("cache.system", "redis"), + ), + ) +} + +// StartExternalAPISpan 开始一个外部API调用span +func (t *Tracer) StartExternalAPISpan(ctx context.Context, service, operation string) (context.Context, trace.Span) { + spanName := fmt.Sprintf("api.%s.%s", service, operation) + + return t.StartSpan(ctx, spanName, + trace.WithSpanKind(trace.SpanKindClient), + trace.WithAttributes( + attribute.String("api.service", service), + attribute.String("api.operation", operation), + ), + ) +} + +// AddSpanAttributes 添加span属性 +func (t *Tracer) AddSpanAttributes(span trace.Span, attrs ...attribute.KeyValue) { + if span.IsRecording() { + span.SetAttributes(attrs...) + } +} + +// SetSpanError 设置span错误 +func (t *Tracer) SetSpanError(span trace.Span, err error) { + if span.IsRecording() { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) + + // 将span操作名更新为"error",以匹配Jaeger采样配置 + // 注意:这是一种变通方法,因为OpenTelemetry不支持直接更改span名称 + // 我们通过添加特殊属性来标识这是一个错误span + span.SetAttributes( + attribute.String("error.operation", "true"), + attribute.String("operation.type", "error"), + ) + + // 记录错误日志,包含trace ID便于关联 + if t.logger != nil { + ctx := trace.ContextWithSpan(context.Background(), span) + t.logger.Error("操作发生错误", + zap.Error(err), + zap.String("trace_id", t.GetTraceID(ctx)), + zap.String("span_id", t.GetSpanID(ctx)), + ) + } + } +} + +// SetSpanSuccess 设置span成功 +func (t *Tracer) SetSpanSuccess(span trace.Span) { + if span.IsRecording() { + span.SetStatus(codes.Ok, "success") + } +} + +// SetHTTPStatus 根据HTTP状态码设置span状态 +func (t *Tracer) SetHTTPStatus(span trace.Span, statusCode int) { + if !span.IsRecording() { + return + } + + // 添加HTTP状态码属性 + span.SetAttributes(attribute.Int("http.status_code", statusCode)) + + // 对于4xx和5xx错误,标记为错误并应用错误采样策略 + if statusCode >= 400 { + errorMsg := fmt.Sprintf("HTTP %d", statusCode) + span.SetStatus(codes.Error, errorMsg) + + // 添加错误操作标记,以匹配Jaeger采样配置 + span.SetAttributes( + attribute.String("error.operation", "true"), + attribute.String("operation.type", "error"), + ) + + // 记录HTTP错误 + if t.logger != nil { + ctx := trace.ContextWithSpan(context.Background(), span) + t.logger.Warn("HTTP请求错误", + zap.Int("status_code", statusCode), + zap.String("trace_id", t.GetTraceID(ctx)), + zap.String("span_id", t.GetSpanID(ctx)), + ) + } + } else { + span.SetStatus(codes.Ok, "success") + } +} + +// GetTraceID 获取当前上下文的trace ID +func (t *Tracer) GetTraceID(ctx context.Context) string { + span := trace.SpanFromContext(ctx) + if span.SpanContext().IsValid() { + return span.SpanContext().TraceID().String() + } + return "" +} + +// GetSpanID 获取当前上下文的span ID +func (t *Tracer) GetSpanID(ctx context.Context) string { + span := trace.SpanFromContext(ctx) + if span.SpanContext().IsValid() { + return span.SpanContext().SpanID().String() + } + return "" +} + +// IsTracing 检查是否正在追踪 +func (t *Tracer) IsTracing(ctx context.Context) bool { + span := trace.SpanFromContext(ctx) + return span.SpanContext().IsValid() && span.IsRecording() +} + +// Shutdown 关闭追踪器 +func (t *Tracer) Shutdown(ctx context.Context) error { + t.mutex.Lock() + defer t.mutex.Unlock() + + if !t.initialized || t.shutdown == nil { + return nil + } + + err := t.shutdown(ctx) + if err != nil { + t.logger.Error("Failed to shutdown tracer", zap.Error(err)) + return err + } + + t.initialized = false + t.logger.Info("Tracer shutdown successfully") + return nil +} + +// GetStats 获取追踪统计信息 +func (t *Tracer) GetStats() map[string]interface{} { + t.mutex.RLock() + defer t.mutex.RUnlock() + + return map[string]interface{}{ + "initialized": t.initialized, + "enabled": t.config.Enabled, + "service_name": t.config.ServiceName, + "service_version": t.config.ServiceVersion, + "environment": t.config.Environment, + "sample_rate": t.config.SampleRate, + "endpoint": t.config.Endpoint, + } +} + +// 实现Service接口 + +// Name 返回服务名称 +func (t *Tracer) Name() string { + return "tracer" +} + +// HealthCheck 健康检查 +func (t *Tracer) HealthCheck(ctx context.Context) error { + if !t.config.Enabled { + return nil + } + + if !t.initialized { + return fmt.Errorf("tracer not initialized") + } + + return nil +} + +// noopExporter 简单的无操作导出器(用于演示) +type noopExporter struct{} + +func (e *noopExporter) ExportSpans(ctx context.Context, spans []sdktrace.ReadOnlySpan) error { + // 在实际应用中,这里应该将spans发送到Jaeger或其他追踪系统 + return nil +} + +func (e *noopExporter) Shutdown(ctx context.Context) error { + return nil +} + +// TraceMiddleware 追踪中间件工厂 +func (t *Tracer) TraceMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + if !t.initialized || !t.config.Enabled { + c.Next() + return + } + + // 开始HTTP span + ctx, span := t.StartHTTPSpan(c.Request.Context(), c.Request.Method, c.FullPath()) + defer span.End() + + // 将trace ID添加到响应头 + traceID := t.GetTraceID(ctx) + if traceID != "" { + c.Header("X-Trace-ID", traceID) + } + + // 将span上下文存储到gin上下文 + c.Request = c.Request.WithContext(ctx) + + // 处理请求 + c.Next() + + // 设置HTTP状态码 + t.SetHTTPStatus(span, c.Writer.Status()) + + // 添加响应信息 + t.AddSpanAttributes(span, + attribute.Int("http.status_code", c.Writer.Status()), + attribute.Int("http.response_size", c.Writer.Size()), + ) + + // 添加错误信息 + if len(c.Errors) > 0 { + errMsg := c.Errors.String() + t.SetSpanError(span, fmt.Errorf(errMsg)) + } + } +} + +// GinTraceMiddleware 兼容旧的方法名,保持向后兼容 +func (t *Tracer) GinTraceMiddleware() gin.HandlerFunc { + return t.TraceMiddleware() +} + +// WithTracing 添加追踪到上下文的辅助函数 +func WithTracing(ctx context.Context, tracer *Tracer, name string) (context.Context, trace.Span) { + return tracer.StartSpan(ctx, name) +} + +// TraceFunction 追踪函数执行的辅助函数 +func (t *Tracer) TraceFunction(ctx context.Context, name string, fn func(context.Context) error) error { + ctx, span := t.StartSpan(ctx, name) + defer span.End() + + err := fn(ctx) + if err != nil { + t.SetSpanError(span, err) + } else { + t.SetSpanSuccess(span) + } + + return err +} + +// TraceFunctionWithResult 追踪带返回值的函数执行 +func TraceFunctionWithResult[T any](ctx context.Context, tracer *Tracer, name string, fn func(context.Context) (T, error)) (T, error) { + ctx, span := tracer.StartSpan(ctx, name) + defer span.End() + + result, err := fn(ctx) + if err != nil { + tracer.SetSpanError(span, err) + } else { + tracer.SetSpanSuccess(span) + } + + return result, err +} diff --git a/scripts/deploy.ps1 b/scripts/deploy.ps1 new file mode 100644 index 0000000..5cddd3f --- /dev/null +++ b/scripts/deploy.ps1 @@ -0,0 +1,255 @@ +# TYAPI 生产环境部署脚本 (PowerShell版本) +# 使用方法: .\scripts\deploy.ps1 [版本号] + +param( + [string]$Version = "latest" +) + +# 配置 +$REGISTRY_URL = "docker-registry.tianyuanapi.com" +$IMAGE_NAME = "tyapi-server" +$APP_VERSION = $Version +$BUILD_TIME = (Get-Date -Format "yyyy-MM-ddTHH:mm:ssZ") + +try { + $GIT_COMMIT = git rev-parse --short HEAD 2>$null + if (-not $GIT_COMMIT) { $GIT_COMMIT = "dev" } +} +catch { + $GIT_COMMIT = "dev" +} + +# 颜色输出函数 +function Write-Info($message) { + Write-Host "[INFO] $message" -ForegroundColor Blue +} + +function Write-Success($message) { + Write-Host "[SUCCESS] $message" -ForegroundColor Green +} + +function Write-Warning($message) { + Write-Host "[WARNING] $message" -ForegroundColor Yellow +} + +function Write-Error($message) { + Write-Host "[ERROR] $message" -ForegroundColor Red +} + +# 检查必要工具 +function Test-Requirements { + Write-Info "检查部署环境..." + + if (-not (Get-Command docker -ErrorAction SilentlyContinue)) { + Write-Error "Docker 未安装或不在 PATH 中" + exit 1 + } + + if (-not (Get-Command docker-compose -ErrorAction SilentlyContinue)) { + Write-Error "docker-compose 未安装或不在 PATH 中" + exit 1 + } + + if (-not (Get-Command git -ErrorAction SilentlyContinue)) { + Write-Warning "Git 未安装,将使用默认提交哈希" + } + + Write-Success "环境检查通过" +} + +# 构建 Docker 镜像 +function Build-Image { + Write-Info "开始构建 Docker 镜像..." + + docker build ` + --build-arg VERSION="$APP_VERSION" ` + --build-arg COMMIT="$GIT_COMMIT" ` + --build-arg BUILD_TIME="$BUILD_TIME" ` + -t "$REGISTRY_URL/$IMAGE_NAME`:$APP_VERSION" ` + -t "$REGISTRY_URL/$IMAGE_NAME`:latest" ` + . + + if ($LASTEXITCODE -ne 0) { + Write-Error "Docker 镜像构建失败" + exit 1 + } + + Write-Success "Docker 镜像构建完成" +} + +# 推送镜像到私有仓库 +function Push-Image { + Write-Info "推送镜像到私有仓库..." + + # 推送版本标签 + docker push "$REGISTRY_URL/$IMAGE_NAME`:$APP_VERSION" + if ($LASTEXITCODE -eq 0) { + Write-Success "已推送版本标签: $APP_VERSION" + } + else { + Write-Error "推送版本标签失败" + exit 1 + } + + # 推送latest标签 + docker push "$REGISTRY_URL/$IMAGE_NAME`:latest" + if ($LASTEXITCODE -eq 0) { + Write-Success "已推送latest标签" + } + else { + Write-Error "推送latest标签失败" + exit 1 + } +} + +# 准备生产环境配置 +function Test-Config { + Write-Info "准备生产环境配置..." + + # 检查.env文件是否存在 + if (-not (Test-Path ".env")) { + if (Test-Path ".env.production") { + Write-Warning ".env文件不存在,正在复制模板..." + Copy-Item ".env.production" ".env" + Write-Warning "请编辑 .env 文件并设置正确的配置值" + exit 1 + } + else { + Write-Error "配置文件 .env 和 .env.production 都不存在" + exit 1 + } + } + + # 验证关键配置 + $envContent = Get-Content ".env" -Raw + if (-not ($envContent -match "^DB_PASSWORD=" -and -not ($envContent -match "your_secure_database_password_here"))) { + Write-Error "请在 .env 文件中设置安全的数据库密码" + exit 1 + } + + if (-not ($envContent -match "^JWT_SECRET=" -and -not ($envContent -match "your_super_secure_jwt_secret"))) { + Write-Error "请在 .env 文件中设置安全的JWT密钥" + exit 1 + } + + Write-Success "配置检查通过" +} + +# 部署到生产环境 +function Start-Deploy { + Write-Info "开始部署到生产环境..." + + # 设置版本环境变量 + $env:APP_VERSION = $APP_VERSION + + # 停止现有服务 + Write-Info "停止现有服务..." + docker-compose -f docker-compose.prod.yml down --remove-orphans + + # 清理未使用的镜像 + Write-Info "清理未使用的Docker资源..." + docker image prune -f + + # 拉取最新镜像 + Write-Info "拉取最新镜像..." + docker-compose -f docker-compose.prod.yml pull + + # 启动服务 + Write-Info "启动生产环境服务..." + docker-compose -f docker-compose.prod.yml up -d + + if ($LASTEXITCODE -ne 0) { + Write-Error "服务启动失败" + exit 1 + } + + # 等待服务启动 + Write-Info "等待服务启动..." + Start-Sleep -Seconds 30 + + # 检查服务状态 + Write-Info "检查服务状态..." + docker-compose -f docker-compose.prod.yml ps + + # 健康检查 + Write-Info "执行健康检查..." + $maxAttempts = 10 + $attempt = 0 + + while ($attempt -lt $maxAttempts) { + try { + $response = Invoke-WebRequest -Uri "http://localhost:8080/health" -TimeoutSec 5 -ErrorAction Stop + if ($response.StatusCode -eq 200) { + Write-Success "应用健康检查通过" + break + } + } + catch { + $attempt++ + Write-Info "健康检查失败,重试 $attempt/$maxAttempts..." + Start-Sleep -Seconds 10 + } + } + + if ($attempt -eq $maxAttempts) { + Write-Error "应用健康检查失败,请检查日志" + docker-compose -f docker-compose.prod.yml logs tyapi-app + exit 1 + } + + Write-Success "部署完成!" +} + +# 显示部署信息 +function Show-Info { + Write-Info "部署信息:" + Write-Host " 版本: $APP_VERSION" + Write-Host " 提交: $GIT_COMMIT" + Write-Host " 构建时间: $BUILD_TIME" + Write-Host " 镜像: $REGISTRY_URL/$IMAGE_NAME`:$APP_VERSION" + Write-Host "" + Write-Host "🌐 服务访问地址:" + Write-Host " 📱 API服务: http://localhost:8080" + Write-Host " 📚 API文档: http://localhost:8080/swagger/index.html" + Write-Host " 💚 健康检查: http://localhost:8080/health" + Write-Host "" + Write-Host "📊 监控和追踪:" + Write-Host " 📈 Grafana仪表盘: http://localhost:3000" + Write-Host " 🔍 Prometheus监控: http://localhost:9090" + Write-Host " 🔗 Jaeger链路追踪: http://localhost:16686" + Write-Host "" + Write-Host "🛠 管理工具:" + Write-Host " 🗄️ pgAdmin数据库: http://localhost:5050" + Write-Host " 📦 MinIO对象存储: http://localhost:9000" + Write-Host " 🎛️ MinIO控制台: http://localhost:9001" + Write-Host "" + Write-Host "🔧 管理命令:" + Write-Host " 查看日志: docker-compose -f docker-compose.prod.yml logs -f" + Write-Host " 停止服务: docker-compose -f docker-compose.prod.yml down" + Write-Host " 查看状态: docker-compose -f docker-compose.prod.yml ps" + Write-Host " 重启应用: docker-compose -f docker-compose.prod.yml restart tyapi-app" +} + +# 主函数 +function Main { + Write-Info "开始 TYAPI 生产环境部署..." + Write-Info "版本: $APP_VERSION" + + Test-Requirements + Test-Config + Build-Image + Push-Image + Start-Deploy + Show-Info + + Write-Success "🎉 部署成功!" +} + +# 运行主函数 +try { + Main +} +catch { + Write-Error "部署过程中发生错误: $($_.Exception.Message)" + exit 1 +} \ No newline at end of file diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100644 index 0000000..4535aba --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,221 @@ +#!/bin/bash + +# TYAPI 生产环境部署脚本 +# 使用方法: ./scripts/deploy.sh [version] + +set -e + +# 配置 +REGISTRY_URL="docker-registry.tianyuanapi.com" +IMAGE_NAME="tyapi-server" +APP_VERSION=${1:-latest} +BUILD_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") +GIT_COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo 'dev') + +# 颜色输出 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# 日志函数 +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +log_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# 检查必要工具 +check_requirements() { + log_info "检查部署环境..." + + if ! command -v docker &> /dev/null; then + log_error "Docker 未安装或不在 PATH 中" + exit 1 + fi + + if ! command -v docker-compose &> /dev/null; then + log_error "docker-compose 未安装或不在 PATH 中" + exit 1 + fi + + if ! command -v git &> /dev/null; then + log_warning "Git 未安装,将使用默认提交哈希" + fi + + log_success "环境检查通过" +} + +# 构建 Docker 镜像 +build_image() { + log_info "开始构建 Docker 镜像..." + + docker build \ + --build-arg VERSION="$APP_VERSION" \ + --build-arg COMMIT="$GIT_COMMIT" \ + --build-arg BUILD_TIME="$BUILD_TIME" \ + -t "$REGISTRY_URL/$IMAGE_NAME:$APP_VERSION" \ + -t "$REGISTRY_URL/$IMAGE_NAME:latest" \ + . + + log_success "Docker 镜像构建完成" +} + +# 推送镜像到私有仓库 +push_image() { + log_info "推送镜像到私有仓库..." + + # 推送版本标签 + docker push "$REGISTRY_URL/$IMAGE_NAME:$APP_VERSION" + log_success "已推送版本标签: $APP_VERSION" + + # 推送latest标签 + docker push "$REGISTRY_URL/$IMAGE_NAME:latest" + log_success "已推送latest标签" +} + +# 准备生产环境配置 +prepare_config() { + log_info "准备生产环境配置..." + + # 检查.env文件是否存在 + if [ ! -f ".env" ]; then + if [ -f ".env.production" ]; then + log_warning ".env文件不存在,正在复制模板..." + cp .env.production .env + log_warning "请编辑 .env 文件并设置正确的配置值" + exit 1 + else + log_error "配置文件 .env 和 .env.production 都不存在" + exit 1 + fi + fi + + # 验证关键配置 + if ! grep -q "^DB_PASSWORD=" .env || grep -q "your_secure_database_password_here" .env; then + log_error "请在 .env 文件中设置安全的数据库密码" + exit 1 + fi + + if ! grep -q "^JWT_SECRET=" .env || grep -q "your_super_secure_jwt_secret" .env; then + log_error "请在 .env 文件中设置安全的JWT密钥" + exit 1 + fi + + log_success "配置检查通过" +} + +# 部署到生产环境 +deploy() { + log_info "开始部署到生产环境..." + + # 设置版本环境变量 + export APP_VERSION="$APP_VERSION" + + # 停止现有服务 + log_info "停止现有服务..." + docker-compose -f docker-compose.prod.yml down --remove-orphans + + # 清理未使用的镜像 + log_info "清理未使用的Docker资源..." + docker image prune -f + + # 拉取最新镜像 + log_info "拉取最新镜像..." + docker-compose -f docker-compose.prod.yml pull + + # 启动服务 + log_info "启动生产环境服务..." + docker-compose -f docker-compose.prod.yml up -d + + # 等待服务启动 + log_info "等待服务启动..." + sleep 30 + + # 检查服务状态 + log_info "检查服务状态..." + docker-compose -f docker-compose.prod.yml ps + + # 健康检查 + log_info "执行健康检查..." + max_attempts=10 + attempt=0 + + while [ $attempt -lt $max_attempts ]; do + if curl -f http://localhost:8080/health > /dev/null 2>&1; then + log_success "应用健康检查通过" + break + else + attempt=$((attempt + 1)) + log_info "健康检查失败,重试 $attempt/$max_attempts..." + sleep 10 + fi + done + + if [ $attempt -eq $max_attempts ]; then + log_error "应用健康检查失败,请检查日志" + docker-compose -f docker-compose.prod.yml logs tyapi-app + exit 1 + fi + + log_success "部署完成!" +} + +# 显示部署信息 +show_info() { + log_info "部署信息:" + echo " 版本: $APP_VERSION" + echo " 提交: $GIT_COMMIT" + echo " 构建时间: $BUILD_TIME" + echo " 镜像: $REGISTRY_URL/$IMAGE_NAME:$APP_VERSION" + echo "" + echo "🌐 服务访问地址:" + echo " 📱 API服务: http://localhost:8080" + echo " 📚 API文档: http://localhost:8080/swagger/index.html" + echo " 💚 健康检查: http://localhost:8080/health" + echo "" + echo "📊 监控和追踪:" + echo " 📈 Grafana仪表盘: http://localhost:3000" + echo " 🔍 Prometheus监控: http://localhost:9090" + echo " 🔗 Jaeger链路追踪: http://localhost:16686" + echo "" + echo "🛠 管理工具:" + echo " 🗄️ pgAdmin数据库: http://localhost:5050" + echo " 📦 MinIO对象存储: http://localhost:9000" + echo " 🎛️ MinIO控制台: http://localhost:9001" + echo "" + echo "🔧 管理命令:" + echo " 查看日志: docker-compose -f docker-compose.prod.yml logs -f" + echo " 停止服务: docker-compose -f docker-compose.prod.yml down" + echo " 查看状态: docker-compose -f docker-compose.prod.yml ps" + echo " 重启应用: docker-compose -f docker-compose.prod.yml restart tyapi-app" +} + +# 主函数 +main() { + log_info "开始 TYAPI 生产环境部署..." + log_info "版本: $APP_VERSION" + + check_requirements + prepare_config + build_image + push_image + deploy + show_info + + log_success "🎉 部署成功!" +} + +# 运行主函数 +main "$@" \ No newline at end of file diff --git a/scripts/init.sql b/scripts/init.sql index febe8fe..82fb120 100644 --- a/scripts/init.sql +++ b/scripts/init.sql @@ -2,10 +2,10 @@ -- This script runs when PostgreSQL container starts for the first time -- Create development database if it doesn't exist -CREATE DATABASE tyapi_dev; +-- Note: tyapi_dev is already created by POSTGRES_DB environment variable -- Create test database for running tests -CREATE DATABASE tyapi_test; +-- Note: Skip database creation in init script, handle in application if needed -- Create production database (for reference) -- CREATE DATABASE tyapi_prod; @@ -30,25 +30,11 @@ CREATE SCHEMA IF NOT EXISTS metrics; -- Set search path SET search_path TO public, logs, metrics; --- Connect to test database and setup extensions -\c tyapi_test; +-- Test database setup will be handled by application migrations +-- when needed, since we don't create it in this init script -CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; - -CREATE EXTENSION IF NOT EXISTS "pg_trgm"; - -CREATE EXTENSION IF NOT EXISTS "btree_gin"; - -CREATE SCHEMA IF NOT EXISTS public; - -CREATE SCHEMA IF NOT EXISTS logs; - -CREATE SCHEMA IF NOT EXISTS metrics; - -SET search_path TO public, logs, metrics; - --- Switch back to development database -\c tyapi_dev; +-- Continue with development database setup +-- (already connected to tyapi_dev) -- Create application-specific roles (optional) -- CREATE ROLE tyapi_app WITH LOGIN PASSWORD 'app_password'; @@ -63,9 +49,7 @@ SET search_path TO public, logs, metrics; -- This will be replaced by proper migrations in the application -- Log the initialization -INSERT INTO - pg_stat_statements_info (dealloc) -VALUES (0) ON CONFLICT DO NOTHING; +-- Note: pg_stat_statements extension may not be available, skip this insert -- Create a simple health check function CREATE OR REPLACE FUNCTION health_check()