From 03e615a8fdda0150a23ca929151d441475bd4978 Mon Sep 17 00:00:00 2001 From: liangzai <2440983361@qq.com> Date: Mon, 30 Jun 2025 19:21:56 +0800 Subject: [PATCH] Initial commit: Basic project structure and dependencies --- .cursorignore | 1 + .gitignore | 66 ++ COMPLETE_ARCHITECTURE_PLAN.md | 811 ++++++++++++++++++ Makefile | 246 ++++++ README.md | 334 ++++++++ cmd/api/main.go | 110 +++ config.prod.yaml | 91 ++ config.yaml | 91 ++ deployments/docker/redis.conf | 104 +++ docker-compose.dev.yml | 152 ++++ docs/API使用指南.md | 316 +++++++ docs/ARCHITECTURE.md | 469 ++++++++++ docs/开发指南.md | 279 ++++++ docs/快速开始指南.md | 56 ++ docs/故障排除指南.md | 404 +++++++++ docs/文档索引.md | 102 +++ docs/最佳实践指南.md | 536 ++++++++++++ docs/环境搭建指南.md | 127 +++ docs/部署指南.md | 476 ++++++++++ env.example | 137 +++ go.mod | 64 ++ go.sum | 155 ++++ internal/app/app.go | 235 +++++ internal/config/config.go | 166 ++++ internal/config/loader.go | 311 +++++++ internal/container/container.go | 441 ++++++++++ internal/domains/user/dto/user_dto.go | 173 ++++ internal/domains/user/entities/user.go | 138 +++ internal/domains/user/events/user_events.go | 299 +++++++ .../domains/user/handlers/user_handler.go | 455 ++++++++++ .../user/repositories/user_repository.go | 339 ++++++++ internal/domains/user/routes/user_routes.go | 133 +++ .../domains/user/services/user_service.go | 469 ++++++++++ internal/shared/cache/redis_cache.go | 284 ++++++ internal/shared/database/database.go | 195 +++++ internal/shared/events/event_bus.go | 313 +++++++ internal/shared/health/health_checker.go | 282 ++++++ internal/shared/http/response.go | 260 ++++++ internal/shared/http/router.go | 258 ++++++ internal/shared/http/validator.go | 273 ++++++ internal/shared/interfaces/event.go | 92 ++ internal/shared/interfaces/http.go | 152 ++++ internal/shared/interfaces/repository.go | 74 ++ internal/shared/interfaces/service.go | 101 +++ internal/shared/logger/logger.go | 241 ++++++ internal/shared/middleware/auth.go | 261 ++++++ internal/shared/middleware/cors.go | 104 +++ internal/shared/middleware/ratelimit.go | 166 ++++ internal/shared/middleware/request_logger.go | 241 ++++++ scripts/init.sql | 81 ++ 50 files changed, 11664 insertions(+) create mode 100644 .cursorignore create mode 100644 .gitignore create mode 100644 COMPLETE_ARCHITECTURE_PLAN.md create mode 100644 Makefile create mode 100644 README.md create mode 100644 cmd/api/main.go create mode 100644 config.prod.yaml create mode 100644 config.yaml create mode 100644 deployments/docker/redis.conf create mode 100644 docker-compose.dev.yml create mode 100644 docs/API使用指南.md create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/开发指南.md create mode 100644 docs/快速开始指南.md create mode 100644 docs/故障排除指南.md create mode 100644 docs/文档索引.md create mode 100644 docs/最佳实践指南.md create mode 100644 docs/环境搭建指南.md create mode 100644 docs/部署指南.md create mode 100644 env.example create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/app/app.go create mode 100644 internal/config/config.go create mode 100644 internal/config/loader.go create mode 100644 internal/container/container.go create mode 100644 internal/domains/user/dto/user_dto.go create mode 100644 internal/domains/user/entities/user.go create mode 100644 internal/domains/user/events/user_events.go create mode 100644 internal/domains/user/handlers/user_handler.go create mode 100644 internal/domains/user/repositories/user_repository.go create mode 100644 internal/domains/user/routes/user_routes.go create mode 100644 internal/domains/user/services/user_service.go create mode 100644 internal/shared/cache/redis_cache.go create mode 100644 internal/shared/database/database.go create mode 100644 internal/shared/events/event_bus.go create mode 100644 internal/shared/health/health_checker.go create mode 100644 internal/shared/http/response.go create mode 100644 internal/shared/http/router.go create mode 100644 internal/shared/http/validator.go create mode 100644 internal/shared/interfaces/event.go create mode 100644 internal/shared/interfaces/http.go create mode 100644 internal/shared/interfaces/repository.go create mode 100644 internal/shared/interfaces/service.go create mode 100644 internal/shared/logger/logger.go create mode 100644 internal/shared/middleware/auth.go create mode 100644 internal/shared/middleware/cors.go create mode 100644 internal/shared/middleware/ratelimit.go create mode 100644 internal/shared/middleware/request_logger.go create mode 100644 scripts/init.sql diff --git a/.cursorignore b/.cursorignore new file mode 100644 index 0000000..6f9f00f --- /dev/null +++ b/.cursorignore @@ -0,0 +1 @@ +# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b80c85c --- /dev/null +++ b/.gitignore @@ -0,0 +1,66 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +vendor/ + +# Go workspace file +go.work + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Local environment files +.env +.env.local +.env.*.local + +# Log files +*.log +logs/ + +# Database +*.db +*.sqlite +*.sqlite3 + +# Temporary files +tmp/ +temp/ + +# Build directories +build/ +dist/ + +# Air live reload +tmp/ + +# Compiled binary +main +tyapi-server + +# Docker volumes +docker-data/ \ No newline at end of file diff --git a/COMPLETE_ARCHITECTURE_PLAN.md b/COMPLETE_ARCHITECTURE_PLAN.md new file mode 100644 index 0000000..992db91 --- /dev/null +++ b/COMPLETE_ARCHITECTURE_PLAN.md @@ -0,0 +1,811 @@ +# 🏗️ 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/Makefile b/Makefile new file mode 100644 index 0000000..f173bbb --- /dev/null +++ b/Makefile @@ -0,0 +1,246 @@ +# TYAPI Server Makefile + +# 应用信息 +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}') + +# 构建参数 +LDFLAGS := -ldflags "-X main.version=$(VERSION) -X main.commit=$(GIT_COMMIT) -X main.date=$(BUILD_TIME)" +BUILD_DIR := bin +MAIN_PATH := cmd/api/main.go + +# Go 相关 +GOCMD := go +GOBUILD := $(GOCMD) build +GOCLEAN := $(GOCMD) clean +GOTEST := $(GOCMD) test +GOGET := $(GOCMD) get +GOMOD := $(GOCMD) mod +GOFMT := $(GOCMD) fmt + +# Docker 相关 +DOCKER_IMAGE := $(APP_NAME):$(VERSION) +DOCKER_LATEST := $(APP_NAME):latest + +# 默认目标 +.DEFAULT_GOAL := help + +## 显示帮助信息 +help: + @echo "TYAPI Server Makefile" + @echo "" + @echo "使用方法: make [目标]" + @echo "" + @echo "可用目标:" + @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " %-20s %s\n", $$1, $$2}' $(MAKEFILE_LIST) + +## 安装依赖 +deps: + @echo "安装依赖..." + $(GOMOD) download + $(GOMOD) tidy + +## 代码格式化 +fmt: + @echo "格式化代码..." + $(GOFMT) ./... + +## 代码检查 +lint: + @echo "代码检查..." + @if command -v golangci-lint >/dev/null 2>&1; then \ + golangci-lint run; \ + else \ + echo "golangci-lint 未安装,跳过代码检查"; \ + fi + +## 运行测试 +test: + @echo "运行测试..." + $(GOTEST) -v -race -coverprofile=coverage.out ./... + +## 生成测试覆盖率报告 +coverage: test + @echo "生成覆盖率报告..." + $(GOCMD) tool cover -html=coverage.out -o coverage.html + @echo "覆盖率报告已生成: coverage.html" + +## 构建应用 (开发环境) +build: + @echo "构建应用..." + @mkdir -p $(BUILD_DIR) + $(GOBUILD) $(LDFLAGS) -o $(BUILD_DIR)/$(APP_NAME) $(MAIN_PATH) + +## 构建生产版本 +build-prod: + @echo "构建生产版本..." + @mkdir -p $(BUILD_DIR) + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GOBUILD) $(LDFLAGS) -a -installsuffix cgo -o $(BUILD_DIR)/$(APP_NAME)-linux-amd64 $(MAIN_PATH) + +## 交叉编译 +build-all: + @echo "交叉编译..." + @mkdir -p $(BUILD_DIR) + # Linux AMD64 + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GOBUILD) $(LDFLAGS) -o $(BUILD_DIR)/$(APP_NAME)-linux-amd64 $(MAIN_PATH) + # Linux ARM64 + CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(GOBUILD) $(LDFLAGS) -o $(BUILD_DIR)/$(APP_NAME)-linux-arm64 $(MAIN_PATH) + # macOS AMD64 + CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 $(GOBUILD) $(LDFLAGS) -o $(BUILD_DIR)/$(APP_NAME)-darwin-amd64 $(MAIN_PATH) + # macOS ARM64 + CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 $(GOBUILD) $(LDFLAGS) -o $(BUILD_DIR)/$(APP_NAME)-darwin-arm64 $(MAIN_PATH) + # Windows AMD64 + CGO_ENABLED=0 GOOS=windows GOARCH=amd64 $(GOBUILD) $(LDFLAGS) -o $(BUILD_DIR)/$(APP_NAME)-windows-amd64.exe $(MAIN_PATH) + +## 运行应用 +run: build + @echo "启动应用..." + ./$(BUILD_DIR)/$(APP_NAME) + +## 开发模式运行 (热重载) +dev: + @echo "开发模式启动..." + @if command -v air >/dev/null 2>&1; then \ + air; \ + else \ + echo "air 未安装,使用普通模式运行..."; \ + $(GOCMD) run $(MAIN_PATH); \ + fi + +## 运行数据库迁移 +migrate: build + @echo "运行数据库迁移..." + ./$(BUILD_DIR)/$(APP_NAME) -migrate + +## 显示版本信息 +version: build + @echo "版本信息:" + ./$(BUILD_DIR)/$(APP_NAME) -version + +## 健康检查 +health: build + @echo "执行健康检查..." + ./$(BUILD_DIR)/$(APP_NAME) -health + +## 清理构建文件 +clean: + @echo "清理构建文件..." + $(GOCLEAN) + rm -rf $(BUILD_DIR) + rm -f coverage.out coverage.html + +## 创建 .env 文件 +env: + @if [ ! -f .env ]; then \ + echo "创建 .env 文件..."; \ + cp env.example .env; \ + echo ".env 文件已创建,请根据需要修改配置"; \ + else \ + echo ".env 文件已存在"; \ + fi + +## 设置开发环境 +setup: deps env + @echo "设置开发环境..." + @echo "1. 依赖已安装" + @echo "2. .env 文件已创建" + @echo "3. 请确保 PostgreSQL 和 Redis 正在运行" + @echo "4. 运行 'make migrate' 创建数据库表" + @echo "5. 运行 'make dev' 启动开发服务器" + +## 构建 Docker 镜像 +docker-build: + @echo "构建 Docker 镜像..." + docker build -t $(DOCKER_IMAGE) -t $(DOCKER_LATEST) . + +## 运行 Docker 容器 +docker-run: + @echo "运行 Docker 容器..." + docker run -d --name $(APP_NAME) -p 8080:8080 --env-file .env $(DOCKER_LATEST) + +## 停止 Docker 容器 +docker-stop: + @echo "停止 Docker 容器..." + 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) +services-up: + @echo "启动开发依赖服务..." + @if [ -f docker-compose.dev.yml ]; then \ + docker-compose -f docker-compose.dev.yml up -d; \ + else \ + echo "docker-compose.dev.yml 不存在"; \ + fi + +## 停止开发依赖服务 +services-down: + @echo "停止开发依赖服务..." + @if [ -f docker-compose.dev.yml ]; then \ + docker-compose -f docker-compose.dev.yml down; \ + else \ + echo "docker-compose.dev.yml 不存在"; \ + fi + +## 查看服务日志 +logs: + @echo "查看应用日志..." + @if [ -f logs/app.log ]; then \ + tail -f logs/app.log; \ + else \ + echo "日志文件不存在"; \ + fi + +## 生成 API 文档 +docs: + @echo "生成 API 文档..." + @if command -v swag >/dev/null 2>&1; then \ + swag init -g $(MAIN_PATH) -o docs/swagger; \ + else \ + echo "swag 未安装,跳过文档生成"; \ + fi + +## 性能测试 +bench: + @echo "运行性能测试..." + $(GOTEST) -bench=. -benchmem ./... + +## 内存泄漏检测 +race: + @echo "运行竞态条件检测..." + $(GOTEST) -race ./... + +## 安全扫描 +security: + @echo "运行安全扫描..." + @if command -v gosec >/dev/null 2>&1; then \ + gosec ./...; \ + else \ + echo "gosec 未安装,跳过安全扫描"; \ + fi + +## 生成模拟数据 +mock: + @echo "生成模拟数据..." + @if command -v mockgen >/dev/null 2>&1; then \ + echo "生成模拟数据..."; \ + else \ + echo "mockgen 未安装,请先安装: go install github.com/golang/mock/mockgen@latest"; \ + fi + +## 完整的 CI 流程 +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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..b679965 --- /dev/null +++ b/README.md @@ -0,0 +1,334 @@ +# TYAPI Server + +## 🚀 2025 年最前沿的 Go Web 架构系统 + +TYAPI Server 是一个基于 Go 语言和 Gin 框架构建的现代化、高性能、模块化的 Web API 服务器。采用领域驱动设计(DDD)、CQRS、事件驱动架构等先进设计模式,为企业级应用提供坚实的技术基础。 + +## ✨ 核心特性 + +### 🏗️ 架构特性 + +- **领域驱动设计(DDD)**: 清晰的业务边界和模型隔离 +- **CQRS 模式**: 命令查询责任分离,优化读写性能 +- **事件驱动**: 基于事件的异步处理和系统解耦 +- **依赖注入**: 基于 Uber FX 的完整 IoC 容器 +- **模块化设计**: 高内聚、低耦合的组件架构 + +### 🔧 技术栈 + +- **Web 框架**: Gin (高性能 HTTP 路由) +- **ORM**: GORM (功能强大的对象关系映射) +- **数据库**: PostgreSQL (主数据库) + Redis (缓存) +- **日志**: Zap (结构化高性能日志) +- **配置**: Viper (多格式配置管理) +- **监控**: Prometheus + Grafana + Jaeger +- **依赖注入**: Uber FX + +### 🛡️ 生产就绪特性 + +- **安全性**: JWT 认证、CORS、安全头部、输入验证 +- **性能**: 智能缓存、连接池、限流、压缩 +- **可观测性**: 链路追踪、指标监控、结构化日志 +- **容错性**: 熔断器、重试机制、优雅降级 +- **运维**: 健康检查、优雅关闭、Docker 化部署 + +## 📁 项目结构 + +``` +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 # 项目说明 +``` + +## 🚀 快速开始 + +### 环境要求 + +- Go 1.23.4+ +- PostgreSQL 12+ +- Redis 6+ +- Docker & Docker Compose (可选) + +### 1. 克隆项目 + +```bash +git clone +cd tyapi-server-gin +``` + +### 2. 安装依赖 + +```bash +make deps +``` + +### 3. 配置环境 + +```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` - 系统信息 + +### 配置说明 + +主要配置项说明: + +```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" + +# JWT配置 +jwt: + secret: "your-secret-key" + expires_in: 24h +``` + +## 🏗️ 架构说明 + +### 领域驱动设计 + +项目采用 DDD 架构模式,每个业务域包含: + +- **Entities**: 业务实体,包含业务逻辑 +- **DTOs**: 数据传输对象,用于 API 交互 +- **Services**: 业务服务,协调实体完成业务操作 +- **Repositories**: 仓储模式,抽象数据访问 +- **Events**: 域事件,实现模块间解耦 + +### 事件驱动架构 + +- **事件总线**: 异步事件分发机制 +- **事件处理器**: 响应特定事件的处理逻辑 +- **事件存储**: 事件溯源和审计日志 + +### 中间件系统 + +- **认证中间件**: JWT token 验证 +- **限流中间件**: API 调用频率控制 +- **日志中间件**: 请求/响应日志记录 +- **CORS 中间件**: 跨域请求处理 +- **安全中间件**: 安全头部设置 + +## 📊 监控和运维 + +### 健康检查 + +```bash +# 应用健康检查 +curl http://localhost:8080/health + +# 系统信息 +curl http://localhost:8080/info +``` + +### 指标监控 + +- **Prometheus**: `http://localhost:9090` +- **Grafana**: `http://localhost:3000` (admin/admin) +- **Jaeger**: `http://localhost:16686` + +### 日志管理 + +结构化 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/cmd/api/main.go b/cmd/api/main.go new file mode 100644 index 0000000..ef9acbb --- /dev/null +++ b/cmd/api/main.go @@ -0,0 +1,110 @@ +package main + +import ( + "flag" + "fmt" + "log" + "os" + + "tyapi-server/internal/app" +) + +var ( + // 版本信息 + version = "1.0.0" + commit = "unknown" + 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)") + ) + flag.Parse() + + // 显示版本信息 + if *showVersion { + printVersion() + return + } + + // 创建应用程序实例 + application, err := app.NewApplication() + if err != nil { + 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 { + log.Fatalf("Migration failed: %v", err) + } + fmt.Println("Migration completed successfully") + return + } + + // 执行健康检查 + if *healthCheck { + if err := application.HealthCheck(); err != nil { + log.Fatalf("Health check failed: %v", err) + } + fmt.Println("Health check passed") + return + } + + // 默认:启动应用程序服务器 + logger := application.GetLogger() + logger.Info("Starting TYAPI Server...") + + 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") + } +} diff --git a/config.prod.yaml b/config.prod.yaml new file mode 100644 index 0000000..bdf2572 --- /dev/null +++ b/config.prod.yaml @@ -0,0 +1,91 @@ +# 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 new file mode 100644 index 0000000..6a2cece --- /dev/null +++ b/config.yaml @@ -0,0 +1,91 @@ +# TYAPI Server Configuration + +app: + name: "TYAPI Server" + version: "1.0.0" + env: "development" + +server: + host: "0.0.0.0" + port: "8080" + mode: "debug" + read_timeout: 30s + write_timeout: 30s + idle_timeout: 120s + +database: + host: "localhost" + port: "5432" + user: "postgres" + password: "Pg9mX4kL8nW2rT5y" + name: "tyapi_dev" + sslmode: "disable" + timezone: "Asia/Shanghai" + max_open_conns: 25 + max_idle_conns: 10 + conn_max_lifetime: 300s + +redis: + host: "localhost" + port: "6379" + password: "" + db: 0 + pool_size: 10 + min_idle_conns: 3 + max_retries: 3 + dial_timeout: 5s + read_timeout: 3s + write_timeout: 3s + +cache: + default_ttl: 3600s + cleanup_interval: 600s + max_size: 1000 + +logger: + level: "info" + format: "json" + output: "stdout" + file_path: "logs/app.log" + max_size: 100 + max_backups: 3 + max_age: 7 + compress: true + +jwt: + secret: "JwT8xR4mN9vP2sL7kH3oB6yC1zA5uF0qE9tW" + expires_in: 24h + refresh_expires_in: 168h # 7 days + +ratelimit: + requests: 100 + window: 60s + burst: 50 + +monitoring: + metrics_enabled: true + metrics_port: "9090" + tracing_enabled: false + tracing_endpoint: "http://localhost:14268/api/traces" + sample_rate: 0.1 + +health: + enabled: true + interval: 30s + timeout: 10s + +resilience: + circuit_breaker_enabled: true + circuit_breaker_threshold: 5 + circuit_breaker_timeout: 60s + retry_max_attempts: 3 + retry_initial_delay: 100ms + retry_max_delay: 5s + +development: + 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" diff --git a/deployments/docker/redis.conf b/deployments/docker/redis.conf new file mode 100644 index 0000000..d9ed7fa --- /dev/null +++ b/deployments/docker/redis.conf @@ -0,0 +1,104 @@ +# Redis Configuration for TYAPI Server Development + +# Network +bind 0.0.0.0 +port 6379 +timeout 0 +tcp-keepalive 300 + +# General +daemonize no +supervised no +pidfile /var/run/redis_6379.pid +loglevel notice +logfile "" +databases 16 + +# Snapshotting +save 900 1 +save 300 10 +save 60 10000 +stop-writes-on-bgsave-error yes +rdbcompression yes +rdbchecksum yes +dbfilename dump.rdb +dir ./ + +# Replication +# slaveof +# masterauth +slave-serve-stale-data yes +slave-read-only yes +repl-diskless-sync no +repl-diskless-sync-delay 5 +repl-ping-slave-period 10 +repl-timeout 60 +repl-disable-tcp-nodelay no +repl-backlog-size 1mb +repl-backlog-ttl 3600 +slave-priority 100 + +# Security +# requirepass foobared +# rename-command FLUSHDB "" +# rename-command FLUSHALL "" + +# Limits +maxclients 10000 +maxmemory 256mb +maxmemory-policy allkeys-lru +maxmemory-samples 5 + +# Append only file +appendonly yes +appendfilename "appendonly.aof" +appendfsync everysec +no-appendfsync-on-rewrite no +auto-aof-rewrite-percentage 100 +auto-aof-rewrite-min-size 64mb +aof-load-truncated yes + +# Lua scripting +lua-time-limit 5000 + +# Slow log +slowlog-log-slower-than 10000 +slowlog-max-len 128 + +# Latency monitor +latency-monitor-threshold 100 + +# Event notification +notify-keyspace-events Ex + +# Hash +hash-max-ziplist-entries 512 +hash-max-ziplist-value 64 + +# List +list-max-ziplist-size -2 +list-compress-depth 0 + +# Set +set-max-intset-entries 512 + +# Sorted set +zset-max-ziplist-entries 128 +zset-max-ziplist-value 64 + +# HyperLogLog +hll-sparse-max-bytes 3000 + +# Active rehashing +activerehashing yes + +# Client output buffer limits +client-output-buffer-limit normal 0 0 0 +client-output-buffer-limit slave 256mb 64mb 60 +client-output-buffer-limit pubsub 32mb 8mb 60 + +# Hz +hz 10 + +# AOF rewrite +aof-rewrite-incremental-fsync yes \ No newline at end of file diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..ead963c --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,152 @@ +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 + + # 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: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: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: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: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: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 + +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 + +networks: + tyapi-network: + driver: bridge \ No newline at end of file diff --git a/docs/API使用指南.md b/docs/API使用指南.md new file mode 100644 index 0000000..627557f --- /dev/null +++ b/docs/API使用指南.md @@ -0,0 +1,316 @@ +# 🌐 API 使用指南 + +## 认证机制 + +### 1. 用户注册 + +```bash +curl -X POST http://localhost:8080/api/v1/auth/register \ + -H "Content-Type: application/json" \ + -d '{ + "username": "testuser", + "email": "test@example.com", + "password": "Test123!@#" + }' +``` + +### 2. 用户登录 + +```bash +curl -X POST http://localhost:8080/api/v1/auth/login \ + -H "Content-Type: application/json" \ + -d '{ + "username": "testuser", + "password": "Test123!@#" + }' +``` + +响应示例: + +```json +{ + "status": "success", + "data": { + "access_token": "eyJhbGciOiJIUzI1NiIs...", + "refresh_token": "eyJhbGciOiJIUzI1NiIs...", + "expires_in": 86400, + "user": { + "id": 1, + "username": "testuser", + "email": "test@example.com" + } + }, + "request_id": "req_123456789", + "timestamp": "2024-01-01T00:00:00Z" +} +``` + +### 3. 使用访问令牌 + +```bash +curl -X GET http://localhost:8080/api/v1/users/profile \ + -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." +``` + +## 用户管理 API + +### 获取用户列表 + +```bash +curl -X GET "http://localhost:8080/api/v1/users?page=1&limit=10&search=test" \ + -H "Authorization: Bearer " +``` + +### 获取用户详情 + +```bash +curl -X GET http://localhost:8080/api/v1/users/1 \ + -H "Authorization: Bearer " +``` + +### 更新用户信息 + +```bash +curl -X PUT http://localhost:8080/api/v1/users/1 \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{ + "username": "newusername", + "email": "newemail@example.com" + }' +``` + +### 修改密码 + +```bash +curl -X POST http://localhost:8080/api/v1/users/change-password \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{ + "current_password": "oldpassword", + "new_password": "newpassword123" + }' +``` + +### 删除用户 + +```bash +curl -X DELETE http://localhost:8080/api/v1/users/1 \ + -H "Authorization: Bearer " +``` + +## 响应格式 + +### 成功响应 + +```json +{ + "status": "success", + "data": { + // 响应数据 + }, + "request_id": "req_123456789", + "timestamp": "2024-01-01T00:00:00Z" +} +``` + +### 分页响应 + +```json +{ + "status": "success", + "data": { + "items": [...], + "pagination": { + "page": 1, + "limit": 10, + "total": 100, + "total_pages": 10 + } + }, + "request_id": "req_123456789", + "timestamp": "2024-01-01T00:00:00Z" +} +``` + +### 错误响应 + +```json +{ + "status": "error", + "error": { + "code": "VALIDATION_ERROR", + "message": "请求参数无效", + "details": { + "username": ["用户名不能为空"], + "email": ["邮箱格式不正确"] + } + }, + "request_id": "req_123456789", + "timestamp": "2024-01-01T00:00:00Z" +} +``` + +## 常用查询参数 + +### 分页参数 + +- `page`: 页码,从 1 开始 +- `limit`: 每页数量,默认 10,最大 100 +- `sort`: 排序字段,如 `created_at` +- `order`: 排序方向,`asc` 或 `desc` + +示例: + +```bash +GET /api/v1/users?page=2&limit=20&sort=created_at&order=desc +``` + +### 搜索参数 + +- `search`: 关键词搜索 +- `filter`: 字段过滤 + +示例: + +```bash +GET /api/v1/users?search=john&filter=status:active +``` + +### 时间范围参数 + +- `start_date`: 开始时间,ISO 8601 格式 +- `end_date`: 结束时间,ISO 8601 格式 + +示例: + +```bash +GET /api/v1/users?start_date=2024-01-01T00:00:00Z&end_date=2024-01-31T23:59:59Z +``` + +## HTTP 状态码 + +### 成功状态码 + +- `200 OK`: 请求成功 +- `201 Created`: 创建成功 +- `202 Accepted`: 请求已接受,正在处理 +- `204 No Content`: 成功,无返回内容 + +### 客户端错误 + +- `400 Bad Request`: 请求参数错误 +- `401 Unauthorized`: 未认证 +- `403 Forbidden`: 无权限 +- `404 Not Found`: 资源不存在 +- `409 Conflict`: 资源冲突 +- `422 Unprocessable Entity`: 请求格式正确但语义错误 +- `429 Too Many Requests`: 请求过于频繁 + +### 服务器错误 + +- `500 Internal Server Error`: 服务器内部错误 +- `502 Bad Gateway`: 网关错误 +- `503 Service Unavailable`: 服务不可用 +- `504 Gateway Timeout`: 网关超时 + +## API 测试 + +### 使用 Postman + +1. 导入 API 集合文件(如果有) +2. 设置环境变量: + - `base_url`: http://localhost:8080 + - `access_token`: 从登录接口获取 + +### 使用 curl 脚本 + +创建测试脚本 `test_api.sh`: + +```bash +#!/bin/bash + +BASE_URL="http://localhost:8080" +ACCESS_TOKEN="" + +# 登录获取token +login() { + response=$(curl -s -X POST "$BASE_URL/api/v1/auth/login" \ + -H "Content-Type: application/json" \ + -d '{"username":"admin","password":"admin123"}') + + ACCESS_TOKEN=$(echo $response | jq -r '.data.access_token') + echo "Token: $ACCESS_TOKEN" +} + +# 测试用户API +test_users() { + echo "Testing Users API..." + + # 获取用户列表 + curl -s -X GET "$BASE_URL/api/v1/users" \ + -H "Authorization: Bearer $ACCESS_TOKEN" | jq + + # 创建用户 + curl -s -X POST "$BASE_URL/api/v1/users" \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"username":"testuser","email":"test@example.com","password":"test123"}' | jq +} + +# 执行测试 +login +test_users +``` + +## API 文档 + +启动服务后,访问以下地址获取完整 API 文档: + +- **Swagger UI**: http://localhost:8080/swagger/ +- **API 规范**: http://localhost:8080/api/docs +- **健康检查**: http://localhost:8080/api/v1/health + +## 限流和配额 + +### 请求限制 + +- 默认每分钟 100 个请求 +- 突发请求限制 50 个 +- 超出限制返回 429 状态码 + +### 提高限制 + +如需提高限制,请联系管理员或在配置文件中调整: + +```yaml +rate_limit: + enabled: true + requests_per_minute: 200 + burst: 100 +``` + +## 错误处理 + +### 常见错误及解决方案 + +1. **401 Unauthorized** + + - 检查 Token 是否有效 + - 确认 Token 格式正确 + - 验证 Token 是否过期 + +2. **403 Forbidden** + + - 检查用户权限 + - 确认访问的资源是否允许 + +3. **429 Too Many Requests** + + - 降低请求频率 + - 实现指数退避重试 + +4. **500 Internal Server Error** + - 检查服务器日志 + - 确认请求参数格式 + - 联系技术支持 diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..977c211 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,469 @@ +# TYAPI Server 架构设计文档 + +## 📖 概述 + +TYAPI Server 是一个基于现代化软件工程实践构建的企业级 Go Web 应用架构。本架构综合了领域驱动设计(DDD)、CQRS、事件驱动架构、微服务设计模式等先进理念,旨在提供一个高性能、可扩展、易维护的 Web 服务基础框架。 + +## 🏗️ 核心设计理念 + +### 1. 领域驱动设计 (Domain-Driven Design) + +**理念**:将复杂的业务逻辑组织成清晰的业务域,每个域负责特定的业务职责。 + +**实现方式**: + +- **实体(Entities)**:包含业务逻辑的核心对象 +- **值对象(Value Objects)**:不可变的数据对象 +- **聚合根(Aggregate Root)**:实体集合的统一入口 +- **仓储(Repository)**:数据访问的抽象层 +- **域服务(Domain Service)**:跨实体的业务逻辑 +- **域事件(Domain Event)**:业务状态变化的通知机制 + +**优势**: + +- 业务逻辑与技术实现分离 +- 代码组织清晰,易于理解和维护 +- 支持复杂业务场景的建模 + +### 2. CQRS (Command Query Responsibility Segregation) + +**理念**:将数据的读操作和写操作分离,优化不同场景下的性能需求。 + +**实现方式**: + +- **命令端**:处理数据修改操作(Create、Update、Delete) +- **查询端**:处理数据读取操作(Read、List、Search) +- **读写模型分离**:不同的数据结构优化不同的操作 + +**优势**: + +- 读写性能独立优化 +- 支持复杂查询需求 +- 易于实现缓存策略 + +### 3. 事件驱动架构 (Event-Driven Architecture) + +**理念**:通过事件实现系统组件间的松耦合通信。 + +**实现方式**: + +- **事件总线**:异步消息分发机制 +- **事件处理器**:响应特定事件的业务逻辑 +- **事件溯源**:通过事件重建系统状态 + +**优势**: + +- 系统组件解耦 +- 支持异步处理 +- 易于扩展和集成 + +### 4. 六边形架构 (Hexagonal Architecture) + +**理念**:将应用程序的核心逻辑与外部系统隔离,通过端口和适配器进行交互。 + +**实现方式**: + +- **内层**:业务逻辑和域模型 +- **中层**:应用服务和用例 +- **外层**:适配器和基础设施 + +**优势**: + +- 核心逻辑与技术实现解耦 +- 易于测试和替换组件 +- 支持多种接口类型 + +## 🛠️ 技术栈详解 + +### Web 框架层 + +#### Gin Framework + +- **选择理由**:高性能、轻量级、丰富的中间件生态 +- **核心特性**:快速路由、中间件支持、JSON 绑定 +- **性能优势**:比其他 Go 框架快 40 倍,内存占用低 + +#### 中间件系统 + +- **CORS 中间件**:跨域资源共享控制 +- **认证中间件**:JWT token 验证和用户身份识别 +- **限流中间件**:API 调用频率控制,防止滥用 +- **日志中间件**:请求追踪和性能监控 +- **安全中间件**:HTTP 安全头部设置 + +### 数据层 + +#### PostgreSQL + +- **选择理由**:强一致性、复杂查询支持、JSON 文档存储 +- **特性使用**: + - JSONB 字段存储灵活数据 + - 全文搜索功能 + - 事务支持 + - 扩展生态(UUID、pg_trgm 等) + +#### GORM + +- **选择理由**:功能强大、活跃维护、Go 生态最佳 +- **核心特性**: + - 自动迁移 + - 关联查询 + - 钩子函数 + - 事务支持 + - 连接池管理 + +#### Redis + +- **使用场景**: + - 应用缓存:查询结果缓存 + - 会话存储:用户登录状态 + - 限流计数:API 调用频率统计 + - 分布式锁:并发控制 + +### 基础设施层 + +#### 依赖注入 - Uber FX + +- **优势**: + - 编译时依赖检查 + - 生命周期管理 + - 模块化设计 + - 测试友好 + +#### 日志系统 - Zap + +- **特性**: + - 高性能结构化日志 + - 多级别日志控制 + - 灵活的输出格式 + - 生产环境优化 + +#### 配置管理 - Viper + +- **支持格式**:YAML、JSON、ENV 等 +- **特性**: + - 环境变量替换 + - 配置热重载 + - 多层级配置合并 + +### 监控和观测性 + +#### Prometheus + Grafana + +- **Prometheus**:指标收集和存储 +- **Grafana**:数据可视化和告警 +- **监控指标**: + - HTTP 请求量和延迟 + - 数据库连接池状态 + - 缓存命中率 + - 系统资源使用率 + +#### Jaeger + +- **分布式链路追踪**:请求在系统中的完整路径 +- **性能分析**:识别性能瓶颈 +- **依赖图谱**:服务间依赖关系可视化 + +## 📁 架构分层 + +### 1. 表现层 (Presentation Layer) + +``` +cmd/api/ # 应用程序入口 +├── main.go # 主程序启动 +└── handlers/ # HTTP处理器 +``` + +**职责**: + +- HTTP 请求处理 +- 请求验证和响应格式化 +- 路由定义和中间件配置 + +### 2. 应用层 (Application Layer) + +``` +internal/app/ # 应用协调 +├── app.go # 应用启动器 +└── container/ # 依赖注入容器 +``` + +**职责**: + +- 应用程序生命周期管理 +- 依赖关系配置 +- 跨领域服务协调 + +### 3. 领域层 (Domain Layer) + +``` +internal/domains/ # 业务领域 +├── user/ # 用户领域 +│ ├── entities/ # 实体 +│ ├── services/ # 领域服务 +│ ├── repositories/ # 仓储接口 +│ ├── events/ # 领域事件 +│ └── dto/ # 数据传输对象 +``` + +**职责**: + +- 业务逻辑实现 +- 领域模型定义 +- 业务规则验证 + +### 4. 基础设施层 (Infrastructure Layer) + +``` +internal/shared/ # 共享基础设施 +├── database/ # 数据库连接 +├── cache/ # 缓存服务 +├── events/ # 事件总线 +├── logger/ # 日志服务 +└── middleware/ # 中间件 +``` + +**职责**: + +- 外部系统集成 +- 技术基础设施 +- 通用工具和服务 + +## 🔄 数据流向 + +### 请求处理流程 + +1. **HTTP 请求** → **路由器** +2. **中间件链** → **认证/限流/日志** +3. **处理器** → **请求验证** +4. **应用服务** → **业务逻辑协调** +5. **领域服务** → **业务规则执行** +6. **仓储层** → **数据持久化** +7. **事件总线** → **异步事件处理** +8. **响应构建** → **HTTP 响应** + +### 事件驱动流程 + +1. **业务操作** → **触发领域事件** +2. **事件总线** → **异步分发事件** +3. **事件处理器** → **响应事件处理** +4. **副作用执行** → **缓存更新/通知发送** + +## 🔒 安全设计 + +### 认证和授权 + +#### JWT Token 机制 + +- **访问令牌**:短期有效(24 小时) +- **刷新令牌**:长期有效(7 天) +- **无状态设计**:服务端无需存储会话 + +#### 安全中间件 + +- **CORS 保护**:跨域请求控制 +- **安全头部**:XSS、CSRF、点击劫持防护 +- **输入验证**:防止 SQL 注入和 XSS 攻击 +- **限流保护**:防止暴力破解和 DDoS + +### 数据安全 + +#### 密码安全 + +- **Bcrypt 加密**:不可逆密码存储 +- **盐值随机**:防止彩虹表攻击 +- **密码策略**:强密码要求 + +#### 数据传输 + +- **HTTPS 强制**:加密数据传输 +- **API 版本控制**:向后兼容性 +- **敏感信息过滤**:日志脱敏 + +## 🚀 性能优化 + +### 缓存策略 + +#### 多级缓存 + +- **应用缓存**:查询结果缓存 +- **数据库缓存**:连接池和查询缓存 +- **CDN 缓存**:静态资源分发 + +#### 缓存模式 + +- **Cache-Aside**:应用控制缓存 +- **Write-Through**:同步写入缓存 +- **Write-Behind**:异步写入数据库 + +### 数据库优化 + +#### 连接池管理 + +- **最大连接数**:控制资源消耗 +- **空闲连接**:保持最小连接数 +- **连接超时**:防止连接泄露 + +#### 查询优化 + +- **索引策略**:覆盖常用查询 +- **分页查询**:避免大结果集 +- **预加载**:减少 N+1 查询问题 + +### 并发处理 + +#### Goroutine 池 + +- **有界队列**:控制并发数量 +- **优雅降级**:过载保护机制 +- **超时控制**:防止资源泄露 + +## 🧪 可测试性 + +### 测试策略 + +#### 单元测试 + +- **Mock 接口**:隔离外部依赖 +- **测试覆盖**:核心业务逻辑 100%覆盖 +- **快速反馈**:毫秒级测试执行 + +#### 集成测试 + +- **Testcontainers**:真实数据库环境 +- **API 测试**:端到端功能验证 +- **并发测试**:竞态条件检测 + +#### 测试工具 + +- **Testify**:断言和 Mock 框架 +- **GoConvey**:BDD 风格测试 +- **Ginkgo**:规范化测试结构 + +## 🔧 可维护性 + +### 代码组织 + +#### 包结构设计 + +- **按功能分包**:清晰的职责边界 +- **依赖方向**:依赖倒置原则 +- **接口隔离**:最小化接口暴露 + +#### 编码规范 + +- **Go 官方规范**:gofmt、golint 标准 +- **命名约定**:一致的命名风格 +- **注释文档**:完整的 API 文档 + +### 配置管理 + +#### 环境分离 + +- **开发环境**:详细日志、调试工具 +- **测试环境**:稳定配置、自动化测试 +- **生产环境**:性能优化、安全加固 + +#### 配置热更新 + +- **文件监控**:配置文件变化检测 +- **优雅重启**:无停机配置更新 +- **回滚机制**:配置错误恢复 + +## 🌐 可扩展性 + +### 水平扩展 + +#### 无状态设计 + +- **会话外置**:Redis 存储用户状态 +- **负载均衡**:多实例部署 +- **自动伸缩**:基于指标的扩缩容 + +#### 数据库扩展 + +- **读写分离**:主从复制架构 +- **分库分表**:水平分片策略 +- **缓存预热**:减少数据库压力 + +### 服务拆分 + +#### 微服务就绪 + +- **领域边界**:清晰的服务边界 +- **API 网关**:统一入口和路由 +- **服务发现**:动态服务注册 + +#### 通信机制 + +- **REST API**:同步通信标准 +- **消息队列**:异步解耦通信 +- **事件溯源**:状态重建机制 + +## 📈 监控和运维 + +### 可观测性三大支柱 + +#### 日志 (Logging) + +- **结构化日志**:JSON 格式便于检索 +- **日志级别**:灵活的详细程度控制 +- **日志聚合**:集中式日志管理 + +#### 指标 (Metrics) + +- **业务指标**:用户行为和业务 KPI +- **技术指标**:系统性能和资源使用 +- **SLI/SLO**:服务水平指标和目标 + +#### 链路追踪 (Tracing) + +- **请求追踪**:完整的请求处理路径 +- **性能分析**:识别瓶颈和优化点 +- **依赖分析**:服务间调用关系 + +### 运维自动化 + +#### 健康检查 + +- **多层级检查**:应用、数据库、缓存 +- **智能告警**:基于阈值的自动通知 +- **故障恢复**:自动重启和故障转移 + +#### 部署策略 + +- **蓝绿部署**:零停机更新 +- **滚动更新**:渐进式版本发布 +- **回滚机制**:快速恢复到稳定版本 + +## 🚀 未来演进方向 + +### 技术演进 + +#### 云原生 + +- **容器化**:Docker 标准化部署 +- **编排平台**:Kubernetes 集群管理 +- **服务网格**:Istio 流量治理 + +#### 新技术集成 + +- **GraphQL**:灵活的 API 查询语言 +- **gRPC**:高性能 RPC 通信 +- **WebAssembly**:高性能计算扩展 + +### 架构演进 + +#### 事件溯源 + +- **完整事件历史**:业务状态重建 +- **审计日志**:合规性和追溯性 +- **时间旅行**:历史状态查询 + +#### CQRS 进阶 + +- **独立数据存储**:读写数据库分离 +- **最终一致性**:分布式数据同步 +- **投影视图**:优化的查询模型 + +这个架构设计文档展示了 TYAPI Server 的完整技术架构和设计理念,为开发团队提供了全面的技术指导和最佳实践参考。 diff --git a/docs/开发指南.md b/docs/开发指南.md new file mode 100644 index 0000000..78756c5 --- /dev/null +++ b/docs/开发指南.md @@ -0,0 +1,279 @@ +# 👨‍💻 开发指南 + +## 项目结构理解 + +``` +tyapi-server-gin/ +├── cmd/api/ # 应用入口 +├── internal/ # 内部代码 +│ ├── app/ # 应用层 +│ ├── config/ # 配置管理 +│ ├── container/ # 依赖注入 +│ ├── domains/ # 业务域 +│ │ └── user/ # 用户域示例 +│ └── shared/ # 共享基础设施 +├── pkg/ # 外部包 +├── scripts/ # 脚本文件 +├── test/ # 测试文件 +└── docs/ # 文档目录 +``` + +## 开发流程 + +### 1. 创建新的业务域 + +```bash +# 使用Makefile创建新域 +make new-domain DOMAIN=product + +# 手动创建目录结构 +mkdir -p internal/domains/product/{entities,dto,services,repositories,handlers,routes,events} +``` + +### 2. 实现业务实体 + +创建 `internal/domains/product/entities/product.go`: + +```go +type Product struct { + BaseEntity + Name string `gorm:"not null;size:100"` + Description string `gorm:"size:500"` + Price float64 `gorm:"not null"` + CategoryID uint `gorm:"not null"` +} + +func (p *Product) Validate() error { + if p.Name == "" { + return errors.New("产品名称不能为空") + } + if p.Price <= 0 { + return errors.New("产品价格必须大于0") + } + return nil +} +``` + +### 3. 定义仓储接口 + +创建 `internal/domains/product/repositories/product_repository.go`: + +```go +type ProductRepository interface { + shared.BaseRepository[entities.Product] + FindByCategory(ctx context.Context, categoryID uint) ([]*entities.Product, error) + FindByPriceRange(ctx context.Context, min, max float64) ([]*entities.Product, error) +} +``` + +### 4. 实现业务服务 + +创建 `internal/domains/product/services/product_service.go`: + +```go +type ProductService interface { + CreateProduct(ctx context.Context, req *dto.CreateProductRequest) (*dto.ProductResponse, error) + GetProduct(ctx context.Context, id uint) (*dto.ProductResponse, error) + UpdateProduct(ctx context.Context, id uint, req *dto.UpdateProductRequest) (*dto.ProductResponse, error) + DeleteProduct(ctx context.Context, id uint) error +} +``` + +### 5. 实现 HTTP 处理器 + +创建 `internal/domains/product/handlers/product_handler.go`: + +```go +func (h *ProductHandler) CreateProduct(c *gin.Context) { + var req dto.CreateProductRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.Error(c, http.StatusBadRequest, "请求参数无效", err) + return + } + + product, err := h.productService.CreateProduct(c.Request.Context(), &req) + if err != nil { + response.Error(c, http.StatusInternalServerError, "创建产品失败", err) + return + } + + response.Success(c, product) +} +``` + +### 6. 配置路由 + +创建 `internal/domains/product/routes/product_routes.go`: + +```go +func RegisterProductRoutes(router shared.Router, handler *handlers.ProductHandler) { + v1 := router.Group("/api/v1") + { + products := v1.Group("/products") + { + products.POST("", handler.CreateProduct) + products.GET("/:id", handler.GetProduct) + products.PUT("/:id", handler.UpdateProduct) + products.DELETE("/:id", handler.DeleteProduct) + } + } +} +``` + +## 常用开发命令 + +```bash +# 代码格式化 +make fmt + +# 代码检查 +make lint + +# 运行测试 +make test + +# 测试覆盖率 +make test-coverage + +# 生成API文档 +make docs + +# 热重载开发 +make dev + +# 构建二进制文件 +make build + +# 清理临时文件 +make clean +``` + +## 测试编写 + +### 单元测试示例 + +```go +func TestProductService_CreateProduct(t *testing.T) { + // 设置测试数据 + mockRepo := mocks.NewProductRepository(t) + service := services.NewProductService(mockRepo, nil) + + req := &dto.CreateProductRequest{ + Name: "测试产品", + Description: "测试描述", + Price: 99.99, + CategoryID: 1, + } + + // 设置Mock期望 + mockRepo.On("Create", mock.Anything, mock.AnythingOfType("*entities.Product")). + Return(&entities.Product{}, nil) + + // 执行测试 + result, err := service.CreateProduct(context.Background(), req) + + // 断言结果 + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, req.Name, result.Name) +} +``` + +### 集成测试示例 + +```go +func TestProductAPI_Integration(t *testing.T) { + // 启动测试服务器 + testApp := setupTestApp(t) + defer testApp.Cleanup() + + // 创建测试请求 + reqBody := `{"name":"测试产品","price":99.99,"category_id":1}` + req := httptest.NewRequest("POST", "/api/v1/products", strings.NewReader(reqBody)) + req.Header.Set("Content-Type", "application/json") + + // 执行请求 + w := httptest.NewRecorder() + testApp.ServeHTTP(w, req) + + // 验证响应 + assert.Equal(t, http.StatusCreated, w.Code) + + var response map[string]interface{} + json.Unmarshal(w.Body.Bytes(), &response) + assert.Equal(t, "success", response["status"]) +} +``` + +## 开发规范 + +### 代码风格 + +- 使用 `gofmt` 格式化代码 +- 遵循 Go 命名规范 +- 添加必要的注释和文档 +- 函数长度不超过 50 行 + +### Git 提交规范 + +```bash +# 功能开发 +git commit -m "feat: 添加用户注册功能" + +# 问题修复 +git commit -m "fix: 修复登录验证问题" + +# 文档更新 +git commit -m "docs: 更新API文档" + +# 重构代码 +git commit -m "refactor: 重构用户服务层" +``` + +### 分支管理 + +- `main`: 主分支,用于生产发布 +- `develop`: 开发分支,用于集成测试 +- `feature/xxx`: 功能分支,用于新功能开发 +- `hotfix/xxx`: 热修复分支,用于紧急修复 + +## 调试技巧 + +### 使用 Delve 调试器 + +```bash +# 安装 Delve +go install github.com/go-delve/delve/cmd/dlv@latest + +# 启动调试 +dlv debug ./cmd/api/main.go + +# 设置断点 +(dlv) break main.main +(dlv) continue +``` + +### 日志调试 + +```go +// 添加调试日志 +logger.Debug("Processing user request", + zap.String("user_id", userID), + zap.String("action", "create_product")) + +// 临时调试信息 +fmt.Printf("Debug: %+v\n", debugData) +``` + +### 性能分析 + +```bash +# 启用 pprof +go tool pprof http://localhost:8080/debug/pprof/profile + +# 内存分析 +go tool pprof http://localhost:8080/debug/pprof/heap + +# 协程分析 +go tool pprof http://localhost:8080/debug/pprof/goroutine +``` diff --git a/docs/快速开始指南.md b/docs/快速开始指南.md new file mode 100644 index 0000000..5a8aab4 --- /dev/null +++ b/docs/快速开始指南.md @@ -0,0 +1,56 @@ +# 🚀 快速开始指南 + +## 前置要求 + +确保您的开发环境中安装了以下工具: + +- **Go 1.23.4+** - 编程语言环境 +- **Docker & Docker Compose** - 容器化环境 +- **Git** - 版本控制工具 +- **Make** - 构建工具(可选,推荐) + +## 一键启动 + +```bash +# 1. 克隆项目 +git clone +cd tyapi-server-gin + +# 2. 启动开发环境 +make dev-up + +# 3. 运行应用 +make run +``` + +访问 http://localhost:8080/api/v1/health 验证启动成功。 + +## 验证安装 + +### 检查服务状态 + +```bash +# 检查所有容器是否正常运行 +docker-compose -f docker-compose.dev.yml ps + +# 检查应用健康状态 +curl http://localhost:8080/api/v1/health +``` + +### 访问管理界面 + +启动成功后,您可以访问以下管理界面: + +- **API 文档**: http://localhost:8080/swagger/ +- **数据库管理**: http://localhost:5050 (pgAdmin) +- **监控面板**: http://localhost:3000 (Grafana) +- **链路追踪**: http://localhost:16686 (Jaeger) +- **邮件测试**: http://localhost:8025 (MailHog) + +## 下一步 + +快速启动完成后,建议您: + +1. 阅读 [环境搭建指南](./环境搭建指南.md) 了解详细配置 +2. 查看 [开发指南](./开发指南.md) 开始开发 +3. 参考 [API 使用指南](./API使用指南.md) 了解 API 用法 diff --git a/docs/故障排除指南.md b/docs/故障排除指南.md new file mode 100644 index 0000000..7e03964 --- /dev/null +++ b/docs/故障排除指南.md @@ -0,0 +1,404 @@ +# 🔍 故障排除指南 + +## 常见问题 + +### 1. 数据库连接失败 + +**问题**:`failed to connect to database` + +**解决方案**: + +```bash +# 检查数据库配置 +cat config.yaml | grep -A 10 database + +# 测试数据库连接 +psql -h localhost -U postgres -d tyapi_dev + +# 检查环境变量 +env | grep DB_ +``` + +### 2. Redis 连接失败 + +**问题**:`failed to connect to redis` + +**解决方案**: + +```bash +# 检查Redis状态 +redis-cli ping + +# 检查配置 +cat config.yaml | grep -A 5 redis + +# 重启Redis +docker restart tyapi-redis +``` + +### 3. JWT 令牌验证失败 + +**问题**:`invalid token` + +**解决方案**: + +```bash +# 检查JWT密钥配置 +echo $JWT_SECRET + +# 验证令牌格式 +echo "your-token" | cut -d. -f2 | base64 -d +``` + +### 4. 内存使用过高 + +**问题**:应用内存占用持续增长 + +**解决方案**: + +```bash +# 启用pprof分析 +go tool pprof http://localhost:8080/debug/pprof/heap + +# 检查Goroutine泄露 +go tool pprof http://localhost:8080/debug/pprof/goroutine + +# 优化数据库连接池 +# 在config.yaml中调整max_open_conns和max_idle_conns +``` + +### 5. 端口冲突 + +**问题**:`bind: address already in use` + +**解决方案**: + +```bash +# 查找占用端口的进程 +netstat -tlnp | grep :8080 +lsof -i :8080 + +# 终止占用端口的进程 +kill -9 + +# 修改配置使用其他端口 +``` + +### 6. 权限问题 + +**问题**:`permission denied` + +**解决方案**: + +```bash +# 检查文件权限 +ls -la config.yaml +ls -la logs/ + +# 修复权限 +chmod 644 config.yaml +chmod 755 logs/ +chown -R $(whoami) logs/ +``` + +## 日志分析 + +### 1. 应用日志 + +```bash +# 查看应用日志 +tail -f logs/app.log + +# 过滤错误日志 +grep "ERROR" logs/app.log + +# 分析请求延迟 +grep "request_duration" logs/app.log | awk '{print $NF}' | sort -n +``` + +### 2. 数据库日志 + +```bash +# PostgreSQL日志 +docker logs tyapi-postgres 2>&1 | grep ERROR + +# 慢查询分析 +grep "duration:" logs/postgresql.log | awk '$3 > 1000' +``` + +### 3. 性能监控 + +```bash +# 查看系统指标 +curl http://localhost:8080/metrics + +# Prometheus查询示例 +# HTTP请求QPS +rate(http_requests_total[5m]) + +# 平均响应时间 +rate(http_request_duration_seconds_sum[5m]) / rate(http_request_duration_seconds_count[5m]) +``` + +## 容器相关问题 + +### 1. 容器启动失败 + +```bash +# 查看容器状态 +docker-compose ps + +# 查看容器日志 +docker-compose logs + +# 重新构建镜像 +docker-compose build --no-cache +``` + +### 2. 网络连接问题 + +```bash +# 检查网络配置 +docker network ls +docker network inspect tyapi-network + +# 测试容器间连接 +docker exec -it tyapi-server ping postgres +``` + +### 3. 数据持久化问题 + +```bash +# 检查数据卷 +docker volume ls +docker volume inspect postgres_data + +# 备份数据 +docker exec tyapi-postgres pg_dump -U postgres tyapi_dev > backup.sql +``` + +## 性能问题 + +### 1. 响应时间过长 + +**诊断步骤**: + +```bash +# 启用详细日志 +export LOG_LEVEL=debug + +# 分析慢查询 +grep "slow query" logs/app.log + +# 检查数据库索引 +psql -h localhost -U postgres -d tyapi_dev -c "\di" +``` + +### 2. 内存泄漏 + +**诊断步骤**: + +```bash +# 监控内存使用 +top -p $(pgrep tyapi-server) + +# 生成内存分析报告 +go tool pprof -http=:6060 http://localhost:8080/debug/pprof/heap +``` + +### 3. 高 CPU 使用率 + +**诊断步骤**: + +```bash +# CPU性能分析 +go tool pprof http://localhost:8080/debug/pprof/profile + +# 检查系统负载 +uptime +iostat 1 5 +``` + +## 开发环境问题 + +### 1. 热重载不工作 + +```bash +# 检查文件监控 +ls -la .air.toml + +# 重启开发服务器 +make dev-restart + +# 检查文件权限 +chmod +x scripts/dev.sh +``` + +### 2. 测试失败 + +```bash +# 运行特定测试 +go test -v ./internal/domains/user/... + +# 清理测试缓存 +go clean -testcache + +# 运行集成测试 +go test -tags=integration ./test/... +``` + +## 生产环境问题 + +### 1. 健康检查失败 + +```bash +# 手动测试健康检查 +curl -f http://localhost:8080/api/v1/health + +# 检查依赖服务 +curl -f http://localhost:8080/api/v1/health/ready + +# 查看详细错误 +curl -v http://localhost:8080/api/v1/health +``` + +### 2. 负载均衡问题 + +```bash +# 检查上游服务器状态 +nginx -t +systemctl status nginx + +# 查看负载均衡日志 +tail -f /var/log/nginx/access.log +tail -f /var/log/nginx/error.log +``` + +### 3. 证书问题 + +```bash +# 检查SSL证书 +openssl x509 -in /etc/ssl/certs/server.crt -text -noout + +# 验证证书有效期 +openssl x509 -in /etc/ssl/certs/server.crt -checkend 86400 + +# 测试HTTPS连接 +curl -I https://api.yourdomain.com +``` + +## 调试工具 + +### 1. 日志查看工具 + +```bash +# 实时查看日志 +journalctl -u tyapi-server -f + +# 过滤特定级别日志 +journalctl -u tyapi-server -p err + +# 按时间范围查看日志 +journalctl -u tyapi-server --since "2024-01-01 00:00:00" +``` + +### 2. 网络调试 + +```bash +# 检查端口监听 +ss -tlnp | grep :8080 + +# 网络连接测试 +telnet localhost 8080 +nc -zv localhost 8080 + +# DNS解析测试 +nslookup api.yourdomain.com +dig api.yourdomain.com +``` + +### 3. 数据库调试 + +```bash +# 连接数据库 +psql -h localhost -U postgres -d tyapi_dev + +# 查看活动连接 +SELECT * FROM pg_stat_activity; + +# 查看慢查询 +SELECT query, mean_time, calls FROM pg_stat_statements ORDER BY mean_time DESC LIMIT 5; +``` + +## 紧急响应流程 + +### 1. 服务宕机 + +1. **快速恢复**: + + ```bash + # 重启服务 + systemctl restart tyapi-server + + # 或使用Docker + docker-compose restart tyapi-server + ``` + +2. **回滚部署**: + + ```bash + # K8s回滚 + kubectl rollout undo deployment/tyapi-server + + # Docker回滚 + docker-compose down + docker-compose up -d --scale tyapi-server=3 + ``` + +### 2. 数据库问题 + +1. **主从切换**: + + ```bash + # 提升从库为主库 + sudo -u postgres /usr/lib/postgresql/13/bin/pg_promote -D /var/lib/postgresql/13/main + ``` + +2. **数据恢复**: + ```bash + # 从备份恢复 + psql -h localhost -U postgres -d tyapi_dev < backup_latest.sql + ``` + +### 3. 联系支持 + +当遇到无法解决的问题时: + +1. 收集错误信息和日志 +2. 记录重现步骤 +3. 准备系统环境信息 +4. 联系技术支持团队 + +**支持信息收集脚本**: + +```bash +#!/bin/bash +echo "=== TYAPI Server Debug Info ===" > debug_info.txt +echo "Date: $(date)" >> debug_info.txt +echo "Version: $(cat VERSION 2>/dev/null || echo 'unknown')" >> debug_info.txt +echo "" >> debug_info.txt + +echo "=== System Info ===" >> debug_info.txt +uname -a >> debug_info.txt +echo "" >> debug_info.txt + +echo "=== Docker Status ===" >> debug_info.txt +docker-compose ps >> debug_info.txt +echo "" >> debug_info.txt + +echo "=== Recent Logs ===" >> debug_info.txt +tail -50 logs/app.log >> debug_info.txt +echo "" >> debug_info.txt + +echo "Debug info collected in debug_info.txt" +``` diff --git a/docs/文档索引.md b/docs/文档索引.md new file mode 100644 index 0000000..6782e29 --- /dev/null +++ b/docs/文档索引.md @@ -0,0 +1,102 @@ +# 📚 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/docs/最佳实践指南.md b/docs/最佳实践指南.md new file mode 100644 index 0000000..6d2b95d --- /dev/null +++ b/docs/最佳实践指南.md @@ -0,0 +1,536 @@ +# 📋 最佳实践指南 + +## 开发最佳实践 + +### 1. 代码规范 + +```bash +# 格式化代码 +gofmt -w . +goimports -w . + +# 代码检查 +golangci-lint run + +# 生成文档 +godoc -http=:6060 +``` + +**编码标准**: + +- 遵循 Go 官方编码规范 +- 使用有意义的变量和函数名 +- 保持函数简洁,单一职责 +- 添加必要的注释和文档 + +### 2. Git 工作流 + +```bash +# 功能分支开发 +git checkout -b feature/user-profile +git add . +git commit -m "feat: add user profile management" +git push origin feature/user-profile + +# 代码审查后合并 +git checkout main +git merge feature/user-profile +git push origin main +``` + +**提交规范**: + +- `feat`: 新功能 +- `fix`: 修复问题 +- `docs`: 文档更新 +- `style`: 代码格式修改 +- `refactor`: 代码重构 +- `test`: 测试相关 +- `chore`: 构建过程或辅助工具的变动 + +### 3. 测试策略 + +```bash +# 运行所有测试 +make test + +# 只运行单元测试 +go test ./internal/domains/... + +# 运行集成测试 +go test -tags=integration ./test/integration/... + +# 生成覆盖率报告 +make test-coverage +open coverage.html +``` + +**测试金字塔**: + +- **单元测试**:70% - 测试单个函数/方法 +- **集成测试**:20% - 测试模块间交互 +- **端到端测试**:10% - 测试完整用户流程 + +### 4. 错误处理 + +```go +// 统一错误处理 +func (s *userService) CreateUser(ctx context.Context, req *dto.CreateUserRequest) (*dto.UserResponse, error) { + // 参数验证 + if err := req.Validate(); err != nil { + return nil, errors.NewValidationError("invalid request", err) + } + + // 业务逻辑 + user, err := s.userRepo.Create(ctx, req.ToEntity()) + if err != nil { + s.logger.Error("failed to create user", zap.Error(err)) + return nil, errors.NewInternalError("failed to create user") + } + + return dto.ToUserResponse(user), nil +} +``` + +### 5. 日志记录 + +```go +// 结构化日志 +logger.Info("user created successfully", + zap.String("user_id", user.ID), + zap.String("username", user.Username), + zap.Duration("duration", time.Since(start))) + +// 错误日志 +logger.Error("database connection failed", + zap.Error(err), + zap.String("host", dbHost), + zap.String("database", dbName)) +``` + +**日志级别使用**: + +- `DEBUG`: 详细调试信息 +- `INFO`: 一般信息记录 +- `WARN`: 警告信息 +- `ERROR`: 错误信息 +- `FATAL`: 致命错误 + +## 安全最佳实践 + +### 1. 认证和授权 + +```go +// JWT 令牌配置 +type JWTConfig struct { + Secret string `yaml:"secret"` + AccessTokenTTL time.Duration `yaml:"access_token_ttl"` + RefreshTokenTTL time.Duration `yaml:"refresh_token_ttl"` + Issuer string `yaml:"issuer"` +} + +// 密码加密 +func HashPassword(password string) (string, error) { + bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + return string(bytes), err +} +``` + +### 2. 输入验证 + +```go +// 请求验证 +type CreateUserRequest struct { + Username string `json:"username" validate:"required,min=3,max=20,alphanum"` + Email string `json:"email" validate:"required,email"` + Password string `json:"password" validate:"required,min=8,containsany=!@#$%^&*"` +} + +func (r *CreateUserRequest) Validate() error { + validate := validator.New() + return validate.Struct(r) +} +``` + +### 3. SQL 注入防护 + +```go +// 使用参数化查询 +func (r *userRepository) FindByEmail(ctx context.Context, email string) (*entities.User, error) { + var user entities.User + err := r.db.WithContext(ctx). + Where("email = ?", email). // 参数化查询 + First(&user).Error + return &user, err +} +``` + +### 4. HTTPS 配置 + +```yaml +# 生产环境配置 +server: + tls: + enabled: true + cert_file: "/etc/ssl/certs/server.crt" + key_file: "/etc/ssl/private/server.key" + min_version: "1.2" +``` + +## 性能最佳实践 + +### 1. 数据库优化 + +```go +// 连接池配置 +func setupDB(config *config.DatabaseConfig) (*gorm.DB, error) { + db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) + if err != nil { + return nil, err + } + + sqlDB, err := db.DB() + if err != nil { + return nil, err + } + + // 连接池设置 + sqlDB.SetMaxOpenConns(config.MaxOpenConns) // 最大连接数 + sqlDB.SetMaxIdleConns(config.MaxIdleConns) // 最大空闲连接 + sqlDB.SetConnMaxLifetime(config.ConnMaxLifetime) // 连接最大生命周期 + + return db, nil +} +``` + +**查询优化**: + +```sql +-- 添加索引 +CREATE INDEX idx_users_email ON users(email); +CREATE INDEX idx_users_created_at ON users(created_at); + +-- 复合索引 +CREATE INDEX idx_users_status_created ON users(status, created_at); +``` + +### 2. 缓存策略 + +```go +// 多级缓存 +type CacheService struct { + localCache cache.Cache + redisCache redis.Client + ttl time.Duration +} + +func (c *CacheService) Get(ctx context.Context, key string) (string, error) { + // L1: 本地缓存 + if value, ok := c.localCache.Get(key); ok { + return value.(string), nil + } + + // L2: Redis 缓存 + value, err := c.redisCache.Get(ctx, key).Result() + if err == nil { + c.localCache.Set(key, value, c.ttl) + return value, nil + } + + return "", cache.ErrCacheMiss +} +``` + +### 3. 异步处理 + +```go +// 使用工作池处理任务 +type WorkerPool struct { + workers int + jobQueue chan Job + wg sync.WaitGroup +} + +func (wp *WorkerPool) Start() { + for i := 0; i < wp.workers; i++ { + wp.wg.Add(1) + go wp.worker() + } +} + +func (wp *WorkerPool) Submit(job Job) { + wp.jobQueue <- job +} +``` + +### 4. 内存管理 + +```go +// 对象池减少GC压力 +var bufferPool = sync.Pool{ + New: func() interface{} { + return make([]byte, 4096) + }, +} + +func processData(data []byte) error { + buffer := bufferPool.Get().([]byte) + defer bufferPool.Put(buffer) + + // 使用buffer处理数据 + return nil +} +``` + +## 运维最佳实践 + +### 1. 监控告警 + +**关键指标监控**: + +```yaml +# Prometheus 告警规则 +groups: + - name: tyapi-server + rules: + - alert: HighErrorRate + expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.01 + labels: + severity: critical + annotations: + summary: "High error rate detected" + + - alert: HighResponseTime + expr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) > 0.5 + labels: + severity: warning + annotations: + summary: "High response time detected" +``` + +### 2. 备份策略 + +```bash +#!/bin/bash +# 数据库备份脚本 + +BACKUP_DIR="/backups/tyapi" +DATE=$(date +%Y%m%d_%H%M%S) +BACKUP_FILE="$BACKUP_DIR/tyapi_backup_$DATE.sql" + +# 创建备份 +pg_dump -h localhost -U postgres tyapi_prod > $BACKUP_FILE + +# 压缩备份文件 +gzip $BACKUP_FILE + +# 保留最近7天的备份 +find $BACKUP_DIR -name "*.sql.gz" -mtime +7 -delete + +# 上传到云存储 +aws s3 cp $BACKUP_FILE.gz s3://tyapi-backups/ +``` + +### 3. 配置管理 + +```yaml +# 生产环境配置模板 +server: + port: 8080 + mode: release + read_timeout: 30s + write_timeout: 30s + +database: + host: ${DB_HOST} + port: ${DB_PORT} + user: ${DB_USER} + password: ${DB_PASSWORD} + name: ${DB_NAME} + max_open_conns: 100 + max_idle_conns: 10 + +redis: + host: ${REDIS_HOST} + port: ${REDIS_PORT} + password: ${REDIS_PASSWORD} + pool_size: 10 + +security: + jwt_secret: ${JWT_SECRET} + bcrypt_cost: 12 + +logging: + level: info + format: json + output: stdout +``` + +### 4. 容器化最佳实践 + +```dockerfile +# 多阶段构建 Dockerfile +FROM golang:1.21-alpine AS builder + +WORKDIR /app +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main ./cmd/api + +FROM alpine:3.18 + +# 安全加固 +RUN adduser -D -s /bin/sh appuser +RUN apk --no-cache add ca-certificates tzdata + +WORKDIR /root/ + +COPY --from=builder /app/main . +COPY --from=builder /app/config.yaml . + +USER appuser + +EXPOSE 8080 + +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8080/api/v1/health || exit 1 + +CMD ["./main"] +``` + +## 团队协作最佳实践 + +### 1. 代码审查 + +**审查检查清单**: + +- [ ] 代码符合项目规范 +- [ ] 测试覆盖率充足 +- [ ] 错误处理正确 +- [ ] 性能影响评估 +- [ ] 安全漏洞检查 +- [ ] 文档更新 + +### 2. CI/CD 流程 + +```yaml +# GitHub Actions 示例 +name: CI/CD Pipeline + +on: + push: + branches: [main, develop] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v3 + with: + go-version: 1.21 + + - name: Run tests + run: | + go test -v -race -coverprofile=coverage.out ./... + go tool cover -html=coverage.out -o coverage.html + + - name: Upload coverage + uses: codecov/codecov-action@v3 + + deploy: + needs: test + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' + steps: + - name: Deploy to production + run: | + echo "Deploying to production..." +``` + +### 3. 文档维护 + +- **API 文档**:使用 Swagger/OpenAPI 自动生成 +- **架构文档**:定期更新系统架构图 +- **运维手册**:记录部署和运维流程 +- **故障手册**:记录常见问题和解决方案 + +### 4. 版本管理 + +```bash +# 语义化版本控制 +git tag v1.2.3 + +# 版本发布流程 +git checkout main +git pull origin main +git tag v1.2.3 +git push origin v1.2.3 +``` + +**版本号规则**: + +- **MAJOR**:不兼容的 API 修改 +- **MINOR**:向下兼容的功能性新增 +- **PATCH**:向下兼容的问题修正 + +## 扩展性最佳实践 + +### 1. 微服务拆分 + +**拆分原则**: + +- 按业务边界拆分 +- 保持服务自治 +- 数据库分离 +- 独立部署 + +### 2. 事件驱动架构 + +```go +// 事件发布 +type EventBus interface { + Publish(ctx context.Context, event Event) error + Subscribe(eventType string, handler EventHandler) error +} + +// 事件处理 +func (h *UserEventHandler) HandleUserCreated(ctx context.Context, event *UserCreatedEvent) error { + // 发送欢迎邮件 + return h.emailService.SendWelcomeEmail(ctx, event.UserID) +} +``` + +### 3. 配置外部化 + +```go +// 配置热重载 +type ConfigManager struct { + config *Config + watchers []ConfigWatcher +} + +func (cm *ConfigManager) Watch() { + for { + if changed := cm.checkConfigChange(); changed { + cm.reloadConfig() + cm.notifyWatchers() + } + time.Sleep(time.Second * 10) + } +} +``` + +### 4. 服务治理 + +- **服务注册与发现** +- **负载均衡** +- **熔断器** +- **限流器** +- **链路追踪** + +通过遵循这些最佳实践,可以确保 TYAPI Server 项目的高质量、高性能和高可维护性。 diff --git a/docs/环境搭建指南.md b/docs/环境搭建指南.md new file mode 100644 index 0000000..bd84c60 --- /dev/null +++ b/docs/环境搭建指南.md @@ -0,0 +1,127 @@ +# 🔧 环境搭建指南 + +## 开发环境配置 + +### 1. 配置环境变量 + +```bash +# 复制环境变量模板 +cp env.example .env + +# 编辑环境变量(根据需要修改) +vim .env +``` + +### 2. 启动基础服务 + +```bash +# 启动 PostgreSQL 和 Redis +docker-compose -f docker-compose.dev.yml up -d postgres redis + +# 查看服务状态 +docker-compose -f docker-compose.dev.yml ps +``` + +### 3. 数据库初始化 + +```bash +# 创建数据库表 +make migrate + +# 或手动执行SQL +psql -h localhost -U postgres -d tyapi_dev -f scripts/init.sql +``` + +### 4. 依赖安装 + +```bash +# 安装Go依赖 +go mod download + +# 验证依赖 +go mod verify +``` + +## 生产环境配置 + +### 1. 配置文件准备 + +```bash +# 复制生产配置模板 +cp config.prod.yaml config.yaml + +# 修改生产配置 +vim config.yaml +``` + +### 2. 环境变量设置 + +```bash +export APP_ENV=production +export DB_HOST=your-db-host +export DB_PASSWORD=your-secure-password +export JWT_SECRET=your-jwt-secret +export REDIS_HOST=your-redis-host +``` + +## 服务配置说明 + +### PostgreSQL 配置 + +默认配置: + +- 端口:5432 +- 数据库:tyapi_dev +- 用户名:postgres +- 密码:Pg9mX4kL8nW2rT5y(开发环境) + +### Redis 配置 + +默认配置: + +- 端口:6379 +- 无密码(开发环境) +- 数据库:0 + +### 监控服务配置 + +- **Prometheus**: http://localhost:9090 +- **Grafana**: http://localhost:3000 (admin/Gf7nB3xM9cV6pQ2w) +- **Jaeger**: http://localhost:16686 + +### 存储服务配置 + +- **MinIO**: http://localhost:9000 (minioadmin/Mn5oH8yK3bR7vX1z) +- **对象存储控制台**: http://localhost:9001 + +## 常见配置问题 + +### 端口冲突 + +如果遇到端口冲突,可以修改 `docker-compose.dev.yml` 中的端口映射: + +```yaml +ports: + - "15432:5432" # 将 PostgreSQL 映射到本地 15432 端口 +``` + +### 权限问题 + +在 Linux/macOS 系统中,可能需要调整文件权限: + +```bash +# 给予脚本执行权限 +chmod +x scripts/*.sh + +# 修复数据目录权限 +sudo chown -R $(whoami) ./data/ +``` + +### 内存不足 + +如果系统内存不足,可以减少启动的服务: + +```bash +# 只启动核心服务 +docker-compose -f docker-compose.dev.yml up -d postgres redis +``` diff --git a/docs/部署指南.md b/docs/部署指南.md new file mode 100644 index 0000000..60a4f76 --- /dev/null +++ b/docs/部署指南.md @@ -0,0 +1,476 @@ +# 🚀 部署指南 + +## Docker 部署 + +### 1. 构建镜像 + +```bash +# 构建生产镜像 +make docker-build + +# 或使用Docker命令 +docker build -t tyapi-server:latest . +``` + +### 2. 运行容器 + +```bash +# 单容器运行 +docker run -d \ + --name tyapi-server \ + -p 8080:8080 \ + -e APP_ENV=production \ + -e DB_HOST=your-db-host \ + -e DB_PASSWORD=your-password \ + tyapi-server:latest + +# 使用Docker Compose +docker-compose up -d +``` + +### 3. 多环境部署 + +#### 开发环境 + +```bash +# 启动完整开发环境 +docker-compose -f docker-compose.dev.yml up -d + +# 仅启动依赖服务 +docker-compose -f docker-compose.dev.yml up -d postgres redis +``` + +#### 测试环境 + +```bash +# 使用测试配置 +docker-compose -f docker-compose.test.yml up -d +``` + +#### 生产环境 + +```bash +# 使用生产配置 +docker-compose -f docker-compose.prod.yml up -d +``` + +## Kubernetes 部署 + +### 1. 配置清单文件 + +创建 `k8s/deployment.yaml`: + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: tyapi-server +spec: + replicas: 3 + selector: + matchLabels: + app: tyapi-server + template: + metadata: + labels: + app: tyapi-server + spec: + containers: + - name: tyapi-server + image: tyapi-server:latest + ports: + - containerPort: 8080 + env: + - name: APP_ENV + value: "production" + - name: DB_HOST + valueFrom: + secretKeyRef: + name: tyapi-secrets + key: db-host +``` + +### 2. 服务配置 + +创建 `k8s/service.yaml`: + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: tyapi-server-service +spec: + selector: + app: tyapi-server + ports: + - protocol: TCP + port: 80 + targetPort: 8080 + type: LoadBalancer +``` + +### 3. 配置管理 + +创建 `k8s/configmap.yaml`: + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: tyapi-config +data: + config.yaml: | + server: + port: 8080 + mode: release + database: + host: postgres-service + port: 5432 +``` + +### 4. 密钥管理 + +```bash +# 创建密钥 +kubectl create secret generic tyapi-secrets \ + --from-literal=db-password=your-db-password \ + --from-literal=jwt-secret=your-jwt-secret +``` + +### 5. 部署到集群 + +```bash +# 应用所有配置 +kubectl apply -f k8s/ + +# 查看部署状态 +kubectl get pods -l app=tyapi-server + +# 查看服务状态 +kubectl get services + +# 查看服务日志 +kubectl logs -f deployment/tyapi-server +``` + +## 云平台部署 + +### AWS ECS + +#### 1. 推送镜像到 ECR + +```bash +# 登录 ECR +aws ecr get-login-password --region us-west-2 | docker login --username AWS --password-stdin .dkr.ecr.us-west-2.amazonaws.com + +# 标记镜像 +docker tag tyapi-server:latest .dkr.ecr.us-west-2.amazonaws.com/tyapi-server:latest + +# 推送镜像 +docker push .dkr.ecr.us-west-2.amazonaws.com/tyapi-server:latest +``` + +#### 2. 创建任务定义 + +```json +{ + "family": "tyapi-server", + "networkMode": "awsvpc", + "requiresCompatibilities": ["FARGATE"], + "cpu": "256", + "memory": "512", + "executionRoleArn": "arn:aws:iam::account:role/ecsTaskExecutionRole", + "containerDefinitions": [ + { + "name": "tyapi-server", + "image": ".dkr.ecr.us-west-2.amazonaws.com/tyapi-server:latest", + "portMappings": [ + { + "containerPort": 8080, + "protocol": "tcp" + } + ], + "environment": [ + { + "name": "APP_ENV", + "value": "production" + } + ], + "secrets": [ + { + "name": "DB_PASSWORD", + "valueFrom": "arn:aws:secretsmanager:us-west-2:account:secret:db-password" + } + ] + } + ] +} +``` + +#### 3. 部署服务 + +```bash +# 更新ECS服务 +aws ecs update-service \ + --cluster tyapi-cluster \ + --service tyapi-service \ + --force-new-deployment +``` + +### Google Cloud Run + +```bash +# 推送到 GCR +docker tag tyapi-server:latest gcr.io/your-project/tyapi-server:latest +docker push gcr.io/your-project/tyapi-server:latest + +# 部署到 Cloud Run +gcloud run deploy tyapi-server \ + --image gcr.io/your-project/tyapi-server:latest \ + --platform managed \ + --region us-central1 \ + --allow-unauthenticated \ + --set-env-vars APP_ENV=production \ + --set-secrets DB_PASSWORD=db-password:latest +``` + +### Azure Container Instances + +```bash +# 推送到 ACR +az acr login --name your-registry +docker tag tyapi-server:latest your-registry.azurecr.io/tyapi-server:latest +docker push your-registry.azurecr.io/tyapi-server:latest + +# 部署容器实例 +az container create \ + --resource-group tyapi-rg \ + --name tyapi-server \ + --image your-registry.azurecr.io/tyapi-server:latest \ + --dns-name-label tyapi-server \ + --ports 8080 \ + --environment-variables APP_ENV=production \ + --secure-environment-variables DB_PASSWORD=your-password +``` + +## 负载均衡配置 + +### Nginx 配置 + +创建 `/etc/nginx/sites-available/tyapi-server`: + +```nginx +upstream tyapi_backend { + server 127.0.0.1:8080; + server 127.0.0.1:8081; + server 127.0.0.1:8082; +} + +server { + listen 80; + server_name api.yourdomain.com; + + 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; + + # 超时设置 + proxy_connect_timeout 30s; + proxy_send_timeout 30s; + proxy_read_timeout 30s; + } + + # 健康检查 + location /health { + proxy_pass http://tyapi_backend/api/v1/health; + access_log off; + } +} +``` + +### HAProxy 配置 + +```haproxy +global + daemon + +defaults + mode http + timeout connect 5000ms + timeout client 50000ms + timeout server 50000ms + +frontend tyapi_frontend + bind *:80 + default_backend tyapi_backend + +backend tyapi_backend + balance roundrobin + option httpchk GET /api/v1/health + server app1 127.0.0.1:8080 check + server app2 127.0.0.1:8081 check + server app3 127.0.0.1:8082 check +``` + +## 数据库部署 + +### PostgreSQL 高可用 + +#### 主从配置 + +主库配置 `/etc/postgresql/13/main/postgresql.conf`: + +```conf +# 复制设置 +wal_level = replica +max_wal_senders = 3 +wal_keep_segments = 64 +``` + +从库配置: + +```bash +# 创建从库 +pg_basebackup -h master-host -D /var/lib/postgresql/13/main -U replicator -P -W + +# 配置恢复 +echo "standby_mode = 'on'" >> /var/lib/postgresql/13/main/recovery.conf +echo "primary_conninfo = 'host=master-host port=5432 user=replicator'" >> /var/lib/postgresql/13/main/recovery.conf +``` + +#### 连接池配置 + +使用 PgBouncer: + +```ini +[databases] +tyapi_prod = host=127.0.0.1 port=5432 dbname=tyapi_prod + +[pgbouncer] +listen_port = 6432 +listen_addr = 127.0.0.1 +auth_type = md5 +auth_file = /etc/pgbouncer/userlist.txt +pool_mode = transaction +max_client_conn = 1000 +default_pool_size = 25 +``` + +### Redis 集群 + +```bash +# 启动 Redis 集群 +redis-server redis-7000.conf +redis-server redis-7001.conf +redis-server redis-7002.conf + +# 创建集群 +redis-cli --cluster create 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 --cluster-replicas 0 +``` + +## 监控部署 + +### Prometheus 配置 + +```yaml +global: + scrape_interval: 15s + +scrape_configs: + - job_name: "tyapi-server" + static_configs: + - targets: ["localhost:8080"] + metrics_path: /metrics + scrape_interval: 5s + + - job_name: "postgres" + static_configs: + - targets: ["localhost:9187"] + + - job_name: "redis" + static_configs: + - targets: ["localhost:9121"] +``` + +### Grafana 仪表板 + +导入预配置的仪表板或创建自定义面板监控: + +- 应用性能指标 +- 数据库性能 +- 系统资源使用 +- 错误率和响应时间 + +## SSL/TLS 配置 + +### Let's Encrypt 证书 + +```bash +# 安装 Certbot +sudo apt-get install certbot python3-certbot-nginx + +# 获取证书 +sudo certbot --nginx -d api.yourdomain.com + +# 自动续期 +sudo crontab -e +0 12 * * * /usr/bin/certbot renew --quiet +``` + +### 自签名证书(开发环境) + +```bash +# 生成私钥 +openssl genrsa -out server.key 2048 + +# 生成证书 +openssl req -new -x509 -key server.key -out server.crt -days 365 +``` + +## 部署检查清单 + +### 部署前检查 + +- [ ] 环境变量配置完整 +- [ ] 数据库连接正常 +- [ ] Redis 连接正常 +- [ ] SSL 证书有效 +- [ ] 防火墙规则配置 +- [ ] 监控告警设置 + +### 部署后验证 + +- [ ] 健康检查通过 +- [ ] API 响应正常 +- [ ] 日志输出正常 +- [ ] 监控指标采集 +- [ ] 负载均衡工作 +- [ ] 备份机制测试 + +## 回滚策略 + +### 蓝绿部署 + +```bash +# 部署新版本到绿环境 +kubectl apply -f k8s/green/ + +# 切换流量到绿环境 +kubectl patch service tyapi-service -p '{"spec":{"selector":{"version":"green"}}}' + +# 验证后删除蓝环境 +kubectl delete -f k8s/blue/ +``` + +### 金丝雀发布 + +```bash +# 部署金丝雀版本(10%流量) +kubectl apply -f k8s/canary/ + +# 逐步增加流量 +kubectl patch virtualservice tyapi-vs -p '{"spec":{"http":[{"match":[{"headers":{"canary":{"exact":"true"}}}],"route":[{"destination":{"host":"tyapi-canary"}}]},{"route":[{"destination":{"host":"tyapi-stable"},"weight":90},{"destination":{"host":"tyapi-canary"},"weight":10}]}]}}' +``` diff --git a/env.example b/env.example new file mode 100644 index 0000000..4722f9f --- /dev/null +++ b/env.example @@ -0,0 +1,137 @@ +# =========================================== +# 服务配置 +# =========================================== +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 new file mode 100644 index 0000000..d6aaf1c --- /dev/null +++ b/go.mod @@ -0,0 +1,64 @@ +module tyapi-server + +go 1.23.4 + +require ( + github.com/gin-contrib/cors v1.7.6 + github.com/gin-gonic/gin v1.10.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/redis/go-redis/v9 v9.11.0 + github.com/spf13/viper v1.20.1 + go.uber.org/fx v1.24.0 + go.uber.org/zap v1.27.0 + golang.org/x/crypto v0.39.0 + golang.org/x/time v0.12.0 + gorm.io/driver/postgres v1.6.0 + gorm.io/gorm v1.30.0 +) + +require ( + github.com/bytedance/sonic v1.13.3 // indirect + github.com/bytedance/sonic/loader v0.2.4 // 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-viper/mapstructure/v2 v2.2.1 // indirect + github.com/goccy/go-json v0.10.5 // 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/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/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/pelletier/go-toml/v2 v2.2.4 // 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 + github.com/spf13/cast v1.7.1 // indirect + github.com/spf13/pflag v1.0.6 // indirect + 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.uber.org/dig v1.19.0 // indirect + go.uber.org/multierr v1.10.0 // indirect + golang.org/x/arch v0.18.0 // indirect + golang.org/x/net v0.41.0 // indirect + golang.org/x/sync v0.15.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.26.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..05f3452 --- /dev/null +++ b/go.sum @@ -0,0 +1,155 @@ +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= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0= +github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= +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/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/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/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= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= +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/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-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= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= +github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +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/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +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/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= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= +github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +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.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +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/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= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +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/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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +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/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= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= +github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= +github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= +github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= +github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +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.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= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +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/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/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= +github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +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= +go.uber.org/fx v1.24.0/go.mod h1:AmDeGyS+ZARGKM4tlH4FY2Jr63VjbEDJHtqXTGP5hbo= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +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.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +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.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +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/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= +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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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= +gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= +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= diff --git a/internal/app/app.go b/internal/app/app.go new file mode 100644 index 0000000..1bb3c23 --- /dev/null +++ b/internal/app/app.go @@ -0,0 +1,235 @@ +package app + +import ( + "fmt" + "os" + "os/signal" + "syscall" + + "go.uber.org/zap" + "gorm.io/gorm" + + "tyapi-server/internal/config" + "tyapi-server/internal/container" + "tyapi-server/internal/domains/user/entities" +) + +// Application 应用程序结构 +type Application struct { + container *container.Container + config *config.Config + logger *zap.Logger +} + +// NewApplication 创建新的应用程序实例 +func NewApplication() (*Application, error) { + // 加载配置 + cfg, err := config.LoadConfig() + if err != nil { + return nil, fmt.Errorf("failed to load config: %w", err) + } + + // 创建日志器 + logger, err := createLogger(cfg) + if err != nil { + return nil, fmt.Errorf("failed to create logger: %w", err) + } + + // 创建容器 + cont := container.NewContainer() + + return &Application{ + container: cont, + config: cfg, + logger: logger, + }, nil +} + +// Run 运行应用程序 +func (a *Application) Run() error { + // 打印启动信息 + a.printBanner() + + // 启动容器 + a.logger.Info("Starting application container...") + if err := a.container.Start(); err != nil { + a.logger.Error("Failed to start container", zap.Error(err)) + return err + } + + // 设置优雅关闭 + a.setupGracefulShutdown() + + a.logger.Info("Application started successfully", + zap.String("version", a.config.App.Version), + zap.String("environment", a.config.App.Env), + zap.String("port", a.config.Server.Port)) + + // 等待信号 + return a.waitForShutdown() +} + +// RunMigrations 运行数据库迁移 +func (a *Application) RunMigrations() error { + a.logger.Info("Running database migrations...") + + // 创建数据库连接 + db, err := a.createDatabaseConnection() + if err != nil { + return fmt.Errorf("failed to create database connection: %w", err) + } + + // 自动迁移 + if err := a.autoMigrate(db); err != nil { + return fmt.Errorf("failed to run migrations: %w", err) + } + + a.logger.Info("Database migrations completed successfully") + return nil +} + +// printBanner 打印启动横幅 +func (a *Application) printBanner() { + banner := fmt.Sprintf(` + ╔══════════════════════════════════════════════════════════════╗ + ║ %s ║ + ║ Version: %s ║ + ║ Environment: %s ║ + ║ Port: %s ║ + ╚══════════════════════════════════════════════════════════════╝ + `, + a.config.App.Name, + a.config.App.Version, + a.config.App.Env, + a.config.Server.Port, + ) + + fmt.Println(banner) +} + +// setupGracefulShutdown 设置优雅关闭 +func (a *Application) setupGracefulShutdown() { + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + + go func() { + <-c + a.logger.Info("Received shutdown signal, starting graceful shutdown...") + + // 停止容器 + if err := a.container.Stop(); err != nil { + a.logger.Error("Error during container shutdown", zap.Error(err)) + } + + a.logger.Info("Application shutdown completed") + os.Exit(0) + }() +} + +// waitForShutdown 等待关闭信号 +func (a *Application) waitForShutdown() error { + // 创建一个通道来等待关闭 + done := make(chan bool, 1) + + // 启动一个协程来监听信号 + go func() { + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + <-c + done <- true + }() + + // 等待关闭信号 + <-done + return nil +} + +// createDatabaseConnection 创建数据库连接 +func (a *Application) createDatabaseConnection() (*gorm.DB, error) { + return container.NewDatabase(a.config, a.logger) +} + +// autoMigrate 自动迁移 +func (a *Application) autoMigrate(db *gorm.DB) error { + // 迁移用户相关表 + return db.AutoMigrate( + &entities.User{}, + // 后续可以添加其他实体 + ) +} + +// createLogger 创建日志器 +func createLogger(cfg *config.Config) (*zap.Logger, error) { + level, err := zap.ParseAtomicLevel(cfg.Logger.Level) + if err != nil { + 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"}, + } + + if cfg.Logger.Format == "" { + config.Encoding = "json" + } + if cfg.Logger.Output == "" { + config.OutputPaths = []string{"stdout"} + } + + return config.Build() +} + +// GetConfig 获取配置 +func (a *Application) GetConfig() *config.Config { + return a.config +} + +// GetLogger 获取日志器 +func (a *Application) GetLogger() *zap.Logger { + return a.logger +} + +// HealthCheck 应用程序健康检查 +func (a *Application) HealthCheck() error { + // 这里可以添加应用程序级别的健康检查逻辑 + return nil +} + +// GetVersion 获取版本信息 +func (a *Application) GetVersion() map[string]string { + return map[string]string{ + "name": a.config.App.Name, + "version": a.config.App.Version, + "environment": a.config.App.Env, + "go_version": "1.23.4+", + } +} + +// RunCommand 运行特定命令 +func (a *Application) RunCommand(command string, args ...string) error { + switch command { + case "migrate": + return a.RunMigrations() + case "version": + version := a.GetVersion() + fmt.Printf("Name: %s\n", version["name"]) + fmt.Printf("Version: %s\n", version["version"]) + fmt.Printf("Environment: %s\n", version["environment"]) + fmt.Printf("Go Version: %s\n", version["go_version"]) + return nil + case "health": + if err := a.HealthCheck(); err != nil { + fmt.Printf("Health check failed: %v\n", err) + return err + } + fmt.Println("Application is healthy") + return nil + default: + return fmt.Errorf("unknown command: %s", command) + } +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..4efc283 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,166 @@ +package config + +import ( + "time" +) + +// Config 应用程序总配置 +type Config struct { + Server ServerConfig `mapstructure:"server"` + Database DatabaseConfig `mapstructure:"database"` + Redis RedisConfig `mapstructure:"redis"` + Cache CacheConfig `mapstructure:"cache"` + Logger LoggerConfig `mapstructure:"logger"` + JWT JWTConfig `mapstructure:"jwt"` + RateLimit RateLimitConfig `mapstructure:"ratelimit"` + Monitoring MonitoringConfig `mapstructure:"monitoring"` + Health HealthConfig `mapstructure:"health"` + Resilience ResilienceConfig `mapstructure:"resilience"` + Development DevelopmentConfig `mapstructure:"development"` + App AppConfig `mapstructure:"app"` +} + +// ServerConfig HTTP服务器配置 +type ServerConfig struct { + Port string `mapstructure:"port"` + Mode string `mapstructure:"mode"` + Host string `mapstructure:"host"` + ReadTimeout time.Duration `mapstructure:"read_timeout"` + WriteTimeout time.Duration `mapstructure:"write_timeout"` + IdleTimeout time.Duration `mapstructure:"idle_timeout"` +} + +// DatabaseConfig 数据库配置 +type DatabaseConfig struct { + Host string `mapstructure:"host"` + Port string `mapstructure:"port"` + User string `mapstructure:"user"` + Password string `mapstructure:"password"` + Name string `mapstructure:"name"` + SSLMode string `mapstructure:"sslmode"` + Timezone string `mapstructure:"timezone"` + MaxOpenConns int `mapstructure:"max_open_conns"` + MaxIdleConns int `mapstructure:"max_idle_conns"` + ConnMaxLifetime time.Duration `mapstructure:"conn_max_lifetime"` +} + +// RedisConfig Redis配置 +type RedisConfig struct { + Host string `mapstructure:"host"` + Port string `mapstructure:"port"` + Password string `mapstructure:"password"` + DB int `mapstructure:"db"` + PoolSize int `mapstructure:"pool_size"` + MinIdleConns int `mapstructure:"min_idle_conns"` + MaxRetries int `mapstructure:"max_retries"` + DialTimeout time.Duration `mapstructure:"dial_timeout"` + ReadTimeout time.Duration `mapstructure:"read_timeout"` + WriteTimeout time.Duration `mapstructure:"write_timeout"` +} + +// CacheConfig 缓存配置 +type CacheConfig struct { + DefaultTTL time.Duration `mapstructure:"default_ttl"` + CleanupInterval time.Duration `mapstructure:"cleanup_interval"` + MaxSize int `mapstructure:"max_size"` +} + +// 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"` +} + +// JWTConfig JWT配置 +type JWTConfig struct { + Secret string `mapstructure:"secret"` + ExpiresIn time.Duration `mapstructure:"expires_in"` + RefreshExpiresIn time.Duration `mapstructure:"refresh_expires_in"` +} + +// RateLimitConfig 限流配置 +type RateLimitConfig struct { + Requests int `mapstructure:"requests"` + Window time.Duration `mapstructure:"window"` + Burst int `mapstructure:"burst"` +} + +// MonitoringConfig 监控配置 +type MonitoringConfig struct { + MetricsEnabled bool `mapstructure:"metrics_enabled"` + MetricsPort string `mapstructure:"metrics_port"` + TracingEnabled bool `mapstructure:"tracing_enabled"` + TracingEndpoint string `mapstructure:"tracing_endpoint"` + SampleRate float64 `mapstructure:"sample_rate"` +} + +// HealthConfig 健康检查配置 +type HealthConfig struct { + Enabled bool `mapstructure:"enabled"` + Interval time.Duration `mapstructure:"interval"` + Timeout time.Duration `mapstructure:"timeout"` +} + +// ResilienceConfig 容错配置 +type ResilienceConfig struct { + CircuitBreakerEnabled bool `mapstructure:"circuit_breaker_enabled"` + CircuitBreakerThreshold int `mapstructure:"circuit_breaker_threshold"` + CircuitBreakerTimeout time.Duration `mapstructure:"circuit_breaker_timeout"` + RetryMaxAttempts int `mapstructure:"retry_max_attempts"` + RetryInitialDelay time.Duration `mapstructure:"retry_initial_delay"` + RetryMaxDelay time.Duration `mapstructure:"retry_max_delay"` +} + +// 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"` +} + +// AppConfig 应用程序配置 +type AppConfig struct { + Name string `mapstructure:"name"` + Version string `mapstructure:"version"` + Env string `mapstructure:"env"` +} + +// GetDSN 获取数据库DSN连接字符串 +func (d DatabaseConfig) GetDSN() string { + return "host=" + d.Host + + " user=" + d.User + + " password=" + d.Password + + " dbname=" + d.Name + + " port=" + d.Port + + " sslmode=" + d.SSLMode + + " TimeZone=" + d.Timezone +} + +// GetRedisAddr 获取Redis地址 +func (r RedisConfig) GetRedisAddr() string { + return r.Host + ":" + r.Port +} + +// IsProduction 检查是否为生产环境 +func (a AppConfig) IsProduction() bool { + return a.Env == "production" +} + +// IsDevelopment 检查是否为开发环境 +func (a AppConfig) IsDevelopment() bool { + return a.Env == "development" +} + +// 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 new file mode 100644 index 0000000..7b1ff43 --- /dev/null +++ b/internal/config/loader.go @@ -0,0 +1,311 @@ +package config + +import ( + "fmt" + "os" + "strings" + "time" + + "github.com/spf13/viper" +) + +// LoadConfig 加载应用程序配置 +func LoadConfig() (*Config, error) { + // 设置配置文件名和路径 + viper.SetConfigName("config") + viper.SetConfigType("yaml") + viper.AddConfigPath(".") + viper.AddConfigPath("./configs") + viper.AddConfigPath("$HOME/.tyapi") + + // 设置环境变量前缀 + viper.SetEnvPrefix("") + viper.AutomaticEnv() + + // 配置环境变量键名映射 + setupEnvKeyMapping() + + // 设置默认值 + setDefaults() + + // 尝试读取配置文件(可选) + if err := viper.ReadInConfig(); err != nil { + if _, ok := err.(viper.ConfigFileNotFoundError); !ok { + return nil, fmt.Errorf("读取配置文件失败: %w", err) + } + // 配置文件不存在时使用环境变量和默认值 + } + + var config Config + if err := viper.Unmarshal(&config); err != nil { + return nil, fmt.Errorf("解析配置失败: %w", err) + } + + // 验证配置 + if err := validateConfig(&config); err != nil { + return nil, fmt.Errorf("配置验证失败: %w", err) + } + + 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") + + // 数据库配置 + 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") +} + +// 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") + + // 数据库默认值 + 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") + + // 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") + + // 缓存默认值 + viper.SetDefault("cache.default_ttl", "300s") + viper.SetDefault("cache.cleanup_interval", "600s") + viper.SetDefault("cache.max_size", 1000) + + // 日志默认值 + 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) + + // 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") + + // 限流默认值 + viper.SetDefault("ratelimit.requests", 100) + viper.SetDefault("ratelimit.window", "60s") + viper.SetDefault("ratelimit.burst", 10) + + // 监控默认值 + 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) + + // 健康检查默认值 + viper.SetDefault("health.enabled", true) + viper.SetDefault("health.interval", "30s") + viper.SetDefault("health.timeout", "5s") + + // 容错默认值 + 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") + + // 开发默认值 + 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") +} + +// validateConfig 验证配置 +func validateConfig(config *Config) error { + // 验证必要的配置项 + if config.Database.Host == "" { + return fmt.Errorf("数据库主机地址不能为空") + } + + if config.Database.User == "" { + return fmt.Errorf("数据库用户名不能为空") + } + + if config.Database.Name == "" { + return fmt.Errorf("数据库名称不能为空") + } + + if config.JWT.Secret == "" || config.JWT.Secret == "your-super-secret-jwt-key-change-this-in-production" { + if config.App.IsProduction() { + return fmt.Errorf("生产环境必须设置安全的JWT密钥") + } + } + + // 验证超时配置 + if config.Server.ReadTimeout <= 0 { + return fmt.Errorf("服务器读取超时时间必须大于0") + } + + if config.Server.WriteTimeout <= 0 { + return fmt.Errorf("服务器写入超时时间必须大于0") + } + + // 验证数据库连接池配置 + if config.Database.MaxOpenConns <= 0 { + return fmt.Errorf("数据库最大连接数必须大于0") + } + + if config.Database.MaxIdleConns <= 0 { + return fmt.Errorf("数据库最大空闲连接数必须大于0") + } + + if config.Database.MaxIdleConns > config.Database.MaxOpenConns { + return fmt.Errorf("数据库最大空闲连接数不能大于最大连接数") + } + + return nil +} + +// GetEnv 获取环境变量,如果不存在则返回默认值 +func GetEnv(key, defaultValue string) string { + if value := os.Getenv(key); value != "" { + return value + } + return defaultValue +} + +// ParseDuration 解析时间字符串 +func ParseDuration(s string) time.Duration { + d, err := time.ParseDuration(s) + if err != nil { + return 0 + } + return d +} + +// SplitAndTrim 分割字符串并去除空格 +func SplitAndTrim(s, sep string) []string { + parts := strings.Split(s, sep) + result := make([]string, 0, len(parts)) + for _, part := range parts { + if trimmed := strings.TrimSpace(part); trimmed != "" { + result = append(result, trimmed) + } + } + return result +} diff --git a/internal/container/container.go b/internal/container/container.go new file mode 100644 index 0000000..7ed3475 --- /dev/null +++ b/internal/container/container.go @@ -0,0 +1,441 @@ +package container + +import ( + "context" + "fmt" + "time" + + "github.com/redis/go-redis/v9" + "go.uber.org/fx" + "go.uber.org/zap" + "gorm.io/gorm" + + "tyapi-server/internal/config" + "tyapi-server/internal/domains/user/handlers" + "tyapi-server/internal/domains/user/repositories" + "tyapi-server/internal/domains/user/routes" + "tyapi-server/internal/domains/user/services" + "tyapi-server/internal/shared/cache" + "tyapi-server/internal/shared/database" + "tyapi-server/internal/shared/events" + "tyapi-server/internal/shared/health" + "tyapi-server/internal/shared/http" + "tyapi-server/internal/shared/interfaces" + "tyapi-server/internal/shared/middleware" +) + +// Container 应用容器 +type Container struct { + App *fx.App +} + +// NewContainer 创建新的应用容器 +func NewContainer() *Container { + app := fx.New( + // 配置模块 + fx.Provide( + config.LoadConfig, + ), + + // 基础设施模块 + fx.Provide( + NewLogger, + NewDatabase, + NewRedisClient, + NewRedisCache, + NewEventBus, + NewHealthChecker, + ), + + // HTTP基础组件 + fx.Provide( + NewResponseBuilder, + NewRequestValidator, + NewGinRouter, + ), + + // 中间件组件 + fx.Provide( + NewRequestIDMiddleware, + NewSecurityHeadersMiddleware, + NewResponseTimeMiddleware, + NewCORSMiddleware, + NewRateLimitMiddleware, + NewRequestLoggerMiddleware, + NewJWTAuthMiddleware, + NewOptionalAuthMiddleware, + ), + + // 用户域组件 + fx.Provide( + NewUserRepository, + NewUserService, + NewUserHandler, + NewUserRoutes, + ), + + // 应用生命周期 + fx.Invoke( + RegisterLifecycleHooks, + RegisterMiddlewares, + RegisterRoutes, + ), + ) + + return &Container{App: app} +} + +// Start 启动容器 +func (c *Container) Start() error { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + return c.App.Start(ctx) +} + +// Stop 停止容器 +func (c *Container) Stop() error { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + return c.App.Stop(ctx) +} + +// 基础设施构造函数 + +// NewLogger 创建日志器 +func NewLogger(cfg *config.Config) (*zap.Logger, error) { + level, err := zap.ParseAtomicLevel(cfg.Logger.Level) + if err != nil { + 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"}, + } + + if cfg.Logger.Format == "" { + config.Encoding = "json" + } + if cfg.Logger.Output == "" { + config.OutputPaths = []string{"stdout"} + } + + return config.Build() +} + +// NewDatabase 创建数据库连接 +func NewDatabase(cfg *config.Config, logger *zap.Logger) (*gorm.DB, error) { + dbConfig := database.Config{ + Host: cfg.Database.Host, + Port: cfg.Database.Port, + User: cfg.Database.User, + Password: cfg.Database.Password, + Name: cfg.Database.Name, + SSLMode: cfg.Database.SSLMode, + Timezone: cfg.Database.Timezone, + MaxOpenConns: cfg.Database.MaxOpenConns, + MaxIdleConns: cfg.Database.MaxIdleConns, + ConnMaxLifetime: cfg.Database.ConnMaxLifetime, + } + + db, err := database.NewConnection(dbConfig) + if err != nil { + return nil, err + } + + return db.DB, nil +} + +// NewRedisClient 创建Redis客户端 +func NewRedisClient(cfg *config.Config, logger *zap.Logger) (*redis.Client, error) { + client := redis.NewClient(&redis.Options{ + Addr: cfg.Redis.GetRedisAddr(), + Password: cfg.Redis.Password, + DB: cfg.Redis.DB, + PoolSize: cfg.Redis.PoolSize, + MinIdleConns: cfg.Redis.MinIdleConns, + DialTimeout: cfg.Redis.DialTimeout, + ReadTimeout: cfg.Redis.ReadTimeout, + 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)) + return nil, err + } + + logger.Info("Redis connection established") + return client, nil +} + +// NewRedisCache 创建Redis缓存服务 +func NewRedisCache(client *redis.Client, logger *zap.Logger, cfg *config.Config) interfaces.CacheService { + return cache.NewRedisCache(client, logger, "app") +} + +// NewEventBus 创建事件总线 +func NewEventBus(logger *zap.Logger, cfg *config.Config) interfaces.EventBus { + return events.NewMemoryEventBus(logger, 5) // 默认5个工作协程 +} + +// NewHealthChecker 创建健康检查器 +func NewHealthChecker(logger *zap.Logger) *health.HealthChecker { + return health.NewHealthChecker(logger) +} + +// HTTP组件构造函数 + +// NewResponseBuilder 创建响应构建器 +func NewResponseBuilder() interfaces.ResponseBuilder { + return http.NewResponseBuilder() +} + +// NewRequestValidator 创建请求验证器 +func NewRequestValidator(response interfaces.ResponseBuilder) interfaces.RequestValidator { + return http.NewRequestValidator(response) +} + +// NewGinRouter 创建Gin路由器 +func NewGinRouter(cfg *config.Config, logger *zap.Logger) *http.GinRouter { + return http.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) +} + +// NewRequestLoggerMiddleware 创建请求日志中间件 +func NewRequestLoggerMiddleware(logger *zap.Logger) *middleware.RequestLoggerMiddleware { + return middleware.NewRequestLoggerMiddleware(logger) +} + +// 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) +} + +// 用户域构造函数 + +// NewUserRepository 创建用户仓储 +func NewUserRepository(db *gorm.DB, cache interfaces.CacheService, logger *zap.Logger) *repositories.UserRepository { + return repositories.NewUserRepository(db, cache, logger) +} + +// NewUserService 创建用户服务 +func NewUserService( + repo *repositories.UserRepository, + eventBus interfaces.EventBus, + logger *zap.Logger, +) *services.UserService { + return services.NewUserService(repo, eventBus, logger) +} + +// NewUserHandler 创建用户处理器 +func NewUserHandler( + userService *services.UserService, + response interfaces.ResponseBuilder, + validator interfaces.RequestValidator, + logger *zap.Logger, + jwtAuth *middleware.JWTAuthMiddleware, +) *handlers.UserHandler { + return handlers.NewUserHandler(userService, response, validator, logger, jwtAuth) +} + +// NewUserRoutes 创建用户路由 +func NewUserRoutes( + handler *handlers.UserHandler, + jwtAuth *middleware.JWTAuthMiddleware, + optionalAuth *middleware.OptionalAuthMiddleware, +) *routes.UserRoutes { + return routes.NewUserRoutes(handler, jwtAuth, optionalAuth) +} + +// 注册函数 + +// RegisterMiddlewares 注册中间件 +func RegisterMiddlewares( + router *http.GinRouter, + requestID *middleware.RequestIDMiddleware, + security *middleware.SecurityHeadersMiddleware, + responseTime *middleware.ResponseTimeMiddleware, + cors *middleware.CORSMiddleware, + rateLimit *middleware.RateLimitMiddleware, + requestLogger *middleware.RequestLoggerMiddleware, +) { + // 注册全局中间件 + router.RegisterMiddleware(requestID) + router.RegisterMiddleware(security) + router.RegisterMiddleware(responseTime) + router.RegisterMiddleware(cors) + router.RegisterMiddleware(rateLimit) + router.RegisterMiddleware(requestLogger) +} + +// RegisterRoutes 注册路由 +func RegisterRoutes( + router *http.GinRouter, + userRoutes *routes.UserRoutes, +) { + // 设置默认路由 + router.SetupDefaultRoutes() + + // 注册用户路由 + userRoutes.RegisterRoutes(router.GetEngine()) + userRoutes.RegisterPublicRoutes(router.GetEngine()) + userRoutes.RegisterAdminRoutes(router.GetEngine()) + userRoutes.RegisterHealthRoutes(router.GetEngine()) + + // 打印路由信息 + router.PrintRoutes() +} + +// 生命周期钩子 + +// RegisterLifecycleHooks 注册生命周期钩子 +func RegisterLifecycleHooks( + lc fx.Lifecycle, + logger *zap.Logger, + cfg *config.Config, + db *gorm.DB, + cache interfaces.CacheService, + eventBus interfaces.EventBus, + healthChecker *health.HealthChecker, + router *http.GinRouter, + userService *services.UserService, +) { + lc.Append(fx.Hook{ + OnStart: func(ctx context.Context) error { + logger.Info("Starting application services...") + + // 注册服务到健康检查器 + healthChecker.RegisterService(userService) + + // 初始化缓存服务 + if err := cache.Initialize(ctx); err != nil { + logger.Error("Failed to initialize cache", zap.Error(err)) + return err + } + + // 启动事件总线 + if err := eventBus.Start(ctx); err != nil { + logger.Error("Failed to start event bus", zap.Error(err)) + return err + } + + // 启动健康检查(如果启用) + if cfg.Health.Enabled { + go healthChecker.StartPeriodicCheck(ctx, cfg.Health.Interval) + } + + // 启动HTTP服务器 + 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.Info("All services started successfully") + return nil + }, + OnStop: func(ctx context.Context) error { + logger.Info("Stopping application services...") + + // 停止HTTP服务器 + if err := router.Stop(ctx); err != nil { + logger.Error("Failed to stop HTTP server", zap.Error(err)) + } + + // 停止事件总线 + 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)) + } + } + + logger.Info("All services stopped") + return nil + }, + }) +} + +// ServiceRegistrar 服务注册器接口 +type ServiceRegistrar interface { + RegisterServices() fx.Option +} + +// DomainModule 领域模块接口 +type DomainModule interface { + ServiceRegistrar + GetName() string + GetDependencies() []string +} + +// RegisterDomainModule 注册领域模块 +func RegisterDomainModule(module DomainModule) fx.Option { + return fx.Options( + fx.Provide( + fx.Annotated{ + Name: module.GetName(), + Target: func() DomainModule { + return module + }, + }, + ), + module.RegisterServices(), + ) +} + +// GetContainer 获取容器实例(用于测试或特殊情况) +func GetContainer(cfg *config.Config) *Container { + return NewContainer() +} diff --git a/internal/domains/user/dto/user_dto.go b/internal/domains/user/dto/user_dto.go new file mode 100644 index 0000000..e452593 --- /dev/null +++ b/internal/domains/user/dto/user_dto.go @@ -0,0 +1,173 @@ +package dto + +import ( + "time" + + "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"` +} + +// 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"` +} + +// 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"` +} + +// 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"` +} + +// LoginResponse 登录响应 +type LoginResponse struct { + User *UserResponse `json:"user"` + 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"` +} + +// 转换方法 +func (r *CreateUserRequest) 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, + } +} + +func FromEntity(user *entities.User) *UserResponse { + if user == nil { + return nil + } + + 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, + } +} + +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/user.go b/internal/domains/user/entities/user.go new file mode 100644 index 0000000..015aa75 --- /dev/null +++ b/internal/domains/user/entities/user.go @@ -0,0 +1,138 @@ +package entities + +import ( + "time" + + "gorm.io/gorm" +) + +// 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"` +} + +// 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 +} + +func (u *User) GetCreatedAt() time.Time { + return u.CreatedAt +} + +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.Password == "" { + return NewValidationError("password is required") + } + return nil +} + +// TableName 指定表名 +func (User) TableName() string { + return "users" +} + +// ValidationError 验证错误 +type ValidationError struct { + Message string +} + +func (e *ValidationError) Error() string { + return e.Message +} + +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 new file mode 100644 index 0000000..b09a987 --- /dev/null +++ b/internal/domains/user/events/user_events.go @@ -0,0 +1,299 @@ +package events + +import ( + "encoding/json" + "time" + + "tyapi-server/internal/domains/user/entities" + + "github.com/google/uuid" +) + +// UserEventType 用户事件类型 +type UserEventType string + +const ( + UserCreatedEvent UserEventType = "user.created" + UserUpdatedEvent UserEventType = "user.updated" + UserDeletedEvent UserEventType = "user.deleted" + UserRestoredEvent UserEventType = "user.restored" + 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 用户事件基础结构 +type BaseUserEvent struct { + ID string `json:"id"` + Type string `json:"type"` + Version string `json:"version"` + Timestamp time.Time `json:"timestamp"` + Source string `json:"source"` + AggregateID string `json:"aggregate_id"` + AggregateType string `json:"aggregate_type"` + Metadata map[string]interface{} `json:"metadata"` + Payload interface{} `json:"payload"` + + // DDD特有字段 + DomainVersion string `json:"domain_version"` + CausationID string `json:"causation_id"` + CorrelationID string `json:"correlation_id"` +} + +// 实现 Event 接口 +func (e *BaseUserEvent) GetID() string { + return e.ID +} + +func (e *BaseUserEvent) GetType() string { + return e.Type +} + +func (e *BaseUserEvent) GetVersion() string { + return e.Version +} + +func (e *BaseUserEvent) GetTimestamp() time.Time { + return e.Timestamp +} + +func (e *BaseUserEvent) GetPayload() interface{} { + return e.Payload +} + +func (e *BaseUserEvent) GetMetadata() map[string]interface{} { + return e.Metadata +} + +func (e *BaseUserEvent) GetSource() string { + return e.Source +} + +func (e *BaseUserEvent) GetAggregateID() string { + return e.AggregateID +} + +func (e *BaseUserEvent) GetAggregateType() string { + return e.AggregateType +} + +func (e *BaseUserEvent) GetDomainVersion() string { + return e.DomainVersion +} + +func (e *BaseUserEvent) GetCausationID() string { + return e.CausationID +} + +func (e *BaseUserEvent) GetCorrelationID() string { + return e.CorrelationID +} + +func (e *BaseUserEvent) Marshal() ([]byte, error) { + return json.Marshal(e) +} + +func (e *BaseUserEvent) Unmarshal(data []byte) error { + return json.Unmarshal(data, e) +} + +// UserCreated 用户创建事件 +type UserCreated struct { + *BaseUserEvent + User *entities.User `json:"user"` +} + +func NewUserCreatedEvent(user *entities.User, correlationID string) *UserCreated { + return &UserCreated{ + BaseUserEvent: &BaseUserEvent{ + ID: uuid.New().String(), + Type: string(UserCreatedEvent), + Version: "1.0", + Timestamp: time.Now(), + Source: "user-service", + AggregateID: user.ID, + AggregateType: "User", + DomainVersion: "1.0", + CorrelationID: correlationID, + Metadata: map[string]interface{}{ + "user_id": user.ID, + "username": user.Username, + "email": user.Email, + }, + }, + User: user, + } +} + +func (e *UserCreated) 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"` + IPAddress string `json:"ip_address"` + UserAgent string `json:"user_agent"` +} + +func NewUserLoggedInEvent(userID, username, ipAddress, userAgent, correlationID string) *UserLoggedIn { + return &UserLoggedIn{ + BaseUserEvent: &BaseUserEvent{ + ID: uuid.New().String(), + Type: string(UserLoggedInEvent), + 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, + "ip_address": ipAddress, + "user_agent": userAgent, + }, + }, + UserID: userID, + Username: username, + IPAddress: ipAddress, + UserAgent: userAgent, + } +} + +// UserPasswordChanged 用户密码修改事件 +type UserPasswordChanged struct { + *BaseUserEvent + UserID string `json:"user_id"` + Username string `json:"username"` +} + +func NewUserPasswordChangedEvent(userID, username, correlationID string) *UserPasswordChanged { + return &UserPasswordChanged{ + BaseUserEvent: &BaseUserEvent{ + ID: uuid.New().String(), + Type: string(UserPasswordChangedEvent), + 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, + }, + }, + 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, + } +} diff --git a/internal/domains/user/handlers/user_handler.go b/internal/domains/user/handlers/user_handler.go new file mode 100644 index 0000000..8c9bb85 --- /dev/null +++ b/internal/domains/user/handlers/user_handler.go @@ -0,0 +1,455 @@ +package handlers + +import ( + "strconv" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" + + "tyapi-server/internal/domains/user/dto" + "tyapi-server/internal/domains/user/services" + "tyapi-server/internal/shared/interfaces" + "tyapi-server/internal/shared/middleware" +) + +// UserHandler 用户HTTP处理器 +type UserHandler struct { + userService *services.UserService + response interfaces.ResponseBuilder + validator interfaces.RequestValidator + logger *zap.Logger + jwtAuth *middleware.JWTAuthMiddleware +} + +// NewUserHandler 创建用户处理器 +func NewUserHandler( + userService *services.UserService, + 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, + } +} + +// 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 + + // 验证请求体 + 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)) + 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) + + h.response.Paginated(c, userResponses, pagination) +} + +// Login 用户登录 +func (h *UserHandler) Login(c *gin.Context) { + var req dto.LoginRequest + + // 验证请求体 + if err := h.validator.BindAndValidate(c, &req); err != nil { + return + } + + // 用户登录 + user, err := h.userService.Login(c.Request.Context(), &req) + if err != nil { + h.logger.Error("Login failed", zap.Error(err)) + h.response.Unauthorized(c, "Invalid credentials") + return + } + + // 生成JWT token + accessToken, err := h.jwtAuth.GenerateToken(user.ID, user.Username, user.Email) + if err != nil { + h.logger.Error("Failed to generate token", zap.Error(err)) + h.response.InternalError(c, "Failed to generate access token") + return + } + + // 构建登录响应 + loginResponse := &dto.LoginResponse{ + User: dto.FromEntity(user), + AccessToken: accessToken, + TokenType: "Bearer", + ExpiresIn: 86400, // 24小时,从配置获取 + } + + h.response.Success(c, loginResponse, "Login successful") +} + +// 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 + + // 验证请求体 + if err := h.validator.BindAndValidate(c, &req); err != nil { + return + } + + // 更新用户 + user, err := h.userService.Update(c.Request.Context(), userID, &req) + if err != nil { + h.logger.Error("Failed to update profile", zap.Error(err)) + h.response.BadRequest(c, err.Error()) + return + } + + // 返回响应 + response := dto.FromEntity(user) + h.response.Success(c, response, "Profile updated successfully") +} + +// ChangePassword 修改密码 +func (h *UserHandler) ChangePassword(c *gin.Context) { + userID := h.getCurrentUserID(c) + if userID == "" { + h.response.Unauthorized(c, "User not authenticated") + return + } + + var req dto.ChangePasswordRequest + + // 验证请求体 + if err := h.validator.BindAndValidate(c, &req); err != nil { + return + } + + // 修改密码 + if err := h.userService.ChangePassword(c.Request.Context(), userID, &req); err != nil { + h.logger.Error("Failed to change password", zap.Error(err)) + h.response.BadRequest(c, err.Error()) + return + } + + h.response.Success(c, nil, "Password changed successfully") +} + +// 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 { + if id, ok := userID.(string); ok { + return id + } + } + 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/user_repository.go b/internal/domains/user/repositories/user_repository.go new file mode 100644 index 0000000..4cf3bf1 --- /dev/null +++ b/internal/domains/user/repositories/user_repository.go @@ -0,0 +1,339 @@ +package repositories + +import ( + "context" + "fmt" + "time" + + "go.uber.org/zap" + "gorm.io/gorm" + + "tyapi-server/internal/domains/user/entities" + "tyapi-server/internal/shared/interfaces" +) + +// UserRepository 用户仓储实现 +type UserRepository struct { + db *gorm.DB + cache interfaces.CacheService + logger *zap.Logger +} + +// NewUserRepository 创建用户仓储 +func NewUserRepository(db *gorm.DB, cache interfaces.CacheService, logger *zap.Logger) *UserRepository { + return &UserRepository{ + db: db, + cache: cache, + logger: logger, + } +} + +// 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)) + return err + } + + // 清除相关缓存 + r.invalidateUserCaches(ctx, entity.ID) + + return nil +} + +// GetByID 根据ID获取用户 +func (r *UserRepository) GetByID(ctx context.Context, id string) (*entities.User, error) { + // 先尝试从缓存获取 + cacheKey := r.GetCacheKey(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") + } + return nil, err + } + + // 缓存结果 + r.cache.Set(ctx, cacheKey, &user, 1*time.Hour) + + 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)) + return err + } + + // 清除相关缓存 + r.invalidateUserCaches(ctx, entity.ID) + + return nil +} + +// Delete 删除用户 +func (r *UserRepository) Delete(ctx context.Context, id string) error { + if err := r.db.WithContext(ctx).Delete(&entities.User{}, "id = ?", id).Error; err != nil { + r.logger.Error("Failed to delete user", zap.Error(err)) + return err + } + + // 清除相关缓存 + r.invalidateUserCaches(ctx, 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) + 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 { + 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+"%") + } + + var count int64 + if err := query.Count(&count).Error; err != nil { + return 0, err + } + + return count, nil +} + +// Exists 检查用户是否存在 +func (r *UserRepository) Exists(ctx context.Context, id 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 { + 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, + } +} + +// 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 + } + + 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 new file mode 100644 index 0000000..32bd510 --- /dev/null +++ b/internal/domains/user/routes/user_routes.go @@ -0,0 +1,133 @@ +package routes + +import ( + "tyapi-server/internal/domains/user/handlers" + "tyapi-server/internal/shared/middleware" + + "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") + { + public.POST("/login", r.handler.Login) + public.POST("/register", r.handler.Create) + } + + // 需要认证的路由 + 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", + }) + }) + } +} diff --git a/internal/domains/user/services/user_service.go b/internal/domains/user/services/user_service.go new file mode 100644 index 0000000..771614f --- /dev/null +++ b/internal/domains/user/services/user_service.go @@ -0,0 +1,469 @@ +package services + +import ( + "context" + "fmt" + "time" + + "github.com/google/uuid" + "go.uber.org/zap" + "golang.org/x/crypto/bcrypt" + + "tyapi-server/internal/domains/user/dto" + "tyapi-server/internal/domains/user/entities" + "tyapi-server/internal/domains/user/events" + "tyapi-server/internal/domains/user/repositories" + "tyapi-server/internal/shared/interfaces" +) + +// UserService 用户服务实现 +type UserService struct { + repo *repositories.UserRepository + eventBus interfaces.EventBus + logger *zap.Logger +} + +// NewUserService 创建用户服务 +func NewUserService( + repo *repositories.UserRepository, + eventBus interfaces.EventBus, + logger *zap.Logger, +) *UserService { + return &UserService{ + repo: repo, + eventBus: eventBus, + logger: logger, + } +} + +// Name 返回服务名称 +func (s *UserService) Name() string { + return "user-service" +} + +// Initialize 初始化服务 +func (s *UserService) Initialize(ctx context.Context) error { + s.logger.Info("User service initialized") + return nil +} + +// HealthCheck 健康检查 +func (s *UserService) HealthCheck(ctx context.Context) error { + // 简单检查:尝试查询用户数量 + _, err := s.repo.Count(ctx, interfaces.CountOptions{}) + return err +} + +// Shutdown 关闭服务 +func (s *UserService) Shutdown(ctx context.Context) error { + s.logger.Info("User service shutdown") + 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") + } + + // 验证业务规则 + if err := s.ValidateCreate(ctx, req); err != nil { + return nil, err + } + + // 检查用户名和邮箱是否已存在 + if err := s.checkDuplicates(ctx, req.Username, req.Email); err != nil { + return nil, err + } + + // 创建用户实体 + user := req.ToEntity() + user.ID = uuid.New().String() + + // 加密密码 + hashedPassword, err := s.hashPassword(req.Password) + if err != nil { + return nil, fmt.Errorf("failed to hash password: %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) + } + + // 发布用户创建事件 + event := events.NewUserCreatedEvent(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.Info("User created successfully", + zap.String("user_id", user.ID), + zap.String("username", user.Username)) + + 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) + 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") + } + + // 验证密码 + if !s.checkPassword(loginReq.Password, user.Password) { + return nil, fmt.Errorf("invalid credentials") + } + + // 检查用户状态 + 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, + 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.Info("User logged in successfully", + zap.String("user_id", user.ID), + zap.String("username", user.Username)) + + return user, nil +} + +// ChangePassword 修改密码 +func (s *UserService) ChangePassword(ctx context.Context, userID string, req *dto.ChangePasswordRequest) error { + // 获取用户 + user, err := s.repo.GetByID(ctx, userID) + if err != nil { + return fmt.Errorf("user not found: %w", err) + } + + // 验证旧密码 + if !s.checkPassword(req.OldPassword, user.Password) { + return fmt.Errorf("current password is incorrect") + } + + // 加密新密码 + hashedPassword, err := s.hashPassword(req.NewPassword) + if err != nil { + return fmt.Errorf("failed to hash new password: %w", err) + } + + // 更新密码 + user.Password = hashedPassword + if err := s.repo.Update(ctx, user); err != nil { + return fmt.Errorf("failed to update password: %w", err) + } + + // 发布密码修改事件 + event := events.NewUserPasswordChangedEvent(user.ID, user.Username, 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.Info("Password changed successfully", 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 + } + + // 这里可以并行查询不同状态的用户数量 + // 简化实现,返回基础统计 + return &dto.UserStatsResponse{ + TotalUsers: total, + ActiveUsers: total, // 简化 + InactiveUsers: 0, + SuspendedUsers: 0, + NewUsersToday: 0, + NewUsersWeek: 0, + NewUsersMonth: 0, + }, 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") + } + + // 检查邮箱 + 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) + if err != nil { + return "", err + } + return string(hash), nil +} + +// checkPassword 验证密码 +func (s *UserService) checkPassword(password, hash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + 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") + // 简化的邮箱检查,实际应该使用正则表达式 +} + +// 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 +} + +// 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 + } + } + return uuid.New().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 + } + } + return "unknown" +} + +// 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 + } + } + return "unknown" +} diff --git a/internal/shared/cache/redis_cache.go b/internal/shared/cache/redis_cache.go new file mode 100644 index 0000000..128c34e --- /dev/null +++ b/internal/shared/cache/redis_cache.go @@ -0,0 +1,284 @@ +package cache + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/redis/go-redis/v9" + "go.uber.org/zap" + + "tyapi-server/internal/shared/interfaces" +) + +// RedisCache Redis缓存实现 +type RedisCache struct { + client *redis.Client + logger *zap.Logger + prefix string + + // 统计信息 + hits int64 + misses int64 +} + +// NewRedisCache 创建Redis缓存实例 +func NewRedisCache(client *redis.Client, logger *zap.Logger, prefix string) *RedisCache { + return &RedisCache{ + client: client, + logger: logger, + prefix: prefix, + } +} + +// Name 返回服务名称 +func (r *RedisCache) Name() string { + return "redis-cache" +} + +// Initialize 初始化服务 +func (r *RedisCache) Initialize(ctx context.Context) error { + // 测试连接 + _, err := r.client.Ping(ctx).Result() + if err != nil { + r.logger.Error("Failed to connect to Redis", zap.Error(err)) + return fmt.Errorf("redis connection failed: %w", err) + } + + r.logger.Info("Redis cache service initialized") + return nil +} + +// HealthCheck 健康检查 +func (r *RedisCache) HealthCheck(ctx context.Context) error { + _, err := r.client.Ping(ctx).Result() + return err +} + +// Shutdown 关闭服务 +func (r *RedisCache) Shutdown(ctx context.Context) error { + return r.client.Close() +} + +// Get 获取缓存值 +func (r *RedisCache) Get(ctx context.Context, key string, dest interface{}) error { + fullKey := r.getFullKey(key) + + val, err := r.client.Get(ctx, fullKey).Result() + if err != nil { + if err == redis.Nil { + r.misses++ + return fmt.Errorf("cache miss: key %s not found", key) + } + r.logger.Error("Failed to get cache", zap.String("key", key), zap.Error(err)) + return err + } + + r.hits++ + return json.Unmarshal([]byte(val), dest) +} + +// Set 设置缓存值 +func (r *RedisCache) Set(ctx context.Context, key string, value interface{}, ttl ...interface{}) error { + fullKey := r.getFullKey(key) + + data, err := json.Marshal(value) + if err != nil { + return fmt.Errorf("failed to marshal value: %w", err) + } + + var expiration time.Duration + if len(ttl) > 0 { + switch v := ttl[0].(type) { + case time.Duration: + expiration = v + case int: + expiration = time.Duration(v) * time.Second + case string: + expiration, _ = time.ParseDuration(v) + default: + expiration = 24 * time.Hour // 默认24小时 + } + } else { + expiration = 24 * time.Hour // 默认24小时 + } + + err = r.client.Set(ctx, fullKey, data, expiration).Err() + if err != nil { + r.logger.Error("Failed to set cache", zap.String("key", key), zap.Error(err)) + return err + } + + return nil +} + +// Delete 删除缓存 +func (r *RedisCache) Delete(ctx context.Context, keys ...string) error { + if len(keys) == 0 { + return nil + } + + fullKeys := make([]string, len(keys)) + for i, key := range keys { + fullKeys[i] = r.getFullKey(key) + } + + err := r.client.Del(ctx, fullKeys...).Err() + if err != nil { + r.logger.Error("Failed to delete cache", zap.Strings("keys", keys), zap.Error(err)) + return err + } + + return nil +} + +// Exists 检查键是否存在 +func (r *RedisCache) Exists(ctx context.Context, key string) (bool, error) { + fullKey := r.getFullKey(key) + + count, err := r.client.Exists(ctx, fullKey).Result() + if err != nil { + return false, err + } + + return count > 0, nil +} + +// GetMultiple 批量获取 +func (r *RedisCache) GetMultiple(ctx context.Context, keys []string) (map[string]interface{}, error) { + if len(keys) == 0 { + return make(map[string]interface{}), nil + } + + fullKeys := make([]string, len(keys)) + for i, key := range keys { + fullKeys[i] = r.getFullKey(key) + } + + values, err := r.client.MGet(ctx, fullKeys...).Result() + if err != nil { + return nil, err + } + + result := make(map[string]interface{}) + for i, val := range values { + if val != nil { + var data interface{} + if err := json.Unmarshal([]byte(val.(string)), &data); err == nil { + result[keys[i]] = data + } + } + } + + return result, nil +} + +// SetMultiple 批量设置 +func (r *RedisCache) SetMultiple(ctx context.Context, data map[string]interface{}, ttl ...interface{}) error { + if len(data) == 0 { + return nil + } + + var expiration time.Duration + if len(ttl) > 0 { + switch v := ttl[0].(type) { + case time.Duration: + expiration = v + case int: + expiration = time.Duration(v) * time.Second + default: + expiration = 24 * time.Hour + } + } else { + expiration = 24 * time.Hour + } + + pipe := r.client.Pipeline() + for key, value := range data { + fullKey := r.getFullKey(key) + jsonData, err := json.Marshal(value) + if err != nil { + continue + } + pipe.Set(ctx, fullKey, jsonData, expiration) + } + + _, err := pipe.Exec(ctx) + return err +} + +// DeletePattern 按模式删除 +func (r *RedisCache) DeletePattern(ctx context.Context, pattern string) error { + fullPattern := r.getFullKey(pattern) + + keys, err := r.client.Keys(ctx, fullPattern).Result() + if err != nil { + return err + } + + if len(keys) > 0 { + return r.client.Del(ctx, keys...).Err() + } + + return nil +} + +// Keys 获取匹配的键 +func (r *RedisCache) Keys(ctx context.Context, pattern string) ([]string, error) { + fullPattern := r.getFullKey(pattern) + + keys, err := r.client.Keys(ctx, fullPattern).Result() + if err != nil { + return nil, err + } + + // 移除前缀 + result := make([]string, len(keys)) + prefixLen := len(r.prefix) + 1 // +1 for ":" + for i, key := range keys { + if len(key) > prefixLen { + result[i] = key[prefixLen:] + } else { + result[i] = key + } + } + + return result, nil +} + +// Stats 获取缓存统计 +func (r *RedisCache) Stats(ctx context.Context) (interfaces.CacheStats, error) { + dbSize, _ := r.client.DBSize(ctx).Result() + + return interfaces.CacheStats{ + Hits: r.hits, + Misses: r.misses, + Keys: dbSize, + Memory: 0, // 暂时设为0,后续可解析Redis info + Connections: 0, // 暂时设为0,后续可解析Redis info + }, nil +} + +// getFullKey 获取完整键名 +func (r *RedisCache) getFullKey(key string) string { + if r.prefix == "" { + return key + } + return fmt.Sprintf("%s:%s", r.prefix, key) +} + +// Flush 清空所有缓存 +func (r *RedisCache) Flush(ctx context.Context) error { + if r.prefix == "" { + return r.client.FlushDB(ctx).Err() + } + + // 只删除带前缀的键 + return r.DeletePattern(ctx, "*") +} + +// GetClient 获取原始Redis客户端 +func (r *RedisCache) GetClient() *redis.Client { + return r.client +} diff --git a/internal/shared/database/database.go b/internal/shared/database/database.go new file mode 100644 index 0000000..f972b15 --- /dev/null +++ b/internal/shared/database/database.go @@ -0,0 +1,195 @@ +package database + +import ( + "context" + "fmt" + "time" + + "gorm.io/driver/postgres" + "gorm.io/gorm" + "gorm.io/gorm/logger" + "gorm.io/gorm/schema" +) + +// Config 数据库配置 +type Config struct { + Host string + Port string + User string + Password string + Name string + SSLMode string + Timezone string + MaxOpenConns int + MaxIdleConns int + ConnMaxLifetime time.Duration +} + +// DB 数据库包装器 +type DB struct { + *gorm.DB + config Config +} + +// NewConnection 创建新的数据库连接 +func NewConnection(config Config) (*DB, error) { + // 构建DSN + dsn := buildDSN(config) + + // 配置GORM + gormConfig := &gorm.Config{ + Logger: logger.Default.LogMode(logger.Info), + NamingStrategy: schema.NamingStrategy{ + SingularTable: true, // 使用单数表名 + }, + DisableForeignKeyConstraintWhenMigrating: true, + } + + // 连接数据库 + db, err := gorm.Open(postgres.Open(dsn), gormConfig) + if err != nil { + return nil, fmt.Errorf("连接数据库失败: %w", err) + } + + // 获取底层sql.DB + sqlDB, err := db.DB() + if err != nil { + return nil, fmt.Errorf("获取数据库实例失败: %w", err) + } + + // 配置连接池 + sqlDB.SetMaxOpenConns(config.MaxOpenConns) + sqlDB.SetMaxIdleConns(config.MaxIdleConns) + sqlDB.SetConnMaxLifetime(config.ConnMaxLifetime) + + // 测试连接 + if err := sqlDB.Ping(); err != nil { + return nil, fmt.Errorf("数据库连接测试失败: %w", err) + } + + return &DB{ + DB: db, + config: config, + }, nil +} + +// buildDSN 构建数据库连接字符串 +func buildDSN(config Config) string { + return fmt.Sprintf( + "host=%s user=%s password=%s dbname=%s port=%s sslmode=%s TimeZone=%s", + config.Host, + config.User, + config.Password, + config.Name, + config.Port, + config.SSLMode, + config.Timezone, + ) +} + +// Close 关闭数据库连接 +func (db *DB) Close() error { + sqlDB, err := db.DB.DB() + if err != nil { + return err + } + return sqlDB.Close() +} + +// Ping 检查数据库连接 +func (db *DB) Ping() error { + sqlDB, err := db.DB.DB() + if err != nil { + return err + } + return sqlDB.Ping() +} + +// GetStats 获取连接池统计信息 +func (db *DB) GetStats() (map[string]interface{}, error) { + sqlDB, err := db.DB.DB() + if err != nil { + return nil, err + } + + stats := sqlDB.Stats() + return map[string]interface{}{ + "max_open_connections": stats.MaxOpenConnections, + "open_connections": stats.OpenConnections, + "in_use": stats.InUse, + "idle": stats.Idle, + "wait_count": stats.WaitCount, + "wait_duration": stats.WaitDuration, + "max_idle_closed": stats.MaxIdleClosed, + "max_idle_time_closed": stats.MaxIdleTimeClosed, + "max_lifetime_closed": stats.MaxLifetimeClosed, + }, nil +} + +// BeginTx 开始事务 +func (db *DB) BeginTx() *gorm.DB { + return db.DB.Begin() +} + +// Migrate 执行数据库迁移 +func (db *DB) Migrate(models ...interface{}) error { + return db.DB.AutoMigrate(models...) +} + +// IsHealthy 检查数据库健康状态 +func (db *DB) IsHealthy() bool { + return db.Ping() == nil +} + +// WithContext 返回带上下文的数据库实例 +func (db *DB) WithContext(ctx interface{}) *gorm.DB { + if c, ok := ctx.(context.Context); ok { + return db.DB.WithContext(c) + } + return db.DB +} + +// 事务包装器 +type TxWrapper struct { + tx *gorm.DB +} + +// NewTxWrapper 创建事务包装器 +func (db *DB) NewTxWrapper() *TxWrapper { + return &TxWrapper{ + tx: db.BeginTx(), + } +} + +// Commit 提交事务 +func (tx *TxWrapper) Commit() error { + return tx.tx.Commit().Error +} + +// Rollback 回滚事务 +func (tx *TxWrapper) Rollback() error { + return tx.tx.Rollback().Error +} + +// GetDB 获取事务数据库实例 +func (tx *TxWrapper) GetDB() *gorm.DB { + return tx.tx +} + +// WithTx 在事务中执行函数 +func (db *DB) WithTx(fn func(*gorm.DB) error) error { + tx := db.BeginTx() + defer func() { + if r := recover(); r != nil { + tx.Rollback() + panic(r) + } + }() + + if err := fn(tx); err != nil { + tx.Rollback() + return err + } + + return tx.Commit().Error +} diff --git a/internal/shared/events/event_bus.go b/internal/shared/events/event_bus.go new file mode 100644 index 0000000..e55c2e2 --- /dev/null +++ b/internal/shared/events/event_bus.go @@ -0,0 +1,313 @@ +package events + +import ( + "context" + "fmt" + "sync" + "time" + + "go.uber.org/zap" + + "tyapi-server/internal/shared/interfaces" +) + +// MemoryEventBus 内存事件总线实现 +type MemoryEventBus struct { + subscribers map[string][]interfaces.EventHandler + mutex sync.RWMutex + logger *zap.Logger + running bool + stopCh chan struct{} + eventQueue chan eventTask + workerCount int +} + +// eventTask 事件任务 +type eventTask struct { + event interfaces.Event + handler interfaces.EventHandler + retries int +} + +// NewMemoryEventBus 创建内存事件总线 +func NewMemoryEventBus(logger *zap.Logger, workerCount int) *MemoryEventBus { + if workerCount <= 0 { + workerCount = 5 // 默认5个工作协程 + } + + return &MemoryEventBus{ + subscribers: make(map[string][]interfaces.EventHandler), + logger: logger, + eventQueue: make(chan eventTask, 1000), // 缓冲1000个事件 + workerCount: workerCount, + stopCh: make(chan struct{}), + } +} + +// Name 返回服务名称 +func (bus *MemoryEventBus) Name() string { + return "memory-event-bus" +} + +// Initialize 初始化服务 +func (bus *MemoryEventBus) Initialize(ctx context.Context) error { + bus.logger.Info("Memory event bus service initialized") + return nil +} + +// HealthCheck 健康检查 +func (bus *MemoryEventBus) HealthCheck(ctx context.Context) error { + if !bus.running { + return fmt.Errorf("event bus is not running") + } + return nil +} + +// Shutdown 关闭服务 +func (bus *MemoryEventBus) Shutdown(ctx context.Context) error { + bus.Stop(ctx) + return nil +} + +// Start 启动事件总线 +func (bus *MemoryEventBus) Start(ctx context.Context) error { + bus.mutex.Lock() + defer bus.mutex.Unlock() + + if bus.running { + return nil + } + + bus.running = true + + // 启动工作协程 + for i := 0; i < bus.workerCount; i++ { + go bus.worker(i) + } + + bus.logger.Info("Event bus started", zap.Int("workers", bus.workerCount)) + return nil +} + +// Stop 停止事件总线 +func (bus *MemoryEventBus) Stop(ctx context.Context) error { + bus.mutex.Lock() + defer bus.mutex.Unlock() + + if !bus.running { + return nil + } + + bus.running = false + close(bus.stopCh) + + // 等待所有工作协程结束或超时 + done := make(chan struct{}) + go func() { + time.Sleep(5 * time.Second) // 给工作协程5秒时间结束 + close(done) + }() + + select { + case <-done: + case <-ctx.Done(): + } + + bus.logger.Info("Event bus stopped") + return nil +} + +// Publish 发布事件(同步) +func (bus *MemoryEventBus) Publish(ctx context.Context, event interfaces.Event) error { + bus.mutex.RLock() + handlers := bus.subscribers[event.GetType()] + bus.mutex.RUnlock() + + if len(handlers) == 0 { + bus.logger.Debug("No handlers for event type", zap.String("type", event.GetType())) + return nil + } + + for _, handler := range handlers { + if handler.IsAsync() { + // 异步处理 + select { + case bus.eventQueue <- eventTask{event: event, handler: handler, retries: 0}: + default: + bus.logger.Warn("Event queue is full, dropping event", + zap.String("type", event.GetType()), + zap.String("handler", handler.GetName())) + } + } else { + // 同步处理 + if err := bus.handleEventWithRetry(ctx, event, handler); err != nil { + bus.logger.Error("Failed to handle event synchronously", + zap.String("type", event.GetType()), + zap.String("handler", handler.GetName()), + zap.Error(err)) + } + } + } + + return nil +} + +// PublishBatch 批量发布事件 +func (bus *MemoryEventBus) PublishBatch(ctx context.Context, events []interfaces.Event) error { + for _, event := range events { + if err := bus.Publish(ctx, event); err != nil { + return err + } + } + return nil +} + +// Subscribe 订阅事件 +func (bus *MemoryEventBus) Subscribe(eventType string, handler interfaces.EventHandler) error { + bus.mutex.Lock() + defer bus.mutex.Unlock() + + handlers := bus.subscribers[eventType] + + // 检查是否已经订阅 + for _, h := range handlers { + if h.GetName() == handler.GetName() { + return fmt.Errorf("handler %s already subscribed to event type %s", handler.GetName(), eventType) + } + } + + bus.subscribers[eventType] = append(handlers, handler) + + bus.logger.Info("Handler subscribed to event", + zap.String("handler", handler.GetName()), + zap.String("event_type", eventType)) + + return nil +} + +// Unsubscribe 取消订阅 +func (bus *MemoryEventBus) Unsubscribe(eventType string, handler interfaces.EventHandler) error { + bus.mutex.Lock() + defer bus.mutex.Unlock() + + handlers := bus.subscribers[eventType] + for i, h := range handlers { + if h.GetName() == handler.GetName() { + // 删除处理器 + bus.subscribers[eventType] = append(handlers[:i], handlers[i+1:]...) + + bus.logger.Info("Handler unsubscribed from event", + zap.String("handler", handler.GetName()), + zap.String("event_type", eventType)) + + return nil + } + } + + return fmt.Errorf("handler %s not found for event type %s", handler.GetName(), eventType) +} + +// GetSubscribers 获取订阅者 +func (bus *MemoryEventBus) GetSubscribers(eventType string) []interfaces.EventHandler { + bus.mutex.RLock() + defer bus.mutex.RUnlock() + + handlers := bus.subscribers[eventType] + result := make([]interfaces.EventHandler, len(handlers)) + copy(result, handlers) + + return result +} + +// worker 工作协程 +func (bus *MemoryEventBus) worker(id int) { + bus.logger.Debug("Event worker started", zap.Int("worker_id", id)) + + for { + select { + case task := <-bus.eventQueue: + bus.processEventTask(task) + case <-bus.stopCh: + bus.logger.Debug("Event worker stopped", zap.Int("worker_id", id)) + return + } + } +} + +// processEventTask 处理事件任务 +func (bus *MemoryEventBus) processEventTask(task eventTask) { + ctx := context.Background() + + err := bus.handleEventWithRetry(ctx, task.event, task.handler) + if err != nil { + retryConfig := task.handler.GetRetryConfig() + + if task.retries < retryConfig.MaxRetries { + // 重试 + delay := time.Duration(float64(retryConfig.RetryDelay) * + (1 + retryConfig.BackoffFactor*float64(task.retries))) + + if delay > retryConfig.MaxDelay { + delay = retryConfig.MaxDelay + } + + go func() { + time.Sleep(delay) + task.retries++ + + select { + case bus.eventQueue <- task: + default: + bus.logger.Error("Failed to requeue event for retry", + zap.String("type", task.event.GetType()), + zap.String("handler", task.handler.GetName()), + zap.Int("retries", task.retries)) + } + }() + } else { + bus.logger.Error("Event processing failed after max retries", + zap.String("type", task.event.GetType()), + zap.String("handler", task.handler.GetName()), + zap.Int("retries", task.retries), + zap.Error(err)) + } + } +} + +// handleEventWithRetry 处理事件并支持重试 +func (bus *MemoryEventBus) handleEventWithRetry(ctx context.Context, event interfaces.Event, handler interfaces.EventHandler) error { + start := time.Now() + + defer func() { + duration := time.Since(start) + bus.logger.Debug("Event handled", + zap.String("type", event.GetType()), + zap.String("handler", handler.GetName()), + zap.Duration("duration", duration)) + }() + + return handler.Handle(ctx, event) +} + +// GetStats 获取事件总线统计信息 +func (bus *MemoryEventBus) GetStats() map[string]interface{} { + bus.mutex.RLock() + defer bus.mutex.RUnlock() + + stats := map[string]interface{}{ + "running": bus.running, + "worker_count": bus.workerCount, + "queue_length": len(bus.eventQueue), + "queue_capacity": cap(bus.eventQueue), + "event_types": len(bus.subscribers), + } + + // 各事件类型的订阅者数量 + eventTypes := make(map[string]int) + for eventType, handlers := range bus.subscribers { + eventTypes[eventType] = len(handlers) + } + stats["subscribers"] = eventTypes + + return stats +} diff --git a/internal/shared/health/health_checker.go b/internal/shared/health/health_checker.go new file mode 100644 index 0000000..64c8e88 --- /dev/null +++ b/internal/shared/health/health_checker.go @@ -0,0 +1,282 @@ +package health + +import ( + "context" + "fmt" + "sync" + "time" + + "tyapi-server/internal/shared/interfaces" + + "go.uber.org/zap" +) + +// HealthChecker 健康检查器实现 +type HealthChecker struct { + services map[string]interfaces.Service + cache map[string]*interfaces.HealthStatus + cacheTTL time.Duration + mutex sync.RWMutex + logger *zap.Logger +} + +// NewHealthChecker 创建健康检查器 +func NewHealthChecker(logger *zap.Logger) *HealthChecker { + return &HealthChecker{ + services: make(map[string]interfaces.Service), + cache: make(map[string]*interfaces.HealthStatus), + cacheTTL: 30 * time.Second, // 缓存30秒 + logger: logger, + } +} + +// RegisterService 注册服务 +func (h *HealthChecker) RegisterService(service interfaces.Service) { + h.mutex.Lock() + defer h.mutex.Unlock() + + h.services[service.Name()] = service + h.logger.Info("Registered service for health check", zap.String("service", service.Name())) +} + +// CheckHealth 检查单个服务健康状态 +func (h *HealthChecker) CheckHealth(ctx context.Context, serviceName string) *interfaces.HealthStatus { + h.mutex.RLock() + service, exists := h.services[serviceName] + if !exists { + h.mutex.RUnlock() + return &interfaces.HealthStatus{ + Status: "DOWN", + Message: "Service not found", + Details: map[string]interface{}{"error": "service not registered"}, + CheckedAt: time.Now().Unix(), + ResponseTime: 0, + } + } + + // 检查缓存 + if cached, exists := h.cache[serviceName]; exists { + if time.Since(time.Unix(cached.CheckedAt, 0)) < h.cacheTTL { + h.mutex.RUnlock() + return cached + } + } + h.mutex.RUnlock() + + // 执行健康检查 + start := time.Now() + status := &interfaces.HealthStatus{ + CheckedAt: start.Unix(), + } + + // 设置超时上下文 + checkCtx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + + err := service.HealthCheck(checkCtx) + responseTime := time.Since(start).Milliseconds() + status.ResponseTime = responseTime + + if err != nil { + status.Status = "DOWN" + status.Message = "Health check failed" + status.Details = map[string]interface{}{ + "error": err.Error(), + "service_name": serviceName, + "check_time": start.Format(time.RFC3339), + } + h.logger.Warn("Service health check failed", + zap.String("service", serviceName), + zap.Error(err), + zap.Int64("response_time_ms", responseTime)) + } else { + status.Status = "UP" + status.Message = "Service is healthy" + status.Details = map[string]interface{}{ + "service_name": serviceName, + "check_time": start.Format(time.RFC3339), + } + h.logger.Debug("Service health check passed", + zap.String("service", serviceName), + zap.Int64("response_time_ms", responseTime)) + } + + // 更新缓存 + h.mutex.Lock() + h.cache[serviceName] = status + h.mutex.Unlock() + + return status +} + +// CheckAllHealth 检查所有服务的健康状态 +func (h *HealthChecker) CheckAllHealth(ctx context.Context) map[string]*interfaces.HealthStatus { + h.mutex.RLock() + serviceNames := make([]string, 0, len(h.services)) + for name := range h.services { + serviceNames = append(serviceNames, name) + } + h.mutex.RUnlock() + + results := make(map[string]*interfaces.HealthStatus) + var wg sync.WaitGroup + var mutex sync.Mutex + + // 并发检查所有服务 + for _, serviceName := range serviceNames { + wg.Add(1) + go func(name string) { + defer wg.Done() + status := h.CheckHealth(ctx, name) + + mutex.Lock() + results[name] = status + mutex.Unlock() + }(serviceName) + } + + wg.Wait() + return results +} + +// GetOverallStatus 获取整体健康状态 +func (h *HealthChecker) GetOverallStatus(ctx context.Context) *interfaces.HealthStatus { + allStatus := h.CheckAllHealth(ctx) + + overall := &interfaces.HealthStatus{ + CheckedAt: time.Now().Unix(), + ResponseTime: 0, + Details: make(map[string]interface{}), + } + + var totalResponseTime int64 + healthyCount := 0 + totalCount := len(allStatus) + + for serviceName, status := range allStatus { + overall.Details[serviceName] = map[string]interface{}{ + "status": status.Status, + "message": status.Message, + "response_time": status.ResponseTime, + } + + totalResponseTime += status.ResponseTime + if status.Status == "UP" { + healthyCount++ + } + } + + if totalCount > 0 { + overall.ResponseTime = totalResponseTime / int64(totalCount) + } + + // 确定整体状态 + if healthyCount == totalCount { + overall.Status = "UP" + overall.Message = "All services are healthy" + } else if healthyCount == 0 { + overall.Status = "DOWN" + overall.Message = "All services are down" + } else { + overall.Status = "DEGRADED" + overall.Message = fmt.Sprintf("%d of %d services are healthy", healthyCount, totalCount) + } + + return overall +} + +// GetServiceNames 获取所有注册的服务名称 +func (h *HealthChecker) GetServiceNames() []string { + h.mutex.RLock() + defer h.mutex.RUnlock() + + names := make([]string, 0, len(h.services)) + for name := range h.services { + names = append(names, name) + } + return names +} + +// RemoveService 移除服务 +func (h *HealthChecker) RemoveService(serviceName string) { + h.mutex.Lock() + defer h.mutex.Unlock() + + delete(h.services, serviceName) + delete(h.cache, serviceName) + + h.logger.Info("Removed service from health check", zap.String("service", serviceName)) +} + +// ClearCache 清除缓存 +func (h *HealthChecker) ClearCache() { + h.mutex.Lock() + defer h.mutex.Unlock() + + h.cache = make(map[string]*interfaces.HealthStatus) + h.logger.Debug("Health check cache cleared") +} + +// GetCacheStats 获取缓存统计 +func (h *HealthChecker) GetCacheStats() map[string]interface{} { + h.mutex.RLock() + defer h.mutex.RUnlock() + + stats := map[string]interface{}{ + "total_services": len(h.services), + "cached_results": len(h.cache), + "cache_ttl_seconds": h.cacheTTL.Seconds(), + } + + // 计算缓存命中率 + if len(h.services) > 0 { + hitRate := float64(len(h.cache)) / float64(len(h.services)) * 100 + stats["cache_hit_rate"] = fmt.Sprintf("%.2f%%", hitRate) + } + + return stats +} + +// SetCacheTTL 设置缓存TTL +func (h *HealthChecker) SetCacheTTL(ttl time.Duration) { + h.mutex.Lock() + defer h.mutex.Unlock() + + h.cacheTTL = ttl + h.logger.Info("Updated health check cache TTL", zap.Duration("ttl", ttl)) +} + +// StartPeriodicCheck 启动定期健康检查 +func (h *HealthChecker) StartPeriodicCheck(ctx context.Context, interval time.Duration) { + ticker := time.NewTicker(interval) + defer ticker.Stop() + + h.logger.Info("Started periodic health check", zap.Duration("interval", interval)) + + for { + select { + case <-ctx.Done(): + h.logger.Info("Stopped periodic health check") + return + case <-ticker.C: + h.performPeriodicCheck(ctx) + } + } +} + +// performPeriodicCheck 执行定期检查 +func (h *HealthChecker) performPeriodicCheck(ctx context.Context) { + overall := h.GetOverallStatus(ctx) + + h.logger.Info("Periodic health check completed", + 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", + zap.String("status", overall.Status), + zap.Any("details", overall.Details)) + } +} diff --git a/internal/shared/http/response.go b/internal/shared/http/response.go new file mode 100644 index 0000000..53ae802 --- /dev/null +++ b/internal/shared/http/response.go @@ -0,0 +1,260 @@ +package http + +import ( + "math" + "net/http" + "time" + + "tyapi-server/internal/shared/interfaces" + + "github.com/gin-gonic/gin" +) + +// ResponseBuilder 响应构建器实现 +type ResponseBuilder struct{} + +// NewResponseBuilder 创建响应构建器 +func NewResponseBuilder() interfaces.ResponseBuilder { + return &ResponseBuilder{} +} + +// Success 成功响应 +func (r *ResponseBuilder) Success(c *gin.Context, data interface{}, message ...string) { + msg := "Success" + if len(message) > 0 && message[0] != "" { + msg = message[0] + } + + response := interfaces.APIResponse{ + Success: true, + Message: msg, + Data: data, + RequestID: r.getRequestID(c), + Timestamp: time.Now().Unix(), + } + + c.JSON(http.StatusOK, response) +} + +// Created 创建成功响应 +func (r *ResponseBuilder) Created(c *gin.Context, data interface{}, message ...string) { + msg := "Created successfully" + if len(message) > 0 && message[0] != "" { + msg = message[0] + } + + response := interfaces.APIResponse{ + Success: true, + Message: msg, + Data: data, + RequestID: r.getRequestID(c), + Timestamp: time.Now().Unix(), + } + + c.JSON(http.StatusCreated, response) +} + +// Error 错误响应 +func (r *ResponseBuilder) Error(c *gin.Context, err error) { + // 根据错误类型确定状态码 + statusCode := http.StatusInternalServerError + message := "Internal server error" + errorDetail := err.Error() + + // 这里可以根据不同的错误类型设置不同的状态码 + // 例如:ValidationError -> 400, NotFoundError -> 404, etc. + + response := interfaces.APIResponse{ + Success: false, + Message: message, + Errors: errorDetail, + RequestID: r.getRequestID(c), + Timestamp: time.Now().Unix(), + } + + c.JSON(statusCode, response) +} + +// BadRequest 400错误响应 +func (r *ResponseBuilder) BadRequest(c *gin.Context, message string, errors ...interface{}) { + response := interfaces.APIResponse{ + Success: false, + Message: message, + RequestID: r.getRequestID(c), + Timestamp: time.Now().Unix(), + } + + if len(errors) > 0 { + response.Errors = errors[0] + } + + c.JSON(http.StatusBadRequest, response) +} + +// Unauthorized 401错误响应 +func (r *ResponseBuilder) Unauthorized(c *gin.Context, message ...string) { + msg := "Unauthorized" + if len(message) > 0 && message[0] != "" { + msg = message[0] + } + + response := interfaces.APIResponse{ + Success: false, + Message: msg, + RequestID: r.getRequestID(c), + Timestamp: time.Now().Unix(), + } + + c.JSON(http.StatusUnauthorized, response) +} + +// Forbidden 403错误响应 +func (r *ResponseBuilder) Forbidden(c *gin.Context, message ...string) { + msg := "Forbidden" + if len(message) > 0 && message[0] != "" { + msg = message[0] + } + + response := interfaces.APIResponse{ + Success: false, + Message: msg, + RequestID: r.getRequestID(c), + Timestamp: time.Now().Unix(), + } + + c.JSON(http.StatusForbidden, response) +} + +// NotFound 404错误响应 +func (r *ResponseBuilder) NotFound(c *gin.Context, message ...string) { + msg := "Resource not found" + if len(message) > 0 && message[0] != "" { + msg = message[0] + } + + response := interfaces.APIResponse{ + Success: false, + Message: msg, + RequestID: r.getRequestID(c), + Timestamp: time.Now().Unix(), + } + + c.JSON(http.StatusNotFound, response) +} + +// Conflict 409错误响应 +func (r *ResponseBuilder) Conflict(c *gin.Context, message string) { + response := interfaces.APIResponse{ + Success: false, + Message: message, + RequestID: r.getRequestID(c), + Timestamp: time.Now().Unix(), + } + + c.JSON(http.StatusConflict, response) +} + +// InternalError 500错误响应 +func (r *ResponseBuilder) InternalError(c *gin.Context, message ...string) { + msg := "Internal server error" + if len(message) > 0 && message[0] != "" { + msg = message[0] + } + + response := interfaces.APIResponse{ + Success: false, + Message: msg, + RequestID: r.getRequestID(c), + Timestamp: time.Now().Unix(), + } + + c.JSON(http.StatusInternalServerError, response) +} + +// Paginated 分页响应 +func (r *ResponseBuilder) Paginated(c *gin.Context, data interface{}, pagination interfaces.PaginationMeta) { + response := interfaces.APIResponse{ + Success: true, + Message: "Success", + Data: data, + Pagination: &pagination, + RequestID: r.getRequestID(c), + Timestamp: time.Now().Unix(), + } + + c.JSON(http.StatusOK, response) +} + +// getRequestID 从上下文获取请求ID +func (r *ResponseBuilder) getRequestID(c *gin.Context) string { + if requestID, exists := c.Get("request_id"); exists { + if id, ok := requestID.(string); ok { + return id + } + } + return "" +} + +// BuildPagination 构建分页元数据 +func BuildPagination(page, pageSize int, total int64) interfaces.PaginationMeta { + totalPages := int(math.Ceil(float64(total) / float64(pageSize))) + + if totalPages < 1 { + totalPages = 1 + } + + return interfaces.PaginationMeta{ + Page: page, + PageSize: pageSize, + Total: total, + TotalPages: totalPages, + HasNext: page < totalPages, + HasPrev: page > 1, + } +} + +// CustomResponse 自定义响应 +func (r *ResponseBuilder) CustomResponse(c *gin.Context, statusCode int, data interface{}) { + response := interfaces.APIResponse{ + Success: statusCode >= 200 && statusCode < 300, + Message: http.StatusText(statusCode), + Data: data, + RequestID: r.getRequestID(c), + Timestamp: time.Now().Unix(), + } + + c.JSON(statusCode, response) +} + +// ValidationError 验证错误响应 +func (r *ResponseBuilder) ValidationError(c *gin.Context, errors interface{}) { + response := interfaces.APIResponse{ + Success: false, + Message: "Validation failed", + Errors: errors, + RequestID: r.getRequestID(c), + Timestamp: time.Now().Unix(), + } + + c.JSON(http.StatusUnprocessableEntity, response) +} + +// TooManyRequests 限流错误响应 +func (r *ResponseBuilder) TooManyRequests(c *gin.Context, message ...string) { + msg := "Too many requests" + if len(message) > 0 && message[0] != "" { + msg = message[0] + } + + response := interfaces.APIResponse{ + Success: false, + Message: msg, + RequestID: r.getRequestID(c), + Timestamp: time.Now().Unix(), + Meta: map[string]interface{}{ + "retry_after": "60s", + }, + } + + c.JSON(http.StatusTooManyRequests, response) +} diff --git a/internal/shared/http/router.go b/internal/shared/http/router.go new file mode 100644 index 0000000..12559d8 --- /dev/null +++ b/internal/shared/http/router.go @@ -0,0 +1,258 @@ +package http + +import ( + "context" + "fmt" + "net/http" + "sort" + "time" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" + + "tyapi-server/internal/config" + "tyapi-server/internal/shared/interfaces" +) + +// GinRouter Gin路由器实现 +type GinRouter struct { + engine *gin.Engine + config *config.Config + logger *zap.Logger + middlewares []interfaces.Middleware + server *http.Server +} + +// NewGinRouter 创建Gin路由器 +func NewGinRouter(cfg *config.Config, logger *zap.Logger) *GinRouter { + // 设置Gin模式 + if cfg.App.IsProduction() { + gin.SetMode(gin.ReleaseMode) + } else { + gin.SetMode(gin.DebugMode) + } + + // 创建Gin引擎 + engine := gin.New() + + return &GinRouter{ + engine: engine, + config: cfg, + logger: logger, + middlewares: make([]interfaces.Middleware, 0), + } +} + +// RegisterHandler 注册处理器 +func (r *GinRouter) RegisterHandler(handler interfaces.HTTPHandler) error { + // 应用处理器中间件 + middlewares := handler.GetMiddlewares() + + // 注册路由 + r.engine.Handle(handler.GetMethod(), handler.GetPath(), append(middlewares, handler.Handle)...) + + r.logger.Info("Registered HTTP handler", + zap.String("method", handler.GetMethod()), + zap.String("path", handler.GetPath())) + + return nil +} + +// RegisterMiddleware 注册中间件 +func (r *GinRouter) RegisterMiddleware(middleware interfaces.Middleware) error { + r.middlewares = append(r.middlewares, middleware) + + r.logger.Info("Registered middleware", + zap.String("name", middleware.GetName()), + zap.Int("priority", middleware.GetPriority())) + + return nil +} + +// RegisterGroup 注册路由组 +func (r *GinRouter) RegisterGroup(prefix string, middlewares ...gin.HandlerFunc) gin.IRoutes { + return r.engine.Group(prefix, middlewares...) +} + +// GetRoutes 获取路由信息 +func (r *GinRouter) GetRoutes() gin.RoutesInfo { + return r.engine.Routes() +} + +// Start 启动路由器 +func (r *GinRouter) Start(addr string) error { + // 应用中间件(按优先级排序) + r.applyMiddlewares() + + // 创建HTTP服务器 + r.server = &http.Server{ + Addr: addr, + Handler: r.engine, + ReadTimeout: r.config.Server.ReadTimeout, + WriteTimeout: r.config.Server.WriteTimeout, + IdleTimeout: r.config.Server.IdleTimeout, + } + + r.logger.Info("Starting HTTP server", zap.String("addr", addr)) + + // 启动服务器 + if err := r.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + return fmt.Errorf("failed to start server: %w", err) + } + + return nil +} + +// Stop 停止路由器 +func (r *GinRouter) Stop(ctx context.Context) error { + if r.server == nil { + return nil + } + + r.logger.Info("Stopping HTTP server...") + + // 优雅关闭服务器 + if err := r.server.Shutdown(ctx); err != nil { + r.logger.Error("Failed to shutdown server gracefully", zap.Error(err)) + return err + } + + r.logger.Info("HTTP server stopped") + return nil +} + +// GetEngine 获取Gin引擎 +func (r *GinRouter) GetEngine() *gin.Engine { + return r.engine +} + +// applyMiddlewares 应用中间件 +func (r *GinRouter) applyMiddlewares() { + // 按优先级排序中间件 + sort.Slice(r.middlewares, func(i, j int) bool { + return r.middlewares[i].GetPriority() > r.middlewares[j].GetPriority() + }) + + // 应用全局中间件 + for _, middleware := range r.middlewares { + if middleware.IsGlobal() { + r.engine.Use(middleware.Handle()) + r.logger.Debug("Applied global middleware", + zap.String("name", middleware.GetName()), + zap.Int("priority", middleware.GetPriority())) + } + } +} + +// SetupDefaultRoutes 设置默认路由 +func (r *GinRouter) SetupDefaultRoutes() { + // 健康检查 + r.engine.GET("/health", 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, + }) + }) + + // API信息 + r.engine.GET("/info", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "name": r.config.App.Name, + "version": r.config.App.Version, + "environment": r.config.App.Env, + "timestamp": time.Now().Unix(), + }) + }) + + // 404处理 + r.engine.NoRoute(func(c *gin.Context) { + c.JSON(http.StatusNotFound, gin.H{ + "success": false, + "message": "Route not found", + "path": c.Request.URL.Path, + "method": c.Request.Method, + "timestamp": time.Now().Unix(), + }) + }) + + // 405处理 + r.engine.NoMethod(func(c *gin.Context) { + c.JSON(http.StatusMethodNotAllowed, gin.H{ + "success": false, + "message": "Method not allowed", + "path": c.Request.URL.Path, + "method": c.Request.Method, + "timestamp": time.Now().Unix(), + }) + }) +} + +// PrintRoutes 打印路由信息 +func (r *GinRouter) PrintRoutes() { + routes := r.GetRoutes() + + r.logger.Info("Registered routes:") + for _, route := range routes { + r.logger.Info("Route", + zap.String("method", route.Method), + zap.String("path", route.Path), + zap.String("handler", route.Handler)) + } +} + +// GetStats 获取路由器统计信息 +func (r *GinRouter) GetStats() map[string]interface{} { + routes := r.GetRoutes() + + stats := map[string]interface{}{ + "total_routes": len(routes), + "total_middlewares": len(r.middlewares), + "server_config": map[string]interface{}{ + "read_timeout": r.config.Server.ReadTimeout, + "write_timeout": r.config.Server.WriteTimeout, + "idle_timeout": r.config.Server.IdleTimeout, + }, + } + + // 按方法统计路由数量 + methodStats := make(map[string]int) + for _, route := range routes { + methodStats[route.Method]++ + } + stats["routes_by_method"] = methodStats + + // 中间件统计 + middlewareStats := make([]map[string]interface{}, 0, len(r.middlewares)) + for _, middleware := range r.middlewares { + middlewareStats = append(middlewareStats, map[string]interface{}{ + "name": middleware.GetName(), + "priority": middleware.GetPriority(), + "global": middleware.IsGlobal(), + }) + } + stats["middlewares"] = middlewareStats + + return stats +} + +// EnableMetrics 启用指标收集 +func (r *GinRouter) EnableMetrics(collector interfaces.MetricsCollector) { + r.engine.Use(func(c *gin.Context) { + start := time.Now() + + c.Next() + + duration := time.Since(start).Seconds() + collector.RecordHTTPRequest(c.Request.Method, c.FullPath(), c.Writer.Status(), duration) + }) +} + +// EnableProfiling 启用性能分析 +func (r *GinRouter) EnableProfiling() { + if r.config.Development.EnableProfiler { + // 这里可以集成pprof + r.logger.Info("Profiling enabled") + } +} diff --git a/internal/shared/http/validator.go b/internal/shared/http/validator.go new file mode 100644 index 0000000..3fdc46b --- /dev/null +++ b/internal/shared/http/validator.go @@ -0,0 +1,273 @@ +package http + +import ( + "fmt" + "strings" + + "tyapi-server/internal/shared/interfaces" + + "github.com/gin-gonic/gin" + "github.com/go-playground/validator/v10" +) + +// RequestValidator 请求验证器实现 +type RequestValidator struct { + validator *validator.Validate + response interfaces.ResponseBuilder +} + +// NewRequestValidator 创建请求验证器 +func NewRequestValidator(response interfaces.ResponseBuilder) interfaces.RequestValidator { + v := validator.New() + + // 注册自定义验证器 + registerCustomValidators(v) + + return &RequestValidator{ + validator: v, + response: response, + } +} + +// Validate 验证请求体 +func (v *RequestValidator) Validate(c *gin.Context, dto interface{}) error { + if err := v.validator.Struct(dto); err != nil { + validationErrors := v.formatValidationErrors(err) + v.response.BadRequest(c, "Validation failed", validationErrors) + return err + } + return nil +} + +// 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()) + return err + } + + if err := v.validator.Struct(dto); err != nil { + validationErrors := v.formatValidationErrors(err) + v.response.BadRequest(c, "Validation failed", validationErrors) + return err + } + return nil +} + +// 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()) + return err + } + + if err := v.validator.Struct(dto); err != nil { + validationErrors := v.formatValidationErrors(err) + v.response.BadRequest(c, "Validation failed", validationErrors) + return err + } + return nil +} + +// BindAndValidate 绑定并验证请求 +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()) + return err + } + + // 验证数据 + return v.Validate(c, dto) +} + +// formatValidationErrors 格式化验证错误 +func (v *RequestValidator) formatValidationErrors(err error) map[string][]string { + errors := make(map[string][]string) + + if validationErrors, ok := err.(validator.ValidationErrors); ok { + for _, fieldError := range validationErrors { + fieldName := v.getFieldName(fieldError) + errorMessage := v.getErrorMessage(fieldError) + + if _, exists := errors[fieldName]; !exists { + errors[fieldName] = []string{} + } + errors[fieldName] = append(errors[fieldName], errorMessage) + } + } + + return errors +} + +// getFieldName 获取字段名(JSON标签优先) +func (v *RequestValidator) getFieldName(fieldError validator.FieldError) string { + // 可以通过反射获取JSON标签,这里简化处理 + fieldName := fieldError.Field() + + // 转换为snake_case(可选) + return v.toSnakeCase(fieldName) +} + +// getErrorMessage 获取错误消息 +func (v *RequestValidator) getErrorMessage(fieldError validator.FieldError) string { + field := fieldError.Field() + tag := fieldError.Tag() + param := fieldError.Param() + + switch tag { + case "required": + return fmt.Sprintf("%s is required", field) + case "email": + return fmt.Sprintf("%s must be a valid email address", field) + case "min": + return fmt.Sprintf("%s must be at least %s characters", field, param) + case "max": + return fmt.Sprintf("%s must be at most %s characters", field, param) + case "len": + return fmt.Sprintf("%s must be exactly %s characters", field, param) + case "gt": + return fmt.Sprintf("%s must be greater than %s", field, param) + case "gte": + return fmt.Sprintf("%s must be greater than or equal to %s", field, param) + case "lt": + return fmt.Sprintf("%s must be less than %s", field, param) + case "lte": + return fmt.Sprintf("%s must be less than or equal to %s", field, param) + case "oneof": + return fmt.Sprintf("%s must be one of [%s]", field, param) + case "url": + return fmt.Sprintf("%s must be a valid URL", field) + case "alpha": + return fmt.Sprintf("%s must contain only alphabetic characters", field) + case "alphanum": + return fmt.Sprintf("%s must contain only alphanumeric characters", field) + case "numeric": + return fmt.Sprintf("%s must be numeric", field) + case "phone": + return fmt.Sprintf("%s must be a valid phone number", field) + case "username": + return fmt.Sprintf("%s must be a valid username", field) + default: + return fmt.Sprintf("%s is invalid", field) + } +} + +// toSnakeCase 转换为snake_case +func (v *RequestValidator) 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()) +} + +// registerCustomValidators 注册自定义验证器 +func registerCustomValidators(v *validator.Validate) { + // 注册手机号验证器 + v.RegisterValidation("phone", validatePhone) + + // 注册用户名验证器 + v.RegisterValidation("username", validateUsername) + + // 注册密码强度验证器 + v.RegisterValidation("strong_password", validateStrongPassword) +} + +// validatePhone 验证手机号 +func validatePhone(fl validator.FieldLevel) bool { + phone := fl.Field().String() + if phone == "" { + return true // 空值由required标签处理 + } + + // 简单的手机号验证(可根据需要完善) + if len(phone) < 10 || len(phone) > 15 { + return false + } + + // 检查是否以+开头或全是数字 + if strings.HasPrefix(phone, "+") { + phone = phone[1:] + } + + for _, r := range phone { + if r < '0' || r > '9' { + if r != '-' && r != ' ' && r != '(' && r != ')' { + return false + } + } + } + + return true +} + +// validateUsername 验证用户名 +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 + } + + // 不能以数字开头 + 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 +} + +// validateStrongPassword 验证密码强度 +func validateStrongPassword(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 直接验证结构体(不通过HTTP上下文) +func (v *RequestValidator) ValidateStruct(dto interface{}) error { + return v.validator.Struct(dto) +} + +// GetValidator 获取原始验证器(用于特殊情况) +func (v *RequestValidator) GetValidator() *validator.Validate { + return v.validator +} diff --git a/internal/shared/interfaces/event.go b/internal/shared/interfaces/event.go new file mode 100644 index 0000000..22877dd --- /dev/null +++ b/internal/shared/interfaces/event.go @@ -0,0 +1,92 @@ +package interfaces + +import ( + "context" + "time" +) + +// Event 事件接口 +type Event interface { + // 事件基础信息 + GetID() string + GetType() string + GetVersion() string + GetTimestamp() time.Time + + // 事件数据 + GetPayload() interface{} + GetMetadata() map[string]interface{} + + // 事件来源 + GetSource() string + GetAggregateID() string + GetAggregateType() string + + // 序列化 + Marshal() ([]byte, error) + Unmarshal(data []byte) error +} + +// EventHandler 事件处理器接口 +type EventHandler interface { + // 处理器标识 + GetName() string + GetEventTypes() []string + + // 事件处理 + Handle(ctx context.Context, event Event) error + + // 处理器配置 + IsAsync() bool + GetRetryConfig() RetryConfig +} + +// DomainEvent 领域事件基础接口 +type DomainEvent interface { + Event + + // 领域特定信息 + GetDomainVersion() string + GetCausationID() string + GetCorrelationID() string +} + +// RetryConfig 重试配置 +type RetryConfig struct { + MaxRetries int `json:"max_retries"` + RetryDelay time.Duration `json:"retry_delay"` + BackoffFactor float64 `json:"backoff_factor"` + MaxDelay time.Duration `json:"max_delay"` +} + +// EventStore 事件存储接口 +type EventStore interface { + // 事件存储 + SaveEvent(ctx context.Context, event Event) error + SaveEvents(ctx context.Context, events []Event) error + + // 事件查询 + GetEvents(ctx context.Context, aggregateID string, fromVersion int) ([]Event, error) + GetEventsByType(ctx context.Context, eventType string, limit int) ([]Event, error) + GetEventsSince(ctx context.Context, timestamp time.Time, limit int) ([]Event, error) + + // 快照支持 + SaveSnapshot(ctx context.Context, aggregateID string, snapshot interface{}) error + GetSnapshot(ctx context.Context, aggregateID string) (interface{}, error) +} + +// EventBus 事件总线接口 +type EventBus interface { + // 事件发布 + Publish(ctx context.Context, event Event) error + PublishBatch(ctx context.Context, events []Event) error + + // 事件订阅 + Subscribe(eventType string, handler EventHandler) error + Unsubscribe(eventType string, handler EventHandler) error + + // 订阅管理 + GetSubscribers(eventType string) []EventHandler + Start(ctx context.Context) error + Stop(ctx context.Context) error +} diff --git a/internal/shared/interfaces/http.go b/internal/shared/interfaces/http.go new file mode 100644 index 0000000..043b0e1 --- /dev/null +++ b/internal/shared/interfaces/http.go @@ -0,0 +1,152 @@ +package interfaces + +import ( + "context" + "net/http" + + "github.com/gin-gonic/gin" +) + +// HTTPHandler HTTP处理器接口 +type HTTPHandler interface { + // 处理器信息 + GetPath() string + GetMethod() string + GetMiddlewares() []gin.HandlerFunc + + // 处理函数 + Handle(c *gin.Context) + + // 权限验证 + RequiresAuth() bool + GetPermissions() []string +} + +// RESTHandler REST风格处理器接口 +type RESTHandler interface { + HTTPHandler + + // CRUD操作 + Create(c *gin.Context) + GetByID(c *gin.Context) + Update(c *gin.Context) + Delete(c *gin.Context) + List(c *gin.Context) +} + +// Middleware 中间件接口 +type Middleware interface { + // 中间件名称 + GetName() string + // 中间件优先级 + GetPriority() int + // 中间件处理函数 + Handle() gin.HandlerFunc + // 是否全局中间件 + IsGlobal() bool +} + +// Router 路由器接口 +type Router interface { + // 路由注册 + RegisterHandler(handler HTTPHandler) error + RegisterMiddleware(middleware Middleware) error + RegisterGroup(prefix string, middlewares ...gin.HandlerFunc) gin.IRoutes + + // 路由管理 + GetRoutes() gin.RoutesInfo + Start(addr string) error + Stop(ctx context.Context) error + + // 引擎获取 + GetEngine() *gin.Engine +} + +// ResponseBuilder 响应构建器接口 +type ResponseBuilder interface { + // 成功响应 + Success(c *gin.Context, data interface{}, message ...string) + Created(c *gin.Context, data interface{}, message ...string) + + // 错误响应 + Error(c *gin.Context, err error) + BadRequest(c *gin.Context, message string, errors ...interface{}) + Unauthorized(c *gin.Context, message ...string) + Forbidden(c *gin.Context, message ...string) + NotFound(c *gin.Context, message ...string) + Conflict(c *gin.Context, message string) + InternalError(c *gin.Context, message ...string) + + // 分页响应 + Paginated(c *gin.Context, data interface{}, pagination PaginationMeta) +} + +// RequestValidator 请求验证器接口 +type RequestValidator interface { + // 验证请求 + Validate(c *gin.Context, dto interface{}) error + ValidateQuery(c *gin.Context, dto interface{}) error + ValidateParam(c *gin.Context, dto interface{}) error + + // 绑定和验证 + BindAndValidate(c *gin.Context, dto interface{}) error +} + +// PaginationMeta 分页元数据 +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"` +} + +// APIResponse 标准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"` + Timestamp int64 `json:"timestamp"` +} + +// HealthChecker 健康检查器接口 +type HealthChecker interface { + // 健康检查 + CheckHealth(ctx context.Context) HealthStatus + GetName() string + GetDependencies() []string +} + +// HealthStatus 健康状态 +type HealthStatus struct { + Status string `json:"status"` // UP, DOWN, DEGRADED + Message string `json:"message"` + Details map[string]interface{} `json:"details"` + CheckedAt int64 `json:"checked_at"` + ResponseTime int64 `json:"response_time_ms"` +} + +// MetricsCollector 指标收集器接口 +type MetricsCollector interface { + // HTTP指标 + RecordHTTPRequest(method, path string, status int, duration float64) + RecordHTTPDuration(method, path string, duration float64) + + // 业务指标 + IncrementCounter(name string, labels map[string]string) + RecordGauge(name string, value float64, labels map[string]string) + RecordHistogram(name string, value float64, labels map[string]string) + + // 自定义指标 + RegisterCounter(name, help string, labels []string) error + RegisterGauge(name, help string, labels []string) error + RegisterHistogram(name, help string, labels []string, buckets []float64) error + + // 指标导出 + GetHandler() http.Handler +} diff --git a/internal/shared/interfaces/repository.go b/internal/shared/interfaces/repository.go new file mode 100644 index 0000000..f065c31 --- /dev/null +++ b/internal/shared/interfaces/repository.go @@ -0,0 +1,74 @@ +package interfaces + +import ( + "context" + "time" +) + +// Entity 通用实体接口 +type Entity interface { + GetID() string + GetCreatedAt() time.Time + GetUpdatedAt() time.Time +} + +// BaseRepository 基础仓储接口 +type BaseRepository interface { + // 基础操作 + Delete(ctx context.Context, id string) error + Count(ctx context.Context, options CountOptions) (int64, error) + Exists(ctx context.Context, id string) (bool, error) + + // 软删除支持 + SoftDelete(ctx context.Context, id string) error + Restore(ctx context.Context, id string) error +} + +// Repository 通用仓储接口,支持泛型 +type Repository[T any] interface { + BaseRepository + + // 基础CRUD操作 + Create(ctx context.Context, entity T) error + GetByID(ctx context.Context, id string) (T, error) + Update(ctx context.Context, entity T) error + + // 批量操作 + CreateBatch(ctx context.Context, entities []T) error + GetByIDs(ctx context.Context, ids []string) ([]T, error) + UpdateBatch(ctx context.Context, entities []T) error + DeleteBatch(ctx context.Context, ids []string) error + + // 查询操作 + List(ctx context.Context, options ListOptions) ([]T, error) + + // 事务支持 + WithTx(tx interface{}) Repository[T] +} + +// ListOptions 列表查询选项 +type ListOptions struct { + Page int `json:"page"` + PageSize int `json:"page_size"` + Sort string `json:"sort"` + Order string `json:"order"` + Filters map[string]interface{} `json:"filters"` + Search string `json:"search"` + Include []string `json:"include"` +} + +// CountOptions 计数查询选项 +type CountOptions struct { + Filters map[string]interface{} `json:"filters"` + Search string `json:"search"` +} + +// CachedRepository 支持缓存的仓储接口 +type CachedRepository[T Entity] interface { + Repository[T] + + // 缓存操作 + InvalidateCache(ctx context.Context, keys ...string) error + WarmupCache(ctx context.Context) error + GetCacheKey(id string) string +} diff --git a/internal/shared/interfaces/service.go b/internal/shared/interfaces/service.go new file mode 100644 index 0000000..0e8647e --- /dev/null +++ b/internal/shared/interfaces/service.go @@ -0,0 +1,101 @@ +package interfaces + +import ( + "context" +) + +// Service 通用服务接口 +type Service interface { + // 服务名称 + Name() string + // 服务初始化 + Initialize(ctx context.Context) error + // 服务健康检查 + HealthCheck(ctx context.Context) error + // 服务关闭 + Shutdown(ctx context.Context) error +} + +// DomainService 领域服务接口,支持泛型 +type DomainService[T Entity] interface { + Service + + // 基础业务操作 + Create(ctx context.Context, dto interface{}) (*T, error) + GetByID(ctx context.Context, id string) (*T, error) + Update(ctx context.Context, id string, dto interface{}) (*T, error) + Delete(ctx context.Context, id string) error + + // 列表和查询 + List(ctx context.Context, options ListOptions) ([]*T, error) + Search(ctx context.Context, query string, options ListOptions) ([]*T, error) + Count(ctx context.Context, options CountOptions) (int64, error) + + // 业务规则验证 + Validate(ctx context.Context, entity *T) error + ValidateCreate(ctx context.Context, dto interface{}) error + ValidateUpdate(ctx context.Context, id string, dto interface{}) error +} + +// EventService 事件服务接口 +type EventService interface { + Service + + // 事件发布 + Publish(ctx context.Context, event Event) error + PublishBatch(ctx context.Context, events []Event) error + + // 事件订阅 + Subscribe(eventType string, handler EventHandler) error + Unsubscribe(eventType string, handler EventHandler) error + + // 异步处理 + PublishAsync(ctx context.Context, event Event) error +} + +// CacheService 缓存服务接口 +type CacheService interface { + Service + + // 基础缓存操作 + Get(ctx context.Context, key string, dest interface{}) error + Set(ctx context.Context, key string, value interface{}, ttl ...interface{}) error + Delete(ctx context.Context, keys ...string) error + Exists(ctx context.Context, key string) (bool, error) + + // 批量操作 + GetMultiple(ctx context.Context, keys []string) (map[string]interface{}, error) + SetMultiple(ctx context.Context, data map[string]interface{}, ttl ...interface{}) error + + // 模式操作 + DeletePattern(ctx context.Context, pattern string) error + Keys(ctx context.Context, pattern string) ([]string, error) + + // 缓存统计 + Stats(ctx context.Context) (CacheStats, error) +} + +// CacheStats 缓存统计信息 +type CacheStats struct { + Hits int64 `json:"hits"` + Misses int64 `json:"misses"` + Keys int64 `json:"keys"` + Memory int64 `json:"memory"` + Connections int64 `json:"connections"` +} + +// TransactionService 事务服务接口 +type TransactionService interface { + Service + + // 事务操作 + Begin(ctx context.Context) (Transaction, error) + RunInTransaction(ctx context.Context, fn func(Transaction) error) error +} + +// Transaction 事务接口 +type Transaction interface { + Commit() error + Rollback() error + GetDB() interface{} +} diff --git a/internal/shared/logger/logger.go b/internal/shared/logger/logger.go new file mode 100644 index 0000000..e42c627 --- /dev/null +++ b/internal/shared/logger/logger.go @@ -0,0 +1,241 @@ +package logger + +import ( + "context" + "fmt" + "os" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// Logger 日志接口 +type Logger interface { + Debug(msg string, fields ...zapcore.Field) + Info(msg string, fields ...zapcore.Field) + Warn(msg string, fields ...zapcore.Field) + Error(msg string, fields ...zapcore.Field) + Fatal(msg string, fields ...zapcore.Field) + Panic(msg string, fields ...zapcore.Field) + + With(fields ...zapcore.Field) Logger + WithContext(ctx context.Context) Logger + Sync() error +} + +// ZapLogger Zap日志实现 +type ZapLogger struct { + logger *zap.Logger +} + +// Config 日志配置 +type Config struct { + Level string + Format string + Output string + FilePath string + MaxSize int + MaxBackups int + MaxAge int + Compress bool +} + +// NewLogger 创建新的日志实例 +func NewLogger(config Config) (Logger, error) { + // 设置日志级别 + level, err := zapcore.ParseLevel(config.Level) + if err != nil { + return nil, fmt.Errorf("无效的日志级别: %w", err) + } + + // 配置编码器 + var encoder zapcore.Encoder + encoderConfig := getEncoderConfig() + + switch config.Format { + case "json": + encoder = zapcore.NewJSONEncoder(encoderConfig) + case "console": + encoder = zapcore.NewConsoleEncoder(encoderConfig) + default: + encoder = zapcore.NewJSONEncoder(encoderConfig) + } + + // 配置输出 + var writeSyncer zapcore.WriteSyncer + switch config.Output { + case "stdout": + writeSyncer = zapcore.AddSync(os.Stdout) + case "stderr": + writeSyncer = zapcore.AddSync(os.Stderr) + case "file": + if config.FilePath == "" { + config.FilePath = "logs/app.log" + } + // 确保目录存在 + if err := os.MkdirAll("logs", 0755); err != nil { + return nil, fmt.Errorf("创建日志目录失败: %w", err) + } + + file, err := os.OpenFile(config.FilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) + if err != nil { + return nil, fmt.Errorf("打开日志文件失败: %w", err) + } + writeSyncer = zapcore.AddSync(file) + default: + writeSyncer = zapcore.AddSync(os.Stdout) + } + + // 创建核心 + core := zapcore.NewCore(encoder, writeSyncer, level) + + // 创建logger + logger := zap.New(core, zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel)) + + return &ZapLogger{ + logger: logger, + }, nil +} + +// getEncoderConfig 获取编码器配置 +func getEncoderConfig() zapcore.EncoderConfig { + return zapcore.EncoderConfig{ + TimeKey: "timestamp", + LevelKey: "level", + NameKey: "logger", + CallerKey: "caller", + FunctionKey: zapcore.OmitKey, + MessageKey: "message", + StacktraceKey: "stacktrace", + LineEnding: zapcore.DefaultLineEnding, + EncodeLevel: zapcore.LowercaseLevelEncoder, + EncodeTime: zapcore.ISO8601TimeEncoder, + EncodeDuration: zapcore.StringDurationEncoder, + EncodeCaller: zapcore.ShortCallerEncoder, + } +} + +// Debug 调试日志 +func (l *ZapLogger) Debug(msg string, fields ...zapcore.Field) { + l.logger.Debug(msg, fields...) +} + +// Info 信息日志 +func (l *ZapLogger) Info(msg string, fields ...zapcore.Field) { + l.logger.Info(msg, fields...) +} + +// Warn 警告日志 +func (l *ZapLogger) Warn(msg string, fields ...zapcore.Field) { + l.logger.Warn(msg, fields...) +} + +// Error 错误日志 +func (l *ZapLogger) Error(msg string, fields ...zapcore.Field) { + l.logger.Error(msg, fields...) +} + +// Fatal 致命错误日志 +func (l *ZapLogger) Fatal(msg string, fields ...zapcore.Field) { + l.logger.Fatal(msg, fields...) +} + +// Panic 恐慌日志 +func (l *ZapLogger) Panic(msg string, fields ...zapcore.Field) { + l.logger.Panic(msg, fields...) +} + +// With 添加字段 +func (l *ZapLogger) With(fields ...zapcore.Field) Logger { + return &ZapLogger{ + logger: l.logger.With(fields...), + } +} + +// WithContext 从上下文添加字段 +func (l *ZapLogger) WithContext(ctx context.Context) Logger { + // 从上下文中提取常用字段 + fields := []zapcore.Field{} + + if traceID := getTraceIDFromContext(ctx); traceID != "" { + fields = append(fields, zap.String("trace_id", traceID)) + } + + if userID := getUserIDFromContext(ctx); userID != "" { + fields = append(fields, zap.String("user_id", userID)) + } + + if requestID := getRequestIDFromContext(ctx); requestID != "" { + fields = append(fields, zap.String("request_id", requestID)) + } + + return l.With(fields...) +} + +// Sync 同步日志 +func (l *ZapLogger) Sync() error { + return l.logger.Sync() +} + +// getTraceIDFromContext 从上下文获取追踪ID +func getTraceIDFromContext(ctx context.Context) string { + if traceID := ctx.Value("trace_id"); traceID != nil { + if id, ok := traceID.(string); ok { + return id + } + } + return "" +} + +// getUserIDFromContext 从上下文获取用户ID +func getUserIDFromContext(ctx context.Context) string { + if userID := ctx.Value("user_id"); userID != nil { + if id, ok := userID.(string); ok { + return id + } + } + return "" +} + +// getRequestIDFromContext 从上下文获取请求ID +func getRequestIDFromContext(ctx context.Context) string { + if requestID := ctx.Value("request_id"); requestID != nil { + if id, ok := requestID.(string); ok { + return id + } + } + return "" +} + +// Field 创建日志字段的便捷函数 +func String(key, val string) zapcore.Field { + return zap.String(key, val) +} + +func Int(key string, val int) zapcore.Field { + return zap.Int(key, val) +} + +func Int64(key string, val int64) zapcore.Field { + return zap.Int64(key, val) +} + +func Float64(key string, val float64) zapcore.Field { + return zap.Float64(key, val) +} + +func Bool(key string, val bool) zapcore.Field { + return zap.Bool(key, val) +} + +func Error(err error) zapcore.Field { + return zap.Error(err) +} + +func Any(key string, val interface{}) zapcore.Field { + return zap.Any(key, val) +} + +func Duration(key string, val interface{}) zapcore.Field { + return zap.Any(key, val) +} diff --git a/internal/shared/middleware/auth.go b/internal/shared/middleware/auth.go new file mode 100644 index 0000000..b96e283 --- /dev/null +++ b/internal/shared/middleware/auth.go @@ -0,0 +1,261 @@ +package middleware + +import ( + "net/http" + "strings" + "time" + + "tyapi-server/internal/config" + + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v5" + "go.uber.org/zap" +) + +// JWTAuthMiddleware JWT认证中间件 +type JWTAuthMiddleware struct { + config *config.Config + logger *zap.Logger +} + +// NewJWTAuthMiddleware 创建JWT认证中间件 +func NewJWTAuthMiddleware(cfg *config.Config, logger *zap.Logger) *JWTAuthMiddleware { + return &JWTAuthMiddleware{ + config: cfg, + logger: logger, + } +} + +// GetName 返回中间件名称 +func (m *JWTAuthMiddleware) GetName() string { + return "jwt_auth" +} + +// GetPriority 返回中间件优先级 +func (m *JWTAuthMiddleware) GetPriority() int { + return 60 // 中等优先级,在日志之后,业务处理之前 +} + +// Handle 返回中间件处理函数 +func (m *JWTAuthMiddleware) Handle() gin.HandlerFunc { + return func(c *gin.Context) { + // 获取Authorization头部 + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + m.respondUnauthorized(c, "Missing authorization header") + return + } + + // 检查Bearer前缀 + const bearerPrefix = "Bearer " + if !strings.HasPrefix(authHeader, bearerPrefix) { + m.respondUnauthorized(c, "Invalid authorization header format") + return + } + + // 提取token + tokenString := authHeader[len(bearerPrefix):] + if tokenString == "" { + m.respondUnauthorized(c, "Missing token") + return + } + + // 验证token + claims, err := m.validateToken(tokenString) + if err != nil { + m.logger.Warn("Invalid token", + zap.Error(err), + zap.String("request_id", c.GetString("request_id"))) + m.respondUnauthorized(c, "Invalid token") + return + } + + // 将用户信息添加到上下文 + c.Set("user_id", claims.UserID) + c.Set("username", claims.Username) + c.Set("email", claims.Email) + c.Set("token_claims", claims) + + c.Next() + } +} + +// IsGlobal 是否为全局中间件 +func (m *JWTAuthMiddleware) IsGlobal() bool { + return false // 不是全局中间件,需要手动应用到需要认证的路由 +} + +// JWTClaims JWT声明结构 +type JWTClaims struct { + UserID string `json:"user_id"` + Username string `json:"username"` + Email string `json:"email"` + jwt.RegisteredClaims +} + +// validateToken 验证JWT token +func (m *JWTAuthMiddleware) validateToken(tokenString string) (*JWTClaims, error) { + token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (interface{}, error) { + // 验证签名方法 + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, jwt.ErrSignatureInvalid + } + return []byte(m.config.JWT.Secret), nil + }) + + if err != nil { + return nil, err + } + + claims, ok := token.Claims.(*JWTClaims) + if !ok || !token.Valid { + return nil, jwt.ErrSignatureInvalid + } + + return claims, nil +} + +// respondUnauthorized 返回未授权响应 +func (m *JWTAuthMiddleware) respondUnauthorized(c *gin.Context, message string) { + c.JSON(http.StatusUnauthorized, gin.H{ + "success": false, + "message": "Unauthorized", + "error": message, + "request_id": c.GetString("request_id"), + "timestamp": time.Now().Unix(), + }) + c.Abort() +} + +// GenerateToken 生成JWT token +func (m *JWTAuthMiddleware) GenerateToken(userID, username, email string) (string, error) { + now := time.Now() + + claims := &JWTClaims{ + UserID: userID, + Username: username, + Email: email, + RegisteredClaims: jwt.RegisteredClaims{ + Issuer: "tyapi-server", + Subject: userID, + Audience: []string{"tyapi-client"}, + ExpiresAt: jwt.NewNumericDate(now.Add(m.config.JWT.ExpiresIn)), + NotBefore: jwt.NewNumericDate(now), + IssuedAt: jwt.NewNumericDate(now), + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString([]byte(m.config.JWT.Secret)) +} + +// GenerateRefreshToken 生成刷新token +func (m *JWTAuthMiddleware) GenerateRefreshToken(userID string) (string, error) { + now := time.Now() + + claims := &jwt.RegisteredClaims{ + Issuer: "tyapi-server", + Subject: userID, + Audience: []string{"tyapi-refresh"}, + ExpiresAt: jwt.NewNumericDate(now.Add(m.config.JWT.RefreshExpiresIn)), + NotBefore: jwt.NewNumericDate(now), + IssuedAt: jwt.NewNumericDate(now), + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString([]byte(m.config.JWT.Secret)) +} + +// ValidateRefreshToken 验证刷新token +func (m *JWTAuthMiddleware) ValidateRefreshToken(tokenString string) (string, error) { + token, err := jwt.ParseWithClaims(tokenString, &jwt.RegisteredClaims{}, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, jwt.ErrSignatureInvalid + } + return []byte(m.config.JWT.Secret), nil + }) + + if err != nil { + return "", err + } + + claims, ok := token.Claims.(*jwt.RegisteredClaims) + if !ok || !token.Valid { + return "", jwt.ErrSignatureInvalid + } + + // 检查是否为刷新token + if len(claims.Audience) == 0 || claims.Audience[0] != "tyapi-refresh" { + return "", jwt.ErrSignatureInvalid + } + + return claims.Subject, nil +} + +// OptionalAuthMiddleware 可选认证中间件(用户可能登录也可能未登录) +type OptionalAuthMiddleware struct { + jwtAuth *JWTAuthMiddleware +} + +// NewOptionalAuthMiddleware 创建可选认证中间件 +func NewOptionalAuthMiddleware(jwtAuth *JWTAuthMiddleware) *OptionalAuthMiddleware { + return &OptionalAuthMiddleware{ + jwtAuth: jwtAuth, + } +} + +// GetName 返回中间件名称 +func (m *OptionalAuthMiddleware) GetName() string { + return "optional_auth" +} + +// GetPriority 返回中间件优先级 +func (m *OptionalAuthMiddleware) GetPriority() int { + return 60 // 与JWT认证中间件相同 +} + +// Handle 返回中间件处理函数 +func (m *OptionalAuthMiddleware) Handle() gin.HandlerFunc { + return func(c *gin.Context) { + // 获取Authorization头部 + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + // 没有认证头部,设置匿名用户标识 + c.Set("is_authenticated", false) + c.Next() + return + } + + // 检查Bearer前缀 + const bearerPrefix = "Bearer " + if !strings.HasPrefix(authHeader, bearerPrefix) { + c.Set("is_authenticated", false) + c.Next() + return + } + + // 提取并验证token + tokenString := authHeader[len(bearerPrefix):] + claims, err := m.jwtAuth.validateToken(tokenString) + if err != nil { + // token无效,但不返回错误,设置为未认证 + c.Set("is_authenticated", false) + c.Next() + return + } + + // token有效,设置用户信息 + c.Set("is_authenticated", true) + c.Set("user_id", claims.UserID) + c.Set("username", claims.Username) + c.Set("email", claims.Email) + c.Set("token_claims", claims) + + c.Next() + } +} + +// IsGlobal 是否为全局中间件 +func (m *OptionalAuthMiddleware) IsGlobal() bool { + return false +} diff --git a/internal/shared/middleware/cors.go b/internal/shared/middleware/cors.go new file mode 100644 index 0000000..a4a846d --- /dev/null +++ b/internal/shared/middleware/cors.go @@ -0,0 +1,104 @@ +package middleware + +import ( + "tyapi-server/internal/config" + + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" +) + +// CORSMiddleware CORS中间件 +type CORSMiddleware struct { + config *config.Config +} + +// NewCORSMiddleware 创建CORS中间件 +func NewCORSMiddleware(cfg *config.Config) *CORSMiddleware { + return &CORSMiddleware{ + config: cfg, + } +} + +// GetName 返回中间件名称 +func (m *CORSMiddleware) GetName() string { + return "cors" +} + +// GetPriority 返回中间件优先级 +func (m *CORSMiddleware) GetPriority() int { + return 100 // 高优先级,最先执行 +} + +// Handle 返回中间件处理函数 +func (m *CORSMiddleware) Handle() gin.HandlerFunc { + if !m.config.Development.EnableCors { + // 如果没有启用CORS,返回空处理函数 + return func(c *gin.Context) { + c.Next() + } + } + + config := cors.Config{ + AllowAllOrigins: false, + AllowOrigins: m.getAllowedOrigins(), + AllowMethods: m.getAllowedMethods(), + AllowHeaders: m.getAllowedHeaders(), + ExposeHeaders: []string{ + "Content-Length", + "Content-Type", + "X-Request-ID", + "X-Response-Time", + }, + AllowCredentials: true, + MaxAge: 86400, // 24小时 + } + + return cors.New(config) +} + +// IsGlobal 是否为全局中间件 +func (m *CORSMiddleware) IsGlobal() bool { + return true +} + +// getAllowedOrigins 获取允许的来源 +func (m *CORSMiddleware) getAllowedOrigins() []string { + if m.config.Development.CorsOrigins == "" { + return []string{"http://localhost:3000", "http://localhost:8080"} + } + + // TODO: 解析配置中的origins字符串 + return []string{m.config.Development.CorsOrigins} +} + +// getAllowedMethods 获取允许的方法 +func (m *CORSMiddleware) getAllowedMethods() []string { + if m.config.Development.CorsMethods == "" { + return []string{ + "GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", + } + } + + // TODO: 解析配置中的methods字符串 + return []string{m.config.Development.CorsMethods} +} + +// getAllowedHeaders 获取允许的头部 +func (m *CORSMiddleware) getAllowedHeaders() []string { + if m.config.Development.CorsHeaders == "" { + return []string{ + "Origin", + "Content-Length", + "Content-Type", + "Authorization", + "X-Requested-With", + "Accept", + "Accept-Encoding", + "Accept-Language", + "X-Request-ID", + } + } + + // TODO: 解析配置中的headers字符串 + return []string{m.config.Development.CorsHeaders} +} diff --git a/internal/shared/middleware/ratelimit.go b/internal/shared/middleware/ratelimit.go new file mode 100644 index 0000000..433e05b --- /dev/null +++ b/internal/shared/middleware/ratelimit.go @@ -0,0 +1,166 @@ +package middleware + +import ( + "fmt" + "net/http" + "sync" + "time" + + "tyapi-server/internal/config" + + "github.com/gin-gonic/gin" + "golang.org/x/time/rate" +) + +// RateLimitMiddleware 限流中间件 +type RateLimitMiddleware struct { + config *config.Config + limiters map[string]*rate.Limiter + mutex sync.RWMutex +} + +// NewRateLimitMiddleware 创建限流中间件 +func NewRateLimitMiddleware(cfg *config.Config) *RateLimitMiddleware { + return &RateLimitMiddleware{ + config: cfg, + limiters: make(map[string]*rate.Limiter), + } +} + +// GetName 返回中间件名称 +func (m *RateLimitMiddleware) GetName() string { + return "ratelimit" +} + +// GetPriority 返回中间件优先级 +func (m *RateLimitMiddleware) GetPriority() int { + return 90 // 高优先级 +} + +// Handle 返回中间件处理函数 +func (m *RateLimitMiddleware) Handle() gin.HandlerFunc { + return func(c *gin.Context) { + // 获取客户端标识(IP地址) + clientID := m.getClientID(c) + + // 获取或创建限流器 + 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") + + c.JSON(http.StatusTooManyRequests, gin.H{ + "success": false, + "message": "Rate limit exceeded", + "error": "Too many requests", + }) + c.Abort() + return + } + + // 添加限流头部信息 + c.Header("X-RateLimit-Limit", fmt.Sprintf("%d", m.config.RateLimit.Requests)) + c.Header("X-RateLimit-Window", m.config.RateLimit.Window.String()) + + c.Next() + } +} + +// IsGlobal 是否为全局中间件 +func (m *RateLimitMiddleware) IsGlobal() bool { + return true +} + +// getClientID 获取客户端标识 +func (m *RateLimitMiddleware) getClientID(c *gin.Context) string { + // 优先使用X-Forwarded-For头部 + if xff := c.GetHeader("X-Forwarded-For"); xff != "" { + return xff + } + + // 使用X-Real-IP头部 + if xri := c.GetHeader("X-Real-IP"); xri != "" { + return xri + } + + // 使用RemoteAddr + return c.ClientIP() +} + +// getLimiter 获取或创建限流器 +func (m *RateLimitMiddleware) getLimiter(clientID string) *rate.Limiter { + m.mutex.RLock() + limiter, exists := m.limiters[clientID] + m.mutex.RUnlock() + + if exists { + return limiter + } + + m.mutex.Lock() + defer m.mutex.Unlock() + + // 双重检查 + if limiter, exists := m.limiters[clientID]; exists { + return limiter + } + + // 创建新的限流器 + // rate.Every计算每个请求之间的间隔 + rateLimit := rate.Every(m.config.RateLimit.Window / time.Duration(m.config.RateLimit.Requests)) + limiter = rate.NewLimiter(rateLimit, m.config.RateLimit.Burst) + + m.limiters[clientID] = limiter + + // 启动清理协程(仅第一次创建时) + if len(m.limiters) == 1 { + go m.cleanupRoutine() + } + + return limiter +} + +// cleanupRoutine 定期清理不活跃的限流器 +func (m *RateLimitMiddleware) cleanupRoutine() { + ticker := time.NewTicker(10 * time.Minute) // 每10分钟清理一次 + defer ticker.Stop() + + for { + select { + case <-ticker.C: + m.cleanup() + } + } +} + +// cleanup 清理不活跃的限流器 +func (m *RateLimitMiddleware) cleanup() { + m.mutex.Lock() + defer m.mutex.Unlock() + + now := time.Now() + for clientID, limiter := range m.limiters { + // 如果限流器在过去1小时内没有被使用,则删除它 + if limiter.Reserve().Delay() == 0 && now.Sub(time.Now()) > time.Hour { + delete(m.limiters, clientID) + } + } +} + +// GetStats 获取限流统计 +func (m *RateLimitMiddleware) GetStats() map[string]interface{} { + m.mutex.RLock() + defer m.mutex.RUnlock() + + return map[string]interface{}{ + "active_limiters": len(m.limiters), + "rate_limit": map[string]interface{}{ + "requests": m.config.RateLimit.Requests, + "window": m.config.RateLimit.Window, + "burst": m.config.RateLimit.Burst, + }, + } +} diff --git a/internal/shared/middleware/request_logger.go b/internal/shared/middleware/request_logger.go new file mode 100644 index 0000000..a8125f1 --- /dev/null +++ b/internal/shared/middleware/request_logger.go @@ -0,0 +1,241 @@ +package middleware + +import ( + "bytes" + "io" + "time" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "go.uber.org/zap" +) + +// RequestLoggerMiddleware 请求日志中间件 +type RequestLoggerMiddleware struct { + logger *zap.Logger +} + +// NewRequestLoggerMiddleware 创建请求日志中间件 +func NewRequestLoggerMiddleware(logger *zap.Logger) *RequestLoggerMiddleware { + return &RequestLoggerMiddleware{ + logger: logger, + } +} + +// GetName 返回中间件名称 +func (m *RequestLoggerMiddleware) GetName() string { + return "request_logger" +} + +// GetPriority 返回中间件优先级 +func (m *RequestLoggerMiddleware) GetPriority() int { + return 80 // 中等优先级 +} + +// 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")), + ) + + // 返回空字符串,因为我们已经用zap记录了 + return "" + }) +} + +// IsGlobal 是否为全局中间件 +func (m *RequestLoggerMiddleware) IsGlobal() bool { + return true +} + +// RequestIDMiddleware 请求ID中间件 +type RequestIDMiddleware struct{} + +// NewRequestIDMiddleware 创建请求ID中间件 +func NewRequestIDMiddleware() *RequestIDMiddleware { + return &RequestIDMiddleware{} +} + +// GetName 返回中间件名称 +func (m *RequestIDMiddleware) GetName() string { + return "request_id" +} + +// GetPriority 返回中间件优先级 +func (m *RequestIDMiddleware) GetPriority() int { + return 95 // 最高优先级,第一个执行 +} + +// Handle 返回中间件处理函数 +func (m *RequestIDMiddleware) Handle() gin.HandlerFunc { + return func(c *gin.Context) { + // 获取或生成请求ID + requestID := c.GetHeader("X-Request-ID") + if requestID == "" { + requestID = uuid.New().String() + } + + // 设置请求ID到上下文和响应头 + c.Set("request_id", requestID) + c.Header("X-Request-ID", requestID) + + // 添加到响应头,方便客户端追踪 + c.Writer.Header().Set("X-Request-ID", requestID) + + c.Next() + } +} + +// IsGlobal 是否为全局中间件 +func (m *RequestIDMiddleware) IsGlobal() bool { + return true +} + +// SecurityHeadersMiddleware 安全头部中间件 +type SecurityHeadersMiddleware struct{} + +// NewSecurityHeadersMiddleware 创建安全头部中间件 +func NewSecurityHeadersMiddleware() *SecurityHeadersMiddleware { + return &SecurityHeadersMiddleware{} +} + +// GetName 返回中间件名称 +func (m *SecurityHeadersMiddleware) GetName() string { + return "security_headers" +} + +// GetPriority 返回中间件优先级 +func (m *SecurityHeadersMiddleware) GetPriority() int { + return 85 // 高优先级 +} + +// Handle 返回中间件处理函数 +func (m *SecurityHeadersMiddleware) Handle() gin.HandlerFunc { + return func(c *gin.Context) { + // 设置安全头部 + c.Header("X-Content-Type-Options", "nosniff") + c.Header("X-Frame-Options", "DENY") + c.Header("X-XSS-Protection", "1; mode=block") + c.Header("Referrer-Policy", "strict-origin-when-cross-origin") + c.Header("Content-Security-Policy", "default-src 'self'") + c.Header("Strict-Transport-Security", "max-age=31536000; includeSubDomains") + + c.Next() + } +} + +// IsGlobal 是否为全局中间件 +func (m *SecurityHeadersMiddleware) IsGlobal() bool { + return true +} + +// ResponseTimeMiddleware 响应时间中间件 +type ResponseTimeMiddleware struct{} + +// NewResponseTimeMiddleware 创建响应时间中间件 +func NewResponseTimeMiddleware() *ResponseTimeMiddleware { + return &ResponseTimeMiddleware{} +} + +// GetName 返回中间件名称 +func (m *ResponseTimeMiddleware) GetName() string { + return "response_time" +} + +// GetPriority 返回中间件优先级 +func (m *ResponseTimeMiddleware) GetPriority() int { + return 75 // 中等优先级 +} + +// Handle 返回中间件处理函数 +func (m *ResponseTimeMiddleware) Handle() gin.HandlerFunc { + return func(c *gin.Context) { + start := time.Now() + + c.Next() + + // 计算响应时间并添加到头部 + duration := time.Since(start) + c.Header("X-Response-Time", duration.String()) + + // 记录到上下文中,供其他中间件使用 + c.Set("response_time", duration) + } +} + +// IsGlobal 是否为全局中间件 +func (m *ResponseTimeMiddleware) IsGlobal() bool { + return true +} + +// RequestBodyLoggerMiddleware 请求体日志中间件(用于调试) +type RequestBodyLoggerMiddleware struct { + logger *zap.Logger + enable bool +} + +// NewRequestBodyLoggerMiddleware 创建请求体日志中间件 +func NewRequestBodyLoggerMiddleware(logger *zap.Logger, enable bool) *RequestBodyLoggerMiddleware { + return &RequestBodyLoggerMiddleware{ + logger: logger, + enable: enable, + } +} + +// GetName 返回中间件名称 +func (m *RequestBodyLoggerMiddleware) GetName() string { + return "request_body_logger" +} + +// GetPriority 返回中间件优先级 +func (m *RequestBodyLoggerMiddleware) GetPriority() int { + return 70 // 较低优先级 +} + +// Handle 返回中间件处理函数 +func (m *RequestBodyLoggerMiddleware) Handle() gin.HandlerFunc { + if !m.enable { + return func(c *gin.Context) { + c.Next() + } + } + + return func(c *gin.Context) { + // 只记录POST, PUT, PATCH请求的body + if c.Request.Method == "POST" || c.Request.Method == "PUT" || c.Request.Method == "PATCH" { + if c.Request.Body != nil { + bodyBytes, err := io.ReadAll(c.Request.Body) + if err == nil { + // 重新设置body供后续处理使用 + c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) + + // 记录请求体(注意:生产环境中应该谨慎记录敏感信息) + m.logger.Debug("Request Body", + 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")), + ) + } + } + } + + c.Next() + } +} + +// IsGlobal 是否为全局中间件 +func (m *RequestBodyLoggerMiddleware) IsGlobal() bool { + return false // 可选中间件,不是全局的 +} diff --git a/scripts/init.sql b/scripts/init.sql new file mode 100644 index 0000000..febe8fe --- /dev/null +++ b/scripts/init.sql @@ -0,0 +1,81 @@ +-- TYAPI Server Database Initialization Script +-- This script runs when PostgreSQL container starts for the first time + +-- Create development database if it doesn't exist +CREATE DATABASE tyapi_dev; + +-- Create test database for running tests +CREATE DATABASE tyapi_test; + +-- Create production database (for reference) +-- CREATE DATABASE tyapi_prod; + +-- Connect to development database +\c tyapi_dev; + +-- Enable necessary extensions +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +CREATE EXTENSION IF NOT EXISTS "pg_trgm"; + +CREATE EXTENSION IF NOT EXISTS "btree_gin"; + +-- Create schemas for better organization +CREATE SCHEMA IF NOT EXISTS public; + +CREATE SCHEMA IF NOT EXISTS logs; + +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; + +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; + +-- Create application-specific roles (optional) +-- CREATE ROLE tyapi_app WITH LOGIN PASSWORD 'app_password'; +-- CREATE ROLE tyapi_readonly WITH LOGIN PASSWORD 'readonly_password'; + +-- Grant permissions +-- GRANT CONNECT ON DATABASE tyapi_dev TO tyapi_app; +-- GRANT USAGE ON SCHEMA public TO tyapi_app; +-- GRANT CREATE ON SCHEMA public TO tyapi_app; + +-- Initial seed data can be added here +-- 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; + +-- Create a simple health check function +CREATE OR REPLACE FUNCTION health_check() +RETURNS json AS $$ +BEGIN + RETURN json_build_object( + 'status', 'healthy', + 'database', current_database(), + 'timestamp', now(), + 'version', version() + ); +END; +$$ LANGUAGE plpgsql; \ No newline at end of file