feat(架构): 完善基础架构设计
This commit is contained in:
2527
.cursor/rules/api.mdc
Normal file
2527
.cursor/rules/api.mdc
Normal file
File diff suppressed because it is too large
Load Diff
76
.cursor/rules/global.mdc
Normal file
76
.cursor/rules/global.mdc
Normal file
@@ -0,0 +1,76 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
# 语言规范
|
||||
-- 对于用户端输出和前端响应看到的文字内容部分,尽量使用中文
|
||||
|
||||
## 中文化规则
|
||||
|
||||
### 1. 面向用户的内容
|
||||
- 所有面向用户的响应消息必须使用中文
|
||||
- HTTP API 响应中的消息字段(`message`)必须使用中文
|
||||
- 错误提示信息必须使用中文
|
||||
- 用户界面上的所有文本必须使用中文
|
||||
|
||||
### 2. 错误和验证信息
|
||||
- 所有验证错误提示必须使用中文
|
||||
- 字段验证规则的错误消息必须使用中文
|
||||
- 业务逻辑错误提示必须使用中文
|
||||
- HTTP状态码对应的消息必须使用中文
|
||||
|
||||
### 3. 日志信息
|
||||
- 业务操作相关的日志消息应使用中文
|
||||
- 系统状态和生命周期相关的日志应使用中文
|
||||
- 错误和警告日志消息应使用中文
|
||||
- 日志中的上下文信息(字段名)可使用英文
|
||||
|
||||
### 4. 代码规范
|
||||
- 代码注释可以使用中文,提高可读性
|
||||
- 变量名、函数名、类名等标识符仍使用英文
|
||||
- 中文字符串应使用双引号,不使用反引号
|
||||
- 避免在代码中使用特殊中文标点符号
|
||||
|
||||
### 5. 中文规范示例
|
||||
|
||||
#### HTTP 响应示例:
|
||||
```go
|
||||
// 成功响应
|
||||
h.response.Success(c, data, "操作成功")
|
||||
h.response.Created(c, user, "用户创建成功")
|
||||
|
||||
// 错误响应
|
||||
h.response.BadRequest(c, "请求参数错误")
|
||||
h.response.Unauthorized(c, "用户未认证或登录已过期")
|
||||
h.response.NotFound(c, "请求的资源不存在")
|
||||
h.response.TooManyRequests(c, "请求过于频繁,请稍后再试")
|
||||
```
|
||||
|
||||
#### 验证错误示例:
|
||||
```go
|
||||
// 字段验证错误
|
||||
"手机号格式不正确"
|
||||
"密码强度不足,必须包含大小写字母和数字"
|
||||
"两次输入的密码不一致"
|
||||
|
||||
// 注册自定义验证器
|
||||
v.RegisterTranslation("phone", trans, func(ut ut.Translator) error {
|
||||
return ut.Add("phone", "{0}必须是有效的手机号", true)
|
||||
})
|
||||
```
|
||||
|
||||
#### 日志消息示例:
|
||||
```go
|
||||
// 成功日志
|
||||
logger.Info("用户注册成功", zap.String("user_id", user.ID))
|
||||
logger.Info("支付订单已完成", zap.String("order_id", order.ID))
|
||||
|
||||
// 错误日志
|
||||
logger.Error("创建用户失败", zap.Error(err))
|
||||
logger.Error("数据库连接失败", zap.Error(err))
|
||||
|
||||
// 警告日志
|
||||
logger.Warn("用户多次登录失败", zap.String("phone", phone))
|
||||
logger.Warn("接口调用频率异常", zap.String("client_ip", clientIP))
|
||||
```
|
||||
21
.cursor/rules/start.mdc
Normal file
21
.cursor/rules/start.mdc
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
# 开发环境和偏好设置
|
||||
|
||||
## 开发环境
|
||||
- **操作系统**: Windows
|
||||
- **Shell环境**: PowerShell
|
||||
- **项目路径**: D:\Code\tyapi-project\tyapi-server-gin
|
||||
|
||||
## 开发偏好
|
||||
- **代码测试**: 不需要AI在编写完代码后自动运行项目进行测试
|
||||
- **测试责任**: 用户自己负责测试和运行项目
|
||||
- **AI角色**: 专注于代码编写、修改和解决方案提供,不进行实际的项目运行测试
|
||||
|
||||
## 注意事项
|
||||
- 在Windows PowerShell环境中,某些命令可能与Linux/Mac不同
|
||||
- 避免使用需要用户交互的命令
|
||||
- 如需运行命令,优先考虑PowerShell兼容的语法
|
||||
47
.dockerignore
Normal file
47
.dockerignore
Normal file
@@ -0,0 +1,47 @@
|
||||
# 开发环境文件
|
||||
.env*
|
||||
*.log
|
||||
logs/
|
||||
tmp/
|
||||
|
||||
# 构建产物
|
||||
bin/
|
||||
dist/
|
||||
build/
|
||||
|
||||
# 测试文件
|
||||
*_test.go
|
||||
coverage.out
|
||||
coverage.html
|
||||
|
||||
# 开发工具
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS 文件
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Git
|
||||
.git/
|
||||
.gitignore
|
||||
|
||||
# Docker 相关
|
||||
Dockerfile*
|
||||
docker-compose*.yml
|
||||
.dockerignore
|
||||
|
||||
# 文档
|
||||
README.md
|
||||
docs/
|
||||
*.md
|
||||
|
||||
# 开发依赖
|
||||
node_modules/
|
||||
|
||||
# 临时文件
|
||||
*.tmp
|
||||
*.temp
|
||||
53
.env.production
Normal file
53
.env.production
Normal file
@@ -0,0 +1,53 @@
|
||||
# 生产环境配置模板
|
||||
# 复制此文件到服务器并重命名为 .env,然后修改相应的值
|
||||
|
||||
# 应用配置
|
||||
APP_VERSION=latest
|
||||
APP_PORT=8080
|
||||
|
||||
# 数据库配置 (必须修改)
|
||||
DB_USER=tyapi_user
|
||||
DB_PASSWORD=your_secure_database_password_here
|
||||
DB_NAME=tyapi_prod
|
||||
DB_SSLMODE=require
|
||||
|
||||
# Redis配置 (必须修改)
|
||||
REDIS_PASSWORD=your_secure_redis_password_here
|
||||
|
||||
# JWT配置 (必须修改)
|
||||
JWT_SECRET=your_super_secure_jwt_secret_key_for_production_at_least_32_chars
|
||||
|
||||
# 日志级别
|
||||
LOG_LEVEL=info
|
||||
|
||||
# 端口配置
|
||||
NGINX_HTTP_PORT=80
|
||||
NGINX_HTTPS_PORT=443
|
||||
JAEGER_UI_PORT=16686
|
||||
PROMETHEUS_PORT=9090
|
||||
GRAFANA_PORT=3000
|
||||
MINIO_API_PORT=9000
|
||||
MINIO_CONSOLE_PORT=9001
|
||||
PGADMIN_PORT=5050
|
||||
|
||||
# Grafana 配置
|
||||
GRAFANA_ADMIN_USER=admin
|
||||
GRAFANA_ADMIN_PASSWORD=your_secure_grafana_password_here
|
||||
|
||||
# MinIO 配置
|
||||
MINIO_ROOT_USER=minioadmin
|
||||
MINIO_ROOT_PASSWORD=your_secure_minio_password_here
|
||||
|
||||
# pgAdmin 配置
|
||||
PGADMIN_EMAIL=admin@tyapi.com
|
||||
PGADMIN_PASSWORD=your_secure_pgadmin_password_here
|
||||
|
||||
# 短信服务配置 (必须修改)
|
||||
SMS_ACCESS_KEY_ID=your_sms_access_key_id
|
||||
SMS_ACCESS_KEY_SECRET=your_sms_access_key_secret
|
||||
SMS_SIGN_NAME=your_sms_sign_name
|
||||
SMS_TEMPLATE_CODE=your_sms_template_code
|
||||
|
||||
# SSL证书路径 (如果使用HTTPS)
|
||||
# SSL_CERT_PATH=/path/to/cert.pem
|
||||
# SSL_KEY_PATH=/path/to/key.pem
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -55,7 +55,7 @@ temp/
|
||||
build/
|
||||
dist/
|
||||
|
||||
# Air live reload
|
||||
# Temporary directories
|
||||
tmp/
|
||||
|
||||
# Compiled binary
|
||||
|
||||
@@ -1,811 +0,0 @@
|
||||
# 🏗️ 2025 年最佳 Gin Web 架构完整实施计划
|
||||
|
||||
## 📋 项目概述
|
||||
|
||||
构建一个基于 Clean Architecture + DDD 的高性能、模块化 Gin Web 应用架构,支持快速开发、易维护、高扩展性。
|
||||
|
||||
## 🎯 架构目标
|
||||
|
||||
- ✅ **高度解耦**: 清晰的分层架构,依赖倒置
|
||||
- ✅ **模块化**: 支持模块间快速复制和一键接入
|
||||
- ✅ **高性能**: 优化的并发处理和自动缓存策略
|
||||
- ✅ **易测试**: 完整的单元测试和集成测试
|
||||
- ✅ **易维护**: 标准化的代码结构和文档
|
||||
- ✅ **可扩展**: 支持微服务演进
|
||||
- ✅ **生产就绪**: 完整的安全、监控、容错机制
|
||||
|
||||
## 📁 完整项目目录结构
|
||||
|
||||
```
|
||||
tyapi-server-gin/
|
||||
├── cmd/
|
||||
│ └── api/
|
||||
│ └── main.go # 应用程序入口
|
||||
├── internal/
|
||||
│ ├── config/ # 全局配置
|
||||
│ │ ├── config.go # 配置结构体
|
||||
│ │ ├── database.go # 数据库配置
|
||||
│ │ ├── server.go # 服务器配置
|
||||
│ │ └── loader.go # 配置加载器
|
||||
│ ├── container/ # 依赖注入容器
|
||||
│ │ ├── container.go # FX容器
|
||||
│ │ ├── providers.go # 全局依赖提供者
|
||||
│ │ └── module_registry.go # 模块注册器
|
||||
│ ├── shared/ # 共享基础设施
|
||||
│ │ ├── database/
|
||||
│ │ │ ├── connection.go # 数据库连接
|
||||
│ │ │ ├── base_repository.go # 通用仓储基类
|
||||
│ │ │ └── pool_manager.go # 连接池管理器
|
||||
│ │ ├── cache/
|
||||
│ │ │ ├── redis.go # Redis缓存实现
|
||||
│ │ │ ├── cache_wrapper.go # 查询缓存包装器
|
||||
│ │ │ └── cache_manager.go # 缓存管理器
|
||||
│ │ ├── logger/
|
||||
│ │ │ └── logger.go # 结构化日志
|
||||
│ │ ├── middleware/ # 共享中间件
|
||||
│ │ │ ├── auth.go # 简单认证
|
||||
│ │ │ ├── cors.go # 跨域处理
|
||||
│ │ │ ├── logger.go # 日志中间件
|
||||
│ │ │ ├── recovery.go # 异常恢复
|
||||
│ │ │ └── security.go # 安全中间件栈
|
||||
│ │ ├── events/ # 事件总线
|
||||
│ │ │ ├── event_bus.go
|
||||
│ │ │ └── event_handler.go
|
||||
│ │ ├── saga/ # 分布式事务
|
||||
│ │ │ ├── saga.go
|
||||
│ │ │ └── saga_executor.go
|
||||
│ │ ├── metrics/ # 指标收集
|
||||
│ │ │ ├── simple_metrics.go
|
||||
│ │ │ └── business_metrics.go
|
||||
│ │ ├── tracing/ # 链路追踪
|
||||
│ │ │ └── simple_tracer.go
|
||||
│ │ ├── resilience/ # 容错机制
|
||||
│ │ │ ├── circuit_breaker.go
|
||||
│ │ │ └── retry.go
|
||||
│ │ ├── hooks/ # 钩子系统
|
||||
│ │ │ └── hook_system.go
|
||||
│ │ ├── health/ # 健康检查
|
||||
│ │ │ └── health_checker.go
|
||||
│ │ └── interfaces/
|
||||
│ │ └── module.go # 模块接口定义
|
||||
│ └── domains/ # 业务域
|
||||
│ ├── user/ # 用户域
|
||||
│ │ ├── entities/
|
||||
│ │ │ ├── user.go
|
||||
│ │ │ ├── profile.go
|
||||
│ │ │ └── auth_token.go
|
||||
│ │ ├── repositories/
|
||||
│ │ │ ├── user_repository.go # 接口
|
||||
│ │ │ └── user_repository_impl.go # 实现
|
||||
│ │ ├── services/
|
||||
│ │ │ ├── user_service.go # 接口
|
||||
│ │ │ ├── user_service_impl.go # 实现
|
||||
│ │ │ └── auth_service.go
|
||||
│ │ ├── dto/
|
||||
│ │ │ ├── user_dto.go
|
||||
│ │ │ └── auth_dto.go
|
||||
│ │ ├── handlers/
|
||||
│ │ │ ├── user_handler.go
|
||||
│ │ │ └── auth_handler.go
|
||||
│ │ ├── routes/
|
||||
│ │ │ └── user_routes.go
|
||||
│ │ ├── validators/
|
||||
│ │ │ └── user_validator.go
|
||||
│ │ ├── migrations/
|
||||
│ │ │ └── 001_create_users_table.sql
|
||||
│ │ ├── events/
|
||||
│ │ │ └── user_events.go
|
||||
│ │ └── module.go # 模块定义
|
||||
│ ├── product/ # 产品域
|
||||
│ │ ├── entities/
|
||||
│ │ │ ├── product.go
|
||||
│ │ │ ├── category.go
|
||||
│ │ │ └── inventory.go
|
||||
│ │ ├── repositories/
|
||||
│ │ │ ├── product_repository.go
|
||||
│ │ │ ├── product_repository_impl.go
|
||||
│ │ │ └── category_repository.go
|
||||
│ │ ├── services/
|
||||
│ │ │ ├── product_service.go
|
||||
│ │ │ ├── product_service_impl.go
|
||||
│ │ │ └── inventory_service.go
|
||||
│ │ ├── dto/
|
||||
│ │ │ ├── product_dto.go
|
||||
│ │ │ └── inventory_dto.go
|
||||
│ │ ├── handlers/
|
||||
│ │ │ ├── product_handler.go
|
||||
│ │ │ └── category_handler.go
|
||||
│ │ ├── routes/
|
||||
│ │ │ └── product_routes.go
|
||||
│ │ ├── validators/
|
||||
│ │ │ └── product_validator.go
|
||||
│ │ ├── migrations/
|
||||
│ │ │ └── 002_create_products_table.sql
|
||||
│ │ ├── events/
|
||||
│ │ │ └── product_events.go
|
||||
│ │ └── module.go
|
||||
│ ├── finance/ # 财务域
|
||||
│ │ ├── entities/
|
||||
│ │ │ ├── order.go
|
||||
│ │ │ ├── payment.go
|
||||
│ │ │ └── invoice.go
|
||||
│ │ ├── repositories/
|
||||
│ │ │ ├── order_repository.go
|
||||
│ │ │ ├── order_repository_impl.go
|
||||
│ │ │ └── payment_repository.go
|
||||
│ │ ├── services/
|
||||
│ │ │ ├── order_service.go
|
||||
│ │ │ ├── order_service_impl.go
|
||||
│ │ │ └── payment_service.go
|
||||
│ │ ├── dto/
|
||||
│ │ │ ├── order_dto.go
|
||||
│ │ │ └── payment_dto.go
|
||||
│ │ ├── handlers/
|
||||
│ │ │ ├── order_handler.go
|
||||
│ │ │ └── payment_handler.go
|
||||
│ │ ├── routes/
|
||||
│ │ │ └── finance_routes.go
|
||||
│ │ ├── validators/
|
||||
│ │ │ └── order_validator.go
|
||||
│ │ ├── migrations/
|
||||
│ │ │ └── 003_create_orders_table.sql
|
||||
│ │ ├── events/
|
||||
│ │ │ └── finance_events.go
|
||||
│ │ └── module.go
|
||||
│ └── analytics/ # 数据业务域
|
||||
│ ├── entities/
|
||||
│ │ ├── report.go
|
||||
│ │ ├── metric.go
|
||||
│ │ └── dashboard.go
|
||||
│ ├── repositories/
|
||||
│ │ ├── analytics_repository.go
|
||||
│ │ └── analytics_repository_impl.go
|
||||
│ ├── services/
|
||||
│ │ ├── analytics_service.go
|
||||
│ │ ├── analytics_service_impl.go
|
||||
│ │ └── report_service.go
|
||||
│ ├── dto/
|
||||
│ │ ├── report_dto.go
|
||||
│ │ └── metric_dto.go
|
||||
│ ├── handlers/
|
||||
│ │ ├── analytics_handler.go
|
||||
│ │ └── report_handler.go
|
||||
│ ├── routes/
|
||||
│ │ └── analytics_routes.go
|
||||
│ ├── validators/
|
||||
│ │ └── report_validator.go
|
||||
│ ├── migrations/
|
||||
│ │ └── 004_create_analytics_table.sql
|
||||
│ ├── events/
|
||||
│ │ └── analytics_events.go
|
||||
│ └── module.go
|
||||
├── pkg/ # 公共工具包
|
||||
│ ├── utils/
|
||||
│ │ ├── converter.go # 类型转换
|
||||
│ │ ├── validator.go # 验证工具
|
||||
│ │ └── response.go # 响应工具
|
||||
│ ├── constants/
|
||||
│ │ ├── errors.go # 错误常量
|
||||
│ │ └── status.go # 状态常量
|
||||
│ └── errors/
|
||||
│ └── errors.go # 自定义错误
|
||||
├── scripts/ # 脚本工具
|
||||
│ ├── generate-domain.sh # 域生成器
|
||||
│ ├── generate-module.sh # 模块生成器
|
||||
│ ├── register-module.sh # 模块注册器
|
||||
│ ├── build.sh # 构建脚本
|
||||
│ └── migrate.sh # 数据库迁移脚本
|
||||
├── deployments/ # 部署配置
|
||||
│ ├── docker/
|
||||
│ │ ├── Dockerfile # 多阶段构建
|
||||
│ │ └── docker-compose.yml # 生产环境
|
||||
│ └── docker-compose.dev.yml # 开发环境
|
||||
├── docs/ # 文档
|
||||
│ ├── DEVELOPMENT.md # 开发指南
|
||||
│ ├── DEPLOYMENT.md # 部署指南
|
||||
│ └── API.md # API使用说明
|
||||
├── test/ # 测试
|
||||
│ ├── integration/ # 集成测试
|
||||
│ └── fixtures/ # 测试数据
|
||||
├── .env.example # 环境变量示例
|
||||
├── .gitignore
|
||||
├── Makefile # 构建命令
|
||||
├── go.mod
|
||||
└── go.sum
|
||||
```
|
||||
|
||||
## 🔧 技术栈选择
|
||||
|
||||
### 核心框架
|
||||
|
||||
- **Web 框架**: Gin v1.10+
|
||||
- **ORM**: GORM v2 (支持泛型)
|
||||
- **依赖注入**: Uber FX
|
||||
- **配置管理**: Viper
|
||||
- **验证**: go-playground/validator v10
|
||||
|
||||
### 数据存储
|
||||
|
||||
- **主数据库**: PostgreSQL 15+
|
||||
- **缓存**: Redis 7+ (自动缓存数据库查询)
|
||||
|
||||
### 监控和日志
|
||||
|
||||
- **日志**: Zap + OpenTelemetry
|
||||
- **指标收集**: 简化的业务指标系统
|
||||
- **链路追踪**: 轻量级请求追踪
|
||||
- **健康检查**: 数据库和 Redis 状态监控
|
||||
|
||||
### 开发工具
|
||||
|
||||
- **代码生成**: Wire (可选)
|
||||
- **测试**: Testify + Testcontainers
|
||||
- **Linting**: golangci-lint
|
||||
|
||||
## 📅 分步骤实施计划
|
||||
|
||||
### 🚀 阶段 1: 基础架构搭建 (预计 2-3 小时)
|
||||
|
||||
#### Step 1.1: 项目初始化
|
||||
|
||||
- [ ] 初始化 Go 模块
|
||||
- [ ] 创建基础目录结构
|
||||
- [ ] 配置.gitignore 和基础文件
|
||||
- [ ] 安装核心依赖
|
||||
|
||||
#### Step 1.2: 配置系统
|
||||
|
||||
- [ ] 实现配置加载器 (Viper)
|
||||
- [ ] 创建环境变量管理
|
||||
- [ ] 设置不同环境配置 (dev/staging/prod)
|
||||
|
||||
#### Step 1.3: 日志系统
|
||||
|
||||
- [ ] 集成 Zap 日志库
|
||||
- [ ] 配置结构化日志
|
||||
- [ ] 实现日志中间件
|
||||
|
||||
#### Step 1.4: 数据库连接
|
||||
|
||||
- [ ] 配置 PostgreSQL 连接
|
||||
- [ ] 实现连接池管理
|
||||
- [ ] 集成 GORM
|
||||
- [ ] 创建数据库迁移机制
|
||||
|
||||
### 🏗️ 阶段 2: 架构核心层 (预计 3-4 小时)
|
||||
|
||||
#### Step 2.1: 领域层设计
|
||||
|
||||
- [ ] 定义基础实体接口
|
||||
- [ ] 创建用户实体示例
|
||||
- [ ] 实现仓储接口定义
|
||||
- [ ] 设计业务服务接口
|
||||
|
||||
#### Step 2.2: 基础设施层
|
||||
|
||||
- [ ] 实现通用仓储基类 (支持自动缓存)
|
||||
- [ ] 创建缓存包装器和管理器
|
||||
- [ ] 创建用户仓储实现 (集成缓存)
|
||||
- [ ] 集成 Redis 缓存
|
||||
- [ ] 实现监控指标收集
|
||||
|
||||
#### Step 2.3: 应用层
|
||||
|
||||
- [ ] 定义 DTO 结构
|
||||
- [ ] 实现业务服务
|
||||
- [ ] 创建用例处理器
|
||||
- [ ] 实现数据转换器
|
||||
|
||||
#### Step 2.4: 依赖注入
|
||||
|
||||
- [ ] 配置 FX 容器
|
||||
- [ ] 实现依赖提供者
|
||||
- [ ] 创建模块注册机制
|
||||
|
||||
### 🌐 阶段 3: Web 层实现 (预计 2-3 小时)
|
||||
|
||||
#### Step 3.1: HTTP 层基础
|
||||
|
||||
- [ ] 创建 Gin 路由器
|
||||
- [ ] 实现基础中间件 (CORS, Recovery, Logger)
|
||||
- [ ] 配置路由组织结构
|
||||
|
||||
#### Step 3.2: 处理器实现
|
||||
|
||||
- [ ] 实现用户 CRUD 处理器
|
||||
- [ ] 创建健康检查处理器
|
||||
- [ ] 实现统一响应格式
|
||||
- [ ] 添加请求验证
|
||||
|
||||
#### Step 3.3: 中间件系统
|
||||
|
||||
- [ ] 实现简单认证中间件
|
||||
- [ ] 创建限流中间件
|
||||
- [ ] 添加请求 ID 追踪
|
||||
- [ ] 实现错误处理中间件
|
||||
- [ ] 集成安全头中间件
|
||||
|
||||
### 🔧 阶段 4: 高级特性实现 (预计 3-4 小时)
|
||||
|
||||
#### Step 4.1: 自动缓存系统
|
||||
|
||||
- [ ] 实现缓存包装器
|
||||
- [ ] 集成查询结果自动缓存
|
||||
- [ ] 实现智能缓存失效策略
|
||||
- [ ] 添加缓存穿透防护
|
||||
|
||||
#### Step 4.2: 跨域事务处理
|
||||
|
||||
- [ ] 实现 Saga 模式事务协调器
|
||||
- [ ] 创建事件驱动架构
|
||||
- [ ] 实现补偿机制
|
||||
- [ ] 添加视图聚合模式
|
||||
|
||||
#### Step 4.3: 可观测性
|
||||
|
||||
- [ ] 集成简单指标收集
|
||||
- [ ] 实现链路追踪
|
||||
- [ ] 添加业务指标监控
|
||||
- [ ] 创建性能监控面板
|
||||
|
||||
#### Step 4.4: 容错机制
|
||||
|
||||
- [ ] 实现简化熔断器
|
||||
- [ ] 添加智能重试机制
|
||||
- [ ] 集成钩子系统
|
||||
- [ ] 实现优雅降级
|
||||
|
||||
### 🚀 阶段 5: 部署和工具 (预计 2-3 小时)
|
||||
|
||||
#### Step 5.1: 容器化 (Docker Compose)
|
||||
|
||||
- [ ] 创建多阶段 Dockerfile
|
||||
- [ ] 配置生产环境 docker-compose.yml
|
||||
- [ ] 配置开发环境 docker-compose.dev.yml
|
||||
- [ ] 集成 PostgreSQL 和 Redis 容器
|
||||
- [ ] 实现健康检查
|
||||
- [ ] 配置数据卷持久化
|
||||
|
||||
#### Step 5.2: 开发工具
|
||||
|
||||
- [ ] 创建 Makefile 命令
|
||||
- [ ] 实现模块生成器脚本
|
||||
- [ ] 配置代码质量检查
|
||||
- [ ] 创建数据库迁移脚本
|
||||
|
||||
#### Step 5.3: 生产就绪
|
||||
|
||||
- [ ] 实现优雅关闭
|
||||
- [ ] 配置信号处理
|
||||
- [ ] 添加安全性增强
|
||||
- [ ] 创建监控和告警
|
||||
|
||||
## 🔄 自动缓存架构设计
|
||||
|
||||
### 缓存包装器实现
|
||||
|
||||
```go
|
||||
// 缓存包装器接口
|
||||
type CacheableRepository[T any] interface {
|
||||
GetWithCache(ctx context.Context, key string, finder func() (*T, error)) (*T, error)
|
||||
ListWithCache(ctx context.Context, key string, finder func() ([]*T, error)) ([]*T, error)
|
||||
InvalidateCache(ctx context.Context, pattern string) error
|
||||
}
|
||||
|
||||
// 自动缓存的仓储实现
|
||||
type UserRepository struct {
|
||||
db *gorm.DB
|
||||
cache cache.Manager
|
||||
}
|
||||
|
||||
func (r *UserRepository) GetByID(ctx context.Context, id uint) (*User, error) {
|
||||
cacheKey := fmt.Sprintf("user:id:%d", id)
|
||||
return r.cache.GetWithCache(ctx, cacheKey, func() (*User, error) {
|
||||
var user User
|
||||
err := r.db.First(&user, id).Error
|
||||
return &user, err
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 缓存策略
|
||||
|
||||
- **自动查询缓存**: 数据库查询结果自动缓存
|
||||
- **智能缓存失效**: 基于数据变更的缓存失效
|
||||
- **多级缓存架构**: 内存 + Redis 组合
|
||||
- **缓存穿透防护**: 空结果缓存和布隆过滤器
|
||||
- **缓存预热**: 启动时预加载热点数据
|
||||
|
||||
## 🔄 跨域事务处理
|
||||
|
||||
### Saga 模式实现
|
||||
|
||||
```go
|
||||
// Saga事务协调器
|
||||
type Saga struct {
|
||||
ID string
|
||||
Steps []SagaStep
|
||||
Data interface{}
|
||||
Status SagaStatus
|
||||
executor SagaExecutor
|
||||
}
|
||||
|
||||
type SagaStep struct {
|
||||
Name string
|
||||
Action func(ctx context.Context, data interface{}) error
|
||||
Compensate func(ctx context.Context, data interface{}) error
|
||||
}
|
||||
```
|
||||
|
||||
### 跨域查询处理
|
||||
|
||||
#### 视图聚合模式
|
||||
|
||||
```go
|
||||
// OrderView 订单视图聚合
|
||||
type OrderView struct {
|
||||
// 订单基本信息 (来自finance域)
|
||||
OrderID uint `json:"order_id"`
|
||||
Amount float64 `json:"amount"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
|
||||
// 用户信息 (来自user域)
|
||||
UserID uint `json:"user_id"`
|
||||
UserName string `json:"user_name"`
|
||||
UserEmail string `json:"user_email"`
|
||||
|
||||
// 产品信息 (来自product域)
|
||||
ProductID uint `json:"product_id"`
|
||||
ProductName string `json:"product_name"`
|
||||
ProductPrice float64 `json:"product_price"`
|
||||
}
|
||||
```
|
||||
|
||||
## 🔄 模块化特性
|
||||
|
||||
### 快速添加新模块
|
||||
|
||||
```bash
|
||||
# 使用脚本快速生成新模块
|
||||
make add-domain name=inventory
|
||||
|
||||
# 自动生成:
|
||||
# - 完整的目录结构和代码模板
|
||||
# - 自动缓存支持的仓储层
|
||||
# - 标准化的API接口
|
||||
# - 依赖注入自动配置
|
||||
# - 路由自动注册
|
||||
|
||||
# 立即可用的API:
|
||||
# GET /api/v1/inventorys
|
||||
# POST /api/v1/inventorys
|
||||
# GET /api/v1/inventorys/:id
|
||||
# PUT /api/v1/inventorys/:id
|
||||
# DELETE /api/v1/inventorys/:id
|
||||
# GET /api/v1/inventorys/search
|
||||
```
|
||||
|
||||
### 模块间解耦
|
||||
|
||||
- 通过接口定义模块边界
|
||||
- 使用事件驱动通信
|
||||
- 独立的数据模型
|
||||
- 可插拔的模块系统
|
||||
|
||||
## 🐳 Docker Compose 部署架构
|
||||
|
||||
### 生产环境配置
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
version: "3.8"
|
||||
services:
|
||||
api:
|
||||
build: .
|
||||
ports:
|
||||
- "8080:8080"
|
||||
environment:
|
||||
- ENV=production
|
||||
- DB_HOST=postgres
|
||||
- REDIS_HOST=redis
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
|
||||
postgres:
|
||||
image: postgres:15-alpine
|
||||
environment:
|
||||
POSTGRES_DB: tyapi
|
||||
POSTGRES_USER: tyapi
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "5432:5432"
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
ports:
|
||||
- "6379:6379"
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
```
|
||||
|
||||
### 多阶段 Dockerfile
|
||||
|
||||
```dockerfile
|
||||
# Build stage
|
||||
FROM golang:1.23.4-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -o main cmd/api/main.go
|
||||
|
||||
# Development stage
|
||||
FROM golang:1.23.4-alpine AS development
|
||||
WORKDIR /app
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
CMD ["go", "run", "cmd/api/main.go"]
|
||||
|
||||
# Production stage
|
||||
FROM alpine:latest AS production
|
||||
RUN apk --no-cache add ca-certificates
|
||||
WORKDIR /root/
|
||||
COPY --from=builder /app/main .
|
||||
CMD ["./main"]
|
||||
```
|
||||
|
||||
## 🛡️ 安全和监控特性
|
||||
|
||||
### 轻量安全中间件栈
|
||||
|
||||
```go
|
||||
// SecurityMiddleware 安全中间件集合
|
||||
type SecurityMiddleware struct {
|
||||
jwtSecret string
|
||||
rateLimiter *rate.Limiter
|
||||
trustedIPs map[string]bool
|
||||
}
|
||||
|
||||
// Chain 安全中间件链
|
||||
func (s *SecurityMiddleware) Chain() []gin.HandlerFunc {
|
||||
return []gin.HandlerFunc{
|
||||
s.RateLimit(),
|
||||
s.SecurityHeaders(),
|
||||
s.IPWhitelist(),
|
||||
s.RequestID(),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 简化的可观测性
|
||||
|
||||
```go
|
||||
// SimpleMetrics 简化的指标收集器
|
||||
type SimpleMetrics struct {
|
||||
counters map[string]int64
|
||||
gauges map[string]float64
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// BusinessMetrics 业务指标
|
||||
type BusinessMetrics struct {
|
||||
metrics *SimpleMetrics
|
||||
}
|
||||
|
||||
func (b *BusinessMetrics) RecordOrderCreated(amount float64) {
|
||||
b.metrics.IncCounter("orders.created.count", 1)
|
||||
b.metrics.IncCounter("orders.created.amount", int64(amount*100))
|
||||
}
|
||||
```
|
||||
|
||||
### 容错机制
|
||||
|
||||
```go
|
||||
// SimpleCircuitBreaker 简化的熔断器
|
||||
type SimpleCircuitBreaker struct {
|
||||
maxFailures int
|
||||
resetTimeout time.Duration
|
||||
state CircuitState
|
||||
}
|
||||
|
||||
// SimpleRetry 智能重试机制
|
||||
func SimpleRetry(ctx context.Context, config RetryConfig, fn func() error) error {
|
||||
// 指数退避重试实现
|
||||
}
|
||||
```
|
||||
|
||||
### 健康检查
|
||||
|
||||
```go
|
||||
// HealthChecker 健康检查器
|
||||
type HealthChecker struct {
|
||||
db *gorm.DB
|
||||
redis *redis.Client
|
||||
}
|
||||
|
||||
type HealthStatus struct {
|
||||
Status string `json:"status"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Checks map[string]CheckResult `json:"checks"`
|
||||
}
|
||||
```
|
||||
|
||||
## 📊 性能优化策略
|
||||
|
||||
### 数据库优化
|
||||
|
||||
- 连接池动态调优
|
||||
- 查询结果自动缓存
|
||||
- 索引策略优化
|
||||
- 批量操作优化
|
||||
|
||||
### 缓存策略
|
||||
|
||||
- **自动查询缓存**: 透明的查询结果缓存
|
||||
- **智能失效**: 数据变更时自动清理相关缓存
|
||||
- **多级架构**: L1(内存) + L2(Redis) 缓存
|
||||
- **穿透防护**: 空值缓存和布隆过滤器
|
||||
|
||||
### 并发优化
|
||||
|
||||
- Goroutine 池管理
|
||||
- Context 超时控制
|
||||
- 异步任务处理
|
||||
- 批量数据加载
|
||||
|
||||
## 🔌 扩展性设计
|
||||
|
||||
### 钩子系统
|
||||
|
||||
```go
|
||||
// SimpleHookSystem 简化的钩子系统
|
||||
type SimpleHookSystem struct {
|
||||
hooks map[string][]HookFunc
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
hookSystem.Register("user.created", func(ctx context.Context, data interface{}) error {
|
||||
user := data.(*entities.User)
|
||||
return sendWelcomeEmail(user.Email)
|
||||
})
|
||||
```
|
||||
|
||||
### 事件驱动架构
|
||||
|
||||
```go
|
||||
// 跨域事件通信
|
||||
type UserCreatedEvent struct {
|
||||
UserID uint `json:"user_id"`
|
||||
Email string `json:"email"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// 产品域监听用户创建事件
|
||||
func (s *ProductService) OnUserCreated(event UserCreatedEvent) error {
|
||||
return s.CreateUserRecommendations(event.UserID)
|
||||
}
|
||||
```
|
||||
|
||||
## 🛠️ 开发工具
|
||||
|
||||
### Makefile 快速命令
|
||||
|
||||
```makefile
|
||||
# 添加新的业务域
|
||||
add-domain:
|
||||
@./scripts/generate-domain.sh $(name)
|
||||
@./scripts/register-module.sh $(name)
|
||||
|
||||
# 开发环境运行
|
||||
run-dev:
|
||||
@go run cmd/api/main.go
|
||||
|
||||
# Docker开发环境
|
||||
docker-dev:
|
||||
@docker-compose -f docker-compose.dev.yml up -d
|
||||
|
||||
# 数据库迁移
|
||||
migrate:
|
||||
@go run cmd/migrate/main.go
|
||||
```
|
||||
|
||||
### 域生成器脚本
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# scripts/generate-domain.sh
|
||||
|
||||
# 自动生成:
|
||||
# - 实体定义和表结构
|
||||
# - 仓储接口和实现(支持自动缓存)
|
||||
# - 业务服务层
|
||||
# - HTTP处理器
|
||||
# - DTO和验证器
|
||||
# - 路由注册
|
||||
# - 模块定义和依赖注入
|
||||
```
|
||||
|
||||
## 🎯 预期收益
|
||||
|
||||
1. **开发效率**: 模块生成器将新功能开发时间减少 70%
|
||||
2. **代码质量**: 统一的架构模式和代码规范
|
||||
3. **维护性**: 清晰的分层和依赖关系
|
||||
4. **可测试性**: 依赖注入支持 100%的单元测试覆盖
|
||||
5. **性能**: 自动缓存和并发优化策略
|
||||
6. **扩展性**: 支持平滑的微服务演进
|
||||
7. **稳定性**: 完整的容错和监控机制
|
||||
8. **安全性**: 轻量但完备的安全防护
|
||||
|
||||
## ✅ 架构特色总结
|
||||
|
||||
### 🎯 核心优势
|
||||
|
||||
1. **自动缓存系统**:
|
||||
|
||||
- 数据库查询自动缓存,性能提升显著
|
||||
- 智能缓存失效策略
|
||||
- 多级缓存架构(内存 + Redis)
|
||||
|
||||
2. **模块化设计**:
|
||||
|
||||
- Clean Architecture + DDD 设计
|
||||
- 5 分钟快速生成完整业务域
|
||||
- 依赖注入容器(Uber FX)
|
||||
|
||||
3. **跨域事务处理**:
|
||||
|
||||
- Saga 模式处理分布式事务
|
||||
- 视图聚合解决跨域查询
|
||||
- 事件驱动架构
|
||||
|
||||
4. **Docker Compose 部署**:
|
||||
|
||||
- 开发和生产环境分离
|
||||
- 多阶段构建优化
|
||||
- 数据持久化和网络隔离
|
||||
|
||||
5. **生产就绪特性**:
|
||||
- 轻量安全中间件栈
|
||||
- 简化的可观测性
|
||||
- 容错和稳定性保障
|
||||
- 健康检查和监控
|
||||
|
||||
### 📋 技术决策
|
||||
|
||||
✅ **数据库**: PostgreSQL 15+ 主库 + Redis 7+ 缓存
|
||||
✅ **缓存策略**: 自动查询缓存 + 智能失效
|
||||
✅ **部署方式**: Docker Compose(生产/开发分离)
|
||||
✅ **开发工具**: 移除热重载,保留核心工具
|
||||
✅ **认证方案**: 简化 JWT 认证,无复杂 RBAC
|
||||
✅ **测试策略**: 手动编写,无自动生成
|
||||
✅ **文档方案**: 手动维护,无自动 API 文档
|
||||
|
||||
## 🚀 开始实施
|
||||
|
||||
架构设计已完成!总预计实施时间:**12-15 小时**,分 5 个阶段完成。
|
||||
|
||||
### 实施顺序:
|
||||
|
||||
1. **基础架构搭建** (2-3 小时)
|
||||
2. **架构核心层** (3-4 小时)
|
||||
3. **Web 层实现** (2-3 小时)
|
||||
4. **高级特性** (3-4 小时)
|
||||
5. **部署和工具** (2-3 小时)
|
||||
|
||||
### 关键特性:
|
||||
|
||||
- 🚀 **5 分钟生成新业务域** - 完整的模块化开发
|
||||
- ⚡ **自动缓存系统** - 透明的性能优化
|
||||
- 🔄 **跨域事务处理** - 企业级数据一致性
|
||||
- 🐳 **容器化部署** - 生产就绪的部署方案
|
||||
- 🛡️ **安全和监控** - 轻量但完备的保障体系
|
||||
|
||||
**准备好开始实施了吗?** 🎯
|
||||
68
Dockerfile
Normal file
68
Dockerfile
Normal file
@@ -0,0 +1,68 @@
|
||||
# 第一阶段:构建阶段
|
||||
FROM golang:1.23-alpine AS builder
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 安装必要的包
|
||||
RUN apk add --no-cache git ca-certificates tzdata
|
||||
|
||||
# 复制模块文件
|
||||
COPY go.mod go.sum ./
|
||||
|
||||
# 下载依赖
|
||||
RUN go mod download
|
||||
|
||||
# 复制源代码
|
||||
COPY . .
|
||||
|
||||
# 构建应用程序
|
||||
ARG VERSION=1.0.0
|
||||
ARG COMMIT=dev
|
||||
ARG BUILD_TIME
|
||||
RUN BUILD_TIME=${BUILD_TIME:-$(date -u +"%Y-%m-%dT%H:%M:%SZ")} && \
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
|
||||
-ldflags "-X main.version=${VERSION} -X main.commit=${COMMIT} -X main.date=${BUILD_TIME} -w -s" \
|
||||
-a -installsuffix cgo \
|
||||
-o tyapi-server \
|
||||
cmd/api/main.go
|
||||
|
||||
# 第二阶段:运行阶段
|
||||
FROM alpine:3.19
|
||||
|
||||
# 安装必要的包
|
||||
RUN apk --no-cache add ca-certificates tzdata curl && \
|
||||
update-ca-certificates
|
||||
|
||||
# 设置时区
|
||||
ENV TZ=Asia/Shanghai
|
||||
|
||||
# 创建非root用户
|
||||
RUN addgroup -g 1001 tyapi && \
|
||||
adduser -D -s /bin/sh -u 1001 -G tyapi tyapi
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 从构建阶段复制二进制文件
|
||||
COPY --from=builder /app/tyapi-server .
|
||||
|
||||
# 复制配置文件
|
||||
COPY --chown=tyapi:tyapi config.yaml .
|
||||
COPY --chown=tyapi:tyapi configs/ ./configs/
|
||||
|
||||
# 创建日志目录
|
||||
RUN mkdir -p logs && chown -R tyapi:tyapi logs
|
||||
|
||||
# 切换到非root用户
|
||||
USER tyapi
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 8080
|
||||
|
||||
# 健康检查
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
|
||||
CMD curl -f http://localhost:8080/health || exit 1
|
||||
|
||||
# 启动命令
|
||||
CMD ["./tyapi-server", "-env=production"]
|
||||
450
Makefile
450
Makefile
@@ -3,9 +3,25 @@
|
||||
# 应用信息
|
||||
APP_NAME := tyapi-server
|
||||
VERSION := 1.0.0
|
||||
BUILD_TIME := $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
GIT_COMMIT := $(shell git rev-parse --short HEAD)
|
||||
GO_VERSION := $(shell go version | awk '{print $$3}')
|
||||
|
||||
# 检测操作系统
|
||||
ifeq ($(OS),Windows_NT)
|
||||
# Windows 环境
|
||||
BUILD_TIME := $(shell powershell -Command "Get-Date -Format 'yyyy-MM-ddTHH:mm:ssZ'")
|
||||
GIT_COMMIT := $(shell powershell -Command "try { git rev-parse --short HEAD } catch { 'dev' }")
|
||||
GO_VERSION := $(shell go version)
|
||||
MKDIR := mkdir
|
||||
RM := del /f /q
|
||||
RMDIR := rmdir /s /q
|
||||
else
|
||||
# Unix 环境
|
||||
BUILD_TIME := $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
GIT_COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo 'dev')
|
||||
GO_VERSION := $(shell go version | awk '{print $$3}')
|
||||
MKDIR := mkdir -p
|
||||
RM := rm -f
|
||||
RMDIR := rm -rf
|
||||
endif
|
||||
|
||||
# 构建参数
|
||||
LDFLAGS := -ldflags "-X main.version=$(VERSION) -X main.commit=$(GIT_COMMIT) -X main.date=$(BUILD_TIME)"
|
||||
@@ -32,58 +48,132 @@ DOCKER_LATEST := $(APP_NAME):latest
|
||||
help:
|
||||
@echo "TYAPI Server Makefile"
|
||||
@echo ""
|
||||
@echo "使用方法: make [目标]"
|
||||
@echo "Usage: make [target]"
|
||||
@echo ""
|
||||
@echo "可用目标:"
|
||||
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " %-20s %s\n", $$1, $$2}' $(MAKEFILE_LIST)
|
||||
@echo "Development Basics:"
|
||||
@echo " help Show this help message"
|
||||
@echo " setup Setup development environment"
|
||||
@echo " deps Install dependencies"
|
||||
@echo " fmt Format code"
|
||||
@echo " lint Lint code"
|
||||
@echo " test Run tests"
|
||||
@echo " coverage Generate test coverage report"
|
||||
@echo ""
|
||||
@echo "Build & Compile:"
|
||||
@echo " build Build application"
|
||||
@echo " build-prod Build production version"
|
||||
@echo " build-all Cross compile for all platforms"
|
||||
@echo " clean Clean build files"
|
||||
@echo ""
|
||||
@echo "Run & Manage:"
|
||||
@echo " dev Run in development mode"
|
||||
@echo " run Run compiled application"
|
||||
@echo " migrate Run database migration"
|
||||
@echo " version Show version info"
|
||||
@echo " health Run health check"
|
||||
@echo ""
|
||||
@echo "Docker Containers:"
|
||||
@echo " docker-build Build Docker image"
|
||||
@echo " docker-build-prod Build production Docker image"
|
||||
@echo " docker-push-prod Push image to registry"
|
||||
@echo " docker-run Run Docker container"
|
||||
@echo " docker-stop Stop Docker container"
|
||||
@echo ""
|
||||
@echo "Production Environment:"
|
||||
@echo " deploy-prod Deploy to production"
|
||||
@echo " prod-up Start production services"
|
||||
@echo " prod-down Stop production services"
|
||||
@echo " prod-logs View production logs"
|
||||
@echo " prod-status Check production status"
|
||||
@echo ""
|
||||
@echo "Development Environment:"
|
||||
@echo " services-up Start dev dependencies"
|
||||
@echo " services-down Stop dev dependencies"
|
||||
@echo " services-update Update dev dependencies (rebuild & restart)"
|
||||
@echo " dev-up Alias for services-up"
|
||||
@echo " dev-down Alias for services-down"
|
||||
@echo " dev-update Alias for services-update"
|
||||
@echo ""
|
||||
@echo "Tools & Utilities:"
|
||||
@echo " env Create .env file from template"
|
||||
@echo " logs View application logs"
|
||||
@echo " docs Generate API documentation"
|
||||
@echo " bench Run performance benchmark"
|
||||
@echo " race Run race condition detection"
|
||||
@echo " security Run security scan"
|
||||
@echo " mock Generate mock data"
|
||||
@echo ""
|
||||
@echo "CI/CD Pipeline:"
|
||||
@echo " ci Run complete CI pipeline"
|
||||
@echo " release Run complete release pipeline"
|
||||
|
||||
## 安装依赖
|
||||
## Install dependencies
|
||||
deps:
|
||||
@echo "安装依赖..."
|
||||
@echo "Installing dependencies..."
|
||||
$(GOMOD) download
|
||||
$(GOMOD) tidy
|
||||
|
||||
## 代码格式化
|
||||
## Format code
|
||||
fmt:
|
||||
@echo "格式化代码..."
|
||||
@echo "Formatting code..."
|
||||
$(GOFMT) ./...
|
||||
|
||||
## 代码检查
|
||||
## Lint code
|
||||
lint:
|
||||
@echo "代码检查..."
|
||||
@echo "Linting code..."
|
||||
ifeq ($(OS),Windows_NT)
|
||||
@where golangci-lint >nul 2>&1 && golangci-lint run || echo "golangci-lint not installed, skipping lint check"
|
||||
else
|
||||
@if command -v golangci-lint >/dev/null 2>&1; then \
|
||||
golangci-lint run; \
|
||||
else \
|
||||
echo "golangci-lint 未安装,跳过代码检查"; \
|
||||
echo "golangci-lint not installed, skipping lint check"; \
|
||||
fi
|
||||
endif
|
||||
|
||||
## 运行测试
|
||||
## Run tests
|
||||
test:
|
||||
@echo "运行测试..."
|
||||
@echo "Running tests..."
|
||||
ifeq ($(OS),Windows_NT)
|
||||
$(GOTEST) -v -coverprofile=coverage.out ./...
|
||||
else
|
||||
$(GOTEST) -v -race -coverprofile=coverage.out ./...
|
||||
endif
|
||||
|
||||
## 生成测试覆盖率报告
|
||||
## Generate test coverage report
|
||||
coverage: test
|
||||
@echo "生成覆盖率报告..."
|
||||
@echo "Generating coverage report..."
|
||||
$(GOCMD) tool cover -html=coverage.out -o coverage.html
|
||||
@echo "覆盖率报告已生成: coverage.html"
|
||||
@echo "Coverage report generated: coverage.html"
|
||||
|
||||
## 构建应用 (开发环境)
|
||||
## Build application (development)
|
||||
build:
|
||||
@echo "构建应用..."
|
||||
@echo "Building application..."
|
||||
ifeq ($(OS),Windows_NT)
|
||||
@if not exist "$(BUILD_DIR)" mkdir "$(BUILD_DIR)"
|
||||
else
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
endif
|
||||
$(GOBUILD) $(LDFLAGS) -o $(BUILD_DIR)/$(APP_NAME) $(MAIN_PATH)
|
||||
|
||||
## 构建生产版本
|
||||
## Build production version
|
||||
build-prod:
|
||||
@echo "构建生产版本..."
|
||||
@echo "Building production version..."
|
||||
ifeq ($(OS),Windows_NT)
|
||||
@if not exist "$(BUILD_DIR)" mkdir "$(BUILD_DIR)"
|
||||
else
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
endif
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GOBUILD) $(LDFLAGS) -a -installsuffix cgo -o $(BUILD_DIR)/$(APP_NAME)-linux-amd64 $(MAIN_PATH)
|
||||
|
||||
## 交叉编译
|
||||
## Cross compile
|
||||
build-all:
|
||||
@echo "交叉编译..."
|
||||
@echo "Cross compiling..."
|
||||
ifeq ($(OS),Windows_NT)
|
||||
@if not exist "$(BUILD_DIR)" mkdir "$(BUILD_DIR)"
|
||||
else
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
endif
|
||||
# Linux AMD64
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GOBUILD) $(LDFLAGS) -o $(BUILD_DIR)/$(APP_NAME)-linux-amd64 $(MAIN_PATH)
|
||||
# Linux ARM64
|
||||
@@ -95,147 +185,323 @@ build-all:
|
||||
# Windows AMD64
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 $(GOBUILD) $(LDFLAGS) -o $(BUILD_DIR)/$(APP_NAME)-windows-amd64.exe $(MAIN_PATH)
|
||||
|
||||
## 运行应用
|
||||
## Run application
|
||||
run: build
|
||||
@echo "启动应用..."
|
||||
@echo "Starting application..."
|
||||
./$(BUILD_DIR)/$(APP_NAME)
|
||||
|
||||
## 开发模式运行 (热重载)
|
||||
## Run in development mode
|
||||
dev:
|
||||
@echo "开发模式启动..."
|
||||
@if command -v air >/dev/null 2>&1; then \
|
||||
air; \
|
||||
else \
|
||||
echo "air 未安装,使用普通模式运行..."; \
|
||||
$(GOCMD) run $(MAIN_PATH); \
|
||||
fi
|
||||
@echo "Starting development mode..."
|
||||
$(GOCMD) run $(MAIN_PATH)
|
||||
|
||||
## 运行数据库迁移
|
||||
## Run database migration
|
||||
migrate: build
|
||||
@echo "运行数据库迁移..."
|
||||
@echo "Running database migration..."
|
||||
./$(BUILD_DIR)/$(APP_NAME) -migrate
|
||||
|
||||
## 显示版本信息
|
||||
## Show version info
|
||||
version: build
|
||||
@echo "版本信息:"
|
||||
@echo "Version info:"
|
||||
./$(BUILD_DIR)/$(APP_NAME) -version
|
||||
|
||||
## 健康检查
|
||||
## Health check
|
||||
health: build
|
||||
@echo "执行健康检查..."
|
||||
@echo "Running health check..."
|
||||
./$(BUILD_DIR)/$(APP_NAME) -health
|
||||
|
||||
## 清理构建文件
|
||||
## Clean build files
|
||||
clean:
|
||||
@echo "清理构建文件..."
|
||||
@echo "Cleaning build files..."
|
||||
$(GOCLEAN)
|
||||
rm -rf $(BUILD_DIR)
|
||||
rm -f coverage.out coverage.html
|
||||
ifeq ($(OS),Windows_NT)
|
||||
@if exist "$(BUILD_DIR)" $(RMDIR) "$(BUILD_DIR)" 2>nul || echo ""
|
||||
@if exist "coverage.out" $(RM) "coverage.out" 2>nul || echo ""
|
||||
@if exist "coverage.html" $(RM) "coverage.html" 2>nul || echo ""
|
||||
else
|
||||
$(RMDIR) $(BUILD_DIR) 2>/dev/null || true
|
||||
$(RM) coverage.out coverage.html 2>/dev/null || true
|
||||
endif
|
||||
|
||||
## 创建 .env 文件
|
||||
## Create .env file
|
||||
env:
|
||||
ifeq ($(OS),Windows_NT)
|
||||
@if not exist ".env" ( \
|
||||
echo Creating .env file from production template... && \
|
||||
copy .env.production .env && \
|
||||
echo .env file created, please modify configuration as needed \
|
||||
) else ( \
|
||||
echo .env file already exists \
|
||||
)
|
||||
else
|
||||
@if [ ! -f .env ]; then \
|
||||
echo "创建 .env 文件..."; \
|
||||
cp env.example .env; \
|
||||
echo ".env 文件已创建,请根据需要修改配置"; \
|
||||
echo "Creating .env file from production template..."; \
|
||||
cp .env.production .env; \
|
||||
echo ".env file created, please modify configuration as needed"; \
|
||||
else \
|
||||
echo ".env 文件已存在"; \
|
||||
echo ".env file already exists"; \
|
||||
fi
|
||||
endif
|
||||
|
||||
## 设置开发环境
|
||||
## Setup development environment
|
||||
setup: deps env
|
||||
@echo "设置开发环境..."
|
||||
@echo "1. 依赖已安装"
|
||||
@echo "2. .env 文件已创建"
|
||||
@echo "3. 请确保 PostgreSQL 和 Redis 正在运行"
|
||||
@echo "4. 运行 'make migrate' 创建数据库表"
|
||||
@echo "5. 运行 'make dev' 启动开发服务器"
|
||||
@echo "Setting up development environment..."
|
||||
@echo "1. [OK] Dependencies installed"
|
||||
@echo "2. [OK] .env file created from production template"
|
||||
@echo "3. [TODO] Please edit .env file and set your configuration"
|
||||
@echo "4. [NEXT] Run 'make services-up' to start PostgreSQL + Redis"
|
||||
@echo "5. [NEXT] Run 'make migrate' to create database tables"
|
||||
@echo "6. [NEXT] Run 'make dev' to start development server"
|
||||
@echo ""
|
||||
@echo "Tip: Use 'make help' to see all available commands"
|
||||
|
||||
## 构建 Docker 镜像
|
||||
## Build Docker image
|
||||
docker-build:
|
||||
@echo "构建 Docker 镜像..."
|
||||
@echo "Building Docker image..."
|
||||
docker build -t $(DOCKER_IMAGE) -t $(DOCKER_LATEST) .
|
||||
|
||||
## 运行 Docker 容器
|
||||
## Build production Docker image with registry
|
||||
docker-build-prod:
|
||||
@echo "Building production Docker image..."
|
||||
docker build \
|
||||
--build-arg VERSION=$(VERSION) \
|
||||
--build-arg COMMIT=$(GIT_COMMIT) \
|
||||
--build-arg BUILD_TIME=$(BUILD_TIME) \
|
||||
-t docker-registry.tianyuanapi.com/tyapi-server:$(VERSION) \
|
||||
-t docker-registry.tianyuanapi.com/tyapi-server:latest \
|
||||
.
|
||||
|
||||
## Push Docker image to registry
|
||||
docker-push-prod:
|
||||
@echo "Pushing Docker image to production registry..."
|
||||
docker push docker-registry.tianyuanapi.com/tyapi-server:$(VERSION)
|
||||
docker push docker-registry.tianyuanapi.com/tyapi-server:latest
|
||||
|
||||
## Deploy to production
|
||||
deploy-prod:
|
||||
@echo "Deploying to production environment..."
|
||||
ifeq ($(OS),Windows_NT)
|
||||
@if exist "scripts\\deploy.sh" ( \
|
||||
bash scripts/deploy.sh $(VERSION) \
|
||||
) else ( \
|
||||
echo "Deploy script not found" \
|
||||
)
|
||||
else
|
||||
@if [ -f scripts/deploy.sh ]; then \
|
||||
./scripts/deploy.sh $(VERSION); \
|
||||
else \
|
||||
echo "Deploy script not found"; \
|
||||
fi
|
||||
endif
|
||||
|
||||
## Start production services
|
||||
prod-up:
|
||||
@echo "Starting production services..."
|
||||
ifeq ($(OS),Windows_NT)
|
||||
@if exist "docker-compose.prod.yml" ( \
|
||||
docker-compose -f docker-compose.prod.yml up -d \
|
||||
) else ( \
|
||||
echo docker-compose.prod.yml not found \
|
||||
)
|
||||
else
|
||||
@if [ -f docker-compose.prod.yml ]; then \
|
||||
docker-compose -f docker-compose.prod.yml up -d; \
|
||||
else \
|
||||
echo "docker-compose.prod.yml not found"; \
|
||||
fi
|
||||
endif
|
||||
|
||||
## Stop production services
|
||||
prod-down:
|
||||
@echo "Stopping production services..."
|
||||
ifeq ($(OS),Windows_NT)
|
||||
@if exist "docker-compose.prod.yml" ( \
|
||||
docker-compose -f docker-compose.prod.yml down \
|
||||
) else ( \
|
||||
echo docker-compose.prod.yml not found \
|
||||
)
|
||||
else
|
||||
@if [ -f docker-compose.prod.yml ]; then \
|
||||
docker-compose -f docker-compose.prod.yml down; \
|
||||
else \
|
||||
echo "docker-compose.prod.yml not found"; \
|
||||
fi
|
||||
endif
|
||||
|
||||
## View production logs
|
||||
prod-logs:
|
||||
@echo "Viewing production logs..."
|
||||
ifeq ($(OS),Windows_NT)
|
||||
@if exist "docker-compose.prod.yml" ( \
|
||||
docker-compose -f docker-compose.prod.yml logs -f \
|
||||
) else ( \
|
||||
echo docker-compose.prod.yml not found \
|
||||
)
|
||||
else
|
||||
@if [ -f docker-compose.prod.yml ]; then \
|
||||
docker-compose -f docker-compose.prod.yml logs -f; \
|
||||
else \
|
||||
echo "docker-compose.prod.yml not found"; \
|
||||
fi
|
||||
endif
|
||||
|
||||
## Check production status
|
||||
prod-status:
|
||||
@echo "Checking production status..."
|
||||
ifeq ($(OS),Windows_NT)
|
||||
@if exist "docker-compose.prod.yml" ( \
|
||||
docker-compose -f docker-compose.prod.yml ps \
|
||||
) else ( \
|
||||
echo docker-compose.prod.yml not found \
|
||||
)
|
||||
else
|
||||
@if [ -f docker-compose.prod.yml ]; then \
|
||||
docker-compose -f docker-compose.prod.yml ps; \
|
||||
else \
|
||||
echo "docker-compose.prod.yml not found"; \
|
||||
fi
|
||||
endif
|
||||
|
||||
## Run Docker container
|
||||
docker-run:
|
||||
@echo "运行 Docker 容器..."
|
||||
@echo "Running Docker container..."
|
||||
docker run -d --name $(APP_NAME) -p 8080:8080 --env-file .env $(DOCKER_LATEST)
|
||||
|
||||
## 停止 Docker 容器
|
||||
## Stop Docker container
|
||||
docker-stop:
|
||||
@echo "停止 Docker 容器..."
|
||||
@echo "Stopping Docker container..."
|
||||
docker stop $(APP_NAME) || true
|
||||
docker rm $(APP_NAME) || true
|
||||
|
||||
## 推送 Docker 镜像
|
||||
docker-push:
|
||||
@echo "推送 Docker 镜像..."
|
||||
docker push $(DOCKER_IMAGE)
|
||||
docker push $(DOCKER_LATEST)
|
||||
|
||||
## 启动开发依赖服务 (Docker Compose)
|
||||
## Start development dependencies (Docker Compose)
|
||||
services-up:
|
||||
@echo "启动开发依赖服务..."
|
||||
@echo "Starting development dependencies..."
|
||||
ifeq ($(OS),Windows_NT)
|
||||
@if exist "docker-compose.dev.yml" ( \
|
||||
docker-compose -f docker-compose.dev.yml up -d \
|
||||
) else ( \
|
||||
echo docker-compose.dev.yml not found \
|
||||
)
|
||||
else
|
||||
@if [ -f docker-compose.dev.yml ]; then \
|
||||
docker-compose -f docker-compose.dev.yml up -d; \
|
||||
else \
|
||||
echo "docker-compose.dev.yml 不存在"; \
|
||||
echo "docker-compose.dev.yml not found"; \
|
||||
fi
|
||||
endif
|
||||
|
||||
## 停止开发依赖服务
|
||||
## Stop development dependencies
|
||||
services-down:
|
||||
@echo "停止开发依赖服务..."
|
||||
@echo "Stopping development dependencies..."
|
||||
ifeq ($(OS),Windows_NT)
|
||||
@if exist "docker-compose.dev.yml" ( \
|
||||
docker-compose -f docker-compose.dev.yml down \
|
||||
) else ( \
|
||||
echo docker-compose.dev.yml not found \
|
||||
)
|
||||
else
|
||||
@if [ -f docker-compose.dev.yml ]; then \
|
||||
docker-compose -f docker-compose.dev.yml down; \
|
||||
else \
|
||||
echo "docker-compose.dev.yml 不存在"; \
|
||||
echo "docker-compose.dev.yml not found"; \
|
||||
fi
|
||||
endif
|
||||
|
||||
## 查看服务日志
|
||||
## Alias for dev-up (start development dependencies)
|
||||
dev-up: services-up
|
||||
|
||||
## Alias for dev-down (stop development dependencies)
|
||||
dev-down: services-down
|
||||
|
||||
## Update development dependencies (rebuild and restart)
|
||||
services-update:
|
||||
@echo "Updating development dependencies..."
|
||||
ifeq ($(OS),Windows_NT)
|
||||
@if exist "docker-compose.dev.yml" ( \
|
||||
docker-compose -f docker-compose.dev.yml down && \
|
||||
docker-compose -f docker-compose.dev.yml pull && \
|
||||
docker-compose -f docker-compose.dev.yml up -d --build \
|
||||
) else ( \
|
||||
echo docker-compose.dev.yml not found \
|
||||
)
|
||||
else
|
||||
@if [ -f docker-compose.dev.yml ]; then \
|
||||
docker-compose -f docker-compose.dev.yml down && \
|
||||
docker-compose -f docker-compose.dev.yml pull && \
|
||||
docker-compose -f docker-compose.dev.yml up -d --build; \
|
||||
else \
|
||||
echo "docker-compose.dev.yml not found"; \
|
||||
fi
|
||||
endif
|
||||
|
||||
## Alias for services-update
|
||||
dev-update: services-update
|
||||
|
||||
## View application logs
|
||||
logs:
|
||||
@echo "查看应用日志..."
|
||||
@echo "Viewing application logs..."
|
||||
ifeq ($(OS),Windows_NT)
|
||||
@if exist "logs\\app.log" ( \
|
||||
powershell -Command "Get-Content logs\\app.log -Wait" \
|
||||
) else ( \
|
||||
echo "Log file does not exist" \
|
||||
)
|
||||
else
|
||||
@if [ -f logs/app.log ]; then \
|
||||
tail -f logs/app.log; \
|
||||
else \
|
||||
echo "日志文件不存在"; \
|
||||
echo "Log file does not exist"; \
|
||||
fi
|
||||
endif
|
||||
|
||||
## 生成 API 文档
|
||||
## Generate API documentation
|
||||
docs:
|
||||
@echo "生成 API 文档..."
|
||||
@echo "Generating API documentation..."
|
||||
ifeq ($(OS),Windows_NT)
|
||||
@where swag >nul 2>&1 && swag init -g $(MAIN_PATH) -o docs/swagger || echo "swag not installed, skipping documentation generation"
|
||||
else
|
||||
@if command -v swag >/dev/null 2>&1; then \
|
||||
swag init -g $(MAIN_PATH) -o docs/swagger; \
|
||||
else \
|
||||
echo "swag 未安装,跳过文档生成"; \
|
||||
echo "swag not installed, skipping documentation generation"; \
|
||||
fi
|
||||
endif
|
||||
|
||||
## 性能测试
|
||||
## Performance benchmark
|
||||
bench:
|
||||
@echo "运行性能测试..."
|
||||
@echo "Running performance benchmark..."
|
||||
$(GOTEST) -bench=. -benchmem ./...
|
||||
|
||||
## 内存泄漏检测
|
||||
## Race condition detection
|
||||
race:
|
||||
@echo "运行竞态条件检测..."
|
||||
@echo "Running race condition detection..."
|
||||
$(GOTEST) -race ./...
|
||||
|
||||
## 安全扫描
|
||||
## Security scan
|
||||
security:
|
||||
@echo "运行安全扫描..."
|
||||
@echo "Running security scan..."
|
||||
ifeq ($(OS),Windows_NT)
|
||||
@where gosec >nul 2>&1 && gosec ./... || echo "gosec not installed, skipping security scan"
|
||||
else
|
||||
@if command -v gosec >/dev/null 2>&1; then \
|
||||
gosec ./...; \
|
||||
else \
|
||||
echo "gosec 未安装,跳过安全扫描"; \
|
||||
echo "gosec not installed, skipping security scan"; \
|
||||
fi
|
||||
endif
|
||||
|
||||
## 生成模拟数据
|
||||
## Generate mock data
|
||||
mock:
|
||||
@echo "生成模拟数据..."
|
||||
@echo "Generating mock data..."
|
||||
ifeq ($(OS),Windows_NT)
|
||||
@where mockgen >nul 2>&1 && echo "Generating mock data..." || echo "mockgen not installed, please install: go install github.com/golang/mock/mockgen@latest"
|
||||
else
|
||||
@if command -v mockgen >/dev/null 2>&1; then \
|
||||
echo "生成模拟数据..."; \
|
||||
echo "Generating mock data..."; \
|
||||
else \
|
||||
echo "mockgen 未安装,请先安装: go install github.com/golang/mock/mockgen@latest"; \
|
||||
echo "mockgen not installed, please install: go install github.com/golang/mock/mockgen@latest"; \
|
||||
fi
|
||||
endif
|
||||
|
||||
## 完整的 CI 流程
|
||||
ci: deps fmt lint test build
|
||||
@@ -243,4 +509,4 @@ ci: deps fmt lint test build
|
||||
## 完整的发布流程
|
||||
release: ci build-all docker-build
|
||||
|
||||
.PHONY: help deps fmt lint test coverage build build-prod build-all run dev migrate version health clean env setup docker-build docker-run docker-stop docker-push services-up services-down logs docs bench race security mock ci release
|
||||
.PHONY: help deps fmt lint test coverage build build-prod build-all run dev migrate version health clean env setup docker-build docker-run docker-stop docker-push docker-build-prod docker-push-prod deploy-prod prod-up prod-down prod-logs prod-status services-up services-down services-update dev-up dev-down dev-update logs docs bench race security mock ci release
|
||||
347
README.md
347
README.md
@@ -1,334 +1,107 @@
|
||||
# TYAPI Server
|
||||
# TYAPI 服务端配置系统
|
||||
|
||||
## 🚀 2025 年最前沿的 Go Web 架构系统
|
||||
## 配置系统设计
|
||||
|
||||
TYAPI Server 是一个基于 Go 语言和 Gin 框架构建的现代化、高性能、模块化的 Web API 服务器。采用领域驱动设计(DDD)、CQRS、事件驱动架构等先进设计模式,为企业级应用提供坚实的技术基础。
|
||||
TYAPI 服务端采用分层配置系统,使配置管理更加灵活和清晰:
|
||||
|
||||
## ✨ 核心特性
|
||||
1. **基础配置文件** (`config.yaml`): 包含所有配置项和默认值
|
||||
2. **环境特定配置文件** (`configs/env.<环境>.yaml`): 包含特定环境需要覆盖的配置项
|
||||
3. **环境变量**: 可以覆盖任何配置项,优先级最高
|
||||
|
||||
### 🏗️ 架构特性
|
||||
## 配置文件格式
|
||||
|
||||
- **领域驱动设计(DDD)**: 清晰的业务边界和模型隔离
|
||||
- **CQRS 模式**: 命令查询责任分离,优化读写性能
|
||||
- **事件驱动**: 基于事件的异步处理和系统解耦
|
||||
- **依赖注入**: 基于 Uber FX 的完整 IoC 容器
|
||||
- **模块化设计**: 高内聚、低耦合的组件架构
|
||||
所有配置文件采用 YAML 格式,保持相同的结构层次。
|
||||
|
||||
### 🔧 技术栈
|
||||
### 基础配置文件 (config.yaml)
|
||||
|
||||
- **Web 框架**: Gin (高性能 HTTP 路由)
|
||||
- **ORM**: GORM (功能强大的对象关系映射)
|
||||
- **数据库**: PostgreSQL (主数据库) + Redis (缓存)
|
||||
- **日志**: Zap (结构化高性能日志)
|
||||
- **配置**: Viper (多格式配置管理)
|
||||
- **监控**: Prometheus + Grafana + Jaeger
|
||||
- **依赖注入**: Uber FX
|
||||
包含所有配置项和默认值,作为配置的基础。
|
||||
|
||||
### 🛡️ 生产就绪特性
|
||||
### 环境配置文件
|
||||
|
||||
- **安全性**: JWT 认证、CORS、安全头部、输入验证
|
||||
- **性能**: 智能缓存、连接池、限流、压缩
|
||||
- **可观测性**: 链路追踪、指标监控、结构化日志
|
||||
- **容错性**: 熔断器、重试机制、优雅降级
|
||||
- **运维**: 健康检查、优雅关闭、Docker 化部署
|
||||
环境配置文件只需包含需要覆盖的配置项,保持与基础配置相同的层次结构:
|
||||
|
||||
## 📁 项目结构
|
||||
- `configs/env.development.yaml`: 开发环境配置
|
||||
- `configs/env.testing.yaml`: 测试环境配置
|
||||
- `configs/env.production.yaml`: 生产环境配置
|
||||
|
||||
```
|
||||
tyapi-server-gin/
|
||||
├── cmd/ # 应用程序入口
|
||||
│ └── api/
|
||||
│ └── main.go # 主程序入口
|
||||
├── internal/ # 内部代码
|
||||
│ ├── app/ # 应用启动器
|
||||
│ ├── config/ # 配置管理
|
||||
│ ├── container/ # 依赖注入容器
|
||||
│ ├── domains/ # 业务域
|
||||
│ │ └── user/ # 用户域
|
||||
│ │ ├── dto/ # 数据传输对象
|
||||
│ │ ├── entities/ # 实体
|
||||
│ │ ├── events/ # 域事件
|
||||
│ │ ├── handlers/ # HTTP处理器
|
||||
│ │ ├── repositories/ # 仓储层
|
||||
│ │ ├── routes/ # 路由定义
|
||||
│ │ └── services/ # 业务服务
|
||||
│ └── shared/ # 共享组件
|
||||
│ ├── cache/ # 缓存服务
|
||||
│ ├── database/ # 数据库连接
|
||||
│ ├── events/ # 事件总线
|
||||
│ ├── health/ # 健康检查
|
||||
│ ├── http/ # HTTP组件
|
||||
│ ├── interfaces/ # 接口定义
|
||||
│ ├── logger/ # 日志服务
|
||||
│ └── middleware/ # 中间件
|
||||
├── deployments/ # 部署相关
|
||||
├── docs/ # 文档
|
||||
├── scripts/ # 脚本文件
|
||||
├── test/ # 测试文件
|
||||
├── config.yaml # 配置文件
|
||||
├── docker-compose.dev.yml # 开发环境
|
||||
├── Makefile # 构建脚本
|
||||
└── README.md # 项目说明
|
||||
```
|
||||
## 配置加载顺序
|
||||
|
||||
## 🚀 快速开始
|
||||
系统按以下顺序加载配置,后加载的会覆盖先加载的:
|
||||
|
||||
### 环境要求
|
||||
1. 首先加载基础配置文件 `config.yaml`
|
||||
2. 然后加载环境特定配置文件 `configs/env.<环境>.yaml`
|
||||
3. 最后应用环境变量覆盖
|
||||
|
||||
- Go 1.23.4+
|
||||
- PostgreSQL 12+
|
||||
- Redis 6+
|
||||
- Docker & Docker Compose (可选)
|
||||
## 环境确定方式
|
||||
|
||||
### 1. 克隆项目
|
||||
系统按以下优先级确定当前环境:
|
||||
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd tyapi-server-gin
|
||||
```
|
||||
1. `CONFIG_ENV` 环境变量
|
||||
2. `ENV` 环境变量
|
||||
3. `APP_ENV` 环境变量
|
||||
4. 默认值 `development`
|
||||
|
||||
### 2. 安装依赖
|
||||
## 统一配置项
|
||||
|
||||
```bash
|
||||
make deps
|
||||
```
|
||||
某些配置项在所有环境中保持一致,直接在基础配置文件中设置:
|
||||
|
||||
### 3. 配置环境
|
||||
1. **短信配置**: 所有环境使用相同的短信服务配置
|
||||
2. **基础服务地址**: 如第三方服务端点等
|
||||
|
||||
```bash
|
||||
# 创建环境变量文件
|
||||
make env
|
||||
## 使用示例
|
||||
|
||||
# 编辑 .env 文件,配置数据库连接等信息
|
||||
vim .env
|
||||
```
|
||||
|
||||
### 4. 启动依赖服务(Docker)
|
||||
|
||||
```bash
|
||||
# 启动 PostgreSQL, Redis 等服务
|
||||
make services-up
|
||||
```
|
||||
|
||||
### 5. 数据库迁移
|
||||
|
||||
```bash
|
||||
# 运行数据库迁移
|
||||
make migrate
|
||||
```
|
||||
|
||||
### 6. 启动应用
|
||||
|
||||
```bash
|
||||
# 开发模式
|
||||
make dev
|
||||
|
||||
# 或构建后运行
|
||||
make build
|
||||
make run
|
||||
```
|
||||
|
||||
## 🛠️ 开发指南
|
||||
|
||||
### Make 命令
|
||||
|
||||
```bash
|
||||
# 开发相关
|
||||
make setup # 设置开发环境
|
||||
make dev # 开发模式运行
|
||||
make build # 构建应用
|
||||
make test # 运行测试
|
||||
make lint # 代码检查
|
||||
|
||||
# 数据库相关
|
||||
make migrate # 运行迁移
|
||||
make services-up # 启动依赖服务
|
||||
make services-down # 停止依赖服务
|
||||
|
||||
# Docker相关
|
||||
make docker-build # 构建Docker镜像
|
||||
make docker-run # 运行Docker容器
|
||||
|
||||
# 其他
|
||||
make clean # 清理构建文件
|
||||
make help # 显示帮助信息
|
||||
```
|
||||
|
||||
### API 端点
|
||||
|
||||
#### 认证相关
|
||||
|
||||
- `POST /api/v1/auth/login` - 用户登录
|
||||
- `POST /api/v1/auth/register` - 用户注册
|
||||
|
||||
#### 用户管理
|
||||
|
||||
- `GET /api/v1/users` - 获取用户列表
|
||||
- `GET /api/v1/users/:id` - 获取用户详情
|
||||
- `POST /api/v1/users` - 创建用户
|
||||
- `PUT /api/v1/users/:id` - 更新用户
|
||||
- `DELETE /api/v1/users/:id` - 删除用户
|
||||
|
||||
#### 个人资料
|
||||
|
||||
- `GET /api/v1/profile` - 获取个人资料
|
||||
- `PUT /api/v1/profile` - 更新个人资料
|
||||
- `POST /api/v1/profile/change-password` - 修改密码
|
||||
|
||||
#### 系统
|
||||
|
||||
- `GET /health` - 健康检查
|
||||
- `GET /info` - 系统信息
|
||||
|
||||
### 配置说明
|
||||
|
||||
主要配置项说明:
|
||||
### 基础配置 (config.yaml)
|
||||
|
||||
```yaml
|
||||
# 应用配置
|
||||
app:
|
||||
name: "TYAPI Server"
|
||||
version: "1.0.0"
|
||||
env: "development"
|
||||
|
||||
# 服务器配置
|
||||
server:
|
||||
host: "0.0.0.0"
|
||||
port: "8080"
|
||||
|
||||
# 数据库配置
|
||||
database:
|
||||
host: "localhost"
|
||||
port: "5432"
|
||||
user: "postgres"
|
||||
password: "password"
|
||||
name: "tyapi_dev"
|
||||
password: "default_password"
|
||||
|
||||
# JWT配置
|
||||
jwt:
|
||||
secret: "your-secret-key"
|
||||
expires_in: 24h
|
||||
# 统一的短信配置
|
||||
sms:
|
||||
access_key_id: "LTAI5tKGB3TVJbMHSoZN3yr9"
|
||||
access_key_secret: "OCQ30GWp4yENMjmfOAaagksE18bp65"
|
||||
endpoint_url: "dysmsapi.aliyuncs.com"
|
||||
```
|
||||
|
||||
## 🏗️ 架构说明
|
||||
### 环境配置 (configs/env.production.yaml)
|
||||
|
||||
### 领域驱动设计
|
||||
```yaml
|
||||
app:
|
||||
env: "production"
|
||||
|
||||
项目采用 DDD 架构模式,每个业务域包含:
|
||||
database:
|
||||
host: "prod-db.example.com"
|
||||
password: "prod_secure_password"
|
||||
```
|
||||
|
||||
- **Entities**: 业务实体,包含业务逻辑
|
||||
- **DTOs**: 数据传输对象,用于 API 交互
|
||||
- **Services**: 业务服务,协调实体完成业务操作
|
||||
- **Repositories**: 仓储模式,抽象数据访问
|
||||
- **Events**: 域事件,实现模块间解耦
|
||||
|
||||
### 事件驱动架构
|
||||
|
||||
- **事件总线**: 异步事件分发机制
|
||||
- **事件处理器**: 响应特定事件的处理逻辑
|
||||
- **事件存储**: 事件溯源和审计日志
|
||||
|
||||
### 中间件系统
|
||||
|
||||
- **认证中间件**: JWT token 验证
|
||||
- **限流中间件**: API 调用频率控制
|
||||
- **日志中间件**: 请求/响应日志记录
|
||||
- **CORS 中间件**: 跨域请求处理
|
||||
- **安全中间件**: 安全头部设置
|
||||
|
||||
## 📊 监控和运维
|
||||
|
||||
### 健康检查
|
||||
### 运行时
|
||||
|
||||
```bash
|
||||
# 应用健康检查
|
||||
curl http://localhost:8080/health
|
||||
# 使用开发环境配置
|
||||
go run cmd/api/main.go
|
||||
|
||||
# 系统信息
|
||||
curl http://localhost:8080/info
|
||||
# 使用生产环境配置
|
||||
ENV=production go run cmd/api/main.go
|
||||
|
||||
# 使用环境变量覆盖特定配置
|
||||
ENV=production DB_PASSWORD=custom_password go run cmd/api/main.go
|
||||
```
|
||||
|
||||
### 指标监控
|
||||
## 敏感信息处理
|
||||
|
||||
- **Prometheus**: `http://localhost:9090`
|
||||
- **Grafana**: `http://localhost:3000` (admin/admin)
|
||||
- **Jaeger**: `http://localhost:16686`
|
||||
对于敏感信息(如密码、密钥等):
|
||||
|
||||
### 日志管理
|
||||
1. 开发环境:可以放在环境配置文件中
|
||||
2. 生产环境:应通过环境变量注入,不应出现在配置文件中
|
||||
|
||||
结构化 JSON 日志,支持不同级别:
|
||||
## 配置验证
|
||||
|
||||
```bash
|
||||
# 查看实时日志
|
||||
make logs
|
||||
|
||||
# 或直接查看文件
|
||||
tail -f logs/app.log
|
||||
```
|
||||
|
||||
## 🧪 测试
|
||||
|
||||
### 运行测试
|
||||
|
||||
```bash
|
||||
# 所有测试
|
||||
make test
|
||||
|
||||
# 生成覆盖率报告
|
||||
make coverage
|
||||
|
||||
# 性能测试
|
||||
make bench
|
||||
|
||||
# 竞态条件检测
|
||||
make race
|
||||
```
|
||||
|
||||
### 测试结构
|
||||
|
||||
- **单元测试**: 业务逻辑测试
|
||||
- **集成测试**: 数据库集成测试
|
||||
- **API 测试**: HTTP 接口测试
|
||||
|
||||
## 🚢 部署
|
||||
|
||||
### Docker 部署
|
||||
|
||||
```bash
|
||||
# 构建镜像
|
||||
make docker-build
|
||||
|
||||
# 运行容器
|
||||
make docker-run
|
||||
```
|
||||
|
||||
### 生产环境
|
||||
|
||||
1. 配置生产环境变量
|
||||
2. 使用 `config.prod.yaml`
|
||||
3. 设置适当的资源限制
|
||||
4. 配置负载均衡和反向代理
|
||||
|
||||
## 🤝 贡献指南
|
||||
|
||||
1. Fork 项目
|
||||
2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
|
||||
3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
|
||||
4. 推送到分支 (`git push origin feature/AmazingFeature`)
|
||||
5. 创建 Pull Request
|
||||
|
||||
## 📝 许可证
|
||||
|
||||
本项目采用 MIT 许可证。详情请参阅 [LICENSE](LICENSE) 文件。
|
||||
|
||||
## 🆘 支持
|
||||
|
||||
如有问题或建议,请:
|
||||
|
||||
1. 查看 [文档](docs/)
|
||||
2. 创建 [Issue](issues)
|
||||
3. 参与 [讨论](discussions)
|
||||
|
||||
---
|
||||
|
||||
**TYAPI Server** - 构建下一代 Web 应用的理想选择 🚀
|
||||
系统在启动时会验证必要的配置项,确保应用能够正常运行。如果缺少关键配置,系统将拒绝启动并提供明确的错误信息。
|
||||
|
||||
150
cmd/api/main.go
150
cmd/api/main.go
@@ -6,30 +6,72 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
_ "tyapi-server/docs" // docs is generated by Swag CLI, you have to import it.
|
||||
"tyapi-server/internal/app"
|
||||
)
|
||||
|
||||
// @title TYAPI Server API
|
||||
// @version 1.0
|
||||
// @description 基于DDD和Clean Architecture的企业级后端API服务
|
||||
// @description 采用Gin框架构建,支持用户管理、JWT认证、事件驱动等功能
|
||||
|
||||
// @contact.name API Support
|
||||
// @contact.url https://github.com/your-org/tyapi-server-gin
|
||||
// @contact.email support@example.com
|
||||
|
||||
// @license.name Apache 2.0
|
||||
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
|
||||
// @host localhost:8080
|
||||
// @BasePath /api/v1
|
||||
|
||||
// @securityDefinitions.apikey Bearer
|
||||
// @in header
|
||||
// @name Authorization
|
||||
// @description Type "Bearer" followed by a space and JWT token.
|
||||
|
||||
// 构建时注入的变量
|
||||
var (
|
||||
// 版本信息
|
||||
version = "1.0.0"
|
||||
commit = "unknown"
|
||||
version = "dev"
|
||||
commit = "none"
|
||||
date = "unknown"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 解析命令行参数
|
||||
// 命令行参数
|
||||
var (
|
||||
showVersion = flag.Bool("version", false, "显示版本信息")
|
||||
runMigrate = flag.Bool("migrate", false, "运行数据库迁移")
|
||||
healthCheck = flag.Bool("health", false, "执行健康检查")
|
||||
command = flag.String("cmd", "", "执行特定命令 (version|migrate|health)")
|
||||
migrate = flag.Bool("migrate", false, "运行数据库迁移")
|
||||
health = flag.Bool("health", false, "执行健康检查")
|
||||
env = flag.String("env", "", "指定运行环境 (development|production|testing)")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
// 显示版本信息
|
||||
// 处理版本信息显示 (不需要初始化完整应用)
|
||||
if *showVersion {
|
||||
printVersion()
|
||||
return
|
||||
fmt.Printf("TYAPI Server\n")
|
||||
fmt.Printf("Version: %s\n", version)
|
||||
fmt.Printf("Commit: %s\n", commit)
|
||||
fmt.Printf("Build Date: %s\n", date)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// 设置环境变量(如果通过命令行指定)
|
||||
if *env != "" {
|
||||
if err := validateEnvironment(*env); err != nil {
|
||||
log.Fatalf("无效的环境参数: %v", err)
|
||||
}
|
||||
os.Setenv("ENV", *env)
|
||||
fmt.Printf("🌍 通过命令行设置环境: %s\n", *env)
|
||||
}
|
||||
|
||||
// 显示当前环境
|
||||
currentEnv := getCurrentEnvironment()
|
||||
fmt.Printf("🔧 当前运行环境: %s\n", currentEnv)
|
||||
|
||||
// 生产环境安全提示
|
||||
if currentEnv == "production" {
|
||||
fmt.Printf("⚠️ 生产环境模式 - 请确保配置正确\n")
|
||||
}
|
||||
|
||||
// 创建应用程序实例
|
||||
@@ -38,73 +80,53 @@ func main() {
|
||||
log.Fatalf("Failed to create application: %v", err)
|
||||
}
|
||||
|
||||
// 处理命令行命令
|
||||
if *command != "" {
|
||||
if err := application.RunCommand(*command); err != nil {
|
||||
log.Fatalf("Command '%s' failed: %v", *command, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 运行数据库迁移
|
||||
if *runMigrate {
|
||||
if err := application.RunMigrations(); err != nil {
|
||||
// 处理命令行参数
|
||||
if *migrate {
|
||||
fmt.Println("Running database migrations...")
|
||||
if err := application.RunCommand("migrate"); err != nil {
|
||||
log.Fatalf("Migration failed: %v", err)
|
||||
}
|
||||
fmt.Println("Migration completed successfully")
|
||||
return
|
||||
fmt.Println("Database migrations completed successfully")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// 执行健康检查
|
||||
if *healthCheck {
|
||||
if err := application.HealthCheck(); err != nil {
|
||||
if *health {
|
||||
fmt.Println("Performing health check...")
|
||||
if err := application.RunCommand("health"); err != nil {
|
||||
log.Fatalf("Health check failed: %v", err)
|
||||
}
|
||||
fmt.Println("Health check passed")
|
||||
return
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// 默认:启动应用程序服务器
|
||||
logger := application.GetLogger()
|
||||
logger.Info("Starting TYAPI Server...")
|
||||
|
||||
// 启动应用程序 (使用完整的架构)
|
||||
fmt.Printf("🚀 Starting TYAPI Server v%s (%s)\n", version, commit)
|
||||
if err := application.Run(); err != nil {
|
||||
log.Fatalf("Application failed to start: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// printVersion 打印版本信息
|
||||
func printVersion() {
|
||||
fmt.Printf("TYAPI Server\n")
|
||||
fmt.Printf("Version: %s\n", version)
|
||||
fmt.Printf("Commit: %s\n", commit)
|
||||
fmt.Printf("Built: %s\n", date)
|
||||
fmt.Printf("Go Version: %s\n", getGoVersion())
|
||||
}
|
||||
|
||||
// getGoVersion 获取Go版本
|
||||
func getGoVersion() string {
|
||||
return fmt.Sprintf("%s %s/%s",
|
||||
os.Getenv("GO_VERSION"),
|
||||
os.Getenv("GOOS"),
|
||||
os.Getenv("GOARCH"))
|
||||
}
|
||||
|
||||
// 信号处理相关的辅助函数
|
||||
|
||||
// handleSignals 处理系统信号(这个函数在app包中已经实现,这里只是示例)
|
||||
func handleSignals() {
|
||||
// 信号处理逻辑已经在 app.Application 中实现
|
||||
// 这里保留作为参考
|
||||
}
|
||||
|
||||
// init 初始化函数
|
||||
func init() {
|
||||
// 设置日志格式
|
||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||
|
||||
// 环境变量检查
|
||||
if os.Getenv("APP_ENV") == "" {
|
||||
os.Setenv("APP_ENV", "development")
|
||||
// validateEnvironment 验证环境参数
|
||||
func validateEnvironment(env string) error {
|
||||
validEnvs := []string{"development", "production", "testing"}
|
||||
for _, validEnv := range validEnvs {
|
||||
if env == validEnv {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("环境必须是以下之一: %v", validEnvs)
|
||||
}
|
||||
|
||||
// getCurrentEnvironment 获取当前环境(与config包中的逻辑保持一致)
|
||||
func getCurrentEnvironment() string {
|
||||
if env := os.Getenv("CONFIG_ENV"); env != "" {
|
||||
return env
|
||||
}
|
||||
if env := os.Getenv("ENV"); env != "" {
|
||||
return env
|
||||
}
|
||||
if env := os.Getenv("APP_ENV"); env != "" {
|
||||
return env
|
||||
}
|
||||
return "development"
|
||||
}
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
# TYAPI Server Production Configuration
|
||||
|
||||
app:
|
||||
name: "TYAPI Server"
|
||||
version: "1.0.0"
|
||||
env: "production"
|
||||
|
||||
server:
|
||||
host: "0.0.0.0"
|
||||
port: "8080"
|
||||
mode: "release"
|
||||
read_timeout: 60s
|
||||
write_timeout: 60s
|
||||
idle_timeout: 300s
|
||||
|
||||
database:
|
||||
host: "${DB_HOST}"
|
||||
port: "${DB_PORT}"
|
||||
user: "${DB_USER}"
|
||||
password: "Pg9mX4kL8nW2rT5y"
|
||||
name: "${DB_NAME}"
|
||||
sslmode: "require"
|
||||
timezone: "UTC"
|
||||
max_open_conns: 50
|
||||
max_idle_conns: 25
|
||||
conn_max_lifetime: 600s
|
||||
|
||||
redis:
|
||||
host: "${REDIS_HOST}"
|
||||
port: "${REDIS_PORT}"
|
||||
password: "${REDIS_PASSWORD}"
|
||||
db: 0
|
||||
pool_size: 20
|
||||
min_idle_conns: 5
|
||||
max_retries: 3
|
||||
dial_timeout: 10s
|
||||
read_timeout: 5s
|
||||
write_timeout: 5s
|
||||
|
||||
cache:
|
||||
default_ttl: 7200s
|
||||
cleanup_interval: 300s
|
||||
max_size: 10000
|
||||
|
||||
logger:
|
||||
level: "warn"
|
||||
format: "json"
|
||||
output: "stdout"
|
||||
file_path: "/var/log/tyapi/app.log"
|
||||
max_size: 500
|
||||
max_backups: 10
|
||||
max_age: 30
|
||||
compress: true
|
||||
|
||||
jwt:
|
||||
secret: "JwT8xR4mN9vP2sL7kH3oB6yC1zA5uF0qE9tW"
|
||||
expires_in: 6h
|
||||
refresh_expires_in: 72h # 3 days
|
||||
|
||||
ratelimit:
|
||||
requests: 1000
|
||||
window: 60s
|
||||
burst: 200
|
||||
|
||||
monitoring:
|
||||
metrics_enabled: true
|
||||
metrics_port: "9090"
|
||||
tracing_enabled: true
|
||||
tracing_endpoint: "${JAEGER_ENDPOINT}"
|
||||
sample_rate: 0.01
|
||||
|
||||
health:
|
||||
enabled: true
|
||||
interval: 60s
|
||||
timeout: 30s
|
||||
|
||||
resilience:
|
||||
circuit_breaker_enabled: true
|
||||
circuit_breaker_threshold: 10
|
||||
circuit_breaker_timeout: 300s
|
||||
retry_max_attempts: 5
|
||||
retry_initial_delay: 200ms
|
||||
retry_max_delay: 30s
|
||||
|
||||
development:
|
||||
debug: false
|
||||
enable_profiler: false
|
||||
enable_cors: true
|
||||
cors_allowed_origins: "${CORS_ALLOWED_ORIGINS}"
|
||||
cors_allowed_methods: "GET,POST,PUT,PATCH,DELETE,OPTIONS"
|
||||
cors_allowed_headers: "Origin,Content-Type,Accept,Authorization,X-Requested-With"
|
||||
31
config.yaml
31
config.yaml
@@ -1,4 +1,5 @@
|
||||
# TYAPI Server Configuration
|
||||
# 🎯 统一配置文件,包含所有默认配置值
|
||||
|
||||
app:
|
||||
name: "TYAPI Server"
|
||||
@@ -24,6 +25,7 @@ database:
|
||||
max_open_conns: 25
|
||||
max_idle_conns: 10
|
||||
conn_max_lifetime: 300s
|
||||
auto_migrate: true
|
||||
|
||||
redis:
|
||||
host: "localhost"
|
||||
@@ -44,29 +46,44 @@ cache:
|
||||
|
||||
logger:
|
||||
level: "info"
|
||||
format: "json"
|
||||
format: "console"
|
||||
output: "stdout"
|
||||
file_path: "logs/app.log"
|
||||
max_size: 100
|
||||
max_backups: 3
|
||||
max_age: 7
|
||||
compress: true
|
||||
use_color: true
|
||||
|
||||
jwt:
|
||||
secret: "JwT8xR4mN9vP2sL7kH3oB6yC1zA5uF0qE9tW"
|
||||
secret: "default-jwt-secret-key-change-in-env-config"
|
||||
expires_in: 24h
|
||||
refresh_expires_in: 168h # 7 days
|
||||
refresh_expires_in: 168h
|
||||
|
||||
sms:
|
||||
access_key_id: "LTAI5tKGB3TVJbMHSoZN3yr9"
|
||||
access_key_secret: "OCQ30GWp4yENMjmfOAaagksE18bp65"
|
||||
endpoint_url: "dysmsapi.aliyuncs.com"
|
||||
sign_name: "天远数据"
|
||||
template_code: "SMS_474525324"
|
||||
code_length: 6
|
||||
expire_time: 5m
|
||||
mock_enabled: false
|
||||
rate_limit:
|
||||
daily_limit: 10
|
||||
hourly_limit: 5
|
||||
min_interval: 60s
|
||||
|
||||
ratelimit:
|
||||
requests: 100
|
||||
requests: 5000
|
||||
window: 60s
|
||||
burst: 50
|
||||
burst: 100
|
||||
|
||||
monitoring:
|
||||
metrics_enabled: true
|
||||
metrics_port: "9090"
|
||||
tracing_enabled: false
|
||||
tracing_endpoint: "http://localhost:14268/api/traces"
|
||||
tracing_enabled: true
|
||||
tracing_endpoint: "http://localhost:4317"
|
||||
sample_rate: 0.1
|
||||
|
||||
health:
|
||||
|
||||
20
configs/env.development.yaml
Normal file
20
configs/env.development.yaml
Normal file
@@ -0,0 +1,20 @@
|
||||
# 🔧 开发环境配置
|
||||
# 只包含与默认配置不同的配置项
|
||||
|
||||
# ===========================================
|
||||
# 🌍 环境标识
|
||||
# ===========================================
|
||||
app:
|
||||
env: development
|
||||
|
||||
# ===========================================
|
||||
# 🗄️ 数据库配置
|
||||
# ===========================================
|
||||
database:
|
||||
password: Pg9mX4kL8nW2rT5y
|
||||
|
||||
# ===========================================
|
||||
# 🔐 JWT配置
|
||||
# ===========================================
|
||||
jwt:
|
||||
secret: JwT8xR4mN9vP2sL7kH3oB6yC1zA5uF0qE9tW
|
||||
32
configs/env.production.yaml
Normal file
32
configs/env.production.yaml
Normal file
@@ -0,0 +1,32 @@
|
||||
# 🏭 生产环境配置
|
||||
# 只包含与默认配置不同的配置项
|
||||
|
||||
# ===========================================
|
||||
# 🌍 环境标识
|
||||
# ===========================================
|
||||
app:
|
||||
env: production
|
||||
|
||||
# ===========================================
|
||||
# 🌐 服务器配置
|
||||
# ===========================================
|
||||
server:
|
||||
mode: release
|
||||
|
||||
# ===========================================
|
||||
# 🗄️ 数据库配置
|
||||
# ===========================================
|
||||
# 敏感信息通过外部环境变量注入
|
||||
database:
|
||||
sslmode: require
|
||||
|
||||
# ===========================================
|
||||
# 📝 日志配置
|
||||
# ===========================================
|
||||
logger:
|
||||
level: warn
|
||||
format: json
|
||||
# ===========================================
|
||||
# 🔐 JWT配置
|
||||
# ===========================================
|
||||
# JWT_SECRET 必须通过外部环境变量注入
|
||||
39
configs/env.testing.yaml
Normal file
39
configs/env.testing.yaml
Normal file
@@ -0,0 +1,39 @@
|
||||
# 🧪 测试环境配置
|
||||
# 只包含与默认配置不同的配置项
|
||||
|
||||
# ===========================================
|
||||
# 🌍 环境标识
|
||||
# ===========================================
|
||||
app:
|
||||
env: testing
|
||||
|
||||
# ===========================================
|
||||
# 🌐 服务器配置
|
||||
# ===========================================
|
||||
server:
|
||||
mode: test
|
||||
|
||||
# ===========================================
|
||||
# 🗄️ 数据库配置
|
||||
# ===========================================
|
||||
database:
|
||||
password: test_password
|
||||
name: tyapi_test
|
||||
|
||||
# ===========================================
|
||||
# 📦 Redis配置
|
||||
# ===========================================
|
||||
redis:
|
||||
db: 15
|
||||
|
||||
# ===========================================
|
||||
# 📝 日志配置
|
||||
# ===========================================
|
||||
logger:
|
||||
level: debug
|
||||
|
||||
# ===========================================
|
||||
# 🔐 JWT配置
|
||||
# ===========================================
|
||||
jwt:
|
||||
secret: test-jwt-secret-key-for-testing-only
|
||||
@@ -0,0 +1,11 @@
|
||||
apiVersion: 1
|
||||
|
||||
providers:
|
||||
- name: "default"
|
||||
orgId: 1
|
||||
folder: ""
|
||||
type: file
|
||||
disableDeletion: false
|
||||
editable: true
|
||||
options:
|
||||
path: /etc/grafana/provisioning/dashboards
|
||||
@@ -0,0 +1,206 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": "-- Grafana --",
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"gnetId": null,
|
||||
"graphTooltip": 0,
|
||||
"id": null,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"datasource": "Jaeger",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"custom": {},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 1,
|
||||
"options": {
|
||||
"showThresholdLabels": false,
|
||||
"showThresholdMarkers": true
|
||||
},
|
||||
"pluginVersion": "7.5.0",
|
||||
"targets": [
|
||||
{
|
||||
"query": "tyapi-server",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "TYAPI服务链路追踪",
|
||||
"type": "jaeger"
|
||||
},
|
||||
{
|
||||
"datasource": "Prometheus",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"custom": {
|
||||
"align": null,
|
||||
"filterable": false
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 0
|
||||
},
|
||||
"id": 2,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"reduceOptions": {
|
||||
"values": false,
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"fields": ""
|
||||
},
|
||||
"textMode": "auto"
|
||||
},
|
||||
"pluginVersion": "7.5.0",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "rate(http_requests_total[5m])",
|
||||
"interval": "",
|
||||
"legendFormat": "{{method}} {{path}}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "HTTP请求速率",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": "Prometheus",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"custom": {},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "yellow",
|
||||
"value": 0.5
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "s"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 8
|
||||
},
|
||||
"id": 3,
|
||||
"options": {
|
||||
"displayMode": "list",
|
||||
"orientation": "horizontal",
|
||||
"reduceOptions": {
|
||||
"values": false,
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"fields": ""
|
||||
},
|
||||
"showUnfilled": true
|
||||
},
|
||||
"pluginVersion": "7.5.0",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))",
|
||||
"interval": "",
|
||||
"legendFormat": "95th percentile - {{method}} {{path}}",
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"expr": "histogram_quantile(0.50, rate(http_request_duration_seconds_bucket[5m]))",
|
||||
"interval": "",
|
||||
"legendFormat": "50th percentile - {{method}} {{path}}",
|
||||
"refId": "B"
|
||||
}
|
||||
],
|
||||
"title": "HTTP请求延迟分布",
|
||||
"type": "bargauge"
|
||||
}
|
||||
],
|
||||
"schemaVersion": 27,
|
||||
"style": "dark",
|
||||
"tags": [
|
||||
"jaeger",
|
||||
"tracing",
|
||||
"tyapi"
|
||||
],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {},
|
||||
"timezone": "",
|
||||
"title": "TYAPI链路追踪监控",
|
||||
"uid": "tyapi-tracing",
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: Prometheus
|
||||
type: prometheus
|
||||
access: proxy
|
||||
url: http://prometheus:9090
|
||||
isDefault: true
|
||||
editable: true
|
||||
|
||||
- name: Jaeger
|
||||
type: jaeger
|
||||
access: proxy
|
||||
url: http://jaeger:16686
|
||||
isDefault: false
|
||||
editable: true
|
||||
jsonData:
|
||||
httpMethod: GET
|
||||
# 启用节点图功能
|
||||
nodeGraph:
|
||||
enabled: true
|
||||
# 启用追踪链路图
|
||||
traceQuery:
|
||||
timeShiftEnabled: true
|
||||
spanStartTimeShift: "1h"
|
||||
spanEndTimeShift: "1h"
|
||||
# 配置标签
|
||||
tracesToLogs:
|
||||
datasourceUid: "loki"
|
||||
tags: ["job", "instance", "pod", "namespace"]
|
||||
mappedTags: [{ key: "service.name", value: "service" }]
|
||||
mapTagNamesEnabled: false
|
||||
spanStartTimeShift: "1h"
|
||||
spanEndTimeShift: "1h"
|
||||
filterByTraceID: false
|
||||
filterBySpanID: false
|
||||
109
deployments/docker/jaeger-sampling-prod.json
Normal file
109
deployments/docker/jaeger-sampling-prod.json
Normal file
@@ -0,0 +1,109 @@
|
||||
{
|
||||
"service_strategies": [
|
||||
{
|
||||
"service": "tyapi-server",
|
||||
"type": "probabilistic",
|
||||
"param": 0.01,
|
||||
"max_traces_per_second": 50,
|
||||
"operation_strategies": [
|
||||
{
|
||||
"operation": "GET /health",
|
||||
"type": "probabilistic",
|
||||
"param": 0.001
|
||||
},
|
||||
{
|
||||
"operation": "GET /metrics",
|
||||
"type": "probabilistic",
|
||||
"param": 0.001
|
||||
},
|
||||
{
|
||||
"operation": "GET /api/v1/health",
|
||||
"type": "probabilistic",
|
||||
"param": 0.001
|
||||
},
|
||||
{
|
||||
"operation": "POST /api/v1/users/register",
|
||||
"type": "probabilistic",
|
||||
"param": 0.2
|
||||
},
|
||||
{
|
||||
"operation": "POST /api/v1/users/login",
|
||||
"type": "probabilistic",
|
||||
"param": 0.1
|
||||
},
|
||||
{
|
||||
"operation": "POST /api/v1/users/logout",
|
||||
"type": "probabilistic",
|
||||
"param": 0.05
|
||||
},
|
||||
{
|
||||
"operation": "POST /api/v1/users/refresh",
|
||||
"type": "probabilistic",
|
||||
"param": 0.05
|
||||
},
|
||||
{
|
||||
"operation": "GET /api/v1/users/profile",
|
||||
"type": "probabilistic",
|
||||
"param": 0.02
|
||||
},
|
||||
{
|
||||
"operation": "PUT /api/v1/users/profile",
|
||||
"type": "probabilistic",
|
||||
"param": 0.1
|
||||
},
|
||||
{
|
||||
"operation": "POST /api/v1/sms/send",
|
||||
"type": "probabilistic",
|
||||
"param": 0.3
|
||||
},
|
||||
{
|
||||
"operation": "POST /api/v1/sms/verify",
|
||||
"type": "probabilistic",
|
||||
"param": 0.3
|
||||
},
|
||||
{
|
||||
"operation": "error",
|
||||
"type": "probabilistic",
|
||||
"param": 1.0
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"default_strategy": {
|
||||
"type": "probabilistic",
|
||||
"param": 0.01,
|
||||
"max_traces_per_second": 50
|
||||
},
|
||||
"per_operation_strategies": [
|
||||
{
|
||||
"operation": "health_check",
|
||||
"type": "probabilistic",
|
||||
"param": 0.001
|
||||
},
|
||||
{
|
||||
"operation": "metrics",
|
||||
"type": "probabilistic",
|
||||
"param": 0.001
|
||||
},
|
||||
{
|
||||
"operation": "database_query",
|
||||
"type": "probabilistic",
|
||||
"param": 0.01
|
||||
},
|
||||
{
|
||||
"operation": "redis_operation",
|
||||
"type": "probabilistic",
|
||||
"param": 0.005
|
||||
},
|
||||
{
|
||||
"operation": "external_api_call",
|
||||
"type": "probabilistic",
|
||||
"param": 0.2
|
||||
},
|
||||
{
|
||||
"operation": "error",
|
||||
"type": "probabilistic",
|
||||
"param": 1.0
|
||||
}
|
||||
]
|
||||
}
|
||||
109
deployments/docker/jaeger-sampling.json
Normal file
109
deployments/docker/jaeger-sampling.json
Normal file
@@ -0,0 +1,109 @@
|
||||
{
|
||||
"service_strategies": [
|
||||
{
|
||||
"service": "tyapi-server",
|
||||
"type": "probabilistic",
|
||||
"param": 0.1,
|
||||
"max_traces_per_second": 200,
|
||||
"operation_strategies": [
|
||||
{
|
||||
"operation": "GET /health",
|
||||
"type": "probabilistic",
|
||||
"param": 0.01
|
||||
},
|
||||
{
|
||||
"operation": "GET /metrics",
|
||||
"type": "probabilistic",
|
||||
"param": 0.01
|
||||
},
|
||||
{
|
||||
"operation": "GET /api/v1/health",
|
||||
"type": "probabilistic",
|
||||
"param": 0.01
|
||||
},
|
||||
{
|
||||
"operation": "POST /api/v1/users/register",
|
||||
"type": "probabilistic",
|
||||
"param": 0.8
|
||||
},
|
||||
{
|
||||
"operation": "POST /api/v1/users/login",
|
||||
"type": "probabilistic",
|
||||
"param": 0.8
|
||||
},
|
||||
{
|
||||
"operation": "POST /api/v1/users/logout",
|
||||
"type": "probabilistic",
|
||||
"param": 0.3
|
||||
},
|
||||
{
|
||||
"operation": "POST /api/v1/users/refresh",
|
||||
"type": "probabilistic",
|
||||
"param": 0.3
|
||||
},
|
||||
{
|
||||
"operation": "GET /api/v1/users/profile",
|
||||
"type": "probabilistic",
|
||||
"param": 0.2
|
||||
},
|
||||
{
|
||||
"operation": "PUT /api/v1/users/profile",
|
||||
"type": "probabilistic",
|
||||
"param": 0.6
|
||||
},
|
||||
{
|
||||
"operation": "POST /api/v1/sms/send",
|
||||
"type": "probabilistic",
|
||||
"param": 0.9
|
||||
},
|
||||
{
|
||||
"operation": "POST /api/v1/sms/verify",
|
||||
"type": "probabilistic",
|
||||
"param": 0.9
|
||||
},
|
||||
{
|
||||
"operation": "error",
|
||||
"type": "probabilistic",
|
||||
"param": 1.0
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"default_strategy": {
|
||||
"type": "probabilistic",
|
||||
"param": 0.1,
|
||||
"max_traces_per_second": 200
|
||||
},
|
||||
"per_operation_strategies": [
|
||||
{
|
||||
"operation": "health_check",
|
||||
"type": "probabilistic",
|
||||
"param": 0.01
|
||||
},
|
||||
{
|
||||
"operation": "metrics",
|
||||
"type": "probabilistic",
|
||||
"param": 0.01
|
||||
},
|
||||
{
|
||||
"operation": "database_query",
|
||||
"type": "probabilistic",
|
||||
"param": 0.1
|
||||
},
|
||||
{
|
||||
"operation": "redis_operation",
|
||||
"type": "probabilistic",
|
||||
"param": 0.05
|
||||
},
|
||||
{
|
||||
"operation": "external_api_call",
|
||||
"type": "probabilistic",
|
||||
"param": 0.8
|
||||
},
|
||||
{
|
||||
"operation": "error",
|
||||
"type": "probabilistic",
|
||||
"param": 1.0
|
||||
}
|
||||
]
|
||||
}
|
||||
46
deployments/docker/jaeger-ui-config.json
Normal file
46
deployments/docker/jaeger-ui-config.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"monitor": {
|
||||
"menuEnabled": true
|
||||
},
|
||||
"dependencies": {
|
||||
"menuEnabled": true
|
||||
},
|
||||
"archiveEnabled": true,
|
||||
"tracking": {
|
||||
"gaID": null,
|
||||
"trackErrors": false
|
||||
},
|
||||
"menu": [
|
||||
{
|
||||
"label": "TYAPI 文档",
|
||||
"url": "http://localhost:3000/docs",
|
||||
"anchorTarget": "_blank"
|
||||
},
|
||||
{
|
||||
"label": "Grafana 监控",
|
||||
"url": "http://localhost:3000",
|
||||
"anchorTarget": "_blank"
|
||||
},
|
||||
{
|
||||
"label": "Prometheus 指标",
|
||||
"url": "http://localhost:9090",
|
||||
"anchorTarget": "_blank"
|
||||
}
|
||||
],
|
||||
"search": {
|
||||
"maxLookback": {
|
||||
"label": "2 days",
|
||||
"value": "2d"
|
||||
},
|
||||
"maxLimit": 1500
|
||||
},
|
||||
"scripts": [],
|
||||
"linkPatterns": [
|
||||
{
|
||||
"type": "process",
|
||||
"key": "jaeger.version",
|
||||
"url": "https://github.com/jaegertracing/jaeger/releases/tag/#{jaeger.version}",
|
||||
"text": "#{jaeger.version} release notes"
|
||||
}
|
||||
]
|
||||
}
|
||||
234
deployments/docker/nginx.conf
Normal file
234
deployments/docker/nginx.conf
Normal file
@@ -0,0 +1,234 @@
|
||||
user nginx;
|
||||
worker_processes auto;
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
use epoll;
|
||||
multi_accept on;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
# 日志格式
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for" '
|
||||
'rt=$request_time uct="$upstream_connect_time" '
|
||||
'uht="$upstream_header_time" urt="$upstream_response_time"';
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
# 基本设置
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
server_tokens off;
|
||||
|
||||
# 客户端设置
|
||||
client_max_body_size 10M;
|
||||
client_body_timeout 60s;
|
||||
client_header_timeout 60s;
|
||||
|
||||
# Gzip 压缩
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_types
|
||||
text/plain
|
||||
text/css
|
||||
text/xml
|
||||
text/javascript
|
||||
application/json
|
||||
application/javascript
|
||||
application/xml+rss
|
||||
application/atom+xml;
|
||||
|
||||
# 上游服务器配置
|
||||
upstream tyapi_backend {
|
||||
server tyapi-app:8080;
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
upstream grafana_backend {
|
||||
server grafana:3000;
|
||||
keepalive 16;
|
||||
}
|
||||
|
||||
upstream prometheus_backend {
|
||||
server prometheus:9090;
|
||||
keepalive 16;
|
||||
}
|
||||
|
||||
upstream minio_backend {
|
||||
server minio:9000;
|
||||
keepalive 16;
|
||||
}
|
||||
|
||||
upstream minio_console_backend {
|
||||
server minio:9001;
|
||||
keepalive 16;
|
||||
}
|
||||
|
||||
upstream jaeger_backend {
|
||||
server jaeger:16686;
|
||||
keepalive 16;
|
||||
}
|
||||
|
||||
upstream pgadmin_backend {
|
||||
server pgadmin:80;
|
||||
keepalive 16;
|
||||
}
|
||||
|
||||
# HTTP 服务器配置
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
# 健康检查端点
|
||||
location /health {
|
||||
proxy_pass http://tyapi_backend/health;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# API 路由
|
||||
location /api/ {
|
||||
proxy_pass http://tyapi_backend;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# 超时设置
|
||||
proxy_connect_timeout 30s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
|
||||
# 缓冲设置
|
||||
proxy_buffering on;
|
||||
proxy_buffer_size 4k;
|
||||
proxy_buffers 8 4k;
|
||||
}
|
||||
|
||||
# Swagger 文档
|
||||
location /swagger/ {
|
||||
proxy_pass http://tyapi_backend;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# 根路径重定向到API文档
|
||||
location = / {
|
||||
return 301 /swagger/index.html;
|
||||
}
|
||||
|
||||
# Grafana 仪表盘
|
||||
location /grafana/ {
|
||||
proxy_pass http://grafana_backend/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# WebSocket 支持
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
|
||||
# Prometheus 监控
|
||||
location /prometheus/ {
|
||||
proxy_pass http://prometheus_backend/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# Jaeger 链路追踪
|
||||
location /jaeger/ {
|
||||
proxy_pass http://jaeger_backend/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# MinIO 对象存储 API
|
||||
location /minio/ {
|
||||
proxy_pass http://minio_backend/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# MinIO 需要的特殊头
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
client_max_body_size 1000M;
|
||||
}
|
||||
|
||||
# MinIO 控制台
|
||||
location /minio-console/ {
|
||||
proxy_pass http://minio_console_backend/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# WebSocket 支持
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
|
||||
# pgAdmin 数据库管理
|
||||
location /pgadmin/ {
|
||||
proxy_pass http://pgadmin_backend/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Script-Name /pgadmin;
|
||||
}
|
||||
|
||||
# 限制某些路径的访问
|
||||
location ~* \.(git|env|log)$ {
|
||||
deny all;
|
||||
return 404;
|
||||
}
|
||||
}
|
||||
|
||||
# HTTPS 服务器配置 (可选,需要SSL证书)
|
||||
# server {
|
||||
# listen 443 ssl http2;
|
||||
# server_name your-domain.com;
|
||||
|
||||
# ssl_certificate /etc/nginx/ssl/server.crt;
|
||||
# ssl_certificate_key /etc/nginx/ssl/server.key;
|
||||
# ssl_protocols TLSv1.2 TLSv1.3;
|
||||
# ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384;
|
||||
# ssl_prefer_server_ciphers off;
|
||||
|
||||
# # HSTS
|
||||
# add_header Strict-Transport-Security "max-age=63072000" always;
|
||||
|
||||
# location / {
|
||||
# proxy_pass http://tyapi_backend;
|
||||
# proxy_set_header Host $host;
|
||||
# proxy_set_header X-Real-IP $remote_addr;
|
||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# proxy_set_header X-Forwarded-Proto $scheme;
|
||||
# }
|
||||
# }
|
||||
}
|
||||
1
deployments/docker/pgadmin-passfile
Normal file
1
deployments/docker/pgadmin-passfile
Normal file
@@ -0,0 +1 @@
|
||||
postgres:5432:tyapi_dev:postgres:Pg9mX4kL8nW2rT5y
|
||||
15
deployments/docker/pgadmin-servers.json
Normal file
15
deployments/docker/pgadmin-servers.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"Servers": {
|
||||
"1": {
|
||||
"Name": "TYAPI PostgreSQL",
|
||||
"Group": "Development Servers",
|
||||
"Host": "postgres",
|
||||
"Port": 5432,
|
||||
"MaintenanceDB": "tyapi_dev",
|
||||
"Username": "postgres",
|
||||
"PassFile": "/var/lib/pgadmin/passfile",
|
||||
"SSLMode": "prefer",
|
||||
"Comment": "TYAPI Development Database"
|
||||
}
|
||||
}
|
||||
}
|
||||
39
deployments/docker/prometheus.yml
Normal file
39
deployments/docker/prometheus.yml
Normal file
@@ -0,0 +1,39 @@
|
||||
global:
|
||||
scrape_interval: 15s
|
||||
evaluation_interval: 15s
|
||||
|
||||
rule_files:
|
||||
# - "first_rules.yml"
|
||||
# - "second_rules.yml"
|
||||
|
||||
scrape_configs:
|
||||
# Prometheus 自身监控
|
||||
- job_name: "prometheus"
|
||||
static_configs:
|
||||
- targets: ["localhost:9090"]
|
||||
|
||||
# TYAPI 应用监控
|
||||
- job_name: "tyapi-server"
|
||||
static_configs:
|
||||
- targets: ["host.docker.internal:8080"]
|
||||
metrics_path: "/metrics"
|
||||
scrape_interval: 10s
|
||||
|
||||
# PostgreSQL 监控 (如果启用了 postgres_exporter)
|
||||
- job_name: "postgres"
|
||||
static_configs:
|
||||
- targets: ["postgres:5432"]
|
||||
scrape_interval: 30s
|
||||
|
||||
# Redis 监控 (如果启用了 redis_exporter)
|
||||
- job_name: "redis"
|
||||
static_configs:
|
||||
- targets: ["redis:6379"]
|
||||
scrape_interval: 30s
|
||||
|
||||
# Docker 容器监控 (如果启用了 cadvisor)
|
||||
- job_name: "docker"
|
||||
static_configs:
|
||||
- targets: ["host.docker.internal:8080"]
|
||||
metrics_path: "/docker/metrics"
|
||||
scrape_interval: 30s
|
||||
@@ -1,152 +1,195 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# PostgreSQL 数据库
|
||||
postgres:
|
||||
image: postgres:15-alpine
|
||||
container_name: tyapi-postgres
|
||||
environment:
|
||||
POSTGRES_DB: tyapi_dev
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: Pg9mX4kL8nW2rT5y
|
||||
POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C"
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
- ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||
networks:
|
||||
- tyapi-network
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
# PostgreSQL 数据库
|
||||
postgres:
|
||||
image: postgres:16.9
|
||||
container_name: tyapi-postgres
|
||||
environment:
|
||||
POSTGRES_DB: tyapi_dev
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: Pg9mX4kL8nW2rT5y
|
||||
POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C"
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
- ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||
networks:
|
||||
- tyapi-network
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
# Redis 缓存
|
||||
redis:
|
||||
image: redis:8.0.2
|
||||
container_name: tyapi-redis
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
- ./deployments/docker/redis.conf:/usr/local/etc/redis/redis.conf
|
||||
command: redis-server /usr/local/etc/redis/redis.conf
|
||||
networks:
|
||||
- tyapi-network
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
|
||||
# Redis 缓存
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: tyapi-redis
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
- ./deployments/docker/redis.conf:/usr/local/etc/redis/redis.conf
|
||||
command: redis-server /usr/local/etc/redis/redis.conf
|
||||
networks:
|
||||
- tyapi-network
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
# Jaeger 链路追踪
|
||||
jaeger:
|
||||
image: jaegertracing/all-in-one:1.70.0
|
||||
container_name: tyapi-jaeger
|
||||
ports:
|
||||
- "16686:16686" # Jaeger UI
|
||||
- "14268:14268" # Jaeger HTTP collector (传统)
|
||||
- "14250:14250" # Jaeger gRPC collector
|
||||
- "4317:4317" # OTLP gRPC receiver
|
||||
- "4318:4318" # OTLP HTTP receiver
|
||||
environment:
|
||||
# 启用OTLP接收器
|
||||
COLLECTOR_OTLP_ENABLED: true
|
||||
# 配置内存存储
|
||||
SPAN_STORAGE_TYPE: memory
|
||||
# 设置日志级别
|
||||
LOG_LEVEL: info
|
||||
# 配置采样策略
|
||||
SAMPLING_STRATEGIES_FILE: /etc/jaeger/sampling_strategies.json
|
||||
# 内存存储配置
|
||||
MEMORY_MAX_TRACES: 50000
|
||||
# 查询服务配置
|
||||
QUERY_MAX_CLOCK_SKEW_ADJUSTMENT: 0
|
||||
# 收集器配置
|
||||
COLLECTOR_QUEUE_SIZE: 2000
|
||||
COLLECTOR_NUM_WORKERS: 50
|
||||
# gRPC服务器配置
|
||||
COLLECTOR_GRPC_SERVER_MAX_RECEIVE_MESSAGE_LENGTH: 4194304
|
||||
COLLECTOR_GRPC_SERVER_MAX_CONNECTION_AGE: 60s
|
||||
# HTTP服务器配置
|
||||
COLLECTOR_HTTP_SERVER_HOST_PORT: :14268
|
||||
# UI配置
|
||||
QUERY_UI_CONFIG: /etc/jaeger/ui-config.json
|
||||
volumes:
|
||||
- ./deployments/docker/jaeger-sampling.json:/etc/jaeger/sampling_strategies.json
|
||||
- ./deployments/docker/jaeger-ui-config.json:/etc/jaeger/ui-config.json
|
||||
networks:
|
||||
- tyapi-network
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"wget",
|
||||
"--no-verbose",
|
||||
"--tries=1",
|
||||
"--spider",
|
||||
"http://localhost:14269/health",
|
||||
]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
|
||||
# Jaeger 链路追踪
|
||||
jaeger:
|
||||
image: jaegertracing/all-in-one:latest
|
||||
container_name: tyapi-jaeger
|
||||
ports:
|
||||
- "16686:16686" # Jaeger UI
|
||||
- "14268:14268" # Jaeger HTTP collector
|
||||
environment:
|
||||
COLLECTOR_OTLP_ENABLED: true
|
||||
networks:
|
||||
- tyapi-network
|
||||
# Prometheus 监控
|
||||
prometheus:
|
||||
image: prom/prometheus:main
|
||||
container_name: tyapi-prometheus
|
||||
ports:
|
||||
- "9090:9090"
|
||||
volumes:
|
||||
- ./deployments/docker/prometheus.yml:/etc/prometheus/prometheus.yml
|
||||
- prometheus_data:/prometheus
|
||||
command:
|
||||
- "--config.file=/etc/prometheus/prometheus.yml"
|
||||
- "--storage.tsdb.path=/prometheus"
|
||||
- "--web.console.libraries=/etc/prometheus/console_libraries"
|
||||
- "--web.console.templates=/etc/prometheus/consoles"
|
||||
- "--web.enable-lifecycle"
|
||||
networks:
|
||||
- tyapi-network
|
||||
|
||||
# Prometheus 监控
|
||||
prometheus:
|
||||
image: prom/prometheus:latest
|
||||
container_name: tyapi-prometheus
|
||||
ports:
|
||||
- "9090:9090"
|
||||
volumes:
|
||||
- ./deployments/docker/prometheus.yml:/etc/prometheus/prometheus.yml
|
||||
- prometheus_data:/prometheus
|
||||
command:
|
||||
- '--config.file=/etc/prometheus/prometheus.yml'
|
||||
- '--storage.tsdb.path=/prometheus'
|
||||
- '--web.console.libraries=/etc/prometheus/console_libraries'
|
||||
- '--web.console.templates=/etc/prometheus/consoles'
|
||||
- '--web.enable-lifecycle'
|
||||
networks:
|
||||
- tyapi-network
|
||||
# Grafana 仪表盘
|
||||
grafana:
|
||||
image: grafana/grafana:12.0.2
|
||||
container_name: tyapi-grafana
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
GF_SECURITY_ADMIN_PASSWORD: Gf7nB3xM9cV6pQ2w
|
||||
volumes:
|
||||
- grafana_data:/var/lib/grafana
|
||||
- ./deployments/docker/grafana/provisioning:/etc/grafana/provisioning
|
||||
networks:
|
||||
- tyapi-network
|
||||
|
||||
# Grafana 仪表盘
|
||||
grafana:
|
||||
image: grafana/grafana:latest
|
||||
container_name: tyapi-grafana
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
GF_SECURITY_ADMIN_PASSWORD: Gf7nB3xM9cV6pQ2w
|
||||
volumes:
|
||||
- grafana_data:/var/lib/grafana
|
||||
- ./deployments/docker/grafana/provisioning:/etc/grafana/provisioning
|
||||
networks:
|
||||
- tyapi-network
|
||||
# MinIO 对象存储 (S3兼容)
|
||||
minio:
|
||||
image: minio/minio:RELEASE.2025-06-13T11-33-47Z-cpuv1
|
||||
container_name: tyapi-minio
|
||||
ports:
|
||||
- "9000:9000"
|
||||
- "9001:9001"
|
||||
environment:
|
||||
MINIO_ROOT_USER: minioadmin
|
||||
MINIO_ROOT_PASSWORD: Mn5oH8yK3bR7vX1z
|
||||
volumes:
|
||||
- minio_data:/data
|
||||
command: server /data --console-address ":9001"
|
||||
networks:
|
||||
- tyapi-network
|
||||
healthcheck:
|
||||
test:
|
||||
["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
||||
interval: 30s
|
||||
timeout: 20s
|
||||
retries: 3
|
||||
|
||||
# MinIO 对象存储 (S3兼容)
|
||||
minio:
|
||||
image: minio/minio:latest
|
||||
container_name: tyapi-minio
|
||||
ports:
|
||||
- "9000:9000"
|
||||
- "9001:9001"
|
||||
environment:
|
||||
MINIO_ROOT_USER: minioadmin
|
||||
MINIO_ROOT_PASSWORD: Mn5oH8yK3bR7vX1z
|
||||
volumes:
|
||||
- minio_data:/data
|
||||
command: server /data --console-address ":9001"
|
||||
networks:
|
||||
- tyapi-network
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
||||
interval: 30s
|
||||
timeout: 20s
|
||||
retries: 3
|
||||
# Mailhog 邮件测试服务
|
||||
mailhog:
|
||||
image: mailhog/mailhog:v1.0.1
|
||||
container_name: tyapi-mailhog
|
||||
ports:
|
||||
- "1025:1025" # SMTP
|
||||
- "8025:8025" # Web UI
|
||||
networks:
|
||||
- tyapi-network
|
||||
|
||||
# Mailhog 邮件测试服务
|
||||
mailhog:
|
||||
image: mailhog/mailhog:latest
|
||||
container_name: tyapi-mailhog
|
||||
ports:
|
||||
- "1025:1025" # SMTP
|
||||
- "8025:8025" # Web UI
|
||||
networks:
|
||||
- tyapi-network
|
||||
|
||||
# pgAdmin 数据库管理
|
||||
pgadmin:
|
||||
image: dpage/pgadmin4:latest
|
||||
container_name: tyapi-pgadmin
|
||||
environment:
|
||||
PGADMIN_DEFAULT_EMAIL: admin@tyapi.com
|
||||
PGADMIN_DEFAULT_PASSWORD: Pa4dG9wF2sL6tN8u
|
||||
PGADMIN_CONFIG_SERVER_MODE: 'False'
|
||||
ports:
|
||||
- "5050:80"
|
||||
volumes:
|
||||
- pgadmin_data:/var/lib/pgadmin
|
||||
networks:
|
||||
- tyapi-network
|
||||
depends_on:
|
||||
- postgres
|
||||
# pgAdmin 数据库管理
|
||||
pgadmin:
|
||||
image: dpage/pgadmin4:snapshot
|
||||
container_name: tyapi-pgadmin
|
||||
environment:
|
||||
PGADMIN_DEFAULT_EMAIL: admin@tyapi.com
|
||||
PGADMIN_DEFAULT_PASSWORD: Pa4dG9wF2sL6tN8u
|
||||
PGADMIN_CONFIG_SERVER_MODE: "True"
|
||||
PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: "False"
|
||||
PGADMIN_CONFIG_UPGRADE_CHECK_ENABLED: "False"
|
||||
ports:
|
||||
- "5050:80"
|
||||
volumes:
|
||||
- pgadmin_data:/var/lib/pgadmin
|
||||
- ./deployments/docker/pgadmin-servers.json:/pgadmin4/servers.json
|
||||
- ./deployments/docker/pgadmin-passfile:/var/lib/pgadmin/passfile
|
||||
networks:
|
||||
- tyapi-network
|
||||
depends_on:
|
||||
- postgres
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
driver: local
|
||||
redis_data:
|
||||
driver: local
|
||||
prometheus_data:
|
||||
driver: local
|
||||
grafana_data:
|
||||
driver: local
|
||||
minio_data:
|
||||
driver: local
|
||||
pgadmin_data:
|
||||
driver: local
|
||||
postgres_data:
|
||||
driver: local
|
||||
redis_data:
|
||||
driver: local
|
||||
prometheus_data:
|
||||
driver: local
|
||||
grafana_data:
|
||||
driver: local
|
||||
minio_data:
|
||||
driver: local
|
||||
pgadmin_data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
tyapi-network:
|
||||
driver: bridge
|
||||
tyapi-network:
|
||||
driver: bridge
|
||||
|
||||
442
docker-compose.prod.yml
Normal file
442
docker-compose.prod.yml
Normal file
@@ -0,0 +1,442 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
# PostgreSQL 数据库 (生产环境)
|
||||
postgres:
|
||||
image: postgres:16.9
|
||||
container_name: tyapi-postgres-prod
|
||||
environment:
|
||||
POSTGRES_DB: ${DB_NAME:-tyapi_prod}
|
||||
POSTGRES_USER: ${DB_USER:-tyapi_user}
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||
POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C"
|
||||
# 性能优化配置
|
||||
POSTGRES_SHARED_PRELOAD_LIBRARIES: pg_stat_statements
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
- ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||
networks:
|
||||
- tyapi-network
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-tyapi_user}"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 60s
|
||||
restart: unless-stopped
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 2G
|
||||
cpus: "1.0"
|
||||
reservations:
|
||||
memory: 512M
|
||||
cpus: "0.5"
|
||||
# 生产环境不暴露端口到主机
|
||||
# ports:
|
||||
# - "5432:5432"
|
||||
|
||||
# Redis 缓存 (生产环境)
|
||||
redis:
|
||||
image: redis:8.0.2
|
||||
container_name: tyapi-redis-prod
|
||||
environment:
|
||||
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
- ./deployments/docker/redis.conf:/usr/local/etc/redis/redis.conf
|
||||
command: >
|
||||
sh -c "
|
||||
if [ ! -z '${REDIS_PASSWORD}' ]; then
|
||||
redis-server /usr/local/etc/redis/redis.conf --requirepass ${REDIS_PASSWORD}
|
||||
else
|
||||
redis-server /usr/local/etc/redis/redis.conf
|
||||
fi
|
||||
"
|
||||
networks:
|
||||
- tyapi-network
|
||||
healthcheck:
|
||||
test: >
|
||||
sh -c "
|
||||
if [ ! -z '${REDIS_PASSWORD}' ]; then
|
||||
redis-cli -a ${REDIS_PASSWORD} ping
|
||||
else
|
||||
redis-cli ping
|
||||
fi
|
||||
"
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
restart: unless-stopped
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 1G
|
||||
cpus: "0.5"
|
||||
reservations:
|
||||
memory: 256M
|
||||
cpus: "0.2"
|
||||
# 生产环境不暴露端口到主机
|
||||
# ports:
|
||||
# - "6379:6379"
|
||||
|
||||
# TYAPI 应用程序
|
||||
tyapi-app:
|
||||
image: docker-registry.tianyuanapi.com/tyapi-server:${APP_VERSION:-latest}
|
||||
container_name: tyapi-app-prod
|
||||
environment:
|
||||
# 环境设置
|
||||
ENV: production
|
||||
|
||||
# 服务器配置
|
||||
SERVER_PORT: ${SERVER_PORT:-8080}
|
||||
SERVER_MODE: release
|
||||
|
||||
# 数据库配置
|
||||
DB_HOST: postgres
|
||||
DB_PORT: 5432
|
||||
DB_USER: ${DB_USER:-tyapi_user}
|
||||
DB_PASSWORD: ${DB_PASSWORD}
|
||||
DB_NAME: ${DB_NAME:-tyapi_prod}
|
||||
DB_SSLMODE: ${DB_SSLMODE:-require}
|
||||
|
||||
# Redis配置
|
||||
REDIS_HOST: redis
|
||||
REDIS_PORT: 6379
|
||||
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
||||
|
||||
# JWT配置
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
|
||||
# 监控配置
|
||||
TRACING_ENABLED: true
|
||||
TRACING_ENDPOINT: http://jaeger:4317
|
||||
METRICS_ENABLED: true
|
||||
|
||||
# 日志配置
|
||||
LOG_LEVEL: ${LOG_LEVEL:-info}
|
||||
LOG_FORMAT: json
|
||||
|
||||
# 短信配置
|
||||
SMS_ACCESS_KEY_ID: ${SMS_ACCESS_KEY_ID}
|
||||
SMS_ACCESS_KEY_SECRET: ${SMS_ACCESS_KEY_SECRET}
|
||||
SMS_SIGN_NAME: ${SMS_SIGN_NAME}
|
||||
SMS_TEMPLATE_CODE: ${SMS_TEMPLATE_CODE}
|
||||
ports:
|
||||
- "${APP_PORT:-8080}:8080"
|
||||
volumes:
|
||||
- app_logs:/app/logs
|
||||
networks:
|
||||
- tyapi-network
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 60s
|
||||
restart: unless-stopped
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 1G
|
||||
cpus: "1.0"
|
||||
reservations:
|
||||
memory: 256M
|
||||
cpus: "0.3"
|
||||
|
||||
# Jaeger 链路追踪 (生产环境配置)
|
||||
jaeger:
|
||||
image: jaegertracing/all-in-one:1.70.0
|
||||
container_name: tyapi-jaeger-prod
|
||||
ports:
|
||||
- "${JAEGER_UI_PORT:-16686}:16686" # Jaeger UI
|
||||
- "4317:4317" # OTLP gRPC receiver
|
||||
- "4318:4318" # OTLP HTTP receiver
|
||||
environment:
|
||||
# 启用OTLP接收器
|
||||
COLLECTOR_OTLP_ENABLED: true
|
||||
# 配置持久化存储 (生产环境建议使用Elasticsearch/Cassandra)
|
||||
SPAN_STORAGE_TYPE: memory
|
||||
# 设置日志级别
|
||||
LOG_LEVEL: warn
|
||||
# 配置采样策略
|
||||
SAMPLING_STRATEGIES_FILE: /etc/jaeger/sampling_strategies.json
|
||||
# 内存存储配置 (生产环境应增加)
|
||||
MEMORY_MAX_TRACES: 100000
|
||||
# 查询服务配置
|
||||
QUERY_MAX_CLOCK_SKEW_ADJUSTMENT: 0
|
||||
# 收集器配置 (生产环境优化)
|
||||
COLLECTOR_QUEUE_SIZE: 5000
|
||||
COLLECTOR_NUM_WORKERS: 100
|
||||
# gRPC服务器配置
|
||||
COLLECTOR_GRPC_SERVER_MAX_RECEIVE_MESSAGE_LENGTH: 8388608
|
||||
COLLECTOR_GRPC_SERVER_MAX_CONNECTION_AGE: 120s
|
||||
COLLECTOR_GRPC_SERVER_MAX_CONNECTION_IDLE: 60s
|
||||
# HTTP服务器配置
|
||||
COLLECTOR_HTTP_SERVER_HOST_PORT: :14268
|
||||
COLLECTOR_HTTP_SERVER_READ_TIMEOUT: 30s
|
||||
COLLECTOR_HTTP_SERVER_WRITE_TIMEOUT: 30s
|
||||
# UI配置
|
||||
QUERY_UI_CONFIG: /etc/jaeger/ui-config.json
|
||||
# 安全配置
|
||||
QUERY_BASE_PATH: /
|
||||
volumes:
|
||||
- ./deployments/docker/jaeger-sampling-prod.json:/etc/jaeger/sampling_strategies.json
|
||||
- ./deployments/docker/jaeger-ui-config.json:/etc/jaeger/ui-config.json
|
||||
networks:
|
||||
- tyapi-network
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"wget",
|
||||
"--no-verbose",
|
||||
"--tries=1",
|
||||
"--spider",
|
||||
"http://localhost:14269/health",
|
||||
]
|
||||
interval: 60s
|
||||
timeout: 30s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
restart: unless-stopped
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 1G
|
||||
cpus: "0.5"
|
||||
reservations:
|
||||
memory: 512M
|
||||
cpus: "0.2"
|
||||
|
||||
# Nginx 反向代理 (可选)
|
||||
nginx:
|
||||
image: nginx:1.27.3-alpine
|
||||
container_name: tyapi-nginx-prod
|
||||
ports:
|
||||
- "${NGINX_HTTP_PORT:-80}:80"
|
||||
- "${NGINX_HTTPS_PORT:-443}:443"
|
||||
volumes:
|
||||
- ./deployments/docker/nginx.conf:/etc/nginx/nginx.conf
|
||||
- ./deployments/docker/ssl:/etc/nginx/ssl
|
||||
- nginx_logs:/var/log/nginx
|
||||
networks:
|
||||
- tyapi-network
|
||||
depends_on:
|
||||
- tyapi-app
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"wget",
|
||||
"--quiet",
|
||||
"--tries=1",
|
||||
"--spider",
|
||||
"http://localhost/health",
|
||||
]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
restart: unless-stopped
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 256M
|
||||
cpus: "0.3"
|
||||
reservations:
|
||||
memory: 64M
|
||||
cpus: "0.1"
|
||||
|
||||
# Prometheus 监控 (生产环境)
|
||||
prometheus:
|
||||
image: prom/prometheus:v2.55.1
|
||||
container_name: tyapi-prometheus-prod
|
||||
ports:
|
||||
- "${PROMETHEUS_PORT:-9090}:9090"
|
||||
volumes:
|
||||
- ./deployments/docker/prometheus.yml:/etc/prometheus/prometheus.yml
|
||||
- prometheus_data:/prometheus
|
||||
command:
|
||||
- "--config.file=/etc/prometheus/prometheus.yml"
|
||||
- "--storage.tsdb.path=/prometheus"
|
||||
- "--web.console.libraries=/etc/prometheus/console_libraries"
|
||||
- "--web.console.templates=/etc/prometheus/consoles"
|
||||
- "--web.enable-lifecycle"
|
||||
- "--storage.tsdb.retention.time=30d"
|
||||
- "--storage.tsdb.retention.size=10GB"
|
||||
- "--web.enable-admin-api"
|
||||
networks:
|
||||
- tyapi-network
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"wget",
|
||||
"--quiet",
|
||||
"--tries=1",
|
||||
"--spider",
|
||||
"http://localhost:9090/-/healthy",
|
||||
]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
restart: unless-stopped
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 2G
|
||||
cpus: "1.0"
|
||||
reservations:
|
||||
memory: 512M
|
||||
cpus: "0.3"
|
||||
|
||||
# Grafana 仪表盘 (生产环境)
|
||||
grafana:
|
||||
image: grafana/grafana:11.4.0
|
||||
container_name: tyapi-grafana-prod
|
||||
ports:
|
||||
- "${GRAFANA_PORT:-3000}:3000"
|
||||
environment:
|
||||
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD:-Gf7nB3xM9cV6pQ2w}
|
||||
GF_SECURITY_ADMIN_USER: ${GRAFANA_ADMIN_USER:-admin}
|
||||
GF_INSTALL_PLUGINS: "grafana-clock-panel,grafana-simple-json-datasource"
|
||||
GF_ANALYTICS_REPORTING_ENABLED: "false"
|
||||
GF_ANALYTICS_CHECK_FOR_UPDATES: "false"
|
||||
GF_USERS_ALLOW_SIGN_UP: "false"
|
||||
GF_SERVER_ROOT_URL: "http://localhost:3000"
|
||||
volumes:
|
||||
- grafana_data:/var/lib/grafana
|
||||
- ./deployments/docker/grafana/provisioning:/etc/grafana/provisioning
|
||||
networks:
|
||||
- tyapi-network
|
||||
depends_on:
|
||||
- prometheus
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"wget",
|
||||
"--quiet",
|
||||
"--tries=1",
|
||||
"--spider",
|
||||
"http://localhost:3000/api/health",
|
||||
]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
restart: unless-stopped
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 1G
|
||||
cpus: "0.5"
|
||||
reservations:
|
||||
memory: 256M
|
||||
cpus: "0.2"
|
||||
|
||||
# MinIO 对象存储 (生产环境)
|
||||
minio:
|
||||
image: minio/minio:RELEASE.2024-12-18T13-15-44Z
|
||||
container_name: tyapi-minio-prod
|
||||
ports:
|
||||
- "${MINIO_API_PORT:-9000}:9000"
|
||||
- "${MINIO_CONSOLE_PORT:-9001}:9001"
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minioadmin}
|
||||
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-Mn5oH8yK3bR7vX1z}
|
||||
MINIO_BROWSER_REDIRECT_URL: "http://localhost:9001"
|
||||
volumes:
|
||||
- minio_data:/data
|
||||
command: server /data --console-address ":9001"
|
||||
networks:
|
||||
- tyapi-network
|
||||
healthcheck:
|
||||
test:
|
||||
["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
||||
interval: 30s
|
||||
timeout: 20s
|
||||
retries: 3
|
||||
restart: unless-stopped
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 1G
|
||||
cpus: "0.5"
|
||||
reservations:
|
||||
memory: 256M
|
||||
cpus: "0.2"
|
||||
|
||||
# pgAdmin 数据库管理 (生产环境)
|
||||
pgadmin:
|
||||
image: dpage/pgadmin4:8.15
|
||||
container_name: tyapi-pgadmin-prod
|
||||
environment:
|
||||
PGADMIN_DEFAULT_EMAIL: ${PGADMIN_EMAIL:-admin@tyapi.com}
|
||||
PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_PASSWORD:-Pa4dG9wF2sL6tN8u}
|
||||
PGADMIN_CONFIG_SERVER_MODE: "True"
|
||||
PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: "False"
|
||||
PGADMIN_CONFIG_UPGRADE_CHECK_ENABLED: "False"
|
||||
PGADMIN_CONFIG_ENHANCED_COOKIE_PROTECTION: "False"
|
||||
ports:
|
||||
- "${PGADMIN_PORT:-5050}:80"
|
||||
volumes:
|
||||
- pgadmin_data:/var/lib/pgadmin
|
||||
- ./deployments/docker/pgadmin-servers.json:/pgadmin4/servers.json
|
||||
- ./deployments/docker/pgadmin-passfile:/var/lib/pgadmin/passfile
|
||||
networks:
|
||||
- tyapi-network
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"wget",
|
||||
"--quiet",
|
||||
"--tries=1",
|
||||
"--spider",
|
||||
"http://localhost/misc/ping",
|
||||
]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
restart: unless-stopped
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 512M
|
||||
cpus: "0.3"
|
||||
reservations:
|
||||
memory: 128M
|
||||
cpus: "0.1"
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
driver: local
|
||||
redis_data:
|
||||
driver: local
|
||||
app_logs:
|
||||
driver: local
|
||||
nginx_logs:
|
||||
driver: local
|
||||
prometheus_data:
|
||||
driver: local
|
||||
grafana_data:
|
||||
driver: local
|
||||
minio_data:
|
||||
driver: local
|
||||
pgadmin_data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
tyapi-network:
|
||||
driver: bridge
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 172.20.0.0/16
|
||||
668
docs/Makefile使用指南.md
Normal file
668
docs/Makefile使用指南.md
Normal file
@@ -0,0 +1,668 @@
|
||||
# 📋 Makefile 命令详细执行逻辑指南
|
||||
|
||||
本文档详细说明了 TYAPI 项目中每个 Make 命令的**执行逻辑、具体步骤和背后原理**。
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
```bash
|
||||
# 查看所有可用命令
|
||||
make help
|
||||
|
||||
# 设置开发环境
|
||||
make setup
|
||||
|
||||
# 启动开发依赖服务
|
||||
make dev-up
|
||||
|
||||
# 开发模式运行应用
|
||||
make dev
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📖 命令详细执行逻辑
|
||||
|
||||
### 🔍 **信息查看命令**
|
||||
|
||||
#### `make help`
|
||||
|
||||
**执行逻辑**:
|
||||
|
||||
```bash
|
||||
# 1. 检测操作系统类型
|
||||
# 2. 输出预定义的帮助信息
|
||||
@echo "TYAPI Server Makefile"
|
||||
@echo "Usage: make [target]"
|
||||
@echo "Main targets:"
|
||||
@echo " help Show this help message"
|
||||
# ... 更多帮助信息
|
||||
```
|
||||
|
||||
**实际效果**:
|
||||
|
||||
- 📄 直接打印硬编码的帮助文本
|
||||
- 🚫 不执行任何文件操作
|
||||
- ⚡ 瞬间完成,无依赖
|
||||
|
||||
#### `make version`
|
||||
|
||||
**执行逻辑**:
|
||||
|
||||
```bash
|
||||
# 步骤1: 自动执行 make build (依赖检查)
|
||||
# 步骤2: 执行构建好的二进制文件
|
||||
./bin/tyapi-server -version
|
||||
```
|
||||
|
||||
**详细步骤**:
|
||||
|
||||
1. **依赖检查**: 检查是否需要重新构建 (通过 make build)
|
||||
2. **参数传递**: 向应用程序传递 `-version` 标志
|
||||
3. **读取构建信息**: 应用程序输出编译时注入的版本信息
|
||||
- `main.version` (来自 Makefile 的 VERSION 变量)
|
||||
- `main.commit` (来自 git rev-parse --short HEAD)
|
||||
- `main.date` (来自构建时间)
|
||||
|
||||
**相关文件**: `bin/tyapi-server`, `cmd/api/main.go`
|
||||
|
||||
---
|
||||
|
||||
### 🏗️ **构建相关命令**
|
||||
|
||||
#### `make build`
|
||||
|
||||
**执行逻辑**:
|
||||
|
||||
```bash
|
||||
# 步骤1: 检测操作系统
|
||||
ifeq ($(OS),Windows_NT)
|
||||
# Windows: 创建bin目录 (如果不存在)
|
||||
@if not exist "bin" mkdir "bin"
|
||||
else
|
||||
# Unix: 创建bin目录
|
||||
@mkdir -p bin
|
||||
endif
|
||||
|
||||
# 步骤2: 构建Go应用程序
|
||||
go build -ldflags "-X main.version=1.0.0 -X main.commit=abc123 -X main.date=2025-01-01T00:00:00Z" -o bin/tyapi-server cmd/api/main.go
|
||||
```
|
||||
|
||||
**详细步骤**:
|
||||
|
||||
1. **环境检测**: 通过 `$(OS)` 变量检测操作系统
|
||||
2. **目录创建**:
|
||||
- Windows: `if not exist "bin" mkdir "bin"`
|
||||
- Unix: `mkdir -p bin`
|
||||
3. **版本信息收集**:
|
||||
- `BUILD_TIME`: PowerShell/date 命令获取当前时间
|
||||
- `GIT_COMMIT`: git 命令获取当前 commit hash
|
||||
- `VERSION`: 硬编码版本号 1.0.0
|
||||
4. **编译执行**:
|
||||
- 使用 `go build` 命令
|
||||
- `-ldflags` 注入版本信息到可执行文件
|
||||
- 输出文件到 `bin/tyapi-server`
|
||||
|
||||
**生成文件**: `bin/tyapi-server` (Windows 下为 `.exe`)
|
||||
|
||||
#### `make build-prod`
|
||||
|
||||
**执行逻辑**:
|
||||
|
||||
```bash
|
||||
# 步骤1: 创建bin目录 (同build)
|
||||
# 步骤2: 生产环境构建
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "..." -a -installsuffix cgo -o bin/tyapi-server-linux-amd64 cmd/api/main.go
|
||||
```
|
||||
|
||||
**关键参数解析**:
|
||||
|
||||
- `CGO_ENABLED=0`: 禁用 CGO,生成纯静态二进制
|
||||
- `GOOS=linux GOARCH=amd64`: 强制 Linux 64 位平台
|
||||
- `-a`: 重新构建所有包
|
||||
- `-installsuffix cgo`: 避免缓存冲突
|
||||
- **结果**: 生成可在任何 Linux x64 环境运行的静态二进制文件
|
||||
|
||||
#### `make build-all`
|
||||
|
||||
**执行逻辑**:
|
||||
|
||||
```bash
|
||||
# 步骤1: 创建bin目录
|
||||
# 步骤2: 循环构建5个平台
|
||||
# Linux AMD64
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build ... -o bin/tyapi-server-linux-amd64
|
||||
# Linux ARM64
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build ... -o bin/tyapi-server-linux-arm64
|
||||
# macOS Intel
|
||||
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build ... -o bin/tyapi-server-darwin-amd64
|
||||
# macOS Apple Silicon
|
||||
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build ... -o bin/tyapi-server-darwin-arm64
|
||||
# Windows
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build ... -o bin/tyapi-server-windows-amd64.exe
|
||||
```
|
||||
|
||||
**生成的 5 个文件**:
|
||||
|
||||
- `bin/tyapi-server-linux-amd64`
|
||||
- `bin/tyapi-server-linux-arm64`
|
||||
- `bin/tyapi-server-darwin-amd64`
|
||||
- `bin/tyapi-server-darwin-arm64`
|
||||
- `bin/tyapi-server-windows-amd64.exe`
|
||||
|
||||
---
|
||||
|
||||
### ▶️ **运行相关命令**
|
||||
|
||||
#### `make run`
|
||||
|
||||
**执行逻辑**:
|
||||
|
||||
```bash
|
||||
# 步骤1: 执行make build (依赖)
|
||||
# 步骤2: 直接运行构建好的二进制文件
|
||||
./bin/tyapi-server
|
||||
```
|
||||
|
||||
**详细流程**:
|
||||
|
||||
1. **依赖检查**: Make 自动检查 build 目标是否需要重新执行
|
||||
2. **文件存在性检查**: 确认 `bin/tyapi-server` 存在
|
||||
3. **权限检查**: Unix 系统检查执行权限
|
||||
4. **进程启动**: 启动 Gin HTTP 服务器
|
||||
5. **端口监听**: 默认监听 8080 端口
|
||||
|
||||
**运行环境**: 需要 PostgreSQL 和 Redis 服务可用
|
||||
|
||||
#### `make dev`
|
||||
|
||||
**执行逻辑**:
|
||||
|
||||
```bash
|
||||
# 直接执行 (已去掉air检查)
|
||||
go run cmd/api/main.go
|
||||
```
|
||||
|
||||
**详细步骤**:
|
||||
|
||||
1. **源码编译**: Go 编译器实时编译 main.go 及其依赖
|
||||
2. **内存运行**: 不生成磁盘文件,直接在内存中运行
|
||||
3. **依赖加载**: 自动下载并编译所有 import 的包
|
||||
4. **服务启动**: 启动 HTTP 服务器监听 8080 端口
|
||||
|
||||
**与 build 的区别**:
|
||||
|
||||
- ✅ 无需预先构建
|
||||
- ✅ 代码变更后需手动重启
|
||||
- ❌ 每次启动都需要重新编译
|
||||
|
||||
---
|
||||
|
||||
### 🛠️ **开发工具命令**
|
||||
|
||||
#### `make deps`
|
||||
|
||||
**执行逻辑**:
|
||||
|
||||
```bash
|
||||
# 步骤1: 下载依赖
|
||||
go mod download
|
||||
# 步骤2: 整理依赖
|
||||
go mod tidy
|
||||
```
|
||||
|
||||
**详细步骤**:
|
||||
|
||||
1. **go mod download**:
|
||||
- 读取 `go.mod` 文件
|
||||
- 下载所有依赖包到 `$GOPATH/pkg/mod/`
|
||||
- 验证包的 checksum (通过 go.sum)
|
||||
- 不修改 go.mod 文件
|
||||
2. **go mod tidy**:
|
||||
- 扫描所有.go 文件中的 import 语句
|
||||
- 添加缺失的依赖到 go.mod
|
||||
- 移除未使用的依赖
|
||||
- 更新 go.sum 文件
|
||||
|
||||
**影响的文件**: `go.mod`, `go.sum`, `$GOPATH/pkg/mod/`
|
||||
|
||||
#### `make fmt`
|
||||
|
||||
**执行逻辑**:
|
||||
|
||||
```bash
|
||||
go fmt ./...
|
||||
```
|
||||
|
||||
**详细步骤**:
|
||||
|
||||
1. **递归扫描**: 扫描当前目录及所有子目录的.go 文件
|
||||
2. **格式化规则应用**:
|
||||
- 统一缩进 (tab)
|
||||
- 统一换行
|
||||
- 移除行尾空格
|
||||
- 规范化大括号位置
|
||||
3. **文件修改**: 直接修改源文件 (in-place)
|
||||
4. **报告**: 输出被修改的文件列表
|
||||
|
||||
**影响的文件**: 所有.go 源码文件
|
||||
|
||||
#### `make lint`
|
||||
|
||||
**执行逻辑**:
|
||||
|
||||
```bash
|
||||
# 检查golangci-lint是否安装
|
||||
@if command -v golangci-lint >/dev/null 2>&1; then \
|
||||
golangci-lint run; \
|
||||
else \
|
||||
echo "golangci-lint not installed, skipping lint check"; \
|
||||
fi
|
||||
```
|
||||
|
||||
**详细步骤**:
|
||||
|
||||
1. **工具检查**: 使用 `command -v` 检查 golangci-lint 是否在 PATH 中
|
||||
2. **如果已安装**:
|
||||
- 读取 `.golangci.yml` 配置 (如果存在)
|
||||
- 运行多个 linter 检查 (默认包括: errcheck, gosimple, govet, ineffassign 等)
|
||||
- 分析所有.go 文件
|
||||
- 输出问题报告
|
||||
3. **如果未安装**: 显示提示信息并跳过
|
||||
|
||||
**检查项目**: 代码质量、潜在 bug、性能问题、安全问题等
|
||||
|
||||
---
|
||||
|
||||
### 🧪 **测试相关命令**
|
||||
|
||||
#### `make test`
|
||||
|
||||
**执行逻辑**:
|
||||
|
||||
```bash
|
||||
go test -v -race -coverprofile=coverage.out ./...
|
||||
```
|
||||
|
||||
**参数详解**:
|
||||
|
||||
- `-v`: 详细输出,显示每个测试的名称和结果
|
||||
- `-race`: 启用竞态条件检测器
|
||||
- `-coverprofile=coverage.out`: 生成覆盖率数据文件
|
||||
- `./...`: 递归测试所有包
|
||||
|
||||
**详细步骤**:
|
||||
|
||||
1. **包发现**: 递归扫描所有包含\*\_test.go 的目录
|
||||
2. **编译测试**: 为每个包编译测试二进制文件
|
||||
3. **竞态检测**: 启用 Go race detector
|
||||
4. **执行测试**: 逐个运行 Test\*函数
|
||||
5. **覆盖率收集**: 记录每行代码是否被执行
|
||||
6. **生成报告**: 输出到 coverage.out 文件
|
||||
|
||||
**生成文件**: `coverage.out`
|
||||
|
||||
#### `make coverage`
|
||||
|
||||
**执行逻辑**:
|
||||
|
||||
```bash
|
||||
# 步骤1: 执行make test (依赖)
|
||||
# 步骤2: 生成HTML报告
|
||||
go tool cover -html=coverage.out -o coverage.html
|
||||
# 步骤3: 提示用户
|
||||
@echo "Coverage report generated: coverage.html"
|
||||
```
|
||||
|
||||
**详细步骤**:
|
||||
|
||||
1. **依赖检查**: 确保 coverage.out 文件存在
|
||||
2. **HTML 生成**:
|
||||
- 读取 coverage.out 二进制数据
|
||||
- 生成带颜色标记的 HTML 页面
|
||||
- 绿色 = 已覆盖,红色 = 未覆盖
|
||||
3. **文件输出**: 生成 coverage.html 文件
|
||||
|
||||
**生成文件**: `coverage.html` (可在浏览器中打开)
|
||||
|
||||
---
|
||||
|
||||
### 🗂️ **环境管理命令**
|
||||
|
||||
#### `make env`
|
||||
|
||||
**执行逻辑**:
|
||||
|
||||
```bash
|
||||
# Windows环境
|
||||
ifeq ($(OS),Windows_NT)
|
||||
@if not exist ".env" ( \
|
||||
echo Creating .env file... && \
|
||||
copy env.example .env && \
|
||||
echo .env file created, please modify configuration as needed \
|
||||
) else ( \
|
||||
echo .env file already exists \
|
||||
)
|
||||
# Unix环境
|
||||
else
|
||||
@if [ ! -f .env ]; then \
|
||||
echo "Creating .env file..."; \
|
||||
cp env.example .env; \
|
||||
echo ".env file created, please modify configuration as needed"; \
|
||||
else \
|
||||
echo ".env file already exists"; \
|
||||
fi
|
||||
endif
|
||||
```
|
||||
|
||||
**详细步骤**:
|
||||
|
||||
1. **文件检查**: 检查.env 文件是否已存在
|
||||
2. **如果不存在**:
|
||||
- Windows: 使用 `copy` 命令复制
|
||||
- Unix: 使用 `cp` 命令复制
|
||||
- 复制 `env.example` → `.env`
|
||||
3. **如果已存在**: 显示提示信息,不覆盖现有文件
|
||||
|
||||
**相关文件**: `env.example` → `.env`
|
||||
|
||||
#### `make setup`
|
||||
|
||||
**执行逻辑**:
|
||||
|
||||
```bash
|
||||
# 依赖: make deps env
|
||||
# 然后执行:
|
||||
@echo "Setting up development environment..."
|
||||
@echo "1. Dependencies installed"
|
||||
@echo "2. .env file created"
|
||||
@echo "3. Please ensure PostgreSQL and Redis are running"
|
||||
@echo "4. Run 'make migrate' to create database tables"
|
||||
@echo "5. Run 'make dev' to start development server"
|
||||
```
|
||||
|
||||
**详细步骤**:
|
||||
|
||||
1. **执行 make deps**: 安装 Go 依赖
|
||||
2. **执行 make env**: 创建环境配置文件
|
||||
3. **输出设置指南**: 显示后续步骤提示
|
||||
|
||||
**完成后状态**: 开发环境基本就绪,需要手动启动数据库服务
|
||||
|
||||
---
|
||||
|
||||
### 🐳 **Docker 相关命令**
|
||||
|
||||
#### `make docker-build`
|
||||
|
||||
**执行逻辑**:
|
||||
|
||||
```bash
|
||||
docker build -t tyapi-server:1.0.0 -t tyapi-server:latest .
|
||||
```
|
||||
|
||||
**详细步骤**:
|
||||
|
||||
1. **读取 Dockerfile**: 从当前目录读取 Dockerfile
|
||||
2. **构建上下文**: 将当前目录作为构建上下文发送给 Docker daemon
|
||||
3. **镜像构建**:
|
||||
- 执行 Dockerfile 中的每个指令
|
||||
- 逐层构建镜像
|
||||
- 缓存中间层以提高构建速度
|
||||
4. **标签应用**: 同时打上版本标签和 latest 标签
|
||||
|
||||
**生成镜像**: `tyapi-server:1.0.0`, `tyapi-server:latest`
|
||||
|
||||
#### `make docker-run`
|
||||
|
||||
**执行逻辑**:
|
||||
|
||||
```bash
|
||||
docker run -d --name tyapi-server -p 8080:8080 --env-file .env tyapi-server:latest
|
||||
```
|
||||
|
||||
**详细步骤**:
|
||||
|
||||
1. **环境检查**: 确认.env 文件存在
|
||||
2. **容器创建**: 基于 latest 镜像创建容器
|
||||
3. **参数应用**:
|
||||
- `-d`: 后台运行 (detached mode)
|
||||
- `--name tyapi-server`: 设置容器名称
|
||||
- `-p 8080:8080`: 端口映射 (主机:容器)
|
||||
- `--env-file .env`: 加载环境变量文件
|
||||
4. **容器启动**: 启动应用程序进程
|
||||
|
||||
**结果**: 后台运行的 Docker 容器,端口 8080 可访问
|
||||
|
||||
---
|
||||
|
||||
### 🔧 **服务管理命令**
|
||||
|
||||
#### `make services-up` / `make dev-up`
|
||||
|
||||
**执行逻辑**:
|
||||
|
||||
```bash
|
||||
# 检查docker-compose.dev.yml文件
|
||||
# Windows:
|
||||
@if exist "docker-compose.dev.yml" ( \
|
||||
docker-compose -f docker-compose.dev.yml up -d \
|
||||
) else ( \
|
||||
echo docker-compose.dev.yml not found \
|
||||
)
|
||||
```
|
||||
|
||||
**详细步骤**:
|
||||
|
||||
1. **文件检查**: 验证 docker-compose.dev.yml 文件存在
|
||||
2. **Docker Compose 启动**:
|
||||
- 读取 docker-compose.dev.yml 配置
|
||||
- 拉取所需的 Docker 镜像 (如果本地不存在)
|
||||
- 创建 Docker 网络 (tyapi-network)
|
||||
- 创建数据卷 (postgres_data, redis_data 等)
|
||||
- 按依赖顺序启动服务:
|
||||
- PostgreSQL (端口 5432)
|
||||
- Redis (端口 6379)
|
||||
- pgAdmin (端口 5050)
|
||||
- Prometheus (端口 9090)
|
||||
- Grafana (端口 3000)
|
||||
- Jaeger (端口 16686)
|
||||
- MinIO (端口 9000/9001)
|
||||
- MailHog (端口 8025)
|
||||
|
||||
**启动的 8 个服务**: 数据库、缓存、监控、管理工具等完整开发环境
|
||||
|
||||
#### `make services-down` / `make dev-down`
|
||||
|
||||
**执行逻辑**:
|
||||
|
||||
```bash
|
||||
docker-compose -f docker-compose.dev.yml down
|
||||
```
|
||||
|
||||
**详细步骤**:
|
||||
|
||||
1. **容器停止**: 优雅停止所有服务容器 (发送 SIGTERM)
|
||||
2. **容器删除**: 删除所有相关容器
|
||||
3. **网络清理**: 删除自定义网络
|
||||
4. **数据保留**: 保留数据卷 (数据不丢失)
|
||||
|
||||
**保留的资源**: 数据卷、镜像
|
||||
**删除的资源**: 容器、网络
|
||||
|
||||
---
|
||||
|
||||
### 🗃️ **数据库相关命令**
|
||||
|
||||
#### `make migrate`
|
||||
|
||||
**执行逻辑**:
|
||||
|
||||
```bash
|
||||
# 步骤1: 执行make build (依赖)
|
||||
# 步骤2: 运行迁移
|
||||
./bin/tyapi-server -migrate
|
||||
```
|
||||
|
||||
**详细步骤**:
|
||||
|
||||
1. **应用程序启动**: 以迁移模式启动应用
|
||||
2. **数据库连接**: 连接到 PostgreSQL 数据库
|
||||
3. **迁移文件扫描**: 扫描 migrations 目录下的 SQL 文件
|
||||
4. **版本检查**: 检查数据库中的迁移版本表
|
||||
5. **增量执行**: 只执行未应用的迁移文件
|
||||
6. **版本更新**: 更新迁移版本记录
|
||||
7. **应用退出**: 迁移完成后程序退出
|
||||
|
||||
**相关文件**: `internal/domains/user/migrations/`, PostgreSQL 数据库
|
||||
|
||||
#### `make health`
|
||||
|
||||
**执行逻辑**:
|
||||
|
||||
```bash
|
||||
# 步骤1: 执行make build (依赖)
|
||||
# 步骤2: 运行健康检查
|
||||
./bin/tyapi-server -health
|
||||
```
|
||||
|
||||
**详细步骤**:
|
||||
|
||||
1. **健康检查启动**: 以健康检查模式启动应用
|
||||
2. **组件检查**:
|
||||
- PostgreSQL 数据库连接
|
||||
- Redis 缓存连接
|
||||
- 关键配置项验证
|
||||
- 必要文件存在性检查
|
||||
3. **状态报告**: 输出每个组件的健康状态
|
||||
4. **退出码**: 成功返回 0,失败返回非零
|
||||
|
||||
**检查项目**: 数据库、缓存、配置、权限等
|
||||
|
||||
---
|
||||
|
||||
### 🧹 **清理命令**
|
||||
|
||||
#### `make clean`
|
||||
|
||||
**执行逻辑**:
|
||||
|
||||
```bash
|
||||
# 步骤1: Go缓存清理
|
||||
go clean
|
||||
# 步骤2: 文件删除 (根据操作系统)
|
||||
# Windows:
|
||||
@if exist "bin" rmdir /s /q "bin" 2>nul || echo ""
|
||||
@if exist "coverage.out" del /f /q "coverage.out" 2>nul || echo ""
|
||||
@if exist "coverage.html" del /f /q "coverage.html" 2>nul || echo ""
|
||||
```
|
||||
|
||||
**详细步骤**:
|
||||
|
||||
1. **Go 清理**:
|
||||
- 清理编译缓存
|
||||
- 删除临时构建文件
|
||||
- 清理测试缓存
|
||||
2. **目录删除**:
|
||||
- 删除整个 bin 目录及内容
|
||||
- Windows: `rmdir /s /q`
|
||||
- Unix: `rm -rf`
|
||||
3. **文件删除**:
|
||||
- 删除 coverage.out 测试覆盖率文件
|
||||
- 删除 coverage.html 覆盖率报告
|
||||
4. **错误抑制**: 使用 `2>nul` 或 `2>/dev/null` 忽略"文件不存在"错误
|
||||
|
||||
**删除的内容**: `bin/`, `coverage.out`, `coverage.html`, Go 构建缓存
|
||||
|
||||
---
|
||||
|
||||
### 🚀 **流水线命令**
|
||||
|
||||
#### `make ci`
|
||||
|
||||
**执行逻辑**:
|
||||
|
||||
```bash
|
||||
# 顺序执行5个步骤:
|
||||
make deps # 安装依赖
|
||||
make fmt # 代码格式化
|
||||
make lint # 代码检查
|
||||
make test # 运行测试
|
||||
make build # 构建应用
|
||||
```
|
||||
|
||||
**详细流程**:
|
||||
|
||||
1. **deps**: 确保所有依赖最新且完整
|
||||
2. **fmt**: 统一代码格式,确保可读性
|
||||
3. **lint**: 静态代码分析,发现潜在问题
|
||||
4. **test**: 运行所有测试,确保功能正确
|
||||
5. **build**: 验证代码可以成功编译
|
||||
|
||||
**失败策略**: 任何一步失败,立即停止后续步骤
|
||||
|
||||
#### `make release`
|
||||
|
||||
**执行逻辑**:
|
||||
|
||||
```bash
|
||||
# 顺序执行3个步骤:
|
||||
make ci # 完整CI检查
|
||||
make build-all # 交叉编译所有平台
|
||||
make docker-build # 构建Docker镜像
|
||||
```
|
||||
|
||||
**详细流程**:
|
||||
|
||||
1. **CI 检查**: 确保代码质量和功能正确性
|
||||
2. **多平台构建**: 生成 5 个平台的可执行文件
|
||||
3. **Docker 镜像**: 构建容器化版本
|
||||
|
||||
**输出产物**:
|
||||
|
||||
- 5 个平台的二进制文件
|
||||
- Docker 镜像 (2 个标签)
|
||||
- 测试覆盖率报告
|
||||
|
||||
---
|
||||
|
||||
## 💡 **命令执行顺序和依赖关系**
|
||||
|
||||
### 🔗 **依赖关系图**
|
||||
|
||||
```
|
||||
version ─────► build
|
||||
health ──────► build
|
||||
run ─────────► build
|
||||
migrate ─────► build
|
||||
|
||||
coverage ────► test
|
||||
ci ──────────► deps → fmt → lint → test → build
|
||||
release ─────► ci → build-all → docker-build
|
||||
setup ───────► deps → env
|
||||
```
|
||||
|
||||
### ⚡ **执行时机建议**
|
||||
|
||||
#### **每次开发前**
|
||||
|
||||
```bash
|
||||
make setup # 首次使用
|
||||
make dev-up # 启动依赖服务
|
||||
make dev # 开始开发
|
||||
```
|
||||
|
||||
#### **提交代码前**
|
||||
|
||||
```bash
|
||||
make ci # 完整检查
|
||||
```
|
||||
|
||||
#### **发布版本前**
|
||||
|
||||
```bash
|
||||
make release # 完整构建
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**这个指南详细说明了每个命令背后的具体操作逻辑,帮助您完全理解 Makefile 的工作原理!** 🎯
|
||||
8
docs/docs.go
Normal file
8
docs/docs.go
Normal file
@@ -0,0 +1,8 @@
|
||||
// Package docs 生成的API文档包
|
||||
// 这个包导入了自动生成的Swagger文档
|
||||
package docs
|
||||
|
||||
import (
|
||||
// 导入生成的swagger文档
|
||||
_ "tyapi-server/docs/swagger"
|
||||
)
|
||||
592
docs/swagger/docs.go
Normal file
592
docs/swagger/docs.go
Normal file
@@ -0,0 +1,592 @@
|
||||
// Package swagger Code generated by swaggo/swag. DO NOT EDIT
|
||||
package swagger
|
||||
|
||||
import "github.com/swaggo/swag"
|
||||
|
||||
const docTemplate = `{
|
||||
"schemes": {{ marshal .Schemes }},
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"description": "{{escape .Description}}",
|
||||
"title": "{{.Title}}",
|
||||
"contact": {
|
||||
"name": "API Support",
|
||||
"url": "https://github.com/your-org/tyapi-server-gin",
|
||||
"email": "support@example.com"
|
||||
},
|
||||
"license": {
|
||||
"name": "Apache 2.0",
|
||||
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
|
||||
},
|
||||
"version": "{{.Version}}"
|
||||
},
|
||||
"host": "{{.Host}}",
|
||||
"basePath": "{{.BasePath}}",
|
||||
"paths": {
|
||||
"/users/login-password": {
|
||||
"post": {
|
||||
"description": "使用手机号和密码进行用户登录,返回JWT令牌",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"用户认证"
|
||||
],
|
||||
"summary": "用户密码登录",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "密码登录请求",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.LoginWithPasswordRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "登录成功",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.LoginResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "请求参数错误",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "认证失败",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "服务器内部错误",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/login-sms": {
|
||||
"post": {
|
||||
"description": "使用手机号和短信验证码进行用户登录,返回JWT令牌",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"用户认证"
|
||||
],
|
||||
"summary": "用户短信验证码登录",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "短信登录请求",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.LoginWithSMSRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "登录成功",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.LoginResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "请求参数错误或验证码无效",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "认证失败",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "服务器内部错误",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/me": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"description": "根据JWT令牌获取当前登录用户的详细信息",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"用户管理"
|
||||
],
|
||||
"summary": "获取当前用户信息",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "用户信息",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.UserResponse"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "未认证",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "用户不存在",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "服务器内部错误",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/me/password": {
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"description": "使用旧密码、新密码确认和验证码修改当前用户的密码",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"用户管理"
|
||||
],
|
||||
"summary": "修改密码",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "修改密码请求",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ChangePasswordRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "密码修改成功",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "请求参数错误或验证码无效",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "未认证",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "服务器内部错误",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/register": {
|
||||
"post": {
|
||||
"description": "使用手机号、密码和验证码进行用户注册,需要确认密码",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"用户认证"
|
||||
],
|
||||
"summary": "用户注册",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "用户注册请求",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.RegisterRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "注册成功",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.UserResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "请求参数错误或验证码无效",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"409": {
|
||||
"description": "手机号已存在",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "服务器内部错误",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/send-code": {
|
||||
"post": {
|
||||
"description": "向指定手机号发送验证码,支持注册、登录、修改密码等场景",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"用户认证"
|
||||
],
|
||||
"summary": "发送短信验证码",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "发送验证码请求",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.SendCodeRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "验证码发送成功",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.SendCodeResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "请求参数错误",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"429": {
|
||||
"description": "请求频率限制",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "服务器内部错误",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"dto.ChangePasswordRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"code",
|
||||
"confirm_new_password",
|
||||
"new_password",
|
||||
"old_password"
|
||||
],
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "string",
|
||||
"example": "123456"
|
||||
},
|
||||
"confirm_new_password": {
|
||||
"type": "string",
|
||||
"example": "newpassword123"
|
||||
},
|
||||
"new_password": {
|
||||
"type": "string",
|
||||
"maxLength": 128,
|
||||
"minLength": 6,
|
||||
"example": "newpassword123"
|
||||
},
|
||||
"old_password": {
|
||||
"type": "string",
|
||||
"example": "oldpassword123"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.LoginResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"access_token": {
|
||||
"type": "string",
|
||||
"example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||
},
|
||||
"expires_in": {
|
||||
"type": "integer",
|
||||
"example": 86400
|
||||
},
|
||||
"login_method": {
|
||||
"description": "password 或 sms",
|
||||
"type": "string",
|
||||
"example": "password"
|
||||
},
|
||||
"token_type": {
|
||||
"type": "string",
|
||||
"example": "Bearer"
|
||||
},
|
||||
"user": {
|
||||
"$ref": "#/definitions/dto.UserResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.LoginWithPasswordRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"password",
|
||||
"phone"
|
||||
],
|
||||
"properties": {
|
||||
"password": {
|
||||
"type": "string",
|
||||
"example": "password123"
|
||||
},
|
||||
"phone": {
|
||||
"type": "string",
|
||||
"example": "13800138000"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.LoginWithSMSRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"code",
|
||||
"phone"
|
||||
],
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "string",
|
||||
"example": "123456"
|
||||
},
|
||||
"phone": {
|
||||
"type": "string",
|
||||
"example": "13800138000"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.RegisterRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"code",
|
||||
"confirm_password",
|
||||
"password",
|
||||
"phone"
|
||||
],
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "string",
|
||||
"example": "123456"
|
||||
},
|
||||
"confirm_password": {
|
||||
"type": "string",
|
||||
"example": "password123"
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"maxLength": 128,
|
||||
"minLength": 6,
|
||||
"example": "password123"
|
||||
},
|
||||
"phone": {
|
||||
"type": "string",
|
||||
"example": "13800138000"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.SendCodeRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"phone",
|
||||
"scene"
|
||||
],
|
||||
"properties": {
|
||||
"phone": {
|
||||
"type": "string",
|
||||
"example": "13800138000"
|
||||
},
|
||||
"scene": {
|
||||
"enum": [
|
||||
"register",
|
||||
"login",
|
||||
"change_password",
|
||||
"reset_password",
|
||||
"bind",
|
||||
"unbind"
|
||||
],
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/entities.SMSScene"
|
||||
}
|
||||
],
|
||||
"example": "register"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.SendCodeResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"expires_at": {
|
||||
"type": "string",
|
||||
"example": "2024-01-01T00:05:00Z"
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"example": "验证码发送成功"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.UserResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"example": "2024-01-01T00:00:00Z"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"example": "123e4567-e89b-12d3-a456-426614174000"
|
||||
},
|
||||
"phone": {
|
||||
"type": "string",
|
||||
"example": "13800138000"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"example": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entities.SMSScene": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"register",
|
||||
"login",
|
||||
"change_password",
|
||||
"reset_password",
|
||||
"bind",
|
||||
"unbind"
|
||||
],
|
||||
"x-enum-comments": {
|
||||
"SMSSceneBind": "绑定手机号",
|
||||
"SMSSceneChangePassword": "修改密码",
|
||||
"SMSSceneLogin": "登录",
|
||||
"SMSSceneRegister": "注册",
|
||||
"SMSSceneResetPassword": "重置密码",
|
||||
"SMSSceneUnbind": "解绑手机号"
|
||||
},
|
||||
"x-enum-varnames": [
|
||||
"SMSSceneRegister",
|
||||
"SMSSceneLogin",
|
||||
"SMSSceneChangePassword",
|
||||
"SMSSceneResetPassword",
|
||||
"SMSSceneBind",
|
||||
"SMSSceneUnbind"
|
||||
]
|
||||
}
|
||||
},
|
||||
"securityDefinitions": {
|
||||
"Bearer": {
|
||||
"description": "Type \"Bearer\" followed by a space and JWT token.",
|
||||
"type": "apiKey",
|
||||
"name": "Authorization",
|
||||
"in": "header"
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
// SwaggerInfo holds exported Swagger Info so clients can modify it
|
||||
var SwaggerInfo = &swag.Spec{
|
||||
Version: "1.0",
|
||||
Host: "localhost:8080",
|
||||
BasePath: "/api/v1",
|
||||
Schemes: []string{},
|
||||
Title: "TYAPI Server API",
|
||||
Description: "基于DDD和Clean Architecture的企业级后端API服务\n采用Gin框架构建,支持用户管理、JWT认证、事件驱动等功能",
|
||||
InfoInstanceName: "swagger",
|
||||
SwaggerTemplate: docTemplate,
|
||||
LeftDelim: "{{",
|
||||
RightDelim: "}}",
|
||||
}
|
||||
|
||||
func init() {
|
||||
swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
|
||||
}
|
||||
568
docs/swagger/swagger.json
Normal file
568
docs/swagger/swagger.json
Normal file
@@ -0,0 +1,568 @@
|
||||
{
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"description": "基于DDD和Clean Architecture的企业级后端API服务\n采用Gin框架构建,支持用户管理、JWT认证、事件驱动等功能",
|
||||
"title": "TYAPI Server API",
|
||||
"contact": {
|
||||
"name": "API Support",
|
||||
"url": "https://github.com/your-org/tyapi-server-gin",
|
||||
"email": "support@example.com"
|
||||
},
|
||||
"license": {
|
||||
"name": "Apache 2.0",
|
||||
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
|
||||
},
|
||||
"version": "1.0"
|
||||
},
|
||||
"host": "localhost:8080",
|
||||
"basePath": "/api/v1",
|
||||
"paths": {
|
||||
"/users/login-password": {
|
||||
"post": {
|
||||
"description": "使用手机号和密码进行用户登录,返回JWT令牌",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"用户认证"
|
||||
],
|
||||
"summary": "用户密码登录",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "密码登录请求",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.LoginWithPasswordRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "登录成功",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.LoginResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "请求参数错误",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "认证失败",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "服务器内部错误",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/login-sms": {
|
||||
"post": {
|
||||
"description": "使用手机号和短信验证码进行用户登录,返回JWT令牌",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"用户认证"
|
||||
],
|
||||
"summary": "用户短信验证码登录",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "短信登录请求",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.LoginWithSMSRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "登录成功",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.LoginResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "请求参数错误或验证码无效",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "认证失败",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "服务器内部错误",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/me": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"description": "根据JWT令牌获取当前登录用户的详细信息",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"用户管理"
|
||||
],
|
||||
"summary": "获取当前用户信息",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "用户信息",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.UserResponse"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "未认证",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "用户不存在",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "服务器内部错误",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/me/password": {
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"description": "使用旧密码、新密码确认和验证码修改当前用户的密码",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"用户管理"
|
||||
],
|
||||
"summary": "修改密码",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "修改密码请求",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.ChangePasswordRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "密码修改成功",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "请求参数错误或验证码无效",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "未认证",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "服务器内部错误",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/register": {
|
||||
"post": {
|
||||
"description": "使用手机号、密码和验证码进行用户注册,需要确认密码",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"用户认证"
|
||||
],
|
||||
"summary": "用户注册",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "用户注册请求",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.RegisterRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "注册成功",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.UserResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "请求参数错误或验证码无效",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"409": {
|
||||
"description": "手机号已存在",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "服务器内部错误",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/send-code": {
|
||||
"post": {
|
||||
"description": "向指定手机号发送验证码,支持注册、登录、修改密码等场景",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"用户认证"
|
||||
],
|
||||
"summary": "发送短信验证码",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "发送验证码请求",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.SendCodeRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "验证码发送成功",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.SendCodeResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "请求参数错误",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"429": {
|
||||
"description": "请求频率限制",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "服务器内部错误",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"dto.ChangePasswordRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"code",
|
||||
"confirm_new_password",
|
||||
"new_password",
|
||||
"old_password"
|
||||
],
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "string",
|
||||
"example": "123456"
|
||||
},
|
||||
"confirm_new_password": {
|
||||
"type": "string",
|
||||
"example": "newpassword123"
|
||||
},
|
||||
"new_password": {
|
||||
"type": "string",
|
||||
"maxLength": 128,
|
||||
"minLength": 6,
|
||||
"example": "newpassword123"
|
||||
},
|
||||
"old_password": {
|
||||
"type": "string",
|
||||
"example": "oldpassword123"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.LoginResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"access_token": {
|
||||
"type": "string",
|
||||
"example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||
},
|
||||
"expires_in": {
|
||||
"type": "integer",
|
||||
"example": 86400
|
||||
},
|
||||
"login_method": {
|
||||
"description": "password 或 sms",
|
||||
"type": "string",
|
||||
"example": "password"
|
||||
},
|
||||
"token_type": {
|
||||
"type": "string",
|
||||
"example": "Bearer"
|
||||
},
|
||||
"user": {
|
||||
"$ref": "#/definitions/dto.UserResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.LoginWithPasswordRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"password",
|
||||
"phone"
|
||||
],
|
||||
"properties": {
|
||||
"password": {
|
||||
"type": "string",
|
||||
"example": "password123"
|
||||
},
|
||||
"phone": {
|
||||
"type": "string",
|
||||
"example": "13800138000"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.LoginWithSMSRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"code",
|
||||
"phone"
|
||||
],
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "string",
|
||||
"example": "123456"
|
||||
},
|
||||
"phone": {
|
||||
"type": "string",
|
||||
"example": "13800138000"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.RegisterRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"code",
|
||||
"confirm_password",
|
||||
"password",
|
||||
"phone"
|
||||
],
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "string",
|
||||
"example": "123456"
|
||||
},
|
||||
"confirm_password": {
|
||||
"type": "string",
|
||||
"example": "password123"
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"maxLength": 128,
|
||||
"minLength": 6,
|
||||
"example": "password123"
|
||||
},
|
||||
"phone": {
|
||||
"type": "string",
|
||||
"example": "13800138000"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.SendCodeRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"phone",
|
||||
"scene"
|
||||
],
|
||||
"properties": {
|
||||
"phone": {
|
||||
"type": "string",
|
||||
"example": "13800138000"
|
||||
},
|
||||
"scene": {
|
||||
"enum": [
|
||||
"register",
|
||||
"login",
|
||||
"change_password",
|
||||
"reset_password",
|
||||
"bind",
|
||||
"unbind"
|
||||
],
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/entities.SMSScene"
|
||||
}
|
||||
],
|
||||
"example": "register"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.SendCodeResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"expires_at": {
|
||||
"type": "string",
|
||||
"example": "2024-01-01T00:05:00Z"
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"example": "验证码发送成功"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.UserResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"example": "2024-01-01T00:00:00Z"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"example": "123e4567-e89b-12d3-a456-426614174000"
|
||||
},
|
||||
"phone": {
|
||||
"type": "string",
|
||||
"example": "13800138000"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"example": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entities.SMSScene": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"register",
|
||||
"login",
|
||||
"change_password",
|
||||
"reset_password",
|
||||
"bind",
|
||||
"unbind"
|
||||
],
|
||||
"x-enum-comments": {
|
||||
"SMSSceneBind": "绑定手机号",
|
||||
"SMSSceneChangePassword": "修改密码",
|
||||
"SMSSceneLogin": "登录",
|
||||
"SMSSceneRegister": "注册",
|
||||
"SMSSceneResetPassword": "重置密码",
|
||||
"SMSSceneUnbind": "解绑手机号"
|
||||
},
|
||||
"x-enum-varnames": [
|
||||
"SMSSceneRegister",
|
||||
"SMSSceneLogin",
|
||||
"SMSSceneChangePassword",
|
||||
"SMSSceneResetPassword",
|
||||
"SMSSceneBind",
|
||||
"SMSSceneUnbind"
|
||||
]
|
||||
}
|
||||
},
|
||||
"securityDefinitions": {
|
||||
"Bearer": {
|
||||
"description": "Type \"Bearer\" followed by a space and JWT token.",
|
||||
"type": "apiKey",
|
||||
"name": "Authorization",
|
||||
"in": "header"
|
||||
}
|
||||
}
|
||||
}
|
||||
397
docs/swagger/swagger.yaml
Normal file
397
docs/swagger/swagger.yaml
Normal file
@@ -0,0 +1,397 @@
|
||||
basePath: /api/v1
|
||||
definitions:
|
||||
dto.ChangePasswordRequest:
|
||||
properties:
|
||||
code:
|
||||
example: "123456"
|
||||
type: string
|
||||
confirm_new_password:
|
||||
example: newpassword123
|
||||
type: string
|
||||
new_password:
|
||||
example: newpassword123
|
||||
maxLength: 128
|
||||
minLength: 6
|
||||
type: string
|
||||
old_password:
|
||||
example: oldpassword123
|
||||
type: string
|
||||
required:
|
||||
- code
|
||||
- confirm_new_password
|
||||
- new_password
|
||||
- old_password
|
||||
type: object
|
||||
dto.LoginResponse:
|
||||
properties:
|
||||
access_token:
|
||||
example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||
type: string
|
||||
expires_in:
|
||||
example: 86400
|
||||
type: integer
|
||||
login_method:
|
||||
description: password 或 sms
|
||||
example: password
|
||||
type: string
|
||||
token_type:
|
||||
example: Bearer
|
||||
type: string
|
||||
user:
|
||||
$ref: '#/definitions/dto.UserResponse'
|
||||
type: object
|
||||
dto.LoginWithPasswordRequest:
|
||||
properties:
|
||||
password:
|
||||
example: password123
|
||||
type: string
|
||||
phone:
|
||||
example: "13800138000"
|
||||
type: string
|
||||
required:
|
||||
- password
|
||||
- phone
|
||||
type: object
|
||||
dto.LoginWithSMSRequest:
|
||||
properties:
|
||||
code:
|
||||
example: "123456"
|
||||
type: string
|
||||
phone:
|
||||
example: "13800138000"
|
||||
type: string
|
||||
required:
|
||||
- code
|
||||
- phone
|
||||
type: object
|
||||
dto.RegisterRequest:
|
||||
properties:
|
||||
code:
|
||||
example: "123456"
|
||||
type: string
|
||||
confirm_password:
|
||||
example: password123
|
||||
type: string
|
||||
password:
|
||||
example: password123
|
||||
maxLength: 128
|
||||
minLength: 6
|
||||
type: string
|
||||
phone:
|
||||
example: "13800138000"
|
||||
type: string
|
||||
required:
|
||||
- code
|
||||
- confirm_password
|
||||
- password
|
||||
- phone
|
||||
type: object
|
||||
dto.SendCodeRequest:
|
||||
properties:
|
||||
phone:
|
||||
example: "13800138000"
|
||||
type: string
|
||||
scene:
|
||||
allOf:
|
||||
- $ref: '#/definitions/entities.SMSScene'
|
||||
enum:
|
||||
- register
|
||||
- login
|
||||
- change_password
|
||||
- reset_password
|
||||
- bind
|
||||
- unbind
|
||||
example: register
|
||||
required:
|
||||
- phone
|
||||
- scene
|
||||
type: object
|
||||
dto.SendCodeResponse:
|
||||
properties:
|
||||
expires_at:
|
||||
example: "2024-01-01T00:05:00Z"
|
||||
type: string
|
||||
message:
|
||||
example: 验证码发送成功
|
||||
type: string
|
||||
type: object
|
||||
dto.UserResponse:
|
||||
properties:
|
||||
created_at:
|
||||
example: "2024-01-01T00:00:00Z"
|
||||
type: string
|
||||
id:
|
||||
example: 123e4567-e89b-12d3-a456-426614174000
|
||||
type: string
|
||||
phone:
|
||||
example: "13800138000"
|
||||
type: string
|
||||
updated_at:
|
||||
example: "2024-01-01T00:00:00Z"
|
||||
type: string
|
||||
type: object
|
||||
entities.SMSScene:
|
||||
enum:
|
||||
- register
|
||||
- login
|
||||
- change_password
|
||||
- reset_password
|
||||
- bind
|
||||
- unbind
|
||||
type: string
|
||||
x-enum-comments:
|
||||
SMSSceneBind: 绑定手机号
|
||||
SMSSceneChangePassword: 修改密码
|
||||
SMSSceneLogin: 登录
|
||||
SMSSceneRegister: 注册
|
||||
SMSSceneResetPassword: 重置密码
|
||||
SMSSceneUnbind: 解绑手机号
|
||||
x-enum-varnames:
|
||||
- SMSSceneRegister
|
||||
- SMSSceneLogin
|
||||
- SMSSceneChangePassword
|
||||
- SMSSceneResetPassword
|
||||
- SMSSceneBind
|
||||
- SMSSceneUnbind
|
||||
host: localhost:8080
|
||||
info:
|
||||
contact:
|
||||
email: support@example.com
|
||||
name: API Support
|
||||
url: https://github.com/your-org/tyapi-server-gin
|
||||
description: |-
|
||||
基于DDD和Clean Architecture的企业级后端API服务
|
||||
采用Gin框架构建,支持用户管理、JWT认证、事件驱动等功能
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
title: TYAPI Server API
|
||||
version: "1.0"
|
||||
paths:
|
||||
/users/login-password:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 使用手机号和密码进行用户登录,返回JWT令牌
|
||||
parameters:
|
||||
- description: 密码登录请求
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/dto.LoginWithPasswordRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: 登录成功
|
||||
schema:
|
||||
$ref: '#/definitions/dto.LoginResponse'
|
||||
"400":
|
||||
description: 请求参数错误
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"401":
|
||||
description: 认证失败
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"500":
|
||||
description: 服务器内部错误
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
summary: 用户密码登录
|
||||
tags:
|
||||
- 用户认证
|
||||
/users/login-sms:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 使用手机号和短信验证码进行用户登录,返回JWT令牌
|
||||
parameters:
|
||||
- description: 短信登录请求
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/dto.LoginWithSMSRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: 登录成功
|
||||
schema:
|
||||
$ref: '#/definitions/dto.LoginResponse'
|
||||
"400":
|
||||
description: 请求参数错误或验证码无效
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"401":
|
||||
description: 认证失败
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"500":
|
||||
description: 服务器内部错误
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
summary: 用户短信验证码登录
|
||||
tags:
|
||||
- 用户认证
|
||||
/users/me:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 根据JWT令牌获取当前登录用户的详细信息
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: 用户信息
|
||||
schema:
|
||||
$ref: '#/definitions/dto.UserResponse'
|
||||
"401":
|
||||
description: 未认证
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"404":
|
||||
description: 用户不存在
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"500":
|
||||
description: 服务器内部错误
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: 获取当前用户信息
|
||||
tags:
|
||||
- 用户管理
|
||||
/users/me/password:
|
||||
put:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 使用旧密码、新密码确认和验证码修改当前用户的密码
|
||||
parameters:
|
||||
- description: 修改密码请求
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/dto.ChangePasswordRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: 密码修改成功
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"400":
|
||||
description: 请求参数错误或验证码无效
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"401":
|
||||
description: 未认证
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"500":
|
||||
description: 服务器内部错误
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: 修改密码
|
||||
tags:
|
||||
- 用户管理
|
||||
/users/register:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 使用手机号、密码和验证码进行用户注册,需要确认密码
|
||||
parameters:
|
||||
- description: 用户注册请求
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/dto.RegisterRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"201":
|
||||
description: 注册成功
|
||||
schema:
|
||||
$ref: '#/definitions/dto.UserResponse'
|
||||
"400":
|
||||
description: 请求参数错误或验证码无效
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"409":
|
||||
description: 手机号已存在
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"500":
|
||||
description: 服务器内部错误
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
summary: 用户注册
|
||||
tags:
|
||||
- 用户认证
|
||||
/users/send-code:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 向指定手机号发送验证码,支持注册、登录、修改密码等场景
|
||||
parameters:
|
||||
- description: 发送验证码请求
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/dto.SendCodeRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: 验证码发送成功
|
||||
schema:
|
||||
$ref: '#/definitions/dto.SendCodeResponse'
|
||||
"400":
|
||||
description: 请求参数错误
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"429":
|
||||
description: 请求频率限制
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"500":
|
||||
description: 服务器内部错误
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
summary: 发送短信验证码
|
||||
tags:
|
||||
- 用户认证
|
||||
securityDefinitions:
|
||||
Bearer:
|
||||
description: Type "Bearer" followed by a space and JWT token.
|
||||
in: header
|
||||
name: Authorization
|
||||
type: apiKey
|
||||
swagger: "2.0"
|
||||
@@ -221,17 +221,18 @@ iostat 1 5
|
||||
|
||||
## 开发环境问题
|
||||
|
||||
### 1. 热重载不工作
|
||||
### 1. 开发服务器问题
|
||||
|
||||
```bash
|
||||
# 检查文件监控
|
||||
ls -la .air.toml
|
||||
# 停止当前开发服务器
|
||||
Ctrl+C
|
||||
|
||||
# 重启开发服务器
|
||||
make dev-restart
|
||||
# 重新启动开发服务器
|
||||
make dev
|
||||
|
||||
# 检查文件权限
|
||||
chmod +x scripts/dev.sh
|
||||
# 检查Go模块状态
|
||||
go mod tidy
|
||||
go mod download
|
||||
```
|
||||
|
||||
### 2. 测试失败
|
||||
145
docs/开始指南/文档索引.md
Normal file
145
docs/开始指南/文档索引.md
Normal file
@@ -0,0 +1,145 @@
|
||||
# 📚 TYAPI Server 文档中心
|
||||
|
||||
欢迎使用 TYAPI Server 文档中心!我们已将原本的使用指南拆分为多个专题文档,方便您按需查阅。
|
||||
|
||||
## 📋 文档导航
|
||||
|
||||
### 🚀 [快速开始指南](./快速开始指南.md)
|
||||
|
||||
- 前置要求
|
||||
- 一键启动
|
||||
- 验证安装
|
||||
- 访问管理界面
|
||||
|
||||
### 🔧 [环境搭建指南](./环境搭建指南.md)
|
||||
|
||||
- 开发环境配置
|
||||
- 生产环境配置
|
||||
- 服务配置说明
|
||||
- 常见配置问题
|
||||
|
||||
### 📋 [Makefile 命令指南](./MAKEFILE_GUIDE.md)
|
||||
|
||||
- 所有 Make 命令详细说明
|
||||
- 常用工作流程
|
||||
- 构建和部署命令
|
||||
- 开发工具命令
|
||||
- 故障排除技巧
|
||||
|
||||
### 👨💻 [开发指南](./开发指南.md)
|
||||
|
||||
- 项目结构理解
|
||||
- 开发流程
|
||||
- 测试编写
|
||||
- 调试技巧
|
||||
- 代码规范
|
||||
|
||||
### 🌐 [API 使用指南](./API使用指南.md)
|
||||
|
||||
- 认证机制
|
||||
- 用户管理 API
|
||||
- 响应格式
|
||||
- HTTP 状态码
|
||||
- API 测试
|
||||
|
||||
### 🚀 [部署指南](./部署指南.md)
|
||||
|
||||
- Docker 部署
|
||||
- Kubernetes 部署
|
||||
- 云平台部署
|
||||
- 负载均衡配置
|
||||
- 监控部署
|
||||
|
||||
### 📦 [生产环境部署指南](./生产环境部署指南.md)
|
||||
|
||||
- Docker + 私有 Registry 完整部署方案
|
||||
- 多阶段构建生产级镜像
|
||||
- 安全配置和资源限制
|
||||
- 自动化部署脚本
|
||||
- 监控和故障排除
|
||||
|
||||
### 🔍 [故障排除指南](./故障排除指南.md)
|
||||
|
||||
- 常见问题
|
||||
- 日志分析
|
||||
- 性能问题
|
||||
- 紧急响应流程
|
||||
|
||||
### 📋 [最佳实践指南](./最佳实践指南.md)
|
||||
|
||||
- 开发最佳实践
|
||||
- 安全最佳实践
|
||||
- 性能最佳实践
|
||||
- 运维最佳实践
|
||||
- 团队协作
|
||||
|
||||
### 🔍 [链路追踪指南](./链路追踪指南.md)
|
||||
|
||||
- Jaeger 配置和使用
|
||||
- OpenTelemetry 集成
|
||||
- Grafana 可视化
|
||||
- 性能监控和优化
|
||||
- 故障排查技巧
|
||||
|
||||
## 🎯 快速索引
|
||||
|
||||
### 新手入门
|
||||
|
||||
1. [快速开始指南](./快速开始指南.md) - 5 分钟快速体验
|
||||
2. [环境搭建指南](./环境搭建指南.md) - 配置开发环境
|
||||
3. [Makefile 命令指南](./MAKEFILE_GUIDE.md) - 掌握所有开发命令
|
||||
4. [开发指南](./开发指南.md) - 开始第一个功能
|
||||
|
||||
### 日常开发
|
||||
|
||||
- [Makefile 命令指南](./MAKEFILE_GUIDE.md) - 构建、测试、部署命令
|
||||
- [API 使用指南](./API使用指南.md) - API 调用参考
|
||||
- [开发指南](./开发指南.md) - 开发流程和规范
|
||||
- [链路追踪指南](./链路追踪指南.md) - 性能监控和问题排查
|
||||
- [故障排除指南](./故障排除指南.md) - 解决常见问题
|
||||
|
||||
### 生产部署
|
||||
|
||||
- [部署指南](./部署指南.md) - 生产环境部署
|
||||
- [生产环境部署指南](./生产环境部署指南.md) - Docker + 私有 Registry 完整方案
|
||||
- [最佳实践指南](./最佳实践指南.md) - 运维最佳实践
|
||||
- [故障排除指南](./故障排除指南.md) - 生产问题排查
|
||||
|
||||
## 🔗 相关文档
|
||||
|
||||
### 技术文档
|
||||
|
||||
- [架构文档](./ARCHITECTURE.md) - 系统架构设计
|
||||
- [API 规范](http://localhost:8080/swagger/) - 在线 API 文档
|
||||
|
||||
### 项目文档
|
||||
|
||||
- [README](../README.md) - 项目介绍
|
||||
- [更新日志](../CHANGELOG.md) - 版本变更记录
|
||||
|
||||
## 📞 获取帮助
|
||||
|
||||
### 在线资源
|
||||
|
||||
- **Swagger UI**: http://localhost:8080/swagger/
|
||||
- **健康检查**: http://localhost:8080/api/v1/health
|
||||
- **监控面板**: http://localhost:3000 (Grafana)
|
||||
- **链路追踪**: http://localhost:16686 (Jaeger)
|
||||
|
||||
### 社区支持
|
||||
|
||||
- **GitHub Issues**: 提交问题和建议
|
||||
- **Wiki**: 查看详细技术文档
|
||||
- **讨论区**: 参与技术讨论
|
||||
|
||||
## 🔄 文档更新
|
||||
|
||||
本文档会持续更新,如果您发现任何问题或有改进建议,请:
|
||||
|
||||
1. 提交 GitHub Issue
|
||||
2. 发起 Pull Request
|
||||
3. 联系维护团队
|
||||
|
||||
---
|
||||
|
||||
**提示**:建议将此页面加入书签,方便随时查阅相关文档。
|
||||
338
docs/开始指南/生产环境部署指南.md
Normal file
338
docs/开始指南/生产环境部署指南.md
Normal file
@@ -0,0 +1,338 @@
|
||||
# TYAPI 生产环境部署指南
|
||||
|
||||
## 🎯 **部署架构概览**
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 生产环境架构 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Nginx (80/443) ──► TYAPI App (8080) ──► PostgreSQL (5432) │
|
||||
│ │ │ │ │
|
||||
│ │ └──► Redis (6379) │ │
|
||||
│ │ └──► Jaeger (4317) │ │
|
||||
│ └──► Jaeger UI (16686) │ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
私有镜像仓库: docker-registry.tianyuanapi.com
|
||||
```
|
||||
|
||||
## 📋 **部署清单**
|
||||
|
||||
### ✅ **已创建的文件**
|
||||
|
||||
- `Dockerfile` - 多阶段构建的生产级镜像
|
||||
- `docker-compose.prod.yml` - 生产环境服务编排
|
||||
- `.dockerignore` - Docker 构建忽略文件
|
||||
- `deployments/docker/nginx.conf` - Nginx 反向代理配置
|
||||
- `.env.production` - 生产环境配置模板
|
||||
- `scripts/deploy.sh` - Linux/macOS 部署脚本
|
||||
- `scripts/deploy.ps1` - Windows PowerShell 部署脚本
|
||||
- `Makefile` - 新增生产环境相关命令
|
||||
|
||||
### 🛠 **服务组件**
|
||||
|
||||
1. **PostgreSQL** - 主数据库 (生产优化配置)
|
||||
2. **Redis** - 缓存和会话存储 (密码保护)
|
||||
3. **TYAPI App** - 主应用程序 (生产模式)
|
||||
4. **Jaeger** - 链路追踪 (生产级配置)
|
||||
5. **Nginx** - 反向代理和负载均衡
|
||||
6. **Prometheus** - 监控数据收集和存储
|
||||
7. **Grafana** - 监控数据可视化仪表盘
|
||||
8. **MinIO** - S3 兼容对象存储服务
|
||||
9. **pgAdmin** - PostgreSQL 数据库管理工具
|
||||
|
||||
## 🚀 **快速部署步骤**
|
||||
|
||||
### 1️⃣ **环境准备**
|
||||
|
||||
```bash
|
||||
# 确保服务器已安装
|
||||
- Docker 20.10+
|
||||
- Docker Compose 2.0+
|
||||
- Git (可选)
|
||||
|
||||
# 检查版本
|
||||
docker --version
|
||||
docker-compose --version
|
||||
```
|
||||
|
||||
### 2️⃣ **获取代码**
|
||||
|
||||
```bash
|
||||
# 克隆项目到服务器
|
||||
git clone <your-repo-url> tyapi-server
|
||||
cd tyapi-server
|
||||
|
||||
# 或直接上传项目文件
|
||||
```
|
||||
|
||||
### 3️⃣ **配置环境变量**
|
||||
|
||||
```bash
|
||||
# 复制配置模板
|
||||
cp .env.production .env
|
||||
|
||||
# 编辑配置文件
|
||||
nano .env
|
||||
```
|
||||
|
||||
**必须修改的关键配置:**
|
||||
|
||||
```bash
|
||||
# 数据库配置
|
||||
DB_PASSWORD=your_secure_database_password_here
|
||||
|
||||
# Redis配置
|
||||
REDIS_PASSWORD=your_secure_redis_password_here
|
||||
|
||||
# JWT密钥 (至少32位)
|
||||
JWT_SECRET=your_super_secure_jwt_secret_key_for_production_at_least_32_chars
|
||||
|
||||
# Grafana管理员配置
|
||||
GRAFANA_ADMIN_PASSWORD=your_secure_grafana_password_here
|
||||
|
||||
# MinIO对象存储配置
|
||||
MINIO_ROOT_PASSWORD=your_secure_minio_password_here
|
||||
|
||||
# pgAdmin数据库管理配置
|
||||
PGADMIN_PASSWORD=your_secure_pgadmin_password_here
|
||||
|
||||
# 短信服务配置
|
||||
SMS_ACCESS_KEY_ID=your_sms_access_key_id
|
||||
SMS_ACCESS_KEY_SECRET=your_sms_access_key_secret
|
||||
SMS_SIGN_NAME=your_sms_sign_name
|
||||
SMS_TEMPLATE_CODE=your_sms_template_code
|
||||
```
|
||||
|
||||
### 4️⃣ **执行部署**
|
||||
|
||||
#### **Linux/macOS:**
|
||||
|
||||
```bash
|
||||
# 给脚本执行权限
|
||||
chmod +x scripts/deploy.sh
|
||||
|
||||
# 部署指定版本
|
||||
./scripts/deploy.sh v1.0.0
|
||||
|
||||
# 或部署最新版本
|
||||
./scripts/deploy.sh
|
||||
```
|
||||
|
||||
#### **Windows:**
|
||||
|
||||
```powershell
|
||||
# 执行部署脚本
|
||||
.\scripts\deploy.ps1 -Version "v1.0.0"
|
||||
|
||||
# 或使用Makefile
|
||||
make docker-build-prod
|
||||
make docker-push-prod
|
||||
make prod-up
|
||||
```
|
||||
|
||||
## 📊 **部署脚本功能**
|
||||
|
||||
### 🔄 **自动化流程**
|
||||
|
||||
1. **环境检查** - 验证 Docker、docker-compose 等工具
|
||||
2. **配置验证** - 检查关键配置项的安全性
|
||||
3. **镜像构建** - 构建生产级 Docker 镜像
|
||||
4. **镜像推送** - 推送到私有 Registry
|
||||
5. **服务部署** - 启动所有生产服务
|
||||
6. **健康检查** - 验证服务运行状态
|
||||
7. **信息展示** - 显示访问地址和管理命令
|
||||
|
||||
### 🛡 **安全特性**
|
||||
|
||||
- **非 root 用户运行** - 容器内使用专用用户
|
||||
- **资源限制** - CPU 和内存使用限制
|
||||
- **健康检查** - 自动重启异常服务
|
||||
- **网络隔离** - 独立的 Docker 网络
|
||||
- **密码保护** - 数据库和 Redis 强制密码
|
||||
- **SSL 就绪** - Nginx HTTPS 配置模板
|
||||
|
||||
## 🎛 **管理命令**
|
||||
|
||||
### **通过 Makefile 管理:**
|
||||
|
||||
```bash
|
||||
# 构建生产镜像
|
||||
make docker-build-prod
|
||||
|
||||
# 推送到Registry
|
||||
make docker-push-prod
|
||||
|
||||
# 启动生产服务
|
||||
make prod-up
|
||||
|
||||
# 停止生产服务
|
||||
make prod-down
|
||||
|
||||
# 查看服务状态
|
||||
make prod-status
|
||||
|
||||
# 查看实时日志
|
||||
make prod-logs
|
||||
```
|
||||
|
||||
### **通过 docker-compose 管理:**
|
||||
|
||||
```bash
|
||||
# 启动所有服务
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
|
||||
# 停止所有服务
|
||||
docker-compose -f docker-compose.prod.yml down
|
||||
|
||||
# 查看服务状态
|
||||
docker-compose -f docker-compose.prod.yml ps
|
||||
|
||||
# 查看日志
|
||||
docker-compose -f docker-compose.prod.yml logs -f
|
||||
|
||||
# 重启特定服务
|
||||
docker-compose -f docker-compose.prod.yml restart tyapi-app
|
||||
```
|
||||
|
||||
## 🌐 **服务访问地址**
|
||||
|
||||
部署成功后,可以通过以下地址访问服务:
|
||||
|
||||
### **核心服务**
|
||||
|
||||
- **API 服务**: `http://your-server:8080`
|
||||
- **API 文档**: `http://your-server:8080/swagger/index.html`
|
||||
- **健康检查**: `http://your-server:8080/health`
|
||||
|
||||
### **监控和追踪**
|
||||
|
||||
- **Grafana 仪表盘**: `http://your-server:3000`
|
||||
- **Prometheus 监控**: `http://your-server:9090`
|
||||
- **Jaeger 链路追踪**: `http://your-server:16686`
|
||||
|
||||
### **管理工具**
|
||||
|
||||
- **pgAdmin 数据库管理**: `http://your-server:5050`
|
||||
- **MinIO 对象存储**: `http://your-server:9000` (API)
|
||||
- **MinIO 控制台**: `http://your-server:9001` (管理界面)
|
||||
|
||||
### **通过 Nginx 代理访问**
|
||||
|
||||
如果启用了 Nginx,也可以通过以下路径访问:
|
||||
|
||||
- **根目录**: `http://your-server/` → 重定向到 API 文档
|
||||
- **API 服务**: `http://your-server/api/`
|
||||
- **Grafana**: `http://your-server/grafana/`
|
||||
- **Prometheus**: `http://your-server/prometheus/`
|
||||
- **Jaeger**: `http://your-server/jaeger/`
|
||||
- **MinIO API**: `http://your-server/minio/`
|
||||
- **MinIO 控制台**: `http://your-server/minio-console/`
|
||||
- **pgAdmin**: `http://your-server/pgadmin/`
|
||||
|
||||
## 🔍 **监控和故障排除**
|
||||
|
||||
### **查看日志:**
|
||||
|
||||
```bash
|
||||
# 查看应用日志
|
||||
docker-compose -f docker-compose.prod.yml logs tyapi-app
|
||||
|
||||
# 查看数据库日志
|
||||
docker-compose -f docker-compose.prod.yml logs postgres
|
||||
|
||||
# 查看所有服务日志
|
||||
docker-compose -f docker-compose.prod.yml logs
|
||||
```
|
||||
|
||||
### **健康检查:**
|
||||
|
||||
```bash
|
||||
# 检查服务状态
|
||||
curl -f http://localhost:8080/health
|
||||
|
||||
# 检查Jaeger
|
||||
curl -f http://localhost:16686
|
||||
|
||||
# 查看容器状态
|
||||
docker ps
|
||||
```
|
||||
|
||||
### **常见问题:**
|
||||
|
||||
1. **镜像拉取失败**
|
||||
|
||||
```bash
|
||||
# 检查Registry连接
|
||||
docker pull docker-registry.tianyuanapi.com/tyapi-server:latest
|
||||
```
|
||||
|
||||
2. **数据库连接失败**
|
||||
|
||||
```bash
|
||||
# 检查数据库配置
|
||||
docker-compose -f docker-compose.prod.yml logs postgres
|
||||
```
|
||||
|
||||
3. **应用启动失败**
|
||||
```bash
|
||||
# 查看应用日志
|
||||
docker-compose -f docker-compose.prod.yml logs tyapi-app
|
||||
```
|
||||
|
||||
## 🔧 **配置优化**
|
||||
|
||||
### **性能调优:**
|
||||
|
||||
1. **数据库优化** - 根据服务器配置调整 PostgreSQL 参数
|
||||
2. **Redis 优化** - 配置内存和持久化策略
|
||||
3. **应用调优** - 调整连接池大小和超时时间
|
||||
4. **Nginx 优化** - 配置缓存和压缩
|
||||
|
||||
### **扩展配置:**
|
||||
|
||||
1. **HTTPS 配置** - 添加 SSL 证书支持
|
||||
2. **域名配置** - 绑定自定义域名
|
||||
3. **备份策略** - 配置数据库自动备份
|
||||
4. **日志收集** - 集成 ELK 或其他日志系统
|
||||
|
||||
## 🔄 **版本更新**
|
||||
|
||||
### **零停机更新:**
|
||||
|
||||
```bash
|
||||
# 构建新版本
|
||||
./scripts/deploy.sh v1.1.0
|
||||
|
||||
# 或渐进式更新
|
||||
docker-compose -f docker-compose.prod.yml pull tyapi-app
|
||||
docker-compose -f docker-compose.prod.yml up -d --no-deps tyapi-app
|
||||
```
|
||||
|
||||
### **回滚操作:**
|
||||
|
||||
```bash
|
||||
# 回滚到指定版本
|
||||
docker tag docker-registry.tianyuanapi.com/tyapi-server:v1.0.0 \
|
||||
docker-registry.tianyuanapi.com/tyapi-server:latest
|
||||
|
||||
docker-compose -f docker-compose.prod.yml up -d --no-deps tyapi-app
|
||||
```
|
||||
|
||||
## 📞 **技术支持**
|
||||
|
||||
如果在部署过程中遇到问题,请:
|
||||
|
||||
1. 检查本文档的故障排除部分
|
||||
2. 查看服务日志定位问题
|
||||
3. 确认配置文件的正确性
|
||||
4. 验证网络和防火墙设置
|
||||
|
||||
---
|
||||
|
||||
**部署前请务必:**
|
||||
|
||||
- ✅ 测试配置文件
|
||||
- ✅ 备份现有数据
|
||||
- ✅ 验证 Registry 访问
|
||||
- ✅ 确认服务器资源充足
|
||||
338
docs/开始指南/链路追踪指南.md
Normal file
338
docs/开始指南/链路追踪指南.md
Normal file
@@ -0,0 +1,338 @@
|
||||
# TYAPI 项目链路追踪指南
|
||||
|
||||
## 概述
|
||||
|
||||
本项目使用 **Jaeger** 进行分布式链路追踪,通过 **OpenTelemetry** 标准实现数据收集,并在 **Grafana** 中进行可视化展示。
|
||||
|
||||
## 架构说明
|
||||
|
||||
```
|
||||
应用程序 -> OpenTelemetry -> Jaeger -> Grafana 可视化
|
||||
```
|
||||
|
||||
- **应用程序**:使用 OpenTelemetry Go SDK 生成链路追踪数据
|
||||
- **Jaeger**:收集、存储和查询链路追踪数据
|
||||
- **Grafana**:提供链路追踪数据的可视化界面
|
||||
|
||||
## 快速启动
|
||||
|
||||
### 1. 启动基础设施服务
|
||||
|
||||
```bash
|
||||
# 启动所有服务(包括Jaeger)
|
||||
docker-compose -f docker-compose.dev.yml up -d
|
||||
|
||||
# 检查服务状态
|
||||
docker-compose -f docker-compose.dev.yml ps
|
||||
```
|
||||
|
||||
### 2. 验证服务启动
|
||||
|
||||
- **Jaeger UI**: http://localhost:16686
|
||||
- **Grafana**: http://localhost:3000 (admin/Gf7nB3xM9cV6pQ2w)
|
||||
- **应用程序**: http://localhost:8080
|
||||
|
||||
### 3. 启动应用程序
|
||||
|
||||
```bash
|
||||
# 确保配置正确
|
||||
make run
|
||||
```
|
||||
|
||||
## 配置说明
|
||||
|
||||
### 应用配置(config.yaml)
|
||||
|
||||
```yaml
|
||||
monitoring:
|
||||
metrics_enabled: true
|
||||
metrics_port: "9090"
|
||||
tracing_enabled: true # 启用链路追踪
|
||||
tracing_endpoint: "http://localhost:4317" # OTLP gRPC 端点
|
||||
sample_rate: 0.1 # 采样率:10%
|
||||
```
|
||||
|
||||
### Jaeger 配置
|
||||
|
||||
- **UI 端口**: 16686
|
||||
- **OTLP gRPC**: 4317
|
||||
- **OTLP HTTP**: 4318
|
||||
- **传统 gRPC**: 14250
|
||||
- **传统 HTTP**: 14268
|
||||
|
||||
### 采样策略
|
||||
|
||||
项目使用智能采样策略(配置在 `deployments/docker/jaeger-sampling.json`):
|
||||
|
||||
- **默认采样率**: 10%
|
||||
- **健康检查接口**: 1%(减少噪音)
|
||||
- **关键业务接口**: 50%(如注册、登录)
|
||||
- **错误请求**: 100%(所有 4xx 和 5xx 错误)
|
||||
|
||||
#### 错误优先采样
|
||||
|
||||
系统实现了错误优先采样策略,确保所有出现错误的请求都被 100%采样记录,即使它们不在高采样率的关键业务接口中。这包括:
|
||||
|
||||
- 所有返回 4xx 状态码的客户端错误(如 404、400、403 等)
|
||||
- 所有返回 5xx 状态码的服务器错误(如 500、503 等)
|
||||
- 所有抛出异常的数据库操作
|
||||
- 所有失败的缓存操作
|
||||
- 所有失败的外部 API 调用
|
||||
|
||||
这种策略确保了在出现问题时,相关的链路追踪数据始终可用,便于问题排查和根因分析。
|
||||
|
||||
## 使用指南
|
||||
|
||||
### 在 Grafana 中查看链路追踪
|
||||
|
||||
1. **访问 Grafana**: http://localhost:3000
|
||||
2. **登录**: admin / Gf7nB3xM9cV6pQ2w
|
||||
3. **导航**: Dashboard → TYAPI 链路追踪监控
|
||||
4. **数据源**:
|
||||
- Jaeger 数据源已自动配置
|
||||
- URL: http://jaeger:16686
|
||||
|
||||
### 在 Jaeger UI 中查看链路追踪
|
||||
|
||||
1. **访问 Jaeger**: http://localhost:16686
|
||||
2. **选择服务**: TYAPI Server
|
||||
3. **查询追踪**:
|
||||
- 按时间范围筛选
|
||||
- 按操作类型筛选
|
||||
- 按标签筛选
|
||||
- 按错误状态筛选(使用标签`error=true`)
|
||||
|
||||
### 生成测试数据
|
||||
|
||||
```bash
|
||||
# 注册用户(会生成链路追踪数据)
|
||||
curl -X POST http://localhost:8080/api/v1/users/send-sms \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"phone": "13800138000"}'
|
||||
|
||||
# 用户注册
|
||||
curl -X POST http://localhost:8080/api/v1/users/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"phone": "13800138000",
|
||||
"password": "Test123456",
|
||||
"sms_code": "123456"
|
||||
}'
|
||||
|
||||
# 用户登录
|
||||
curl -X POST http://localhost:8080/api/v1/users/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"phone": "13800138000",
|
||||
"password": "Test123456"
|
||||
}'
|
||||
|
||||
# 生成错误请求(测试错误采样)
|
||||
curl -X GET http://localhost:8080/api/v1/not-exist-path
|
||||
```
|
||||
|
||||
## 链路追踪功能特性
|
||||
|
||||
### 自动追踪的操作
|
||||
|
||||
1. **HTTP 请求**: 所有入站 HTTP 请求
|
||||
2. **数据库查询**: GORM 操作
|
||||
3. **缓存操作**: Redis 读写
|
||||
4. **外部调用**: 短信服务等
|
||||
5. **业务逻辑**: 用户注册、登录等
|
||||
|
||||
### 追踪数据包含的信息
|
||||
|
||||
- **请求信息**: URL、HTTP 方法、状态码
|
||||
- **时间信息**: 开始时间、持续时间
|
||||
- **错误信息**: 异常堆栈和错误消息
|
||||
- **上下文信息**: TraceID、SpanID
|
||||
- **自定义标签**: 服务名、操作类型等
|
||||
|
||||
### TraceID 传播
|
||||
|
||||
应用程序会在 HTTP 响应头中返回 TraceID:
|
||||
|
||||
```
|
||||
X-Trace-ID: 4bf92f3577b34da6a3ce929d0e0e4736
|
||||
```
|
||||
|
||||
通过这个 ID,可以在日志系统和 Jaeger UI 中关联同一请求的所有信息。
|
||||
|
||||
## 错误追踪与分析
|
||||
|
||||
### 错误链路的查询
|
||||
|
||||
在 Jaeger UI 中,可以通过以下方式查询错误链路:
|
||||
|
||||
1. 在查询界面选择"Tags"标签
|
||||
2. 添加条件:`error=true`或`operation.type=error`
|
||||
3. 点击"Find Traces"按钮
|
||||
|
||||
这将显示所有被标记为错误的链路追踪数据,包括:
|
||||
|
||||
- 所有 HTTP 4xx/5xx 错误
|
||||
- 所有数据库操作错误
|
||||
- 所有缓存操作错误
|
||||
- 所有外部 API 调用错误
|
||||
|
||||
### 错误根因分析
|
||||
|
||||
链路追踪系统记录了错误发生的完整上下文,包括:
|
||||
|
||||
- 错误发生的具体操作
|
||||
- 错误的详细信息和堆栈
|
||||
- 错误发生前的所有操作序列
|
||||
- 相关的请求参数和环境信息
|
||||
|
||||
通过这些信息,可以快速定位问题根源,而不需要在多个日志文件中搜索。
|
||||
|
||||
## 性能优化建议
|
||||
|
||||
### 采样率配置
|
||||
|
||||
- **开发环境**: 10-50%(便于调试)
|
||||
- **测试环境**: 5-10%
|
||||
- **生产环境**: 1-5%(减少性能影响)
|
||||
- **错误请求**: 始终保持 100%(所有环境)
|
||||
|
||||
### 批处理配置
|
||||
|
||||
生产环境建议使用批处理导出器:
|
||||
|
||||
```yaml
|
||||
monitoring:
|
||||
tracing_enabled: true
|
||||
tracing_endpoint: "http://jaeger:4317"
|
||||
sample_rate: 0.01 # 生产环境1%采样率
|
||||
```
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **链路追踪数据未显示**
|
||||
|
||||
- 检查应用配置中 `tracing_enabled: true`
|
||||
- 确认 Jaeger 服务正常运行
|
||||
- 检查网络连接和端口
|
||||
|
||||
2. **Grafana 无法连接 Jaeger**
|
||||
|
||||
- 确认 Jaeger 数据源配置正确
|
||||
- 检查容器网络连接
|
||||
- 验证 Jaeger UI 可访问
|
||||
|
||||
3. **性能影响过大**
|
||||
|
||||
- 降低采样率
|
||||
- 检查批处理配置
|
||||
- 监控内存和 CPU 使用率
|
||||
|
||||
4. **错误请求未被 100%采样**
|
||||
- 检查 Jaeger 采样配置中是否包含`"operation": "error"`的配置
|
||||
- 确认中间件正确设置了错误标记
|
||||
- 验证错误处理逻辑是否正确调用了`SetSpanError`方法
|
||||
|
||||
### 调试命令
|
||||
|
||||
```bash
|
||||
# 检查Jaeger健康状态
|
||||
curl http://localhost:14269/health
|
||||
|
||||
# 检查容器日志
|
||||
docker logs tyapi-jaeger
|
||||
|
||||
# 检查应用追踪配置
|
||||
curl http://localhost:8080/health
|
||||
```
|
||||
|
||||
## 监控仪表板
|
||||
|
||||
### 默认仪表板
|
||||
|
||||
项目提供了预配置的 Grafana 仪表板:
|
||||
|
||||
- **TYAPI 链路追踪监控**: 展示追踪概览和关键指标
|
||||
- **HTTP 请求分析**: 请求速率和延迟分布
|
||||
- **服务依赖图**: 服务间调用关系
|
||||
- **错误分析**: 错误率和错误类型分布
|
||||
|
||||
### 自定义仪表板
|
||||
|
||||
可以根据业务需求创建自定义仪表板:
|
||||
|
||||
1. 在 Grafana 中创建新仪表板
|
||||
2. 添加 Jaeger 查询面板
|
||||
3. 配置告警规则
|
||||
4. 导出仪表板配置
|
||||
|
||||
## 生产环境部署
|
||||
|
||||
### 环境变量配置
|
||||
|
||||
```bash
|
||||
export JAEGER_ENDPOINT="http://jaeger:4317"
|
||||
export TRACING_SAMPLE_RATE="0.01"
|
||||
```
|
||||
|
||||
### Kubernetes 部署
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: app-config
|
||||
data:
|
||||
config.yaml: |
|
||||
monitoring:
|
||||
tracing_enabled: true
|
||||
tracing_endpoint: "http://jaeger-collector:4317"
|
||||
sample_rate: 0.01
|
||||
```
|
||||
|
||||
## 扩展功能
|
||||
|
||||
### 自定义追踪
|
||||
|
||||
```go
|
||||
// 在业务代码中添加自定义追踪
|
||||
ctx, span := tracer.StartSpan(ctx, "custom-operation")
|
||||
defer span.End()
|
||||
|
||||
// 添加自定义属性
|
||||
tracer.AddSpanAttributes(span,
|
||||
attribute.String("user.id", userID),
|
||||
attribute.String("operation.type", "business"),
|
||||
)
|
||||
|
||||
// 记录错误
|
||||
if err != nil {
|
||||
tracer.SetSpanError(span, err)
|
||||
}
|
||||
```
|
||||
|
||||
### 业务指标集成
|
||||
|
||||
链路追踪数据可以与业务指标结合:
|
||||
|
||||
- 用户行为分析
|
||||
- 性能瓶颈定位
|
||||
- 错误率监控
|
||||
- 服务依赖分析
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **合理设置采样率**: 平衡数据完整性和性能影响
|
||||
2. **添加有意义的标签**: 便于后续查询和分析
|
||||
3. **处理敏感信息**: 避免在追踪数据中记录密码等敏感信息
|
||||
4. **监控存储空间**: 定期清理过期的追踪数据
|
||||
5. **设置告警规则**: 对异常追踪模式设置告警
|
||||
6. **错误优先采样**: 确保所有错误请求都被记录,无论采样率如何
|
||||
7. **关联日志系统**: 在日志中包含 TraceID,便于关联查询
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [OpenTelemetry Go 文档](https://opentelemetry.io/docs/instrumentation/go/)
|
||||
- [Jaeger 官方文档](https://www.jaegertracing.io/docs/)
|
||||
- [Grafana Jaeger 数据源](https://grafana.com/docs/grafana/latest/datasources/jaeger/)
|
||||
102
docs/文档索引.md
102
docs/文档索引.md
@@ -1,102 +0,0 @@
|
||||
# 📚 TYAPI Server 文档中心
|
||||
|
||||
欢迎使用 TYAPI Server 文档中心!我们已将原本的使用指南拆分为多个专题文档,方便您按需查阅。
|
||||
|
||||
## 📋 文档导航
|
||||
|
||||
### 🚀 [快速开始指南](./快速开始指南.md)
|
||||
- 前置要求
|
||||
- 一键启动
|
||||
- 验证安装
|
||||
- 访问管理界面
|
||||
|
||||
### 🔧 [环境搭建指南](./环境搭建指南.md)
|
||||
- 开发环境配置
|
||||
- 生产环境配置
|
||||
- 服务配置说明
|
||||
- 常见配置问题
|
||||
|
||||
### 👨💻 [开发指南](./开发指南.md)
|
||||
- 项目结构理解
|
||||
- 开发流程
|
||||
- 测试编写
|
||||
- 调试技巧
|
||||
- 代码规范
|
||||
|
||||
### 🌐 [API使用指南](./API使用指南.md)
|
||||
- 认证机制
|
||||
- 用户管理 API
|
||||
- 响应格式
|
||||
- HTTP 状态码
|
||||
- API 测试
|
||||
|
||||
### 🚀 [部署指南](./部署指南.md)
|
||||
- Docker 部署
|
||||
- Kubernetes 部署
|
||||
- 云平台部署
|
||||
- 负载均衡配置
|
||||
- 监控部署
|
||||
|
||||
### 🔍 [故障排除指南](./故障排除指南.md)
|
||||
- 常见问题
|
||||
- 日志分析
|
||||
- 性能问题
|
||||
- 紧急响应流程
|
||||
|
||||
### 📋 [最佳实践指南](./最佳实践指南.md)
|
||||
- 开发最佳实践
|
||||
- 安全最佳实践
|
||||
- 性能最佳实践
|
||||
- 运维最佳实践
|
||||
- 团队协作
|
||||
|
||||
## 🎯 快速索引
|
||||
|
||||
### 新手入门
|
||||
1. [快速开始指南](./快速开始指南.md) - 5分钟快速体验
|
||||
2. [环境搭建指南](./环境搭建指南.md) - 配置开发环境
|
||||
3. [开发指南](./开发指南.md) - 开始第一个功能
|
||||
|
||||
### 日常开发
|
||||
- [API使用指南](./API使用指南.md) - API 调用参考
|
||||
- [开发指南](./开发指南.md) - 开发流程和规范
|
||||
- [故障排除指南](./故障排除指南.md) - 解决常见问题
|
||||
|
||||
### 生产部署
|
||||
- [部署指南](./部署指南.md) - 生产环境部署
|
||||
- [最佳实践指南](./最佳实践指南.md) - 运维最佳实践
|
||||
- [故障排除指南](./故障排除指南.md) - 生产问题排查
|
||||
|
||||
## 🔗 相关文档
|
||||
|
||||
### 技术文档
|
||||
- [架构文档](./ARCHITECTURE.md) - 系统架构设计
|
||||
- [API 规范](http://localhost:8080/swagger/) - 在线 API 文档
|
||||
|
||||
### 项目文档
|
||||
- [README](../README.md) - 项目介绍
|
||||
- [更新日志](../CHANGELOG.md) - 版本变更记录
|
||||
|
||||
## 📞 获取帮助
|
||||
|
||||
### 在线资源
|
||||
- **Swagger UI**: http://localhost:8080/swagger/
|
||||
- **健康检查**: http://localhost:8080/api/v1/health
|
||||
- **监控面板**: http://localhost:3000 (Grafana)
|
||||
|
||||
### 社区支持
|
||||
- **GitHub Issues**: 提交问题和建议
|
||||
- **Wiki**: 查看详细技术文档
|
||||
- **讨论区**: 参与技术讨论
|
||||
|
||||
## 🔄 文档更新
|
||||
|
||||
本文档会持续更新,如果您发现任何问题或有改进建议,请:
|
||||
|
||||
1. 提交 GitHub Issue
|
||||
2. 发起 Pull Request
|
||||
3. 联系维护团队
|
||||
|
||||
---
|
||||
|
||||
**提示**:建议将此页面加入书签,方便随时查阅相关文档。
|
||||
137
env.example
137
env.example
@@ -1,137 +0,0 @@
|
||||
# ===========================================
|
||||
# 服务配置
|
||||
# ===========================================
|
||||
SERVER_PORT=8080
|
||||
SERVER_MODE=debug
|
||||
SERVER_HOST=0.0.0.0
|
||||
SERVER_READ_TIMEOUT=30s
|
||||
SERVER_WRITE_TIMEOUT=30s
|
||||
SERVER_IDLE_TIMEOUT=120s
|
||||
|
||||
# ===========================================
|
||||
# 数据库配置 (PostgreSQL)
|
||||
# ===========================================
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_USER=postgres
|
||||
DB_PASSWORD=password
|
||||
DB_NAME=tyapi_dev
|
||||
DB_SSLMODE=disable
|
||||
DB_TIMEZONE=Asia/Shanghai
|
||||
DB_MAX_OPEN_CONNS=100
|
||||
DB_MAX_IDLE_CONNS=10
|
||||
DB_CONN_MAX_LIFETIME=300s
|
||||
|
||||
# ===========================================
|
||||
# Redis配置
|
||||
# ===========================================
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=
|
||||
REDIS_DB=0
|
||||
REDIS_POOL_SIZE=10
|
||||
REDIS_MIN_IDLE_CONNS=5
|
||||
REDIS_MAX_RETRIES=3
|
||||
REDIS_DIAL_TIMEOUT=5s
|
||||
REDIS_READ_TIMEOUT=3s
|
||||
REDIS_WRITE_TIMEOUT=3s
|
||||
|
||||
# ===========================================
|
||||
# 缓存配置
|
||||
# ===========================================
|
||||
CACHE_DEFAULT_TTL=300s
|
||||
CACHE_CLEANUP_INTERVAL=600s
|
||||
CACHE_MAX_SIZE=1000
|
||||
|
||||
# ===========================================
|
||||
# 日志配置
|
||||
# ===========================================
|
||||
LOG_LEVEL=info
|
||||
LOG_FORMAT=json
|
||||
LOG_OUTPUT=stdout
|
||||
LOG_FILE_PATH=logs/app.log
|
||||
LOG_MAX_SIZE=100
|
||||
LOG_MAX_BACKUPS=5
|
||||
LOG_MAX_AGE=30
|
||||
LOG_COMPRESS=true
|
||||
|
||||
# ===========================================
|
||||
# JWT 认证配置
|
||||
# ===========================================
|
||||
JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
|
||||
JWT_EXPIRES_IN=24h
|
||||
JWT_REFRESH_EXPIRES_IN=168h
|
||||
|
||||
# ===========================================
|
||||
# 限流配置
|
||||
# ===========================================
|
||||
RATE_LIMIT_REQUESTS=100
|
||||
RATE_LIMIT_WINDOW=60s
|
||||
RATE_LIMIT_BURST=50
|
||||
|
||||
# ===========================================
|
||||
# 监控和追踪配置
|
||||
# ===========================================
|
||||
METRICS_ENABLED=true
|
||||
METRICS_PORT=9090
|
||||
TRACING_ENABLED=true
|
||||
TRACING_ENDPOINT=http://localhost:14268/api/traces
|
||||
TRACING_SAMPLE_RATE=0.1
|
||||
|
||||
# ===========================================
|
||||
# 健康检查配置
|
||||
# ===========================================
|
||||
HEALTH_CHECK_ENABLED=true
|
||||
HEALTH_CHECK_INTERVAL=30s
|
||||
HEALTH_CHECK_TIMEOUT=10s
|
||||
|
||||
# ===========================================
|
||||
# 容错配置
|
||||
# ===========================================
|
||||
CIRCUIT_BREAKER_ENABLED=true
|
||||
CIRCUIT_BREAKER_THRESHOLD=5
|
||||
CIRCUIT_BREAKER_TIMEOUT=60s
|
||||
RETRY_MAX_ATTEMPTS=3
|
||||
RETRY_INITIAL_DELAY=100ms
|
||||
RETRY_MAX_DELAY=2s
|
||||
|
||||
# ===========================================
|
||||
# 开发模式配置
|
||||
# ===========================================
|
||||
DEBUG=true
|
||||
ENABLE_PROFILER=true
|
||||
ENABLE_CORS=true
|
||||
CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:3001
|
||||
CORS_ALLOWED_METHODS=GET,POST,PUT,PATCH,DELETE,OPTIONS
|
||||
CORS_ALLOWED_HEADERS=Origin,Content-Type,Accept,Authorization,X-Requested-With
|
||||
|
||||
# ===========================================
|
||||
# 环境标识
|
||||
# ===========================================
|
||||
ENV=development
|
||||
APP_NAME=TYAPI Server
|
||||
APP_VERSION=1.0.0
|
||||
|
||||
# ===========================================
|
||||
# 监控和追踪配置
|
||||
# ===========================================
|
||||
JAEGER_ENDPOINT=http://localhost:14268/api/traces
|
||||
|
||||
# ===========================================
|
||||
# 健康检查配置
|
||||
# ===========================================
|
||||
HEALTH_ENABLED=true
|
||||
HEALTH_INTERVAL=30s
|
||||
HEALTH_TIMEOUT=10s
|
||||
|
||||
# ===========================================
|
||||
# 配置文件路径
|
||||
# ===========================================
|
||||
CONFIG_FILE=config.yaml
|
||||
|
||||
# ===========================================
|
||||
# Go 环境变量
|
||||
# ===========================================
|
||||
GO_VERSION=go1.23.4
|
||||
GOOS=linux
|
||||
GOARCH=amd64
|
||||
43
go.mod
43
go.mod
@@ -3,13 +3,24 @@ module tyapi-server
|
||||
go 1.23.4
|
||||
|
||||
require (
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.63.107
|
||||
github.com/gin-contrib/cors v1.7.6
|
||||
github.com/gin-gonic/gin v1.10.1
|
||||
github.com/go-playground/locales v0.14.1
|
||||
github.com/go-playground/universal-translator v0.18.1
|
||||
github.com/go-playground/validator/v10 v10.26.0
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/prometheus/client_golang v1.22.0
|
||||
github.com/redis/go-redis/v9 v9.11.0
|
||||
github.com/spf13/viper v1.20.1
|
||||
github.com/swaggo/files v1.0.1
|
||||
github.com/swaggo/gin-swagger v1.6.0
|
||||
github.com/swaggo/swag v1.16.4
|
||||
go.opentelemetry.io/otel v1.37.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0
|
||||
go.opentelemetry.io/otel/sdk v1.37.0
|
||||
go.opentelemetry.io/otel/trace v1.37.0
|
||||
go.uber.org/fx v1.24.0
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/crypto v0.39.0
|
||||
@@ -19,31 +30,49 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bytedance/sonic v1.13.3 // indirect
|
||||
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
github.com/go-openapi/jsonreference v0.19.6 // indirect
|
||||
github.com/go-openapi/spec v0.20.4 // indirect
|
||||
github.com/go-openapi/swag v0.19.15 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.6.0 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.6 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.62.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/sagikazarmark/locafero v0.7.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.12.0 // indirect
|
||||
@@ -52,6 +81,10 @@ require (
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.37.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.7.0 // indirect
|
||||
go.uber.org/dig v1.19.0 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
golang.org/x/arch v0.18.0 // indirect
|
||||
@@ -59,6 +92,12 @@ require (
|
||||
golang.org/x/sync v0.15.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.26.0 // indirect
|
||||
golang.org/x/tools v0.33.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
|
||||
google.golang.org/grpc v1.73.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
185
go.sum
185
go.sum
@@ -1,3 +1,17 @@
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
|
||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.63.107 h1:qagvUyrgOnBIlVRQWOyCZGVKUIYbMBdGdJ104vBpRFU=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.63.107/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
@@ -7,16 +21,20 @@ github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
|
||||
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
|
||||
github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
||||
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||
@@ -25,10 +43,28 @@ github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBv
|
||||
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
||||
github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY=
|
||||
github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk=
|
||||
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
|
||||
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
||||
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
|
||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
|
||||
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
|
||||
github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
|
||||
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
|
||||
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
|
||||
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
@@ -41,13 +77,20 @@ github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIx
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
@@ -60,18 +103,36 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
|
||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -79,14 +140,29 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A=
|
||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
|
||||
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/redis/go-redis/v9 v9.11.0 h1:E3S08Gl/nJNn5vkxd2i78wZxWAPNZgUNTp8WIJUAiIs=
|
||||
github.com/redis/go-redis/v9 v9.11.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
|
||||
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
@@ -103,6 +179,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
@@ -111,10 +188,41 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
|
||||
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
|
||||
github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M=
|
||||
github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo=
|
||||
github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
|
||||
github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o=
|
||||
github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
|
||||
github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg=
|
||||
github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
|
||||
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 h1:EtFWSnwW9hGObjkIdmlnWSydO+Qs8OwzfzXLUPg4xOc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h1:QjUEoiGCPkvFZ/MjK6ZZfNOS6mfVEVKYE99dFhuN2LI=
|
||||
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
|
||||
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||
go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os=
|
||||
go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4=
|
||||
go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE=
|
||||
go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg=
|
||||
@@ -127,25 +235,97 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=
|
||||
golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
||||
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
|
||||
gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
|
||||
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
|
||||
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
|
||||
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
||||
@@ -153,3 +333,4 @@ gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXD
|
||||
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
|
||||
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
|
||||
@@ -50,6 +50,16 @@ func (a *Application) Run() error {
|
||||
// 打印启动信息
|
||||
a.printBanner()
|
||||
|
||||
// 检查是否需要自动迁移
|
||||
if a.config.Database.AutoMigrate {
|
||||
a.logger.Info("Auto migration is enabled, running database migrations...")
|
||||
if err := a.RunMigrations(); err != nil {
|
||||
a.logger.Error("Auto migration failed", zap.Error(err))
|
||||
return fmt.Errorf("auto migration failed: %w", err)
|
||||
}
|
||||
a.logger.Info("Auto migration completed successfully")
|
||||
}
|
||||
|
||||
// 启动容器
|
||||
a.logger.Info("Starting application container...")
|
||||
if err := a.container.Start(); err != nil {
|
||||
@@ -92,10 +102,10 @@ func (a *Application) RunMigrations() error {
|
||||
func (a *Application) printBanner() {
|
||||
banner := fmt.Sprintf(`
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ %s ║
|
||||
║ Version: %s ║
|
||||
║ Environment: %s ║
|
||||
║ Port: %s ║
|
||||
║ %s
|
||||
║ Version: %s
|
||||
║ Environment: %s
|
||||
║ Port: %s
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
`,
|
||||
a.config.App.Name,
|
||||
@@ -151,9 +161,20 @@ func (a *Application) createDatabaseConnection() (*gorm.DB, error) {
|
||||
|
||||
// autoMigrate 自动迁移
|
||||
func (a *Application) autoMigrate(db *gorm.DB) error {
|
||||
// 如果需要删除某些表,可以在这里手动删除
|
||||
// 注意:这会永久删除数据,请谨慎使用!
|
||||
/*
|
||||
// 删除不再需要的表(示例,请根据实际情况使用)
|
||||
if err := db.Migrator().DropTable(&entities.FavoriteItem{}); err != nil {
|
||||
a.logger.Warn("Failed to drop table", zap.Error(err))
|
||||
// 继续执行,不阻断迁移
|
||||
}
|
||||
*/
|
||||
|
||||
// 迁移用户相关表
|
||||
return db.AutoMigrate(
|
||||
&entities.User{},
|
||||
&entities.SMSCode{},
|
||||
// 后续可以添加其他实体
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ type Config struct {
|
||||
Cache CacheConfig `mapstructure:"cache"`
|
||||
Logger LoggerConfig `mapstructure:"logger"`
|
||||
JWT JWTConfig `mapstructure:"jwt"`
|
||||
SMS SMSConfig `mapstructure:"sms"`
|
||||
RateLimit RateLimitConfig `mapstructure:"ratelimit"`
|
||||
Monitoring MonitoringConfig `mapstructure:"monitoring"`
|
||||
Health HealthConfig `mapstructure:"health"`
|
||||
@@ -42,6 +43,7 @@ type DatabaseConfig struct {
|
||||
MaxOpenConns int `mapstructure:"max_open_conns"`
|
||||
MaxIdleConns int `mapstructure:"max_idle_conns"`
|
||||
ConnMaxLifetime time.Duration `mapstructure:"conn_max_lifetime"`
|
||||
AutoMigrate bool `mapstructure:"auto_migrate"`
|
||||
}
|
||||
|
||||
// RedisConfig Redis配置
|
||||
@@ -67,14 +69,15 @@ type CacheConfig struct {
|
||||
|
||||
// LoggerConfig 日志配置
|
||||
type LoggerConfig struct {
|
||||
Level string `mapstructure:"level"`
|
||||
Format string `mapstructure:"format"`
|
||||
Output string `mapstructure:"output"`
|
||||
FilePath string `mapstructure:"file_path"`
|
||||
MaxSize int `mapstructure:"max_size"`
|
||||
MaxBackups int `mapstructure:"max_backups"`
|
||||
MaxAge int `mapstructure:"max_age"`
|
||||
Compress bool `mapstructure:"compress"`
|
||||
Level string `mapstructure:"level"`
|
||||
Format string `mapstructure:"format"`
|
||||
Output string `mapstructure:"output"`
|
||||
FilePath string `mapstructure:"file_path"`
|
||||
MaxSize int `mapstructure:"max_size"`
|
||||
MaxBackups int `mapstructure:"max_backups"`
|
||||
MaxAge int `mapstructure:"max_age"`
|
||||
Compress bool `mapstructure:"compress"`
|
||||
UseColor bool `mapstructure:"use_color"`
|
||||
}
|
||||
|
||||
// JWTConfig JWT配置
|
||||
@@ -119,12 +122,12 @@ type ResilienceConfig struct {
|
||||
|
||||
// DevelopmentConfig 开发配置
|
||||
type DevelopmentConfig struct {
|
||||
Debug bool `mapstructure:"debug"`
|
||||
EnableProfiler bool `mapstructure:"enable_profiler"`
|
||||
EnableCors bool `mapstructure:"enable_cors"`
|
||||
CorsOrigins string `mapstructure:"cors_allowed_origins"`
|
||||
CorsMethods string `mapstructure:"cors_allowed_methods"`
|
||||
CorsHeaders string `mapstructure:"cors_allowed_headers"`
|
||||
Debug bool `mapstructure:"debug"`
|
||||
EnableProfiler bool `mapstructure:"enable_profiler"`
|
||||
EnableCors bool `mapstructure:"enable_cors"`
|
||||
CorsOrigins string `mapstructure:"cors_allowed_origins"`
|
||||
CorsMethods string `mapstructure:"cors_allowed_methods"`
|
||||
CorsHeaders string `mapstructure:"cors_allowed_headers"`
|
||||
}
|
||||
|
||||
// AppConfig 应用程序配置
|
||||
@@ -134,6 +137,26 @@ type AppConfig struct {
|
||||
Env string `mapstructure:"env"`
|
||||
}
|
||||
|
||||
// SMSConfig 短信配置
|
||||
type SMSConfig struct {
|
||||
AccessKeyID string `mapstructure:"access_key_id"`
|
||||
AccessKeySecret string `mapstructure:"access_key_secret"`
|
||||
EndpointURL string `mapstructure:"endpoint_url"`
|
||||
SignName string `mapstructure:"sign_name"`
|
||||
TemplateCode string `mapstructure:"template_code"`
|
||||
CodeLength int `mapstructure:"code_length"`
|
||||
ExpireTime time.Duration `mapstructure:"expire_time"`
|
||||
RateLimit SMSRateLimit `mapstructure:"rate_limit"`
|
||||
MockEnabled bool `mapstructure:"mock_enabled"` // 是否启用模拟短信服务
|
||||
}
|
||||
|
||||
// SMSRateLimit 短信限流配置
|
||||
type SMSRateLimit struct {
|
||||
DailyLimit int `mapstructure:"daily_limit"` // 每日发送限制
|
||||
HourlyLimit int `mapstructure:"hourly_limit"` // 每小时发送限制
|
||||
MinInterval time.Duration `mapstructure:"min_interval"` // 最小发送间隔
|
||||
}
|
||||
|
||||
// GetDSN 获取数据库DSN连接字符串
|
||||
func (d DatabaseConfig) GetDSN() string {
|
||||
return "host=" + d.Host +
|
||||
@@ -163,4 +186,4 @@ func (a AppConfig) IsDevelopment() bool {
|
||||
// IsStaging 检查是否为测试环境
|
||||
func (a AppConfig) IsStaging() bool {
|
||||
return a.Env == "staging"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package config
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -11,228 +12,173 @@ import (
|
||||
|
||||
// LoadConfig 加载应用程序配置
|
||||
func LoadConfig() (*Config, error) {
|
||||
// 设置配置文件名和路径
|
||||
viper.SetConfigName("config")
|
||||
viper.SetConfigType("yaml")
|
||||
viper.AddConfigPath(".")
|
||||
viper.AddConfigPath("./configs")
|
||||
viper.AddConfigPath("$HOME/.tyapi")
|
||||
// 1️⃣ 获取环境变量决定配置文件
|
||||
env := getEnvironment()
|
||||
fmt.Printf("🔧 当前运行环境: %s\n", env)
|
||||
|
||||
// 设置环境变量前缀
|
||||
viper.SetEnvPrefix("")
|
||||
viper.AutomaticEnv()
|
||||
// 2️⃣ 加载基础配置文件
|
||||
baseConfig := viper.New()
|
||||
baseConfig.SetConfigName("config")
|
||||
baseConfig.SetConfigType("yaml")
|
||||
baseConfig.AddConfigPath(".")
|
||||
baseConfig.AddConfigPath("./configs")
|
||||
baseConfig.AddConfigPath("$HOME/.tyapi")
|
||||
|
||||
// 配置环境变量键名映射
|
||||
setupEnvKeyMapping()
|
||||
|
||||
// 设置默认值
|
||||
setDefaults()
|
||||
|
||||
// 尝试读取配置文件(可选)
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
// 读取基础配置文件
|
||||
if err := baseConfig.ReadInConfig(); err != nil {
|
||||
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
|
||||
return nil, fmt.Errorf("读取配置文件失败: %w", err)
|
||||
return nil, fmt.Errorf("读取基础配置文件失败: %w", err)
|
||||
}
|
||||
// 配置文件不存在时使用环境变量和默认值
|
||||
return nil, fmt.Errorf("未找到 config.yaml 文件,请确保配置文件存在")
|
||||
}
|
||||
fmt.Printf("✅ 已加载配置文件: %s\n", baseConfig.ConfigFileUsed())
|
||||
|
||||
// 3️⃣ 加载环境特定配置文件
|
||||
envConfigFile := findEnvConfigFile(env)
|
||||
if envConfigFile != "" {
|
||||
// 创建一个新的viper实例来读取环境配置
|
||||
envConfig := viper.New()
|
||||
envConfig.SetConfigFile(envConfigFile)
|
||||
|
||||
if err := envConfig.ReadInConfig(); err != nil {
|
||||
fmt.Printf("⚠️ 环境配置文件加载警告: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("✅ 已加载环境配置: %s\n", envConfigFile)
|
||||
|
||||
// 将环境配置合并到基础配置中
|
||||
if err := mergeConfigs(baseConfig, envConfig.AllSettings()); err != nil {
|
||||
return nil, fmt.Errorf("合并配置失败: %w", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("⚠️ 未找到环境配置文件 env.%s.yaml\n", env)
|
||||
}
|
||||
|
||||
// 4️⃣ 设置环境变量前缀和自动读取
|
||||
baseConfig.SetEnvPrefix("")
|
||||
baseConfig.AutomaticEnv()
|
||||
baseConfig.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||
|
||||
// 5️⃣ 解析配置到结构体
|
||||
var config Config
|
||||
if err := viper.Unmarshal(&config); err != nil {
|
||||
if err := baseConfig.Unmarshal(&config); err != nil {
|
||||
return nil, fmt.Errorf("解析配置失败: %w", err)
|
||||
}
|
||||
|
||||
// 验证配置
|
||||
// 6️⃣ 验证配置
|
||||
if err := validateConfig(&config); err != nil {
|
||||
return nil, fmt.Errorf("配置验证失败: %w", err)
|
||||
}
|
||||
|
||||
// 7️⃣ 输出配置摘要
|
||||
printConfigSummary(&config, env)
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// setupEnvKeyMapping 设置环境变量到配置键的映射
|
||||
func setupEnvKeyMapping() {
|
||||
// 服务器配置
|
||||
viper.BindEnv("server.port", "SERVER_PORT")
|
||||
viper.BindEnv("server.mode", "SERVER_MODE")
|
||||
viper.BindEnv("server.host", "SERVER_HOST")
|
||||
viper.BindEnv("server.read_timeout", "SERVER_READ_TIMEOUT")
|
||||
viper.BindEnv("server.write_timeout", "SERVER_WRITE_TIMEOUT")
|
||||
viper.BindEnv("server.idle_timeout", "SERVER_IDLE_TIMEOUT")
|
||||
// mergeConfigs 递归合并配置
|
||||
func mergeConfigs(baseConfig *viper.Viper, overrideSettings map[string]interface{}) error {
|
||||
for key, val := range overrideSettings {
|
||||
// 如果值是一个嵌套的map,则递归合并
|
||||
if subMap, ok := val.(map[string]interface{}); ok {
|
||||
// 创建子键路径
|
||||
subKey := key
|
||||
|
||||
// 数据库配置
|
||||
viper.BindEnv("database.host", "DB_HOST")
|
||||
viper.BindEnv("database.port", "DB_PORT")
|
||||
viper.BindEnv("database.user", "DB_USER")
|
||||
viper.BindEnv("database.password", "DB_PASSWORD")
|
||||
viper.BindEnv("database.name", "DB_NAME")
|
||||
viper.BindEnv("database.sslmode", "DB_SSLMODE")
|
||||
viper.BindEnv("database.timezone", "DB_TIMEZONE")
|
||||
viper.BindEnv("database.max_open_conns", "DB_MAX_OPEN_CONNS")
|
||||
viper.BindEnv("database.max_idle_conns", "DB_MAX_IDLE_CONNS")
|
||||
viper.BindEnv("database.conn_max_lifetime", "DB_CONN_MAX_LIFETIME")
|
||||
|
||||
// Redis配置
|
||||
viper.BindEnv("redis.host", "REDIS_HOST")
|
||||
viper.BindEnv("redis.port", "REDIS_PORT")
|
||||
viper.BindEnv("redis.password", "REDIS_PASSWORD")
|
||||
viper.BindEnv("redis.db", "REDIS_DB")
|
||||
viper.BindEnv("redis.pool_size", "REDIS_POOL_SIZE")
|
||||
viper.BindEnv("redis.min_idle_conns", "REDIS_MIN_IDLE_CONNS")
|
||||
viper.BindEnv("redis.max_retries", "REDIS_MAX_RETRIES")
|
||||
viper.BindEnv("redis.dial_timeout", "REDIS_DIAL_TIMEOUT")
|
||||
viper.BindEnv("redis.read_timeout", "REDIS_READ_TIMEOUT")
|
||||
viper.BindEnv("redis.write_timeout", "REDIS_WRITE_TIMEOUT")
|
||||
|
||||
// 缓存配置
|
||||
viper.BindEnv("cache.default_ttl", "CACHE_DEFAULT_TTL")
|
||||
viper.BindEnv("cache.cleanup_interval", "CACHE_CLEANUP_INTERVAL")
|
||||
viper.BindEnv("cache.max_size", "CACHE_MAX_SIZE")
|
||||
|
||||
// 日志配置
|
||||
viper.BindEnv("logger.level", "LOG_LEVEL")
|
||||
viper.BindEnv("logger.format", "LOG_FORMAT")
|
||||
viper.BindEnv("logger.output", "LOG_OUTPUT")
|
||||
viper.BindEnv("logger.file_path", "LOG_FILE_PATH")
|
||||
viper.BindEnv("logger.max_size", "LOG_MAX_SIZE")
|
||||
viper.BindEnv("logger.max_backups", "LOG_MAX_BACKUPS")
|
||||
viper.BindEnv("logger.max_age", "LOG_MAX_AGE")
|
||||
viper.BindEnv("logger.compress", "LOG_COMPRESS")
|
||||
|
||||
// JWT配置
|
||||
viper.BindEnv("jwt.secret", "JWT_SECRET")
|
||||
viper.BindEnv("jwt.expires_in", "JWT_EXPIRES_IN")
|
||||
viper.BindEnv("jwt.refresh_expires_in", "JWT_REFRESH_EXPIRES_IN")
|
||||
|
||||
// 限流配置
|
||||
viper.BindEnv("ratelimit.requests", "RATE_LIMIT_REQUESTS")
|
||||
viper.BindEnv("ratelimit.window", "RATE_LIMIT_WINDOW")
|
||||
viper.BindEnv("ratelimit.burst", "RATE_LIMIT_BURST")
|
||||
|
||||
// 监控配置
|
||||
viper.BindEnv("monitoring.metrics_enabled", "METRICS_ENABLED")
|
||||
viper.BindEnv("monitoring.metrics_port", "METRICS_PORT")
|
||||
viper.BindEnv("monitoring.tracing_enabled", "TRACING_ENABLED")
|
||||
viper.BindEnv("monitoring.tracing_endpoint", "TRACING_ENDPOINT")
|
||||
viper.BindEnv("monitoring.sample_rate", "TRACING_SAMPLE_RATE")
|
||||
|
||||
// 健康检查配置
|
||||
viper.BindEnv("health.enabled", "HEALTH_CHECK_ENABLED")
|
||||
viper.BindEnv("health.interval", "HEALTH_CHECK_INTERVAL")
|
||||
viper.BindEnv("health.timeout", "HEALTH_CHECK_TIMEOUT")
|
||||
|
||||
// 容错配置
|
||||
viper.BindEnv("resilience.circuit_breaker_enabled", "CIRCUIT_BREAKER_ENABLED")
|
||||
viper.BindEnv("resilience.circuit_breaker_threshold", "CIRCUIT_BREAKER_THRESHOLD")
|
||||
viper.BindEnv("resilience.circuit_breaker_timeout", "CIRCUIT_BREAKER_TIMEOUT")
|
||||
viper.BindEnv("resilience.retry_max_attempts", "RETRY_MAX_ATTEMPTS")
|
||||
viper.BindEnv("resilience.retry_initial_delay", "RETRY_INITIAL_DELAY")
|
||||
viper.BindEnv("resilience.retry_max_delay", "RETRY_MAX_DELAY")
|
||||
|
||||
// 开发配置
|
||||
viper.BindEnv("development.debug", "DEBUG")
|
||||
viper.BindEnv("development.enable_profiler", "ENABLE_PROFILER")
|
||||
viper.BindEnv("development.enable_cors", "ENABLE_CORS")
|
||||
viper.BindEnv("development.cors_allowed_origins", "CORS_ALLOWED_ORIGINS")
|
||||
viper.BindEnv("development.cors_allowed_methods", "CORS_ALLOWED_METHODS")
|
||||
viper.BindEnv("development.cors_allowed_headers", "CORS_ALLOWED_HEADERS")
|
||||
|
||||
// 应用程序配置
|
||||
viper.BindEnv("app.name", "APP_NAME")
|
||||
viper.BindEnv("app.version", "APP_VERSION")
|
||||
viper.BindEnv("app.env", "ENV")
|
||||
// 递归合并子配置
|
||||
for subK, subV := range subMap {
|
||||
fullKey := fmt.Sprintf("%s.%s", subKey, subK)
|
||||
baseConfig.Set(fullKey, subV)
|
||||
}
|
||||
} else {
|
||||
// 直接设置值
|
||||
baseConfig.Set(key, val)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// setDefaults 设置默认配置值
|
||||
func setDefaults() {
|
||||
// 服务器默认值
|
||||
viper.SetDefault("server.port", "8080")
|
||||
viper.SetDefault("server.mode", "debug")
|
||||
viper.SetDefault("server.host", "0.0.0.0")
|
||||
viper.SetDefault("server.read_timeout", "30s")
|
||||
viper.SetDefault("server.write_timeout", "30s")
|
||||
viper.SetDefault("server.idle_timeout", "120s")
|
||||
// findEnvConfigFile 查找环境特定的配置文件
|
||||
func findEnvConfigFile(env string) string {
|
||||
// 尝试查找的配置文件路径
|
||||
possiblePaths := []string{
|
||||
fmt.Sprintf("configs/env.%s.yaml", env),
|
||||
fmt.Sprintf("configs/env.%s.yml", env),
|
||||
fmt.Sprintf("configs/env.%s", env),
|
||||
fmt.Sprintf("env.%s.yaml", env),
|
||||
fmt.Sprintf("env.%s.yml", env),
|
||||
fmt.Sprintf("env.%s", env),
|
||||
}
|
||||
|
||||
// 数据库默认值
|
||||
viper.SetDefault("database.host", "localhost")
|
||||
viper.SetDefault("database.port", "5432")
|
||||
viper.SetDefault("database.user", "postgres")
|
||||
viper.SetDefault("database.password", "password")
|
||||
viper.SetDefault("database.name", "tyapi_db")
|
||||
viper.SetDefault("database.sslmode", "disable")
|
||||
viper.SetDefault("database.timezone", "Asia/Shanghai")
|
||||
viper.SetDefault("database.max_open_conns", 100)
|
||||
viper.SetDefault("database.max_idle_conns", 10)
|
||||
viper.SetDefault("database.conn_max_lifetime", "300s")
|
||||
// 如果有自定义环境文件路径
|
||||
if customEnvFile := os.Getenv("ENV_FILE"); customEnvFile != "" {
|
||||
possiblePaths = append([]string{customEnvFile}, possiblePaths...)
|
||||
}
|
||||
|
||||
// Redis默认值
|
||||
viper.SetDefault("redis.host", "localhost")
|
||||
viper.SetDefault("redis.port", "6379")
|
||||
viper.SetDefault("redis.password", "")
|
||||
viper.SetDefault("redis.db", 0)
|
||||
viper.SetDefault("redis.pool_size", 10)
|
||||
viper.SetDefault("redis.min_idle_conns", 5)
|
||||
viper.SetDefault("redis.max_retries", 3)
|
||||
viper.SetDefault("redis.dial_timeout", "5s")
|
||||
viper.SetDefault("redis.read_timeout", "3s")
|
||||
viper.SetDefault("redis.write_timeout", "3s")
|
||||
for _, path := range possiblePaths {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
absPath, _ := filepath.Abs(path)
|
||||
return absPath
|
||||
}
|
||||
}
|
||||
|
||||
// 缓存默认值
|
||||
viper.SetDefault("cache.default_ttl", "300s")
|
||||
viper.SetDefault("cache.cleanup_interval", "600s")
|
||||
viper.SetDefault("cache.max_size", 1000)
|
||||
return ""
|
||||
}
|
||||
|
||||
// 日志默认值
|
||||
viper.SetDefault("logger.level", "info")
|
||||
viper.SetDefault("logger.format", "json")
|
||||
viper.SetDefault("logger.output", "stdout")
|
||||
viper.SetDefault("logger.file_path", "logs/app.log")
|
||||
viper.SetDefault("logger.max_size", 100)
|
||||
viper.SetDefault("logger.max_backups", 5)
|
||||
viper.SetDefault("logger.max_age", 30)
|
||||
viper.SetDefault("logger.compress", true)
|
||||
// getEnvironment 获取当前环境
|
||||
func getEnvironment() string {
|
||||
var env string
|
||||
var source string
|
||||
|
||||
// JWT默认值
|
||||
viper.SetDefault("jwt.secret", "your-super-secret-jwt-key-change-this-in-production")
|
||||
viper.SetDefault("jwt.expires_in", "24h")
|
||||
viper.SetDefault("jwt.refresh_expires_in", "168h")
|
||||
// 优先级:CONFIG_ENV > ENV > APP_ENV > 默认值
|
||||
if env = os.Getenv("CONFIG_ENV"); env != "" {
|
||||
source = "CONFIG_ENV"
|
||||
} else if env = os.Getenv("ENV"); env != "" {
|
||||
source = "ENV"
|
||||
} else if env = os.Getenv("APP_ENV"); env != "" {
|
||||
source = "APP_ENV"
|
||||
} else {
|
||||
env = "development"
|
||||
source = "默认值"
|
||||
}
|
||||
|
||||
// 限流默认值
|
||||
viper.SetDefault("ratelimit.requests", 100)
|
||||
viper.SetDefault("ratelimit.window", "60s")
|
||||
viper.SetDefault("ratelimit.burst", 10)
|
||||
fmt.Printf("🌍 环境检测: %s (来源: %s)\n", env, source)
|
||||
|
||||
// 监控默认值
|
||||
viper.SetDefault("monitoring.metrics_enabled", true)
|
||||
viper.SetDefault("monitoring.metrics_port", "9090")
|
||||
viper.SetDefault("monitoring.tracing_enabled", false)
|
||||
viper.SetDefault("monitoring.tracing_endpoint", "http://localhost:14268/api/traces")
|
||||
viper.SetDefault("monitoring.sample_rate", 0.1)
|
||||
// 验证环境值
|
||||
validEnvs := []string{"development", "production", "testing"}
|
||||
isValid := false
|
||||
for _, validEnv := range validEnvs {
|
||||
if env == validEnv {
|
||||
isValid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 健康检查默认值
|
||||
viper.SetDefault("health.enabled", true)
|
||||
viper.SetDefault("health.interval", "30s")
|
||||
viper.SetDefault("health.timeout", "5s")
|
||||
if !isValid {
|
||||
fmt.Printf("⚠️ 警告: 未识别的环境 '%s',将使用默认环境 'development'\n", env)
|
||||
return "development"
|
||||
}
|
||||
|
||||
// 容错默认值
|
||||
viper.SetDefault("resilience.circuit_breaker_enabled", true)
|
||||
viper.SetDefault("resilience.circuit_breaker_threshold", 5)
|
||||
viper.SetDefault("resilience.circuit_breaker_timeout", "60s")
|
||||
viper.SetDefault("resilience.retry_max_attempts", 3)
|
||||
viper.SetDefault("resilience.retry_initial_delay", "100ms")
|
||||
viper.SetDefault("resilience.retry_max_delay", "2s")
|
||||
return env
|
||||
}
|
||||
|
||||
// 开发默认值
|
||||
viper.SetDefault("development.debug", true)
|
||||
viper.SetDefault("development.enable_profiler", false)
|
||||
viper.SetDefault("development.enable_cors", true)
|
||||
viper.SetDefault("development.cors_allowed_origins", "*")
|
||||
viper.SetDefault("development.cors_allowed_methods", "GET,POST,PUT,DELETE,OPTIONS")
|
||||
viper.SetDefault("development.cors_allowed_headers", "*")
|
||||
|
||||
// 应用程序默认值
|
||||
viper.SetDefault("app.name", "tyapi-server")
|
||||
viper.SetDefault("app.version", "1.0.0")
|
||||
viper.SetDefault("app.env", "development")
|
||||
// printConfigSummary 打印配置摘要
|
||||
func printConfigSummary(config *Config, env string) {
|
||||
fmt.Printf("\n🔧 配置摘要:\n")
|
||||
fmt.Printf(" 🌍 环境: %s\n", env)
|
||||
fmt.Printf(" 📄 配置模板: config.yaml\n")
|
||||
fmt.Printf(" 📱 应用名称: %s\n", config.App.Name)
|
||||
fmt.Printf(" 🔖 版本: %s\n", config.App.Version)
|
||||
fmt.Printf(" 🌐 服务端口: %s\n", config.Server.Port)
|
||||
fmt.Printf(" 🗄️ 数据库: %s@%s:%s/%s\n",
|
||||
config.Database.User,
|
||||
config.Database.Host,
|
||||
config.Database.Port,
|
||||
config.Database.Name)
|
||||
fmt.Printf(" 📊 追踪状态: %v (端点: %s)\n",
|
||||
config.Monitoring.TracingEnabled,
|
||||
config.Monitoring.TracingEndpoint)
|
||||
fmt.Printf(" 📈 采样率: %.1f%%\n", config.Monitoring.SampleRate*100)
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
|
||||
// validateConfig 验证配置
|
||||
|
||||
@@ -3,11 +3,14 @@ package container
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
nethttp "net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"go.uber.org/fx"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"tyapi-server/internal/config"
|
||||
@@ -19,9 +22,15 @@ import (
|
||||
"tyapi-server/internal/shared/database"
|
||||
"tyapi-server/internal/shared/events"
|
||||
"tyapi-server/internal/shared/health"
|
||||
"tyapi-server/internal/shared/http"
|
||||
"tyapi-server/internal/shared/hooks"
|
||||
sharedhttp "tyapi-server/internal/shared/http"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
"tyapi-server/internal/shared/metrics"
|
||||
"tyapi-server/internal/shared/middleware"
|
||||
"tyapi-server/internal/shared/resilience"
|
||||
"tyapi-server/internal/shared/saga"
|
||||
"tyapi-server/internal/shared/sms"
|
||||
"tyapi-server/internal/shared/tracing"
|
||||
)
|
||||
|
||||
// Container 应用容器
|
||||
@@ -40,11 +49,24 @@ func NewContainer() *Container {
|
||||
// 基础设施模块
|
||||
fx.Provide(
|
||||
NewLogger,
|
||||
NewDatabase,
|
||||
// 使用带追踪的组件
|
||||
NewTracedDatabase,
|
||||
NewRedisClient,
|
||||
NewRedisCache,
|
||||
fx.Annotate(NewTracedRedisCache, fx.As(new(interfaces.CacheService))),
|
||||
NewEventBus,
|
||||
NewHealthChecker,
|
||||
NewSMSService,
|
||||
),
|
||||
|
||||
// 高级特性模块
|
||||
fx.Provide(
|
||||
NewTracer,
|
||||
NewPrometheusMetrics,
|
||||
NewBusinessMetrics,
|
||||
NewCircuitBreakerWrapper,
|
||||
NewRetryerWrapper,
|
||||
NewSagaManager,
|
||||
NewHookSystem,
|
||||
),
|
||||
|
||||
// HTTP基础组件
|
||||
@@ -64,14 +86,22 @@ func NewContainer() *Container {
|
||||
NewRequestLoggerMiddleware,
|
||||
NewJWTAuthMiddleware,
|
||||
NewOptionalAuthMiddleware,
|
||||
NewTracingMiddleware,
|
||||
NewMetricsMiddleware,
|
||||
NewTraceIDMiddleware,
|
||||
NewErrorTrackingMiddleware,
|
||||
NewRequestBodyLoggerMiddleware,
|
||||
),
|
||||
|
||||
// 用户域组件
|
||||
fx.Provide(
|
||||
NewUserRepository,
|
||||
NewSMSCodeRepository,
|
||||
NewSMSCodeService,
|
||||
NewUserService,
|
||||
// 使用带自动追踪的用户服务
|
||||
fx.Annotate(NewTracedUserService, fx.As(new(interfaces.UserService))),
|
||||
NewUserHandler,
|
||||
NewUserRoutes,
|
||||
),
|
||||
|
||||
// 应用生命周期
|
||||
@@ -101,7 +131,7 @@ func (c *Container) Stop() error {
|
||||
return c.App.Stop(ctx)
|
||||
}
|
||||
|
||||
// 基础设施构造函数
|
||||
// ================ 基础设施构造函数 ================
|
||||
|
||||
// NewLogger 创建日志器
|
||||
func NewLogger(cfg *config.Config) (*zap.Logger, error) {
|
||||
@@ -110,18 +140,27 @@ func NewLogger(cfg *config.Config) (*zap.Logger, error) {
|
||||
level = zap.NewAtomicLevelAt(zap.InfoLevel)
|
||||
}
|
||||
|
||||
config := zap.Config{
|
||||
Level: level,
|
||||
Development: cfg.App.IsDevelopment(),
|
||||
Encoding: cfg.Logger.Format,
|
||||
EncoderConfig: zap.NewProductionEncoderConfig(),
|
||||
OutputPaths: []string{cfg.Logger.Output},
|
||||
ErrorOutputPaths: []string{"stderr"},
|
||||
var config zap.Config
|
||||
|
||||
if cfg.App.IsDevelopment() {
|
||||
config = zap.NewDevelopmentConfig()
|
||||
config.Level = level
|
||||
config.Encoding = "console"
|
||||
if cfg.Logger.UseColor {
|
||||
config.EncoderConfig.EncodeLevel = zapcore.LowercaseColorLevelEncoder
|
||||
}
|
||||
} else {
|
||||
config = zap.NewProductionConfig()
|
||||
config.Level = level
|
||||
config.Encoding = cfg.Logger.Format
|
||||
if config.Encoding == "" {
|
||||
config.Encoding = "json"
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.Logger.Format == "" {
|
||||
config.Encoding = "json"
|
||||
}
|
||||
config.OutputPaths = []string{cfg.Logger.Output}
|
||||
config.ErrorOutputPaths = []string{"stderr"}
|
||||
|
||||
if cfg.Logger.Output == "" {
|
||||
config.OutputPaths = []string{"stdout"}
|
||||
}
|
||||
@@ -152,6 +191,25 @@ func NewDatabase(cfg *config.Config, logger *zap.Logger) (*gorm.DB, error) {
|
||||
return db.DB, nil
|
||||
}
|
||||
|
||||
// NewTracedDatabase 创建带追踪的数据库连接
|
||||
func NewTracedDatabase(cfg *config.Config, tracer *tracing.Tracer, logger *zap.Logger) (*gorm.DB, error) {
|
||||
// 先创建基础数据库连接
|
||||
db, err := NewDatabase(cfg, logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 创建并注册GORM追踪插件
|
||||
tracingPlugin := tracing.NewGormTracingPlugin(tracer, logger)
|
||||
if err := db.Use(tracingPlugin); err != nil {
|
||||
logger.Error("注册GORM追踪插件失败", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logger.Info("GORM自动追踪已启用")
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// NewRedisClient 创建Redis客户端
|
||||
func NewRedisClient(cfg *config.Config, logger *zap.Logger) (*redis.Client, error) {
|
||||
client := redis.NewClient(&redis.Options{
|
||||
@@ -165,17 +223,16 @@ func NewRedisClient(cfg *config.Config, logger *zap.Logger) (*redis.Client, erro
|
||||
WriteTimeout: cfg.Redis.WriteTimeout,
|
||||
})
|
||||
|
||||
// 测试连接
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_, err := client.Ping(ctx).Result()
|
||||
if err != nil {
|
||||
logger.Error("Failed to connect to Redis", zap.Error(err))
|
||||
logger.Error("Redis连接失败", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logger.Info("Redis connection established")
|
||||
logger.Info("Redis连接已建立")
|
||||
return client, nil
|
||||
}
|
||||
|
||||
@@ -184,9 +241,14 @@ func NewRedisCache(client *redis.Client, logger *zap.Logger, cfg *config.Config)
|
||||
return cache.NewRedisCache(client, logger, "app")
|
||||
}
|
||||
|
||||
// NewTracedRedisCache 创建带追踪的Redis缓存服务
|
||||
func NewTracedRedisCache(client *redis.Client, tracer *tracing.Tracer, logger *zap.Logger, cfg *config.Config) interfaces.CacheService {
|
||||
return tracing.NewTracedRedisCache(client, tracer, logger, "app")
|
||||
}
|
||||
|
||||
// NewEventBus 创建事件总线
|
||||
func NewEventBus(logger *zap.Logger, cfg *config.Config) interfaces.EventBus {
|
||||
return events.NewMemoryEventBus(logger, 5) // 默认5个工作协程
|
||||
return events.NewMemoryEventBus(logger, 5)
|
||||
}
|
||||
|
||||
// NewHealthChecker 创建健康检查器
|
||||
@@ -194,142 +256,293 @@ func NewHealthChecker(logger *zap.Logger) *health.HealthChecker {
|
||||
return health.NewHealthChecker(logger)
|
||||
}
|
||||
|
||||
// HTTP组件构造函数
|
||||
// NewSMSService 创建短信服务
|
||||
func NewSMSService(cfg *config.Config, logger *zap.Logger) (sms.Service, error) {
|
||||
if cfg.SMS.MockEnabled {
|
||||
logger.Info("使用模拟短信服务 (mock_enabled=true)")
|
||||
return sms.NewMockSMSService(logger), nil
|
||||
}
|
||||
|
||||
logger.Info("使用阿里云短信服务")
|
||||
return sms.NewAliSMSService(cfg.SMS, logger)
|
||||
}
|
||||
|
||||
// ================ HTTP组件构造函数 ================
|
||||
|
||||
// NewResponseBuilder 创建响应构建器
|
||||
func NewResponseBuilder() interfaces.ResponseBuilder {
|
||||
return http.NewResponseBuilder()
|
||||
return sharedhttp.NewResponseBuilder()
|
||||
}
|
||||
|
||||
// NewRequestValidator 创建请求验证器
|
||||
// NewRequestValidator 创建中文请求验证器
|
||||
func NewRequestValidator(response interfaces.ResponseBuilder) interfaces.RequestValidator {
|
||||
return http.NewRequestValidator(response)
|
||||
return sharedhttp.NewRequestValidatorZh(response)
|
||||
}
|
||||
|
||||
// NewGinRouter 创建Gin路由器
|
||||
func NewGinRouter(cfg *config.Config, logger *zap.Logger) *http.GinRouter {
|
||||
return http.NewGinRouter(cfg, logger)
|
||||
func NewGinRouter(cfg *config.Config, logger *zap.Logger) *sharedhttp.GinRouter {
|
||||
return sharedhttp.NewGinRouter(cfg, logger)
|
||||
}
|
||||
|
||||
// 中间件构造函数
|
||||
// ================ 中间件构造函数 ================
|
||||
|
||||
// NewRequestIDMiddleware 创建请求ID中间件
|
||||
func NewRequestIDMiddleware() *middleware.RequestIDMiddleware {
|
||||
return middleware.NewRequestIDMiddleware()
|
||||
}
|
||||
|
||||
// NewSecurityHeadersMiddleware 创建安全头部中间件
|
||||
func NewSecurityHeadersMiddleware() *middleware.SecurityHeadersMiddleware {
|
||||
return middleware.NewSecurityHeadersMiddleware()
|
||||
}
|
||||
|
||||
// NewResponseTimeMiddleware 创建响应时间中间件
|
||||
func NewResponseTimeMiddleware() *middleware.ResponseTimeMiddleware {
|
||||
return middleware.NewResponseTimeMiddleware()
|
||||
}
|
||||
|
||||
// NewCORSMiddleware 创建CORS中间件
|
||||
func NewCORSMiddleware(cfg *config.Config) *middleware.CORSMiddleware {
|
||||
return middleware.NewCORSMiddleware(cfg)
|
||||
}
|
||||
|
||||
// NewRateLimitMiddleware 创建限流中间件
|
||||
func NewRateLimitMiddleware(cfg *config.Config) *middleware.RateLimitMiddleware {
|
||||
return middleware.NewRateLimitMiddleware(cfg)
|
||||
func NewRateLimitMiddleware(cfg *config.Config, response interfaces.ResponseBuilder) *middleware.RateLimitMiddleware {
|
||||
return middleware.NewRateLimitMiddleware(cfg, response)
|
||||
}
|
||||
|
||||
// NewRequestLoggerMiddleware 创建请求日志中间件
|
||||
func NewRequestLoggerMiddleware(logger *zap.Logger) *middleware.RequestLoggerMiddleware {
|
||||
return middleware.NewRequestLoggerMiddleware(logger)
|
||||
func NewRequestLoggerMiddleware(logger *zap.Logger, cfg *config.Config, tracer *tracing.Tracer) *middleware.RequestLoggerMiddleware {
|
||||
return middleware.NewRequestLoggerMiddleware(logger, cfg.App.IsDevelopment(), tracer)
|
||||
}
|
||||
|
||||
// NewJWTAuthMiddleware 创建JWT认证中间件
|
||||
func NewJWTAuthMiddleware(cfg *config.Config, logger *zap.Logger) *middleware.JWTAuthMiddleware {
|
||||
return middleware.NewJWTAuthMiddleware(cfg, logger)
|
||||
}
|
||||
|
||||
// NewOptionalAuthMiddleware 创建可选认证中间件
|
||||
func NewOptionalAuthMiddleware(jwtAuth *middleware.JWTAuthMiddleware) *middleware.OptionalAuthMiddleware {
|
||||
return middleware.NewOptionalAuthMiddleware(jwtAuth)
|
||||
}
|
||||
|
||||
// 用户域构造函数
|
||||
func NewTraceIDMiddleware(tracer *tracing.Tracer) *middleware.TraceIDMiddleware {
|
||||
return middleware.NewTraceIDMiddleware(tracer)
|
||||
}
|
||||
|
||||
func NewErrorTrackingMiddleware(logger *zap.Logger, tracer *tracing.Tracer) *middleware.ErrorTrackingMiddleware {
|
||||
return middleware.NewErrorTrackingMiddleware(logger, tracer)
|
||||
}
|
||||
|
||||
func NewRequestBodyLoggerMiddleware(logger *zap.Logger, cfg *config.Config, tracer *tracing.Tracer) *middleware.RequestBodyLoggerMiddleware {
|
||||
return middleware.NewRequestBodyLoggerMiddleware(logger, cfg.App.IsDevelopment(), tracer)
|
||||
}
|
||||
|
||||
// ================ 高级特性构造函数 ================
|
||||
|
||||
// NewTracer 创建链路追踪器
|
||||
func NewTracer(cfg *config.Config, logger *zap.Logger) *tracing.Tracer {
|
||||
tracingConfig := tracing.TracerConfig{
|
||||
ServiceName: cfg.App.Name,
|
||||
ServiceVersion: cfg.App.Version,
|
||||
Environment: cfg.App.Env,
|
||||
Endpoint: cfg.Monitoring.TracingEndpoint,
|
||||
SampleRate: cfg.Monitoring.SampleRate,
|
||||
Enabled: cfg.Monitoring.TracingEnabled,
|
||||
}
|
||||
return tracing.NewTracer(tracingConfig, logger)
|
||||
}
|
||||
|
||||
// NewPrometheusMetrics 创建Prometheus指标收集器
|
||||
func NewPrometheusMetrics(logger *zap.Logger, cfg *config.Config) interfaces.MetricsCollector {
|
||||
if !cfg.Monitoring.MetricsEnabled {
|
||||
return &NoopMetricsCollector{}
|
||||
}
|
||||
return metrics.NewPrometheusMetrics(logger)
|
||||
}
|
||||
|
||||
// NewBusinessMetrics 创建业务指标收集器
|
||||
func NewBusinessMetrics(prometheusMetrics interfaces.MetricsCollector, logger *zap.Logger) *metrics.BusinessMetrics {
|
||||
return metrics.NewBusinessMetrics(prometheusMetrics, logger)
|
||||
}
|
||||
|
||||
// NewCircuitBreakerWrapper 创建熔断器包装器
|
||||
func NewCircuitBreakerWrapper(logger *zap.Logger, cfg *config.Config) *resilience.Wrapper {
|
||||
return resilience.NewWrapper(logger)
|
||||
}
|
||||
|
||||
// NewRetryerWrapper 创建重试器包装器
|
||||
func NewRetryerWrapper(logger *zap.Logger) *resilience.RetryerWrapper {
|
||||
return resilience.NewRetryerWrapper(logger)
|
||||
}
|
||||
|
||||
// NewSagaManager 创建Saga管理器
|
||||
func NewSagaManager(cfg *config.Config, logger *zap.Logger) *saga.SagaManager {
|
||||
sagaConfig := saga.SagaConfig{
|
||||
DefaultTimeout: 30 * time.Second,
|
||||
DefaultMaxRetries: 3,
|
||||
Parallel: false,
|
||||
}
|
||||
return saga.NewSagaManager(sagaConfig, logger)
|
||||
}
|
||||
|
||||
// NewHookSystem 创建钩子系统
|
||||
func NewHookSystem(logger *zap.Logger) *hooks.HookSystem {
|
||||
hookConfig := hooks.HookConfig{
|
||||
DefaultTimeout: 30 * time.Second,
|
||||
TrackDuration: true,
|
||||
ErrorStrategy: hooks.ContinueOnError,
|
||||
}
|
||||
return hooks.NewHookSystem(hookConfig, logger)
|
||||
}
|
||||
|
||||
// NewTracingMiddleware 创建追踪中间件
|
||||
func NewTracingMiddleware(tracer *tracing.Tracer) *TracingMiddleware {
|
||||
return &TracingMiddleware{tracer: tracer}
|
||||
}
|
||||
|
||||
// NewMetricsMiddleware 创建指标中间件
|
||||
func NewMetricsMiddleware(metricsCollector interfaces.MetricsCollector) *MetricsMiddleware {
|
||||
return &MetricsMiddleware{metrics: metricsCollector}
|
||||
}
|
||||
|
||||
// ================ 用户域构造函数 ================
|
||||
|
||||
// NewUserRepository 创建用户仓储
|
||||
func NewUserRepository(db *gorm.DB, cache interfaces.CacheService, logger *zap.Logger) *repositories.UserRepository {
|
||||
return repositories.NewUserRepository(db, cache, logger)
|
||||
}
|
||||
|
||||
// NewUserService 创建用户服务
|
||||
func NewSMSCodeRepository(db *gorm.DB, cache interfaces.CacheService, logger *zap.Logger) *repositories.SMSCodeRepository {
|
||||
return repositories.NewSMSCodeRepository(db, cache, logger)
|
||||
}
|
||||
|
||||
func NewSMSCodeService(
|
||||
repo *repositories.SMSCodeRepository,
|
||||
smsClient sms.Service,
|
||||
cache interfaces.CacheService,
|
||||
cfg *config.Config,
|
||||
logger *zap.Logger,
|
||||
) *services.SMSCodeService {
|
||||
return services.NewSMSCodeService(repo, smsClient, cache, cfg.SMS, logger)
|
||||
}
|
||||
|
||||
func NewUserService(
|
||||
repo *repositories.UserRepository,
|
||||
smsCodeService *services.SMSCodeService,
|
||||
eventBus interfaces.EventBus,
|
||||
logger *zap.Logger,
|
||||
) *services.UserService {
|
||||
return services.NewUserService(repo, eventBus, logger)
|
||||
return services.NewUserService(repo, smsCodeService, eventBus, logger)
|
||||
}
|
||||
|
||||
// NewTracedUserService 创建带自动追踪的用户服务
|
||||
func NewTracedUserService(
|
||||
baseService *services.UserService,
|
||||
tracer *tracing.Tracer,
|
||||
logger *zap.Logger,
|
||||
) interfaces.UserService {
|
||||
serviceWrapper := tracing.NewServiceWrapper(tracer, logger)
|
||||
return tracing.NewTracedUserService(baseService, serviceWrapper)
|
||||
}
|
||||
|
||||
// NewUserHandler 创建用户处理器
|
||||
func NewUserHandler(
|
||||
userService *services.UserService,
|
||||
userService interfaces.UserService,
|
||||
smsCodeService *services.SMSCodeService,
|
||||
response interfaces.ResponseBuilder,
|
||||
validator interfaces.RequestValidator,
|
||||
logger *zap.Logger,
|
||||
jwtAuth *middleware.JWTAuthMiddleware,
|
||||
) *handlers.UserHandler {
|
||||
return handlers.NewUserHandler(userService, response, validator, logger, jwtAuth)
|
||||
return handlers.NewUserHandler(userService, smsCodeService, response, validator, logger, jwtAuth)
|
||||
}
|
||||
|
||||
// NewUserRoutes 创建用户路由
|
||||
func NewUserRoutes(
|
||||
handler *handlers.UserHandler,
|
||||
jwtAuth *middleware.JWTAuthMiddleware,
|
||||
optionalAuth *middleware.OptionalAuthMiddleware,
|
||||
) *routes.UserRoutes {
|
||||
return routes.NewUserRoutes(handler, jwtAuth, optionalAuth)
|
||||
// ================ 中间件定义 ================
|
||||
|
||||
// TracingMiddleware 追踪中间件
|
||||
type TracingMiddleware struct {
|
||||
tracer *tracing.Tracer
|
||||
}
|
||||
|
||||
// 注册函数
|
||||
func (tm *TracingMiddleware) GetName() string { return "tracing" }
|
||||
func (tm *TracingMiddleware) GetPriority() int { return 1 }
|
||||
func (tm *TracingMiddleware) IsGlobal() bool { return true }
|
||||
func (tm *TracingMiddleware) Handle() gin.HandlerFunc {
|
||||
return tm.tracer.TraceMiddleware()
|
||||
}
|
||||
|
||||
// MetricsMiddleware 指标中间件
|
||||
type MetricsMiddleware struct {
|
||||
metrics interfaces.MetricsCollector
|
||||
}
|
||||
|
||||
func (mm *MetricsMiddleware) GetName() string { return "metrics" }
|
||||
func (mm *MetricsMiddleware) GetPriority() int { return 2 }
|
||||
func (mm *MetricsMiddleware) IsGlobal() bool { return true }
|
||||
func (mm *MetricsMiddleware) Handle() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
start := time.Now()
|
||||
c.Next()
|
||||
duration := time.Since(start).Seconds()
|
||||
mm.metrics.RecordHTTPRequest(c.Request.Method, c.FullPath(), c.Writer.Status(), duration)
|
||||
mm.metrics.RecordHTTPDuration(c.Request.Method, c.FullPath(), duration)
|
||||
}
|
||||
}
|
||||
|
||||
// NoopMetricsCollector 空的指标收集器实现
|
||||
type NoopMetricsCollector struct{}
|
||||
|
||||
func (n *NoopMetricsCollector) RecordHTTPRequest(method, path string, status int, duration float64) {}
|
||||
func (n *NoopMetricsCollector) RecordHTTPDuration(method, path string, duration float64) {}
|
||||
func (n *NoopMetricsCollector) IncrementCounter(name string, labels map[string]string) {}
|
||||
func (n *NoopMetricsCollector) RecordGauge(name string, value float64, labels map[string]string) {}
|
||||
func (n *NoopMetricsCollector) RecordHistogram(name string, value float64, labels map[string]string) {
|
||||
}
|
||||
func (n *NoopMetricsCollector) RegisterCounter(name, help string, labels []string) error { return nil }
|
||||
func (n *NoopMetricsCollector) RegisterGauge(name, help string, labels []string) error { return nil }
|
||||
func (n *NoopMetricsCollector) RegisterHistogram(name, help string, labels []string, buckets []float64) error {
|
||||
return nil
|
||||
}
|
||||
func (n *NoopMetricsCollector) GetHandler() nethttp.Handler { return nil }
|
||||
|
||||
// ================ 注册函数 ================
|
||||
|
||||
// RegisterMiddlewares 注册中间件
|
||||
func RegisterMiddlewares(
|
||||
router *http.GinRouter,
|
||||
router *sharedhttp.GinRouter,
|
||||
requestID *middleware.RequestIDMiddleware,
|
||||
security *middleware.SecurityHeadersMiddleware,
|
||||
responseTime *middleware.ResponseTimeMiddleware,
|
||||
cors *middleware.CORSMiddleware,
|
||||
rateLimit *middleware.RateLimitMiddleware,
|
||||
requestLogger *middleware.RequestLoggerMiddleware,
|
||||
tracingMiddleware *TracingMiddleware,
|
||||
metricsMiddleware *MetricsMiddleware,
|
||||
traceIDMiddleware *middleware.TraceIDMiddleware,
|
||||
errorTrackingMiddleware *middleware.ErrorTrackingMiddleware,
|
||||
requestBodyLogger *middleware.RequestBodyLoggerMiddleware,
|
||||
) {
|
||||
// 注册全局中间件
|
||||
router.RegisterMiddleware(requestID)
|
||||
router.RegisterMiddleware(security)
|
||||
router.RegisterMiddleware(responseTime)
|
||||
router.RegisterMiddleware(cors)
|
||||
router.RegisterMiddleware(rateLimit)
|
||||
router.RegisterMiddleware(requestLogger)
|
||||
router.RegisterMiddleware(tracingMiddleware)
|
||||
router.RegisterMiddleware(metricsMiddleware)
|
||||
router.RegisterMiddleware(traceIDMiddleware)
|
||||
router.RegisterMiddleware(errorTrackingMiddleware)
|
||||
router.RegisterMiddleware(requestBodyLogger)
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册路由
|
||||
func RegisterRoutes(
|
||||
router *http.GinRouter,
|
||||
userRoutes *routes.UserRoutes,
|
||||
router *sharedhttp.GinRouter,
|
||||
userHandler *handlers.UserHandler,
|
||||
jwtAuth *middleware.JWTAuthMiddleware,
|
||||
metricsCollector interfaces.MetricsCollector,
|
||||
) {
|
||||
// 设置默认路由
|
||||
router.SetupDefaultRoutes()
|
||||
|
||||
// 注册用户路由
|
||||
userRoutes.RegisterRoutes(router.GetEngine())
|
||||
userRoutes.RegisterPublicRoutes(router.GetEngine())
|
||||
userRoutes.RegisterAdminRoutes(router.GetEngine())
|
||||
userRoutes.RegisterHealthRoutes(router.GetEngine())
|
||||
if handler := metricsCollector.GetHandler(); handler != nil {
|
||||
router.GetEngine().GET("/metrics", gin.WrapH(handler))
|
||||
}
|
||||
|
||||
// 打印路由信息
|
||||
routes.UserRoutes(router.GetEngine(), userHandler, jwtAuth)
|
||||
router.PrintRoutes()
|
||||
}
|
||||
|
||||
// 生命周期钩子
|
||||
|
||||
// RegisterLifecycleHooks 注册生命周期钩子
|
||||
func RegisterLifecycleHooks(
|
||||
lc fx.Lifecycle,
|
||||
@@ -339,29 +552,60 @@ func RegisterLifecycleHooks(
|
||||
cache interfaces.CacheService,
|
||||
eventBus interfaces.EventBus,
|
||||
healthChecker *health.HealthChecker,
|
||||
router *http.GinRouter,
|
||||
router *sharedhttp.GinRouter,
|
||||
userService *services.UserService,
|
||||
tracer *tracing.Tracer,
|
||||
prometheusMetrics interfaces.MetricsCollector,
|
||||
businessMetrics *metrics.BusinessMetrics,
|
||||
circuitBreakerWrapper *resilience.Wrapper,
|
||||
retryerWrapper *resilience.RetryerWrapper,
|
||||
sagaManager *saga.SagaManager,
|
||||
hookSystem *hooks.HookSystem,
|
||||
) {
|
||||
lc.Append(fx.Hook{
|
||||
OnStart: func(ctx context.Context) error {
|
||||
logger.Info("Starting application services...")
|
||||
logger.Info("正在启动应用服务...")
|
||||
|
||||
// 初始化高级特性
|
||||
if err := tracer.Initialize(ctx); err != nil {
|
||||
logger.Error("初始化追踪器失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
if err := hookSystem.Initialize(ctx); err != nil {
|
||||
logger.Error("初始化钩子系统失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
if err := sagaManager.Initialize(ctx); err != nil {
|
||||
logger.Error("初始化事务管理器失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 注册服务到健康检查器
|
||||
healthChecker.RegisterService(userService)
|
||||
healthChecker.RegisterService(sagaManager)
|
||||
|
||||
// 初始化缓存服务
|
||||
// 启动基础服务
|
||||
if err := cache.Initialize(ctx); err != nil {
|
||||
logger.Error("Failed to initialize cache", zap.Error(err))
|
||||
logger.Error("初始化缓存失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 启动事件总线
|
||||
if err := eventBus.Start(ctx); err != nil {
|
||||
logger.Error("Failed to start event bus", zap.Error(err))
|
||||
logger.Error("启动事件总线失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 启动健康检查(如果启用)
|
||||
if err := hookSystem.Start(ctx); err != nil {
|
||||
logger.Error("启动钩子系统失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 注册应用钩子
|
||||
registerApplicationHooks(hookSystem, businessMetrics, logger)
|
||||
|
||||
// 启动健康检查
|
||||
if cfg.Health.Enabled {
|
||||
go healthChecker.StartPeriodicCheck(ctx, cfg.Health.Interval)
|
||||
}
|
||||
@@ -370,44 +614,65 @@ func RegisterLifecycleHooks(
|
||||
go func() {
|
||||
addr := fmt.Sprintf("%s:%s", cfg.Server.Host, cfg.Server.Port)
|
||||
if err := router.Start(addr); err != nil {
|
||||
logger.Error("Failed to start HTTP server", zap.Error(err))
|
||||
logger.Error("启动HTTP服务器失败", zap.Error(err))
|
||||
}
|
||||
}()
|
||||
|
||||
logger.Info("All services started successfully")
|
||||
logger.Info("所有服务已成功启动")
|
||||
return nil
|
||||
},
|
||||
OnStop: func(ctx context.Context) error {
|
||||
logger.Info("Stopping application services...")
|
||||
logger.Info("正在停止应用服务...")
|
||||
|
||||
// 停止HTTP服务器
|
||||
if err := router.Stop(ctx); err != nil {
|
||||
logger.Error("Failed to stop HTTP server", zap.Error(err))
|
||||
}
|
||||
// 按顺序关闭服务
|
||||
router.Stop(ctx)
|
||||
hookSystem.Shutdown(ctx)
|
||||
sagaManager.Shutdown(ctx)
|
||||
eventBus.Stop(ctx)
|
||||
tracer.Shutdown(ctx)
|
||||
cache.Shutdown(ctx)
|
||||
|
||||
// 停止事件总线
|
||||
if err := eventBus.Stop(ctx); err != nil {
|
||||
logger.Error("Failed to stop event bus", zap.Error(err))
|
||||
}
|
||||
|
||||
// 关闭缓存服务
|
||||
if err := cache.Shutdown(ctx); err != nil {
|
||||
logger.Error("Failed to shutdown cache service", zap.Error(err))
|
||||
}
|
||||
|
||||
// 关闭数据库连接
|
||||
if sqlDB, err := db.DB(); err == nil {
|
||||
if err := sqlDB.Close(); err != nil {
|
||||
logger.Error("Failed to close database", zap.Error(err))
|
||||
}
|
||||
sqlDB.Close()
|
||||
}
|
||||
|
||||
logger.Info("All services stopped")
|
||||
logger.Info("所有服务已停止")
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// registerApplicationHooks 注册应用钩子
|
||||
func registerApplicationHooks(hookSystem *hooks.HookSystem, businessMetrics *metrics.BusinessMetrics, logger *zap.Logger) {
|
||||
userCreatedHook := &hooks.Hook{
|
||||
Name: "metrics.user_created",
|
||||
Priority: 1,
|
||||
Async: false,
|
||||
Timeout: 5 * time.Second,
|
||||
Func: func(ctx context.Context, data interface{}) error {
|
||||
businessMetrics.RecordUserCreated("register")
|
||||
logger.Debug("记录用户创建指标")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
hookSystem.Register("user.created", userCreatedHook)
|
||||
|
||||
userLoginHook := &hooks.Hook{
|
||||
Name: "metrics.user_login",
|
||||
Priority: 1,
|
||||
Async: false,
|
||||
Timeout: 5 * time.Second,
|
||||
Func: func(ctx context.Context, data interface{}) error {
|
||||
businessMetrics.RecordUserLogin("web", "success")
|
||||
logger.Debug("记录用户登录指标")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
hookSystem.Register("user.logged_in", userLoginHook)
|
||||
|
||||
logger.Info("应用钩子已成功注册")
|
||||
}
|
||||
|
||||
// ServiceRegistrar 服务注册器接口
|
||||
type ServiceRegistrar interface {
|
||||
RegisterServices() fx.Option
|
||||
|
||||
72
internal/domains/user/dto/sms_dto.go
Normal file
72
internal/domains/user/dto/sms_dto.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"tyapi-server/internal/domains/user/entities"
|
||||
)
|
||||
|
||||
// SendCodeRequest 发送验证码请求
|
||||
type SendCodeRequest struct {
|
||||
Phone string `json:"phone" binding:"required,len=11" example:"13800138000"`
|
||||
Scene entities.SMSScene `json:"scene" binding:"required,oneof=register login change_password reset_password bind unbind" example:"register"`
|
||||
}
|
||||
|
||||
// SendCodeResponse 发送验证码响应
|
||||
type SendCodeResponse struct {
|
||||
Message string `json:"message" example:"验证码发送成功"`
|
||||
ExpiresAt time.Time `json:"expires_at" example:"2024-01-01T00:05:00Z"`
|
||||
}
|
||||
|
||||
// VerifyCodeRequest 验证验证码请求
|
||||
type VerifyCodeRequest struct {
|
||||
Phone string `json:"phone" binding:"required,len=11" example:"13800138000"`
|
||||
Code string `json:"code" binding:"required,len=6" example:"123456"`
|
||||
Scene entities.SMSScene `json:"scene" binding:"required,oneof=register login change_password reset_password bind unbind" example:"register"`
|
||||
}
|
||||
|
||||
// SMSCodeResponse SMS验证码记录响应
|
||||
type SMSCodeResponse struct {
|
||||
ID string `json:"id" example:"123e4567-e89b-12d3-a456-426614174000"`
|
||||
Phone string `json:"phone" example:"13800138000"`
|
||||
Scene entities.SMSScene `json:"scene" example:"register"`
|
||||
Used bool `json:"used" example:"false"`
|
||||
ExpiresAt time.Time `json:"expires_at" example:"2024-01-01T00:05:00Z"`
|
||||
CreatedAt time.Time `json:"created_at" example:"2024-01-01T00:00:00Z"`
|
||||
}
|
||||
|
||||
// SMSCodeListRequest SMS验证码列表请求
|
||||
type SMSCodeListRequest struct {
|
||||
Phone string `form:"phone" binding:"omitempty,len=11" example:"13800138000"`
|
||||
Scene entities.SMSScene `form:"scene" binding:"omitempty,oneof=register login change_password reset_password bind unbind" example:"register"`
|
||||
Page int `form:"page" binding:"omitempty,min=1" example:"1"`
|
||||
PageSize int `form:"page_size" binding:"omitempty,min=1,max=100" example:"20"`
|
||||
}
|
||||
|
||||
// 转换方法
|
||||
func FromSMSCodeEntity(smsCode *entities.SMSCode) *SMSCodeResponse {
|
||||
if smsCode == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &SMSCodeResponse{
|
||||
ID: smsCode.ID,
|
||||
Phone: smsCode.Phone,
|
||||
Scene: smsCode.Scene,
|
||||
Used: smsCode.Used,
|
||||
ExpiresAt: smsCode.ExpiresAt,
|
||||
CreatedAt: smsCode.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func FromSMSCodeEntities(smsCodes []*entities.SMSCode) []*SMSCodeResponse {
|
||||
if smsCodes == nil {
|
||||
return []*SMSCodeResponse{}
|
||||
}
|
||||
|
||||
responses := make([]*SMSCodeResponse, len(smsCodes))
|
||||
for i, smsCode := range smsCodes {
|
||||
responses[i] = FromSMSCodeEntity(smsCode)
|
||||
}
|
||||
return responses
|
||||
}
|
||||
@@ -6,88 +6,40 @@ import (
|
||||
"tyapi-server/internal/domains/user/entities"
|
||||
)
|
||||
|
||||
// CreateUserRequest 创建用户请求
|
||||
type CreateUserRequest struct {
|
||||
Username string `json:"username" binding:"required,min=3,max=50" example:"john_doe"`
|
||||
Email string `json:"email" binding:"required,email" example:"john@example.com"`
|
||||
Password string `json:"password" binding:"required,min=6,max=128" example:"password123"`
|
||||
FirstName string `json:"first_name" binding:"max=50" example:"John"`
|
||||
LastName string `json:"last_name" binding:"max=50" example:"Doe"`
|
||||
Phone string `json:"phone" binding:"omitempty,max=20" example:"+86-13800138000"`
|
||||
// RegisterRequest 用户注册请求
|
||||
type RegisterRequest struct {
|
||||
Phone string `json:"phone" binding:"required,len=11" example:"13800138000"`
|
||||
Password string `json:"password" binding:"required,min=6,max=128" example:"password123"`
|
||||
ConfirmPassword string `json:"confirm_password" binding:"required,eqfield=Password" example:"password123"`
|
||||
Code string `json:"code" binding:"required,len=6" example:"123456"`
|
||||
}
|
||||
|
||||
// UpdateUserRequest 更新用户请求
|
||||
type UpdateUserRequest struct {
|
||||
FirstName *string `json:"first_name,omitempty" binding:"omitempty,max=50" example:"John"`
|
||||
LastName *string `json:"last_name,omitempty" binding:"omitempty,max=50" example:"Doe"`
|
||||
Phone *string `json:"phone,omitempty" binding:"omitempty,max=20" example:"+86-13800138000"`
|
||||
Avatar *string `json:"avatar,omitempty" binding:"omitempty,url" example:"https://example.com/avatar.jpg"`
|
||||
// LoginWithPasswordRequest 密码登录请求
|
||||
type LoginWithPasswordRequest struct {
|
||||
Phone string `json:"phone" binding:"required,len=11" example:"13800138000"`
|
||||
Password string `json:"password" binding:"required" example:"password123"`
|
||||
}
|
||||
|
||||
// LoginWithSMSRequest 短信验证码登录请求
|
||||
type LoginWithSMSRequest struct {
|
||||
Phone string `json:"phone" binding:"required,len=11" example:"13800138000"`
|
||||
Code string `json:"code" binding:"required,len=6" example:"123456"`
|
||||
}
|
||||
|
||||
// ChangePasswordRequest 修改密码请求
|
||||
type ChangePasswordRequest struct {
|
||||
OldPassword string `json:"old_password" binding:"required" example:"oldpassword123"`
|
||||
NewPassword string `json:"new_password" binding:"required,min=6,max=128" example:"newpassword123"`
|
||||
OldPassword string `json:"old_password" binding:"required" example:"oldpassword123"`
|
||||
NewPassword string `json:"new_password" binding:"required,min=6,max=128" example:"newpassword123"`
|
||||
ConfirmNewPassword string `json:"confirm_new_password" binding:"required,eqfield=NewPassword" example:"newpassword123"`
|
||||
Code string `json:"code" binding:"required,len=6" example:"123456"`
|
||||
}
|
||||
|
||||
// UserResponse 用户响应
|
||||
type UserResponse struct {
|
||||
ID string `json:"id" example:"123e4567-e89b-12d3-a456-426614174000"`
|
||||
Username string `json:"username" example:"john_doe"`
|
||||
Email string `json:"email" example:"john@example.com"`
|
||||
FirstName string `json:"first_name" example:"John"`
|
||||
LastName string `json:"last_name" example:"Doe"`
|
||||
Phone string `json:"phone" example:"+86-13800138000"`
|
||||
Avatar string `json:"avatar" example:"https://example.com/avatar.jpg"`
|
||||
Status entities.UserStatus `json:"status" example:"active"`
|
||||
LastLoginAt *time.Time `json:"last_login_at,omitempty" example:"2024-01-01T00:00:00Z"`
|
||||
CreatedAt time.Time `json:"created_at" example:"2024-01-01T00:00:00Z"`
|
||||
UpdatedAt time.Time `json:"updated_at" example:"2024-01-01T00:00:00Z"`
|
||||
Profile *UserProfileResponse `json:"profile,omitempty"`
|
||||
}
|
||||
|
||||
// UserProfileResponse 用户档案响应
|
||||
type UserProfileResponse struct {
|
||||
Bio string `json:"bio,omitempty" example:"Software Developer"`
|
||||
Location string `json:"location,omitempty" example:"Beijing, China"`
|
||||
Website string `json:"website,omitempty" example:"https://johndoe.com"`
|
||||
Birthday *time.Time `json:"birthday,omitempty" example:"1990-01-01T00:00:00Z"`
|
||||
Gender string `json:"gender,omitempty" example:"male"`
|
||||
Timezone string `json:"timezone,omitempty" example:"Asia/Shanghai"`
|
||||
Language string `json:"language,omitempty" example:"zh-CN"`
|
||||
}
|
||||
|
||||
// UserListRequest 用户列表请求
|
||||
type UserListRequest struct {
|
||||
Page int `form:"page" binding:"omitempty,min=1" example:"1"`
|
||||
PageSize int `form:"page_size" binding:"omitempty,min=1,max=100" example:"20"`
|
||||
Sort string `form:"sort" binding:"omitempty,oneof=created_at updated_at username email" example:"created_at"`
|
||||
Order string `form:"order" binding:"omitempty,oneof=asc desc" example:"desc"`
|
||||
Status entities.UserStatus `form:"status" binding:"omitempty,oneof=active inactive suspended pending" example:"active"`
|
||||
Search string `form:"search" binding:"omitempty,max=100" example:"john"`
|
||||
Filters map[string]interface{} `form:"-"`
|
||||
}
|
||||
|
||||
// UserListResponse 用户列表响应
|
||||
type UserListResponse struct {
|
||||
Users []*UserResponse `json:"users"`
|
||||
Pagination PaginationMeta `json:"pagination"`
|
||||
}
|
||||
|
||||
// PaginationMeta 分页元数据
|
||||
type PaginationMeta struct {
|
||||
Page int `json:"page" example:"1"`
|
||||
PageSize int `json:"page_size" example:"20"`
|
||||
Total int64 `json:"total" example:"100"`
|
||||
TotalPages int `json:"total_pages" example:"5"`
|
||||
HasNext bool `json:"has_next" example:"true"`
|
||||
HasPrev bool `json:"has_prev" example:"false"`
|
||||
}
|
||||
|
||||
// LoginRequest 登录请求
|
||||
type LoginRequest struct {
|
||||
Login string `json:"login" binding:"required" example:"john_doe"`
|
||||
Password string `json:"password" binding:"required" example:"password123"`
|
||||
ID string `json:"id" example:"123e4567-e89b-12d3-a456-426614174000"`
|
||||
Phone string `json:"phone" example:"13800138000"`
|
||||
CreatedAt time.Time `json:"created_at" example:"2024-01-01T00:00:00Z"`
|
||||
UpdatedAt time.Time `json:"updated_at" example:"2024-01-01T00:00:00Z"`
|
||||
}
|
||||
|
||||
// LoginResponse 登录响应
|
||||
@@ -96,47 +48,27 @@ type LoginResponse struct {
|
||||
AccessToken string `json:"access_token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."`
|
||||
TokenType string `json:"token_type" example:"Bearer"`
|
||||
ExpiresIn int64 `json:"expires_in" example:"86400"`
|
||||
}
|
||||
|
||||
// UpdateProfileRequest 更新用户档案请求
|
||||
type UpdateProfileRequest struct {
|
||||
Bio *string `json:"bio,omitempty" binding:"omitempty,max=500" example:"Software Developer"`
|
||||
Location *string `json:"location,omitempty" binding:"omitempty,max=100" example:"Beijing, China"`
|
||||
Website *string `json:"website,omitempty" binding:"omitempty,url" example:"https://johndoe.com"`
|
||||
Birthday *time.Time `json:"birthday,omitempty" example:"1990-01-01T00:00:00Z"`
|
||||
Gender *string `json:"gender,omitempty" binding:"omitempty,oneof=male female other" example:"male"`
|
||||
Timezone *string `json:"timezone,omitempty" binding:"omitempty,max=50" example:"Asia/Shanghai"`
|
||||
Language *string `json:"language,omitempty" binding:"omitempty,max=10" example:"zh-CN"`
|
||||
}
|
||||
|
||||
// UserStatsResponse 用户统计响应
|
||||
type UserStatsResponse struct {
|
||||
TotalUsers int64 `json:"total_users" example:"1000"`
|
||||
ActiveUsers int64 `json:"active_users" example:"950"`
|
||||
InactiveUsers int64 `json:"inactive_users" example:"30"`
|
||||
SuspendedUsers int64 `json:"suspended_users" example:"20"`
|
||||
NewUsersToday int64 `json:"new_users_today" example:"5"`
|
||||
NewUsersWeek int64 `json:"new_users_week" example:"25"`
|
||||
NewUsersMonth int64 `json:"new_users_month" example:"120"`
|
||||
}
|
||||
|
||||
// UserSearchRequest 用户搜索请求
|
||||
type UserSearchRequest struct {
|
||||
Query string `form:"q" binding:"required,min=1,max=100" example:"john"`
|
||||
Page int `form:"page" binding:"omitempty,min=1" example:"1"`
|
||||
PageSize int `form:"page_size" binding:"omitempty,min=1,max=50" example:"10"`
|
||||
LoginMethod string `json:"login_method" example:"password"` // password 或 sms
|
||||
}
|
||||
|
||||
// 转换方法
|
||||
func (r *CreateUserRequest) ToEntity() *entities.User {
|
||||
func (r *RegisterRequest) ToEntity() *entities.User {
|
||||
return &entities.User{
|
||||
Username: r.Username,
|
||||
Email: r.Email,
|
||||
Password: r.Password,
|
||||
FirstName: r.FirstName,
|
||||
LastName: r.LastName,
|
||||
Phone: r.Phone,
|
||||
Status: entities.UserStatusActive,
|
||||
Phone: r.Phone,
|
||||
Password: r.Password,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *LoginWithPasswordRequest) ToEntity() *entities.User {
|
||||
return &entities.User{
|
||||
Phone: r.Phone,
|
||||
Password: r.Password,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *LoginWithSMSRequest) ToEntity() *entities.User {
|
||||
return &entities.User{
|
||||
Phone: r.Phone,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,28 +78,9 @@ func FromEntity(user *entities.User) *UserResponse {
|
||||
}
|
||||
|
||||
return &UserResponse{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
FirstName: user.FirstName,
|
||||
LastName: user.LastName,
|
||||
Phone: user.Phone,
|
||||
Avatar: user.Avatar,
|
||||
Status: user.Status,
|
||||
LastLoginAt: user.LastLoginAt,
|
||||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
ID: user.ID,
|
||||
Phone: user.Phone,
|
||||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func FromEntities(users []*entities.User) []*UserResponse {
|
||||
if users == nil {
|
||||
return []*UserResponse{}
|
||||
}
|
||||
|
||||
responses := make([]*UserResponse, len(users))
|
||||
for i, user := range users {
|
||||
responses[i] = FromEntity(user)
|
||||
}
|
||||
return responses
|
||||
}
|
||||
|
||||
88
internal/domains/user/entities/sms_code.go
Normal file
88
internal/domains/user/entities/sms_code.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// SMSCode 短信验证码记录
|
||||
type SMSCode struct {
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" json:"id"`
|
||||
Phone string `gorm:"type:varchar(20);not null;index" json:"phone"`
|
||||
Code string `gorm:"type:varchar(10);not null" json:"-"` // 不返回给前端
|
||||
Scene SMSScene `gorm:"type:varchar(20);not null" json:"scene"`
|
||||
Used bool `gorm:"default:false" json:"used"`
|
||||
ExpiresAt time.Time `gorm:"not null" json:"expires_at"`
|
||||
UsedAt *time.Time `json:"used_at,omitempty"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
// 额外信息
|
||||
IP string `gorm:"type:varchar(45)" json:"ip"`
|
||||
UserAgent string `gorm:"type:varchar(500)" json:"user_agent"`
|
||||
}
|
||||
|
||||
// SMSScene 短信验证码使用场景
|
||||
type SMSScene string
|
||||
|
||||
const (
|
||||
SMSSceneRegister SMSScene = "register" // 注册
|
||||
SMSSceneLogin SMSScene = "login" // 登录
|
||||
SMSSceneChangePassword SMSScene = "change_password" // 修改密码
|
||||
SMSSceneResetPassword SMSScene = "reset_password" // 重置密码
|
||||
SMSSceneBind SMSScene = "bind" // 绑定手机号
|
||||
SMSSceneUnbind SMSScene = "unbind" // 解绑手机号
|
||||
)
|
||||
|
||||
// 实现 Entity 接口
|
||||
func (s *SMSCode) GetID() string {
|
||||
return s.ID
|
||||
}
|
||||
|
||||
func (s *SMSCode) GetCreatedAt() time.Time {
|
||||
return s.CreatedAt
|
||||
}
|
||||
|
||||
func (s *SMSCode) GetUpdatedAt() time.Time {
|
||||
return s.UpdatedAt
|
||||
}
|
||||
|
||||
// Validate 验证短信验证码
|
||||
func (s *SMSCode) Validate() error {
|
||||
if s.Phone == "" {
|
||||
return &ValidationError{Message: "手机号不能为空"}
|
||||
}
|
||||
if s.Code == "" {
|
||||
return &ValidationError{Message: "验证码不能为空"}
|
||||
}
|
||||
if s.Scene == "" {
|
||||
return &ValidationError{Message: "使用场景不能为空"}
|
||||
}
|
||||
if s.ExpiresAt.IsZero() {
|
||||
return &ValidationError{Message: "过期时间不能为空"}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 业务方法
|
||||
func (s *SMSCode) IsExpired() bool {
|
||||
return time.Now().After(s.ExpiresAt)
|
||||
}
|
||||
|
||||
func (s *SMSCode) IsValid() bool {
|
||||
return !s.Used && !s.IsExpired()
|
||||
}
|
||||
|
||||
func (s *SMSCode) MarkAsUsed() {
|
||||
s.Used = true
|
||||
now := time.Now()
|
||||
s.UsedAt = &now
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (SMSCode) TableName() string {
|
||||
return "sms_codes"
|
||||
}
|
||||
@@ -8,37 +8,14 @@ import (
|
||||
|
||||
// User 用户实体
|
||||
type User struct {
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" json:"id"`
|
||||
Username string `gorm:"uniqueIndex;type:varchar(50);not null" json:"username"`
|
||||
Email string `gorm:"uniqueIndex;type:varchar(100);not null" json:"email"`
|
||||
Password string `gorm:"type:varchar(255);not null" json:"-"`
|
||||
FirstName string `gorm:"type:varchar(50)" json:"first_name"`
|
||||
LastName string `gorm:"type:varchar(50)" json:"last_name"`
|
||||
Phone string `gorm:"type:varchar(20)" json:"phone"`
|
||||
Avatar string `gorm:"type:varchar(255)" json:"avatar"`
|
||||
Status UserStatus `gorm:"type:varchar(20);default:'active'" json:"status"`
|
||||
LastLoginAt *time.Time `json:"last_login_at"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
// 软删除字段
|
||||
IsDeleted bool `gorm:"default:false" json:"is_deleted"`
|
||||
|
||||
// 版本控制
|
||||
Version int `gorm:"default:1" json:"version"`
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" json:"id"`
|
||||
Phone string `gorm:"uniqueIndex;type:varchar(20);not null" json:"phone"`
|
||||
Password string `gorm:"type:varchar(255);not null" json:"-"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
}
|
||||
|
||||
// UserStatus 用户状态枚举
|
||||
type UserStatus string
|
||||
|
||||
const (
|
||||
UserStatusActive UserStatus = "active"
|
||||
UserStatusInactive UserStatus = "inactive"
|
||||
UserStatusSuspended UserStatus = "suspended"
|
||||
UserStatusPending UserStatus = "pending"
|
||||
)
|
||||
|
||||
// 实现 Entity 接口
|
||||
func (u *User) GetID() string {
|
||||
return u.ID
|
||||
@@ -52,47 +29,13 @@ func (u *User) GetUpdatedAt() time.Time {
|
||||
return u.UpdatedAt
|
||||
}
|
||||
|
||||
// 业务方法
|
||||
func (u *User) IsActive() bool {
|
||||
return u.Status == UserStatusActive && !u.IsDeleted
|
||||
}
|
||||
|
||||
func (u *User) GetFullName() string {
|
||||
if u.FirstName == "" && u.LastName == "" {
|
||||
return u.Username
|
||||
}
|
||||
return u.FirstName + " " + u.LastName
|
||||
}
|
||||
|
||||
func (u *User) CanLogin() bool {
|
||||
return u.IsActive() && u.Status != UserStatusSuspended
|
||||
}
|
||||
|
||||
func (u *User) MarkAsDeleted() {
|
||||
u.IsDeleted = true
|
||||
u.Status = UserStatusInactive
|
||||
}
|
||||
|
||||
func (u *User) Restore() {
|
||||
u.IsDeleted = false
|
||||
u.Status = UserStatusActive
|
||||
}
|
||||
|
||||
func (u *User) UpdateLastLogin() {
|
||||
now := time.Now()
|
||||
u.LastLoginAt = &now
|
||||
}
|
||||
|
||||
// 验证方法
|
||||
func (u *User) Validate() error {
|
||||
if u.Username == "" {
|
||||
return NewValidationError("username is required")
|
||||
}
|
||||
if u.Email == "" {
|
||||
return NewValidationError("email is required")
|
||||
if u.Phone == "" {
|
||||
return NewValidationError("手机号不能为空")
|
||||
}
|
||||
if u.Password == "" {
|
||||
return NewValidationError("password is required")
|
||||
return NewValidationError("密码不能为空")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -114,25 +57,3 @@ func (e *ValidationError) Error() string {
|
||||
func NewValidationError(message string) *ValidationError {
|
||||
return &ValidationError{Message: message}
|
||||
}
|
||||
|
||||
// UserProfile 用户档案(扩展信息)
|
||||
type UserProfile struct {
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" json:"id"`
|
||||
UserID string `gorm:"type:varchar(36);not null;index" json:"user_id"`
|
||||
Bio string `gorm:"type:text" json:"bio"`
|
||||
Location string `gorm:"type:varchar(100)" json:"location"`
|
||||
Website string `gorm:"type:varchar(255)" json:"website"`
|
||||
Birthday *time.Time `json:"birthday"`
|
||||
Gender string `gorm:"type:varchar(10)" json:"gender"`
|
||||
Timezone string `gorm:"type:varchar(50)" json:"timezone"`
|
||||
Language string `gorm:"type:varchar(10);default:'zh-CN'" json:"language"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
|
||||
// 关联关系
|
||||
User *User `gorm:"foreignKey:UserID;references:ID" json:"user,omitempty"`
|
||||
}
|
||||
|
||||
func (UserProfile) TableName() string {
|
||||
return "user_profiles"
|
||||
}
|
||||
|
||||
@@ -13,15 +13,9 @@ import (
|
||||
type UserEventType string
|
||||
|
||||
const (
|
||||
UserCreatedEvent UserEventType = "user.created"
|
||||
UserUpdatedEvent UserEventType = "user.updated"
|
||||
UserDeletedEvent UserEventType = "user.deleted"
|
||||
UserRestoredEvent UserEventType = "user.restored"
|
||||
UserRegisteredEvent UserEventType = "user.registered"
|
||||
UserLoggedInEvent UserEventType = "user.logged_in"
|
||||
UserLoggedOutEvent UserEventType = "user.logged_out"
|
||||
UserPasswordChangedEvent UserEventType = "user.password_changed"
|
||||
UserStatusChangedEvent UserEventType = "user.status_changed"
|
||||
UserProfileUpdatedEvent UserEventType = "user.profile_updated"
|
||||
)
|
||||
|
||||
// BaseUserEvent 用户事件基础结构
|
||||
@@ -99,17 +93,17 @@ func (e *BaseUserEvent) Unmarshal(data []byte) error {
|
||||
return json.Unmarshal(data, e)
|
||||
}
|
||||
|
||||
// UserCreated 用户创建事件
|
||||
type UserCreated struct {
|
||||
// UserRegistered 用户注册事件
|
||||
type UserRegistered struct {
|
||||
*BaseUserEvent
|
||||
User *entities.User `json:"user"`
|
||||
}
|
||||
|
||||
func NewUserCreatedEvent(user *entities.User, correlationID string) *UserCreated {
|
||||
return &UserCreated{
|
||||
func NewUserRegisteredEvent(user *entities.User, correlationID string) *UserRegistered {
|
||||
return &UserRegistered{
|
||||
BaseUserEvent: &BaseUserEvent{
|
||||
ID: uuid.New().String(),
|
||||
Type: string(UserCreatedEvent),
|
||||
Type: string(UserRegisteredEvent),
|
||||
Version: "1.0",
|
||||
Timestamp: time.Now(),
|
||||
Source: "user-service",
|
||||
@@ -118,97 +112,28 @@ func NewUserCreatedEvent(user *entities.User, correlationID string) *UserCreated
|
||||
DomainVersion: "1.0",
|
||||
CorrelationID: correlationID,
|
||||
Metadata: map[string]interface{}{
|
||||
"user_id": user.ID,
|
||||
"username": user.Username,
|
||||
"email": user.Email,
|
||||
"user_id": user.ID,
|
||||
"phone": user.Phone,
|
||||
},
|
||||
},
|
||||
User: user,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *UserCreated) GetPayload() interface{} {
|
||||
func (e *UserRegistered) GetPayload() interface{} {
|
||||
return e.User
|
||||
}
|
||||
|
||||
// UserUpdated 用户更新事件
|
||||
type UserUpdated struct {
|
||||
*BaseUserEvent
|
||||
UserID string `json:"user_id"`
|
||||
Changes map[string]interface{} `json:"changes"`
|
||||
OldValues map[string]interface{} `json:"old_values"`
|
||||
NewValues map[string]interface{} `json:"new_values"`
|
||||
}
|
||||
|
||||
func NewUserUpdatedEvent(userID string, changes, oldValues, newValues map[string]interface{}, correlationID string) *UserUpdated {
|
||||
return &UserUpdated{
|
||||
BaseUserEvent: &BaseUserEvent{
|
||||
ID: uuid.New().String(),
|
||||
Type: string(UserUpdatedEvent),
|
||||
Version: "1.0",
|
||||
Timestamp: time.Now(),
|
||||
Source: "user-service",
|
||||
AggregateID: userID,
|
||||
AggregateType: "User",
|
||||
DomainVersion: "1.0",
|
||||
CorrelationID: correlationID,
|
||||
Metadata: map[string]interface{}{
|
||||
"user_id": userID,
|
||||
"changed_fields": len(changes),
|
||||
},
|
||||
},
|
||||
UserID: userID,
|
||||
Changes: changes,
|
||||
OldValues: oldValues,
|
||||
NewValues: newValues,
|
||||
}
|
||||
}
|
||||
|
||||
// UserDeleted 用户删除事件
|
||||
type UserDeleted struct {
|
||||
*BaseUserEvent
|
||||
UserID string `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
SoftDelete bool `json:"soft_delete"`
|
||||
}
|
||||
|
||||
func NewUserDeletedEvent(userID, username, email string, softDelete bool, correlationID string) *UserDeleted {
|
||||
return &UserDeleted{
|
||||
BaseUserEvent: &BaseUserEvent{
|
||||
ID: uuid.New().String(),
|
||||
Type: string(UserDeletedEvent),
|
||||
Version: "1.0",
|
||||
Timestamp: time.Now(),
|
||||
Source: "user-service",
|
||||
AggregateID: userID,
|
||||
AggregateType: "User",
|
||||
DomainVersion: "1.0",
|
||||
CorrelationID: correlationID,
|
||||
Metadata: map[string]interface{}{
|
||||
"user_id": userID,
|
||||
"username": username,
|
||||
"email": email,
|
||||
"soft_delete": softDelete,
|
||||
},
|
||||
},
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
Email: email,
|
||||
SoftDelete: softDelete,
|
||||
}
|
||||
}
|
||||
|
||||
// UserLoggedIn 用户登录事件
|
||||
type UserLoggedIn struct {
|
||||
*BaseUserEvent
|
||||
UserID string `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
Phone string `json:"phone"`
|
||||
IPAddress string `json:"ip_address"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
}
|
||||
|
||||
func NewUserLoggedInEvent(userID, username, ipAddress, userAgent, correlationID string) *UserLoggedIn {
|
||||
func NewUserLoggedInEvent(userID, phone, ipAddress, userAgent, correlationID string) *UserLoggedIn {
|
||||
return &UserLoggedIn{
|
||||
BaseUserEvent: &BaseUserEvent{
|
||||
ID: uuid.New().String(),
|
||||
@@ -222,13 +147,13 @@ func NewUserLoggedInEvent(userID, username, ipAddress, userAgent, correlationID
|
||||
CorrelationID: correlationID,
|
||||
Metadata: map[string]interface{}{
|
||||
"user_id": userID,
|
||||
"username": username,
|
||||
"phone": phone,
|
||||
"ip_address": ipAddress,
|
||||
"user_agent": userAgent,
|
||||
},
|
||||
},
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
Phone: phone,
|
||||
IPAddress: ipAddress,
|
||||
UserAgent: userAgent,
|
||||
}
|
||||
@@ -237,11 +162,11 @@ func NewUserLoggedInEvent(userID, username, ipAddress, userAgent, correlationID
|
||||
// UserPasswordChanged 用户密码修改事件
|
||||
type UserPasswordChanged struct {
|
||||
*BaseUserEvent
|
||||
UserID string `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
UserID string `json:"user_id"`
|
||||
Phone string `json:"phone"`
|
||||
}
|
||||
|
||||
func NewUserPasswordChangedEvent(userID, username, correlationID string) *UserPasswordChanged {
|
||||
func NewUserPasswordChangedEvent(userID, phone, correlationID string) *UserPasswordChanged {
|
||||
return &UserPasswordChanged{
|
||||
BaseUserEvent: &BaseUserEvent{
|
||||
ID: uuid.New().String(),
|
||||
@@ -254,46 +179,11 @@ func NewUserPasswordChangedEvent(userID, username, correlationID string) *UserPa
|
||||
DomainVersion: "1.0",
|
||||
CorrelationID: correlationID,
|
||||
Metadata: map[string]interface{}{
|
||||
"user_id": userID,
|
||||
"username": username,
|
||||
"user_id": userID,
|
||||
"phone": phone,
|
||||
},
|
||||
},
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
}
|
||||
}
|
||||
|
||||
// UserStatusChanged 用户状态变更事件
|
||||
type UserStatusChanged struct {
|
||||
*BaseUserEvent
|
||||
UserID string `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
OldStatus entities.UserStatus `json:"old_status"`
|
||||
NewStatus entities.UserStatus `json:"new_status"`
|
||||
}
|
||||
|
||||
func NewUserStatusChangedEvent(userID, username string, oldStatus, newStatus entities.UserStatus, correlationID string) *UserStatusChanged {
|
||||
return &UserStatusChanged{
|
||||
BaseUserEvent: &BaseUserEvent{
|
||||
ID: uuid.New().String(),
|
||||
Type: string(UserStatusChangedEvent),
|
||||
Version: "1.0",
|
||||
Timestamp: time.Now(),
|
||||
Source: "user-service",
|
||||
AggregateID: userID,
|
||||
AggregateType: "User",
|
||||
DomainVersion: "1.0",
|
||||
CorrelationID: correlationID,
|
||||
Metadata: map[string]interface{}{
|
||||
"user_id": userID,
|
||||
"username": username,
|
||||
"old_status": oldStatus,
|
||||
"new_status": newStatus,
|
||||
},
|
||||
},
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
OldStatus: oldStatus,
|
||||
NewStatus: newStatus,
|
||||
UserID: userID,
|
||||
Phone: phone,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
@@ -14,211 +14,123 @@ import (
|
||||
|
||||
// UserHandler 用户HTTP处理器
|
||||
type UserHandler struct {
|
||||
userService *services.UserService
|
||||
response interfaces.ResponseBuilder
|
||||
validator interfaces.RequestValidator
|
||||
logger *zap.Logger
|
||||
jwtAuth *middleware.JWTAuthMiddleware
|
||||
userService interfaces.UserService
|
||||
smsCodeService *services.SMSCodeService
|
||||
response interfaces.ResponseBuilder
|
||||
validator interfaces.RequestValidator
|
||||
logger *zap.Logger
|
||||
jwtAuth *middleware.JWTAuthMiddleware
|
||||
}
|
||||
|
||||
// NewUserHandler 创建用户处理器
|
||||
func NewUserHandler(
|
||||
userService *services.UserService,
|
||||
userService interfaces.UserService,
|
||||
smsCodeService *services.SMSCodeService,
|
||||
response interfaces.ResponseBuilder,
|
||||
validator interfaces.RequestValidator,
|
||||
logger *zap.Logger,
|
||||
jwtAuth *middleware.JWTAuthMiddleware,
|
||||
) *UserHandler {
|
||||
return &UserHandler{
|
||||
userService: userService,
|
||||
response: response,
|
||||
validator: validator,
|
||||
logger: logger,
|
||||
jwtAuth: jwtAuth,
|
||||
userService: userService,
|
||||
smsCodeService: smsCodeService,
|
||||
response: response,
|
||||
validator: validator,
|
||||
logger: logger,
|
||||
jwtAuth: jwtAuth,
|
||||
}
|
||||
}
|
||||
|
||||
// GetPath 返回处理器路径
|
||||
func (h *UserHandler) GetPath() string {
|
||||
return "/users"
|
||||
}
|
||||
|
||||
// GetMethod 返回HTTP方法
|
||||
func (h *UserHandler) GetMethod() string {
|
||||
return "GET" // 主要用于列表,具体方法在路由注册时指定
|
||||
}
|
||||
|
||||
// GetMiddlewares 返回中间件
|
||||
func (h *UserHandler) GetMiddlewares() []gin.HandlerFunc {
|
||||
return []gin.HandlerFunc{
|
||||
// 这里可以添加特定的中间件
|
||||
}
|
||||
}
|
||||
|
||||
// Handle 主处理函数(用于列表)
|
||||
func (h *UserHandler) Handle(c *gin.Context) {
|
||||
h.List(c)
|
||||
}
|
||||
|
||||
// RequiresAuth 是否需要认证
|
||||
func (h *UserHandler) RequiresAuth() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// GetPermissions 获取所需权限
|
||||
func (h *UserHandler) GetPermissions() []string {
|
||||
return []string{"user:read"}
|
||||
}
|
||||
|
||||
// REST操作实现
|
||||
|
||||
// Create 创建用户
|
||||
func (h *UserHandler) Create(c *gin.Context) {
|
||||
var req dto.CreateUserRequest
|
||||
// SendCode 发送验证码
|
||||
// @Summary 发送短信验证码
|
||||
// @Description 向指定手机号发送验证码,支持注册、登录、修改密码等场景
|
||||
// @Tags 用户认证
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body dto.SendCodeRequest true "发送验证码请求"
|
||||
// @Success 200 {object} dto.SendCodeResponse "验证码发送成功"
|
||||
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
||||
// @Failure 429 {object} map[string]interface{} "请求频率限制"
|
||||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||
// @Router /users/send-code [post]
|
||||
func (h *UserHandler) SendCode(c *gin.Context) {
|
||||
var req dto.SendCodeRequest
|
||||
|
||||
// 验证请求体
|
||||
if err := h.validator.BindAndValidate(c, &req); err != nil {
|
||||
return // 响应已在验证器中处理
|
||||
}
|
||||
|
||||
// 创建用户
|
||||
user, err := h.userService.Create(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to create user", zap.Error(err))
|
||||
// 获取客户端信息
|
||||
clientIP := c.ClientIP()
|
||||
userAgent := c.GetHeader("User-Agent")
|
||||
|
||||
// 发送验证码
|
||||
if err := h.smsCodeService.SendCode(c.Request.Context(), req.Phone, req.Scene, clientIP, userAgent); err != nil {
|
||||
h.logger.Error("发送验证码失败",
|
||||
zap.String("phone", req.Phone),
|
||||
zap.String("scene", string(req.Scene)),
|
||||
zap.Error(err))
|
||||
h.response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 返回响应
|
||||
response := dto.FromEntity(user)
|
||||
h.response.Created(c, response, "User created successfully")
|
||||
}
|
||||
|
||||
// GetByID 根据ID获取用户
|
||||
func (h *UserHandler) GetByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
h.response.BadRequest(c, "User ID is required")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取用户
|
||||
user, err := h.userService.GetByID(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to get user", zap.Error(err))
|
||||
h.response.NotFound(c, "User not found")
|
||||
return
|
||||
}
|
||||
|
||||
// 返回响应
|
||||
response := dto.FromEntity(user)
|
||||
h.response.Success(c, response)
|
||||
}
|
||||
|
||||
// Update 更新用户
|
||||
func (h *UserHandler) Update(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
h.response.BadRequest(c, "User ID is required")
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.UpdateUserRequest
|
||||
|
||||
// 验证请求体
|
||||
if err := h.validator.BindAndValidate(c, &req); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 更新用户
|
||||
user, err := h.userService.Update(c.Request.Context(), id, &req)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to update user", zap.Error(err))
|
||||
h.response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 返回响应
|
||||
response := dto.FromEntity(user)
|
||||
h.response.Success(c, response, "User updated successfully")
|
||||
}
|
||||
|
||||
// Delete 删除用户
|
||||
func (h *UserHandler) Delete(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
h.response.BadRequest(c, "User ID is required")
|
||||
return
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
if err := h.userService.Delete(c.Request.Context(), id); err != nil {
|
||||
h.logger.Error("Failed to delete user", zap.Error(err))
|
||||
h.response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 返回响应
|
||||
h.response.Success(c, nil, "User deleted successfully")
|
||||
}
|
||||
|
||||
// List 获取用户列表
|
||||
func (h *UserHandler) List(c *gin.Context) {
|
||||
var req dto.UserListRequest
|
||||
|
||||
// 验证查询参数
|
||||
if err := h.validator.ValidateQuery(c, &req); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 设置默认值
|
||||
if req.Page <= 0 {
|
||||
req.Page = 1
|
||||
}
|
||||
if req.PageSize <= 0 {
|
||||
req.PageSize = 20
|
||||
}
|
||||
|
||||
// 构建查询选项
|
||||
options := interfaces.ListOptions{
|
||||
Page: req.Page,
|
||||
PageSize: req.PageSize,
|
||||
Sort: req.Sort,
|
||||
Order: req.Order,
|
||||
Search: req.Search,
|
||||
Filters: req.Filters,
|
||||
}
|
||||
|
||||
// 获取用户列表
|
||||
users, err := h.userService.List(c.Request.Context(), options)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to get user list", zap.Error(err))
|
||||
h.response.InternalError(c, "Failed to get user list")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
countOptions := interfaces.CountOptions{
|
||||
Search: req.Search,
|
||||
Filters: req.Filters,
|
||||
}
|
||||
total, err := h.userService.Count(c.Request.Context(), countOptions)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to count users", zap.Error(err))
|
||||
h.response.InternalError(c, "Failed to count users")
|
||||
return
|
||||
}
|
||||
|
||||
// 构建响应
|
||||
userResponses := dto.FromEntities(users)
|
||||
pagination := buildPagination(req.Page, req.PageSize, total)
|
||||
response := &dto.SendCodeResponse{
|
||||
Message: "验证码发送成功",
|
||||
ExpiresAt: time.Now().Add(5 * time.Minute), // 5分钟过期
|
||||
}
|
||||
|
||||
h.response.Paginated(c, userResponses, pagination)
|
||||
h.response.Success(c, response, "验证码发送成功")
|
||||
}
|
||||
|
||||
// Login 用户登录
|
||||
func (h *UserHandler) Login(c *gin.Context) {
|
||||
var req dto.LoginRequest
|
||||
// Register 用户注册
|
||||
// @Summary 用户注册
|
||||
// @Description 使用手机号、密码和验证码进行用户注册,需要确认密码
|
||||
// @Tags 用户认证
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body dto.RegisterRequest true "用户注册请求"
|
||||
// @Success 201 {object} dto.UserResponse "注册成功"
|
||||
// @Failure 400 {object} map[string]interface{} "请求参数错误或验证码无效"
|
||||
// @Failure 409 {object} map[string]interface{} "手机号已存在"
|
||||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||
// @Router /users/register [post]
|
||||
func (h *UserHandler) Register(c *gin.Context) {
|
||||
var req dto.RegisterRequest
|
||||
|
||||
// 验证请求体
|
||||
if err := h.validator.BindAndValidate(c, &req); err != nil {
|
||||
return // 响应已在验证器中处理
|
||||
}
|
||||
|
||||
// 注册用户
|
||||
user, err := h.userService.Register(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
h.logger.Error("注册用户失败", zap.Error(err))
|
||||
h.response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 返回响应
|
||||
response := dto.FromEntity(user)
|
||||
h.response.Created(c, response, "用户注册成功")
|
||||
}
|
||||
|
||||
// LoginWithPassword 密码登录
|
||||
// @Summary 用户密码登录
|
||||
// @Description 使用手机号和密码进行用户登录,返回JWT令牌
|
||||
// @Tags 用户认证
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body dto.LoginWithPasswordRequest true "密码登录请求"
|
||||
// @Success 200 {object} dto.LoginResponse "登录成功"
|
||||
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
||||
// @Failure 401 {object} map[string]interface{} "认证失败"
|
||||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||
// @Router /users/login-password [post]
|
||||
func (h *UserHandler) LoginWithPassword(c *gin.Context) {
|
||||
var req dto.LoginWithPasswordRequest
|
||||
|
||||
// 验证请求体
|
||||
if err := h.validator.BindAndValidate(c, &req); err != nil {
|
||||
@@ -226,18 +138,18 @@ func (h *UserHandler) Login(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 用户登录
|
||||
user, err := h.userService.Login(c.Request.Context(), &req)
|
||||
user, err := h.userService.LoginWithPassword(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
h.logger.Error("Login failed", zap.Error(err))
|
||||
h.response.Unauthorized(c, "Invalid credentials")
|
||||
h.logger.Error("密码登录失败", zap.Error(err))
|
||||
h.response.Unauthorized(c, "用户名或密码错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 生成JWT token
|
||||
accessToken, err := h.jwtAuth.GenerateToken(user.ID, user.Username, user.Email)
|
||||
accessToken, err := h.jwtAuth.GenerateToken(user.ID, user.Phone, user.Phone)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to generate token", zap.Error(err))
|
||||
h.response.InternalError(c, "Failed to generate access token")
|
||||
h.logger.Error("生成令牌失败", zap.Error(err))
|
||||
h.response.InternalError(c, "生成访问令牌失败")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -247,72 +159,109 @@ func (h *UserHandler) Login(c *gin.Context) {
|
||||
AccessToken: accessToken,
|
||||
TokenType: "Bearer",
|
||||
ExpiresIn: 86400, // 24小时,从配置获取
|
||||
LoginMethod: "password",
|
||||
}
|
||||
|
||||
h.response.Success(c, loginResponse, "Login successful")
|
||||
h.response.Success(c, loginResponse, "登录成功")
|
||||
}
|
||||
|
||||
// Logout 用户登出
|
||||
func (h *UserHandler) Logout(c *gin.Context) {
|
||||
// 简单实现,客户端删除token即可
|
||||
// 如果需要服务端黑名单,可以在这里实现
|
||||
h.response.Success(c, nil, "Logout successful")
|
||||
}
|
||||
|
||||
// GetProfile 获取当前用户信息
|
||||
func (h *UserHandler) GetProfile(c *gin.Context) {
|
||||
userID := h.getCurrentUserID(c)
|
||||
if userID == "" {
|
||||
h.response.Unauthorized(c, "User not authenticated")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
user, err := h.userService.GetByID(c.Request.Context(), userID)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to get user profile", zap.Error(err))
|
||||
h.response.NotFound(c, "User not found")
|
||||
return
|
||||
}
|
||||
|
||||
// 返回响应
|
||||
response := dto.FromEntity(user)
|
||||
h.response.Success(c, response)
|
||||
}
|
||||
|
||||
// UpdateProfile 更新当前用户信息
|
||||
func (h *UserHandler) UpdateProfile(c *gin.Context) {
|
||||
userID := h.getCurrentUserID(c)
|
||||
if userID == "" {
|
||||
h.response.Unauthorized(c, "User not authenticated")
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.UpdateUserRequest
|
||||
// LoginWithSMS 短信验证码登录
|
||||
// @Summary 用户短信验证码登录
|
||||
// @Description 使用手机号和短信验证码进行用户登录,返回JWT令牌
|
||||
// @Tags 用户认证
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body dto.LoginWithSMSRequest true "短信登录请求"
|
||||
// @Success 200 {object} dto.LoginResponse "登录成功"
|
||||
// @Failure 400 {object} map[string]interface{} "请求参数错误或验证码无效"
|
||||
// @Failure 401 {object} map[string]interface{} "认证失败"
|
||||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||
// @Router /users/login-sms [post]
|
||||
func (h *UserHandler) LoginWithSMS(c *gin.Context) {
|
||||
var req dto.LoginWithSMSRequest
|
||||
|
||||
// 验证请求体
|
||||
if err := h.validator.BindAndValidate(c, &req); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 更新用户
|
||||
user, err := h.userService.Update(c.Request.Context(), userID, &req)
|
||||
// 用户登录
|
||||
user, err := h.userService.LoginWithSMS(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to update profile", zap.Error(err))
|
||||
h.response.BadRequest(c, err.Error())
|
||||
h.logger.Error("短信登录失败", zap.Error(err))
|
||||
h.response.Unauthorized(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 返回响应
|
||||
// 生成JWT token
|
||||
accessToken, err := h.jwtAuth.GenerateToken(user.ID, user.Phone, user.Phone)
|
||||
if err != nil {
|
||||
h.logger.Error("生成令牌失败", zap.Error(err))
|
||||
h.response.InternalError(c, "生成访问令牌失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 构建登录响应
|
||||
loginResponse := &dto.LoginResponse{
|
||||
User: dto.FromEntity(user),
|
||||
AccessToken: accessToken,
|
||||
TokenType: "Bearer",
|
||||
ExpiresIn: 86400, // 24小时,从配置获取
|
||||
LoginMethod: "sms",
|
||||
}
|
||||
|
||||
h.response.Success(c, loginResponse, "登录成功")
|
||||
}
|
||||
|
||||
// GetProfile 获取当前用户信息
|
||||
// @Summary 获取当前用户信息
|
||||
// @Description 根据JWT令牌获取当前登录用户的详细信息
|
||||
// @Tags 用户管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Success 200 {object} dto.UserResponse "用户信息"
|
||||
// @Failure 401 {object} map[string]interface{} "未认证"
|
||||
// @Failure 404 {object} map[string]interface{} "用户不存在"
|
||||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||
// @Router /users/me [get]
|
||||
func (h *UserHandler) GetProfile(c *gin.Context) {
|
||||
userID := h.getCurrentUserID(c)
|
||||
if userID == "" {
|
||||
h.response.Unauthorized(c, "用户未认证")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
user, err := h.userService.GetByID(c.Request.Context(), userID)
|
||||
if err != nil {
|
||||
h.logger.Error("获取用户资料失败", zap.Error(err))
|
||||
h.response.NotFound(c, "用户不存在")
|
||||
return
|
||||
}
|
||||
|
||||
// 返回用户信息
|
||||
response := dto.FromEntity(user)
|
||||
h.response.Success(c, response, "Profile updated successfully")
|
||||
h.response.Success(c, response, "获取用户资料成功")
|
||||
}
|
||||
|
||||
// ChangePassword 修改密码
|
||||
// @Summary 修改密码
|
||||
// @Description 使用旧密码、新密码确认和验证码修改当前用户的密码
|
||||
// @Tags 用户管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param request body dto.ChangePasswordRequest true "修改密码请求"
|
||||
// @Success 200 {object} map[string]interface{} "密码修改成功"
|
||||
// @Failure 400 {object} map[string]interface{} "请求参数错误或验证码无效"
|
||||
// @Failure 401 {object} map[string]interface{} "未认证"
|
||||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||
// @Router /users/me/password [put]
|
||||
func (h *UserHandler) ChangePassword(c *gin.Context) {
|
||||
userID := h.getCurrentUserID(c)
|
||||
if userID == "" {
|
||||
h.response.Unauthorized(c, "User not authenticated")
|
||||
h.response.Unauthorized(c, "用户未认证")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -325,78 +274,14 @@ func (h *UserHandler) ChangePassword(c *gin.Context) {
|
||||
|
||||
// 修改密码
|
||||
if err := h.userService.ChangePassword(c.Request.Context(), userID, &req); err != nil {
|
||||
h.logger.Error("Failed to change password", zap.Error(err))
|
||||
h.logger.Error("修改密码失败", zap.Error(err))
|
||||
h.response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.response.Success(c, nil, "Password changed successfully")
|
||||
h.response.Success(c, nil, "密码修改成功")
|
||||
}
|
||||
|
||||
// Search 搜索用户
|
||||
func (h *UserHandler) Search(c *gin.Context) {
|
||||
var req dto.UserSearchRequest
|
||||
|
||||
// 验证查询参数
|
||||
if err := h.validator.ValidateQuery(c, &req); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 设置默认值
|
||||
if req.Page <= 0 {
|
||||
req.Page = 1
|
||||
}
|
||||
if req.PageSize <= 0 {
|
||||
req.PageSize = 10
|
||||
}
|
||||
|
||||
// 构建查询选项
|
||||
options := interfaces.ListOptions{
|
||||
Page: req.Page,
|
||||
PageSize: req.PageSize,
|
||||
Search: req.Query,
|
||||
}
|
||||
|
||||
// 搜索用户
|
||||
users, err := h.userService.Search(c.Request.Context(), req.Query, options)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to search users", zap.Error(err))
|
||||
h.response.InternalError(c, "Failed to search users")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取搜索结果总数
|
||||
countOptions := interfaces.CountOptions{
|
||||
Search: req.Query,
|
||||
}
|
||||
total, err := h.userService.Count(c.Request.Context(), countOptions)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to count search results", zap.Error(err))
|
||||
h.response.InternalError(c, "Failed to count search results")
|
||||
return
|
||||
}
|
||||
|
||||
// 构建响应
|
||||
userResponses := dto.FromEntities(users)
|
||||
pagination := buildPagination(req.Page, req.PageSize, total)
|
||||
|
||||
h.response.Paginated(c, userResponses, pagination)
|
||||
}
|
||||
|
||||
// GetStats 获取用户统计
|
||||
func (h *UserHandler) GetStats(c *gin.Context) {
|
||||
stats, err := h.userService.GetStats(c.Request.Context())
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to get user stats", zap.Error(err))
|
||||
h.response.InternalError(c, "Failed to get user statistics")
|
||||
return
|
||||
}
|
||||
|
||||
h.response.Success(c, stats)
|
||||
}
|
||||
|
||||
// 私有方法
|
||||
|
||||
// getCurrentUserID 获取当前用户ID
|
||||
func (h *UserHandler) getCurrentUserID(c *gin.Context) string {
|
||||
if userID, exists := c.Get("user_id"); exists {
|
||||
@@ -406,50 +291,3 @@ func (h *UserHandler) getCurrentUserID(c *gin.Context) string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// parsePageSize 解析页面大小
|
||||
func (h *UserHandler) parsePageSize(str string, defaultValue int) int {
|
||||
if str == "" {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
if size, err := strconv.Atoi(str); err == nil && size > 0 && size <= 100 {
|
||||
return size
|
||||
}
|
||||
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// parsePage 解析页码
|
||||
func (h *UserHandler) parsePage(str string, defaultValue int) int {
|
||||
if str == "" {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
if page, err := strconv.Atoi(str); err == nil && page > 0 {
|
||||
return page
|
||||
}
|
||||
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// buildPagination 构建分页元数据
|
||||
func buildPagination(page, pageSize int, total int64) interfaces.PaginationMeta {
|
||||
totalPages := int(float64(total) / float64(pageSize))
|
||||
if float64(total)/float64(pageSize) > float64(totalPages) {
|
||||
totalPages++
|
||||
}
|
||||
|
||||
if totalPages < 1 {
|
||||
totalPages = 1
|
||||
}
|
||||
|
||||
return interfaces.PaginationMeta{
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
Total: total,
|
||||
TotalPages: totalPages,
|
||||
HasNext: page < totalPages,
|
||||
HasPrev: page > 1,
|
||||
}
|
||||
}
|
||||
|
||||
120
internal/domains/user/repositories/sms_code_repository.go
Normal file
120
internal/domains/user/repositories/sms_code_repository.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"tyapi-server/internal/domains/user/entities"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
)
|
||||
|
||||
// SMSCodeRepository 短信验证码仓储
|
||||
type SMSCodeRepository struct {
|
||||
db *gorm.DB
|
||||
cache interfaces.CacheService
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewSMSCodeRepository 创建短信验证码仓储
|
||||
func NewSMSCodeRepository(db *gorm.DB, cache interfaces.CacheService, logger *zap.Logger) *SMSCodeRepository {
|
||||
return &SMSCodeRepository{
|
||||
db: db,
|
||||
cache: cache,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Create 创建短信验证码记录
|
||||
func (r *SMSCodeRepository) Create(ctx context.Context, smsCode *entities.SMSCode) error {
|
||||
if err := r.db.WithContext(ctx).Create(smsCode).Error; err != nil {
|
||||
r.logger.Error("创建短信验证码失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 缓存验证码
|
||||
cacheKey := r.buildCacheKey(smsCode.Phone, smsCode.Scene)
|
||||
r.cache.Set(ctx, cacheKey, smsCode, 5*time.Minute)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetValidCode 获取有效的验证码
|
||||
func (r *SMSCodeRepository) GetValidCode(ctx context.Context, phone string, scene entities.SMSScene) (*entities.SMSCode, error) {
|
||||
// 先从缓存查找
|
||||
cacheKey := r.buildCacheKey(phone, scene)
|
||||
var smsCode entities.SMSCode
|
||||
if err := r.cache.Get(ctx, cacheKey, &smsCode); err == nil {
|
||||
return &smsCode, nil
|
||||
}
|
||||
|
||||
// 从数据库查找最新的有效验证码
|
||||
if err := r.db.WithContext(ctx).
|
||||
Where("phone = ? AND scene = ? AND expires_at > ? AND used_at IS NULL",
|
||||
phone, scene, time.Now()).
|
||||
Order("created_at DESC").
|
||||
First(&smsCode).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 缓存结果
|
||||
r.cache.Set(ctx, cacheKey, &smsCode, 5*time.Minute)
|
||||
|
||||
return &smsCode, nil
|
||||
}
|
||||
|
||||
// MarkAsUsed 标记验证码为已使用
|
||||
func (r *SMSCodeRepository) MarkAsUsed(ctx context.Context, id string) error {
|
||||
now := time.Now()
|
||||
if err := r.db.WithContext(ctx).
|
||||
Model(&entities.SMSCode{}).
|
||||
Where("id = ?", id).
|
||||
Update("used_at", now).Error; err != nil {
|
||||
r.logger.Error("标记验证码为已使用失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
r.logger.Info("验证码已标记为使用", zap.String("code_id", id))
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanupExpired 清理过期的验证码
|
||||
func (r *SMSCodeRepository) CleanupExpired(ctx context.Context) error {
|
||||
result := r.db.WithContext(ctx).
|
||||
Where("expires_at < ?", time.Now()).
|
||||
Delete(&entities.SMSCode{})
|
||||
|
||||
if result.Error != nil {
|
||||
r.logger.Error("清理过期验证码失败", zap.Error(result.Error))
|
||||
return result.Error
|
||||
}
|
||||
|
||||
if result.RowsAffected > 0 {
|
||||
r.logger.Info("清理过期验证码完成", zap.Int64("count", result.RowsAffected))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CountRecentCodes 统计最近发送的验证码数量
|
||||
func (r *SMSCodeRepository) CountRecentCodes(ctx context.Context, phone string, scene entities.SMSScene, duration time.Duration) (int64, error) {
|
||||
var count int64
|
||||
if err := r.db.WithContext(ctx).
|
||||
Model(&entities.SMSCode{}).
|
||||
Where("phone = ? AND scene = ? AND created_at > ?",
|
||||
phone, scene, time.Now().Add(-duration)).
|
||||
Count(&count).Error; err != nil {
|
||||
r.logger.Error("统计最近验证码数量失败", zap.Error(err))
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// buildCacheKey 构建缓存键
|
||||
func (r *SMSCodeRepository) buildCacheKey(phone string, scene entities.SMSScene) string {
|
||||
return fmt.Sprintf("sms_code:%s:%s", phone, string(scene))
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
@@ -12,6 +13,12 @@ import (
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
)
|
||||
|
||||
// 定义错误常量
|
||||
var (
|
||||
// ErrUserNotFound 用户不存在错误
|
||||
ErrUserNotFound = errors.New("用户不存在")
|
||||
)
|
||||
|
||||
// UserRepository 用户仓储实现
|
||||
type UserRepository struct {
|
||||
db *gorm.DB
|
||||
@@ -29,311 +36,150 @@ func NewUserRepository(db *gorm.DB, cache interfaces.CacheService, logger *zap.L
|
||||
}
|
||||
|
||||
// Create 创建用户
|
||||
func (r *UserRepository) Create(ctx context.Context, entity *entities.User) error {
|
||||
if err := r.db.WithContext(ctx).Create(entity).Error; err != nil {
|
||||
r.logger.Error("Failed to create user", zap.Error(err))
|
||||
func (r *UserRepository) Create(ctx context.Context, user *entities.User) error {
|
||||
if err := r.db.WithContext(ctx).Create(user).Error; err != nil {
|
||||
r.logger.Error("创建用户失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 清除相关缓存
|
||||
r.invalidateUserCaches(ctx, entity.ID)
|
||||
r.deleteCacheByPhone(ctx, user.Phone)
|
||||
|
||||
r.logger.Info("用户创建成功", zap.String("user_id", user.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByID 根据ID获取用户
|
||||
func (r *UserRepository) GetByID(ctx context.Context, id string) (*entities.User, error) {
|
||||
// 先尝试从缓存获取
|
||||
cacheKey := r.GetCacheKey(id)
|
||||
// 尝试从缓存获取
|
||||
cacheKey := fmt.Sprintf("user:id:%s", id)
|
||||
var user entities.User
|
||||
|
||||
if err := r.cache.Get(ctx, cacheKey, &user); err == nil {
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// 从数据库获取
|
||||
if err := r.db.WithContext(ctx).Where("id = ? AND is_deleted = false", id).First(&user).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, fmt.Errorf("user not found")
|
||||
// 从数据库查询
|
||||
if err := r.db.WithContext(ctx).Where("id = ?", id).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrUserNotFound
|
||||
}
|
||||
r.logger.Error("根据ID查询用户失败", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 缓存结果
|
||||
r.cache.Set(ctx, cacheKey, &user, 1*time.Hour)
|
||||
r.cache.Set(ctx, cacheKey, &user, 10*time.Minute)
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// FindByPhone 根据手机号查找用户
|
||||
func (r *UserRepository) FindByPhone(ctx context.Context, phone string) (*entities.User, error) {
|
||||
// 尝试从缓存获取
|
||||
cacheKey := fmt.Sprintf("user:phone:%s", phone)
|
||||
var user entities.User
|
||||
if err := r.cache.Get(ctx, cacheKey, &user); err == nil {
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// 从数据库查询
|
||||
if err := r.db.WithContext(ctx).Where("phone = ?", phone).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrUserNotFound
|
||||
}
|
||||
r.logger.Error("根据手机号查询用户失败", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 缓存结果
|
||||
r.cache.Set(ctx, cacheKey, &user, 10*time.Minute)
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// Update 更新用户
|
||||
func (r *UserRepository) Update(ctx context.Context, entity *entities.User) error {
|
||||
if err := r.db.WithContext(ctx).Save(entity).Error; err != nil {
|
||||
r.logger.Error("Failed to update user", zap.Error(err))
|
||||
func (r *UserRepository) Update(ctx context.Context, user *entities.User) error {
|
||||
if err := r.db.WithContext(ctx).Save(user).Error; err != nil {
|
||||
r.logger.Error("更新用户失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 清除相关缓存
|
||||
r.invalidateUserCaches(ctx, entity.ID)
|
||||
r.deleteCacheByID(ctx, user.ID)
|
||||
r.deleteCacheByPhone(ctx, user.Phone)
|
||||
|
||||
r.logger.Info("用户更新成功", zap.String("user_id", user.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete 删除用户
|
||||
func (r *UserRepository) Delete(ctx context.Context, id string) error {
|
||||
// 先获取用户信息用于清除缓存
|
||||
user, err := r.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := r.db.WithContext(ctx).Delete(&entities.User{}, "id = ?", id).Error; err != nil {
|
||||
r.logger.Error("Failed to delete user", zap.Error(err))
|
||||
r.logger.Error("删除用户失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 清除相关缓存
|
||||
r.invalidateUserCaches(ctx, id)
|
||||
r.deleteCacheByID(ctx, id)
|
||||
r.deleteCacheByPhone(ctx, user.Phone)
|
||||
|
||||
r.logger.Info("用户删除成功", zap.String("user_id", id))
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateBatch 批量创建用户
|
||||
func (r *UserRepository) CreateBatch(ctx context.Context, entities []*entities.User) error {
|
||||
if err := r.db.WithContext(ctx).CreateInBatches(entities, 100).Error; err != nil {
|
||||
r.logger.Error("Failed to create users in batch", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 清除列表缓存
|
||||
r.cache.DeletePattern(ctx, "users:list:*")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByIDs 根据ID列表获取用户
|
||||
func (r *UserRepository) GetByIDs(ctx context.Context, ids []string) ([]*entities.User, error) {
|
||||
var users []entities.User
|
||||
|
||||
if err := r.db.WithContext(ctx).
|
||||
Where("id IN ? AND is_deleted = false", ids).
|
||||
Find(&users).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 转换为指针切片
|
||||
result := make([]*entities.User, len(users))
|
||||
for i := range users {
|
||||
result[i] = &users[i]
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// UpdateBatch 批量更新用户
|
||||
func (r *UserRepository) UpdateBatch(ctx context.Context, entities []*entities.User) error {
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
for _, entity := range entities {
|
||||
if err := tx.Save(entity).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteBatch 批量删除用户
|
||||
func (r *UserRepository) DeleteBatch(ctx context.Context, ids []string) error {
|
||||
if err := r.db.WithContext(ctx).
|
||||
Where("id IN ?", ids).
|
||||
Delete(&entities.User{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 清除相关缓存
|
||||
for _, id := range ids {
|
||||
r.invalidateUserCaches(ctx, id)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// List 获取用户列表
|
||||
func (r *UserRepository) List(ctx context.Context, options interfaces.ListOptions) ([]*entities.User, error) {
|
||||
// 尝试从缓存获取
|
||||
cacheKey := fmt.Sprintf("users:list:%d:%d:%s", options.Page, options.PageSize, options.Sort)
|
||||
// List 分页获取用户列表
|
||||
func (r *UserRepository) List(ctx context.Context, offset, limit int) ([]*entities.User, error) {
|
||||
var users []*entities.User
|
||||
|
||||
if err := r.cache.Get(ctx, cacheKey, &users); err == nil {
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// 从数据库查询
|
||||
query := r.db.WithContext(ctx).Where("is_deleted = false")
|
||||
|
||||
// 应用过滤条件
|
||||
if options.Search != "" {
|
||||
query = query.Where("username ILIKE ? OR email ILIKE ? OR first_name ILIKE ? OR last_name ILIKE ?",
|
||||
"%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%")
|
||||
}
|
||||
|
||||
// 应用排序
|
||||
if options.Sort != "" {
|
||||
order := options.Order
|
||||
if order == "" {
|
||||
order = "asc"
|
||||
}
|
||||
query = query.Order(fmt.Sprintf("%s %s", options.Sort, order))
|
||||
} else {
|
||||
query = query.Order("created_at desc")
|
||||
}
|
||||
|
||||
// 应用分页
|
||||
if options.Page > 0 && options.PageSize > 0 {
|
||||
offset := (options.Page - 1) * options.PageSize
|
||||
query = query.Offset(offset).Limit(options.PageSize)
|
||||
}
|
||||
|
||||
var userEntities []entities.User
|
||||
if err := query.Find(&userEntities).Error; err != nil {
|
||||
if err := r.db.WithContext(ctx).Offset(offset).Limit(limit).Find(&users).Error; err != nil {
|
||||
r.logger.Error("查询用户列表失败", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 转换为指针切片
|
||||
users = make([]*entities.User, len(userEntities))
|
||||
for i := range userEntities {
|
||||
users[i] = &userEntities[i]
|
||||
}
|
||||
|
||||
// 缓存结果
|
||||
r.cache.Set(ctx, cacheKey, users, 30*time.Minute)
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// Count 统计用户数量
|
||||
func (r *UserRepository) Count(ctx context.Context, options interfaces.CountOptions) (int64, error) {
|
||||
query := r.db.WithContext(ctx).Model(&entities.User{}).Where("is_deleted = false")
|
||||
|
||||
// 应用过滤条件
|
||||
if options.Search != "" {
|
||||
query = query.Where("username ILIKE ? OR email ILIKE ? OR first_name ILIKE ? OR last_name ILIKE ?",
|
||||
"%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%")
|
||||
}
|
||||
|
||||
// Count 获取用户总数
|
||||
func (r *UserRepository) Count(ctx context.Context) (int64, error) {
|
||||
var count int64
|
||||
if err := query.Count(&count).Error; err != nil {
|
||||
if err := r.db.WithContext(ctx).Model(&entities.User{}).Count(&count).Error; err != nil {
|
||||
r.logger.Error("统计用户数量失败", zap.Error(err))
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// Exists 检查用户是否存在
|
||||
func (r *UserRepository) Exists(ctx context.Context, id string) (bool, error) {
|
||||
// ExistsByPhone 检查手机号是否存在
|
||||
func (r *UserRepository) ExistsByPhone(ctx context.Context, phone string) (bool, error) {
|
||||
var count int64
|
||||
if err := r.db.WithContext(ctx).
|
||||
Model(&entities.User{}).
|
||||
Where("id = ? AND is_deleted = false", id).
|
||||
Count(&count).Error; err != nil {
|
||||
if err := r.db.WithContext(ctx).Model(&entities.User{}).Where("phone = ?", phone).Count(&count).Error; err != nil {
|
||||
r.logger.Error("检查手机号是否存在失败", zap.Error(err))
|
||||
return false, err
|
||||
}
|
||||
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
// SoftDelete 软删除用户
|
||||
func (r *UserRepository) SoftDelete(ctx context.Context, id string) error {
|
||||
if err := r.db.WithContext(ctx).
|
||||
Model(&entities.User{}).
|
||||
Where("id = ?", id).
|
||||
Update("is_deleted", true).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// 私有辅助方法
|
||||
|
||||
// 清除相关缓存
|
||||
r.invalidateUserCaches(ctx, id)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Restore 恢复用户
|
||||
func (r *UserRepository) Restore(ctx context.Context, id string) error {
|
||||
if err := r.db.WithContext(ctx).
|
||||
Model(&entities.User{}).
|
||||
Where("id = ?", id).
|
||||
Update("is_deleted", false).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 清除相关缓存
|
||||
r.invalidateUserCaches(ctx, id)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithTx 使用事务
|
||||
func (r *UserRepository) WithTx(tx interface{}) interfaces.Repository[*entities.User] {
|
||||
gormTx, ok := tx.(*gorm.DB)
|
||||
if !ok {
|
||||
return r
|
||||
}
|
||||
|
||||
return &UserRepository{
|
||||
db: gormTx,
|
||||
cache: r.cache,
|
||||
logger: r.logger,
|
||||
// deleteCacheByID 根据ID删除缓存
|
||||
func (r *UserRepository) deleteCacheByID(ctx context.Context, id string) {
|
||||
cacheKey := fmt.Sprintf("user:id:%s", id)
|
||||
if err := r.cache.Delete(ctx, cacheKey); err != nil {
|
||||
r.logger.Warn("删除用户ID缓存失败", zap.String("cache_key", cacheKey), zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// InvalidateCache 清除缓存
|
||||
func (r *UserRepository) InvalidateCache(ctx context.Context, keys ...string) error {
|
||||
return r.cache.Delete(ctx, keys...)
|
||||
}
|
||||
|
||||
// WarmupCache 预热缓存
|
||||
func (r *UserRepository) WarmupCache(ctx context.Context) error {
|
||||
// 预热热门用户数据
|
||||
// 这里可以实现具体的预热逻辑
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCacheKey 获取缓存键
|
||||
func (r *UserRepository) GetCacheKey(id string) string {
|
||||
return fmt.Sprintf("user:%s", id)
|
||||
}
|
||||
|
||||
// FindByUsername 根据用户名查找用户
|
||||
func (r *UserRepository) FindByUsername(ctx context.Context, username string) (*entities.User, error) {
|
||||
var user entities.User
|
||||
|
||||
if err := r.db.WithContext(ctx).
|
||||
Where("username = ? AND is_deleted = false", username).
|
||||
First(&user).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, fmt.Errorf("user not found")
|
||||
}
|
||||
return nil, err
|
||||
// deleteCacheByPhone 根据手机号删除缓存
|
||||
func (r *UserRepository) deleteCacheByPhone(ctx context.Context, phone string) {
|
||||
cacheKey := fmt.Sprintf("user:phone:%s", phone)
|
||||
if err := r.cache.Delete(ctx, cacheKey); err != nil {
|
||||
r.logger.Warn("删除用户手机号缓存失败", zap.String("cache_key", cacheKey), zap.Error(err))
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// FindByEmail 根据邮箱查找用户
|
||||
func (r *UserRepository) FindByEmail(ctx context.Context, email string) (*entities.User, error) {
|
||||
var user entities.User
|
||||
|
||||
if err := r.db.WithContext(ctx).
|
||||
Where("email = ? AND is_deleted = false", email).
|
||||
First(&user).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, fmt.Errorf("user not found")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// invalidateUserCaches 清除用户相关缓存
|
||||
func (r *UserRepository) invalidateUserCaches(ctx context.Context, userID string) {
|
||||
keys := []string{
|
||||
r.GetCacheKey(userID),
|
||||
}
|
||||
|
||||
r.cache.Delete(ctx, keys...)
|
||||
r.cache.DeletePattern(ctx, "users:list:*")
|
||||
}
|
||||
|
||||
@@ -7,127 +7,23 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// UserRoutes 用户路由注册器
|
||||
type UserRoutes struct {
|
||||
handler *handlers.UserHandler
|
||||
jwtAuth *middleware.JWTAuthMiddleware
|
||||
optionalAuth *middleware.OptionalAuthMiddleware
|
||||
}
|
||||
|
||||
// NewUserRoutes 创建用户路由注册器
|
||||
func NewUserRoutes(
|
||||
handler *handlers.UserHandler,
|
||||
jwtAuth *middleware.JWTAuthMiddleware,
|
||||
optionalAuth *middleware.OptionalAuthMiddleware,
|
||||
) *UserRoutes {
|
||||
return &UserRoutes{
|
||||
handler: handler,
|
||||
jwtAuth: jwtAuth,
|
||||
optionalAuth: optionalAuth,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册用户路由
|
||||
func (r *UserRoutes) RegisterRoutes(router *gin.Engine) {
|
||||
// API版本组
|
||||
v1 := router.Group("/api/v1")
|
||||
|
||||
// 公开路由(不需要认证)
|
||||
public := v1.Group("/auth")
|
||||
// UserRoutes 注册用户相关路由
|
||||
func UserRoutes(router *gin.Engine, handler *handlers.UserHandler, authMiddleware *middleware.JWTAuthMiddleware) {
|
||||
// 用户域路由组
|
||||
usersGroup := router.Group("/api/v1/users")
|
||||
{
|
||||
public.POST("/login", r.handler.Login)
|
||||
public.POST("/register", r.handler.Create)
|
||||
}
|
||||
// 公开路由(不需要认证)
|
||||
usersGroup.POST("/send-code", handler.SendCode) // 发送验证码
|
||||
usersGroup.POST("/register", handler.Register) // 用户注册
|
||||
usersGroup.POST("/login-password", handler.LoginWithPassword) // 密码登录
|
||||
usersGroup.POST("/login-sms", handler.LoginWithSMS) // 短信验证码登录
|
||||
|
||||
// 需要认证的路由
|
||||
protected := v1.Group("/users")
|
||||
protected.Use(r.jwtAuth.Handle())
|
||||
{
|
||||
// 用户管理(管理员)
|
||||
protected.GET("", r.handler.List)
|
||||
protected.POST("", r.handler.Create)
|
||||
protected.GET("/:id", r.handler.GetByID)
|
||||
protected.PUT("/:id", r.handler.Update)
|
||||
protected.DELETE("/:id", r.handler.Delete)
|
||||
|
||||
// 用户搜索
|
||||
protected.GET("/search", r.handler.Search)
|
||||
|
||||
// 用户统计
|
||||
protected.GET("/stats", r.handler.GetStats)
|
||||
}
|
||||
|
||||
// 用户个人操作路由
|
||||
profile := v1.Group("/profile")
|
||||
profile.Use(r.jwtAuth.Handle())
|
||||
{
|
||||
profile.GET("", r.handler.GetProfile)
|
||||
profile.PUT("", r.handler.UpdateProfile)
|
||||
profile.POST("/change-password", r.handler.ChangePassword)
|
||||
profile.POST("/logout", r.handler.Logout)
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterPublicRoutes 注册公开路由
|
||||
func (r *UserRoutes) RegisterPublicRoutes(router *gin.Engine) {
|
||||
v1 := router.Group("/api/v1")
|
||||
|
||||
// 公开的用户相关路由
|
||||
public := v1.Group("/public")
|
||||
{
|
||||
// 可选认证的路由(用户可能登录也可能未登录)
|
||||
public.Use(r.optionalAuth.Handle())
|
||||
|
||||
// 这里可以添加一些公开的用户信息查询接口
|
||||
// 比如根据用户名查看公开信息(如果用户设置为公开)
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterAdminRoutes 注册管理员路由
|
||||
func (r *UserRoutes) RegisterAdminRoutes(router *gin.Engine) {
|
||||
admin := router.Group("/admin/v1")
|
||||
admin.Use(r.jwtAuth.Handle())
|
||||
// 这里可以添加管理员权限检查中间件
|
||||
|
||||
// 管理员用户管理
|
||||
users := admin.Group("/users")
|
||||
{
|
||||
users.GET("", r.handler.List)
|
||||
users.GET("/:id", r.handler.GetByID)
|
||||
users.PUT("/:id", r.handler.Update)
|
||||
users.DELETE("/:id", r.handler.Delete)
|
||||
users.GET("/stats", r.handler.GetStats)
|
||||
users.GET("/search", r.handler.Search)
|
||||
|
||||
// 批量操作
|
||||
users.POST("/batch-delete", r.handleBatchDelete)
|
||||
users.POST("/batch-update", r.handleBatchUpdate)
|
||||
}
|
||||
}
|
||||
|
||||
// 批量删除处理器
|
||||
func (r *UserRoutes) handleBatchDelete(c *gin.Context) {
|
||||
// 实现批量删除逻辑
|
||||
// 这里可以接收用户ID列表并调用服务进行批量删除
|
||||
c.JSON(200, gin.H{"message": "Batch delete not implemented yet"})
|
||||
}
|
||||
|
||||
// 批量更新处理器
|
||||
func (r *UserRoutes) handleBatchUpdate(c *gin.Context) {
|
||||
// 实现批量更新逻辑
|
||||
c.JSON(200, gin.H{"message": "Batch update not implemented yet"})
|
||||
}
|
||||
|
||||
// RegisterHealthRoutes 注册健康检查路由
|
||||
func (r *UserRoutes) RegisterHealthRoutes(router *gin.Engine) {
|
||||
health := router.Group("/health")
|
||||
{
|
||||
health.GET("/users", func(c *gin.Context) {
|
||||
// 用户服务健康检查
|
||||
c.JSON(200, gin.H{
|
||||
"service": "users",
|
||||
"status": "healthy",
|
||||
})
|
||||
})
|
||||
// 需要认证的路由
|
||||
authenticated := usersGroup.Group("")
|
||||
authenticated.Use(authMiddleware.Handle())
|
||||
{
|
||||
authenticated.GET("/me", handler.GetProfile) // 获取当前用户信息
|
||||
authenticated.PUT("/me/password", handler.ChangePassword) // 修改密码
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
187
internal/domains/user/services/sms_code_service.go
Normal file
187
internal/domains/user/services/sms_code_service.go
Normal file
@@ -0,0 +1,187 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/config"
|
||||
"tyapi-server/internal/domains/user/entities"
|
||||
"tyapi-server/internal/domains/user/repositories"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
"tyapi-server/internal/shared/sms"
|
||||
)
|
||||
|
||||
// SMSCodeService 短信验证码服务
|
||||
type SMSCodeService struct {
|
||||
repo *repositories.SMSCodeRepository
|
||||
smsClient sms.Service
|
||||
cache interfaces.CacheService
|
||||
config config.SMSConfig
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewSMSCodeService 创建短信验证码服务
|
||||
func NewSMSCodeService(
|
||||
repo *repositories.SMSCodeRepository,
|
||||
smsClient sms.Service,
|
||||
cache interfaces.CacheService,
|
||||
config config.SMSConfig,
|
||||
logger *zap.Logger,
|
||||
) *SMSCodeService {
|
||||
return &SMSCodeService{
|
||||
repo: repo,
|
||||
smsClient: smsClient,
|
||||
cache: cache,
|
||||
config: config,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// SendCode 发送验证码
|
||||
func (s *SMSCodeService) SendCode(ctx context.Context, phone string, scene entities.SMSScene, clientIP, userAgent string) error {
|
||||
// 检查频率限制
|
||||
if err := s.checkRateLimit(ctx, phone); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 生成验证码
|
||||
code := s.smsClient.GenerateCode(s.config.CodeLength)
|
||||
|
||||
// 创建SMS验证码记录
|
||||
smsCode := &entities.SMSCode{
|
||||
ID: uuid.New().String(),
|
||||
Phone: phone,
|
||||
Code: code,
|
||||
Scene: scene,
|
||||
IP: clientIP,
|
||||
UserAgent: userAgent,
|
||||
Used: false,
|
||||
ExpiresAt: time.Now().Add(s.config.ExpireTime),
|
||||
}
|
||||
|
||||
// 保存验证码
|
||||
if err := s.repo.Create(ctx, smsCode); err != nil {
|
||||
s.logger.Error("保存短信验证码失败",
|
||||
zap.String("phone", phone),
|
||||
zap.String("scene", string(scene)),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("保存验证码失败: %w", err)
|
||||
}
|
||||
|
||||
// 发送短信
|
||||
if err := s.smsClient.SendVerificationCode(ctx, phone, code); err != nil {
|
||||
// 记录发送失败但不删除验证码记录,让其自然过期
|
||||
s.logger.Error("发送短信验证码失败",
|
||||
zap.String("phone", phone),
|
||||
zap.String("code", code),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("短信发送失败: %w", err)
|
||||
}
|
||||
|
||||
// 更新发送记录缓存
|
||||
s.updateSendRecord(ctx, phone)
|
||||
|
||||
s.logger.Info("短信验证码发送成功",
|
||||
zap.String("phone", phone),
|
||||
zap.String("scene", string(scene)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyCode 验证验证码
|
||||
func (s *SMSCodeService) VerifyCode(ctx context.Context, phone, code string, scene entities.SMSScene) error {
|
||||
// 根据手机号和场景获取有效的验证码记录
|
||||
smsCode, err := s.repo.GetValidCode(ctx, phone, scene)
|
||||
if err != nil {
|
||||
return fmt.Errorf("验证码无效或已过期")
|
||||
}
|
||||
|
||||
// 验证验证码是否匹配
|
||||
if smsCode.Code != code {
|
||||
return fmt.Errorf("验证码无效或已过期")
|
||||
}
|
||||
|
||||
// 标记验证码为已使用
|
||||
if err := s.repo.MarkAsUsed(ctx, smsCode.ID); err != nil {
|
||||
s.logger.Error("标记验证码为已使用失败",
|
||||
zap.String("code_id", smsCode.ID),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("验证码状态更新失败")
|
||||
}
|
||||
|
||||
s.logger.Info("短信验证码验证成功",
|
||||
zap.String("phone", phone),
|
||||
zap.String("scene", string(scene)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkRateLimit 检查发送频率限制
|
||||
func (s *SMSCodeService) checkRateLimit(ctx context.Context, phone string) error {
|
||||
now := time.Now()
|
||||
|
||||
// 检查最小发送间隔
|
||||
lastSentKey := fmt.Sprintf("sms:last_sent:%s", phone)
|
||||
var lastSent time.Time
|
||||
if err := s.cache.Get(ctx, lastSentKey, &lastSent); err == nil {
|
||||
if now.Sub(lastSent) < s.config.RateLimit.MinInterval {
|
||||
return fmt.Errorf("请等待 %v 后再试", s.config.RateLimit.MinInterval)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查每小时发送限制
|
||||
hourlyKey := fmt.Sprintf("sms:hourly:%s:%s", phone, now.Format("2006010215"))
|
||||
var hourlyCount int
|
||||
if err := s.cache.Get(ctx, hourlyKey, &hourlyCount); err == nil {
|
||||
if hourlyCount >= s.config.RateLimit.HourlyLimit {
|
||||
return fmt.Errorf("每小时最多发送 %d 条短信", s.config.RateLimit.HourlyLimit)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查每日发送限制
|
||||
dailyKey := fmt.Sprintf("sms:daily:%s:%s", phone, now.Format("20060102"))
|
||||
var dailyCount int
|
||||
if err := s.cache.Get(ctx, dailyKey, &dailyCount); err == nil {
|
||||
if dailyCount >= s.config.RateLimit.DailyLimit {
|
||||
return fmt.Errorf("每日最多发送 %d 条短信", s.config.RateLimit.DailyLimit)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateSendRecord 更新发送记录
|
||||
func (s *SMSCodeService) updateSendRecord(ctx context.Context, phone string) {
|
||||
now := time.Now()
|
||||
|
||||
// 更新最后发送时间
|
||||
lastSentKey := fmt.Sprintf("sms:last_sent:%s", phone)
|
||||
s.cache.Set(ctx, lastSentKey, now, s.config.RateLimit.MinInterval)
|
||||
|
||||
// 更新每小时计数
|
||||
hourlyKey := fmt.Sprintf("sms:hourly:%s:%s", phone, now.Format("2006010215"))
|
||||
var hourlyCount int
|
||||
if err := s.cache.Get(ctx, hourlyKey, &hourlyCount); err == nil {
|
||||
s.cache.Set(ctx, hourlyKey, hourlyCount+1, time.Hour)
|
||||
} else {
|
||||
s.cache.Set(ctx, hourlyKey, 1, time.Hour)
|
||||
}
|
||||
|
||||
// 更新每日计数
|
||||
dailyKey := fmt.Sprintf("sms:daily:%s:%s", phone, now.Format("20060102"))
|
||||
var dailyCount int
|
||||
if err := s.cache.Get(ctx, dailyKey, &dailyCount); err == nil {
|
||||
s.cache.Set(ctx, dailyKey, dailyCount+1, 24*time.Hour)
|
||||
} else {
|
||||
s.cache.Set(ctx, dailyKey, 1, 24*time.Hour)
|
||||
}
|
||||
}
|
||||
|
||||
// CleanExpiredCodes 清理过期验证码
|
||||
func (s *SMSCodeService) CleanExpiredCodes(ctx context.Context) error {
|
||||
return s.repo.CleanupExpired(ctx)
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package services
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
"regexp"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
@@ -18,21 +18,24 @@ import (
|
||||
|
||||
// UserService 用户服务实现
|
||||
type UserService struct {
|
||||
repo *repositories.UserRepository
|
||||
eventBus interfaces.EventBus
|
||||
logger *zap.Logger
|
||||
repo *repositories.UserRepository
|
||||
smsCodeService *SMSCodeService
|
||||
eventBus interfaces.EventBus
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewUserService 创建用户服务
|
||||
func NewUserService(
|
||||
repo *repositories.UserRepository,
|
||||
smsCodeService *SMSCodeService,
|
||||
eventBus interfaces.EventBus,
|
||||
logger *zap.Logger,
|
||||
) *UserService {
|
||||
return &UserService{
|
||||
repo: repo,
|
||||
eventBus: eventBus,
|
||||
logger: logger,
|
||||
repo: repo,
|
||||
smsCodeService: smsCodeService,
|
||||
eventBus: eventBus,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,341 +46,209 @@ func (s *UserService) Name() string {
|
||||
|
||||
// Initialize 初始化服务
|
||||
func (s *UserService) Initialize(ctx context.Context) error {
|
||||
s.logger.Info("User service initialized")
|
||||
s.logger.Info("用户服务已初始化")
|
||||
return nil
|
||||
}
|
||||
|
||||
// HealthCheck 健康检查
|
||||
func (s *UserService) HealthCheck(ctx context.Context) error {
|
||||
// 简单检查:尝试查询用户数量
|
||||
_, err := s.repo.Count(ctx, interfaces.CountOptions{})
|
||||
return err
|
||||
// 简单的健康检查
|
||||
return nil
|
||||
}
|
||||
|
||||
// Shutdown 关闭服务
|
||||
func (s *UserService) Shutdown(ctx context.Context) error {
|
||||
s.logger.Info("User service shutdown")
|
||||
s.logger.Info("用户服务已关闭")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create 创建用户
|
||||
func (s *UserService) Create(ctx context.Context, createDTO interface{}) (*entities.User, error) {
|
||||
req, ok := createDTO.(*dto.CreateUserRequest)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid DTO type for user creation")
|
||||
// Register 用户注册
|
||||
func (s *UserService) Register(ctx context.Context, registerReq *dto.RegisterRequest) (*entities.User, error) {
|
||||
// 验证手机号格式
|
||||
if !s.isValidPhone(registerReq.Phone) {
|
||||
return nil, fmt.Errorf("手机号格式无效")
|
||||
}
|
||||
|
||||
// 验证业务规则
|
||||
if err := s.ValidateCreate(ctx, req); err != nil {
|
||||
return nil, err
|
||||
// 验证密码确认
|
||||
if registerReq.Password != registerReq.ConfirmPassword {
|
||||
return nil, fmt.Errorf("密码和确认密码不匹配")
|
||||
}
|
||||
|
||||
// 检查用户名和邮箱是否已存在
|
||||
if err := s.checkDuplicates(ctx, req.Username, req.Email); err != nil {
|
||||
// 验证短信验证码
|
||||
if err := s.smsCodeService.VerifyCode(ctx, registerReq.Phone, registerReq.Code, entities.SMSSceneRegister); err != nil {
|
||||
return nil, fmt.Errorf("验证码验证失败: %w", err)
|
||||
}
|
||||
|
||||
// 检查手机号是否已存在
|
||||
if err := s.checkPhoneDuplicate(ctx, registerReq.Phone); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 创建用户实体
|
||||
user := req.ToEntity()
|
||||
user := registerReq.ToEntity()
|
||||
user.ID = uuid.New().String()
|
||||
|
||||
// 加密密码
|
||||
hashedPassword, err := s.hashPassword(req.Password)
|
||||
// 哈希密码
|
||||
hashedPassword, err := s.hashPassword(registerReq.Password)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to hash password: %w", err)
|
||||
return nil, fmt.Errorf("密码加密失败: %w", err)
|
||||
}
|
||||
user.Password = hashedPassword
|
||||
|
||||
// 保存用户
|
||||
if err := s.repo.Create(ctx, user); err != nil {
|
||||
s.logger.Error("Failed to create user", zap.Error(err))
|
||||
return nil, fmt.Errorf("failed to create user: %w", err)
|
||||
s.logger.Error("创建用户失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("创建用户失败: %w", err)
|
||||
}
|
||||
|
||||
// 发布用户创建事件
|
||||
event := events.NewUserCreatedEvent(user, s.getCorrelationID(ctx))
|
||||
// 发布用户注册事件
|
||||
event := events.NewUserRegisteredEvent(user, s.getCorrelationID(ctx))
|
||||
if err := s.eventBus.Publish(ctx, event); err != nil {
|
||||
s.logger.Warn("Failed to publish user created event", zap.Error(err))
|
||||
s.logger.Warn("发布用户注册事件失败", zap.Error(err))
|
||||
}
|
||||
|
||||
s.logger.Info("User created successfully",
|
||||
s.logger.Info("用户注册成功",
|
||||
zap.String("user_id", user.ID),
|
||||
zap.String("username", user.Username))
|
||||
zap.String("phone", user.Phone))
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// GetByID 根据ID获取用户
|
||||
func (s *UserService) GetByID(ctx context.Context, id string) (*entities.User, error) {
|
||||
if id == "" {
|
||||
return nil, fmt.Errorf("user ID is required")
|
||||
}
|
||||
|
||||
user, err := s.repo.GetByID(ctx, id)
|
||||
// LoginWithPassword 密码登录
|
||||
func (s *UserService) LoginWithPassword(ctx context.Context, loginReq *dto.LoginWithPasswordRequest) (*entities.User, error) {
|
||||
// 根据手机号查找用户
|
||||
user, err := s.repo.FindByPhone(ctx, loginReq.Phone)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("user not found: %w", err)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// Update 更新用户
|
||||
func (s *UserService) Update(ctx context.Context, id string, updateDTO interface{}) (*entities.User, error) {
|
||||
req, ok := updateDTO.(*dto.UpdateUserRequest)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid DTO type for user update")
|
||||
}
|
||||
|
||||
// 验证业务规则
|
||||
if err := s.ValidateUpdate(ctx, id, req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取现有用户
|
||||
user, err := s.repo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("user not found: %w", err)
|
||||
}
|
||||
|
||||
// 记录变更前的值
|
||||
oldValues := s.captureUserValues(user)
|
||||
|
||||
// 应用更新
|
||||
s.applyUserUpdates(user, req)
|
||||
|
||||
// 保存更新
|
||||
if err := s.repo.Update(ctx, user); err != nil {
|
||||
s.logger.Error("Failed to update user", zap.Error(err))
|
||||
return nil, fmt.Errorf("failed to update user: %w", err)
|
||||
}
|
||||
|
||||
// 发布用户更新事件
|
||||
newValues := s.captureUserValues(user)
|
||||
changes := s.findChanges(oldValues, newValues)
|
||||
if len(changes) > 0 {
|
||||
event := events.NewUserUpdatedEvent(user.ID, changes, oldValues, newValues, s.getCorrelationID(ctx))
|
||||
if err := s.eventBus.Publish(ctx, event); err != nil {
|
||||
s.logger.Warn("Failed to publish user updated event", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Info("User updated successfully",
|
||||
zap.String("user_id", user.ID),
|
||||
zap.Int("changes", len(changes)))
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// Delete 删除用户
|
||||
func (s *UserService) Delete(ctx context.Context, id string) error {
|
||||
if id == "" {
|
||||
return fmt.Errorf("user ID is required")
|
||||
}
|
||||
|
||||
// 获取用户信息用于事件
|
||||
user, err := s.repo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("user not found: %w", err)
|
||||
}
|
||||
|
||||
// 软删除用户
|
||||
if err := s.repo.SoftDelete(ctx, id); err != nil {
|
||||
s.logger.Error("Failed to delete user", zap.Error(err))
|
||||
return fmt.Errorf("failed to delete user: %w", err)
|
||||
}
|
||||
|
||||
// 发布用户删除事件
|
||||
event := events.NewUserDeletedEvent(user.ID, user.Username, user.Email, true, s.getCorrelationID(ctx))
|
||||
if err := s.eventBus.Publish(ctx, event); err != nil {
|
||||
s.logger.Warn("Failed to publish user deleted event", zap.Error(err))
|
||||
}
|
||||
|
||||
s.logger.Info("User deleted successfully", zap.String("user_id", id))
|
||||
return nil
|
||||
}
|
||||
|
||||
// List 获取用户列表
|
||||
func (s *UserService) List(ctx context.Context, options interfaces.ListOptions) ([]*entities.User, error) {
|
||||
return s.repo.List(ctx, options)
|
||||
}
|
||||
|
||||
// Search 搜索用户
|
||||
func (s *UserService) Search(ctx context.Context, query string, options interfaces.ListOptions) ([]*entities.User, error) {
|
||||
// 设置搜索关键字
|
||||
searchOptions := options
|
||||
searchOptions.Search = query
|
||||
|
||||
return s.repo.List(ctx, searchOptions)
|
||||
}
|
||||
|
||||
// Count 统计用户数量
|
||||
func (s *UserService) Count(ctx context.Context, options interfaces.CountOptions) (int64, error) {
|
||||
return s.repo.Count(ctx, options)
|
||||
}
|
||||
|
||||
// Validate 验证用户实体
|
||||
func (s *UserService) Validate(ctx context.Context, entity *entities.User) error {
|
||||
return entity.Validate()
|
||||
}
|
||||
|
||||
// ValidateCreate 验证创建请求
|
||||
func (s *UserService) ValidateCreate(ctx context.Context, createDTO interface{}) error {
|
||||
req, ok := createDTO.(*dto.CreateUserRequest)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid DTO type")
|
||||
}
|
||||
|
||||
// 基础验证已经由binding标签处理,这里添加业务规则验证
|
||||
if req.Username == "admin" || req.Username == "root" {
|
||||
return fmt.Errorf("username '%s' is reserved", req.Username)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateUpdate 验证更新请求
|
||||
func (s *UserService) ValidateUpdate(ctx context.Context, id string, updateDTO interface{}) error {
|
||||
_, ok := updateDTO.(*dto.UpdateUserRequest)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid DTO type")
|
||||
}
|
||||
|
||||
if id == "" {
|
||||
return fmt.Errorf("user ID is required")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 业务方法
|
||||
|
||||
// Login 用户登录
|
||||
func (s *UserService) Login(ctx context.Context, loginReq *dto.LoginRequest) (*entities.User, error) {
|
||||
// 根据用户名或邮箱查找用户
|
||||
var user *entities.User
|
||||
var err error
|
||||
|
||||
if s.isEmail(loginReq.Login) {
|
||||
user, err = s.repo.FindByEmail(ctx, loginReq.Login)
|
||||
} else {
|
||||
user, err = s.repo.FindByUsername(ctx, loginReq.Login)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid credentials")
|
||||
return nil, fmt.Errorf("用户名或密码错误")
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
if !s.checkPassword(loginReq.Password, user.Password) {
|
||||
return nil, fmt.Errorf("invalid credentials")
|
||||
return nil, fmt.Errorf("用户名或密码错误")
|
||||
}
|
||||
|
||||
// 检查用户状态
|
||||
if !user.CanLogin() {
|
||||
return nil, fmt.Errorf("account is disabled or suspended")
|
||||
}
|
||||
|
||||
// 更新最后登录时间
|
||||
user.UpdateLastLogin()
|
||||
if err := s.repo.Update(ctx, user); err != nil {
|
||||
s.logger.Warn("Failed to update last login time", zap.Error(err))
|
||||
}
|
||||
|
||||
// 发布登录事件
|
||||
// 发布用户登录事件
|
||||
event := events.NewUserLoggedInEvent(
|
||||
user.ID, user.Username,
|
||||
user.ID, user.Phone,
|
||||
s.getClientIP(ctx), s.getUserAgent(ctx),
|
||||
s.getCorrelationID(ctx))
|
||||
if err := s.eventBus.Publish(ctx, event); err != nil {
|
||||
s.logger.Warn("Failed to publish user logged in event", zap.Error(err))
|
||||
s.logger.Warn("发布用户登录事件失败", zap.Error(err))
|
||||
}
|
||||
|
||||
s.logger.Info("User logged in successfully",
|
||||
s.logger.Info("用户密码登录成功",
|
||||
zap.String("user_id", user.ID),
|
||||
zap.String("username", user.Username))
|
||||
zap.String("phone", user.Phone))
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// LoginWithSMS 短信验证码登录
|
||||
func (s *UserService) LoginWithSMS(ctx context.Context, loginReq *dto.LoginWithSMSRequest) (*entities.User, error) {
|
||||
// 验证短信验证码
|
||||
if err := s.smsCodeService.VerifyCode(ctx, loginReq.Phone, loginReq.Code, entities.SMSSceneLogin); err != nil {
|
||||
return nil, fmt.Errorf("验证码验证失败: %w", err)
|
||||
}
|
||||
|
||||
// 根据手机号查找用户
|
||||
user, err := s.repo.FindByPhone(ctx, loginReq.Phone)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("用户不存在")
|
||||
}
|
||||
|
||||
// 发布用户登录事件
|
||||
event := events.NewUserLoggedInEvent(
|
||||
user.ID, user.Phone,
|
||||
s.getClientIP(ctx), s.getUserAgent(ctx),
|
||||
s.getCorrelationID(ctx))
|
||||
if err := s.eventBus.Publish(ctx, event); err != nil {
|
||||
s.logger.Warn("发布用户登录事件失败", zap.Error(err))
|
||||
}
|
||||
|
||||
s.logger.Info("用户短信登录成功",
|
||||
zap.String("user_id", user.ID),
|
||||
zap.String("phone", user.Phone))
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// ChangePassword 修改密码
|
||||
func (s *UserService) ChangePassword(ctx context.Context, userID string, req *dto.ChangePasswordRequest) error {
|
||||
// 获取用户
|
||||
// 验证新密码确认
|
||||
if req.NewPassword != req.ConfirmNewPassword {
|
||||
return fmt.Errorf("新密码和确认新密码不匹配")
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
user, err := s.repo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("user not found: %w", err)
|
||||
return fmt.Errorf("用户不存在: %w", err)
|
||||
}
|
||||
|
||||
// 验证旧密码
|
||||
// 验证短信验证码
|
||||
if err := s.smsCodeService.VerifyCode(ctx, user.Phone, req.Code, entities.SMSSceneChangePassword); err != nil {
|
||||
return fmt.Errorf("验证码验证失败: %w", err)
|
||||
}
|
||||
|
||||
// 验证当前密码
|
||||
if !s.checkPassword(req.OldPassword, user.Password) {
|
||||
return fmt.Errorf("current password is incorrect")
|
||||
return fmt.Errorf("当前密码错误")
|
||||
}
|
||||
|
||||
// 加密新密码
|
||||
// 哈希新密码
|
||||
hashedPassword, err := s.hashPassword(req.NewPassword)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to hash new password: %w", err)
|
||||
return fmt.Errorf("新密码加密失败: %w", err)
|
||||
}
|
||||
|
||||
// 更新密码
|
||||
user.Password = hashedPassword
|
||||
if err := s.repo.Update(ctx, user); err != nil {
|
||||
return fmt.Errorf("failed to update password: %w", err)
|
||||
return fmt.Errorf("密码更新失败: %w", err)
|
||||
}
|
||||
|
||||
// 发布密码修改事件
|
||||
event := events.NewUserPasswordChangedEvent(user.ID, user.Username, s.getCorrelationID(ctx))
|
||||
event := events.NewUserPasswordChangedEvent(user.ID, user.Phone, s.getCorrelationID(ctx))
|
||||
if err := s.eventBus.Publish(ctx, event); err != nil {
|
||||
s.logger.Warn("Failed to publish password changed event", zap.Error(err))
|
||||
s.logger.Warn("发布密码修改事件失败", zap.Error(err))
|
||||
}
|
||||
|
||||
s.logger.Info("Password changed successfully", zap.String("user_id", userID))
|
||||
s.logger.Info("密码修改成功", zap.String("user_id", userID))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetStats 获取用户统计
|
||||
func (s *UserService) GetStats(ctx context.Context) (*dto.UserStatsResponse, error) {
|
||||
total, err := s.repo.Count(ctx, interfaces.CountOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// GetByID 根据ID获取用户
|
||||
func (s *UserService) GetByID(ctx context.Context, id string) (*entities.User, error) {
|
||||
if id == "" {
|
||||
return nil, fmt.Errorf("用户ID不能为空")
|
||||
}
|
||||
|
||||
// 这里可以并行查询不同状态的用户数量
|
||||
// 简化实现,返回基础统计
|
||||
return &dto.UserStatsResponse{
|
||||
TotalUsers: total,
|
||||
ActiveUsers: total, // 简化
|
||||
InactiveUsers: 0,
|
||||
SuspendedUsers: 0,
|
||||
NewUsersToday: 0,
|
||||
NewUsersWeek: 0,
|
||||
NewUsersMonth: 0,
|
||||
}, nil
|
||||
user, err := s.repo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("用户不存在: %w", err)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// 私有方法
|
||||
// 工具方法
|
||||
|
||||
// checkDuplicates 检查重复的用户名和邮箱
|
||||
func (s *UserService) checkDuplicates(ctx context.Context, username, email string) error {
|
||||
// 检查用户名
|
||||
if existingUser, err := s.repo.FindByUsername(ctx, username); err == nil && existingUser != nil {
|
||||
return fmt.Errorf("username already exists")
|
||||
// checkPhoneDuplicate 检查手机号重复
|
||||
func (s *UserService) checkPhoneDuplicate(ctx context.Context, phone string) error {
|
||||
if _, err := s.repo.FindByPhone(ctx, phone); err == nil {
|
||||
return fmt.Errorf("手机号已存在")
|
||||
}
|
||||
|
||||
// 检查邮箱
|
||||
if existingUser, err := s.repo.FindByEmail(ctx, email); err == nil && existingUser != nil {
|
||||
return fmt.Errorf("email already exists")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// hashPassword 加密密码
|
||||
func (s *UserService) hashPassword(password string) (string, error) {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
hashedBytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(hash), nil
|
||||
return string(hashedBytes), nil
|
||||
}
|
||||
|
||||
// checkPassword 验证密码
|
||||
@@ -386,63 +257,24 @@ func (s *UserService) checkPassword(password, hash string) bool {
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// isEmail 检查是否为邮箱格式
|
||||
func (s *UserService) isEmail(str string) bool {
|
||||
return len(str) > 0 && len(str) < 255 &&
|
||||
len(str) > 5 &&
|
||||
str[len(str)-4:] != ".." &&
|
||||
(len(str) > 6 && str[len(str)-4:] == ".com") ||
|
||||
(len(str) > 5 && str[len(str)-3:] == ".cn") ||
|
||||
(len(str) > 6 && str[len(str)-4:] == ".org") ||
|
||||
(len(str) > 6 && str[len(str)-4:] == ".net")
|
||||
// 简化的邮箱检查,实际应该使用正则表达式
|
||||
// isValidPhone 验证手机号格式
|
||||
func (s *UserService) isValidPhone(phone string) bool {
|
||||
// 简单的中国手机号验证(11位数字,以1开头)
|
||||
pattern := `^1[3-9]\d{9}$`
|
||||
matched, _ := regexp.MatchString(pattern, phone)
|
||||
return matched
|
||||
}
|
||||
|
||||
// applyUserUpdates 应用用户更新
|
||||
func (s *UserService) applyUserUpdates(user *entities.User, req *dto.UpdateUserRequest) {
|
||||
if req.FirstName != nil {
|
||||
user.FirstName = *req.FirstName
|
||||
}
|
||||
if req.LastName != nil {
|
||||
user.LastName = *req.LastName
|
||||
}
|
||||
if req.Phone != nil {
|
||||
user.Phone = *req.Phone
|
||||
}
|
||||
if req.Avatar != nil {
|
||||
user.Avatar = *req.Avatar
|
||||
}
|
||||
user.UpdatedAt = time.Now()
|
||||
}
|
||||
|
||||
// captureUserValues 捕获用户值用于变更比较
|
||||
func (s *UserService) captureUserValues(user *entities.User) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"first_name": user.FirstName,
|
||||
"last_name": user.LastName,
|
||||
"phone": user.Phone,
|
||||
"avatar": user.Avatar,
|
||||
}
|
||||
}
|
||||
|
||||
// findChanges 找出变更的字段
|
||||
func (s *UserService) findChanges(oldValues, newValues map[string]interface{}) map[string]interface{} {
|
||||
changes := make(map[string]interface{})
|
||||
|
||||
for key, newValue := range newValues {
|
||||
if oldValue, exists := oldValues[key]; !exists || oldValue != newValue {
|
||||
changes[key] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
return changes
|
||||
// generateUserID 生成用户ID
|
||||
func (s *UserService) generateUserID() string {
|
||||
return uuid.New().String()
|
||||
}
|
||||
|
||||
// getCorrelationID 获取关联ID
|
||||
func (s *UserService) getCorrelationID(ctx context.Context) string {
|
||||
if id := ctx.Value("correlation_id"); id != nil {
|
||||
if correlationID, ok := id.(string); ok {
|
||||
return correlationID
|
||||
if strID, ok := id.(string); ok {
|
||||
return strID
|
||||
}
|
||||
}
|
||||
return uuid.New().String()
|
||||
@@ -451,19 +283,19 @@ func (s *UserService) getCorrelationID(ctx context.Context) string {
|
||||
// getClientIP 获取客户端IP
|
||||
func (s *UserService) getClientIP(ctx context.Context) string {
|
||||
if ip := ctx.Value("client_ip"); ip != nil {
|
||||
if clientIP, ok := ip.(string); ok {
|
||||
return clientIP
|
||||
if strIP, ok := ip.(string); ok {
|
||||
return strIP
|
||||
}
|
||||
}
|
||||
return "unknown"
|
||||
return ""
|
||||
}
|
||||
|
||||
// getUserAgent 获取用户代理
|
||||
func (s *UserService) getUserAgent(ctx context.Context) string {
|
||||
if ua := ctx.Value("user_agent"); ua != nil {
|
||||
if userAgent, ok := ua.(string); ok {
|
||||
return userAgent
|
||||
if strUA, ok := ua.(string); ok {
|
||||
return strUA
|
||||
}
|
||||
}
|
||||
return "unknown"
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ func (h *HealthChecker) RegisterService(service interfaces.Service) {
|
||||
defer h.mutex.Unlock()
|
||||
|
||||
h.services[service.Name()] = service
|
||||
h.logger.Info("Registered service for health check", zap.String("service", service.Name()))
|
||||
h.logger.Info("服务已注册健康检查", zap.String("service", service.Name()))
|
||||
}
|
||||
|
||||
// CheckHealth 检查单个服务健康状态
|
||||
@@ -47,8 +47,8 @@ func (h *HealthChecker) CheckHealth(ctx context.Context, serviceName string) *in
|
||||
h.mutex.RUnlock()
|
||||
return &interfaces.HealthStatus{
|
||||
Status: "DOWN",
|
||||
Message: "Service not found",
|
||||
Details: map[string]interface{}{"error": "service not registered"},
|
||||
Message: "服务未找到",
|
||||
Details: map[string]interface{}{"error": "服务未注册"},
|
||||
CheckedAt: time.Now().Unix(),
|
||||
ResponseTime: 0,
|
||||
}
|
||||
@@ -79,24 +79,24 @@ func (h *HealthChecker) CheckHealth(ctx context.Context, serviceName string) *in
|
||||
|
||||
if err != nil {
|
||||
status.Status = "DOWN"
|
||||
status.Message = "Health check failed"
|
||||
status.Message = "健康检查失败"
|
||||
status.Details = map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"service_name": serviceName,
|
||||
"check_time": start.Format(time.RFC3339),
|
||||
}
|
||||
h.logger.Warn("Service health check failed",
|
||||
h.logger.Warn("服务健康检查失败",
|
||||
zap.String("service", serviceName),
|
||||
zap.Error(err),
|
||||
zap.Int64("response_time_ms", responseTime))
|
||||
} else {
|
||||
status.Status = "UP"
|
||||
status.Message = "Service is healthy"
|
||||
status.Message = "服务运行正常"
|
||||
status.Details = map[string]interface{}{
|
||||
"service_name": serviceName,
|
||||
"check_time": start.Format(time.RFC3339),
|
||||
}
|
||||
h.logger.Debug("Service health check passed",
|
||||
h.logger.Debug("服务健康检查通过",
|
||||
zap.String("service", serviceName),
|
||||
zap.Int64("response_time_ms", responseTime))
|
||||
}
|
||||
@@ -173,13 +173,13 @@ func (h *HealthChecker) GetOverallStatus(ctx context.Context) *interfaces.Health
|
||||
// 确定整体状态
|
||||
if healthyCount == totalCount {
|
||||
overall.Status = "UP"
|
||||
overall.Message = "All services are healthy"
|
||||
overall.Message = "所有服务运行正常"
|
||||
} else if healthyCount == 0 {
|
||||
overall.Status = "DOWN"
|
||||
overall.Message = "All services are down"
|
||||
overall.Message = "所有服务均已下线"
|
||||
} else {
|
||||
overall.Status = "DEGRADED"
|
||||
overall.Message = fmt.Sprintf("%d of %d services are healthy", healthyCount, totalCount)
|
||||
overall.Message = fmt.Sprintf("%d/%d 个服务运行正常", healthyCount, totalCount)
|
||||
}
|
||||
|
||||
return overall
|
||||
@@ -205,7 +205,7 @@ func (h *HealthChecker) RemoveService(serviceName string) {
|
||||
delete(h.services, serviceName)
|
||||
delete(h.cache, serviceName)
|
||||
|
||||
h.logger.Info("Removed service from health check", zap.String("service", serviceName))
|
||||
h.logger.Info("服务已从健康检查中移除", zap.String("service", serviceName))
|
||||
}
|
||||
|
||||
// ClearCache 清除缓存
|
||||
@@ -214,7 +214,7 @@ func (h *HealthChecker) ClearCache() {
|
||||
defer h.mutex.Unlock()
|
||||
|
||||
h.cache = make(map[string]*interfaces.HealthStatus)
|
||||
h.logger.Debug("Health check cache cleared")
|
||||
h.logger.Debug("健康检查缓存已清除")
|
||||
}
|
||||
|
||||
// GetCacheStats 获取缓存统计
|
||||
@@ -243,7 +243,7 @@ func (h *HealthChecker) SetCacheTTL(ttl time.Duration) {
|
||||
defer h.mutex.Unlock()
|
||||
|
||||
h.cacheTTL = ttl
|
||||
h.logger.Info("Updated health check cache TTL", zap.Duration("ttl", ttl))
|
||||
h.logger.Info("健康检查缓存TTL已更新", zap.Duration("ttl", ttl))
|
||||
}
|
||||
|
||||
// StartPeriodicCheck 启动定期健康检查
|
||||
@@ -251,12 +251,12 @@ func (h *HealthChecker) StartPeriodicCheck(ctx context.Context, interval time.Du
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
h.logger.Info("Started periodic health check", zap.Duration("interval", interval))
|
||||
h.logger.Info("已启动定期健康检查", zap.Duration("interval", interval))
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
h.logger.Info("Stopped periodic health check")
|
||||
h.logger.Info("已停止定期健康检查")
|
||||
return
|
||||
case <-ticker.C:
|
||||
h.performPeriodicCheck(ctx)
|
||||
@@ -268,14 +268,14 @@ func (h *HealthChecker) StartPeriodicCheck(ctx context.Context, interval time.Du
|
||||
func (h *HealthChecker) performPeriodicCheck(ctx context.Context) {
|
||||
overall := h.GetOverallStatus(ctx)
|
||||
|
||||
h.logger.Info("Periodic health check completed",
|
||||
h.logger.Info("定期健康检查已完成",
|
||||
zap.String("overall_status", overall.Status),
|
||||
zap.String("message", overall.Message),
|
||||
zap.Int64("response_time_ms", overall.ResponseTime))
|
||||
|
||||
// 如果有服务下线,记录警告
|
||||
if overall.Status != "UP" {
|
||||
h.logger.Warn("Some services are not healthy",
|
||||
h.logger.Warn("部分服务不健康",
|
||||
zap.String("status", overall.Status),
|
||||
zap.Any("details", overall.Details))
|
||||
}
|
||||
|
||||
587
internal/shared/hooks/hook_system.go
Normal file
587
internal/shared/hooks/hook_system.go
Normal file
@@ -0,0 +1,587 @@
|
||||
package hooks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// HookPriority 钩子优先级
|
||||
type HookPriority int
|
||||
|
||||
const (
|
||||
// PriorityLowest 最低优先级
|
||||
PriorityLowest HookPriority = 0
|
||||
// PriorityLow 低优先级
|
||||
PriorityLow HookPriority = 25
|
||||
// PriorityNormal 普通优先级
|
||||
PriorityNormal HookPriority = 50
|
||||
// PriorityHigh 高优先级
|
||||
PriorityHigh HookPriority = 75
|
||||
// PriorityHighest 最高优先级
|
||||
PriorityHighest HookPriority = 100
|
||||
)
|
||||
|
||||
// HookFunc 钩子函数类型
|
||||
type HookFunc func(ctx context.Context, data interface{}) error
|
||||
|
||||
// Hook 钩子定义
|
||||
type Hook struct {
|
||||
Name string
|
||||
Func HookFunc
|
||||
Priority HookPriority
|
||||
Async bool
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
// HookResult 钩子执行结果
|
||||
type HookResult struct {
|
||||
HookName string `json:"hook_name"`
|
||||
Success bool `json:"success"`
|
||||
Duration time.Duration `json:"duration"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// HookConfig 钩子配置
|
||||
type HookConfig struct {
|
||||
// 默认超时时间
|
||||
DefaultTimeout time.Duration
|
||||
// 是否记录执行时间
|
||||
TrackDuration bool
|
||||
// 错误处理策略
|
||||
ErrorStrategy ErrorStrategy
|
||||
}
|
||||
|
||||
// ErrorStrategy 错误处理策略
|
||||
type ErrorStrategy int
|
||||
|
||||
const (
|
||||
// ContinueOnError 遇到错误继续执行
|
||||
ContinueOnError ErrorStrategy = iota
|
||||
// StopOnError 遇到错误停止执行
|
||||
StopOnError
|
||||
// CollectErrors 收集所有错误
|
||||
CollectErrors
|
||||
)
|
||||
|
||||
// DefaultHookConfig 默认钩子配置
|
||||
func DefaultHookConfig() HookConfig {
|
||||
return HookConfig{
|
||||
DefaultTimeout: 30 * time.Second,
|
||||
TrackDuration: true,
|
||||
ErrorStrategy: ContinueOnError,
|
||||
}
|
||||
}
|
||||
|
||||
// HookSystem 钩子系统
|
||||
type HookSystem struct {
|
||||
hooks map[string][]*Hook
|
||||
config HookConfig
|
||||
logger *zap.Logger
|
||||
mutex sync.RWMutex
|
||||
stats map[string]*HookStats
|
||||
}
|
||||
|
||||
// HookStats 钩子统计
|
||||
type HookStats struct {
|
||||
TotalExecutions int `json:"total_executions"`
|
||||
Successes int `json:"successes"`
|
||||
Failures int `json:"failures"`
|
||||
TotalDuration time.Duration `json:"total_duration"`
|
||||
AverageDuration time.Duration `json:"average_duration"`
|
||||
LastExecution time.Time `json:"last_execution"`
|
||||
LastError string `json:"last_error,omitempty"`
|
||||
}
|
||||
|
||||
// NewHookSystem 创建钩子系统
|
||||
func NewHookSystem(config HookConfig, logger *zap.Logger) *HookSystem {
|
||||
return &HookSystem{
|
||||
hooks: make(map[string][]*Hook),
|
||||
config: config,
|
||||
logger: logger,
|
||||
stats: make(map[string]*HookStats),
|
||||
}
|
||||
}
|
||||
|
||||
// Register 注册钩子
|
||||
func (hs *HookSystem) Register(event string, hook *Hook) error {
|
||||
if hook.Name == "" {
|
||||
return fmt.Errorf("hook name cannot be empty")
|
||||
}
|
||||
|
||||
if hook.Func == nil {
|
||||
return fmt.Errorf("hook function cannot be nil")
|
||||
}
|
||||
|
||||
if hook.Timeout == 0 {
|
||||
hook.Timeout = hs.config.DefaultTimeout
|
||||
}
|
||||
|
||||
hs.mutex.Lock()
|
||||
defer hs.mutex.Unlock()
|
||||
|
||||
// 检查是否已经注册了同名钩子
|
||||
for _, existingHook := range hs.hooks[event] {
|
||||
if existingHook.Name == hook.Name {
|
||||
return fmt.Errorf("hook %s already registered for event %s", hook.Name, event)
|
||||
}
|
||||
}
|
||||
|
||||
hs.hooks[event] = append(hs.hooks[event], hook)
|
||||
|
||||
// 按优先级排序
|
||||
sort.Slice(hs.hooks[event], func(i, j int) bool {
|
||||
return hs.hooks[event][i].Priority > hs.hooks[event][j].Priority
|
||||
})
|
||||
|
||||
// 初始化统计
|
||||
hookKey := fmt.Sprintf("%s.%s", event, hook.Name)
|
||||
hs.stats[hookKey] = &HookStats{}
|
||||
|
||||
hs.logger.Info("Registered hook",
|
||||
zap.String("event", event),
|
||||
zap.String("hook_name", hook.Name),
|
||||
zap.Int("priority", int(hook.Priority)),
|
||||
zap.Bool("async", hook.Async))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterFunc 注册钩子函数(简化版)
|
||||
func (hs *HookSystem) RegisterFunc(event, name string, priority HookPriority, fn HookFunc) error {
|
||||
hook := &Hook{
|
||||
Name: name,
|
||||
Func: fn,
|
||||
Priority: priority,
|
||||
Async: false,
|
||||
Timeout: hs.config.DefaultTimeout,
|
||||
}
|
||||
|
||||
return hs.Register(event, hook)
|
||||
}
|
||||
|
||||
// RegisterAsyncFunc 注册异步钩子函数
|
||||
func (hs *HookSystem) RegisterAsyncFunc(event, name string, priority HookPriority, fn HookFunc) error {
|
||||
hook := &Hook{
|
||||
Name: name,
|
||||
Func: fn,
|
||||
Priority: priority,
|
||||
Async: true,
|
||||
Timeout: hs.config.DefaultTimeout,
|
||||
}
|
||||
|
||||
return hs.Register(event, hook)
|
||||
}
|
||||
|
||||
// Unregister 取消注册钩子
|
||||
func (hs *HookSystem) Unregister(event, hookName string) error {
|
||||
hs.mutex.Lock()
|
||||
defer hs.mutex.Unlock()
|
||||
|
||||
hooks := hs.hooks[event]
|
||||
for i, hook := range hooks {
|
||||
if hook.Name == hookName {
|
||||
// 删除钩子
|
||||
hs.hooks[event] = append(hooks[:i], hooks[i+1:]...)
|
||||
|
||||
// 删除统计
|
||||
hookKey := fmt.Sprintf("%s.%s", event, hookName)
|
||||
delete(hs.stats, hookKey)
|
||||
|
||||
hs.logger.Info("Unregistered hook",
|
||||
zap.String("event", event),
|
||||
zap.String("hook_name", hookName))
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("hook %s not found for event %s", hookName, event)
|
||||
}
|
||||
|
||||
// Trigger 触发事件
|
||||
func (hs *HookSystem) Trigger(ctx context.Context, event string, data interface{}) ([]HookResult, error) {
|
||||
hs.mutex.RLock()
|
||||
hooks := make([]*Hook, len(hs.hooks[event]))
|
||||
copy(hooks, hs.hooks[event])
|
||||
hs.mutex.RUnlock()
|
||||
|
||||
if len(hooks) == 0 {
|
||||
hs.logger.Debug("No hooks registered for event", zap.String("event", event))
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
hs.logger.Debug("Triggering event",
|
||||
zap.String("event", event),
|
||||
zap.Int("hook_count", len(hooks)))
|
||||
|
||||
results := make([]HookResult, 0, len(hooks))
|
||||
var errors []error
|
||||
|
||||
for _, hook := range hooks {
|
||||
result := hs.executeHook(ctx, event, hook, data)
|
||||
results = append(results, result)
|
||||
|
||||
if !result.Success {
|
||||
err := fmt.Errorf("hook %s failed: %s", hook.Name, result.Error)
|
||||
errors = append(errors, err)
|
||||
|
||||
// 根据错误策略决定是否继续
|
||||
if hs.config.ErrorStrategy == StopOnError {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理错误
|
||||
if len(errors) > 0 {
|
||||
switch hs.config.ErrorStrategy {
|
||||
case StopOnError:
|
||||
return results, errors[0]
|
||||
case CollectErrors:
|
||||
return results, fmt.Errorf("multiple hook errors: %v", errors)
|
||||
case ContinueOnError:
|
||||
// 继续执行,但记录错误
|
||||
hs.logger.Warn("Some hooks failed but continuing execution",
|
||||
zap.String("event", event),
|
||||
zap.Int("error_count", len(errors)))
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// executeHook 执行单个钩子
|
||||
func (hs *HookSystem) executeHook(ctx context.Context, event string, hook *Hook, data interface{}) HookResult {
|
||||
hookKey := fmt.Sprintf("%s.%s", event, hook.Name)
|
||||
start := time.Now()
|
||||
|
||||
result := HookResult{
|
||||
HookName: hook.Name,
|
||||
Success: false,
|
||||
}
|
||||
|
||||
// 更新统计
|
||||
defer func() {
|
||||
result.Duration = time.Since(start)
|
||||
hs.updateStats(hookKey, result)
|
||||
}()
|
||||
|
||||
if hook.Async {
|
||||
// 异步执行
|
||||
go func() {
|
||||
hs.doExecuteHook(ctx, hook, data)
|
||||
}()
|
||||
result.Success = true // 异步执行总是认为成功
|
||||
return result
|
||||
}
|
||||
|
||||
// 同步执行
|
||||
err := hs.doExecuteHook(ctx, hook, data)
|
||||
if err != nil {
|
||||
result.Error = err.Error()
|
||||
hs.logger.Error("Hook execution failed",
|
||||
zap.String("event", event),
|
||||
zap.String("hook_name", hook.Name),
|
||||
zap.Error(err))
|
||||
} else {
|
||||
result.Success = true
|
||||
hs.logger.Debug("Hook executed successfully",
|
||||
zap.String("event", event),
|
||||
zap.String("hook_name", hook.Name))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// doExecuteHook 实际执行钩子
|
||||
func (hs *HookSystem) doExecuteHook(ctx context.Context, hook *Hook, data interface{}) error {
|
||||
// 设置超时上下文
|
||||
hookCtx, cancel := context.WithTimeout(ctx, hook.Timeout)
|
||||
defer cancel()
|
||||
|
||||
// 在goroutine中执行,以便处理超时
|
||||
errChan := make(chan error, 1)
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
errChan <- fmt.Errorf("hook panicked: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
errChan <- hook.Func(hookCtx, data)
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-errChan:
|
||||
return err
|
||||
case <-hookCtx.Done():
|
||||
return fmt.Errorf("hook execution timeout after %v", hook.Timeout)
|
||||
}
|
||||
}
|
||||
|
||||
// updateStats 更新统计信息
|
||||
func (hs *HookSystem) updateStats(hookKey string, result HookResult) {
|
||||
hs.mutex.Lock()
|
||||
defer hs.mutex.Unlock()
|
||||
|
||||
stats, exists := hs.stats[hookKey]
|
||||
if !exists {
|
||||
stats = &HookStats{}
|
||||
hs.stats[hookKey] = stats
|
||||
}
|
||||
|
||||
stats.TotalExecutions++
|
||||
stats.LastExecution = time.Now()
|
||||
|
||||
if result.Success {
|
||||
stats.Successes++
|
||||
} else {
|
||||
stats.Failures++
|
||||
stats.LastError = result.Error
|
||||
}
|
||||
|
||||
if hs.config.TrackDuration {
|
||||
stats.TotalDuration += result.Duration
|
||||
stats.AverageDuration = stats.TotalDuration / time.Duration(stats.TotalExecutions)
|
||||
}
|
||||
}
|
||||
|
||||
// GetHooks 获取事件的所有钩子
|
||||
func (hs *HookSystem) GetHooks(event string) []*Hook {
|
||||
hs.mutex.RLock()
|
||||
defer hs.mutex.RUnlock()
|
||||
|
||||
hooks := make([]*Hook, len(hs.hooks[event]))
|
||||
copy(hooks, hs.hooks[event])
|
||||
return hooks
|
||||
}
|
||||
|
||||
// GetEvents 获取所有注册的事件
|
||||
func (hs *HookSystem) GetEvents() []string {
|
||||
hs.mutex.RLock()
|
||||
defer hs.mutex.RUnlock()
|
||||
|
||||
events := make([]string, 0, len(hs.hooks))
|
||||
for event := range hs.hooks {
|
||||
events = append(events, event)
|
||||
}
|
||||
|
||||
sort.Strings(events)
|
||||
return events
|
||||
}
|
||||
|
||||
// GetStats 获取钩子统计信息
|
||||
func (hs *HookSystem) GetStats() map[string]*HookStats {
|
||||
hs.mutex.RLock()
|
||||
defer hs.mutex.RUnlock()
|
||||
|
||||
stats := make(map[string]*HookStats)
|
||||
for key, stat := range hs.stats {
|
||||
statCopy := *stat
|
||||
stats[key] = &statCopy
|
||||
}
|
||||
|
||||
return stats
|
||||
}
|
||||
|
||||
// GetEventStats 获取特定事件的统计信息
|
||||
func (hs *HookSystem) GetEventStats(event string) map[string]*HookStats {
|
||||
allStats := hs.GetStats()
|
||||
eventStats := make(map[string]*HookStats)
|
||||
|
||||
prefix := event + "."
|
||||
for key, stat := range allStats {
|
||||
if len(key) > len(prefix) && key[:len(prefix)] == prefix {
|
||||
hookName := key[len(prefix):]
|
||||
eventStats[hookName] = stat
|
||||
}
|
||||
}
|
||||
|
||||
return eventStats
|
||||
}
|
||||
|
||||
// Clear 清除所有钩子
|
||||
func (hs *HookSystem) Clear() {
|
||||
hs.mutex.Lock()
|
||||
defer hs.mutex.Unlock()
|
||||
|
||||
hs.hooks = make(map[string][]*Hook)
|
||||
hs.stats = make(map[string]*HookStats)
|
||||
|
||||
hs.logger.Info("Cleared all hooks")
|
||||
}
|
||||
|
||||
// ClearEvent 清除特定事件的所有钩子
|
||||
func (hs *HookSystem) ClearEvent(event string) {
|
||||
hs.mutex.Lock()
|
||||
defer hs.mutex.Unlock()
|
||||
|
||||
// 删除钩子
|
||||
delete(hs.hooks, event)
|
||||
|
||||
// 删除统计
|
||||
prefix := event + "."
|
||||
for key := range hs.stats {
|
||||
if len(key) > len(prefix) && key[:len(prefix)] == prefix {
|
||||
delete(hs.stats, key)
|
||||
}
|
||||
}
|
||||
|
||||
hs.logger.Info("Cleared hooks for event", zap.String("event", event))
|
||||
}
|
||||
|
||||
// Count 获取钩子总数
|
||||
func (hs *HookSystem) Count() int {
|
||||
hs.mutex.RLock()
|
||||
defer hs.mutex.RUnlock()
|
||||
|
||||
total := 0
|
||||
for _, hooks := range hs.hooks {
|
||||
total += len(hooks)
|
||||
}
|
||||
|
||||
return total
|
||||
}
|
||||
|
||||
// EventCount 获取特定事件的钩子数量
|
||||
func (hs *HookSystem) EventCount(event string) int {
|
||||
hs.mutex.RLock()
|
||||
defer hs.mutex.RUnlock()
|
||||
|
||||
return len(hs.hooks[event])
|
||||
}
|
||||
|
||||
// 实现Service接口
|
||||
|
||||
// Name 返回服务名称
|
||||
func (hs *HookSystem) Name() string {
|
||||
return "hook-system"
|
||||
}
|
||||
|
||||
// Initialize 初始化钩子系统
|
||||
func (hs *HookSystem) Initialize(ctx context.Context) error {
|
||||
hs.logger.Info("Hook system initialized")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start 启动钩子系统
|
||||
func (hs *HookSystem) Start(ctx context.Context) error {
|
||||
hs.logger.Info("Hook system started")
|
||||
return nil
|
||||
}
|
||||
|
||||
// HealthCheck 健康检查
|
||||
func (hs *HookSystem) HealthCheck(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Shutdown 关闭钩子系统
|
||||
func (hs *HookSystem) Shutdown(ctx context.Context) error {
|
||||
hs.logger.Info("Hook system shutdown")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 便捷方法
|
||||
|
||||
// OnUserCreated 用户创建事件钩子
|
||||
func (hs *HookSystem) OnUserCreated(name string, priority HookPriority, fn HookFunc) error {
|
||||
return hs.RegisterFunc("user.created", name, priority, fn)
|
||||
}
|
||||
|
||||
// OnUserUpdated 用户更新事件钩子
|
||||
func (hs *HookSystem) OnUserUpdated(name string, priority HookPriority, fn HookFunc) error {
|
||||
return hs.RegisterFunc("user.updated", name, priority, fn)
|
||||
}
|
||||
|
||||
// OnUserDeleted 用户删除事件钩子
|
||||
func (hs *HookSystem) OnUserDeleted(name string, priority HookPriority, fn HookFunc) error {
|
||||
return hs.RegisterFunc("user.deleted", name, priority, fn)
|
||||
}
|
||||
|
||||
// OnOrderCreated 订单创建事件钩子
|
||||
func (hs *HookSystem) OnOrderCreated(name string, priority HookPriority, fn HookFunc) error {
|
||||
return hs.RegisterFunc("order.created", name, priority, fn)
|
||||
}
|
||||
|
||||
// OnOrderCompleted 订单完成事件钩子
|
||||
func (hs *HookSystem) OnOrderCompleted(name string, priority HookPriority, fn HookFunc) error {
|
||||
return hs.RegisterFunc("order.completed", name, priority, fn)
|
||||
}
|
||||
|
||||
// TriggerUserCreated 触发用户创建事件
|
||||
func (hs *HookSystem) TriggerUserCreated(ctx context.Context, user interface{}) ([]HookResult, error) {
|
||||
return hs.Trigger(ctx, "user.created", user)
|
||||
}
|
||||
|
||||
// TriggerUserUpdated 触发用户更新事件
|
||||
func (hs *HookSystem) TriggerUserUpdated(ctx context.Context, user interface{}) ([]HookResult, error) {
|
||||
return hs.Trigger(ctx, "user.updated", user)
|
||||
}
|
||||
|
||||
// TriggerUserDeleted 触发用户删除事件
|
||||
func (hs *HookSystem) TriggerUserDeleted(ctx context.Context, user interface{}) ([]HookResult, error) {
|
||||
return hs.Trigger(ctx, "user.deleted", user)
|
||||
}
|
||||
|
||||
// HookBuilder 钩子构建器
|
||||
type HookBuilder struct {
|
||||
hook *Hook
|
||||
}
|
||||
|
||||
// NewHookBuilder 创建钩子构建器
|
||||
func NewHookBuilder(name string, fn HookFunc) *HookBuilder {
|
||||
return &HookBuilder{
|
||||
hook: &Hook{
|
||||
Name: name,
|
||||
Func: fn,
|
||||
Priority: PriorityNormal,
|
||||
Async: false,
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// WithPriority 设置优先级
|
||||
func (hb *HookBuilder) WithPriority(priority HookPriority) *HookBuilder {
|
||||
hb.hook.Priority = priority
|
||||
return hb
|
||||
}
|
||||
|
||||
// WithTimeout 设置超时时间
|
||||
func (hb *HookBuilder) WithTimeout(timeout time.Duration) *HookBuilder {
|
||||
hb.hook.Timeout = timeout
|
||||
return hb
|
||||
}
|
||||
|
||||
// Async 设置为异步执行
|
||||
func (hb *HookBuilder) Async() *HookBuilder {
|
||||
hb.hook.Async = true
|
||||
return hb
|
||||
}
|
||||
|
||||
// Build 构建钩子
|
||||
func (hb *HookBuilder) Build() *Hook {
|
||||
return hb.hook
|
||||
}
|
||||
|
||||
// TypedHookFunc 类型化钩子函数
|
||||
type TypedHookFunc[T any] func(ctx context.Context, data T) error
|
||||
|
||||
// RegisterTypedFunc 注册类型化钩子函数
|
||||
func RegisterTypedFunc[T any](hs *HookSystem, event, name string, priority HookPriority, fn TypedHookFunc[T]) error {
|
||||
hookFunc := func(ctx context.Context, data interface{}) error {
|
||||
typedData, ok := data.(T)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid data type for hook %s, expected %s", name, reflect.TypeOf((*T)(nil)).Elem().Name())
|
||||
}
|
||||
return fn(ctx, typedData)
|
||||
}
|
||||
|
||||
return hs.RegisterFunc(event, name, priority, hookFunc)
|
||||
}
|
||||
@@ -20,7 +20,7 @@ func NewResponseBuilder() interfaces.ResponseBuilder {
|
||||
|
||||
// Success 成功响应
|
||||
func (r *ResponseBuilder) Success(c *gin.Context, data interface{}, message ...string) {
|
||||
msg := "Success"
|
||||
msg := "操作成功"
|
||||
if len(message) > 0 && message[0] != "" {
|
||||
msg = message[0]
|
||||
}
|
||||
@@ -38,7 +38,7 @@ func (r *ResponseBuilder) Success(c *gin.Context, data interface{}, message ...s
|
||||
|
||||
// Created 创建成功响应
|
||||
func (r *ResponseBuilder) Created(c *gin.Context, data interface{}, message ...string) {
|
||||
msg := "Created successfully"
|
||||
msg := "创建成功"
|
||||
if len(message) > 0 && message[0] != "" {
|
||||
msg = message[0]
|
||||
}
|
||||
@@ -58,7 +58,7 @@ func (r *ResponseBuilder) Created(c *gin.Context, data interface{}, message ...s
|
||||
func (r *ResponseBuilder) Error(c *gin.Context, err error) {
|
||||
// 根据错误类型确定状态码
|
||||
statusCode := http.StatusInternalServerError
|
||||
message := "Internal server error"
|
||||
message := "服务器内部错误"
|
||||
errorDetail := err.Error()
|
||||
|
||||
// 这里可以根据不同的错误类型设置不同的状态码
|
||||
@@ -93,7 +93,7 @@ func (r *ResponseBuilder) BadRequest(c *gin.Context, message string, errors ...i
|
||||
|
||||
// Unauthorized 401错误响应
|
||||
func (r *ResponseBuilder) Unauthorized(c *gin.Context, message ...string) {
|
||||
msg := "Unauthorized"
|
||||
msg := "用户未登录或认证已过期"
|
||||
if len(message) > 0 && message[0] != "" {
|
||||
msg = message[0]
|
||||
}
|
||||
@@ -110,7 +110,7 @@ func (r *ResponseBuilder) Unauthorized(c *gin.Context, message ...string) {
|
||||
|
||||
// Forbidden 403错误响应
|
||||
func (r *ResponseBuilder) Forbidden(c *gin.Context, message ...string) {
|
||||
msg := "Forbidden"
|
||||
msg := "权限不足,无法访问此资源"
|
||||
if len(message) > 0 && message[0] != "" {
|
||||
msg = message[0]
|
||||
}
|
||||
@@ -127,7 +127,7 @@ func (r *ResponseBuilder) Forbidden(c *gin.Context, message ...string) {
|
||||
|
||||
// NotFound 404错误响应
|
||||
func (r *ResponseBuilder) NotFound(c *gin.Context, message ...string) {
|
||||
msg := "Resource not found"
|
||||
msg := "请求的资源不存在"
|
||||
if len(message) > 0 && message[0] != "" {
|
||||
msg = message[0]
|
||||
}
|
||||
@@ -156,7 +156,7 @@ func (r *ResponseBuilder) Conflict(c *gin.Context, message string) {
|
||||
|
||||
// InternalError 500错误响应
|
||||
func (r *ResponseBuilder) InternalError(c *gin.Context, message ...string) {
|
||||
msg := "Internal server error"
|
||||
msg := "服务器内部错误"
|
||||
if len(message) > 0 && message[0] != "" {
|
||||
msg = message[0]
|
||||
}
|
||||
@@ -175,7 +175,7 @@ func (r *ResponseBuilder) InternalError(c *gin.Context, message ...string) {
|
||||
func (r *ResponseBuilder) Paginated(c *gin.Context, data interface{}, pagination interfaces.PaginationMeta) {
|
||||
response := interfaces.APIResponse{
|
||||
Success: true,
|
||||
Message: "Success",
|
||||
Message: "查询成功",
|
||||
Data: data,
|
||||
Pagination: &pagination,
|
||||
RequestID: r.getRequestID(c),
|
||||
@@ -215,9 +215,35 @@ func BuildPagination(page, pageSize int, total int64) interfaces.PaginationMeta
|
||||
|
||||
// CustomResponse 自定义响应
|
||||
func (r *ResponseBuilder) CustomResponse(c *gin.Context, statusCode int, data interface{}) {
|
||||
var message string
|
||||
switch statusCode {
|
||||
case http.StatusOK:
|
||||
message = "请求成功"
|
||||
case http.StatusCreated:
|
||||
message = "创建成功"
|
||||
case http.StatusNoContent:
|
||||
message = "无内容"
|
||||
case http.StatusBadRequest:
|
||||
message = "请求参数错误"
|
||||
case http.StatusUnauthorized:
|
||||
message = "认证失败"
|
||||
case http.StatusForbidden:
|
||||
message = "权限不足"
|
||||
case http.StatusNotFound:
|
||||
message = "资源不存在"
|
||||
case http.StatusConflict:
|
||||
message = "资源冲突"
|
||||
case http.StatusTooManyRequests:
|
||||
message = "请求过于频繁"
|
||||
case http.StatusInternalServerError:
|
||||
message = "服务器内部错误"
|
||||
default:
|
||||
message = "未知状态"
|
||||
}
|
||||
|
||||
response := interfaces.APIResponse{
|
||||
Success: statusCode >= 200 && statusCode < 300,
|
||||
Message: http.StatusText(statusCode),
|
||||
Message: message,
|
||||
Data: data,
|
||||
RequestID: r.getRequestID(c),
|
||||
Timestamp: time.Now().Unix(),
|
||||
@@ -230,7 +256,7 @@ func (r *ResponseBuilder) CustomResponse(c *gin.Context, statusCode int, data in
|
||||
func (r *ResponseBuilder) ValidationError(c *gin.Context, errors interface{}) {
|
||||
response := interfaces.APIResponse{
|
||||
Success: false,
|
||||
Message: "Validation failed",
|
||||
Message: "请求参数验证失败",
|
||||
Errors: errors,
|
||||
RequestID: r.getRequestID(c),
|
||||
Timestamp: time.Now().Unix(),
|
||||
@@ -241,7 +267,7 @@ func (r *ResponseBuilder) ValidationError(c *gin.Context, errors interface{}) {
|
||||
|
||||
// TooManyRequests 限流错误响应
|
||||
func (r *ResponseBuilder) TooManyRequests(c *gin.Context, message ...string) {
|
||||
msg := "Too many requests"
|
||||
msg := "请求过于频繁,请稍后再试"
|
||||
if len(message) > 0 && message[0] != "" {
|
||||
msg = message[0]
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
swaggerFiles "github.com/swaggo/files"
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/config"
|
||||
@@ -51,7 +53,7 @@ func (r *GinRouter) RegisterHandler(handler interfaces.HTTPHandler) error {
|
||||
// 注册路由
|
||||
r.engine.Handle(handler.GetMethod(), handler.GetPath(), append(middlewares, handler.Handle)...)
|
||||
|
||||
r.logger.Info("Registered HTTP handler",
|
||||
r.logger.Info("已注册HTTP处理器",
|
||||
zap.String("method", handler.GetMethod()),
|
||||
zap.String("path", handler.GetPath()))
|
||||
|
||||
@@ -62,7 +64,7 @@ func (r *GinRouter) RegisterHandler(handler interfaces.HTTPHandler) error {
|
||||
func (r *GinRouter) RegisterMiddleware(middleware interfaces.Middleware) error {
|
||||
r.middlewares = append(r.middlewares, middleware)
|
||||
|
||||
r.logger.Info("Registered middleware",
|
||||
r.logger.Info("已注册中间件",
|
||||
zap.String("name", middleware.GetName()),
|
||||
zap.Int("priority", middleware.GetPriority()))
|
||||
|
||||
@@ -93,7 +95,7 @@ func (r *GinRouter) Start(addr string) error {
|
||||
IdleTimeout: r.config.Server.IdleTimeout,
|
||||
}
|
||||
|
||||
r.logger.Info("Starting HTTP server", zap.String("addr", addr))
|
||||
r.logger.Info("正在启动HTTP服务器", zap.String("addr", addr))
|
||||
|
||||
// 启动服务器
|
||||
if err := r.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
@@ -109,15 +111,15 @@ func (r *GinRouter) Stop(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
r.logger.Info("Stopping HTTP server...")
|
||||
r.logger.Info("正在关闭HTTP服务器...")
|
||||
|
||||
// 优雅关闭服务器
|
||||
if err := r.server.Shutdown(ctx); err != nil {
|
||||
r.logger.Error("Failed to shutdown server gracefully", zap.Error(err))
|
||||
r.logger.Error("优雅关闭服务器失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
r.logger.Info("HTTP server stopped")
|
||||
r.logger.Info("HTTP服务器已关闭")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -137,7 +139,7 @@ func (r *GinRouter) applyMiddlewares() {
|
||||
for _, middleware := range r.middlewares {
|
||||
if middleware.IsGlobal() {
|
||||
r.engine.Use(middleware.Handle())
|
||||
r.logger.Debug("Applied global middleware",
|
||||
r.logger.Debug("已应用全局中间件",
|
||||
zap.String("name", middleware.GetName()),
|
||||
zap.Int("priority", middleware.GetPriority()))
|
||||
}
|
||||
@@ -156,6 +158,18 @@ func (r *GinRouter) SetupDefaultRoutes() {
|
||||
})
|
||||
})
|
||||
|
||||
// 详细健康检查
|
||||
r.engine.GET("/health/detailed", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "healthy",
|
||||
"timestamp": time.Now().Unix(),
|
||||
"service": r.config.App.Name,
|
||||
"version": r.config.App.Version,
|
||||
"uptime": time.Now().Unix(),
|
||||
"environment": r.config.App.Env,
|
||||
})
|
||||
})
|
||||
|
||||
// API信息
|
||||
r.engine.GET("/info", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
@@ -166,11 +180,37 @@ func (r *GinRouter) SetupDefaultRoutes() {
|
||||
})
|
||||
})
|
||||
|
||||
// Swagger文档路由 (仅在开发环境启用)
|
||||
if !r.config.App.IsProduction() {
|
||||
// Swagger UI
|
||||
r.engine.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
||||
|
||||
// API文档重定向
|
||||
r.engine.GET("/docs", func(c *gin.Context) {
|
||||
c.Redirect(http.StatusMovedPermanently, "/swagger/index.html")
|
||||
})
|
||||
|
||||
// API文档信息
|
||||
r.engine.GET("/api/docs", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"swagger_ui": fmt.Sprintf("http://%s/swagger/index.html", c.Request.Host),
|
||||
"openapi_json": fmt.Sprintf("http://%s/swagger/doc.json", c.Request.Host),
|
||||
"redoc": fmt.Sprintf("http://%s/redoc", c.Request.Host),
|
||||
"message": "API文档已可用",
|
||||
})
|
||||
})
|
||||
|
||||
r.logger.Info("Swagger documentation enabled",
|
||||
zap.String("swagger_url", "/swagger/index.html"),
|
||||
zap.String("docs_url", "/docs"),
|
||||
zap.String("api_docs_url", "/api/docs"))
|
||||
}
|
||||
|
||||
// 404处理
|
||||
r.engine.NoRoute(func(c *gin.Context) {
|
||||
c.JSON(http.StatusNotFound, gin.H{
|
||||
"success": false,
|
||||
"message": "Route not found",
|
||||
"message": "路由未找到",
|
||||
"path": c.Request.URL.Path,
|
||||
"method": c.Request.Method,
|
||||
"timestamp": time.Now().Unix(),
|
||||
@@ -181,7 +221,7 @@ func (r *GinRouter) SetupDefaultRoutes() {
|
||||
r.engine.NoMethod(func(c *gin.Context) {
|
||||
c.JSON(http.StatusMethodNotAllowed, gin.H{
|
||||
"success": false,
|
||||
"message": "Method not allowed",
|
||||
"message": "请求方法不允许",
|
||||
"path": c.Request.URL.Path,
|
||||
"method": c.Request.Method,
|
||||
"timestamp": time.Now().Unix(),
|
||||
|
||||
@@ -42,13 +42,13 @@ func (v *RequestValidator) Validate(c *gin.Context, dto interface{}) error {
|
||||
// ValidateQuery 验证查询参数
|
||||
func (v *RequestValidator) ValidateQuery(c *gin.Context, dto interface{}) error {
|
||||
if err := c.ShouldBindQuery(dto); err != nil {
|
||||
v.response.BadRequest(c, "Invalid query parameters", err.Error())
|
||||
v.response.BadRequest(c, "查询参数格式错误", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
if err := v.validator.Struct(dto); err != nil {
|
||||
validationErrors := v.formatValidationErrors(err)
|
||||
v.response.BadRequest(c, "Validation failed", validationErrors)
|
||||
v.response.ValidationError(c, validationErrors)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -57,13 +57,13 @@ func (v *RequestValidator) ValidateQuery(c *gin.Context, dto interface{}) error
|
||||
// ValidateParam 验证路径参数
|
||||
func (v *RequestValidator) ValidateParam(c *gin.Context, dto interface{}) error {
|
||||
if err := c.ShouldBindUri(dto); err != nil {
|
||||
v.response.BadRequest(c, "Invalid path parameters", err.Error())
|
||||
v.response.BadRequest(c, "路径参数格式错误", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
if err := v.validator.Struct(dto); err != nil {
|
||||
validationErrors := v.formatValidationErrors(err)
|
||||
v.response.BadRequest(c, "Validation failed", validationErrors)
|
||||
v.response.ValidationError(c, validationErrors)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -73,7 +73,7 @@ func (v *RequestValidator) ValidateParam(c *gin.Context, dto interface{}) error
|
||||
func (v *RequestValidator) BindAndValidate(c *gin.Context, dto interface{}) error {
|
||||
// 绑定请求体
|
||||
if err := c.ShouldBindJSON(dto); err != nil {
|
||||
v.response.BadRequest(c, "Invalid request body", err.Error())
|
||||
v.response.BadRequest(c, "请求体格式错误", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -115,44 +115,74 @@ func (v *RequestValidator) getErrorMessage(fieldError validator.FieldError) stri
|
||||
tag := fieldError.Tag()
|
||||
param := fieldError.Param()
|
||||
|
||||
fieldDisplayName := v.getFieldDisplayName(field)
|
||||
|
||||
switch tag {
|
||||
case "required":
|
||||
return fmt.Sprintf("%s is required", field)
|
||||
return fmt.Sprintf("%s 不能为空", fieldDisplayName)
|
||||
case "email":
|
||||
return fmt.Sprintf("%s must be a valid email address", field)
|
||||
return fmt.Sprintf("%s 必须是有效的邮箱地址", fieldDisplayName)
|
||||
case "min":
|
||||
return fmt.Sprintf("%s must be at least %s characters", field, param)
|
||||
return fmt.Sprintf("%s 长度不能少于 %s 位", fieldDisplayName, param)
|
||||
case "max":
|
||||
return fmt.Sprintf("%s must be at most %s characters", field, param)
|
||||
return fmt.Sprintf("%s 长度不能超过 %s 位", fieldDisplayName, param)
|
||||
case "len":
|
||||
return fmt.Sprintf("%s must be exactly %s characters", field, param)
|
||||
return fmt.Sprintf("%s 长度必须为 %s 位", fieldDisplayName, param)
|
||||
case "gt":
|
||||
return fmt.Sprintf("%s must be greater than %s", field, param)
|
||||
return fmt.Sprintf("%s 必须大于 %s", fieldDisplayName, param)
|
||||
case "gte":
|
||||
return fmt.Sprintf("%s must be greater than or equal to %s", field, param)
|
||||
return fmt.Sprintf("%s 必须大于等于 %s", fieldDisplayName, param)
|
||||
case "lt":
|
||||
return fmt.Sprintf("%s must be less than %s", field, param)
|
||||
return fmt.Sprintf("%s 必须小于 %s", fieldDisplayName, param)
|
||||
case "lte":
|
||||
return fmt.Sprintf("%s must be less than or equal to %s", field, param)
|
||||
return fmt.Sprintf("%s 必须小于等于 %s", fieldDisplayName, param)
|
||||
case "oneof":
|
||||
return fmt.Sprintf("%s must be one of [%s]", field, param)
|
||||
return fmt.Sprintf("%s 必须是以下值之一:[%s]", fieldDisplayName, param)
|
||||
case "url":
|
||||
return fmt.Sprintf("%s must be a valid URL", field)
|
||||
return fmt.Sprintf("%s 必须是有效的URL地址", fieldDisplayName)
|
||||
case "alpha":
|
||||
return fmt.Sprintf("%s must contain only alphabetic characters", field)
|
||||
return fmt.Sprintf("%s 只能包含字母", fieldDisplayName)
|
||||
case "alphanum":
|
||||
return fmt.Sprintf("%s must contain only alphanumeric characters", field)
|
||||
return fmt.Sprintf("%s 只能包含字母和数字", fieldDisplayName)
|
||||
case "numeric":
|
||||
return fmt.Sprintf("%s must be numeric", field)
|
||||
return fmt.Sprintf("%s 必须是数字", fieldDisplayName)
|
||||
case "phone":
|
||||
return fmt.Sprintf("%s must be a valid phone number", field)
|
||||
return fmt.Sprintf("%s 必须是有效的手机号", fieldDisplayName)
|
||||
case "username":
|
||||
return fmt.Sprintf("%s must be a valid username", field)
|
||||
return fmt.Sprintf("%s 格式不正确,只能包含字母、数字、下划线,且不能以数字开头", fieldDisplayName)
|
||||
case "strong_password":
|
||||
return fmt.Sprintf("%s 强度不足,必须包含大小写字母和数字,且不少于8位", fieldDisplayName)
|
||||
case "eqfield":
|
||||
return fmt.Sprintf("%s 必须与 %s 一致", fieldDisplayName, v.getFieldDisplayName(param))
|
||||
default:
|
||||
return fmt.Sprintf("%s is invalid", field)
|
||||
return fmt.Sprintf("%s 格式不正确", fieldDisplayName)
|
||||
}
|
||||
}
|
||||
|
||||
// getFieldDisplayName 获取字段显示名称(中文)
|
||||
func (v *RequestValidator) getFieldDisplayName(field string) string {
|
||||
fieldNames := map[string]string{
|
||||
"phone": "手机号",
|
||||
"password": "密码",
|
||||
"confirm_password": "确认密码",
|
||||
"old_password": "原密码",
|
||||
"new_password": "新密码",
|
||||
"confirm_new_password": "确认新密码",
|
||||
"code": "验证码",
|
||||
"username": "用户名",
|
||||
"email": "邮箱",
|
||||
"display_name": "显示名称",
|
||||
"scene": "使用场景",
|
||||
"Password": "密码",
|
||||
"NewPassword": "新密码",
|
||||
}
|
||||
|
||||
if displayName, exists := fieldNames[field]; exists {
|
||||
return displayName
|
||||
}
|
||||
return field
|
||||
}
|
||||
|
||||
// toSnakeCase 转换为snake_case
|
||||
func (v *RequestValidator) toSnakeCase(str string) string {
|
||||
var result strings.Builder
|
||||
|
||||
294
internal/shared/http/validator_zh.go
Normal file
294
internal/shared/http/validator_zh.go
Normal file
@@ -0,0 +1,294 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/locales/zh"
|
||||
ut "github.com/go-playground/universal-translator"
|
||||
"github.com/go-playground/validator/v10"
|
||||
zh_translations "github.com/go-playground/validator/v10/translations/zh"
|
||||
)
|
||||
|
||||
// RequestValidatorZh 中文验证器实现
|
||||
type RequestValidatorZh struct {
|
||||
validator *validator.Validate
|
||||
translator ut.Translator
|
||||
response interfaces.ResponseBuilder
|
||||
}
|
||||
|
||||
// NewRequestValidatorZh 创建支持中文翻译的请求验证器
|
||||
func NewRequestValidatorZh(response interfaces.ResponseBuilder) interfaces.RequestValidator {
|
||||
// 创建验证器实例
|
||||
validate := validator.New()
|
||||
|
||||
// 创建中文locale
|
||||
zhLocale := zh.New()
|
||||
uni := ut.New(zhLocale, zhLocale)
|
||||
|
||||
// 获取中文翻译器
|
||||
trans, _ := uni.GetTranslator("zh")
|
||||
|
||||
// 注册中文翻译
|
||||
zh_translations.RegisterDefaultTranslations(validate, trans)
|
||||
|
||||
// 注册自定义验证器
|
||||
registerCustomValidatorsZh(validate, trans)
|
||||
|
||||
return &RequestValidatorZh{
|
||||
validator: validate,
|
||||
translator: trans,
|
||||
response: response,
|
||||
}
|
||||
}
|
||||
|
||||
// Validate 验证请求体
|
||||
func (v *RequestValidatorZh) Validate(c *gin.Context, dto interface{}) error {
|
||||
if err := v.validator.Struct(dto); err != nil {
|
||||
validationErrors := v.formatValidationErrorsZh(err)
|
||||
v.response.ValidationError(c, validationErrors)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateQuery 验证查询参数
|
||||
func (v *RequestValidatorZh) ValidateQuery(c *gin.Context, dto interface{}) error {
|
||||
if err := c.ShouldBindQuery(dto); err != nil {
|
||||
v.response.BadRequest(c, "查询参数格式错误", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
if err := v.validator.Struct(dto); err != nil {
|
||||
validationErrors := v.formatValidationErrorsZh(err)
|
||||
v.response.ValidationError(c, validationErrors)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateParam 验证路径参数
|
||||
func (v *RequestValidatorZh) ValidateParam(c *gin.Context, dto interface{}) error {
|
||||
if err := c.ShouldBindUri(dto); err != nil {
|
||||
v.response.BadRequest(c, "路径参数格式错误", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
if err := v.validator.Struct(dto); err != nil {
|
||||
validationErrors := v.formatValidationErrorsZh(err)
|
||||
v.response.ValidationError(c, validationErrors)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BindAndValidate 绑定并验证请求
|
||||
func (v *RequestValidatorZh) BindAndValidate(c *gin.Context, dto interface{}) error {
|
||||
// 绑定请求体
|
||||
if err := c.ShouldBindJSON(dto); err != nil {
|
||||
v.response.BadRequest(c, "请求体格式错误", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
// 验证数据
|
||||
return v.Validate(c, dto)
|
||||
}
|
||||
|
||||
// formatValidationErrorsZh 格式化验证错误(中文翻译版)
|
||||
func (v *RequestValidatorZh) formatValidationErrorsZh(err error) map[string][]string {
|
||||
errors := make(map[string][]string)
|
||||
|
||||
if validationErrors, ok := err.(validator.ValidationErrors); ok {
|
||||
for _, fieldError := range validationErrors {
|
||||
fieldName := v.getFieldNameZh(fieldError)
|
||||
|
||||
// 首先尝试使用翻译器获取翻译后的错误消息
|
||||
errorMessage := fieldError.Translate(v.translator)
|
||||
|
||||
// 如果翻译后的消息包含英文字段名,则替换为中文字段名
|
||||
fieldDisplayName := v.getFieldDisplayName(fieldError.Field())
|
||||
if fieldDisplayName != fieldError.Field() {
|
||||
// 替换字段名为中文
|
||||
errorMessage = strings.ReplaceAll(errorMessage, fieldError.Field(), fieldDisplayName)
|
||||
}
|
||||
|
||||
if _, exists := errors[fieldName]; !exists {
|
||||
errors[fieldName] = []string{}
|
||||
}
|
||||
errors[fieldName] = append(errors[fieldName], errorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
// getFieldNameZh 获取字段名(JSON标签优先)
|
||||
func (v *RequestValidatorZh) getFieldNameZh(fieldError validator.FieldError) string {
|
||||
fieldName := fieldError.Field()
|
||||
return v.toSnakeCase(fieldName)
|
||||
}
|
||||
|
||||
// getFieldDisplayName 获取字段显示名称(中文)
|
||||
func (v *RequestValidatorZh) getFieldDisplayName(field string) string {
|
||||
fieldNames := map[string]string{
|
||||
"phone": "手机号",
|
||||
"password": "密码",
|
||||
"confirm_password": "确认密码",
|
||||
"old_password": "原密码",
|
||||
"new_password": "新密码",
|
||||
"confirm_new_password": "确认新密码",
|
||||
"code": "验证码",
|
||||
"username": "用户名",
|
||||
"email": "邮箱",
|
||||
"display_name": "显示名称",
|
||||
"scene": "使用场景",
|
||||
"Password": "密码",
|
||||
"NewPassword": "新密码",
|
||||
"ConfirmPassword": "确认密码",
|
||||
}
|
||||
|
||||
if displayName, exists := fieldNames[field]; exists {
|
||||
return displayName
|
||||
}
|
||||
return field
|
||||
}
|
||||
|
||||
// toSnakeCase 转换为snake_case
|
||||
func (v *RequestValidatorZh) toSnakeCase(str string) string {
|
||||
var result strings.Builder
|
||||
for i, r := range str {
|
||||
if i > 0 && (r >= 'A' && r <= 'Z') {
|
||||
result.WriteRune('_')
|
||||
}
|
||||
result.WriteRune(r)
|
||||
}
|
||||
return strings.ToLower(result.String())
|
||||
}
|
||||
|
||||
// registerCustomValidatorsZh 注册自定义验证器和中文翻译
|
||||
func registerCustomValidatorsZh(v *validator.Validate, trans ut.Translator) {
|
||||
// 注册手机号验证器
|
||||
v.RegisterValidation("phone", validatePhoneZh)
|
||||
v.RegisterTranslation("phone", trans, func(ut ut.Translator) error {
|
||||
return ut.Add("phone", "{0}必须是有效的手机号", true)
|
||||
}, func(ut ut.Translator, fe validator.FieldError) string {
|
||||
t, _ := ut.T("phone", fe.Field())
|
||||
return t
|
||||
})
|
||||
|
||||
// 注册用户名验证器
|
||||
v.RegisterValidation("username", validateUsernameZh)
|
||||
v.RegisterTranslation("username", trans, func(ut ut.Translator) error {
|
||||
return ut.Add("username", "{0}格式不正确,只能包含字母、数字、下划线,且不能以数字开头", true)
|
||||
}, func(ut ut.Translator, fe validator.FieldError) string {
|
||||
t, _ := ut.T("username", fe.Field())
|
||||
return t
|
||||
})
|
||||
|
||||
// 注册密码强度验证器
|
||||
v.RegisterValidation("strong_password", validateStrongPasswordZh)
|
||||
v.RegisterTranslation("strong_password", trans, func(ut ut.Translator) error {
|
||||
return ut.Add("strong_password", "{0}强度不足,必须包含大小写字母和数字,且不少于8位", true)
|
||||
}, func(ut ut.Translator, fe validator.FieldError) string {
|
||||
t, _ := ut.T("strong_password", fe.Field())
|
||||
return t
|
||||
})
|
||||
|
||||
// 自定义eqfield翻译
|
||||
v.RegisterTranslation("eqfield", trans, func(ut ut.Translator) error {
|
||||
return ut.Add("eqfield", "{0}必须等于{1}", true)
|
||||
}, func(ut ut.Translator, fe validator.FieldError) string {
|
||||
t, _ := ut.T("eqfield", fe.Field(), fe.Param())
|
||||
return t
|
||||
})
|
||||
}
|
||||
|
||||
// validatePhoneZh 验证手机号
|
||||
func validatePhoneZh(fl validator.FieldLevel) bool {
|
||||
phone := fl.Field().String()
|
||||
if phone == "" {
|
||||
return true // 空值由required标签处理
|
||||
}
|
||||
|
||||
// 中国手机号验证:11位,以1开头
|
||||
if len(phone) != 11 {
|
||||
return false
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(phone, "1") {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查是否全是数字
|
||||
for _, r := range phone {
|
||||
if r < '0' || r > '9' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// validateUsernameZh 验证用户名
|
||||
func validateUsernameZh(fl validator.FieldLevel) bool {
|
||||
username := fl.Field().String()
|
||||
if username == "" {
|
||||
return true // 空值由required标签处理
|
||||
}
|
||||
|
||||
// 用户名规则:3-30个字符,只能包含字母、数字、下划线,不能以数字开头
|
||||
if len(username) < 3 || len(username) > 30 {
|
||||
return false
|
||||
}
|
||||
|
||||
// 不能以数字开头
|
||||
if username[0] >= '0' && username[0] <= '9' {
|
||||
return false
|
||||
}
|
||||
|
||||
// 只能包含字母、数字、下划线
|
||||
for _, r := range username {
|
||||
if !((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// validateStrongPasswordZh 验证密码强度
|
||||
func validateStrongPasswordZh(fl validator.FieldLevel) bool {
|
||||
password := fl.Field().String()
|
||||
if password == "" {
|
||||
return true // 空值由required标签处理
|
||||
}
|
||||
|
||||
// 密码强度规则:至少8个字符,包含大小写字母、数字
|
||||
if len(password) < 8 {
|
||||
return false
|
||||
}
|
||||
|
||||
hasUpper := false
|
||||
hasLower := false
|
||||
hasDigit := false
|
||||
|
||||
for _, r := range password {
|
||||
switch {
|
||||
case r >= 'A' && r <= 'Z':
|
||||
hasUpper = true
|
||||
case r >= 'a' && r <= 'z':
|
||||
hasLower = true
|
||||
case r >= '0' && r <= '9':
|
||||
hasDigit = true
|
||||
}
|
||||
}
|
||||
|
||||
return hasUpper && hasLower && hasDigit
|
||||
}
|
||||
|
||||
// ValidateStruct 直接验证结构体
|
||||
func (v *RequestValidatorZh) ValidateStruct(dto interface{}) error {
|
||||
return v.validator.Struct(dto)
|
||||
}
|
||||
@@ -76,9 +76,14 @@ type ResponseBuilder interface {
|
||||
NotFound(c *gin.Context, message ...string)
|
||||
Conflict(c *gin.Context, message string)
|
||||
InternalError(c *gin.Context, message ...string)
|
||||
ValidationError(c *gin.Context, errors interface{})
|
||||
TooManyRequests(c *gin.Context, message ...string)
|
||||
|
||||
// 分页响应
|
||||
Paginated(c *gin.Context, data interface{}, pagination PaginationMeta)
|
||||
|
||||
// 自定义响应
|
||||
CustomResponse(c *gin.Context, statusCode int, data interface{})
|
||||
}
|
||||
|
||||
// RequestValidator 请求验证器接口
|
||||
@@ -90,6 +95,9 @@ type RequestValidator interface {
|
||||
|
||||
// 绑定和验证
|
||||
BindAndValidate(c *gin.Context, dto interface{}) error
|
||||
|
||||
// 直接验证结构体
|
||||
ValidateStruct(dto interface{}) error
|
||||
}
|
||||
|
||||
// PaginationMeta 分页元数据
|
||||
|
||||
@@ -2,6 +2,15 @@ package interfaces
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/user/dto"
|
||||
"tyapi-server/internal/domains/user/entities"
|
||||
)
|
||||
|
||||
// 常见错误定义
|
||||
var (
|
||||
ErrCacheMiss = errors.New("cache miss")
|
||||
)
|
||||
|
||||
// Service 通用服务接口
|
||||
@@ -16,6 +25,22 @@ type Service interface {
|
||||
Shutdown(ctx context.Context) error
|
||||
}
|
||||
|
||||
// UserService 用户服务接口
|
||||
type UserService interface {
|
||||
Service
|
||||
|
||||
// 用户注册
|
||||
Register(ctx context.Context, req *dto.RegisterRequest) (*entities.User, error)
|
||||
// 密码登录
|
||||
LoginWithPassword(ctx context.Context, req *dto.LoginWithPasswordRequest) (*entities.User, error)
|
||||
// 短信验证码登录
|
||||
LoginWithSMS(ctx context.Context, req *dto.LoginWithSMSRequest) (*entities.User, error)
|
||||
// 修改密码
|
||||
ChangePassword(ctx context.Context, userID string, req *dto.ChangePasswordRequest) error
|
||||
// 根据ID获取用户
|
||||
GetByID(ctx context.Context, id string) (*entities.User, error)
|
||||
}
|
||||
|
||||
// DomainService 领域服务接口,支持泛型
|
||||
type DomainService[T Entity] interface {
|
||||
Service
|
||||
|
||||
214
internal/shared/logger/enhanced_logger.go
Normal file
214
internal/shared/logger/enhanced_logger.go
Normal file
@@ -0,0 +1,214 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
// LogLevel 日志级别
|
||||
type LogLevel string
|
||||
|
||||
const (
|
||||
DebugLevel LogLevel = "debug"
|
||||
InfoLevel LogLevel = "info"
|
||||
WarnLevel LogLevel = "warn"
|
||||
ErrorLevel LogLevel = "error"
|
||||
)
|
||||
|
||||
// LogContext 日志上下文
|
||||
type LogContext struct {
|
||||
RequestID string
|
||||
UserID string
|
||||
TraceID string
|
||||
OperationName string
|
||||
Layer string // repository/service/handler
|
||||
Component string
|
||||
}
|
||||
|
||||
// ContextualLogger 上下文感知的日志器
|
||||
type ContextualLogger struct {
|
||||
logger *zap.Logger
|
||||
ctx LogContext
|
||||
}
|
||||
|
||||
// NewContextualLogger 创建上下文日志器
|
||||
func NewContextualLogger(logger *zap.Logger) *ContextualLogger {
|
||||
return &ContextualLogger{
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// WithContext 添加上下文信息
|
||||
func (l *ContextualLogger) WithContext(ctx context.Context) *ContextualLogger {
|
||||
logCtx := LogContext{}
|
||||
|
||||
// 从context中提取常用字段
|
||||
if requestID := getStringFromContext(ctx, "request_id"); requestID != "" {
|
||||
logCtx.RequestID = requestID
|
||||
}
|
||||
if userID := getStringFromContext(ctx, "user_id"); userID != "" {
|
||||
logCtx.UserID = userID
|
||||
}
|
||||
if traceID := getStringFromContext(ctx, "trace_id"); traceID != "" {
|
||||
logCtx.TraceID = traceID
|
||||
}
|
||||
|
||||
return &ContextualLogger{
|
||||
logger: l.logger,
|
||||
ctx: logCtx,
|
||||
}
|
||||
}
|
||||
|
||||
// WithLayer 设置层级信息
|
||||
func (l *ContextualLogger) WithLayer(layer string) *ContextualLogger {
|
||||
newCtx := l.ctx
|
||||
newCtx.Layer = layer
|
||||
return &ContextualLogger{
|
||||
logger: l.logger,
|
||||
ctx: newCtx,
|
||||
}
|
||||
}
|
||||
|
||||
// WithComponent 设置组件信息
|
||||
func (l *ContextualLogger) WithComponent(component string) *ContextualLogger {
|
||||
newCtx := l.ctx
|
||||
newCtx.Component = component
|
||||
return &ContextualLogger{
|
||||
logger: l.logger,
|
||||
ctx: newCtx,
|
||||
}
|
||||
}
|
||||
|
||||
// WithOperation 设置操作名称
|
||||
func (l *ContextualLogger) WithOperation(operation string) *ContextualLogger {
|
||||
newCtx := l.ctx
|
||||
newCtx.OperationName = operation
|
||||
return &ContextualLogger{
|
||||
logger: l.logger,
|
||||
ctx: newCtx,
|
||||
}
|
||||
}
|
||||
|
||||
// 构建基础字段
|
||||
func (l *ContextualLogger) buildBaseFields() []zapcore.Field {
|
||||
fields := []zapcore.Field{}
|
||||
|
||||
if l.ctx.RequestID != "" {
|
||||
fields = append(fields, zap.String("request_id", l.ctx.RequestID))
|
||||
}
|
||||
if l.ctx.UserID != "" {
|
||||
fields = append(fields, zap.String("user_id", l.ctx.UserID))
|
||||
}
|
||||
if l.ctx.TraceID != "" {
|
||||
fields = append(fields, zap.String("trace_id", l.ctx.TraceID))
|
||||
}
|
||||
if l.ctx.Layer != "" {
|
||||
fields = append(fields, zap.String("layer", l.ctx.Layer))
|
||||
}
|
||||
if l.ctx.Component != "" {
|
||||
fields = append(fields, zap.String("component", l.ctx.Component))
|
||||
}
|
||||
if l.ctx.OperationName != "" {
|
||||
fields = append(fields, zap.String("operation", l.ctx.OperationName))
|
||||
}
|
||||
|
||||
return fields
|
||||
}
|
||||
|
||||
// LogTechnicalError 记录技术性错误(Repository层)
|
||||
func (l *ContextualLogger) LogTechnicalError(msg string, err error, fields ...zapcore.Field) {
|
||||
allFields := l.buildBaseFields()
|
||||
allFields = append(allFields, zap.Error(err))
|
||||
allFields = append(allFields, zap.String("error_type", "technical"))
|
||||
allFields = append(allFields, fields...)
|
||||
|
||||
l.logger.Error(msg, allFields...)
|
||||
}
|
||||
|
||||
// LogBusinessWarn 记录业务警告(Service层)
|
||||
func (l *ContextualLogger) LogBusinessWarn(msg string, fields ...zapcore.Field) {
|
||||
allFields := l.buildBaseFields()
|
||||
allFields = append(allFields, zap.String("log_type", "business"))
|
||||
allFields = append(allFields, fields...)
|
||||
|
||||
l.logger.Warn(msg, allFields...)
|
||||
}
|
||||
|
||||
// LogBusinessInfo 记录业务信息(Service层)
|
||||
func (l *ContextualLogger) LogBusinessInfo(msg string, fields ...zapcore.Field) {
|
||||
allFields := l.buildBaseFields()
|
||||
allFields = append(allFields, zap.String("log_type", "business"))
|
||||
allFields = append(allFields, fields...)
|
||||
|
||||
l.logger.Info(msg, allFields...)
|
||||
}
|
||||
|
||||
// LogUserAction 记录用户行为(Handler层)
|
||||
func (l *ContextualLogger) LogUserAction(msg string, fields ...zapcore.Field) {
|
||||
allFields := l.buildBaseFields()
|
||||
allFields = append(allFields, zap.String("log_type", "user_action"))
|
||||
allFields = append(allFields, fields...)
|
||||
|
||||
l.logger.Info(msg, allFields...)
|
||||
}
|
||||
|
||||
// LogRequestFailed 记录请求失败(Handler层)
|
||||
func (l *ContextualLogger) LogRequestFailed(msg string, errorType string, fields ...zapcore.Field) {
|
||||
allFields := l.buildBaseFields()
|
||||
allFields = append(allFields, zap.String("log_type", "request_failed"))
|
||||
allFields = append(allFields, zap.String("error_category", errorType))
|
||||
allFields = append(allFields, fields...)
|
||||
|
||||
l.logger.Info(msg, allFields...)
|
||||
}
|
||||
|
||||
// getStringFromContext 从上下文获取字符串值
|
||||
func getStringFromContext(ctx context.Context, key string) string {
|
||||
if value := ctx.Value(key); value != nil {
|
||||
if str, ok := value.(string); ok {
|
||||
return str
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ErrorCategory 错误分类
|
||||
type ErrorCategory string
|
||||
|
||||
const (
|
||||
DatabaseError ErrorCategory = "database"
|
||||
NetworkError ErrorCategory = "network"
|
||||
ValidationError ErrorCategory = "validation"
|
||||
BusinessError ErrorCategory = "business"
|
||||
AuthError ErrorCategory = "auth"
|
||||
ExternalAPIError ErrorCategory = "external_api"
|
||||
)
|
||||
|
||||
// CategorizeError 错误分类
|
||||
func CategorizeError(err error) ErrorCategory {
|
||||
errMsg := strings.ToLower(err.Error())
|
||||
|
||||
switch {
|
||||
case strings.Contains(errMsg, "database") ||
|
||||
strings.Contains(errMsg, "sql") ||
|
||||
strings.Contains(errMsg, "gorm"):
|
||||
return DatabaseError
|
||||
case strings.Contains(errMsg, "network") ||
|
||||
strings.Contains(errMsg, "connection") ||
|
||||
strings.Contains(errMsg, "timeout"):
|
||||
return NetworkError
|
||||
case strings.Contains(errMsg, "validation") ||
|
||||
strings.Contains(errMsg, "invalid") ||
|
||||
strings.Contains(errMsg, "format"):
|
||||
return ValidationError
|
||||
case strings.Contains(errMsg, "unauthorized") ||
|
||||
strings.Contains(errMsg, "forbidden") ||
|
||||
strings.Contains(errMsg, "token"):
|
||||
return AuthError
|
||||
default:
|
||||
return BusinessError
|
||||
}
|
||||
}
|
||||
263
internal/shared/metrics/business_metrics.go
Normal file
263
internal/shared/metrics/business_metrics.go
Normal file
@@ -0,0 +1,263 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
)
|
||||
|
||||
// BusinessMetrics 业务指标收集器
|
||||
type BusinessMetrics struct {
|
||||
metrics interfaces.MetricsCollector
|
||||
logger *zap.Logger
|
||||
mutex sync.RWMutex
|
||||
|
||||
// 业务指标缓存
|
||||
userMetrics map[string]int64
|
||||
orderMetrics map[string]int64
|
||||
}
|
||||
|
||||
// NewBusinessMetrics 创建业务指标收集器
|
||||
func NewBusinessMetrics(metrics interfaces.MetricsCollector, logger *zap.Logger) *BusinessMetrics {
|
||||
bm := &BusinessMetrics{
|
||||
metrics: metrics,
|
||||
logger: logger,
|
||||
userMetrics: make(map[string]int64),
|
||||
orderMetrics: make(map[string]int64),
|
||||
}
|
||||
|
||||
// 注册业务指标
|
||||
bm.registerBusinessMetrics()
|
||||
|
||||
return bm
|
||||
}
|
||||
|
||||
// registerBusinessMetrics 注册业务指标
|
||||
func (bm *BusinessMetrics) registerBusinessMetrics() {
|
||||
// 用户相关指标
|
||||
bm.metrics.RegisterCounter("users_created_total", "Total number of users created", []string{"source"})
|
||||
bm.metrics.RegisterCounter("users_login_total", "Total number of user logins", []string{"method", "status"})
|
||||
bm.metrics.RegisterGauge("users_active_sessions", "Current number of active user sessions", nil)
|
||||
|
||||
// 订单相关指标
|
||||
bm.metrics.RegisterCounter("orders_created_total", "Total number of orders created", []string{"status"})
|
||||
bm.metrics.RegisterCounter("orders_amount_total", "Total order amount in cents", []string{"currency"})
|
||||
bm.metrics.RegisterHistogram("orders_processing_duration_seconds", "Order processing duration", []string{"status"}, []float64{0.1, 0.5, 1, 2, 5, 10, 30})
|
||||
|
||||
// API相关指标
|
||||
bm.metrics.RegisterCounter("api_errors_total", "Total number of API errors", []string{"endpoint", "error_type"})
|
||||
bm.metrics.RegisterHistogram("api_response_size_bytes", "API response size in bytes", []string{"endpoint"}, []float64{100, 1000, 10000, 100000})
|
||||
|
||||
// 缓存相关指标
|
||||
bm.metrics.RegisterCounter("cache_operations_total", "Total number of cache operations", []string{"operation", "result"})
|
||||
bm.metrics.RegisterGauge("cache_memory_usage_bytes", "Cache memory usage in bytes", []string{"cache_type"})
|
||||
|
||||
// 数据库相关指标
|
||||
bm.metrics.RegisterHistogram("database_query_duration_seconds", "Database query duration", []string{"operation", "table"}, []float64{0.001, 0.01, 0.1, 1, 10})
|
||||
bm.metrics.RegisterCounter("database_errors_total", "Total number of database errors", []string{"operation", "error_type"})
|
||||
|
||||
bm.logger.Info("Business metrics registered successfully")
|
||||
}
|
||||
|
||||
// User相关指标
|
||||
|
||||
// RecordUserCreated 记录用户创建
|
||||
func (bm *BusinessMetrics) RecordUserCreated(source string) {
|
||||
bm.metrics.IncrementCounter("users_created_total", map[string]string{
|
||||
"source": source,
|
||||
})
|
||||
|
||||
bm.mutex.Lock()
|
||||
bm.userMetrics["created"]++
|
||||
bm.mutex.Unlock()
|
||||
|
||||
bm.logger.Debug("Recorded user created", zap.String("source", source))
|
||||
}
|
||||
|
||||
// RecordUserLogin 记录用户登录
|
||||
func (bm *BusinessMetrics) RecordUserLogin(method, status string) {
|
||||
bm.metrics.IncrementCounter("users_login_total", map[string]string{
|
||||
"method": method,
|
||||
"status": status,
|
||||
})
|
||||
|
||||
bm.logger.Debug("Recorded user login", zap.String("method", method), zap.String("status", status))
|
||||
}
|
||||
|
||||
// UpdateActiveUserSessions 更新活跃用户会话数
|
||||
func (bm *BusinessMetrics) UpdateActiveUserSessions(count float64) {
|
||||
bm.metrics.RecordGauge("users_active_sessions", count, nil)
|
||||
}
|
||||
|
||||
// Order相关指标
|
||||
|
||||
// RecordOrderCreated 记录订单创建
|
||||
func (bm *BusinessMetrics) RecordOrderCreated(status string, amount float64, currency string) {
|
||||
bm.metrics.IncrementCounter("orders_created_total", map[string]string{
|
||||
"status": status,
|
||||
})
|
||||
|
||||
// 记录订单金额(以分为单位)
|
||||
amountCents := int64(amount * 100)
|
||||
bm.metrics.IncrementCounter("orders_amount_total", map[string]string{
|
||||
"currency": currency,
|
||||
})
|
||||
|
||||
bm.mutex.Lock()
|
||||
bm.orderMetrics["created"]++
|
||||
bm.orderMetrics["amount"] += amountCents
|
||||
bm.mutex.Unlock()
|
||||
|
||||
bm.logger.Debug("Recorded order created",
|
||||
zap.String("status", status),
|
||||
zap.Float64("amount", amount),
|
||||
zap.String("currency", currency))
|
||||
}
|
||||
|
||||
// RecordOrderProcessingDuration 记录订单处理时长
|
||||
func (bm *BusinessMetrics) RecordOrderProcessingDuration(status string, duration float64) {
|
||||
bm.metrics.RecordHistogram("orders_processing_duration_seconds", duration, map[string]string{
|
||||
"status": status,
|
||||
})
|
||||
}
|
||||
|
||||
// API相关指标
|
||||
|
||||
// RecordAPIError 记录API错误
|
||||
func (bm *BusinessMetrics) RecordAPIError(endpoint, errorType string) {
|
||||
bm.metrics.IncrementCounter("api_errors_total", map[string]string{
|
||||
"endpoint": endpoint,
|
||||
"error_type": errorType,
|
||||
})
|
||||
|
||||
bm.logger.Debug("Recorded API error",
|
||||
zap.String("endpoint", endpoint),
|
||||
zap.String("error_type", errorType))
|
||||
}
|
||||
|
||||
// RecordAPIResponseSize 记录API响应大小
|
||||
func (bm *BusinessMetrics) RecordAPIResponseSize(endpoint string, sizeBytes float64) {
|
||||
bm.metrics.RecordHistogram("api_response_size_bytes", sizeBytes, map[string]string{
|
||||
"endpoint": endpoint,
|
||||
})
|
||||
}
|
||||
|
||||
// Cache相关指标
|
||||
|
||||
// RecordCacheOperation 记录缓存操作
|
||||
func (bm *BusinessMetrics) RecordCacheOperation(operation, result string) {
|
||||
bm.metrics.IncrementCounter("cache_operations_total", map[string]string{
|
||||
"operation": operation,
|
||||
"result": result,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateCacheMemoryUsage 更新缓存内存使用量
|
||||
func (bm *BusinessMetrics) UpdateCacheMemoryUsage(cacheType string, usageBytes float64) {
|
||||
bm.metrics.RecordGauge("cache_memory_usage_bytes", usageBytes, map[string]string{
|
||||
"cache_type": cacheType,
|
||||
})
|
||||
}
|
||||
|
||||
// Database相关指标
|
||||
|
||||
// RecordDatabaseQuery 记录数据库查询
|
||||
func (bm *BusinessMetrics) RecordDatabaseQuery(operation, table string, duration float64) {
|
||||
bm.metrics.RecordHistogram("database_query_duration_seconds", duration, map[string]string{
|
||||
"operation": operation,
|
||||
"table": table,
|
||||
})
|
||||
}
|
||||
|
||||
// RecordDatabaseError 记录数据库错误
|
||||
func (bm *BusinessMetrics) RecordDatabaseError(operation, errorType string) {
|
||||
bm.metrics.IncrementCounter("database_errors_total", map[string]string{
|
||||
"operation": operation,
|
||||
"error_type": errorType,
|
||||
})
|
||||
|
||||
bm.logger.Debug("Recorded database error",
|
||||
zap.String("operation", operation),
|
||||
zap.String("error_type", errorType))
|
||||
}
|
||||
|
||||
// 获取统计信息
|
||||
|
||||
// GetUserStats 获取用户统计
|
||||
func (bm *BusinessMetrics) GetUserStats() map[string]int64 {
|
||||
bm.mutex.RLock()
|
||||
defer bm.mutex.RUnlock()
|
||||
|
||||
stats := make(map[string]int64)
|
||||
for k, v := range bm.userMetrics {
|
||||
stats[k] = v
|
||||
}
|
||||
return stats
|
||||
}
|
||||
|
||||
// GetOrderStats 获取订单统计
|
||||
func (bm *BusinessMetrics) GetOrderStats() map[string]int64 {
|
||||
bm.mutex.RLock()
|
||||
defer bm.mutex.RUnlock()
|
||||
|
||||
stats := make(map[string]int64)
|
||||
for k, v := range bm.orderMetrics {
|
||||
stats[k] = v
|
||||
}
|
||||
return stats
|
||||
}
|
||||
|
||||
// GetOverallStats 获取整体统计
|
||||
func (bm *BusinessMetrics) GetOverallStats() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"user_stats": bm.GetUserStats(),
|
||||
"order_stats": bm.GetOrderStats(),
|
||||
}
|
||||
}
|
||||
|
||||
// Reset 重置统计数据
|
||||
func (bm *BusinessMetrics) Reset() {
|
||||
bm.mutex.Lock()
|
||||
defer bm.mutex.Unlock()
|
||||
|
||||
bm.userMetrics = make(map[string]int64)
|
||||
bm.orderMetrics = make(map[string]int64)
|
||||
|
||||
bm.logger.Info("Business metrics reset")
|
||||
}
|
||||
|
||||
// Context相关方法
|
||||
|
||||
// WithContext 创建带上下文的业务指标收集器
|
||||
func (bm *BusinessMetrics) WithContext(ctx context.Context) *BusinessMetrics {
|
||||
// 这里可以从context中提取追踪信息,关联指标
|
||||
return bm
|
||||
}
|
||||
|
||||
// 实现Service接口(如果需要)
|
||||
|
||||
// Name 返回服务名称
|
||||
func (bm *BusinessMetrics) Name() string {
|
||||
return "business-metrics"
|
||||
}
|
||||
|
||||
// Initialize 初始化服务
|
||||
func (bm *BusinessMetrics) Initialize(ctx context.Context) error {
|
||||
bm.logger.Info("Business metrics service initialized")
|
||||
return nil
|
||||
}
|
||||
|
||||
// HealthCheck 健康检查
|
||||
func (bm *BusinessMetrics) HealthCheck(ctx context.Context) error {
|
||||
// 检查指标收集器是否正常
|
||||
return nil
|
||||
}
|
||||
|
||||
// Shutdown 关闭服务
|
||||
func (bm *BusinessMetrics) Shutdown(ctx context.Context) error {
|
||||
bm.logger.Info("Business metrics service shutdown")
|
||||
return nil
|
||||
}
|
||||
353
internal/shared/metrics/prometheus_metrics.go
Normal file
353
internal/shared/metrics/prometheus_metrics.go
Normal file
@@ -0,0 +1,353 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// PrometheusMetrics Prometheus指标收集器
|
||||
type PrometheusMetrics struct {
|
||||
logger *zap.Logger
|
||||
registry *prometheus.Registry
|
||||
mutex sync.RWMutex
|
||||
|
||||
// 预定义指标
|
||||
httpRequests *prometheus.CounterVec
|
||||
httpDuration *prometheus.HistogramVec
|
||||
activeUsers prometheus.Gauge
|
||||
dbConnections prometheus.Gauge
|
||||
cacheHits *prometheus.CounterVec
|
||||
businessMetrics map[string]prometheus.Collector
|
||||
}
|
||||
|
||||
// NewPrometheusMetrics 创建Prometheus指标收集器
|
||||
func NewPrometheusMetrics(logger *zap.Logger) *PrometheusMetrics {
|
||||
registry := prometheus.NewRegistry()
|
||||
|
||||
// HTTP请求计数器
|
||||
httpRequests := prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "http_requests_total",
|
||||
Help: "Total number of HTTP requests",
|
||||
},
|
||||
[]string{"method", "path", "status"},
|
||||
)
|
||||
|
||||
// HTTP请求耗时直方图
|
||||
httpDuration := prometheus.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "http_request_duration_seconds",
|
||||
Help: "HTTP request duration in seconds",
|
||||
Buckets: prometheus.DefBuckets,
|
||||
},
|
||||
[]string{"method", "path"},
|
||||
)
|
||||
|
||||
// 活跃用户数
|
||||
activeUsers := prometheus.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "active_users_total",
|
||||
Help: "Current number of active users",
|
||||
},
|
||||
)
|
||||
|
||||
// 数据库连接数
|
||||
dbConnections := prometheus.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "database_connections_active",
|
||||
Help: "Current number of active database connections",
|
||||
},
|
||||
)
|
||||
|
||||
// 缓存命中率
|
||||
cacheHits := prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "cache_operations_total",
|
||||
Help: "Total number of cache operations",
|
||||
},
|
||||
[]string{"operation", "result"},
|
||||
)
|
||||
|
||||
// 注册指标
|
||||
registry.MustRegister(httpRequests)
|
||||
registry.MustRegister(httpDuration)
|
||||
registry.MustRegister(activeUsers)
|
||||
registry.MustRegister(dbConnections)
|
||||
registry.MustRegister(cacheHits)
|
||||
|
||||
return &PrometheusMetrics{
|
||||
logger: logger,
|
||||
registry: registry,
|
||||
httpRequests: httpRequests,
|
||||
httpDuration: httpDuration,
|
||||
activeUsers: activeUsers,
|
||||
dbConnections: dbConnections,
|
||||
cacheHits: cacheHits,
|
||||
businessMetrics: make(map[string]prometheus.Collector),
|
||||
}
|
||||
}
|
||||
|
||||
// RecordHTTPRequest 记录HTTP请求指标
|
||||
func (m *PrometheusMetrics) RecordHTTPRequest(method, path string, statusCode int, duration float64) {
|
||||
status := strconv.Itoa(statusCode)
|
||||
|
||||
m.httpRequests.WithLabelValues(method, path, status).Inc()
|
||||
m.httpDuration.WithLabelValues(method, path).Observe(duration)
|
||||
|
||||
m.logger.Debug("Recorded HTTP request metric",
|
||||
zap.String("method", method),
|
||||
zap.String("path", path),
|
||||
zap.String("status", status),
|
||||
zap.Float64("duration", duration))
|
||||
}
|
||||
|
||||
// RecordHTTPDuration 记录HTTP请求耗时
|
||||
func (m *PrometheusMetrics) RecordHTTPDuration(method, path string, duration float64) {
|
||||
m.httpDuration.WithLabelValues(method, path).Observe(duration)
|
||||
|
||||
m.logger.Debug("Recorded HTTP duration metric",
|
||||
zap.String("method", method),
|
||||
zap.String("path", path),
|
||||
zap.Float64("duration", duration))
|
||||
}
|
||||
|
||||
// IncrementCounter 增加计数器
|
||||
func (m *PrometheusMetrics) IncrementCounter(name string, labels map[string]string) {
|
||||
if counter, exists := m.getOrCreateCounter(name, labels); exists {
|
||||
if vec, ok := counter.(*prometheus.CounterVec); ok {
|
||||
vec.With(labels).Inc()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RecordGauge 记录仪表盘值
|
||||
func (m *PrometheusMetrics) RecordGauge(name string, value float64, labels map[string]string) {
|
||||
if gauge, exists := m.getOrCreateGauge(name, labels); exists {
|
||||
if vec, ok := gauge.(*prometheus.GaugeVec); ok {
|
||||
vec.With(labels).Set(value)
|
||||
} else if g, ok := gauge.(prometheus.Gauge); ok {
|
||||
g.Set(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RecordHistogram 记录直方图值
|
||||
func (m *PrometheusMetrics) RecordHistogram(name string, value float64, labels map[string]string) {
|
||||
if histogram, exists := m.getOrCreateHistogram(name, labels); exists {
|
||||
if vec, ok := histogram.(*prometheus.HistogramVec); ok {
|
||||
vec.With(labels).Observe(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterCounter 注册计数器
|
||||
func (m *PrometheusMetrics) RegisterCounter(name, help string, labels []string) error {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
if _, exists := m.businessMetrics[name]; exists {
|
||||
return nil // 已存在
|
||||
}
|
||||
|
||||
var counter prometheus.Collector
|
||||
if len(labels) > 0 {
|
||||
counter = prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{Name: name, Help: help},
|
||||
labels,
|
||||
)
|
||||
} else {
|
||||
counter = prometheus.NewCounter(
|
||||
prometheus.CounterOpts{Name: name, Help: help},
|
||||
)
|
||||
}
|
||||
|
||||
if err := m.registry.Register(counter); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.businessMetrics[name] = counter
|
||||
m.logger.Info("Registered counter metric", zap.String("name", name))
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterGauge 注册仪表盘
|
||||
func (m *PrometheusMetrics) RegisterGauge(name, help string, labels []string) error {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
if _, exists := m.businessMetrics[name]; exists {
|
||||
return nil
|
||||
}
|
||||
|
||||
var gauge prometheus.Collector
|
||||
if len(labels) > 0 {
|
||||
gauge = prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{Name: name, Help: help},
|
||||
labels,
|
||||
)
|
||||
} else {
|
||||
gauge = prometheus.NewGauge(
|
||||
prometheus.GaugeOpts{Name: name, Help: help},
|
||||
)
|
||||
}
|
||||
|
||||
if err := m.registry.Register(gauge); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.businessMetrics[name] = gauge
|
||||
m.logger.Info("Registered gauge metric", zap.String("name", name))
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterHistogram 注册直方图
|
||||
func (m *PrometheusMetrics) RegisterHistogram(name, help string, labels []string, buckets []float64) error {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
if _, exists := m.businessMetrics[name]; exists {
|
||||
return nil
|
||||
}
|
||||
|
||||
if buckets == nil {
|
||||
buckets = prometheus.DefBuckets
|
||||
}
|
||||
|
||||
var histogram prometheus.Collector
|
||||
if len(labels) > 0 {
|
||||
histogram = prometheus.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: name,
|
||||
Help: help,
|
||||
Buckets: buckets,
|
||||
},
|
||||
labels,
|
||||
)
|
||||
} else {
|
||||
histogram = prometheus.NewHistogram(
|
||||
prometheus.HistogramOpts{
|
||||
Name: name,
|
||||
Help: help,
|
||||
Buckets: buckets,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if err := m.registry.Register(histogram); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.businessMetrics[name] = histogram
|
||||
m.logger.Info("Registered histogram metric", zap.String("name", name))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetHandler 获取HTTP处理器
|
||||
func (m *PrometheusMetrics) GetHandler() http.Handler {
|
||||
return promhttp.HandlerFor(m.registry, promhttp.HandlerOpts{})
|
||||
}
|
||||
|
||||
// 内部辅助方法
|
||||
|
||||
func (m *PrometheusMetrics) getOrCreateCounter(name string, labels map[string]string) (prometheus.Collector, bool) {
|
||||
m.mutex.RLock()
|
||||
counter, exists := m.businessMetrics[name]
|
||||
m.mutex.RUnlock()
|
||||
|
||||
if !exists {
|
||||
// 自动创建计数器
|
||||
labelNames := make([]string, 0, len(labels))
|
||||
for k := range labels {
|
||||
labelNames = append(labelNames, k)
|
||||
}
|
||||
|
||||
if err := m.RegisterCounter(name, "Auto-created counter", labelNames); err != nil {
|
||||
m.logger.Error("Failed to auto-create counter", zap.String("name", name), zap.Error(err))
|
||||
return nil, false
|
||||
}
|
||||
|
||||
m.mutex.RLock()
|
||||
counter, exists = m.businessMetrics[name]
|
||||
m.mutex.RUnlock()
|
||||
}
|
||||
|
||||
return counter, exists
|
||||
}
|
||||
|
||||
func (m *PrometheusMetrics) getOrCreateGauge(name string, labels map[string]string) (prometheus.Collector, bool) {
|
||||
m.mutex.RLock()
|
||||
gauge, exists := m.businessMetrics[name]
|
||||
m.mutex.RUnlock()
|
||||
|
||||
if !exists {
|
||||
labelNames := make([]string, 0, len(labels))
|
||||
for k := range labels {
|
||||
labelNames = append(labelNames, k)
|
||||
}
|
||||
|
||||
if err := m.RegisterGauge(name, "Auto-created gauge", labelNames); err != nil {
|
||||
m.logger.Error("Failed to auto-create gauge", zap.String("name", name), zap.Error(err))
|
||||
return nil, false
|
||||
}
|
||||
|
||||
m.mutex.RLock()
|
||||
gauge, exists = m.businessMetrics[name]
|
||||
m.mutex.RUnlock()
|
||||
}
|
||||
|
||||
return gauge, exists
|
||||
}
|
||||
|
||||
func (m *PrometheusMetrics) getOrCreateHistogram(name string, labels map[string]string) (prometheus.Collector, bool) {
|
||||
m.mutex.RLock()
|
||||
histogram, exists := m.businessMetrics[name]
|
||||
m.mutex.RUnlock()
|
||||
|
||||
if !exists {
|
||||
labelNames := make([]string, 0, len(labels))
|
||||
for k := range labels {
|
||||
labelNames = append(labelNames, k)
|
||||
}
|
||||
|
||||
if err := m.RegisterHistogram(name, "Auto-created histogram", labelNames, nil); err != nil {
|
||||
m.logger.Error("Failed to auto-create histogram", zap.String("name", name), zap.Error(err))
|
||||
return nil, false
|
||||
}
|
||||
|
||||
m.mutex.RLock()
|
||||
histogram, exists = m.businessMetrics[name]
|
||||
m.mutex.RUnlock()
|
||||
}
|
||||
|
||||
return histogram, exists
|
||||
}
|
||||
|
||||
// UpdateActiveUsers 更新活跃用户数
|
||||
func (m *PrometheusMetrics) UpdateActiveUsers(count float64) {
|
||||
m.activeUsers.Set(count)
|
||||
}
|
||||
|
||||
// UpdateDBConnections 更新数据库连接数
|
||||
func (m *PrometheusMetrics) UpdateDBConnections(count float64) {
|
||||
m.dbConnections.Set(count)
|
||||
}
|
||||
|
||||
// RecordCacheOperation 记录缓存操作
|
||||
func (m *PrometheusMetrics) RecordCacheOperation(operation, result string) {
|
||||
m.cacheHits.WithLabelValues(operation, result).Inc()
|
||||
}
|
||||
|
||||
// GetStats 获取指标统计
|
||||
func (m *PrometheusMetrics) GetStats() map[string]interface{} {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
return map[string]interface{}{
|
||||
"registered_metrics": len(m.businessMetrics),
|
||||
}
|
||||
}
|
||||
@@ -42,31 +42,31 @@ func (m *JWTAuthMiddleware) Handle() gin.HandlerFunc {
|
||||
// 获取Authorization头部
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
m.respondUnauthorized(c, "Missing authorization header")
|
||||
m.respondUnauthorized(c, "缺少认证头部")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查Bearer前缀
|
||||
const bearerPrefix = "Bearer "
|
||||
if !strings.HasPrefix(authHeader, bearerPrefix) {
|
||||
m.respondUnauthorized(c, "Invalid authorization header format")
|
||||
m.respondUnauthorized(c, "认证头部格式无效")
|
||||
return
|
||||
}
|
||||
|
||||
// 提取token
|
||||
tokenString := authHeader[len(bearerPrefix):]
|
||||
if tokenString == "" {
|
||||
m.respondUnauthorized(c, "Missing token")
|
||||
m.respondUnauthorized(c, "缺少认证令牌")
|
||||
return
|
||||
}
|
||||
|
||||
// 验证token
|
||||
claims, err := m.validateToken(tokenString)
|
||||
if err != nil {
|
||||
m.logger.Warn("Invalid token",
|
||||
m.logger.Warn("无效的认证令牌",
|
||||
zap.Error(err),
|
||||
zap.String("request_id", c.GetString("request_id")))
|
||||
m.respondUnauthorized(c, "Invalid token")
|
||||
m.respondUnauthorized(c, "认证令牌无效")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ func (m *JWTAuthMiddleware) validateToken(tokenString string) (*JWTClaims, error
|
||||
func (m *JWTAuthMiddleware) respondUnauthorized(c *gin.Context, message string) {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"success": false,
|
||||
"message": "Unauthorized",
|
||||
"message": "认证失败",
|
||||
"error": message,
|
||||
"request_id": c.GetString("request_id"),
|
||||
"timestamp": time.Now().Unix(),
|
||||
|
||||
@@ -2,11 +2,11 @@ package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"tyapi-server/internal/config"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/time/rate"
|
||||
@@ -15,14 +15,16 @@ import (
|
||||
// RateLimitMiddleware 限流中间件
|
||||
type RateLimitMiddleware struct {
|
||||
config *config.Config
|
||||
response interfaces.ResponseBuilder
|
||||
limiters map[string]*rate.Limiter
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
// NewRateLimitMiddleware 创建限流中间件
|
||||
func NewRateLimitMiddleware(cfg *config.Config) *RateLimitMiddleware {
|
||||
func NewRateLimitMiddleware(cfg *config.Config, response interfaces.ResponseBuilder) *RateLimitMiddleware {
|
||||
return &RateLimitMiddleware{
|
||||
config: cfg,
|
||||
response: response,
|
||||
limiters: make(map[string]*rate.Limiter),
|
||||
}
|
||||
}
|
||||
@@ -48,15 +50,13 @@ func (m *RateLimitMiddleware) Handle() gin.HandlerFunc {
|
||||
|
||||
// 检查是否允许请求
|
||||
if !limiter.Allow() {
|
||||
// 添加限流头部信息
|
||||
c.Header("X-RateLimit-Limit", fmt.Sprintf("%d", m.config.RateLimit.Requests))
|
||||
c.Header("X-RateLimit-Window", m.config.RateLimit.Window.String())
|
||||
c.Header("Retry-After", "60")
|
||||
|
||||
c.JSON(http.StatusTooManyRequests, gin.H{
|
||||
"success": false,
|
||||
"message": "Rate limit exceeded",
|
||||
"error": "Too many requests",
|
||||
})
|
||||
// 使用统一的响应格式
|
||||
m.response.TooManyRequests(c, "请求过于频繁,请稍后再试")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2,23 +2,35 @@ package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/shared/tracing"
|
||||
)
|
||||
|
||||
// RequestLoggerMiddleware 请求日志中间件
|
||||
type RequestLoggerMiddleware struct {
|
||||
logger *zap.Logger
|
||||
logger *zap.Logger
|
||||
useColoredLog bool
|
||||
isDevelopment bool
|
||||
tracer *tracing.Tracer
|
||||
}
|
||||
|
||||
// NewRequestLoggerMiddleware 创建请求日志中间件
|
||||
func NewRequestLoggerMiddleware(logger *zap.Logger) *RequestLoggerMiddleware {
|
||||
func NewRequestLoggerMiddleware(logger *zap.Logger, isDevelopment bool, tracer *tracing.Tracer) *RequestLoggerMiddleware {
|
||||
return &RequestLoggerMiddleware{
|
||||
logger: logger,
|
||||
logger: logger,
|
||||
useColoredLog: isDevelopment, // 开发环境使用彩色日志
|
||||
isDevelopment: isDevelopment,
|
||||
tracer: tracer,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,24 +46,110 @@ func (m *RequestLoggerMiddleware) GetPriority() int {
|
||||
|
||||
// Handle 返回中间件处理函数
|
||||
func (m *RequestLoggerMiddleware) Handle() gin.HandlerFunc {
|
||||
return gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
|
||||
// 使用zap logger记录请求信息
|
||||
m.logger.Info("HTTP Request",
|
||||
zap.String("client_ip", param.ClientIP),
|
||||
zap.String("method", param.Method),
|
||||
zap.String("path", param.Path),
|
||||
zap.String("protocol", param.Request.Proto),
|
||||
zap.Int("status_code", param.StatusCode),
|
||||
zap.Duration("latency", param.Latency),
|
||||
zap.String("user_agent", param.Request.UserAgent()),
|
||||
zap.Int("body_size", param.BodySize),
|
||||
zap.String("referer", param.Request.Referer()),
|
||||
zap.String("request_id", param.Request.Header.Get("X-Request-ID")),
|
||||
)
|
||||
if m.useColoredLog {
|
||||
// 开发环境:使用Gin默认的彩色日志格式
|
||||
return gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
|
||||
var statusColor, methodColor, resetColor string
|
||||
if param.IsOutputColor() {
|
||||
statusColor = param.StatusCodeColor()
|
||||
methodColor = param.MethodColor()
|
||||
resetColor = param.ResetColor()
|
||||
}
|
||||
|
||||
// 返回空字符串,因为我们已经用zap记录了
|
||||
return ""
|
||||
})
|
||||
if param.Latency > time.Minute {
|
||||
param.Latency = param.Latency.Truncate(time.Second)
|
||||
}
|
||||
|
||||
// 获取TraceID
|
||||
traceID := param.Request.Header.Get("X-Trace-ID")
|
||||
if traceID == "" && m.tracer != nil {
|
||||
traceID = m.tracer.GetTraceID(param.Request.Context())
|
||||
}
|
||||
|
||||
// 检查是否为错误响应
|
||||
if param.StatusCode >= 400 && m.tracer != nil {
|
||||
span := trace.SpanFromContext(param.Request.Context())
|
||||
if span.IsRecording() {
|
||||
// 标记为错误操作,确保100%采样
|
||||
span.SetAttributes(
|
||||
attribute.String("error.operation", "true"),
|
||||
attribute.String("operation.type", "error"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
traceInfo := ""
|
||||
if traceID != "" {
|
||||
traceInfo = fmt.Sprintf(" | TraceID: %s", traceID)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("[GIN] %v |%s %3d %s| %13v | %15s |%s %-7s %s %#v%s\n%s",
|
||||
param.TimeStamp.Format("2006/01/02 - 15:04:05"),
|
||||
statusColor, param.StatusCode, resetColor,
|
||||
param.Latency,
|
||||
param.ClientIP,
|
||||
methodColor, param.Method, resetColor,
|
||||
param.Path,
|
||||
traceInfo,
|
||||
param.ErrorMessage,
|
||||
)
|
||||
})
|
||||
} else {
|
||||
// 生产环境:使用结构化JSON日志
|
||||
return gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
|
||||
// 获取TraceID
|
||||
traceID := param.Request.Header.Get("X-Trace-ID")
|
||||
if traceID == "" && m.tracer != nil {
|
||||
traceID = m.tracer.GetTraceID(param.Request.Context())
|
||||
}
|
||||
|
||||
// 检查是否为错误响应
|
||||
if param.StatusCode >= 400 && m.tracer != nil {
|
||||
span := trace.SpanFromContext(param.Request.Context())
|
||||
if span.IsRecording() {
|
||||
// 标记为错误操作,确保100%采样
|
||||
span.SetAttributes(
|
||||
attribute.String("error.operation", "true"),
|
||||
attribute.String("operation.type", "error"),
|
||||
)
|
||||
|
||||
// 对于服务器错误,记录更详细的日志
|
||||
if param.StatusCode >= 500 {
|
||||
m.logger.Error("服务器错误",
|
||||
zap.Int("status_code", param.StatusCode),
|
||||
zap.String("method", param.Method),
|
||||
zap.String("path", param.Path),
|
||||
zap.Duration("latency", param.Latency),
|
||||
zap.String("client_ip", param.ClientIP),
|
||||
zap.String("trace_id", traceID),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 记录请求日志
|
||||
logFields := []zap.Field{
|
||||
zap.String("client_ip", param.ClientIP),
|
||||
zap.String("method", param.Method),
|
||||
zap.String("path", param.Path),
|
||||
zap.String("protocol", param.Request.Proto),
|
||||
zap.Int("status_code", param.StatusCode),
|
||||
zap.Duration("latency", param.Latency),
|
||||
zap.String("user_agent", param.Request.UserAgent()),
|
||||
zap.Int("body_size", param.BodySize),
|
||||
zap.String("referer", param.Request.Referer()),
|
||||
zap.String("request_id", param.Request.Header.Get("X-Request-ID")),
|
||||
}
|
||||
|
||||
// 添加TraceID
|
||||
if traceID != "" {
|
||||
logFields = append(logFields, zap.String("trace_id", traceID))
|
||||
}
|
||||
|
||||
m.logger.Info("HTTP请求", logFields...)
|
||||
return ""
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// IsGlobal 是否为全局中间件
|
||||
@@ -102,6 +200,70 @@ func (m *RequestIDMiddleware) IsGlobal() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// TraceIDMiddleware 追踪ID中间件
|
||||
type TraceIDMiddleware struct {
|
||||
tracer *tracing.Tracer
|
||||
}
|
||||
|
||||
// NewTraceIDMiddleware 创建追踪ID中间件
|
||||
func NewTraceIDMiddleware(tracer *tracing.Tracer) *TraceIDMiddleware {
|
||||
return &TraceIDMiddleware{
|
||||
tracer: tracer,
|
||||
}
|
||||
}
|
||||
|
||||
// GetName 返回中间件名称
|
||||
func (m *TraceIDMiddleware) GetName() string {
|
||||
return "trace_id"
|
||||
}
|
||||
|
||||
// GetPriority 返回中间件优先级
|
||||
func (m *TraceIDMiddleware) GetPriority() int {
|
||||
return 94 // 仅次于请求ID中间件
|
||||
}
|
||||
|
||||
// Handle 返回中间件处理函数
|
||||
func (m *TraceIDMiddleware) Handle() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 获取或生成追踪ID
|
||||
traceID := m.tracer.GetTraceID(c.Request.Context())
|
||||
if traceID != "" {
|
||||
// 设置追踪ID到响应头
|
||||
c.Header("X-Trace-ID", traceID)
|
||||
// 添加到上下文
|
||||
c.Set("trace_id", traceID)
|
||||
}
|
||||
|
||||
// 检查是否为错误请求(例如URL不存在)
|
||||
c.Next()
|
||||
|
||||
// 请求完成后检查状态码
|
||||
if c.Writer.Status() >= 400 {
|
||||
// 获取当前span
|
||||
span := trace.SpanFromContext(c.Request.Context())
|
||||
if span.IsRecording() {
|
||||
// 标记为错误操作,确保100%采样
|
||||
span.SetAttributes(
|
||||
attribute.String("error.operation", "true"),
|
||||
attribute.String("operation.type", "error"),
|
||||
)
|
||||
|
||||
// 设置错误上下文,以便后续span可以识别
|
||||
c.Request = c.Request.WithContext(context.WithValue(
|
||||
c.Request.Context(),
|
||||
"otel_error_request",
|
||||
true,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IsGlobal 是否为全局中间件
|
||||
func (m *TraceIDMiddleware) IsGlobal() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// SecurityHeadersMiddleware 安全头部中间件
|
||||
type SecurityHeadersMiddleware struct{}
|
||||
|
||||
@@ -183,13 +345,15 @@ func (m *ResponseTimeMiddleware) IsGlobal() bool {
|
||||
type RequestBodyLoggerMiddleware struct {
|
||||
logger *zap.Logger
|
||||
enable bool
|
||||
tracer *tracing.Tracer
|
||||
}
|
||||
|
||||
// NewRequestBodyLoggerMiddleware 创建请求体日志中间件
|
||||
func NewRequestBodyLoggerMiddleware(logger *zap.Logger, enable bool) *RequestBodyLoggerMiddleware {
|
||||
func NewRequestBodyLoggerMiddleware(logger *zap.Logger, enable bool, tracer *tracing.Tracer) *RequestBodyLoggerMiddleware {
|
||||
return &RequestBodyLoggerMiddleware{
|
||||
logger: logger,
|
||||
enable: enable,
|
||||
tracer: tracer,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,13 +384,26 @@ func (m *RequestBodyLoggerMiddleware) Handle() gin.HandlerFunc {
|
||||
// 重新设置body供后续处理使用
|
||||
c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
|
||||
// 获取追踪ID
|
||||
traceID := ""
|
||||
if m.tracer != nil {
|
||||
traceID = m.tracer.GetTraceID(c.Request.Context())
|
||||
}
|
||||
|
||||
// 记录请求体(注意:生产环境中应该谨慎记录敏感信息)
|
||||
m.logger.Debug("Request Body",
|
||||
logFields := []zap.Field{
|
||||
zap.String("method", c.Request.Method),
|
||||
zap.String("path", c.Request.URL.Path),
|
||||
zap.String("body", string(bodyBytes)),
|
||||
zap.String("request_id", c.GetString("request_id")),
|
||||
)
|
||||
}
|
||||
|
||||
// 添加追踪ID
|
||||
if traceID != "" {
|
||||
logFields = append(logFields, zap.String("trace_id", traceID))
|
||||
}
|
||||
|
||||
m.logger.Debug("请求体", logFields...)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -239,3 +416,83 @@ func (m *RequestBodyLoggerMiddleware) Handle() gin.HandlerFunc {
|
||||
func (m *RequestBodyLoggerMiddleware) IsGlobal() bool {
|
||||
return false // 可选中间件,不是全局的
|
||||
}
|
||||
|
||||
// ErrorTrackingMiddleware 错误追踪中间件
|
||||
type ErrorTrackingMiddleware struct {
|
||||
logger *zap.Logger
|
||||
tracer *tracing.Tracer
|
||||
}
|
||||
|
||||
// NewErrorTrackingMiddleware 创建错误追踪中间件
|
||||
func NewErrorTrackingMiddleware(logger *zap.Logger, tracer *tracing.Tracer) *ErrorTrackingMiddleware {
|
||||
return &ErrorTrackingMiddleware{
|
||||
logger: logger,
|
||||
tracer: tracer,
|
||||
}
|
||||
}
|
||||
|
||||
// GetName 返回中间件名称
|
||||
func (m *ErrorTrackingMiddleware) GetName() string {
|
||||
return "error_tracking"
|
||||
}
|
||||
|
||||
// GetPriority 返回中间件优先级
|
||||
func (m *ErrorTrackingMiddleware) GetPriority() int {
|
||||
return 60 // 低优先级,在大多数中间件之后执行
|
||||
}
|
||||
|
||||
// Handle 返回中间件处理函数
|
||||
func (m *ErrorTrackingMiddleware) Handle() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Next()
|
||||
|
||||
// 检查是否有错误
|
||||
if len(c.Errors) > 0 || c.Writer.Status() >= 400 {
|
||||
// 获取当前span
|
||||
span := trace.SpanFromContext(c.Request.Context())
|
||||
if span.IsRecording() {
|
||||
// 标记为错误操作,确保100%采样
|
||||
span.SetAttributes(
|
||||
attribute.String("error.operation", "true"),
|
||||
attribute.String("operation.type", "error"),
|
||||
)
|
||||
|
||||
// 记录错误日志
|
||||
traceID := m.tracer.GetTraceID(c.Request.Context())
|
||||
spanID := m.tracer.GetSpanID(c.Request.Context())
|
||||
|
||||
logFields := []zap.Field{
|
||||
zap.Int("status_code", c.Writer.Status()),
|
||||
zap.String("method", c.Request.Method),
|
||||
zap.String("path", c.FullPath()),
|
||||
zap.String("client_ip", c.ClientIP()),
|
||||
}
|
||||
|
||||
// 添加追踪信息
|
||||
if traceID != "" {
|
||||
logFields = append(logFields, zap.String("trace_id", traceID))
|
||||
}
|
||||
if spanID != "" {
|
||||
logFields = append(logFields, zap.String("span_id", spanID))
|
||||
}
|
||||
|
||||
// 添加错误信息
|
||||
if len(c.Errors) > 0 {
|
||||
logFields = append(logFields, zap.String("errors", c.Errors.String()))
|
||||
}
|
||||
|
||||
// 根据状态码决定日志级别
|
||||
if c.Writer.Status() >= 500 {
|
||||
m.logger.Error("服务器错误", logFields...)
|
||||
} else {
|
||||
m.logger.Warn("客户端错误", logFields...)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IsGlobal 是否为全局中间件
|
||||
func (m *ErrorTrackingMiddleware) IsGlobal() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
389
internal/shared/resilience/circuit_breaker.go
Normal file
389
internal/shared/resilience/circuit_breaker.go
Normal file
@@ -0,0 +1,389 @@
|
||||
package resilience
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// CircuitState 熔断器状态
|
||||
type CircuitState int
|
||||
|
||||
const (
|
||||
// StateClosed 关闭状态(正常)
|
||||
StateClosed CircuitState = iota
|
||||
// StateOpen 开启状态(熔断)
|
||||
StateOpen
|
||||
// StateHalfOpen 半开状态(测试)
|
||||
StateHalfOpen
|
||||
)
|
||||
|
||||
func (s CircuitState) String() string {
|
||||
switch s {
|
||||
case StateClosed:
|
||||
return "CLOSED"
|
||||
case StateOpen:
|
||||
return "OPEN"
|
||||
case StateHalfOpen:
|
||||
return "HALF_OPEN"
|
||||
default:
|
||||
return "UNKNOWN"
|
||||
}
|
||||
}
|
||||
|
||||
// CircuitBreakerConfig 熔断器配置
|
||||
type CircuitBreakerConfig struct {
|
||||
// 故障阈值
|
||||
FailureThreshold int
|
||||
// 重置超时时间
|
||||
ResetTimeout time.Duration
|
||||
// 检测窗口大小
|
||||
WindowSize int
|
||||
// 半开状态允许的请求数
|
||||
HalfOpenMaxRequests int
|
||||
// 成功阈值(半开->关闭)
|
||||
SuccessThreshold int
|
||||
}
|
||||
|
||||
// DefaultCircuitBreakerConfig 默认熔断器配置
|
||||
func DefaultCircuitBreakerConfig() CircuitBreakerConfig {
|
||||
return CircuitBreakerConfig{
|
||||
FailureThreshold: 5,
|
||||
ResetTimeout: 60 * time.Second,
|
||||
WindowSize: 10,
|
||||
HalfOpenMaxRequests: 3,
|
||||
SuccessThreshold: 2,
|
||||
}
|
||||
}
|
||||
|
||||
// CircuitBreaker 熔断器
|
||||
type CircuitBreaker struct {
|
||||
config CircuitBreakerConfig
|
||||
logger *zap.Logger
|
||||
mutex sync.RWMutex
|
||||
|
||||
// 状态
|
||||
state CircuitState
|
||||
|
||||
// 计数器
|
||||
failures int
|
||||
successes int
|
||||
requests int
|
||||
consecutiveFailures int
|
||||
|
||||
// 时间记录
|
||||
lastFailTime time.Time
|
||||
lastStateChange time.Time
|
||||
|
||||
// 统计窗口
|
||||
window []bool // true=success, false=failure
|
||||
windowIndex int
|
||||
windowFull bool
|
||||
|
||||
// 事件回调
|
||||
onStateChange func(from, to CircuitState)
|
||||
}
|
||||
|
||||
// NewCircuitBreaker 创建熔断器
|
||||
func NewCircuitBreaker(config CircuitBreakerConfig, logger *zap.Logger) *CircuitBreaker {
|
||||
cb := &CircuitBreaker{
|
||||
config: config,
|
||||
logger: logger,
|
||||
state: StateClosed,
|
||||
window: make([]bool, config.WindowSize),
|
||||
lastStateChange: time.Now(),
|
||||
}
|
||||
|
||||
return cb
|
||||
}
|
||||
|
||||
// Execute 执行函数,如果熔断器开启则快速失败
|
||||
func (cb *CircuitBreaker) Execute(ctx context.Context, fn func() error) error {
|
||||
// 检查是否允许执行
|
||||
if !cb.allowRequest() {
|
||||
return ErrCircuitBreakerOpen
|
||||
}
|
||||
|
||||
// 执行函数
|
||||
start := time.Now()
|
||||
err := fn()
|
||||
duration := time.Since(start)
|
||||
|
||||
// 记录结果
|
||||
cb.recordResult(err == nil, duration)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// allowRequest 检查是否允许请求
|
||||
func (cb *CircuitBreaker) allowRequest() bool {
|
||||
cb.mutex.Lock()
|
||||
defer cb.mutex.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
|
||||
switch cb.state {
|
||||
case StateClosed:
|
||||
return true
|
||||
|
||||
case StateOpen:
|
||||
// 检查是否到了重置时间
|
||||
if now.Sub(cb.lastStateChange) > cb.config.ResetTimeout {
|
||||
cb.setState(StateHalfOpen)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
case StateHalfOpen:
|
||||
// 半开状态下限制请求数
|
||||
return cb.requests < cb.config.HalfOpenMaxRequests
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// recordResult 记录执行结果
|
||||
func (cb *CircuitBreaker) recordResult(success bool, duration time.Duration) {
|
||||
cb.mutex.Lock()
|
||||
defer cb.mutex.Unlock()
|
||||
|
||||
cb.requests++
|
||||
|
||||
// 更新滑动窗口
|
||||
cb.updateWindow(success)
|
||||
|
||||
if success {
|
||||
cb.successes++
|
||||
cb.consecutiveFailures = 0
|
||||
cb.onSuccess()
|
||||
} else {
|
||||
cb.failures++
|
||||
cb.consecutiveFailures++
|
||||
cb.lastFailTime = time.Now()
|
||||
cb.onFailure()
|
||||
}
|
||||
|
||||
cb.logger.Debug("Circuit breaker recorded result",
|
||||
zap.Bool("success", success),
|
||||
zap.Duration("duration", duration),
|
||||
zap.String("state", cb.state.String()),
|
||||
zap.Int("failures", cb.failures),
|
||||
zap.Int("successes", cb.successes))
|
||||
}
|
||||
|
||||
// updateWindow 更新滑动窗口
|
||||
func (cb *CircuitBreaker) updateWindow(success bool) {
|
||||
cb.window[cb.windowIndex] = success
|
||||
cb.windowIndex = (cb.windowIndex + 1) % cb.config.WindowSize
|
||||
|
||||
if cb.windowIndex == 0 {
|
||||
cb.windowFull = true
|
||||
}
|
||||
}
|
||||
|
||||
// onSuccess 成功时的处理
|
||||
func (cb *CircuitBreaker) onSuccess() {
|
||||
if cb.state == StateHalfOpen {
|
||||
// 半开状态下,如果成功次数达到阈值,则关闭熔断器
|
||||
if cb.successes >= cb.config.SuccessThreshold {
|
||||
cb.setState(StateClosed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// onFailure 失败时的处理
|
||||
func (cb *CircuitBreaker) onFailure() {
|
||||
if cb.state == StateClosed {
|
||||
// 关闭状态下,检查是否需要开启熔断器
|
||||
if cb.shouldTrip() {
|
||||
cb.setState(StateOpen)
|
||||
}
|
||||
} else if cb.state == StateHalfOpen {
|
||||
// 半开状态下,如果失败则立即开启熔断器
|
||||
cb.setState(StateOpen)
|
||||
}
|
||||
}
|
||||
|
||||
// shouldTrip 检查是否应该触发熔断
|
||||
func (cb *CircuitBreaker) shouldTrip() bool {
|
||||
// 基于连续失败次数
|
||||
if cb.consecutiveFailures >= cb.config.FailureThreshold {
|
||||
return true
|
||||
}
|
||||
|
||||
// 基于滑动窗口的失败率
|
||||
if cb.windowFull {
|
||||
failures := 0
|
||||
for _, success := range cb.window {
|
||||
if !success {
|
||||
failures++
|
||||
}
|
||||
}
|
||||
|
||||
failureRate := float64(failures) / float64(cb.config.WindowSize)
|
||||
return failureRate >= 0.5 // 50%失败率
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// setState 设置状态
|
||||
func (cb *CircuitBreaker) setState(newState CircuitState) {
|
||||
if cb.state == newState {
|
||||
return
|
||||
}
|
||||
|
||||
oldState := cb.state
|
||||
cb.state = newState
|
||||
cb.lastStateChange = time.Now()
|
||||
|
||||
// 重置计数器
|
||||
if newState == StateClosed {
|
||||
cb.requests = 0
|
||||
cb.failures = 0
|
||||
cb.successes = 0
|
||||
cb.consecutiveFailures = 0
|
||||
} else if newState == StateHalfOpen {
|
||||
cb.requests = 0
|
||||
cb.successes = 0
|
||||
}
|
||||
|
||||
cb.logger.Info("Circuit breaker state changed",
|
||||
zap.String("from", oldState.String()),
|
||||
zap.String("to", newState.String()),
|
||||
zap.Int("failures", cb.failures),
|
||||
zap.Int("successes", cb.successes))
|
||||
|
||||
// 触发状态变更回调
|
||||
if cb.onStateChange != nil {
|
||||
cb.onStateChange(oldState, newState)
|
||||
}
|
||||
}
|
||||
|
||||
// GetState 获取当前状态
|
||||
func (cb *CircuitBreaker) GetState() CircuitState {
|
||||
cb.mutex.RLock()
|
||||
defer cb.mutex.RUnlock()
|
||||
return cb.state
|
||||
}
|
||||
|
||||
// GetStats 获取统计信息
|
||||
func (cb *CircuitBreaker) GetStats() CircuitBreakerStats {
|
||||
cb.mutex.RLock()
|
||||
defer cb.mutex.RUnlock()
|
||||
|
||||
return CircuitBreakerStats{
|
||||
State: cb.state.String(),
|
||||
Failures: cb.failures,
|
||||
Successes: cb.successes,
|
||||
Requests: cb.requests,
|
||||
ConsecutiveFailures: cb.consecutiveFailures,
|
||||
LastFailTime: cb.lastFailTime,
|
||||
LastStateChange: cb.lastStateChange,
|
||||
FailureThreshold: cb.config.FailureThreshold,
|
||||
ResetTimeout: cb.config.ResetTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
// Reset 重置熔断器
|
||||
func (cb *CircuitBreaker) Reset() {
|
||||
cb.mutex.Lock()
|
||||
defer cb.mutex.Unlock()
|
||||
|
||||
cb.setState(StateClosed)
|
||||
cb.window = make([]bool, cb.config.WindowSize)
|
||||
cb.windowIndex = 0
|
||||
cb.windowFull = false
|
||||
|
||||
cb.logger.Info("Circuit breaker reset")
|
||||
}
|
||||
|
||||
// SetStateChangeCallback 设置状态变更回调
|
||||
func (cb *CircuitBreaker) SetStateChangeCallback(callback func(from, to CircuitState)) {
|
||||
cb.mutex.Lock()
|
||||
defer cb.mutex.Unlock()
|
||||
cb.onStateChange = callback
|
||||
}
|
||||
|
||||
// CircuitBreakerStats 熔断器统计信息
|
||||
type CircuitBreakerStats struct {
|
||||
State string `json:"state"`
|
||||
Failures int `json:"failures"`
|
||||
Successes int `json:"successes"`
|
||||
Requests int `json:"requests"`
|
||||
ConsecutiveFailures int `json:"consecutive_failures"`
|
||||
LastFailTime time.Time `json:"last_fail_time"`
|
||||
LastStateChange time.Time `json:"last_state_change"`
|
||||
FailureThreshold int `json:"failure_threshold"`
|
||||
ResetTimeout time.Duration `json:"reset_timeout"`
|
||||
}
|
||||
|
||||
// 预定义错误
|
||||
var (
|
||||
ErrCircuitBreakerOpen = errors.New("circuit breaker is open")
|
||||
)
|
||||
|
||||
// Wrapper 熔断器包装器
|
||||
type Wrapper struct {
|
||||
breakers map[string]*CircuitBreaker
|
||||
logger *zap.Logger
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
// NewWrapper 创建熔断器包装器
|
||||
func NewWrapper(logger *zap.Logger) *Wrapper {
|
||||
return &Wrapper{
|
||||
breakers: make(map[string]*CircuitBreaker),
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// GetOrCreate 获取或创建熔断器
|
||||
func (w *Wrapper) GetOrCreate(name string, config CircuitBreakerConfig) *CircuitBreaker {
|
||||
w.mutex.Lock()
|
||||
defer w.mutex.Unlock()
|
||||
|
||||
if cb, exists := w.breakers[name]; exists {
|
||||
return cb
|
||||
}
|
||||
|
||||
cb := NewCircuitBreaker(config, w.logger.Named(name))
|
||||
w.breakers[name] = cb
|
||||
|
||||
w.logger.Info("Created circuit breaker", zap.String("name", name))
|
||||
return cb
|
||||
}
|
||||
|
||||
// Execute 执行带熔断器的函数
|
||||
func (w *Wrapper) Execute(ctx context.Context, name string, fn func() error) error {
|
||||
cb := w.GetOrCreate(name, DefaultCircuitBreakerConfig())
|
||||
return cb.Execute(ctx, fn)
|
||||
}
|
||||
|
||||
// GetStats 获取所有熔断器统计
|
||||
func (w *Wrapper) GetStats() map[string]CircuitBreakerStats {
|
||||
w.mutex.RLock()
|
||||
defer w.mutex.RUnlock()
|
||||
|
||||
stats := make(map[string]CircuitBreakerStats)
|
||||
for name, cb := range w.breakers {
|
||||
stats[name] = cb.GetStats()
|
||||
}
|
||||
|
||||
return stats
|
||||
}
|
||||
|
||||
// ResetAll 重置所有熔断器
|
||||
func (w *Wrapper) ResetAll() {
|
||||
w.mutex.RLock()
|
||||
defer w.mutex.RUnlock()
|
||||
|
||||
for name, cb := range w.breakers {
|
||||
cb.Reset()
|
||||
w.logger.Info("Reset circuit breaker", zap.String("name", name))
|
||||
}
|
||||
}
|
||||
467
internal/shared/resilience/retry.go
Normal file
467
internal/shared/resilience/retry.go
Normal file
@@ -0,0 +1,467 @@
|
||||
package resilience
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// RetryConfig 重试配置
|
||||
type RetryConfig struct {
|
||||
// 最大重试次数
|
||||
MaxAttempts int
|
||||
// 初始延迟
|
||||
InitialDelay time.Duration
|
||||
// 最大延迟
|
||||
MaxDelay time.Duration
|
||||
// 退避倍数
|
||||
BackoffMultiplier float64
|
||||
// 抖动系数
|
||||
JitterFactor float64
|
||||
// 重试条件
|
||||
RetryCondition func(error) bool
|
||||
// 延迟函数
|
||||
DelayFunc func(attempt int, config RetryConfig) time.Duration
|
||||
}
|
||||
|
||||
// DefaultRetryConfig 默认重试配置
|
||||
func DefaultRetryConfig() RetryConfig {
|
||||
return RetryConfig{
|
||||
MaxAttempts: 3,
|
||||
InitialDelay: 100 * time.Millisecond,
|
||||
MaxDelay: 5 * time.Second,
|
||||
BackoffMultiplier: 2.0,
|
||||
JitterFactor: 0.1,
|
||||
RetryCondition: DefaultRetryCondition,
|
||||
DelayFunc: ExponentialBackoffWithJitter,
|
||||
}
|
||||
}
|
||||
|
||||
// RetryableError 可重试错误接口
|
||||
type RetryableError interface {
|
||||
error
|
||||
IsRetryable() bool
|
||||
}
|
||||
|
||||
// DefaultRetryCondition 默认重试条件
|
||||
func DefaultRetryCondition(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查是否实现了RetryableError接口
|
||||
if retryable, ok := err.(RetryableError); ok {
|
||||
return retryable.IsRetryable()
|
||||
}
|
||||
|
||||
// 默认所有错误都重试
|
||||
return true
|
||||
}
|
||||
|
||||
// IsRetryableHTTPError HTTP错误重试条件
|
||||
func IsRetryableHTTPError(statusCode int) bool {
|
||||
// 5xx错误通常可以重试
|
||||
// 429(Too Many Requests)也可以重试
|
||||
return statusCode >= 500 || statusCode == 429
|
||||
}
|
||||
|
||||
// DelayFunction 延迟函数类型
|
||||
type DelayFunction func(attempt int, config RetryConfig) time.Duration
|
||||
|
||||
// FixedDelay 固定延迟
|
||||
func FixedDelay(attempt int, config RetryConfig) time.Duration {
|
||||
return config.InitialDelay
|
||||
}
|
||||
|
||||
// LinearBackoff 线性退避
|
||||
func LinearBackoff(attempt int, config RetryConfig) time.Duration {
|
||||
delay := time.Duration(attempt) * config.InitialDelay
|
||||
if delay > config.MaxDelay {
|
||||
delay = config.MaxDelay
|
||||
}
|
||||
return delay
|
||||
}
|
||||
|
||||
// ExponentialBackoff 指数退避
|
||||
func ExponentialBackoff(attempt int, config RetryConfig) time.Duration {
|
||||
delay := config.InitialDelay
|
||||
for i := 0; i < attempt; i++ {
|
||||
delay = time.Duration(float64(delay) * config.BackoffMultiplier)
|
||||
}
|
||||
|
||||
if delay > config.MaxDelay {
|
||||
delay = config.MaxDelay
|
||||
}
|
||||
|
||||
return delay
|
||||
}
|
||||
|
||||
// ExponentialBackoffWithJitter 带抖动的指数退避
|
||||
func ExponentialBackoffWithJitter(attempt int, config RetryConfig) time.Duration {
|
||||
delay := ExponentialBackoff(attempt, config)
|
||||
|
||||
// 添加抖动
|
||||
jitter := config.JitterFactor
|
||||
if jitter > 0 {
|
||||
jitterRange := float64(delay) * jitter
|
||||
jitterOffset := (rand.Float64() - 0.5) * 2 * jitterRange
|
||||
delay = time.Duration(float64(delay) + jitterOffset)
|
||||
}
|
||||
|
||||
if delay < 0 {
|
||||
delay = config.InitialDelay
|
||||
}
|
||||
|
||||
return delay
|
||||
}
|
||||
|
||||
// RetryStats 重试统计
|
||||
type RetryStats struct {
|
||||
TotalAttempts int `json:"total_attempts"`
|
||||
Successes int `json:"successes"`
|
||||
Failures int `json:"failures"`
|
||||
TotalRetries int `json:"total_retries"`
|
||||
AverageAttempts float64 `json:"average_attempts"`
|
||||
TotalDelay time.Duration `json:"total_delay"`
|
||||
LastError string `json:"last_error,omitempty"`
|
||||
}
|
||||
|
||||
// Retryer 重试器
|
||||
type Retryer struct {
|
||||
config RetryConfig
|
||||
logger *zap.Logger
|
||||
stats RetryStats
|
||||
}
|
||||
|
||||
// NewRetryer 创建重试器
|
||||
func NewRetryer(config RetryConfig, logger *zap.Logger) *Retryer {
|
||||
if config.DelayFunc == nil {
|
||||
config.DelayFunc = ExponentialBackoffWithJitter
|
||||
}
|
||||
if config.RetryCondition == nil {
|
||||
config.RetryCondition = DefaultRetryCondition
|
||||
}
|
||||
|
||||
return &Retryer{
|
||||
config: config,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Execute 执行带重试的函数
|
||||
func (r *Retryer) Execute(ctx context.Context, operation func() error) error {
|
||||
return r.ExecuteWithResult(ctx, func() (interface{}, error) {
|
||||
return nil, operation()
|
||||
})
|
||||
}
|
||||
|
||||
// ExecuteWithResult 执行带重试和返回值的函数
|
||||
func (r *Retryer) ExecuteWithResult(ctx context.Context, operation func() (interface{}, error)) error {
|
||||
var lastErr error
|
||||
startTime := time.Now()
|
||||
|
||||
for attempt := 0; attempt < r.config.MaxAttempts; attempt++ {
|
||||
// 检查上下文是否被取消
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
// 执行操作
|
||||
attemptStart := time.Now()
|
||||
_, err := operation()
|
||||
attemptDuration := time.Since(attemptStart)
|
||||
|
||||
// 更新统计
|
||||
r.stats.TotalAttempts++
|
||||
if err == nil {
|
||||
r.stats.Successes++
|
||||
r.logger.Debug("Operation succeeded",
|
||||
zap.Int("attempt", attempt+1),
|
||||
zap.Duration("duration", attemptDuration))
|
||||
return nil
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
r.stats.Failures++
|
||||
if attempt > 0 {
|
||||
r.stats.TotalRetries++
|
||||
}
|
||||
|
||||
// 检查是否应该重试
|
||||
if !r.config.RetryCondition(err) {
|
||||
r.logger.Debug("Error is not retryable",
|
||||
zap.Error(err),
|
||||
zap.Int("attempt", attempt+1))
|
||||
break
|
||||
}
|
||||
|
||||
// 如果这是最后一次尝试,不需要延迟
|
||||
if attempt == r.config.MaxAttempts-1 {
|
||||
r.logger.Debug("Reached max attempts",
|
||||
zap.Error(err),
|
||||
zap.Int("max_attempts", r.config.MaxAttempts))
|
||||
break
|
||||
}
|
||||
|
||||
// 计算延迟
|
||||
delay := r.config.DelayFunc(attempt, r.config)
|
||||
r.stats.TotalDelay += delay
|
||||
|
||||
r.logger.Debug("Operation failed, retrying",
|
||||
zap.Error(err),
|
||||
zap.Int("attempt", attempt+1),
|
||||
zap.Duration("delay", delay),
|
||||
zap.Duration("attempt_duration", attemptDuration))
|
||||
|
||||
// 等待重试
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-time.After(delay):
|
||||
}
|
||||
}
|
||||
|
||||
// 更新最终统计
|
||||
totalDuration := time.Since(startTime)
|
||||
if r.stats.TotalAttempts > 0 {
|
||||
r.stats.AverageAttempts = float64(r.stats.TotalRetries) / float64(r.stats.Successes+r.stats.Failures)
|
||||
}
|
||||
if lastErr != nil {
|
||||
r.stats.LastError = lastErr.Error()
|
||||
}
|
||||
|
||||
r.logger.Warn("Operation failed after all retries",
|
||||
zap.Error(lastErr),
|
||||
zap.Int("total_attempts", r.stats.TotalAttempts),
|
||||
zap.Duration("total_duration", totalDuration))
|
||||
|
||||
return fmt.Errorf("operation failed after %d attempts: %w", r.config.MaxAttempts, lastErr)
|
||||
}
|
||||
|
||||
// GetStats 获取重试统计
|
||||
func (r *Retryer) GetStats() RetryStats {
|
||||
return r.stats
|
||||
}
|
||||
|
||||
// Reset 重置统计
|
||||
func (r *Retryer) Reset() {
|
||||
r.stats = RetryStats{}
|
||||
r.logger.Debug("Retry stats reset")
|
||||
}
|
||||
|
||||
// Retry 简单重试函数
|
||||
func Retry(ctx context.Context, config RetryConfig, operation func() error) error {
|
||||
retryer := NewRetryer(config, zap.NewNop())
|
||||
return retryer.Execute(ctx, operation)
|
||||
}
|
||||
|
||||
// RetryWithResult 带返回值的重试函数
|
||||
func RetryWithResult[T any](ctx context.Context, config RetryConfig, operation func() (T, error)) (T, error) {
|
||||
var result T
|
||||
var finalErr error
|
||||
|
||||
retryer := NewRetryer(config, zap.NewNop())
|
||||
err := retryer.ExecuteWithResult(ctx, func() (interface{}, error) {
|
||||
r, e := operation()
|
||||
result = r
|
||||
return r, e
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
finalErr = err
|
||||
}
|
||||
|
||||
return result, finalErr
|
||||
}
|
||||
|
||||
// 预定义的重试配置
|
||||
|
||||
// QuickRetry 快速重试(适用于轻量级操作)
|
||||
func QuickRetry() RetryConfig {
|
||||
return RetryConfig{
|
||||
MaxAttempts: 3,
|
||||
InitialDelay: 50 * time.Millisecond,
|
||||
MaxDelay: 500 * time.Millisecond,
|
||||
BackoffMultiplier: 2.0,
|
||||
JitterFactor: 0.1,
|
||||
RetryCondition: DefaultRetryCondition,
|
||||
DelayFunc: ExponentialBackoffWithJitter,
|
||||
}
|
||||
}
|
||||
|
||||
// StandardRetry 标准重试(适用于一般操作)
|
||||
func StandardRetry() RetryConfig {
|
||||
return DefaultRetryConfig()
|
||||
}
|
||||
|
||||
// PatientRetry 耐心重试(适用于重要操作)
|
||||
func PatientRetry() RetryConfig {
|
||||
return RetryConfig{
|
||||
MaxAttempts: 5,
|
||||
InitialDelay: 200 * time.Millisecond,
|
||||
MaxDelay: 10 * time.Second,
|
||||
BackoffMultiplier: 2.0,
|
||||
JitterFactor: 0.2,
|
||||
RetryCondition: DefaultRetryCondition,
|
||||
DelayFunc: ExponentialBackoffWithJitter,
|
||||
}
|
||||
}
|
||||
|
||||
// DatabaseRetry 数据库重试配置
|
||||
func DatabaseRetry() RetryConfig {
|
||||
return RetryConfig{
|
||||
MaxAttempts: 3,
|
||||
InitialDelay: 100 * time.Millisecond,
|
||||
MaxDelay: 2 * time.Second,
|
||||
BackoffMultiplier: 1.5,
|
||||
JitterFactor: 0.1,
|
||||
RetryCondition: func(err error) bool {
|
||||
// 这里可以根据具体的数据库错误类型判断
|
||||
// 例如:连接超时、临时网络错误等
|
||||
return DefaultRetryCondition(err)
|
||||
},
|
||||
DelayFunc: ExponentialBackoffWithJitter,
|
||||
}
|
||||
}
|
||||
|
||||
// HTTPRetry HTTP重试配置
|
||||
func HTTPRetry() RetryConfig {
|
||||
return RetryConfig{
|
||||
MaxAttempts: 3,
|
||||
InitialDelay: 200 * time.Millisecond,
|
||||
MaxDelay: 5 * time.Second,
|
||||
BackoffMultiplier: 2.0,
|
||||
JitterFactor: 0.15,
|
||||
RetryCondition: func(err error) bool {
|
||||
// HTTP相关的重试条件
|
||||
return DefaultRetryCondition(err)
|
||||
},
|
||||
DelayFunc: ExponentialBackoffWithJitter,
|
||||
}
|
||||
}
|
||||
|
||||
// RetryManager 重试管理器
|
||||
type RetryManager struct {
|
||||
retryers map[string]*Retryer
|
||||
logger *zap.Logger
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
// NewRetryManager 创建重试管理器
|
||||
func NewRetryManager(logger *zap.Logger) *RetryManager {
|
||||
return &RetryManager{
|
||||
retryers: make(map[string]*Retryer),
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// GetOrCreate 获取或创建重试器
|
||||
func (rm *RetryManager) GetOrCreate(name string, config RetryConfig) *Retryer {
|
||||
rm.mutex.Lock()
|
||||
defer rm.mutex.Unlock()
|
||||
|
||||
if retryer, exists := rm.retryers[name]; exists {
|
||||
return retryer
|
||||
}
|
||||
|
||||
retryer := NewRetryer(config, rm.logger.Named(name))
|
||||
rm.retryers[name] = retryer
|
||||
|
||||
rm.logger.Info("Created retryer", zap.String("name", name))
|
||||
return retryer
|
||||
}
|
||||
|
||||
// Execute 执行带重试的操作
|
||||
func (rm *RetryManager) Execute(ctx context.Context, name string, operation func() error) error {
|
||||
retryer := rm.GetOrCreate(name, DefaultRetryConfig())
|
||||
return retryer.Execute(ctx, operation)
|
||||
}
|
||||
|
||||
// GetStats 获取所有重试器统计
|
||||
func (rm *RetryManager) GetStats() map[string]RetryStats {
|
||||
rm.mutex.RLock()
|
||||
defer rm.mutex.RUnlock()
|
||||
|
||||
stats := make(map[string]RetryStats)
|
||||
for name, retryer := range rm.retryers {
|
||||
stats[name] = retryer.GetStats()
|
||||
}
|
||||
|
||||
return stats
|
||||
}
|
||||
|
||||
// ResetAll 重置所有重试器统计
|
||||
func (rm *RetryManager) ResetAll() {
|
||||
rm.mutex.RLock()
|
||||
defer rm.mutex.RUnlock()
|
||||
|
||||
for name, retryer := range rm.retryers {
|
||||
retryer.Reset()
|
||||
rm.logger.Info("Reset retryer stats", zap.String("name", name))
|
||||
}
|
||||
}
|
||||
|
||||
// RetryerWrapper 重试器包装器
|
||||
type RetryerWrapper struct {
|
||||
manager *RetryManager
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewRetryerWrapper 创建重试器包装器
|
||||
func NewRetryerWrapper(logger *zap.Logger) *RetryerWrapper {
|
||||
return &RetryerWrapper{
|
||||
manager: NewRetryManager(logger),
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// ExecuteWithQuickRetry 执行快速重试
|
||||
func (rw *RetryerWrapper) ExecuteWithQuickRetry(ctx context.Context, name string, operation func() error) error {
|
||||
retryer := rw.manager.GetOrCreate(name+".quick", QuickRetry())
|
||||
return retryer.Execute(ctx, operation)
|
||||
}
|
||||
|
||||
// ExecuteWithStandardRetry 执行标准重试
|
||||
func (rw *RetryerWrapper) ExecuteWithStandardRetry(ctx context.Context, name string, operation func() error) error {
|
||||
retryer := rw.manager.GetOrCreate(name+".standard", StandardRetry())
|
||||
return retryer.Execute(ctx, operation)
|
||||
}
|
||||
|
||||
// ExecuteWithDatabaseRetry 执行数据库重试
|
||||
func (rw *RetryerWrapper) ExecuteWithDatabaseRetry(ctx context.Context, name string, operation func() error) error {
|
||||
retryer := rw.manager.GetOrCreate(name+".database", DatabaseRetry())
|
||||
return retryer.Execute(ctx, operation)
|
||||
}
|
||||
|
||||
// ExecuteWithHTTPRetry 执行HTTP重试
|
||||
func (rw *RetryerWrapper) ExecuteWithHTTPRetry(ctx context.Context, name string, operation func() error) error {
|
||||
retryer := rw.manager.GetOrCreate(name+".http", HTTPRetry())
|
||||
return retryer.Execute(ctx, operation)
|
||||
}
|
||||
|
||||
// ExecuteWithCustomRetry 执行自定义重试
|
||||
func (rw *RetryerWrapper) ExecuteWithCustomRetry(ctx context.Context, name string, config RetryConfig, operation func() error) error {
|
||||
retryer := rw.manager.GetOrCreate(name+".custom", config)
|
||||
return retryer.Execute(ctx, operation)
|
||||
}
|
||||
|
||||
// GetManager 获取重试管理器
|
||||
func (rw *RetryerWrapper) GetManager() *RetryManager {
|
||||
return rw.manager
|
||||
}
|
||||
|
||||
// GetAllStats 获取所有统计信息
|
||||
func (rw *RetryerWrapper) GetAllStats() map[string]RetryStats {
|
||||
return rw.manager.GetStats()
|
||||
}
|
||||
|
||||
// ResetAllStats 重置所有统计信息
|
||||
func (rw *RetryerWrapper) ResetAllStats() {
|
||||
rw.manager.ResetAll()
|
||||
}
|
||||
612
internal/shared/saga/saga.go
Normal file
612
internal/shared/saga/saga.go
Normal file
@@ -0,0 +1,612 @@
|
||||
package saga
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
)
|
||||
|
||||
// SagaStatus Saga状态
|
||||
type SagaStatus int
|
||||
|
||||
const (
|
||||
// StatusPending 等待中
|
||||
StatusPending SagaStatus = iota
|
||||
// StatusRunning 执行中
|
||||
StatusRunning
|
||||
// StatusCompleted 已完成
|
||||
StatusCompleted
|
||||
// StatusFailed 失败
|
||||
StatusFailed
|
||||
// StatusCompensating 补偿中
|
||||
StatusCompensating
|
||||
// StatusCompensated 已补偿
|
||||
StatusCompensated
|
||||
// StatusAborted 已中止
|
||||
StatusAborted
|
||||
)
|
||||
|
||||
func (s SagaStatus) String() string {
|
||||
switch s {
|
||||
case StatusPending:
|
||||
return "PENDING"
|
||||
case StatusRunning:
|
||||
return "RUNNING"
|
||||
case StatusCompleted:
|
||||
return "COMPLETED"
|
||||
case StatusFailed:
|
||||
return "FAILED"
|
||||
case StatusCompensating:
|
||||
return "COMPENSATING"
|
||||
case StatusCompensated:
|
||||
return "COMPENSATED"
|
||||
case StatusAborted:
|
||||
return "ABORTED"
|
||||
default:
|
||||
return "UNKNOWN"
|
||||
}
|
||||
}
|
||||
|
||||
// StepStatus 步骤状态
|
||||
type StepStatus int
|
||||
|
||||
const (
|
||||
// StepPending 等待执行
|
||||
StepPending StepStatus = iota
|
||||
// StepRunning 执行中
|
||||
StepRunning
|
||||
// StepCompleted 完成
|
||||
StepCompleted
|
||||
// StepFailed 失败
|
||||
StepFailed
|
||||
// StepCompensated 已补偿
|
||||
StepCompensated
|
||||
// StepSkipped 跳过
|
||||
StepSkipped
|
||||
)
|
||||
|
||||
func (s StepStatus) String() string {
|
||||
switch s {
|
||||
case StepPending:
|
||||
return "PENDING"
|
||||
case StepRunning:
|
||||
return "RUNNING"
|
||||
case StepCompleted:
|
||||
return "COMPLETED"
|
||||
case StepFailed:
|
||||
return "FAILED"
|
||||
case StepCompensated:
|
||||
return "COMPENSATED"
|
||||
case StepSkipped:
|
||||
return "SKIPPED"
|
||||
default:
|
||||
return "UNKNOWN"
|
||||
}
|
||||
}
|
||||
|
||||
// SagaStep Saga步骤
|
||||
type SagaStep struct {
|
||||
Name string
|
||||
Action func(ctx context.Context, data interface{}) error
|
||||
Compensate func(ctx context.Context, data interface{}) error
|
||||
Status StepStatus
|
||||
Error error
|
||||
StartTime time.Time
|
||||
EndTime time.Time
|
||||
RetryCount int
|
||||
MaxRetries int
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
// SagaConfig Saga配置
|
||||
type SagaConfig struct {
|
||||
// 默认超时时间
|
||||
DefaultTimeout time.Duration
|
||||
// 默认重试次数
|
||||
DefaultMaxRetries int
|
||||
// 是否并行执行(当前只支持串行)
|
||||
Parallel bool
|
||||
// 事件发布器
|
||||
EventBus interfaces.EventBus
|
||||
}
|
||||
|
||||
// DefaultSagaConfig 默认Saga配置
|
||||
func DefaultSagaConfig() SagaConfig {
|
||||
return SagaConfig{
|
||||
DefaultTimeout: 30 * time.Second,
|
||||
DefaultMaxRetries: 3,
|
||||
Parallel: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Saga 分布式事务
|
||||
type Saga struct {
|
||||
ID string
|
||||
Name string
|
||||
Steps []*SagaStep
|
||||
Status SagaStatus
|
||||
Data interface{}
|
||||
StartTime time.Time
|
||||
EndTime time.Time
|
||||
Error error
|
||||
Config SagaConfig
|
||||
logger *zap.Logger
|
||||
mutex sync.RWMutex
|
||||
currentStep int
|
||||
result interface{}
|
||||
}
|
||||
|
||||
// NewSaga 创建新的Saga
|
||||
func NewSaga(id, name string, config SagaConfig, logger *zap.Logger) *Saga {
|
||||
return &Saga{
|
||||
ID: id,
|
||||
Name: name,
|
||||
Steps: make([]*SagaStep, 0),
|
||||
Status: StatusPending,
|
||||
Config: config,
|
||||
logger: logger,
|
||||
currentStep: -1,
|
||||
}
|
||||
}
|
||||
|
||||
// AddStep 添加步骤
|
||||
func (s *Saga) AddStep(name string, action, compensate func(ctx context.Context, data interface{}) error) *Saga {
|
||||
step := &SagaStep{
|
||||
Name: name,
|
||||
Action: action,
|
||||
Compensate: compensate,
|
||||
Status: StepPending,
|
||||
MaxRetries: s.Config.DefaultMaxRetries,
|
||||
Timeout: s.Config.DefaultTimeout,
|
||||
}
|
||||
|
||||
s.mutex.Lock()
|
||||
s.Steps = append(s.Steps, step)
|
||||
s.mutex.Unlock()
|
||||
|
||||
s.logger.Debug("Added step to saga",
|
||||
zap.String("saga_id", s.ID),
|
||||
zap.String("step_name", name))
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// AddStepWithConfig 添加带配置的步骤
|
||||
func (s *Saga) AddStepWithConfig(name string, action, compensate func(ctx context.Context, data interface{}) error, maxRetries int, timeout time.Duration) *Saga {
|
||||
step := &SagaStep{
|
||||
Name: name,
|
||||
Action: action,
|
||||
Compensate: compensate,
|
||||
Status: StepPending,
|
||||
MaxRetries: maxRetries,
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
s.mutex.Lock()
|
||||
s.Steps = append(s.Steps, step)
|
||||
s.mutex.Unlock()
|
||||
|
||||
s.logger.Debug("Added step with config to saga",
|
||||
zap.String("saga_id", s.ID),
|
||||
zap.String("step_name", name),
|
||||
zap.Int("max_retries", maxRetries),
|
||||
zap.Duration("timeout", timeout))
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// Execute 执行Saga
|
||||
func (s *Saga) Execute(ctx context.Context, data interface{}) error {
|
||||
s.mutex.Lock()
|
||||
if s.Status != StatusPending {
|
||||
s.mutex.Unlock()
|
||||
return fmt.Errorf("saga %s is not in pending status", s.ID)
|
||||
}
|
||||
|
||||
s.Status = StatusRunning
|
||||
s.Data = data
|
||||
s.StartTime = time.Now()
|
||||
s.mutex.Unlock()
|
||||
|
||||
s.logger.Info("Starting saga execution",
|
||||
zap.String("saga_id", s.ID),
|
||||
zap.String("saga_name", s.Name),
|
||||
zap.Int("total_steps", len(s.Steps)))
|
||||
|
||||
// 发布Saga开始事件
|
||||
s.publishEvent(ctx, "saga.started")
|
||||
|
||||
// 执行所有步骤
|
||||
for i, step := range s.Steps {
|
||||
s.mutex.Lock()
|
||||
s.currentStep = i
|
||||
s.mutex.Unlock()
|
||||
|
||||
if err := s.executeStep(ctx, step, data); err != nil {
|
||||
s.logger.Error("Step execution failed",
|
||||
zap.String("saga_id", s.ID),
|
||||
zap.String("step_name", step.Name),
|
||||
zap.Error(err))
|
||||
|
||||
// 执行补偿
|
||||
if compensateErr := s.compensate(ctx, i-1); compensateErr != nil {
|
||||
s.logger.Error("Compensation failed",
|
||||
zap.String("saga_id", s.ID),
|
||||
zap.Error(compensateErr))
|
||||
|
||||
s.setStatus(StatusAborted)
|
||||
s.publishEvent(ctx, "saga.aborted")
|
||||
return fmt.Errorf("saga execution failed and compensation failed: %w", compensateErr)
|
||||
}
|
||||
|
||||
s.setStatus(StatusCompensated)
|
||||
s.publishEvent(ctx, "saga.compensated")
|
||||
return fmt.Errorf("saga execution failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 所有步骤成功完成
|
||||
s.setStatus(StatusCompleted)
|
||||
s.EndTime = time.Now()
|
||||
|
||||
s.logger.Info("Saga completed successfully",
|
||||
zap.String("saga_id", s.ID),
|
||||
zap.Duration("duration", s.EndTime.Sub(s.StartTime)))
|
||||
|
||||
s.publishEvent(ctx, "saga.completed")
|
||||
return nil
|
||||
}
|
||||
|
||||
// executeStep 执行单个步骤
|
||||
func (s *Saga) executeStep(ctx context.Context, step *SagaStep, data interface{}) error {
|
||||
step.Status = StepRunning
|
||||
step.StartTime = time.Now()
|
||||
|
||||
s.logger.Debug("Executing step",
|
||||
zap.String("saga_id", s.ID),
|
||||
zap.String("step_name", step.Name))
|
||||
|
||||
// 设置超时上下文
|
||||
stepCtx, cancel := context.WithTimeout(ctx, step.Timeout)
|
||||
defer cancel()
|
||||
|
||||
// 重试逻辑
|
||||
var lastErr error
|
||||
for attempt := 0; attempt <= step.MaxRetries; attempt++ {
|
||||
if attempt > 0 {
|
||||
s.logger.Debug("Retrying step",
|
||||
zap.String("saga_id", s.ID),
|
||||
zap.String("step_name", step.Name),
|
||||
zap.Int("attempt", attempt))
|
||||
}
|
||||
|
||||
err := step.Action(stepCtx, data)
|
||||
if err == nil {
|
||||
step.Status = StepCompleted
|
||||
step.EndTime = time.Now()
|
||||
|
||||
s.logger.Debug("Step completed successfully",
|
||||
zap.String("saga_id", s.ID),
|
||||
zap.String("step_name", step.Name),
|
||||
zap.Duration("duration", step.EndTime.Sub(step.StartTime)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
step.RetryCount = attempt
|
||||
|
||||
// 检查是否应该重试
|
||||
if attempt < step.MaxRetries {
|
||||
select {
|
||||
case <-stepCtx.Done():
|
||||
// 上下文被取消,停止重试
|
||||
break
|
||||
case <-time.After(time.Duration(attempt+1) * 100 * time.Millisecond):
|
||||
// 等待一段时间后重试
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 所有重试都失败了
|
||||
step.Status = StepFailed
|
||||
step.Error = lastErr
|
||||
step.EndTime = time.Now()
|
||||
|
||||
return lastErr
|
||||
}
|
||||
|
||||
// compensate 执行补偿
|
||||
func (s *Saga) compensate(ctx context.Context, fromStep int) error {
|
||||
s.setStatus(StatusCompensating)
|
||||
|
||||
s.logger.Info("Starting compensation",
|
||||
zap.String("saga_id", s.ID),
|
||||
zap.Int("from_step", fromStep))
|
||||
|
||||
// 逆序执行补偿
|
||||
for i := fromStep; i >= 0; i-- {
|
||||
step := s.Steps[i]
|
||||
|
||||
// 只补偿已完成的步骤
|
||||
if step.Status != StepCompleted {
|
||||
step.Status = StepSkipped
|
||||
continue
|
||||
}
|
||||
|
||||
if step.Compensate == nil {
|
||||
s.logger.Warn("No compensation function for step",
|
||||
zap.String("saga_id", s.ID),
|
||||
zap.String("step_name", step.Name))
|
||||
continue
|
||||
}
|
||||
|
||||
s.logger.Debug("Compensating step",
|
||||
zap.String("saga_id", s.ID),
|
||||
zap.String("step_name", step.Name))
|
||||
|
||||
// 设置超时上下文
|
||||
compensateCtx, cancel := context.WithTimeout(ctx, step.Timeout)
|
||||
|
||||
err := step.Compensate(compensateCtx, s.Data)
|
||||
cancel()
|
||||
|
||||
if err != nil {
|
||||
s.logger.Error("Compensation failed for step",
|
||||
zap.String("saga_id", s.ID),
|
||||
zap.String("step_name", step.Name),
|
||||
zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
step.Status = StepCompensated
|
||||
|
||||
s.logger.Debug("Step compensated successfully",
|
||||
zap.String("saga_id", s.ID),
|
||||
zap.String("step_name", step.Name))
|
||||
}
|
||||
|
||||
s.logger.Info("Compensation completed",
|
||||
zap.String("saga_id", s.ID))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// setStatus 设置状态
|
||||
func (s *Saga) setStatus(status SagaStatus) {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
s.Status = status
|
||||
}
|
||||
|
||||
// GetStatus 获取状态
|
||||
func (s *Saga) GetStatus() SagaStatus {
|
||||
s.mutex.RLock()
|
||||
defer s.mutex.RUnlock()
|
||||
return s.Status
|
||||
}
|
||||
|
||||
// GetProgress 获取进度
|
||||
func (s *Saga) GetProgress() SagaProgress {
|
||||
s.mutex.RLock()
|
||||
defer s.mutex.RUnlock()
|
||||
|
||||
completed := 0
|
||||
for _, step := range s.Steps {
|
||||
if step.Status == StepCompleted {
|
||||
completed++
|
||||
}
|
||||
}
|
||||
|
||||
var percentage float64
|
||||
if len(s.Steps) > 0 {
|
||||
percentage = float64(completed) / float64(len(s.Steps)) * 100
|
||||
}
|
||||
|
||||
return SagaProgress{
|
||||
SagaID: s.ID,
|
||||
Status: s.Status.String(),
|
||||
TotalSteps: len(s.Steps),
|
||||
CompletedSteps: completed,
|
||||
CurrentStep: s.currentStep + 1,
|
||||
PercentComplete: percentage,
|
||||
StartTime: s.StartTime,
|
||||
Duration: time.Since(s.StartTime),
|
||||
}
|
||||
}
|
||||
|
||||
// GetStepStatus 获取所有步骤状态
|
||||
func (s *Saga) GetStepStatus() []StepProgress {
|
||||
s.mutex.RLock()
|
||||
defer s.mutex.RUnlock()
|
||||
|
||||
progress := make([]StepProgress, len(s.Steps))
|
||||
for i, step := range s.Steps {
|
||||
progress[i] = StepProgress{
|
||||
Name: step.Name,
|
||||
Status: step.Status.String(),
|
||||
RetryCount: step.RetryCount,
|
||||
StartTime: step.StartTime,
|
||||
EndTime: step.EndTime,
|
||||
Duration: step.EndTime.Sub(step.StartTime),
|
||||
Error: "",
|
||||
}
|
||||
|
||||
if step.Error != nil {
|
||||
progress[i].Error = step.Error.Error()
|
||||
}
|
||||
}
|
||||
|
||||
return progress
|
||||
}
|
||||
|
||||
// publishEvent 发布事件
|
||||
func (s *Saga) publishEvent(ctx context.Context, eventType string) {
|
||||
if s.Config.EventBus == nil {
|
||||
return
|
||||
}
|
||||
|
||||
event := &SagaEvent{
|
||||
SagaID: s.ID,
|
||||
SagaName: s.Name,
|
||||
EventType: eventType,
|
||||
Status: s.Status.String(),
|
||||
Timestamp: time.Now(),
|
||||
Data: s.Data,
|
||||
}
|
||||
|
||||
// 这里应该实现Event接口,简化处理
|
||||
_ = event
|
||||
}
|
||||
|
||||
// SagaProgress Saga进度
|
||||
type SagaProgress struct {
|
||||
SagaID string `json:"saga_id"`
|
||||
Status string `json:"status"`
|
||||
TotalSteps int `json:"total_steps"`
|
||||
CompletedSteps int `json:"completed_steps"`
|
||||
CurrentStep int `json:"current_step"`
|
||||
PercentComplete float64 `json:"percent_complete"`
|
||||
StartTime time.Time `json:"start_time"`
|
||||
Duration time.Duration `json:"duration"`
|
||||
}
|
||||
|
||||
// StepProgress 步骤进度
|
||||
type StepProgress struct {
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
RetryCount int `json:"retry_count"`
|
||||
StartTime time.Time `json:"start_time"`
|
||||
EndTime time.Time `json:"end_time"`
|
||||
Duration time.Duration `json:"duration"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// SagaEvent Saga事件
|
||||
type SagaEvent struct {
|
||||
SagaID string `json:"saga_id"`
|
||||
SagaName string `json:"saga_name"`
|
||||
EventType string `json:"event_type"`
|
||||
Status string `json:"status"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// SagaManager Saga管理器
|
||||
type SagaManager struct {
|
||||
sagas map[string]*Saga
|
||||
logger *zap.Logger
|
||||
mutex sync.RWMutex
|
||||
config SagaConfig
|
||||
}
|
||||
|
||||
// NewSagaManager 创建Saga管理器
|
||||
func NewSagaManager(config SagaConfig, logger *zap.Logger) *SagaManager {
|
||||
return &SagaManager{
|
||||
sagas: make(map[string]*Saga),
|
||||
logger: logger,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateSaga 创建Saga
|
||||
func (sm *SagaManager) CreateSaga(id, name string) *Saga {
|
||||
saga := NewSaga(id, name, sm.config, sm.logger.Named("saga"))
|
||||
|
||||
sm.mutex.Lock()
|
||||
sm.sagas[id] = saga
|
||||
sm.mutex.Unlock()
|
||||
|
||||
sm.logger.Info("Created saga",
|
||||
zap.String("saga_id", id),
|
||||
zap.String("saga_name", name))
|
||||
|
||||
return saga
|
||||
}
|
||||
|
||||
// GetSaga 获取Saga
|
||||
func (sm *SagaManager) GetSaga(id string) (*Saga, bool) {
|
||||
sm.mutex.RLock()
|
||||
defer sm.mutex.RUnlock()
|
||||
|
||||
saga, exists := sm.sagas[id]
|
||||
return saga, exists
|
||||
}
|
||||
|
||||
// ListSagas 列出所有Saga
|
||||
func (sm *SagaManager) ListSagas() []*Saga {
|
||||
sm.mutex.RLock()
|
||||
defer sm.mutex.RUnlock()
|
||||
|
||||
sagas := make([]*Saga, 0, len(sm.sagas))
|
||||
for _, saga := range sm.sagas {
|
||||
sagas = append(sagas, saga)
|
||||
}
|
||||
|
||||
return sagas
|
||||
}
|
||||
|
||||
// GetSagaProgress 获取Saga进度
|
||||
func (sm *SagaManager) GetSagaProgress(id string) (SagaProgress, bool) {
|
||||
saga, exists := sm.GetSaga(id)
|
||||
if !exists {
|
||||
return SagaProgress{}, false
|
||||
}
|
||||
|
||||
return saga.GetProgress(), true
|
||||
}
|
||||
|
||||
// RemoveSaga 移除Saga
|
||||
func (sm *SagaManager) RemoveSaga(id string) {
|
||||
sm.mutex.Lock()
|
||||
defer sm.mutex.Unlock()
|
||||
|
||||
delete(sm.sagas, id)
|
||||
sm.logger.Debug("Removed saga", zap.String("saga_id", id))
|
||||
}
|
||||
|
||||
// GetStats 获取统计信息
|
||||
func (sm *SagaManager) GetStats() map[string]interface{} {
|
||||
sm.mutex.RLock()
|
||||
defer sm.mutex.RUnlock()
|
||||
|
||||
statusCount := make(map[string]int)
|
||||
for _, saga := range sm.sagas {
|
||||
status := saga.GetStatus().String()
|
||||
statusCount[status]++
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"total_sagas": len(sm.sagas),
|
||||
"status_count": statusCount,
|
||||
}
|
||||
}
|
||||
|
||||
// 实现Service接口
|
||||
|
||||
// Name 返回服务名称
|
||||
func (sm *SagaManager) Name() string {
|
||||
return "saga-manager"
|
||||
}
|
||||
|
||||
// Initialize 初始化服务
|
||||
func (sm *SagaManager) Initialize(ctx context.Context) error {
|
||||
sm.logger.Info("Saga manager service initialized")
|
||||
return nil
|
||||
}
|
||||
|
||||
// HealthCheck 健康检查
|
||||
func (sm *SagaManager) HealthCheck(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Shutdown 关闭服务
|
||||
func (sm *SagaManager) Shutdown(ctx context.Context) error {
|
||||
sm.logger.Info("Saga manager service shutdown")
|
||||
return nil
|
||||
}
|
||||
130
internal/shared/sms/sms_service.go
Normal file
130
internal/shared/sms/sms_service.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package sms
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"github.com/aliyun/alibaba-cloud-sdk-go/services/dysmsapi"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/config"
|
||||
)
|
||||
|
||||
// Service 短信服务接口
|
||||
type Service interface {
|
||||
SendVerificationCode(ctx context.Context, phone string, code string) error
|
||||
GenerateCode(length int) string
|
||||
}
|
||||
|
||||
// AliSMSService 阿里云短信服务实现
|
||||
type AliSMSService struct {
|
||||
client *dysmsapi.Client
|
||||
config config.SMSConfig
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewAliSMSService 创建阿里云短信服务
|
||||
func NewAliSMSService(cfg config.SMSConfig, logger *zap.Logger) (*AliSMSService, error) {
|
||||
client, err := dysmsapi.NewClientWithAccessKey("cn-hangzhou", cfg.AccessKeyID, cfg.AccessKeySecret)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建短信客户端失败: %w", err)
|
||||
}
|
||||
|
||||
return &AliSMSService{
|
||||
client: client,
|
||||
config: cfg,
|
||||
logger: logger,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SendVerificationCode 发送验证码
|
||||
func (s *AliSMSService) SendVerificationCode(ctx context.Context, phone string, code string) error {
|
||||
request := dysmsapi.CreateSendSmsRequest()
|
||||
request.Scheme = "https"
|
||||
request.PhoneNumbers = phone
|
||||
request.SignName = s.config.SignName
|
||||
request.TemplateCode = s.config.TemplateCode
|
||||
request.TemplateParam = fmt.Sprintf(`{"code":"%s"}`, code)
|
||||
|
||||
response, err := s.client.SendSms(request)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to send SMS",
|
||||
zap.String("phone", phone),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("短信发送失败: %w", err)
|
||||
}
|
||||
|
||||
if response.Code != "OK" {
|
||||
s.logger.Error("SMS send failed",
|
||||
zap.String("phone", phone),
|
||||
zap.String("code", response.Code),
|
||||
zap.String("message", response.Message))
|
||||
return fmt.Errorf("短信发送失败: %s - %s", response.Code, response.Message)
|
||||
}
|
||||
|
||||
s.logger.Info("SMS sent successfully",
|
||||
zap.String("phone", phone),
|
||||
zap.String("bizId", response.BizId))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenerateCode 生成验证码
|
||||
func (s *AliSMSService) GenerateCode(length int) string {
|
||||
if length <= 0 {
|
||||
length = 6
|
||||
}
|
||||
|
||||
// 生成指定长度的数字验证码
|
||||
max := big.NewInt(int64(pow10(length)))
|
||||
n, _ := rand.Int(rand.Reader, max)
|
||||
|
||||
// 格式化为指定长度,不足时前面补0
|
||||
format := fmt.Sprintf("%%0%dd", length)
|
||||
return fmt.Sprintf(format, n.Int64())
|
||||
}
|
||||
|
||||
// pow10 计算10的n次方
|
||||
func pow10(n int) int {
|
||||
result := 1
|
||||
for i := 0; i < n; i++ {
|
||||
result *= 10
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// MockSMSService 模拟短信服务(用于开发和测试)
|
||||
type MockSMSService struct {
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewMockSMSService 创建模拟短信服务
|
||||
func NewMockSMSService(logger *zap.Logger) *MockSMSService {
|
||||
return &MockSMSService{
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// SendVerificationCode 模拟发送验证码
|
||||
func (s *MockSMSService) SendVerificationCode(ctx context.Context, phone string, code string) error {
|
||||
s.logger.Info("Mock SMS sent",
|
||||
zap.String("phone", phone),
|
||||
zap.String("code", code))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenerateCode 生成验证码
|
||||
func (s *MockSMSService) GenerateCode(length int) string {
|
||||
if length <= 0 {
|
||||
length = 6
|
||||
}
|
||||
|
||||
// 开发环境使用固定验证码便于测试
|
||||
result := ""
|
||||
for i := 0; i < length; i++ {
|
||||
result += "1"
|
||||
}
|
||||
return result
|
||||
}
|
||||
292
internal/shared/tracing/decorators.go
Normal file
292
internal/shared/tracing/decorators.go
Normal file
@@ -0,0 +1,292 @@
|
||||
package tracing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// TracableService 可追踪的服务接口
|
||||
type TracableService interface {
|
||||
Name() string
|
||||
}
|
||||
|
||||
// ServiceDecorator 服务装饰器
|
||||
type ServiceDecorator struct {
|
||||
tracer *Tracer
|
||||
logger *zap.Logger
|
||||
config DecoratorConfig
|
||||
}
|
||||
|
||||
// DecoratorConfig 装饰器配置
|
||||
type DecoratorConfig struct {
|
||||
EnableMethodTracing bool
|
||||
ExcludePatterns []string
|
||||
IncludeArguments bool
|
||||
IncludeResults bool
|
||||
SlowMethodThreshold time.Duration
|
||||
}
|
||||
|
||||
// DefaultDecoratorConfig 默认装饰器配置
|
||||
func DefaultDecoratorConfig() DecoratorConfig {
|
||||
return DecoratorConfig{
|
||||
EnableMethodTracing: true,
|
||||
ExcludePatterns: []string{"Health", "Ping", "Name"},
|
||||
IncludeArguments: true,
|
||||
IncludeResults: false,
|
||||
SlowMethodThreshold: 100 * time.Millisecond,
|
||||
}
|
||||
}
|
||||
|
||||
// NewServiceDecorator 创建服务装饰器
|
||||
func NewServiceDecorator(tracer *Tracer, logger *zap.Logger) *ServiceDecorator {
|
||||
return &ServiceDecorator{
|
||||
tracer: tracer,
|
||||
logger: logger,
|
||||
config: DefaultDecoratorConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
// WrapService 自动包装服务,为所有方法添加链路追踪
|
||||
func (d *ServiceDecorator) WrapService(service interface{}) interface{} {
|
||||
serviceValue := reflect.ValueOf(service)
|
||||
serviceType := reflect.TypeOf(service)
|
||||
|
||||
if serviceType.Kind() == reflect.Ptr {
|
||||
serviceType = serviceType.Elem()
|
||||
serviceValue = serviceValue.Elem()
|
||||
}
|
||||
|
||||
// 创建代理结构
|
||||
proxyType := d.createProxyType(serviceType)
|
||||
proxyValue := reflect.New(proxyType).Elem()
|
||||
|
||||
// 设置原始服务字段
|
||||
proxyValue.FieldByName("target").Set(reflect.ValueOf(service))
|
||||
proxyValue.FieldByName("decorator").Set(reflect.ValueOf(d))
|
||||
|
||||
return proxyValue.Addr().Interface()
|
||||
}
|
||||
|
||||
// createProxyType 创建代理类型
|
||||
func (d *ServiceDecorator) createProxyType(serviceType reflect.Type) reflect.Type {
|
||||
// 获取服务名称
|
||||
serviceName := d.getServiceName(serviceType)
|
||||
|
||||
// 创建代理结构字段
|
||||
fields := []reflect.StructField{
|
||||
{
|
||||
Name: "target",
|
||||
Type: reflect.PtrTo(serviceType),
|
||||
},
|
||||
{
|
||||
Name: "decorator",
|
||||
Type: reflect.TypeOf(d),
|
||||
},
|
||||
}
|
||||
|
||||
// 为每个方法创建包装器方法
|
||||
for i := 0; i < serviceType.NumMethod(); i++ {
|
||||
method := serviceType.Method(i)
|
||||
if d.shouldTraceMethod(method.Name) {
|
||||
// 创建方法字段(用于存储方法实现)
|
||||
fields = append(fields, reflect.StructField{
|
||||
Name: method.Name,
|
||||
Type: method.Type,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 创建新的结构类型
|
||||
proxyType := reflect.StructOf(fields)
|
||||
|
||||
// 实现接口方法
|
||||
d.implementMethods(proxyType, serviceType, serviceName)
|
||||
|
||||
return proxyType
|
||||
}
|
||||
|
||||
// shouldTraceMethod 判断是否应该追踪方法
|
||||
func (d *ServiceDecorator) shouldTraceMethod(methodName string) bool {
|
||||
if !d.config.EnableMethodTracing {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, pattern := range d.config.ExcludePatterns {
|
||||
if strings.Contains(methodName, pattern) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// getServiceName 获取服务名称
|
||||
func (d *ServiceDecorator) getServiceName(serviceType reflect.Type) string {
|
||||
serviceName := serviceType.Name()
|
||||
// 移除Service后缀
|
||||
if strings.HasSuffix(serviceName, "Service") {
|
||||
serviceName = strings.TrimSuffix(serviceName, "Service")
|
||||
}
|
||||
return strings.ToLower(serviceName)
|
||||
}
|
||||
|
||||
// TraceMethodCall 追踪方法调用
|
||||
func (d *ServiceDecorator) TraceMethodCall(
|
||||
ctx context.Context,
|
||||
serviceName, methodName string,
|
||||
fn func(context.Context) ([]reflect.Value, error),
|
||||
args []reflect.Value,
|
||||
) ([]reflect.Value, error) {
|
||||
// 创建span名称
|
||||
spanName := fmt.Sprintf("%s.%s", serviceName, methodName)
|
||||
|
||||
// 开始追踪
|
||||
ctx, span := d.tracer.StartSpan(ctx, spanName)
|
||||
defer span.End()
|
||||
|
||||
// 添加基础属性
|
||||
d.tracer.AddSpanAttributes(span,
|
||||
attribute.String("service.name", serviceName),
|
||||
attribute.String("service.method", methodName),
|
||||
attribute.String("service.type", "business"),
|
||||
)
|
||||
|
||||
// 添加参数信息(如果启用)
|
||||
if d.config.IncludeArguments {
|
||||
d.addArgumentAttributes(span, args)
|
||||
}
|
||||
|
||||
// 记录开始时间
|
||||
startTime := time.Now()
|
||||
|
||||
// 执行原始方法
|
||||
results, err := fn(ctx)
|
||||
|
||||
// 计算执行时间
|
||||
duration := time.Since(startTime)
|
||||
d.tracer.AddSpanAttributes(span,
|
||||
attribute.Int64("service.duration_ms", duration.Milliseconds()),
|
||||
)
|
||||
|
||||
// 标记慢方法
|
||||
if duration > d.config.SlowMethodThreshold {
|
||||
d.tracer.AddSpanAttributes(span,
|
||||
attribute.Bool("service.slow_method", true),
|
||||
)
|
||||
d.logger.Warn("慢方法检测",
|
||||
zap.String("service", serviceName),
|
||||
zap.String("method", methodName),
|
||||
zap.Duration("duration", duration),
|
||||
zap.String("trace_id", d.tracer.GetTraceID(ctx)),
|
||||
)
|
||||
}
|
||||
|
||||
// 处理错误
|
||||
if err != nil {
|
||||
d.tracer.SetSpanError(span, err)
|
||||
d.logger.Error("服务方法执行失败",
|
||||
zap.String("service", serviceName),
|
||||
zap.String("method", methodName),
|
||||
zap.Error(err),
|
||||
zap.String("trace_id", d.tracer.GetTraceID(ctx)),
|
||||
)
|
||||
} else {
|
||||
d.tracer.SetSpanSuccess(span)
|
||||
|
||||
// 添加结果信息(如果启用)
|
||||
if d.config.IncludeResults {
|
||||
d.addResultAttributes(span, results)
|
||||
}
|
||||
}
|
||||
|
||||
return results, err
|
||||
}
|
||||
|
||||
// addArgumentAttributes 添加参数属性
|
||||
func (d *ServiceDecorator) addArgumentAttributes(span trace.Span, args []reflect.Value) {
|
||||
for i, arg := range args {
|
||||
if i == 0 && arg.Type().String() == "context.Context" {
|
||||
continue // 跳过context参数
|
||||
}
|
||||
|
||||
argName := fmt.Sprintf("service.arg_%d", i)
|
||||
argValue := d.extractValue(arg)
|
||||
|
||||
if argValue != "" && len(argValue) < 1000 { // 限制长度避免性能问题
|
||||
d.tracer.AddSpanAttributes(span,
|
||||
attribute.String(argName, argValue),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// addResultAttributes 添加结果属性
|
||||
func (d *ServiceDecorator) addResultAttributes(span trace.Span, results []reflect.Value) {
|
||||
for i, result := range results {
|
||||
if result.Type().String() == "error" {
|
||||
continue // 错误在其他地方处理
|
||||
}
|
||||
|
||||
resultName := fmt.Sprintf("service.result_%d", i)
|
||||
resultValue := d.extractValue(result)
|
||||
|
||||
if resultValue != "" && len(resultValue) < 1000 {
|
||||
d.tracer.AddSpanAttributes(span,
|
||||
attribute.String(resultName, resultValue),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// extractValue 提取值的字符串表示
|
||||
func (d *ServiceDecorator) extractValue(value reflect.Value) string {
|
||||
if !value.IsValid() {
|
||||
return ""
|
||||
}
|
||||
|
||||
switch value.Kind() {
|
||||
case reflect.String:
|
||||
return value.String()
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return fmt.Sprintf("%d", value.Int())
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return fmt.Sprintf("%d", value.Uint())
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return fmt.Sprintf("%.2f", value.Float())
|
||||
case reflect.Bool:
|
||||
return fmt.Sprintf("%t", value.Bool())
|
||||
case reflect.Ptr:
|
||||
if value.IsNil() {
|
||||
return "nil"
|
||||
}
|
||||
return d.extractValue(value.Elem())
|
||||
case reflect.Struct:
|
||||
// 对于结构体,只返回类型名
|
||||
return value.Type().Name()
|
||||
case reflect.Slice, reflect.Array:
|
||||
return fmt.Sprintf("[%d items]", value.Len())
|
||||
default:
|
||||
return value.Type().Name()
|
||||
}
|
||||
}
|
||||
|
||||
// implementMethods 实现接口方法(占位符,实际需要运行时代理)
|
||||
func (d *ServiceDecorator) implementMethods(proxyType, serviceType reflect.Type, serviceName string) {
|
||||
// 这里是运行时方法实现的占位符
|
||||
// 实际实现需要使用reflect.MakeFunc或其他运行时代理技术
|
||||
}
|
||||
|
||||
// GetFunctionName 获取函数名称
|
||||
func GetFunctionName(fn interface{}) string {
|
||||
name := runtime.FuncForPC(reflect.ValueOf(fn).Pointer()).Name()
|
||||
parts := strings.Split(name, ".")
|
||||
return parts[len(parts)-1]
|
||||
}
|
||||
320
internal/shared/tracing/gorm_plugin.go
Normal file
320
internal/shared/tracing/gorm_plugin.go
Normal file
@@ -0,0 +1,320 @@
|
||||
package tracing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
gormSpanKey = "otel:span"
|
||||
gormOperationKey = "otel:operation"
|
||||
gormTableNameKey = "otel:table_name"
|
||||
gormStartTimeKey = "otel:start_time"
|
||||
)
|
||||
|
||||
// GormTracingPlugin GORM链路追踪插件
|
||||
type GormTracingPlugin struct {
|
||||
tracer *Tracer
|
||||
logger *zap.Logger
|
||||
config GormPluginConfig
|
||||
}
|
||||
|
||||
// GormPluginConfig GORM插件配置
|
||||
type GormPluginConfig struct {
|
||||
IncludeSQL bool
|
||||
IncludeValues bool
|
||||
SlowThreshold time.Duration
|
||||
ExcludeTables []string
|
||||
SanitizeSQL bool
|
||||
}
|
||||
|
||||
// DefaultGormPluginConfig 默认GORM插件配置
|
||||
func DefaultGormPluginConfig() GormPluginConfig {
|
||||
return GormPluginConfig{
|
||||
IncludeSQL: true,
|
||||
IncludeValues: false, // 生产环境建议设为false避免记录敏感数据
|
||||
SlowThreshold: 200 * time.Millisecond,
|
||||
ExcludeTables: []string{"migrations", "schema_migrations"},
|
||||
SanitizeSQL: true,
|
||||
}
|
||||
}
|
||||
|
||||
// NewGormTracingPlugin 创建GORM追踪插件
|
||||
func NewGormTracingPlugin(tracer *Tracer, logger *zap.Logger) *GormTracingPlugin {
|
||||
return &GormTracingPlugin{
|
||||
tracer: tracer,
|
||||
logger: logger,
|
||||
config: DefaultGormPluginConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
// Name 返回插件名称
|
||||
func (p *GormTracingPlugin) Name() string {
|
||||
return "gorm-otel-tracing"
|
||||
}
|
||||
|
||||
// Initialize 初始化插件
|
||||
func (p *GormTracingPlugin) Initialize(db *gorm.DB) error {
|
||||
// 注册各种操作的回调
|
||||
callbacks := []string{"create", "query", "update", "delete", "raw"}
|
||||
|
||||
for _, operation := range callbacks {
|
||||
switch operation {
|
||||
case "create":
|
||||
err := db.Callback().Create().Before("gorm:create").
|
||||
Register(p.Name()+":before_create", p.beforeOperation)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to register before create callback: %w", err)
|
||||
}
|
||||
err = db.Callback().Create().After("gorm:create").
|
||||
Register(p.Name()+":after_create", p.afterOperation)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to register after create callback: %w", err)
|
||||
}
|
||||
case "query":
|
||||
err := db.Callback().Query().Before("gorm:query").
|
||||
Register(p.Name()+":before_query", p.beforeOperation)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to register before query callback: %w", err)
|
||||
}
|
||||
err = db.Callback().Query().After("gorm:query").
|
||||
Register(p.Name()+":after_query", p.afterOperation)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to register after query callback: %w", err)
|
||||
}
|
||||
case "update":
|
||||
err := db.Callback().Update().Before("gorm:update").
|
||||
Register(p.Name()+":before_update", p.beforeOperation)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to register before update callback: %w", err)
|
||||
}
|
||||
err = db.Callback().Update().After("gorm:update").
|
||||
Register(p.Name()+":after_update", p.afterOperation)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to register after update callback: %w", err)
|
||||
}
|
||||
case "delete":
|
||||
err := db.Callback().Delete().Before("gorm:delete").
|
||||
Register(p.Name()+":before_delete", p.beforeOperation)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to register before delete callback: %w", err)
|
||||
}
|
||||
err = db.Callback().Delete().After("gorm:delete").
|
||||
Register(p.Name()+":after_delete", p.afterOperation)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to register after delete callback: %w", err)
|
||||
}
|
||||
case "raw":
|
||||
err := db.Callback().Raw().Before("gorm:raw").
|
||||
Register(p.Name()+":before_raw", p.beforeOperation)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to register before raw callback: %w", err)
|
||||
}
|
||||
err = db.Callback().Raw().After("gorm:raw").
|
||||
Register(p.Name()+":after_raw", p.afterOperation)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to register after raw callback: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p.logger.Info("GORM追踪插件已初始化")
|
||||
return nil
|
||||
}
|
||||
|
||||
// beforeOperation 操作前回调
|
||||
func (p *GormTracingPlugin) beforeOperation(db *gorm.DB) {
|
||||
// 检查是否应该跳过追踪
|
||||
if p.shouldSkipTracing(db) {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := db.Statement.Context
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
// 获取操作信息
|
||||
operation := p.getOperationType(db)
|
||||
tableName := p.getTableName(db)
|
||||
|
||||
// 检查是否应该排除此表
|
||||
if p.isExcludedTable(tableName) {
|
||||
return
|
||||
}
|
||||
|
||||
// 开始追踪
|
||||
ctx, span := p.tracer.StartDBSpan(ctx, operation, tableName)
|
||||
|
||||
// 添加基础属性
|
||||
p.tracer.AddSpanAttributes(span,
|
||||
attribute.String("db.system", "postgresql"),
|
||||
attribute.String("db.operation", operation),
|
||||
)
|
||||
|
||||
if tableName != "" {
|
||||
p.tracer.AddSpanAttributes(span, attribute.String("db.table", tableName))
|
||||
}
|
||||
|
||||
// 保存追踪信息到GORM context
|
||||
db.Set(gormSpanKey, span)
|
||||
db.Set(gormOperationKey, operation)
|
||||
db.Set(gormTableNameKey, tableName)
|
||||
db.Set(gormStartTimeKey, time.Now())
|
||||
|
||||
// 更新statement context
|
||||
db.Statement.Context = ctx
|
||||
}
|
||||
|
||||
// afterOperation 操作后回调
|
||||
func (p *GormTracingPlugin) afterOperation(db *gorm.DB) {
|
||||
// 获取span
|
||||
spanValue, exists := db.Get(gormSpanKey)
|
||||
if !exists {
|
||||
return
|
||||
}
|
||||
|
||||
span, ok := spanValue.(trace.Span)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
defer span.End()
|
||||
|
||||
// 获取操作信息
|
||||
operation, _ := db.Get(gormOperationKey)
|
||||
tableName, _ := db.Get(gormTableNameKey)
|
||||
startTime, _ := db.Get(gormStartTimeKey)
|
||||
|
||||
// 计算执行时间
|
||||
var duration time.Duration
|
||||
if st, ok := startTime.(time.Time); ok {
|
||||
duration = time.Since(st)
|
||||
p.tracer.AddSpanAttributes(span,
|
||||
attribute.Int64("db.duration_ms", duration.Milliseconds()),
|
||||
)
|
||||
}
|
||||
|
||||
// 添加SQL信息
|
||||
if p.config.IncludeSQL && db.Statement.SQL.String() != "" {
|
||||
sql := db.Statement.SQL.String()
|
||||
if p.config.SanitizeSQL {
|
||||
sql = p.sanitizeSQL(sql)
|
||||
}
|
||||
p.tracer.AddSpanAttributes(span, attribute.String("db.statement", sql))
|
||||
}
|
||||
|
||||
// 添加影响行数
|
||||
if db.Statement.RowsAffected >= 0 {
|
||||
p.tracer.AddSpanAttributes(span,
|
||||
attribute.Int64("db.rows_affected", db.Statement.RowsAffected),
|
||||
)
|
||||
}
|
||||
|
||||
// 处理错误
|
||||
if db.Error != nil {
|
||||
p.tracer.SetSpanError(span, db.Error)
|
||||
span.SetStatus(codes.Error, db.Error.Error())
|
||||
|
||||
p.logger.Error("数据库操作失败",
|
||||
zap.String("operation", fmt.Sprintf("%v", operation)),
|
||||
zap.String("table", fmt.Sprintf("%v", tableName)),
|
||||
zap.Error(db.Error),
|
||||
zap.String("trace_id", p.tracer.GetTraceID(db.Statement.Context)),
|
||||
)
|
||||
} else {
|
||||
p.tracer.SetSpanSuccess(span)
|
||||
span.SetStatus(codes.Ok, "success")
|
||||
|
||||
// 检查慢查询
|
||||
if duration > p.config.SlowThreshold {
|
||||
p.tracer.AddSpanAttributes(span,
|
||||
attribute.Bool("db.slow_query", true),
|
||||
)
|
||||
|
||||
p.logger.Warn("慢SQL查询检测",
|
||||
zap.String("operation", fmt.Sprintf("%v", operation)),
|
||||
zap.String("table", fmt.Sprintf("%v", tableName)),
|
||||
zap.Duration("duration", duration),
|
||||
zap.String("sql", db.Statement.SQL.String()),
|
||||
zap.String("trace_id", p.tracer.GetTraceID(db.Statement.Context)),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// shouldSkipTracing 检查是否应该跳过追踪
|
||||
func (p *GormTracingPlugin) shouldSkipTracing(db *gorm.DB) bool {
|
||||
// 检查是否已有span(避免重复追踪)
|
||||
if _, exists := db.Get(gormSpanKey); exists {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// getOperationType 获取操作类型
|
||||
func (p *GormTracingPlugin) getOperationType(db *gorm.DB) string {
|
||||
switch db.Statement.ReflectValue.Kind() {
|
||||
default:
|
||||
sql := strings.ToUpper(strings.TrimSpace(db.Statement.SQL.String()))
|
||||
if sql == "" {
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
if strings.HasPrefix(sql, "SELECT") {
|
||||
return "select"
|
||||
} else if strings.HasPrefix(sql, "INSERT") {
|
||||
return "insert"
|
||||
} else if strings.HasPrefix(sql, "UPDATE") {
|
||||
return "update"
|
||||
} else if strings.HasPrefix(sql, "DELETE") {
|
||||
return "delete"
|
||||
} else if strings.HasPrefix(sql, "CREATE") {
|
||||
return "create"
|
||||
} else if strings.HasPrefix(sql, "DROP") {
|
||||
return "drop"
|
||||
} else if strings.HasPrefix(sql, "ALTER") {
|
||||
return "alter"
|
||||
}
|
||||
|
||||
return "query"
|
||||
}
|
||||
}
|
||||
|
||||
// getTableName 获取表名
|
||||
func (p *GormTracingPlugin) getTableName(db *gorm.DB) string {
|
||||
if db.Statement.Table != "" {
|
||||
return db.Statement.Table
|
||||
}
|
||||
|
||||
if db.Statement.Schema != nil && db.Statement.Schema.Table != "" {
|
||||
return db.Statement.Schema.Table
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// isExcludedTable 检查是否为排除的表
|
||||
func (p *GormTracingPlugin) isExcludedTable(tableName string) bool {
|
||||
for _, excluded := range p.config.ExcludeTables {
|
||||
if tableName == excluded {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// sanitizeSQL 清理SQL语句,移除敏感信息
|
||||
func (p *GormTracingPlugin) sanitizeSQL(sql string) string {
|
||||
// 简单的SQL清理,将参数替换为占位符
|
||||
// 在生产环境中,您可能需要更复杂的清理逻辑
|
||||
return strings.ReplaceAll(sql, "'", "?")
|
||||
}
|
||||
407
internal/shared/tracing/redis_wrapper.go
Normal file
407
internal/shared/tracing/redis_wrapper.go
Normal file
@@ -0,0 +1,407 @@
|
||||
package tracing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
)
|
||||
|
||||
// TracedRedisCache Redis缓存自动追踪包装器
|
||||
type TracedRedisCache struct {
|
||||
client redis.UniversalClient
|
||||
tracer *Tracer
|
||||
logger *zap.Logger
|
||||
prefix string
|
||||
config RedisTracingConfig
|
||||
}
|
||||
|
||||
// RedisTracingConfig Redis追踪配置
|
||||
type RedisTracingConfig struct {
|
||||
IncludeKeys bool
|
||||
IncludeValues bool
|
||||
MaxKeyLength int
|
||||
MaxValueLength int
|
||||
SlowThreshold time.Duration
|
||||
SanitizeValues bool
|
||||
}
|
||||
|
||||
// DefaultRedisTracingConfig 默认Redis追踪配置
|
||||
func DefaultRedisTracingConfig() RedisTracingConfig {
|
||||
return RedisTracingConfig{
|
||||
IncludeKeys: true,
|
||||
IncludeValues: false, // 生产环境建议设为false保护敏感数据
|
||||
MaxKeyLength: 100,
|
||||
MaxValueLength: 1000,
|
||||
SlowThreshold: 50 * time.Millisecond,
|
||||
SanitizeValues: true,
|
||||
}
|
||||
}
|
||||
|
||||
// NewTracedRedisCache 创建带追踪的Redis缓存
|
||||
func NewTracedRedisCache(client redis.UniversalClient, tracer *Tracer, logger *zap.Logger, prefix string) interfaces.CacheService {
|
||||
return &TracedRedisCache{
|
||||
client: client,
|
||||
tracer: tracer,
|
||||
logger: logger,
|
||||
prefix: prefix,
|
||||
config: DefaultRedisTracingConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
// Name 返回服务名称
|
||||
func (c *TracedRedisCache) Name() string {
|
||||
return "redis-cache"
|
||||
}
|
||||
|
||||
// Initialize 初始化服务
|
||||
func (c *TracedRedisCache) Initialize(ctx context.Context) error {
|
||||
c.logger.Info("Redis缓存服务已初始化")
|
||||
return nil
|
||||
}
|
||||
|
||||
// HealthCheck 健康检查
|
||||
func (c *TracedRedisCache) HealthCheck(ctx context.Context) error {
|
||||
_, err := c.client.Ping(ctx).Result()
|
||||
return err
|
||||
}
|
||||
|
||||
// Shutdown 关闭服务
|
||||
func (c *TracedRedisCache) Shutdown(ctx context.Context) error {
|
||||
c.logger.Info("Redis缓存服务已关闭")
|
||||
return c.client.Close()
|
||||
}
|
||||
|
||||
// Get 获取缓存值
|
||||
func (c *TracedRedisCache) Get(ctx context.Context, key string, dest interface{}) error {
|
||||
// 开始追踪
|
||||
ctx, span := c.tracer.StartCacheSpan(ctx, "get", key)
|
||||
defer span.End()
|
||||
|
||||
// 添加基础属性
|
||||
c.addBaseAttributes(span, "get", key)
|
||||
|
||||
// 记录开始时间
|
||||
startTime := time.Now()
|
||||
|
||||
// 构建完整键名
|
||||
fullKey := c.buildKey(key)
|
||||
|
||||
// 执行Redis操作
|
||||
result, err := c.client.Get(ctx, fullKey).Result()
|
||||
|
||||
// 计算执行时间
|
||||
duration := time.Since(startTime)
|
||||
c.tracer.AddSpanAttributes(span,
|
||||
attribute.Int64("redis.duration_ms", duration.Milliseconds()),
|
||||
)
|
||||
|
||||
// 检查慢操作
|
||||
if duration > c.config.SlowThreshold {
|
||||
c.tracer.AddSpanAttributes(span,
|
||||
attribute.Bool("redis.slow_operation", true),
|
||||
)
|
||||
c.logger.Warn("Redis慢操作检测",
|
||||
zap.String("operation", "get"),
|
||||
zap.String("key", c.sanitizeKey(key)),
|
||||
zap.Duration("duration", duration),
|
||||
zap.String("trace_id", c.tracer.GetTraceID(ctx)),
|
||||
)
|
||||
}
|
||||
|
||||
// 处理结果
|
||||
if err != nil {
|
||||
if err == redis.Nil {
|
||||
// 缓存未命中
|
||||
c.tracer.AddSpanAttributes(span,
|
||||
attribute.Bool("redis.hit", false),
|
||||
attribute.String("redis.result", "miss"),
|
||||
)
|
||||
c.tracer.SetSpanSuccess(span)
|
||||
return interfaces.ErrCacheMiss
|
||||
} else {
|
||||
// Redis错误
|
||||
c.tracer.SetSpanError(span, err)
|
||||
c.logger.Error("Redis GET操作失败",
|
||||
zap.String("key", c.sanitizeKey(key)),
|
||||
zap.Error(err),
|
||||
zap.String("trace_id", c.tracer.GetTraceID(ctx)),
|
||||
)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 缓存命中
|
||||
c.tracer.AddSpanAttributes(span,
|
||||
attribute.Bool("redis.hit", true),
|
||||
attribute.String("redis.result", "hit"),
|
||||
attribute.Int("redis.value_size", len(result)),
|
||||
)
|
||||
|
||||
// 反序列化
|
||||
if err := c.deserialize(result, dest); err != nil {
|
||||
c.tracer.SetSpanError(span, err)
|
||||
return err
|
||||
}
|
||||
|
||||
c.tracer.SetSpanSuccess(span)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set 设置缓存值
|
||||
func (c *TracedRedisCache) Set(ctx context.Context, key string, value interface{}, ttl ...interface{}) error {
|
||||
// 开始追踪
|
||||
ctx, span := c.tracer.StartCacheSpan(ctx, "set", key)
|
||||
defer span.End()
|
||||
|
||||
// 添加基础属性
|
||||
c.addBaseAttributes(span, "set", key)
|
||||
|
||||
// 处理TTL
|
||||
var expiration time.Duration
|
||||
if len(ttl) > 0 {
|
||||
if duration, ok := ttl[0].(time.Duration); ok {
|
||||
expiration = duration
|
||||
c.tracer.AddSpanAttributes(span,
|
||||
attribute.Int64("redis.ttl_seconds", int64(expiration.Seconds())),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 记录开始时间
|
||||
startTime := time.Now()
|
||||
|
||||
// 序列化值
|
||||
serialized, err := c.serialize(value)
|
||||
if err != nil {
|
||||
c.tracer.SetSpanError(span, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 构建完整键名
|
||||
fullKey := c.buildKey(key)
|
||||
|
||||
// 执行Redis操作
|
||||
err = c.client.Set(ctx, fullKey, serialized, expiration).Err()
|
||||
|
||||
// 计算执行时间
|
||||
duration := time.Since(startTime)
|
||||
c.tracer.AddSpanAttributes(span,
|
||||
attribute.Int64("redis.duration_ms", duration.Milliseconds()),
|
||||
attribute.Int("redis.value_size", len(serialized)),
|
||||
)
|
||||
|
||||
// 检查慢操作
|
||||
if duration > c.config.SlowThreshold {
|
||||
c.tracer.AddSpanAttributes(span,
|
||||
attribute.Bool("redis.slow_operation", true),
|
||||
)
|
||||
c.logger.Warn("Redis慢操作检测",
|
||||
zap.String("operation", "set"),
|
||||
zap.String("key", c.sanitizeKey(key)),
|
||||
zap.Duration("duration", duration),
|
||||
zap.String("trace_id", c.tracer.GetTraceID(ctx)),
|
||||
)
|
||||
}
|
||||
|
||||
// 处理错误
|
||||
if err != nil {
|
||||
c.tracer.SetSpanError(span, err)
|
||||
c.logger.Error("Redis SET操作失败",
|
||||
zap.String("key", c.sanitizeKey(key)),
|
||||
zap.Error(err),
|
||||
zap.String("trace_id", c.tracer.GetTraceID(ctx)),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
c.tracer.SetSpanSuccess(span)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete 删除缓存
|
||||
func (c *TracedRedisCache) Delete(ctx context.Context, keys ...string) error {
|
||||
// 开始追踪
|
||||
ctx, span := c.tracer.StartCacheSpan(ctx, "delete", strings.Join(keys, ","))
|
||||
defer span.End()
|
||||
|
||||
// 添加基础属性
|
||||
c.tracer.AddSpanAttributes(span,
|
||||
attribute.String("redis.operation", "delete"),
|
||||
attribute.Int("redis.key_count", len(keys)),
|
||||
)
|
||||
|
||||
// 记录开始时间
|
||||
startTime := time.Now()
|
||||
|
||||
// 构建完整键名
|
||||
fullKeys := make([]string, len(keys))
|
||||
for i, key := range keys {
|
||||
fullKeys[i] = c.buildKey(key)
|
||||
}
|
||||
|
||||
// 执行Redis操作
|
||||
deleted, err := c.client.Del(ctx, fullKeys...).Result()
|
||||
|
||||
// 计算执行时间
|
||||
duration := time.Since(startTime)
|
||||
c.tracer.AddSpanAttributes(span,
|
||||
attribute.Int64("redis.duration_ms", duration.Milliseconds()),
|
||||
attribute.Int64("redis.deleted_count", deleted),
|
||||
)
|
||||
|
||||
// 处理错误
|
||||
if err != nil {
|
||||
c.tracer.SetSpanError(span, err)
|
||||
c.logger.Error("Redis DELETE操作失败",
|
||||
zap.Strings("keys", c.sanitizeKeys(keys)),
|
||||
zap.Error(err),
|
||||
zap.String("trace_id", c.tracer.GetTraceID(ctx)),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
c.tracer.SetSpanSuccess(span)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Exists 检查键是否存在
|
||||
func (c *TracedRedisCache) Exists(ctx context.Context, key string) (bool, error) {
|
||||
// 开始追踪
|
||||
ctx, span := c.tracer.StartCacheSpan(ctx, "exists", key)
|
||||
defer span.End()
|
||||
|
||||
// 添加基础属性
|
||||
c.addBaseAttributes(span, "exists", key)
|
||||
|
||||
// 记录开始时间
|
||||
startTime := time.Now()
|
||||
|
||||
// 构建完整键名
|
||||
fullKey := c.buildKey(key)
|
||||
|
||||
// 执行Redis操作
|
||||
count, err := c.client.Exists(ctx, fullKey).Result()
|
||||
|
||||
// 计算执行时间
|
||||
duration := time.Since(startTime)
|
||||
c.tracer.AddSpanAttributes(span,
|
||||
attribute.Int64("redis.duration_ms", duration.Milliseconds()),
|
||||
attribute.Bool("redis.exists", count > 0),
|
||||
)
|
||||
|
||||
// 处理错误
|
||||
if err != nil {
|
||||
c.tracer.SetSpanError(span, err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
c.tracer.SetSpanSuccess(span)
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
// GetMultiple 批量获取(基础实现)
|
||||
func (c *TracedRedisCache) GetMultiple(ctx context.Context, keys []string) (map[string]interface{}, error) {
|
||||
result := make(map[string]interface{})
|
||||
|
||||
// 简单实现:逐个获取(实际应用中可以使用MGET优化)
|
||||
for _, key := range keys {
|
||||
var value interface{}
|
||||
if err := c.Get(ctx, key, &value); err == nil {
|
||||
result[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// SetMultiple 批量设置(基础实现)
|
||||
func (c *TracedRedisCache) SetMultiple(ctx context.Context, data map[string]interface{}, ttl ...interface{}) error {
|
||||
// 简单实现:逐个设置(实际应用中可以使用pipeline优化)
|
||||
for key, value := range data {
|
||||
if err := c.Set(ctx, key, value, ttl...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeletePattern 按模式删除(基础实现)
|
||||
func (c *TracedRedisCache) DeletePattern(ctx context.Context, pattern string) error {
|
||||
// 这里需要实现模式删除逻辑
|
||||
return fmt.Errorf("DeletePattern not implemented")
|
||||
}
|
||||
|
||||
// Keys 获取匹配的键(基础实现)
|
||||
func (c *TracedRedisCache) Keys(ctx context.Context, pattern string) ([]string, error) {
|
||||
// 这里需要实现键匹配逻辑
|
||||
return nil, fmt.Errorf("Keys not implemented")
|
||||
}
|
||||
|
||||
// Stats 获取缓存统计(基础实现)
|
||||
func (c *TracedRedisCache) Stats(ctx context.Context) (interfaces.CacheStats, error) {
|
||||
return interfaces.CacheStats{}, fmt.Errorf("Stats not implemented")
|
||||
}
|
||||
|
||||
// 辅助方法
|
||||
|
||||
// addBaseAttributes 添加基础属性
|
||||
func (c *TracedRedisCache) addBaseAttributes(span trace.Span, operation, key string) {
|
||||
c.tracer.AddSpanAttributes(span,
|
||||
attribute.String("redis.operation", operation),
|
||||
attribute.String("db.system", "redis"),
|
||||
)
|
||||
|
||||
if c.config.IncludeKeys {
|
||||
sanitizedKey := c.sanitizeKey(key)
|
||||
if len(sanitizedKey) <= c.config.MaxKeyLength {
|
||||
c.tracer.AddSpanAttributes(span,
|
||||
attribute.String("redis.key", sanitizedKey),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// buildKey 构建完整的Redis键名
|
||||
func (c *TracedRedisCache) buildKey(key string) string {
|
||||
if c.prefix == "" {
|
||||
return key
|
||||
}
|
||||
return fmt.Sprintf("%s:%s", c.prefix, key)
|
||||
}
|
||||
|
||||
// sanitizeKey 清理键名用于日志记录
|
||||
func (c *TracedRedisCache) sanitizeKey(key string) string {
|
||||
if len(key) <= c.config.MaxKeyLength {
|
||||
return key
|
||||
}
|
||||
return key[:c.config.MaxKeyLength] + "..."
|
||||
}
|
||||
|
||||
// sanitizeKeys 批量清理键名
|
||||
func (c *TracedRedisCache) sanitizeKeys(keys []string) []string {
|
||||
result := make([]string, len(keys))
|
||||
for i, key := range keys {
|
||||
result[i] = c.sanitizeKey(key)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// serialize 序列化值(简单实现)
|
||||
func (c *TracedRedisCache) serialize(value interface{}) (string, error) {
|
||||
// 这里应该使用JSON或其他序列化方法
|
||||
return fmt.Sprintf("%v", value), nil
|
||||
}
|
||||
|
||||
// deserialize 反序列化值(简单实现)
|
||||
func (c *TracedRedisCache) deserialize(data string, dest interface{}) error {
|
||||
// 这里应该实现真正的反序列化逻辑
|
||||
return fmt.Errorf("deserialize not fully implemented")
|
||||
}
|
||||
189
internal/shared/tracing/service_wrapper.go
Normal file
189
internal/shared/tracing/service_wrapper.go
Normal file
@@ -0,0 +1,189 @@
|
||||
package tracing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/domains/user/dto"
|
||||
"tyapi-server/internal/domains/user/entities"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
)
|
||||
|
||||
// ServiceWrapper 服务包装器,提供自动追踪能力
|
||||
type ServiceWrapper struct {
|
||||
tracer *Tracer
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewServiceWrapper 创建服务包装器
|
||||
func NewServiceWrapper(tracer *Tracer, logger *zap.Logger) *ServiceWrapper {
|
||||
return &ServiceWrapper{
|
||||
tracer: tracer,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// TraceServiceCall 追踪服务调用的通用方法
|
||||
func (w *ServiceWrapper) TraceServiceCall(
|
||||
ctx context.Context,
|
||||
serviceName, methodName string,
|
||||
fn func(context.Context) error,
|
||||
) error {
|
||||
// 创建span名称
|
||||
spanName := fmt.Sprintf("%s.%s", serviceName, methodName)
|
||||
|
||||
// 开始追踪
|
||||
ctx, span := w.tracer.StartSpan(ctx, spanName)
|
||||
defer span.End()
|
||||
|
||||
// 添加基础属性
|
||||
w.tracer.AddSpanAttributes(span,
|
||||
attribute.String("service.name", serviceName),
|
||||
attribute.String("service.method", methodName),
|
||||
attribute.String("service.type", "business"),
|
||||
)
|
||||
|
||||
// 记录开始时间
|
||||
startTime := time.Now()
|
||||
|
||||
// 执行原始方法
|
||||
err := fn(ctx)
|
||||
|
||||
// 计算执行时间
|
||||
duration := time.Since(startTime)
|
||||
w.tracer.AddSpanAttributes(span,
|
||||
attribute.Int64("service.duration_ms", duration.Milliseconds()),
|
||||
)
|
||||
|
||||
// 标记慢方法
|
||||
if duration > 100*time.Millisecond {
|
||||
w.tracer.AddSpanAttributes(span,
|
||||
attribute.Bool("service.slow_method", true),
|
||||
)
|
||||
w.logger.Warn("慢方法检测",
|
||||
zap.String("service", serviceName),
|
||||
zap.String("method", methodName),
|
||||
zap.Duration("duration", duration),
|
||||
zap.String("trace_id", w.tracer.GetTraceID(ctx)),
|
||||
)
|
||||
}
|
||||
|
||||
// 处理错误
|
||||
if err != nil {
|
||||
w.tracer.SetSpanError(span, err)
|
||||
w.logger.Error("服务方法执行失败",
|
||||
zap.String("service", serviceName),
|
||||
zap.String("method", methodName),
|
||||
zap.Error(err),
|
||||
zap.String("trace_id", w.tracer.GetTraceID(ctx)),
|
||||
)
|
||||
} else {
|
||||
w.tracer.SetSpanSuccess(span)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// TracedUserService 自动追踪的用户服务包装器
|
||||
type TracedUserService struct {
|
||||
service interfaces.UserService
|
||||
wrapper *ServiceWrapper
|
||||
}
|
||||
|
||||
// NewTracedUserService 创建带追踪的用户服务
|
||||
func NewTracedUserService(service interfaces.UserService, wrapper *ServiceWrapper) interfaces.UserService {
|
||||
return &TracedUserService{
|
||||
service: service,
|
||||
wrapper: wrapper,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TracedUserService) Name() string {
|
||||
return "user-service"
|
||||
}
|
||||
|
||||
func (t *TracedUserService) Initialize(ctx context.Context) error {
|
||||
return t.wrapper.TraceServiceCall(ctx, "user", "initialize", t.service.Initialize)
|
||||
}
|
||||
|
||||
func (t *TracedUserService) HealthCheck(ctx context.Context) error {
|
||||
return t.service.HealthCheck(ctx) // 不追踪健康检查
|
||||
}
|
||||
|
||||
func (t *TracedUserService) Shutdown(ctx context.Context) error {
|
||||
return t.wrapper.TraceServiceCall(ctx, "user", "shutdown", t.service.Shutdown)
|
||||
}
|
||||
|
||||
func (t *TracedUserService) Register(ctx context.Context, req *dto.RegisterRequest) (*entities.User, error) {
|
||||
var result *entities.User
|
||||
var err error
|
||||
|
||||
traceErr := t.wrapper.TraceServiceCall(ctx, "user", "register", func(ctx context.Context) error {
|
||||
result, err = t.service.Register(ctx, req)
|
||||
return err
|
||||
})
|
||||
|
||||
if traceErr != nil {
|
||||
return nil, traceErr
|
||||
}
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (t *TracedUserService) LoginWithPassword(ctx context.Context, req *dto.LoginWithPasswordRequest) (*entities.User, error) {
|
||||
var result *entities.User
|
||||
var err error
|
||||
|
||||
traceErr := t.wrapper.TraceServiceCall(ctx, "user", "login_password", func(ctx context.Context) error {
|
||||
result, err = t.service.LoginWithPassword(ctx, req)
|
||||
return err
|
||||
})
|
||||
|
||||
if traceErr != nil {
|
||||
return nil, traceErr
|
||||
}
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (t *TracedUserService) LoginWithSMS(ctx context.Context, req *dto.LoginWithSMSRequest) (*entities.User, error) {
|
||||
var result *entities.User
|
||||
var err error
|
||||
|
||||
traceErr := t.wrapper.TraceServiceCall(ctx, "user", "login_sms", func(ctx context.Context) error {
|
||||
result, err = t.service.LoginWithSMS(ctx, req)
|
||||
return err
|
||||
})
|
||||
|
||||
if traceErr != nil {
|
||||
return nil, traceErr
|
||||
}
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (t *TracedUserService) ChangePassword(ctx context.Context, userID string, req *dto.ChangePasswordRequest) error {
|
||||
return t.wrapper.TraceServiceCall(ctx, "user", "change_password", func(ctx context.Context) error {
|
||||
return t.service.ChangePassword(ctx, userID, req)
|
||||
})
|
||||
}
|
||||
|
||||
func (t *TracedUserService) GetByID(ctx context.Context, id string) (*entities.User, error) {
|
||||
var result *entities.User
|
||||
var err error
|
||||
|
||||
traceErr := t.wrapper.TraceServiceCall(ctx, "user", "get_by_id", func(ctx context.Context) error {
|
||||
result, err = t.service.GetByID(ctx, id)
|
||||
return err
|
||||
})
|
||||
|
||||
if traceErr != nil {
|
||||
return nil, traceErr
|
||||
}
|
||||
|
||||
return result, err
|
||||
}
|
||||
474
internal/shared/tracing/tracer.go
Normal file
474
internal/shared/tracing/tracer.go
Normal file
@@ -0,0 +1,474 @@
|
||||
package tracing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
|
||||
"go.opentelemetry.io/otel/sdk/resource"
|
||||
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// TracerConfig 追踪器配置
|
||||
type TracerConfig struct {
|
||||
ServiceName string
|
||||
ServiceVersion string
|
||||
Environment string
|
||||
Endpoint string
|
||||
SampleRate float64
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
// DefaultTracerConfig 默认追踪器配置
|
||||
func DefaultTracerConfig() TracerConfig {
|
||||
return TracerConfig{
|
||||
ServiceName: "tyapi-server",
|
||||
ServiceVersion: "1.0.0",
|
||||
Environment: "development",
|
||||
Endpoint: "http://localhost:4317",
|
||||
SampleRate: 0.1,
|
||||
Enabled: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Tracer 链路追踪器
|
||||
type Tracer struct {
|
||||
config TracerConfig
|
||||
logger *zap.Logger
|
||||
provider *sdktrace.TracerProvider
|
||||
tracer trace.Tracer
|
||||
mutex sync.RWMutex
|
||||
initialized bool
|
||||
shutdown func(context.Context) error
|
||||
}
|
||||
|
||||
// NewTracer 创建链路追踪器
|
||||
func NewTracer(config TracerConfig, logger *zap.Logger) *Tracer {
|
||||
return &Tracer{
|
||||
config: config,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize 初始化追踪器
|
||||
func (t *Tracer) Initialize(ctx context.Context) error {
|
||||
t.mutex.Lock()
|
||||
defer t.mutex.Unlock()
|
||||
|
||||
if t.initialized {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !t.config.Enabled {
|
||||
t.logger.Info("Tracing is disabled")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 创建资源
|
||||
res, err := resource.New(ctx,
|
||||
resource.WithAttributes(
|
||||
attribute.String("service.name", t.config.ServiceName),
|
||||
attribute.String("service.version", t.config.ServiceVersion),
|
||||
attribute.String("environment", t.config.Environment),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create resource: %w", err)
|
||||
}
|
||||
|
||||
// 创建采样器
|
||||
sampler := sdktrace.TraceIDRatioBased(t.config.SampleRate)
|
||||
|
||||
// 创建导出器
|
||||
var spanProcessor sdktrace.SpanProcessor
|
||||
if t.config.Endpoint != "" {
|
||||
// 使用OTLP gRPC导出器(支持Jaeger、Tempo等)
|
||||
exporter, err := otlptracegrpc.New(ctx,
|
||||
otlptracegrpc.WithEndpoint(t.config.Endpoint),
|
||||
otlptracegrpc.WithInsecure(), // 开发环境使用,生产环境应配置TLS
|
||||
otlptracegrpc.WithTimeout(time.Second*10),
|
||||
otlptracegrpc.WithRetry(otlptracegrpc.RetryConfig{
|
||||
Enabled: true,
|
||||
InitialInterval: time.Millisecond * 100,
|
||||
MaxInterval: time.Second * 5,
|
||||
MaxElapsedTime: time.Second * 30,
|
||||
}),
|
||||
)
|
||||
if err != nil {
|
||||
t.logger.Warn("Failed to create OTLP exporter, using noop exporter",
|
||||
zap.Error(err),
|
||||
zap.String("endpoint", t.config.Endpoint))
|
||||
spanProcessor = sdktrace.NewSimpleSpanProcessor(&noopExporter{})
|
||||
} else {
|
||||
// 在生产环境中使用批处理器以提高性能
|
||||
spanProcessor = sdktrace.NewBatchSpanProcessor(exporter,
|
||||
sdktrace.WithBatchTimeout(time.Second*5),
|
||||
sdktrace.WithMaxExportBatchSize(512),
|
||||
sdktrace.WithMaxQueueSize(2048),
|
||||
sdktrace.WithExportTimeout(time.Second*30),
|
||||
)
|
||||
t.logger.Info("OTLP exporter initialized successfully",
|
||||
zap.String("endpoint", t.config.Endpoint))
|
||||
}
|
||||
} else {
|
||||
// 如果没有配置端点,使用空导出器
|
||||
spanProcessor = sdktrace.NewSimpleSpanProcessor(&noopExporter{})
|
||||
t.logger.Info("Using noop exporter (no endpoint configured)")
|
||||
}
|
||||
|
||||
// 创建TracerProvider
|
||||
provider := sdktrace.NewTracerProvider(
|
||||
sdktrace.WithResource(res),
|
||||
sdktrace.WithSampler(sampler),
|
||||
sdktrace.WithSpanProcessor(spanProcessor),
|
||||
)
|
||||
|
||||
// 设置全局TracerProvider
|
||||
otel.SetTracerProvider(provider)
|
||||
|
||||
// 创建Tracer
|
||||
tracer := provider.Tracer(t.config.ServiceName)
|
||||
|
||||
t.provider = provider
|
||||
t.tracer = tracer
|
||||
t.shutdown = func(ctx context.Context) error {
|
||||
return provider.Shutdown(ctx)
|
||||
}
|
||||
t.initialized = true
|
||||
|
||||
t.logger.Info("Tracing initialized successfully",
|
||||
zap.String("service", t.config.ServiceName),
|
||||
zap.Float64("sample_rate", t.config.SampleRate))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartSpan 开始一个新的span
|
||||
func (t *Tracer) StartSpan(ctx context.Context, name string, opts ...trace.SpanStartOption) (context.Context, trace.Span) {
|
||||
if !t.initialized || !t.config.Enabled {
|
||||
return ctx, trace.SpanFromContext(ctx)
|
||||
}
|
||||
|
||||
return t.tracer.Start(ctx, name, opts...)
|
||||
}
|
||||
|
||||
// StartHTTPSpan 开始一个HTTP span
|
||||
func (t *Tracer) StartHTTPSpan(ctx context.Context, method, path string) (context.Context, trace.Span) {
|
||||
spanName := fmt.Sprintf("%s %s", method, path)
|
||||
|
||||
// 检查是否已有错误标记,如果有则使用"error"作为操作名
|
||||
// 这样可以匹配Jaeger采样配置中的错误操作策略
|
||||
if ctx.Value("otel_error_request") != nil {
|
||||
spanName = "error"
|
||||
}
|
||||
|
||||
ctx, span := t.StartSpan(ctx, spanName,
|
||||
trace.WithSpanKind(trace.SpanKindServer),
|
||||
trace.WithAttributes(
|
||||
attribute.String("http.method", method),
|
||||
attribute.String("http.route", path),
|
||||
),
|
||||
)
|
||||
|
||||
// 保存原始操作名,以便在错误发生时可以更新
|
||||
if ctx.Value("otel_error_request") == nil {
|
||||
ctx = context.WithValue(ctx, "otel_original_operation", spanName)
|
||||
}
|
||||
|
||||
return ctx, span
|
||||
}
|
||||
|
||||
// StartDBSpan 开始一个数据库span
|
||||
func (t *Tracer) StartDBSpan(ctx context.Context, operation, table string) (context.Context, trace.Span) {
|
||||
spanName := fmt.Sprintf("db.%s.%s", operation, table)
|
||||
|
||||
return t.StartSpan(ctx, spanName,
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(
|
||||
attribute.String("db.operation", operation),
|
||||
attribute.String("db.table", table),
|
||||
attribute.String("db.system", "postgresql"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// StartCacheSpan 开始一个缓存span
|
||||
func (t *Tracer) StartCacheSpan(ctx context.Context, operation, key string) (context.Context, trace.Span) {
|
||||
spanName := fmt.Sprintf("cache.%s", operation)
|
||||
|
||||
return t.StartSpan(ctx, spanName,
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(
|
||||
attribute.String("cache.operation", operation),
|
||||
attribute.String("cache.system", "redis"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// StartExternalAPISpan 开始一个外部API调用span
|
||||
func (t *Tracer) StartExternalAPISpan(ctx context.Context, service, operation string) (context.Context, trace.Span) {
|
||||
spanName := fmt.Sprintf("api.%s.%s", service, operation)
|
||||
|
||||
return t.StartSpan(ctx, spanName,
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(
|
||||
attribute.String("api.service", service),
|
||||
attribute.String("api.operation", operation),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// AddSpanAttributes 添加span属性
|
||||
func (t *Tracer) AddSpanAttributes(span trace.Span, attrs ...attribute.KeyValue) {
|
||||
if span.IsRecording() {
|
||||
span.SetAttributes(attrs...)
|
||||
}
|
||||
}
|
||||
|
||||
// SetSpanError 设置span错误
|
||||
func (t *Tracer) SetSpanError(span trace.Span, err error) {
|
||||
if span.IsRecording() {
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
span.RecordError(err)
|
||||
|
||||
// 将span操作名更新为"error",以匹配Jaeger采样配置
|
||||
// 注意:这是一种变通方法,因为OpenTelemetry不支持直接更改span名称
|
||||
// 我们通过添加特殊属性来标识这是一个错误span
|
||||
span.SetAttributes(
|
||||
attribute.String("error.operation", "true"),
|
||||
attribute.String("operation.type", "error"),
|
||||
)
|
||||
|
||||
// 记录错误日志,包含trace ID便于关联
|
||||
if t.logger != nil {
|
||||
ctx := trace.ContextWithSpan(context.Background(), span)
|
||||
t.logger.Error("操作发生错误",
|
||||
zap.Error(err),
|
||||
zap.String("trace_id", t.GetTraceID(ctx)),
|
||||
zap.String("span_id", t.GetSpanID(ctx)),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SetSpanSuccess 设置span成功
|
||||
func (t *Tracer) SetSpanSuccess(span trace.Span) {
|
||||
if span.IsRecording() {
|
||||
span.SetStatus(codes.Ok, "success")
|
||||
}
|
||||
}
|
||||
|
||||
// SetHTTPStatus 根据HTTP状态码设置span状态
|
||||
func (t *Tracer) SetHTTPStatus(span trace.Span, statusCode int) {
|
||||
if !span.IsRecording() {
|
||||
return
|
||||
}
|
||||
|
||||
// 添加HTTP状态码属性
|
||||
span.SetAttributes(attribute.Int("http.status_code", statusCode))
|
||||
|
||||
// 对于4xx和5xx错误,标记为错误并应用错误采样策略
|
||||
if statusCode >= 400 {
|
||||
errorMsg := fmt.Sprintf("HTTP %d", statusCode)
|
||||
span.SetStatus(codes.Error, errorMsg)
|
||||
|
||||
// 添加错误操作标记,以匹配Jaeger采样配置
|
||||
span.SetAttributes(
|
||||
attribute.String("error.operation", "true"),
|
||||
attribute.String("operation.type", "error"),
|
||||
)
|
||||
|
||||
// 记录HTTP错误
|
||||
if t.logger != nil {
|
||||
ctx := trace.ContextWithSpan(context.Background(), span)
|
||||
t.logger.Warn("HTTP请求错误",
|
||||
zap.Int("status_code", statusCode),
|
||||
zap.String("trace_id", t.GetTraceID(ctx)),
|
||||
zap.String("span_id", t.GetSpanID(ctx)),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
span.SetStatus(codes.Ok, "success")
|
||||
}
|
||||
}
|
||||
|
||||
// GetTraceID 获取当前上下文的trace ID
|
||||
func (t *Tracer) GetTraceID(ctx context.Context) string {
|
||||
span := trace.SpanFromContext(ctx)
|
||||
if span.SpanContext().IsValid() {
|
||||
return span.SpanContext().TraceID().String()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetSpanID 获取当前上下文的span ID
|
||||
func (t *Tracer) GetSpanID(ctx context.Context) string {
|
||||
span := trace.SpanFromContext(ctx)
|
||||
if span.SpanContext().IsValid() {
|
||||
return span.SpanContext().SpanID().String()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// IsTracing 检查是否正在追踪
|
||||
func (t *Tracer) IsTracing(ctx context.Context) bool {
|
||||
span := trace.SpanFromContext(ctx)
|
||||
return span.SpanContext().IsValid() && span.IsRecording()
|
||||
}
|
||||
|
||||
// Shutdown 关闭追踪器
|
||||
func (t *Tracer) Shutdown(ctx context.Context) error {
|
||||
t.mutex.Lock()
|
||||
defer t.mutex.Unlock()
|
||||
|
||||
if !t.initialized || t.shutdown == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := t.shutdown(ctx)
|
||||
if err != nil {
|
||||
t.logger.Error("Failed to shutdown tracer", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
t.initialized = false
|
||||
t.logger.Info("Tracer shutdown successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetStats 获取追踪统计信息
|
||||
func (t *Tracer) GetStats() map[string]interface{} {
|
||||
t.mutex.RLock()
|
||||
defer t.mutex.RUnlock()
|
||||
|
||||
return map[string]interface{}{
|
||||
"initialized": t.initialized,
|
||||
"enabled": t.config.Enabled,
|
||||
"service_name": t.config.ServiceName,
|
||||
"service_version": t.config.ServiceVersion,
|
||||
"environment": t.config.Environment,
|
||||
"sample_rate": t.config.SampleRate,
|
||||
"endpoint": t.config.Endpoint,
|
||||
}
|
||||
}
|
||||
|
||||
// 实现Service接口
|
||||
|
||||
// Name 返回服务名称
|
||||
func (t *Tracer) Name() string {
|
||||
return "tracer"
|
||||
}
|
||||
|
||||
// HealthCheck 健康检查
|
||||
func (t *Tracer) HealthCheck(ctx context.Context) error {
|
||||
if !t.config.Enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !t.initialized {
|
||||
return fmt.Errorf("tracer not initialized")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// noopExporter 简单的无操作导出器(用于演示)
|
||||
type noopExporter struct{}
|
||||
|
||||
func (e *noopExporter) ExportSpans(ctx context.Context, spans []sdktrace.ReadOnlySpan) error {
|
||||
// 在实际应用中,这里应该将spans发送到Jaeger或其他追踪系统
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *noopExporter) Shutdown(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TraceMiddleware 追踪中间件工厂
|
||||
func (t *Tracer) TraceMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if !t.initialized || !t.config.Enabled {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// 开始HTTP span
|
||||
ctx, span := t.StartHTTPSpan(c.Request.Context(), c.Request.Method, c.FullPath())
|
||||
defer span.End()
|
||||
|
||||
// 将trace ID添加到响应头
|
||||
traceID := t.GetTraceID(ctx)
|
||||
if traceID != "" {
|
||||
c.Header("X-Trace-ID", traceID)
|
||||
}
|
||||
|
||||
// 将span上下文存储到gin上下文
|
||||
c.Request = c.Request.WithContext(ctx)
|
||||
|
||||
// 处理请求
|
||||
c.Next()
|
||||
|
||||
// 设置HTTP状态码
|
||||
t.SetHTTPStatus(span, c.Writer.Status())
|
||||
|
||||
// 添加响应信息
|
||||
t.AddSpanAttributes(span,
|
||||
attribute.Int("http.status_code", c.Writer.Status()),
|
||||
attribute.Int("http.response_size", c.Writer.Size()),
|
||||
)
|
||||
|
||||
// 添加错误信息
|
||||
if len(c.Errors) > 0 {
|
||||
errMsg := c.Errors.String()
|
||||
t.SetSpanError(span, fmt.Errorf(errMsg))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GinTraceMiddleware 兼容旧的方法名,保持向后兼容
|
||||
func (t *Tracer) GinTraceMiddleware() gin.HandlerFunc {
|
||||
return t.TraceMiddleware()
|
||||
}
|
||||
|
||||
// WithTracing 添加追踪到上下文的辅助函数
|
||||
func WithTracing(ctx context.Context, tracer *Tracer, name string) (context.Context, trace.Span) {
|
||||
return tracer.StartSpan(ctx, name)
|
||||
}
|
||||
|
||||
// TraceFunction 追踪函数执行的辅助函数
|
||||
func (t *Tracer) TraceFunction(ctx context.Context, name string, fn func(context.Context) error) error {
|
||||
ctx, span := t.StartSpan(ctx, name)
|
||||
defer span.End()
|
||||
|
||||
err := fn(ctx)
|
||||
if err != nil {
|
||||
t.SetSpanError(span, err)
|
||||
} else {
|
||||
t.SetSpanSuccess(span)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// TraceFunctionWithResult 追踪带返回值的函数执行
|
||||
func TraceFunctionWithResult[T any](ctx context.Context, tracer *Tracer, name string, fn func(context.Context) (T, error)) (T, error) {
|
||||
ctx, span := tracer.StartSpan(ctx, name)
|
||||
defer span.End()
|
||||
|
||||
result, err := fn(ctx)
|
||||
if err != nil {
|
||||
tracer.SetSpanError(span, err)
|
||||
} else {
|
||||
tracer.SetSpanSuccess(span)
|
||||
}
|
||||
|
||||
return result, err
|
||||
}
|
||||
255
scripts/deploy.ps1
Normal file
255
scripts/deploy.ps1
Normal file
@@ -0,0 +1,255 @@
|
||||
# TYAPI 生产环境部署脚本 (PowerShell版本)
|
||||
# 使用方法: .\scripts\deploy.ps1 [版本号]
|
||||
|
||||
param(
|
||||
[string]$Version = "latest"
|
||||
)
|
||||
|
||||
# 配置
|
||||
$REGISTRY_URL = "docker-registry.tianyuanapi.com"
|
||||
$IMAGE_NAME = "tyapi-server"
|
||||
$APP_VERSION = $Version
|
||||
$BUILD_TIME = (Get-Date -Format "yyyy-MM-ddTHH:mm:ssZ")
|
||||
|
||||
try {
|
||||
$GIT_COMMIT = git rev-parse --short HEAD 2>$null
|
||||
if (-not $GIT_COMMIT) { $GIT_COMMIT = "dev" }
|
||||
}
|
||||
catch {
|
||||
$GIT_COMMIT = "dev"
|
||||
}
|
||||
|
||||
# 颜色输出函数
|
||||
function Write-Info($message) {
|
||||
Write-Host "[INFO] $message" -ForegroundColor Blue
|
||||
}
|
||||
|
||||
function Write-Success($message) {
|
||||
Write-Host "[SUCCESS] $message" -ForegroundColor Green
|
||||
}
|
||||
|
||||
function Write-Warning($message) {
|
||||
Write-Host "[WARNING] $message" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
function Write-Error($message) {
|
||||
Write-Host "[ERROR] $message" -ForegroundColor Red
|
||||
}
|
||||
|
||||
# 检查必要工具
|
||||
function Test-Requirements {
|
||||
Write-Info "检查部署环境..."
|
||||
|
||||
if (-not (Get-Command docker -ErrorAction SilentlyContinue)) {
|
||||
Write-Error "Docker 未安装或不在 PATH 中"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if (-not (Get-Command docker-compose -ErrorAction SilentlyContinue)) {
|
||||
Write-Error "docker-compose 未安装或不在 PATH 中"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
|
||||
Write-Warning "Git 未安装,将使用默认提交哈希"
|
||||
}
|
||||
|
||||
Write-Success "环境检查通过"
|
||||
}
|
||||
|
||||
# 构建 Docker 镜像
|
||||
function Build-Image {
|
||||
Write-Info "开始构建 Docker 镜像..."
|
||||
|
||||
docker build `
|
||||
--build-arg VERSION="$APP_VERSION" `
|
||||
--build-arg COMMIT="$GIT_COMMIT" `
|
||||
--build-arg BUILD_TIME="$BUILD_TIME" `
|
||||
-t "$REGISTRY_URL/$IMAGE_NAME`:$APP_VERSION" `
|
||||
-t "$REGISTRY_URL/$IMAGE_NAME`:latest" `
|
||||
.
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Docker 镜像构建失败"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Success "Docker 镜像构建完成"
|
||||
}
|
||||
|
||||
# 推送镜像到私有仓库
|
||||
function Push-Image {
|
||||
Write-Info "推送镜像到私有仓库..."
|
||||
|
||||
# 推送版本标签
|
||||
docker push "$REGISTRY_URL/$IMAGE_NAME`:$APP_VERSION"
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Success "已推送版本标签: $APP_VERSION"
|
||||
}
|
||||
else {
|
||||
Write-Error "推送版本标签失败"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 推送latest标签
|
||||
docker push "$REGISTRY_URL/$IMAGE_NAME`:latest"
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Success "已推送latest标签"
|
||||
}
|
||||
else {
|
||||
Write-Error "推送latest标签失败"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# 准备生产环境配置
|
||||
function Test-Config {
|
||||
Write-Info "准备生产环境配置..."
|
||||
|
||||
# 检查.env文件是否存在
|
||||
if (-not (Test-Path ".env")) {
|
||||
if (Test-Path ".env.production") {
|
||||
Write-Warning ".env文件不存在,正在复制模板..."
|
||||
Copy-Item ".env.production" ".env"
|
||||
Write-Warning "请编辑 .env 文件并设置正确的配置值"
|
||||
exit 1
|
||||
}
|
||||
else {
|
||||
Write-Error "配置文件 .env 和 .env.production 都不存在"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# 验证关键配置
|
||||
$envContent = Get-Content ".env" -Raw
|
||||
if (-not ($envContent -match "^DB_PASSWORD=" -and -not ($envContent -match "your_secure_database_password_here"))) {
|
||||
Write-Error "请在 .env 文件中设置安全的数据库密码"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if (-not ($envContent -match "^JWT_SECRET=" -and -not ($envContent -match "your_super_secure_jwt_secret"))) {
|
||||
Write-Error "请在 .env 文件中设置安全的JWT密钥"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Success "配置检查通过"
|
||||
}
|
||||
|
||||
# 部署到生产环境
|
||||
function Start-Deploy {
|
||||
Write-Info "开始部署到生产环境..."
|
||||
|
||||
# 设置版本环境变量
|
||||
$env:APP_VERSION = $APP_VERSION
|
||||
|
||||
# 停止现有服务
|
||||
Write-Info "停止现有服务..."
|
||||
docker-compose -f docker-compose.prod.yml down --remove-orphans
|
||||
|
||||
# 清理未使用的镜像
|
||||
Write-Info "清理未使用的Docker资源..."
|
||||
docker image prune -f
|
||||
|
||||
# 拉取最新镜像
|
||||
Write-Info "拉取最新镜像..."
|
||||
docker-compose -f docker-compose.prod.yml pull
|
||||
|
||||
# 启动服务
|
||||
Write-Info "启动生产环境服务..."
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "服务启动失败"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 等待服务启动
|
||||
Write-Info "等待服务启动..."
|
||||
Start-Sleep -Seconds 30
|
||||
|
||||
# 检查服务状态
|
||||
Write-Info "检查服务状态..."
|
||||
docker-compose -f docker-compose.prod.yml ps
|
||||
|
||||
# 健康检查
|
||||
Write-Info "执行健康检查..."
|
||||
$maxAttempts = 10
|
||||
$attempt = 0
|
||||
|
||||
while ($attempt -lt $maxAttempts) {
|
||||
try {
|
||||
$response = Invoke-WebRequest -Uri "http://localhost:8080/health" -TimeoutSec 5 -ErrorAction Stop
|
||||
if ($response.StatusCode -eq 200) {
|
||||
Write-Success "应用健康检查通过"
|
||||
break
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$attempt++
|
||||
Write-Info "健康检查失败,重试 $attempt/$maxAttempts..."
|
||||
Start-Sleep -Seconds 10
|
||||
}
|
||||
}
|
||||
|
||||
if ($attempt -eq $maxAttempts) {
|
||||
Write-Error "应用健康检查失败,请检查日志"
|
||||
docker-compose -f docker-compose.prod.yml logs tyapi-app
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Success "部署完成!"
|
||||
}
|
||||
|
||||
# 显示部署信息
|
||||
function Show-Info {
|
||||
Write-Info "部署信息:"
|
||||
Write-Host " 版本: $APP_VERSION"
|
||||
Write-Host " 提交: $GIT_COMMIT"
|
||||
Write-Host " 构建时间: $BUILD_TIME"
|
||||
Write-Host " 镜像: $REGISTRY_URL/$IMAGE_NAME`:$APP_VERSION"
|
||||
Write-Host ""
|
||||
Write-Host "🌐 服务访问地址:"
|
||||
Write-Host " 📱 API服务: http://localhost:8080"
|
||||
Write-Host " 📚 API文档: http://localhost:8080/swagger/index.html"
|
||||
Write-Host " 💚 健康检查: http://localhost:8080/health"
|
||||
Write-Host ""
|
||||
Write-Host "📊 监控和追踪:"
|
||||
Write-Host " 📈 Grafana仪表盘: http://localhost:3000"
|
||||
Write-Host " 🔍 Prometheus监控: http://localhost:9090"
|
||||
Write-Host " 🔗 Jaeger链路追踪: http://localhost:16686"
|
||||
Write-Host ""
|
||||
Write-Host "🛠 管理工具:"
|
||||
Write-Host " 🗄️ pgAdmin数据库: http://localhost:5050"
|
||||
Write-Host " 📦 MinIO对象存储: http://localhost:9000"
|
||||
Write-Host " 🎛️ MinIO控制台: http://localhost:9001"
|
||||
Write-Host ""
|
||||
Write-Host "🔧 管理命令:"
|
||||
Write-Host " 查看日志: docker-compose -f docker-compose.prod.yml logs -f"
|
||||
Write-Host " 停止服务: docker-compose -f docker-compose.prod.yml down"
|
||||
Write-Host " 查看状态: docker-compose -f docker-compose.prod.yml ps"
|
||||
Write-Host " 重启应用: docker-compose -f docker-compose.prod.yml restart tyapi-app"
|
||||
}
|
||||
|
||||
# 主函数
|
||||
function Main {
|
||||
Write-Info "开始 TYAPI 生产环境部署..."
|
||||
Write-Info "版本: $APP_VERSION"
|
||||
|
||||
Test-Requirements
|
||||
Test-Config
|
||||
Build-Image
|
||||
Push-Image
|
||||
Start-Deploy
|
||||
Show-Info
|
||||
|
||||
Write-Success "🎉 部署成功!"
|
||||
}
|
||||
|
||||
# 运行主函数
|
||||
try {
|
||||
Main
|
||||
}
|
||||
catch {
|
||||
Write-Error "部署过程中发生错误: $($_.Exception.Message)"
|
||||
exit 1
|
||||
}
|
||||
221
scripts/deploy.sh
Normal file
221
scripts/deploy.sh
Normal file
@@ -0,0 +1,221 @@
|
||||
#!/bin/bash
|
||||
|
||||
# TYAPI 生产环境部署脚本
|
||||
# 使用方法: ./scripts/deploy.sh [version]
|
||||
|
||||
set -e
|
||||
|
||||
# 配置
|
||||
REGISTRY_URL="docker-registry.tianyuanapi.com"
|
||||
IMAGE_NAME="tyapi-server"
|
||||
APP_VERSION=${1:-latest}
|
||||
BUILD_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
GIT_COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo 'dev')
|
||||
|
||||
# 颜色输出
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 日志函数
|
||||
log_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
log_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# 检查必要工具
|
||||
check_requirements() {
|
||||
log_info "检查部署环境..."
|
||||
|
||||
if ! command -v docker &> /dev/null; then
|
||||
log_error "Docker 未安装或不在 PATH 中"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v docker-compose &> /dev/null; then
|
||||
log_error "docker-compose 未安装或不在 PATH 中"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v git &> /dev/null; then
|
||||
log_warning "Git 未安装,将使用默认提交哈希"
|
||||
fi
|
||||
|
||||
log_success "环境检查通过"
|
||||
}
|
||||
|
||||
# 构建 Docker 镜像
|
||||
build_image() {
|
||||
log_info "开始构建 Docker 镜像..."
|
||||
|
||||
docker build \
|
||||
--build-arg VERSION="$APP_VERSION" \
|
||||
--build-arg COMMIT="$GIT_COMMIT" \
|
||||
--build-arg BUILD_TIME="$BUILD_TIME" \
|
||||
-t "$REGISTRY_URL/$IMAGE_NAME:$APP_VERSION" \
|
||||
-t "$REGISTRY_URL/$IMAGE_NAME:latest" \
|
||||
.
|
||||
|
||||
log_success "Docker 镜像构建完成"
|
||||
}
|
||||
|
||||
# 推送镜像到私有仓库
|
||||
push_image() {
|
||||
log_info "推送镜像到私有仓库..."
|
||||
|
||||
# 推送版本标签
|
||||
docker push "$REGISTRY_URL/$IMAGE_NAME:$APP_VERSION"
|
||||
log_success "已推送版本标签: $APP_VERSION"
|
||||
|
||||
# 推送latest标签
|
||||
docker push "$REGISTRY_URL/$IMAGE_NAME:latest"
|
||||
log_success "已推送latest标签"
|
||||
}
|
||||
|
||||
# 准备生产环境配置
|
||||
prepare_config() {
|
||||
log_info "准备生产环境配置..."
|
||||
|
||||
# 检查.env文件是否存在
|
||||
if [ ! -f ".env" ]; then
|
||||
if [ -f ".env.production" ]; then
|
||||
log_warning ".env文件不存在,正在复制模板..."
|
||||
cp .env.production .env
|
||||
log_warning "请编辑 .env 文件并设置正确的配置值"
|
||||
exit 1
|
||||
else
|
||||
log_error "配置文件 .env 和 .env.production 都不存在"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# 验证关键配置
|
||||
if ! grep -q "^DB_PASSWORD=" .env || grep -q "your_secure_database_password_here" .env; then
|
||||
log_error "请在 .env 文件中设置安全的数据库密码"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! grep -q "^JWT_SECRET=" .env || grep -q "your_super_secure_jwt_secret" .env; then
|
||||
log_error "请在 .env 文件中设置安全的JWT密钥"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_success "配置检查通过"
|
||||
}
|
||||
|
||||
# 部署到生产环境
|
||||
deploy() {
|
||||
log_info "开始部署到生产环境..."
|
||||
|
||||
# 设置版本环境变量
|
||||
export APP_VERSION="$APP_VERSION"
|
||||
|
||||
# 停止现有服务
|
||||
log_info "停止现有服务..."
|
||||
docker-compose -f docker-compose.prod.yml down --remove-orphans
|
||||
|
||||
# 清理未使用的镜像
|
||||
log_info "清理未使用的Docker资源..."
|
||||
docker image prune -f
|
||||
|
||||
# 拉取最新镜像
|
||||
log_info "拉取最新镜像..."
|
||||
docker-compose -f docker-compose.prod.yml pull
|
||||
|
||||
# 启动服务
|
||||
log_info "启动生产环境服务..."
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
|
||||
# 等待服务启动
|
||||
log_info "等待服务启动..."
|
||||
sleep 30
|
||||
|
||||
# 检查服务状态
|
||||
log_info "检查服务状态..."
|
||||
docker-compose -f docker-compose.prod.yml ps
|
||||
|
||||
# 健康检查
|
||||
log_info "执行健康检查..."
|
||||
max_attempts=10
|
||||
attempt=0
|
||||
|
||||
while [ $attempt -lt $max_attempts ]; do
|
||||
if curl -f http://localhost:8080/health > /dev/null 2>&1; then
|
||||
log_success "应用健康检查通过"
|
||||
break
|
||||
else
|
||||
attempt=$((attempt + 1))
|
||||
log_info "健康检查失败,重试 $attempt/$max_attempts..."
|
||||
sleep 10
|
||||
fi
|
||||
done
|
||||
|
||||
if [ $attempt -eq $max_attempts ]; then
|
||||
log_error "应用健康检查失败,请检查日志"
|
||||
docker-compose -f docker-compose.prod.yml logs tyapi-app
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_success "部署完成!"
|
||||
}
|
||||
|
||||
# 显示部署信息
|
||||
show_info() {
|
||||
log_info "部署信息:"
|
||||
echo " 版本: $APP_VERSION"
|
||||
echo " 提交: $GIT_COMMIT"
|
||||
echo " 构建时间: $BUILD_TIME"
|
||||
echo " 镜像: $REGISTRY_URL/$IMAGE_NAME:$APP_VERSION"
|
||||
echo ""
|
||||
echo "🌐 服务访问地址:"
|
||||
echo " 📱 API服务: http://localhost:8080"
|
||||
echo " 📚 API文档: http://localhost:8080/swagger/index.html"
|
||||
echo " 💚 健康检查: http://localhost:8080/health"
|
||||
echo ""
|
||||
echo "📊 监控和追踪:"
|
||||
echo " 📈 Grafana仪表盘: http://localhost:3000"
|
||||
echo " 🔍 Prometheus监控: http://localhost:9090"
|
||||
echo " 🔗 Jaeger链路追踪: http://localhost:16686"
|
||||
echo ""
|
||||
echo "🛠 管理工具:"
|
||||
echo " 🗄️ pgAdmin数据库: http://localhost:5050"
|
||||
echo " 📦 MinIO对象存储: http://localhost:9000"
|
||||
echo " 🎛️ MinIO控制台: http://localhost:9001"
|
||||
echo ""
|
||||
echo "🔧 管理命令:"
|
||||
echo " 查看日志: docker-compose -f docker-compose.prod.yml logs -f"
|
||||
echo " 停止服务: docker-compose -f docker-compose.prod.yml down"
|
||||
echo " 查看状态: docker-compose -f docker-compose.prod.yml ps"
|
||||
echo " 重启应用: docker-compose -f docker-compose.prod.yml restart tyapi-app"
|
||||
}
|
||||
|
||||
# 主函数
|
||||
main() {
|
||||
log_info "开始 TYAPI 生产环境部署..."
|
||||
log_info "版本: $APP_VERSION"
|
||||
|
||||
check_requirements
|
||||
prepare_config
|
||||
build_image
|
||||
push_image
|
||||
deploy
|
||||
show_info
|
||||
|
||||
log_success "🎉 部署成功!"
|
||||
}
|
||||
|
||||
# 运行主函数
|
||||
main "$@"
|
||||
@@ -2,10 +2,10 @@
|
||||
-- This script runs when PostgreSQL container starts for the first time
|
||||
|
||||
-- Create development database if it doesn't exist
|
||||
CREATE DATABASE tyapi_dev;
|
||||
-- Note: tyapi_dev is already created by POSTGRES_DB environment variable
|
||||
|
||||
-- Create test database for running tests
|
||||
CREATE DATABASE tyapi_test;
|
||||
-- Note: Skip database creation in init script, handle in application if needed
|
||||
|
||||
-- Create production database (for reference)
|
||||
-- CREATE DATABASE tyapi_prod;
|
||||
@@ -30,25 +30,11 @@ CREATE SCHEMA IF NOT EXISTS metrics;
|
||||
-- Set search path
|
||||
SET search_path TO public, logs, metrics;
|
||||
|
||||
-- Connect to test database and setup extensions
|
||||
\c tyapi_test;
|
||||
-- Test database setup will be handled by application migrations
|
||||
-- when needed, since we don't create it in this init script
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS "pg_trgm";
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS "btree_gin";
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS public;
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS logs;
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS metrics;
|
||||
|
||||
SET search_path TO public, logs, metrics;
|
||||
|
||||
-- Switch back to development database
|
||||
\c tyapi_dev;
|
||||
-- Continue with development database setup
|
||||
-- (already connected to tyapi_dev)
|
||||
|
||||
-- Create application-specific roles (optional)
|
||||
-- CREATE ROLE tyapi_app WITH LOGIN PASSWORD 'app_password';
|
||||
@@ -63,9 +49,7 @@ SET search_path TO public, logs, metrics;
|
||||
-- This will be replaced by proper migrations in the application
|
||||
|
||||
-- Log the initialization
|
||||
INSERT INTO
|
||||
pg_stat_statements_info (dealloc)
|
||||
VALUES (0) ON CONFLICT DO NOTHING;
|
||||
-- Note: pg_stat_statements extension may not be available, skip this insert
|
||||
|
||||
-- Create a simple health check function
|
||||
CREATE OR REPLACE FUNCTION health_check()
|
||||
|
||||
Reference in New Issue
Block a user