Initial commit: Basic project structure and dependencies
This commit is contained in:
1
.cursorignore
Normal file
1
.cursorignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv)
|
||||||
66
.gitignore
vendored
Normal file
66
.gitignore
vendored
Normal file
@@ -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/
|
||||||
811
COMPLETE_ARCHITECTURE_PLAN.md
Normal file
811
COMPLETE_ARCHITECTURE_PLAN.md
Normal file
@@ -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 分钟生成新业务域** - 完整的模块化开发
|
||||||
|
- ⚡ **自动缓存系统** - 透明的性能优化
|
||||||
|
- 🔄 **跨域事务处理** - 企业级数据一致性
|
||||||
|
- 🐳 **容器化部署** - 生产就绪的部署方案
|
||||||
|
- 🛡️ **安全和监控** - 轻量但完备的保障体系
|
||||||
|
|
||||||
|
**准备好开始实施了吗?** 🎯
|
||||||
246
Makefile
Normal file
246
Makefile
Normal file
@@ -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
|
||||||
334
README.md
Normal file
334
README.md
Normal file
@@ -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 <repository-url>
|
||||||
|
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 应用的理想选择 🚀
|
||||||
110
cmd/api/main.go
Normal file
110
cmd/api/main.go
Normal file
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
91
config.prod.yaml
Normal file
91
config.prod.yaml
Normal file
@@ -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"
|
||||||
91
config.yaml
Normal file
91
config.yaml
Normal file
@@ -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"
|
||||||
104
deployments/docker/redis.conf
Normal file
104
deployments/docker/redis.conf
Normal file
@@ -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 <masterip> <masterport>
|
||||||
|
# masterauth <master-password>
|
||||||
|
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
|
||||||
152
docker-compose.dev.yml
Normal file
152
docker-compose.dev.yml
Normal file
@@ -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
|
||||||
316
docs/API使用指南.md
Normal file
316
docs/API使用指南.md
Normal file
@@ -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 <access_token>"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 获取用户详情
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X GET http://localhost:8080/api/v1/users/1 \
|
||||||
|
-H "Authorization: Bearer <access_token>"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 更新用户信息
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X PUT http://localhost:8080/api/v1/users/1 \
|
||||||
|
-H "Authorization: Bearer <access_token>" \
|
||||||
|
-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 <access_token>" \
|
||||||
|
-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 <access_token>"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 响应格式
|
||||||
|
|
||||||
|
### 成功响应
|
||||||
|
|
||||||
|
```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**
|
||||||
|
- 检查服务器日志
|
||||||
|
- 确认请求参数格式
|
||||||
|
- 联系技术支持
|
||||||
469
docs/ARCHITECTURE.md
Normal file
469
docs/ARCHITECTURE.md
Normal file
@@ -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 的完整技术架构和设计理念,为开发团队提供了全面的技术指导和最佳实践参考。
|
||||||
279
docs/开发指南.md
Normal file
279
docs/开发指南.md
Normal file
@@ -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
|
||||||
|
```
|
||||||
56
docs/快速开始指南.md
Normal file
56
docs/快速开始指南.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# 🚀 快速开始指南
|
||||||
|
|
||||||
|
## 前置要求
|
||||||
|
|
||||||
|
确保您的开发环境中安装了以下工具:
|
||||||
|
|
||||||
|
- **Go 1.23.4+** - 编程语言环境
|
||||||
|
- **Docker & Docker Compose** - 容器化环境
|
||||||
|
- **Git** - 版本控制工具
|
||||||
|
- **Make** - 构建工具(可选,推荐)
|
||||||
|
|
||||||
|
## 一键启动
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 克隆项目
|
||||||
|
git clone <your-repo-url>
|
||||||
|
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 用法
|
||||||
404
docs/故障排除指南.md
Normal file
404
docs/故障排除指南.md
Normal file
@@ -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 <PID>
|
||||||
|
|
||||||
|
# 修改配置使用其他端口
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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 <service_name>
|
||||||
|
|
||||||
|
# 重新构建镜像
|
||||||
|
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"
|
||||||
|
```
|
||||||
102
docs/文档索引.md
Normal file
102
docs/文档索引.md
Normal file
@@ -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. 联系维护团队
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**提示**:建议将此页面加入书签,方便随时查阅相关文档。
|
||||||
536
docs/最佳实践指南.md
Normal file
536
docs/最佳实践指南.md
Normal file
@@ -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 项目的高质量、高性能和高可维护性。
|
||||||
127
docs/环境搭建指南.md
Normal file
127
docs/环境搭建指南.md
Normal file
@@ -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
|
||||||
|
```
|
||||||
476
docs/部署指南.md
Normal file
476
docs/部署指南.md
Normal file
@@ -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 <account>.dkr.ecr.us-west-2.amazonaws.com
|
||||||
|
|
||||||
|
# 标记镜像
|
||||||
|
docker tag tyapi-server:latest <account>.dkr.ecr.us-west-2.amazonaws.com/tyapi-server:latest
|
||||||
|
|
||||||
|
# 推送镜像
|
||||||
|
docker push <account>.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": "<account>.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}]}]}}'
|
||||||
|
```
|
||||||
137
env.example
Normal file
137
env.example
Normal file
@@ -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
|
||||||
64
go.mod
Normal file
64
go.mod
Normal file
@@ -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
|
||||||
|
)
|
||||||
155
go.sum
Normal file
155
go.sum
Normal file
@@ -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=
|
||||||
235
internal/app/app.go
Normal file
235
internal/app/app.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
166
internal/config/config.go
Normal file
166
internal/config/config.go
Normal file
@@ -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"
|
||||||
|
}
|
||||||
311
internal/config/loader.go
Normal file
311
internal/config/loader.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
441
internal/container/container.go
Normal file
441
internal/container/container.go
Normal file
@@ -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()
|
||||||
|
}
|
||||||
173
internal/domains/user/dto/user_dto.go
Normal file
173
internal/domains/user/dto/user_dto.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
138
internal/domains/user/entities/user.go
Normal file
138
internal/domains/user/entities/user.go
Normal file
@@ -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"
|
||||||
|
}
|
||||||
299
internal/domains/user/events/user_events.go
Normal file
299
internal/domains/user/events/user_events.go
Normal file
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
455
internal/domains/user/handlers/user_handler.go
Normal file
455
internal/domains/user/handlers/user_handler.go
Normal file
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
339
internal/domains/user/repositories/user_repository.go
Normal file
339
internal/domains/user/repositories/user_repository.go
Normal file
@@ -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:*")
|
||||||
|
}
|
||||||
133
internal/domains/user/routes/user_routes.go
Normal file
133
internal/domains/user/routes/user_routes.go
Normal file
@@ -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",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
469
internal/domains/user/services/user_service.go
Normal file
469
internal/domains/user/services/user_service.go
Normal file
@@ -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"
|
||||||
|
}
|
||||||
284
internal/shared/cache/redis_cache.go
vendored
Normal file
284
internal/shared/cache/redis_cache.go
vendored
Normal file
@@ -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
|
||||||
|
}
|
||||||
195
internal/shared/database/database.go
Normal file
195
internal/shared/database/database.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
313
internal/shared/events/event_bus.go
Normal file
313
internal/shared/events/event_bus.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
282
internal/shared/health/health_checker.go
Normal file
282
internal/shared/health/health_checker.go
Normal file
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
260
internal/shared/http/response.go
Normal file
260
internal/shared/http/response.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
258
internal/shared/http/router.go
Normal file
258
internal/shared/http/router.go
Normal file
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
273
internal/shared/http/validator.go
Normal file
273
internal/shared/http/validator.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
92
internal/shared/interfaces/event.go
Normal file
92
internal/shared/interfaces/event.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
152
internal/shared/interfaces/http.go
Normal file
152
internal/shared/interfaces/http.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
74
internal/shared/interfaces/repository.go
Normal file
74
internal/shared/interfaces/repository.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
101
internal/shared/interfaces/service.go
Normal file
101
internal/shared/interfaces/service.go
Normal file
@@ -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{}
|
||||||
|
}
|
||||||
241
internal/shared/logger/logger.go
Normal file
241
internal/shared/logger/logger.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
261
internal/shared/middleware/auth.go
Normal file
261
internal/shared/middleware/auth.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
104
internal/shared/middleware/cors.go
Normal file
104
internal/shared/middleware/cors.go
Normal file
@@ -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}
|
||||||
|
}
|
||||||
166
internal/shared/middleware/ratelimit.go
Normal file
166
internal/shared/middleware/ratelimit.go
Normal file
@@ -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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
241
internal/shared/middleware/request_logger.go
Normal file
241
internal/shared/middleware/request_logger.go
Normal file
@@ -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 // 可选中间件,不是全局的
|
||||||
|
}
|
||||||
81
scripts/init.sql
Normal file
81
scripts/init.sql
Normal file
@@ -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;
|
||||||
Reference in New Issue
Block a user