Initial commit: Basic project structure and dependencies

This commit is contained in:
2025-06-30 19:21:56 +08:00
commit 03e615a8fd
50 changed files with 11664 additions and 0 deletions

1
.cursorignore Normal file
View File

@@ -0,0 +1 @@
# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv)

66
.gitignore vendored Normal file
View 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/

View 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
View 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
View 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
View 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
View 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
View 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"

View 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
View 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
View 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
View 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
View 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
```

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}

View 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()
}

View 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
}

View 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"
}

View 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,
}
}

View 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,
}
}

View 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:*")
}

View 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",
})
})
}
}

View 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
View 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
}

View 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
}

View 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
}

View 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))
}
}

View 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)
}

View 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")
}
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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{}
}

View 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)
}

View 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
}

View 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}
}

View 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,
},
}
}

View 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
View 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;