This commit is contained in:
2026-04-21 22:36:48 +08:00
commit 488c695fdf
748 changed files with 266838 additions and 0 deletions

307
internal/config/README.md Normal file
View File

@@ -0,0 +1,307 @@
# 🔧 HYAPI 配置系统文档
## 📋 目录
- [配置策略概述](#配置策略概述)
- [文件结构](#文件结构)
- [配置加载流程](#配置加载流程)
- [环境配置](#环境配置)
- [配置验证](#配置验证)
- [使用指南](#使用指南)
- [最佳实践](#最佳实践)
- [故障排除](#故障排除)
## 🎯 配置策略概述
HYAPI 采用**分层配置策略**,支持多环境部署和灵活的配置管理:
```
📁 配置层次结构
├── 📄 config.yaml (基础配置模板)
└── 📁 configs/
├── 📄 env.development.yaml (开发环境覆盖)
├── 📄 env.production.yaml (生产环境覆盖)
└── 📄 env.testing.yaml (测试环境覆盖)
```
### 配置加载优先级(从高到低)
1. **环境变量** - 用于敏感信息和运行时覆盖
2. **环境特定配置文件** - `configs/env.{environment}.yaml`
3. **基础配置文件** - `config.yaml`
4. **默认值** - 代码中的默认配置
## 📁 文件结构
### 基础配置文件
- **位置**: `config.yaml`
- **作用**: 包含所有默认配置值,作为配置模板
- **特点**:
- 包含完整的配置结构
- 提供合理的默认值
- 作为所有环境的基础配置
### 环境配置文件
- **位置**: `configs/env.{environment}.yaml`
- **支持的环境**: `development`, `production`, `testing`
- **特点**:
- 只包含需要覆盖的配置项
- 继承基础配置的所有默认值
- 支持嵌套配置的深度合并
## 🔄 配置加载流程
### 1. 环境检测
```go
// 环境变量检测优先级
CONFIG_ENV > ENV > APP_ENV > 默认值(development)
```
### 2. 配置文件加载顺序
1. 读取基础配置文件 `config.yaml`
2. 查找环境配置文件 `configs/env.{environment}.yaml`
3. 合并环境配置到基础配置
4. 应用环境变量覆盖
5. 验证配置完整性
6. 输出配置摘要
### 3. 配置合并策略
- **递归合并**: 支持嵌套配置的深度合并
- **覆盖机制**: 环境配置覆盖基础配置
- **环境变量**: 最终覆盖任何配置项
## 🌍 环境配置
### 开发环境 (development)
```yaml
# configs/env.development.yaml
app:
env: development
database:
password: Pg9mX4kL8nW2rT5y
jwt:
secret: JwT8xR4mN9vP2sL7kH3oB6yC1zA5uF0qE9tW
```
### 生产环境 (production)
```yaml
# configs/env.production.yaml
app:
env: production
server:
mode: release
database:
sslmode: require
logger:
level: warn
format: json
```
### 测试环境 (testing)
```yaml
# configs/env.testing.yaml
app:
env: testing
server:
mode: test
database:
password: test_password
name: hyapi_test
redis:
db: 15
logger:
level: debug
jwt:
secret: test-jwt-secret-key-for-testing-only
```
## ✅ 配置验证
### 验证项目
- **数据库配置**: 主机、用户名、数据库名不能为空
- **JWT 配置**: 生产环境必须设置安全的 JWT 密钥
- **服务器配置**: 超时时间必须大于 0
- **连接池配置**: 最大空闲连接数不能大于最大连接数
### 验证失败处理
- 配置验证失败时,应用无法启动
- 提供详细的中文错误信息
- 帮助快速定位配置问题
## 📖 使用指南
### 1. 启动应用
```bash
# 使用默认环境 (development)
go run cmd/api/main.go
# 指定环境
CONFIG_ENV=production go run cmd/api/main.go
ENV=testing go run cmd/api/main.go
APP_ENV=production go run cmd/api/main.go
```
### 2. 添加新的配置项
1.`config.yaml` 中添加默认值
2.`internal/config/config.go` 中定义对应的结构体字段
3. 在环境配置文件中覆盖特定值(如需要)
### 3. 环境变量覆盖
```bash
# 覆盖数据库密码
export DATABASE_PASSWORD="your-secure-password"
# 覆盖JWT密钥
export JWT_SECRET="your-super-secret-jwt-key"
# 覆盖服务器端口
export SERVER_PORT="9090"
```
### 4. 添加新的环境
1. 创建 `configs/env.{new_env}.yaml` 文件
2.`getEnvironment()` 函数中添加环境验证
3. 配置相应的环境特定设置
## 🏆 最佳实践
### 1. 配置文件管理
-**基础配置**: 在 `config.yaml` 中设置合理的默认值
-**环境配置**: 只在环境文件中覆盖必要的配置项
-**敏感信息**: 通过环境变量注入,不要写在配置文件中
-**版本控制**: 将配置文件纳入版本控制,但排除敏感信息
### 2. 环境变量使用
-**生产环境**: 所有敏感信息都通过环境变量注入
-**开发环境**: 可以使用配置文件中的默认值
-**测试环境**: 使用独立的测试配置
### 3. 配置验证
-**启动验证**: 应用启动时验证所有必要配置
-**类型检查**: 确保配置值的类型正确
-**逻辑验证**: 验证配置项之间的逻辑关系
### 4. 日志和监控
-**配置摘要**: 启动时输出关键配置信息
-**环境标识**: 明确显示当前运行环境
-**配置变更**: 记录重要的配置变更
## 🔧 故障排除
### 常见问题
#### 1. 配置文件未找到
```
❌ 错误: 未找到 config.yaml 文件,请确保配置文件存在
```
**解决方案**: 确保项目根目录下存在 `config.yaml` 文件
#### 2. 环境配置文件未找到
```
未找到环境配置文件 configs/env.development.yaml将使用基础配置
```
**解决方案**:
- 检查环境变量设置是否正确
- 确认 `configs/env.{environment}.yaml` 文件存在
#### 3. 配置验证失败
```
❌ 错误: 配置验证失败: 数据库主机地址不能为空
```
**解决方案**:
- 检查 `config.yaml` 中的数据库配置
- 确认环境配置文件中的覆盖值正确
#### 4. JWT 密钥安全问题
```
❌ 错误: 生产环境必须设置安全的JWT密钥
```
**解决方案**:
- 通过环境变量设置安全的 JWT 密钥
- 不要使用默认的测试密钥
### 调试技巧
#### 1. 查看配置摘要
启动时查看配置摘要输出,确认:
- 当前运行环境
- 使用的配置文件
- 关键配置值
#### 2. 环境变量检查
```bash
# 检查环境变量
echo $CONFIG_ENV
echo $ENV
echo $APP_ENV
```
#### 3. 配置文件语法检查
```bash
# 检查YAML语法
yamllint config.yaml
yamllint configs/env.development.yaml
```
## 📚 相关文件
- `internal/config/config.go` - 配置结构体定义
- `internal/config/loader.go` - 配置加载逻辑
- `config.yaml` - 基础配置文件
- `configs/env.*.yaml` - 环境特定配置文件
## 🔄 更新日志
### v1.0.0
- 实现基础的分层配置策略
- 支持多环境配置
- 添加配置验证机制
- 实现环境变量覆盖功能
---
**注意**: 本配置系统遵循中文规范,所有面向用户的错误信息和日志都使用中文。

700
internal/config/config.go Normal file
View File

@@ -0,0 +1,700 @@
package config
import (
"os"
"strings"
"time"
)
// Config 应用程序总配置
type Config struct {
Server ServerConfig `mapstructure:"server"`
Database DatabaseConfig `mapstructure:"database"`
Redis RedisConfig `mapstructure:"redis"`
Cache CacheConfig `mapstructure:"cache"`
Logger LoggerConfig `mapstructure:"logger"`
JWT JWTConfig `mapstructure:"jwt"`
API APIConfig `mapstructure:"api"`
SMS SMSConfig `mapstructure:"sms"`
Email EmailConfig `mapstructure:"email"`
Storage StorageConfig `mapstructure:"storage"`
OCR OCRConfig `mapstructure:"ocr"`
RateLimit RateLimitConfig `mapstructure:"ratelimit"`
DailyRateLimit DailyRateLimitConfig `mapstructure:"daily_ratelimit"`
Monitoring MonitoringConfig `mapstructure:"monitoring"`
Health HealthConfig `mapstructure:"health"`
Resilience ResilienceConfig `mapstructure:"resilience"`
Development DevelopmentConfig `mapstructure:"development"`
App AppConfig `mapstructure:"app"`
WechatWork WechatWorkConfig `mapstructure:"wechat_work"`
Esign EsignConfig `mapstructure:"esign"`
Wallet WalletConfig `mapstructure:"wallet"`
WestDex WestDexConfig `mapstructure:"westdex"`
Zhicha ZhichaConfig `mapstructure:"zhicha"`
Muzi MuziConfig `mapstructure:"muzi"`
AliPay AliPayConfig `mapstructure:"alipay"`
Wxpay WxpayConfig `mapstructure:"wxpay"`
WechatMini WechatMiniConfig `mapstructure:"wechat_mini"`
WechatH5 WechatH5Config `mapstructure:"wechat_h5"`
Yushan YushanConfig `mapstructure:"yushan"`
TianYanCha TianYanChaConfig `mapstructure:"tianyancha"`
Alicloud AlicloudConfig `mapstructure:"alicloud"`
Xingwei XingweiConfig `mapstructure:"xingwei"`
Jiguang JiguangConfig `mapstructure:"jiguang"`
Shumai ShumaiConfig `mapstructure:"shumai"`
Shujubao ShujubaoConfig `mapstructure:"shujubao"`
PDFGen PDFGenConfig `mapstructure:"pdfgen"`
}
// ServerConfig HTTP服务器配置
type ServerConfig struct {
Port string `mapstructure:"port"`
Mode string `mapstructure:"mode"`
Host string `mapstructure:"host"`
ReadTimeout time.Duration `mapstructure:"read_timeout"`
WriteTimeout time.Duration `mapstructure:"write_timeout"`
IdleTimeout time.Duration `mapstructure:"idle_timeout"`
}
// DatabaseConfig 数据库配置
type DatabaseConfig struct {
Host string `mapstructure:"host"`
Port string `mapstructure:"port"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
Name string `mapstructure:"name"`
SSLMode string `mapstructure:"sslmode"`
Timezone string `mapstructure:"timezone"`
MaxOpenConns int `mapstructure:"max_open_conns"`
MaxIdleConns int `mapstructure:"max_idle_conns"`
ConnMaxLifetime time.Duration `mapstructure:"conn_max_lifetime"`
AutoMigrate bool `mapstructure:"auto_migrate"`
}
// RedisConfig Redis配置
type RedisConfig struct {
Host string `mapstructure:"host"`
Port string `mapstructure:"port"`
Password string `mapstructure:"password"`
DB int `mapstructure:"db"`
PoolSize int `mapstructure:"pool_size"`
MinIdleConns int `mapstructure:"min_idle_conns"`
MaxRetries int `mapstructure:"max_retries"`
DialTimeout time.Duration `mapstructure:"dial_timeout"`
ReadTimeout time.Duration `mapstructure:"read_timeout"`
WriteTimeout time.Duration `mapstructure:"write_timeout"`
}
// CacheConfig 缓存配置
type CacheConfig struct {
DefaultTTL time.Duration `mapstructure:"default_ttl"`
CleanupInterval time.Duration `mapstructure:"cleanup_interval"`
MaxSize int `mapstructure:"max_size"`
}
// LoggerConfig 日志配置
type LoggerConfig struct {
Level string `mapstructure:"level"`
Format string `mapstructure:"format"`
Output string `mapstructure:"output"`
LogDir string `mapstructure:"log_dir"` // 日志目录
MaxSize int `mapstructure:"max_size"` // 单个文件最大大小(MB)
MaxBackups int `mapstructure:"max_backups"` // 最大备份文件数
MaxAge int `mapstructure:"max_age"` // 最大保留天数
Compress bool `mapstructure:"compress"` // 是否压缩
UseColor bool `mapstructure:"use_color"` // 是否使用彩色输出
UseDaily bool `mapstructure:"use_daily"` // 是否按日分包
// 按级别分文件配置
EnableLevelSeparation bool `mapstructure:"enable_level_separation"` // 是否启用按级别分文件
LevelConfigs map[string]LevelFileConfig `mapstructure:"level_configs"` // 各级别配置
}
// LevelFileConfig 单个级别文件配置
type LevelFileConfig struct {
MaxSize int `mapstructure:"max_size"` // 单个文件最大大小(MB)
MaxBackups int `mapstructure:"max_backups"` // 最大备份文件数
MaxAge int `mapstructure:"max_age"` // 最大保留天数
Compress bool `mapstructure:"compress"` // 是否压缩
}
// JWTConfig JWT配置
type JWTConfig struct {
Secret string `mapstructure:"secret"`
ExpiresIn time.Duration `mapstructure:"expires_in"`
RefreshExpiresIn time.Duration `mapstructure:"refresh_expires_in"`
}
// RateLimitConfig 限流配置
type RateLimitConfig struct {
Requests int `mapstructure:"requests"`
Window time.Duration `mapstructure:"window"`
Burst int `mapstructure:"burst"`
}
// DailyRateLimitConfig 每日限流配置
type DailyRateLimitConfig struct {
MaxRequestsPerDay int `mapstructure:"max_requests_per_day"` // 每日最大请求次数
MaxRequestsPerIP int `mapstructure:"max_requests_per_ip"` // 每个IP每日最大请求次数
KeyPrefix string `mapstructure:"key_prefix"` // Redis键前缀
TTL time.Duration `mapstructure:"ttl"` // 键过期时间
// 新增安全配置
EnableIPWhitelist bool `mapstructure:"enable_ip_whitelist"` // 是否启用IP白名单
IPWhitelist []string `mapstructure:"ip_whitelist"` // IP白名单
EnableIPBlacklist bool `mapstructure:"enable_ip_blacklist"` // 是否启用IP黑名单
IPBlacklist []string `mapstructure:"ip_blacklist"` // IP黑名单
EnableUserAgent bool `mapstructure:"enable_user_agent"` // 是否检查User-Agent
BlockedUserAgents []string `mapstructure:"blocked_user_agents"` // 被阻止的User-Agent
EnableReferer bool `mapstructure:"enable_referer"` // 是否检查Referer
AllowedReferers []string `mapstructure:"allowed_referers"` // 允许的Referer
EnableGeoBlock bool `mapstructure:"enable_geo_block"` // 是否启用地理位置阻止
BlockedCountries []string `mapstructure:"blocked_countries"` // 被阻止的国家/地区
EnableProxyCheck bool `mapstructure:"enable_proxy_check"` // 是否检查代理
MaxConcurrent int `mapstructure:"max_concurrent"` // 最大并发请求数
// 路径排除配置
ExcludePaths []string `mapstructure:"exclude_paths"` // 排除频率限制的路径
// 域名排除配置
ExcludeDomains []string `mapstructure:"exclude_domains"` // 排除频率限制的域名
}
// MonitoringConfig 监控配置
type MonitoringConfig struct {
MetricsEnabled bool `mapstructure:"metrics_enabled"`
MetricsPort string `mapstructure:"metrics_port"`
TracingEnabled bool `mapstructure:"tracing_enabled"`
TracingEndpoint string `mapstructure:"tracing_endpoint"`
SampleRate float64 `mapstructure:"sample_rate"`
}
// HealthConfig 健康检查配置
type HealthConfig struct {
Enabled bool `mapstructure:"enabled"`
Interval time.Duration `mapstructure:"interval"`
Timeout time.Duration `mapstructure:"timeout"`
}
// ResilienceConfig 容错配置
type ResilienceConfig struct {
CircuitBreakerEnabled bool `mapstructure:"circuit_breaker_enabled"`
CircuitBreakerThreshold int `mapstructure:"circuit_breaker_threshold"`
CircuitBreakerTimeout time.Duration `mapstructure:"circuit_breaker_timeout"`
RetryMaxAttempts int `mapstructure:"retry_max_attempts"`
RetryInitialDelay time.Duration `mapstructure:"retry_initial_delay"`
RetryMaxDelay time.Duration `mapstructure:"retry_max_delay"`
}
// DevelopmentConfig 开发配置
type DevelopmentConfig struct {
Debug bool `mapstructure:"debug"`
EnableProfiler bool `mapstructure:"enable_profiler"`
EnableCors bool `mapstructure:"enable_cors"`
CorsOrigins string `mapstructure:"cors_allowed_origins"`
CorsMethods string `mapstructure:"cors_allowed_methods"`
CorsHeaders string `mapstructure:"cors_allowed_headers"`
}
// AppConfig 应用程序配置
type AppConfig struct {
Name string `mapstructure:"name"`
Version string `mapstructure:"version"`
Env string `mapstructure:"env"`
}
// APIConfig API配置
type APIConfig struct {
Domain string `mapstructure:"domain"`
// PublicBaseURL 浏览器/第三方访问本 API 服务的完整基址(如 https://api.example.com 或 http://127.0.0.1:8080无尾斜杠。
// 用于企业全景报告 reportUrl、headless PDF 预生成等。为空时由 Domain 推导为 https://{Domain}Domain 若已含 scheme 则沿用)。
PublicBaseURL string `mapstructure:"public_base_url"`
}
// ResolvedPublicBaseURL 由配置推导对外基址(不读环境变量)。
func (c *APIConfig) ResolvedPublicBaseURL() string {
u := strings.TrimSpace(c.PublicBaseURL)
if u != "" {
return strings.TrimRight(u, "/")
}
d := strings.TrimSpace(c.Domain)
if d == "" {
return ""
}
lo := strings.ToLower(d)
if strings.HasPrefix(lo, "http://") || strings.HasPrefix(lo, "https://") {
return strings.TrimRight(d, "/")
}
return "https://" + strings.TrimRight(d, "/")
}
// ResolveAPIPublicBaseURL 对外 API 基址。优先环境变量 API_PUBLIC_BASE_URL否则使用 API 配置。
func ResolveAPIPublicBaseURL(cfg *APIConfig) string {
if s := strings.TrimSpace(os.Getenv("API_PUBLIC_BASE_URL")); s != "" {
return strings.TrimRight(s, "/")
}
if cfg == nil {
return ""
}
return cfg.ResolvedPublicBaseURL()
}
// SMSConfig 短信配置
type SMSConfig struct {
// Provider 短信服务商aliyun、tencent为空时默认 tencent
Provider string `mapstructure:"provider"`
// TencentCloud 腾讯云短信provider=tencent 时必填,验证码与余额模板在控制台分别申请)
TencentCloud TencentSMSConfig `mapstructure:"tencent_cloud"`
// BalanceAlertTemplateCode 阿里云余额预警模板 CODE可选默认 SMS_500565339低/欠费共用同一模板
BalanceAlertTemplateCode string `mapstructure:"balance_alert_template_code"`
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"` // 是否启用模拟短信服务
// 签名验证配置
SignatureEnabled bool `mapstructure:"signature_enabled"` // 是否启用签名验证
SignatureSecret string `mapstructure:"signature_secret"` // 签名密钥
// 滑块验证码配置
CaptchaEnabled bool `mapstructure:"captcha_enabled"` // 是否启用滑块验证码
CaptchaSecret string `mapstructure:"captcha_secret"` // 阿里云验证码密钥
CaptchaEndpoint string `mapstructure:"captcha_endpoint"` // 阿里云验证码服务Endpoint
SceneID string `mapstructure:"scene_id"` // 阿里云验证码场景ID
}
// TencentSMSConfig 腾讯云短信
type TencentSMSConfig struct {
SecretId string `mapstructure:"secret_id"`
SecretKey string `mapstructure:"secret_key"`
Region string `mapstructure:"region"` // 如 ap-guangzhou可空则默认 ap-guangzhou
Endpoint string `mapstructure:"endpoint"` // 可空,默认 sms.tencentcloudapi.com
SmsSdkAppId string `mapstructure:"sms_sdk_app_id"` // SdkAppId
SignName string `mapstructure:"sign_name"`
TemplateID string `mapstructure:"template_id"` // 验证码模板 ID
// LowBalanceTemplateID 「余额不足」预警模板 ID与欠费模板可不同按无变量模板发送
LowBalanceTemplateID string `mapstructure:"low_balance_template_id"`
// ArrearsTemplateID 「欠费」预警模板 ID
ArrearsTemplateID string `mapstructure:"arrears_template_id"`
// BalanceAlertTemplateID 已废弃:若 low/arrears 未配则回退为同一模板 ID
BalanceAlertTemplateID string `mapstructure:"balance_alert_template_id"`
}
// SMSRateLimit 短信限流配置
type SMSRateLimit struct {
DailyLimit int `mapstructure:"daily_limit"` // 每日发送限制
HourlyLimit int `mapstructure:"hourly_limit"` // 每小时发送限制
MinInterval time.Duration `mapstructure:"min_interval"` // 最小发送间隔
}
// EmailConfig 邮件服务配置
type EmailConfig struct {
Host string `mapstructure:"host"` // SMTP服务器地址
Port int `mapstructure:"port"` // SMTP服务器端口
Username string `mapstructure:"username"` // 邮箱用户名
Password string `mapstructure:"password"` // 邮箱密码/授权码
FromEmail string `mapstructure:"from_email"` // 发件人邮箱
UseSSL bool `mapstructure:"use_ssl"` // 是否使用SSL
Timeout time.Duration `mapstructure:"timeout"` // 超时时间
Domain string `mapstructure:"domain"` // 控制台域名
}
// GetDSN 获取数据库DSN连接字符串
func (d DatabaseConfig) GetDSN() string {
return "host=" + d.Host +
" user=" + d.User +
" password=" + d.Password +
" dbname=" + d.Name +
" port=" + d.Port +
" sslmode=" + d.SSLMode +
" TimeZone=" + d.Timezone
}
// GetRedisAddr 获取Redis地址
func (r RedisConfig) GetRedisAddr() string {
return r.Host + ":" + r.Port
}
// IsProduction 检查是否为生产环境
func (a AppConfig) IsProduction() bool {
return a.Env == "production"
}
// IsDevelopment 检查是否为开发环境
func (a AppConfig) IsDevelopment() bool {
return a.Env == "development"
}
// IsStaging 检查是否为测试环境
func (a AppConfig) IsStaging() bool {
return a.Env == "staging"
}
// WechatWorkConfig 企业微信配置
type WechatWorkConfig struct {
WebhookURL string `mapstructure:"webhook_url"`
Secret string `mapstructure:"secret"`
}
// StorageConfig 存储服务配置
type StorageConfig struct {
AccessKey string `mapstructure:"access_key"`
SecretKey string `mapstructure:"secret_key"`
Bucket string `mapstructure:"bucket"`
Domain string `mapstructure:"domain"`
}
// OCRConfig OCR服务配置
type OCRConfig struct {
APIKey string `mapstructure:"api_key"`
SecretKey string `mapstructure:"secret_key"`
}
// EsignConfig e签宝配置
type EsignConfig struct {
AppID string `mapstructure:"app_id"` // 应用ID
AppSecret string `mapstructure:"app_secret"` // 应用密钥
ServerURL string `mapstructure:"server_url"` // 服务器URL
TemplateID string `mapstructure:"template_id"` // 模板ID
Contract ContractConfig `mapstructure:"contract"` // 合同配置
Auth AuthConfig `mapstructure:"auth"` // 认证配置
Sign SignConfig `mapstructure:"sign"` // 签署配置
}
// ContractConfig 合同配置
type ContractConfig struct {
Name string `mapstructure:"name"` // 合同名称
ExpireDays int `mapstructure:"expire_days"` // 签署链接过期天数
RetryCount int `mapstructure:"retry_count"` // 重试次数
}
// AuthConfig 认证配置
type AuthConfig struct {
OrgAuthModes []string `mapstructure:"org_auth_modes"` // 机构可用认证模式
DefaultAuthMode string `mapstructure:"default_auth_mode"` // 默认认证模式
PsnAuthModes []string `mapstructure:"psn_auth_modes"` // 个人可用认证模式
WillingnessAuthModes []string `mapstructure:"willingness_auth_modes"` // 意愿认证模式
RedirectURL string `mapstructure:"redirect_url"` // 重定向URL
}
// SignConfig 签署配置
type SignConfig struct {
AutoFinish bool `mapstructure:"auto_finish"` // 是否自动完结
SignFieldStyle int `mapstructure:"sign_field_style"` // 签署区样式
ClientType string `mapstructure:"client_type"` // 客户端类型
RedirectURL string `mapstructure:"redirect_url"` // 重定向URL
}
// WalletConfig 钱包配置
type WalletConfig struct {
DefaultCreditLimit float64 `mapstructure:"default_credit_limit"`
MinAmount string `mapstructure:"min_amount"` // 最低充值金额
MaxAmount string `mapstructure:"max_amount"` // 最高充值金额
RechargeBonusEnabled bool `mapstructure:"recharge_bonus_enabled"` // 是否启用充值赠送,关闭后仅展示商务洽谈提示
ApiStoreRechargeTip string `mapstructure:"api_store_recharge_tip"` // API 商店充值提示文案(大额/批量需求联系商务)
AliPayRechargeBonus []AliPayRechargeBonusRule `mapstructure:"alipay_recharge_bonus"`
BalanceAlert BalanceAlertConfig `mapstructure:"balance_alert"`
}
// BalanceAlertConfig 余额预警配置
type BalanceAlertConfig struct {
DefaultEnabled bool `mapstructure:"default_enabled"` // 默认启用余额预警
DefaultThreshold float64 `mapstructure:"default_threshold"` // 默认预警阈值
AlertCooldownHours int `mapstructure:"alert_cooldown_hours"` // 预警冷却时间(小时)
}
// AliPayRechargeBonusRule 支付宝充值赠送规则
type AliPayRechargeBonusRule struct {
RechargeAmount float64 `mapstructure:"recharge_amount"` // 充值金额
BonusAmount float64 `mapstructure:"bonus_amount"` // 赠送金额
}
// WestDexConfig 西部数据配置
type WestDexConfig struct {
URL string `mapstructure:"url"`
Key string `mapstructure:"key"`
SecretID string `mapstructure:"secret_id"`
SecretSecondID string `mapstructure:"secret_second_id"`
// 西部数据日志配置
Logging WestDexLoggingConfig `mapstructure:"logging"`
}
// WestDexLoggingConfig 西部数据日志配置
type WestDexLoggingConfig struct {
Enabled bool `mapstructure:"enabled"`
LogDir string `mapstructure:"log_dir"`
UseDaily bool `mapstructure:"use_daily"`
EnableLevelSeparation bool `mapstructure:"enable_level_separation"`
LevelConfigs map[string]WestDexLevelFileConfig `mapstructure:"level_configs"`
}
// WestDexLevelFileConfig 西部数据级别文件配置
type WestDexLevelFileConfig struct {
MaxSize int `mapstructure:"max_size"`
MaxBackups int `mapstructure:"max_backups"`
MaxAge int `mapstructure:"max_age"`
Compress bool `mapstructure:"compress"`
}
// ZhichaConfig 智查金控配置
type ZhichaConfig struct {
URL string `mapstructure:"url"`
AppID string `mapstructure:"app_id"`
AppSecret string `mapstructure:"app_secret"`
EncryptKey string `mapstructure:"encrypt_key"`
// 智查金控日志配置
Logging ZhichaLoggingConfig `mapstructure:"logging"`
}
// ZhichaLoggingConfig 智查金控日志配置
type ZhichaLoggingConfig struct {
Enabled bool `mapstructure:"enabled"`
LogDir string `mapstructure:"log_dir"`
UseDaily bool `mapstructure:"use_daily"`
EnableLevelSeparation bool `mapstructure:"enable_level_separation"`
LevelConfigs map[string]ZhichaLevelFileConfig `mapstructure:"level_configs"`
}
// ZhichaLevelFileConfig 智查金控级别文件配置
type ZhichaLevelFileConfig struct {
MaxSize int `mapstructure:"max_size"`
MaxBackups int `mapstructure:"max_backups"`
MaxAge int `mapstructure:"max_age"`
Compress bool `mapstructure:"compress"`
}
// MuziConfig 木子数据配置
type MuziConfig struct {
URL string `mapstructure:"url"`
AppID string `mapstructure:"app_id"`
AppSecret string `mapstructure:"app_secret"`
Timeout time.Duration `mapstructure:"timeout"`
Logging MuziLoggingConfig `mapstructure:"logging"`
}
// MuziLoggingConfig 木子数据日志配置
type MuziLoggingConfig struct {
Enabled bool `mapstructure:"enabled"`
LogDir string `mapstructure:"log_dir"`
UseDaily bool `mapstructure:"use_daily"`
EnableLevelSeparation bool `mapstructure:"enable_level_separation"`
LevelConfigs map[string]MuziLevelFileConfig `mapstructure:"level_configs"`
}
// MuziLevelFileConfig 木子数据日志级别配置
type MuziLevelFileConfig struct {
MaxSize int `mapstructure:"max_size"`
MaxBackups int `mapstructure:"max_backups"`
MaxAge int `mapstructure:"max_age"`
Compress bool `mapstructure:"compress"`
}
// AliPayConfig 支付宝配置
type AliPayConfig struct {
AppID string `mapstructure:"app_id"`
PrivateKey string `mapstructure:"private_key"`
AlipayPublicKey string `mapstructure:"alipay_public_key"`
IsProduction bool `mapstructure:"is_production"`
NotifyURL string `mapstructure:"notify_url"`
ReturnURL string `mapstructure:"return_url"`
}
// WxpayConfig 微信支付配置
type WxpayConfig struct {
AppID string `mapstructure:"app_id"`
MchID string `mapstructure:"mch_id"`
MchCertificateSerialNumber string `mapstructure:"mch_certificate_serial_number"`
MchApiv3Key string `mapstructure:"mch_apiv3_key"`
MchPrivateKeyPath string `mapstructure:"mch_private_key_path"`
MchPublicKeyID string `mapstructure:"mch_public_key_id"`
MchPublicKeyPath string `mapstructure:"mch_public_key_path"`
NotifyUrl string `mapstructure:"notify_url"`
RefundNotifyUrl string `mapstructure:"refund_notify_url"`
}
// WechatMiniConfig 微信小程序配置
type WechatMiniConfig struct {
AppID string `mapstructure:"app_id"`
}
// WechatH5Config 微信H5配置
type WechatH5Config struct {
AppID string `mapstructure:"app_id"`
}
// YushanConfig 羽山配置
type YushanConfig struct {
URL string `mapstructure:"url"`
APIKey string `mapstructure:"api_key"`
AcctID string `mapstructure:"acct_id"`
// 羽山日志配置
Logging YushanLoggingConfig `mapstructure:"logging"`
}
// YushanLoggingConfig 羽山日志配置
type YushanLoggingConfig struct {
Enabled bool `mapstructure:"enabled"`
LogDir string `mapstructure:"log_dir"`
UseDaily bool `mapstructure:"use_daily"`
EnableLevelSeparation bool `mapstructure:"enable_level_separation"`
LevelConfigs map[string]YushanLevelFileConfig `mapstructure:"level_configs"`
}
// YushanLevelFileConfig 羽山级别文件配置
type YushanLevelFileConfig struct {
MaxSize int `mapstructure:"max_size"`
MaxBackups int `mapstructure:"max_backups"`
MaxAge int `mapstructure:"max_age"`
Compress bool `mapstructure:"compress"`
}
// TianYanChaConfig 天眼查配置
type TianYanChaConfig struct {
BaseURL string `mapstructure:"base_url"`
APIKey string `mapstructure:"api_key"`
}
type AlicloudConfig struct {
Host string `mapstructure:"host"`
AppCode string `mapstructure:"app_code"`
}
// XingweiConfig 行为数据配置
type XingweiConfig struct {
URL string `mapstructure:"url"`
ApiID string `mapstructure:"api_id"`
ApiKey string `mapstructure:"api_key"`
// 行为数据日志配置
Logging XingweiLoggingConfig `mapstructure:"logging"`
}
// XingweiLoggingConfig 行为数据日志配置
type XingweiLoggingConfig struct {
Enabled bool `mapstructure:"enabled"`
LogDir string `mapstructure:"log_dir"`
UseDaily bool `mapstructure:"use_daily"`
EnableLevelSeparation bool `mapstructure:"enable_level_separation"`
LevelConfigs map[string]XingweiLevelFileConfig `mapstructure:"level_configs"`
}
// XingweiLevelFileConfig 行为数据级别文件配置
type XingweiLevelFileConfig struct {
MaxSize int `mapstructure:"max_size"`
MaxBackups int `mapstructure:"max_backups"`
MaxAge int `mapstructure:"max_age"`
Compress bool `mapstructure:"compress"`
}
// JiguangConfig 极光配置
type JiguangConfig struct {
URL string `mapstructure:"url"`
AppID string `mapstructure:"app_id"`
AppSecret string `mapstructure:"app_secret"`
SignMethod string `mapstructure:"sign_method"` // md5 或 hmac默认 hmac
Timeout time.Duration `mapstructure:"timeout"`
// 极光日志配置
Logging JiguangLoggingConfig `mapstructure:"logging"`
}
// JiguangLoggingConfig 极光日志配置
type JiguangLoggingConfig struct {
Enabled bool `mapstructure:"enabled"`
LogDir string `mapstructure:"log_dir"`
UseDaily bool `mapstructure:"use_daily"`
EnableLevelSeparation bool `mapstructure:"enable_level_separation"`
LevelConfigs map[string]JiguangLevelFileConfig `mapstructure:"level_configs"`
}
// JiguangLevelFileConfig 极光级别文件配置
type JiguangLevelFileConfig struct {
MaxSize int `mapstructure:"max_size"`
MaxBackups int `mapstructure:"max_backups"`
MaxAge int `mapstructure:"max_age"`
Compress bool `mapstructure:"compress"`
}
// ShumaiConfig 数脉配置
type ShumaiConfig struct {
URL string `mapstructure:"url"`
AppID string `mapstructure:"app_id"`
AppSecret string `mapstructure:"app_secret"`
AppID2 string `mapstructure:"app_id2"` // 走政务接口使用这个
AppSecret2 string `mapstructure:"app_secret2"` // 走政务接口使用这个
SignMethod string `mapstructure:"sign_method"` // md5 或 hmac默认 hmac
Timeout time.Duration `mapstructure:"timeout"`
Logging ShumaiLoggingConfig `mapstructure:"logging"`
}
// ShumaiLoggingConfig 数脉日志配置
type ShumaiLoggingConfig struct {
Enabled bool `mapstructure:"enabled"`
LogDir string `mapstructure:"log_dir"`
UseDaily bool `mapstructure:"use_daily"`
EnableLevelSeparation bool `mapstructure:"enable_level_separation"`
LevelConfigs map[string]ShumaiLevelFileConfig `mapstructure:"level_configs"`
}
// ShumaiLevelFileConfig 数脉级别文件配置
type ShumaiLevelFileConfig struct {
MaxSize int `mapstructure:"max_size"`
MaxBackups int `mapstructure:"max_backups"`
MaxAge int `mapstructure:"max_age"`
Compress bool `mapstructure:"compress"`
}
// ShujubaoConfig 数据宝配置
type ShujubaoConfig struct {
URL string `mapstructure:"url"`
AppSecret string `mapstructure:"app_secret"`
SignMethod string `mapstructure:"sign_method"` // md5 或 hmac默认 hmac
Timeout time.Duration `mapstructure:"timeout"`
Logging ShujubaoLoggingConfig `mapstructure:"logging"`
}
// ShujubaoLoggingConfig 数据宝日志配置
type ShujubaoLoggingConfig struct {
Enabled bool `mapstructure:"enabled"`
LogDir string `mapstructure:"log_dir"`
UseDaily bool `mapstructure:"use_daily"`
EnableLevelSeparation bool `mapstructure:"enable_level_separation"`
LevelConfigs map[string]ShujubaoLevelFileConfig `mapstructure:"level_configs"`
}
// ShujubaoLevelFileConfig 数据宝级别文件配置
type ShujubaoLevelFileConfig struct {
MaxSize int `mapstructure:"max_size"`
MaxBackups int `mapstructure:"max_backups"`
MaxAge int `mapstructure:"max_age"`
Compress bool `mapstructure:"compress"`
}
// PDFGenConfig PDF生成服务配置
type PDFGenConfig struct {
DevelopmentURL string `mapstructure:"development_url"` // 开发环境服务地址
ProductionURL string `mapstructure:"production_url"` // 生产环境服务地址
APIPath string `mapstructure:"api_path"` // API路径
Timeout time.Duration `mapstructure:"timeout"` // 请求超时时间
Cache PDFGenCacheConfig `mapstructure:"cache"` // 缓存配置
}
// PDFGenCacheConfig PDF生成缓存配置
type PDFGenCacheConfig struct {
TTL time.Duration `mapstructure:"ttl"` // 缓存过期时间
CacheDir string `mapstructure:"cache_dir"` // 缓存目录(空则使用默认目录)
MaxSize int64 `mapstructure:"max_size"` // 最大缓存大小0表示不限制单位字节
}
// DomainConfig 域名配置
type DomainConfig struct {
API string `mapstructure:"api"` // API域名
}

View File

@@ -0,0 +1,53 @@
package config
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func TestWalletConfig_AlipayRechargeBonus(t *testing.T) {
// 切换到项目根目录,这样配置加载器就能找到配置文件
originalWd, err := os.Getwd()
assert.NoError(t, err)
// 切换到项目根目录(从 internal/config 目录向上两级)
err = os.Chdir("../../")
assert.NoError(t, err)
defer os.Chdir(originalWd) // 测试结束后恢复原目录
// 加载配置
cfg, err := LoadConfig()
assert.NoError(t, err)
assert.NotNil(t, cfg)
// 验证钱包配置
assert.NotNil(t, cfg.Wallet)
assert.Greater(t, len(cfg.Wallet.AliPayRechargeBonus), 0, "支付宝充值赠送规则不能为空")
// 验证具体的赠送规则
expectedRules := []struct {
rechargeAmount float64
bonusAmount float64
}{
{1000.00, 50.00}, // 充1000送50
{5000.00, 300.00}, // 充5000送300
{10000.00, 800.00}, // 充10000送800
}
for i, expected := range expectedRules {
if i < len(cfg.Wallet.AliPayRechargeBonus) {
rule := cfg.Wallet.AliPayRechargeBonus[i]
assert.Equal(t, expected.rechargeAmount, rule.RechargeAmount,
"充值金额不匹配,期望: %f, 实际: %f", expected.rechargeAmount, rule.RechargeAmount)
assert.Equal(t, expected.bonusAmount, rule.BonusAmount,
"赠送金额不匹配,期望: %f, 实际: %f", expected.bonusAmount, rule.BonusAmount)
}
}
t.Logf("钱包配置加载成功,包含 %d 条支付宝充值赠送规则", len(cfg.Wallet.AliPayRechargeBonus))
for i, rule := range cfg.Wallet.AliPayRechargeBonus {
t.Logf("规则 %d: 充值 %.2f 元,赠送 %.2f 元", i+1, rule.RechargeAmount, rule.BonusAmount)
}
}

267
internal/config/loader.go Normal file
View File

@@ -0,0 +1,267 @@
package config
import (
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/spf13/viper"
)
// LoadConfig 加载应用程序配置
func LoadConfig() (*Config, error) {
// 1⃣ 获取环境变量决定配置文件
env := getEnvironment()
fmt.Printf("🔧 当前运行环境: %s\n", env)
// 2⃣ 加载基础配置文件
baseConfig := viper.New()
baseConfig.SetConfigName("config")
baseConfig.SetConfigType("yaml")
baseConfig.AddConfigPath(".")
baseConfig.AddConfigPath("./configs")
baseConfig.AddConfigPath("$HOME/.hyapi")
// 读取基础配置文件
if err := baseConfig.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
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(" 未找到环境配置文件 configs/env.%s.yaml将使用基础配置\n", env)
}
// 4⃣ 手动处理环境变量覆盖,避免空值覆盖配置文件
// overrideWithEnvVars(baseConfig)
// 5⃣ 解析配置到结构体
var config Config
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
}
// 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
// 递归合并子配置
for subK, subV := range subMap {
fullKey := fmt.Sprintf("%s.%s", subKey, subK)
baseConfig.Set(fullKey, subV)
}
} else {
// 直接设置值
baseConfig.Set(key, val)
}
}
return nil
}
// findEnvConfigFile 查找环境特定的配置文件
func findEnvConfigFile(env string) string {
// 只查找 configs 目录下的环境配置文件
possiblePaths := []string{
fmt.Sprintf("configs/env.%s.yaml", env),
fmt.Sprintf("configs/env.%s.yml", env),
}
for _, path := range possiblePaths {
if _, err := os.Stat(path); err == nil {
absPath, _ := filepath.Abs(path)
return absPath
}
}
return ""
}
// getEnvironment 获取当前环境
func getEnvironment() string {
var env string
var source string
// 优先级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 = "默认值"
}
fmt.Printf("🌍 环境检测: %s (来源: %s)\n", env, source)
// 验证环境值
validEnvs := []string{"development", "production", "testing"}
isValid := false
for _, validEnv := range validEnvs {
if env == validEnv {
isValid = true
break
}
}
if !isValid {
fmt.Printf("⚠️ 警告: 未识别的环境 '%s',将使用默认环境 'development'\n", env)
return "development"
}
return env
}
// printConfigSummary 打印配置摘要
func printConfigSummary(config *Config, env string) {
fmt.Printf("\n🔧 配置摘要:\n")
fmt.Printf(" 🌍 环境: %s\n", env)
fmt.Printf(" 📄 基础配置: config.yaml\n")
fmt.Printf(" 📁 环境配置: configs/env.%s.yaml\n", env)
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 验证配置
func validateConfig(config *Config) error {
// 验证必要的配置项
if config.Database.Host == "" {
return fmt.Errorf("数据库主机地址不能为空")
}
if config.Database.User == "" {
return fmt.Errorf("数据库用户名不能为空")
}
if config.Database.Name == "" {
return fmt.Errorf("数据库名称不能为空")
}
if config.JWT.Secret == "" || config.JWT.Secret == "your-super-secret-jwt-key-change-this-in-production" {
if config.App.IsProduction() {
return fmt.Errorf("生产环境必须设置安全的JWT密钥")
}
}
// 验证超时配置
if config.Server.ReadTimeout <= 0 {
return fmt.Errorf("服务器读取超时时间必须大于0")
}
if config.Server.WriteTimeout <= 0 {
return fmt.Errorf("服务器写入超时时间必须大于0")
}
// 验证数据库连接池配置
if config.Database.MaxOpenConns <= 0 {
return fmt.Errorf("数据库最大连接数必须大于0")
}
if config.Database.MaxIdleConns <= 0 {
return fmt.Errorf("数据库最大空闲连接数必须大于0")
}
if config.Database.MaxIdleConns > config.Database.MaxOpenConns {
return fmt.Errorf("数据库最大空闲连接数不能大于最大连接数")
}
return nil
}
// GetEnv 获取环境变量,如果不存在则返回默认值
func GetEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
// ParseDuration 解析时间字符串
func ParseDuration(s string) time.Duration {
d, err := time.ParseDuration(s)
if err != nil {
return 0
}
return d
}
// overrideWithEnvVars 手动处理环境变量覆盖,避免空值覆盖配置文件
func overrideWithEnvVars(config *viper.Viper) {
// 定义需要环境变量覆盖的敏感配置项
sensitiveConfigs := map[string]string{
"database.password": "DATABASE_PASSWORD",
"jwt.secret": "JWT_SECRET",
"redis.password": "REDIS_PASSWORD",
"wechat_work.webhook_url": "WECHAT_WORK_WEBHOOK_URL",
"wechat_work.secret": "WECHAT_WORK_SECRET",
}
// 只覆盖明确设置的环境变量
for configKey, envKey := range sensitiveConfigs {
if envValue := os.Getenv(envKey); envValue != "" {
config.Set(configKey, envValue)
fmt.Printf("🔐 已从环境变量覆盖配置: %s\n", configKey)
}
}
}
// SplitAndTrim 分割字符串并去除空格
func SplitAndTrim(s, sep string) []string {
parts := strings.Split(s, sep)
result := make([]string, 0, len(parts))
for _, part := range parts {
if trimmed := strings.TrimSpace(part); trimmed != "" {
result = append(result, trimmed)
}
}
return result
}