add JRZQ09J8、FLXGDEA8、FLXGDEA9、JRZQ1D09

add external_services log
This commit is contained in:
2025-08-25 15:44:06 +08:00
parent 365a2a8886
commit 267ff92998
80 changed files with 5555 additions and 1254 deletions

View File

@@ -1,288 +0,0 @@
# DDD规范企业认证信息自动填充实现总结
## 概述
根据DDD领域驱动设计架构规范重新设计了企业认证信息自动填充功能。在DDD中跨域操作应该通过应用服务层来协调而不是在领域服务层直接操作其他领域的仓储。
## DDD架构规范
### 1. 领域边界原则
- **领域服务层**:只能操作本领域的仓储和实体
- **应用服务层**:负责跨域协调,调用不同领域的服务
- **聚合根**:每个领域有自己的聚合根,不能直接访问其他领域的聚合根
### 2. 依赖方向
```
应用服务层 → 领域服务层 → 仓储层
跨域协调
```
## 重新设计架构
### 1. 领域服务层Finance Domain
#### `UserInvoiceInfoService`接口更新
```go
type UserInvoiceInfoService interface {
// 基础方法
GetUserInvoiceInfo(ctx context.Context, userID string) (*entities.UserInvoiceInfo, error)
CreateOrUpdateUserInvoiceInfo(ctx context.Context, userID string, invoiceInfo *value_objects.InvoiceInfo) (*entities.UserInvoiceInfo, error)
// 新增:包含企业认证信息的方法
GetUserInvoiceInfoWithEnterpriseInfo(ctx context.Context, userID string, companyName, taxpayerID string) (*entities.UserInvoiceInfo, error)
CreateOrUpdateUserInvoiceInfoWithEnterpriseInfo(ctx context.Context, userID string, invoiceInfo *value_objects.InvoiceInfo, companyName, taxpayerID string) (*entities.UserInvoiceInfo, error)
ValidateInvoiceInfo(ctx context.Context, invoiceInfo *value_objects.InvoiceInfo, invoiceType value_objects.InvoiceType) error
DeleteUserInvoiceInfo(ctx context.Context, userID string) error
}
```
#### 实现特点
- **移除跨域依赖**:不再直接依赖`user_repo.UserRepository`
- **参数化设计**:通过方法参数接收企业认证信息
- **保持纯净性**:领域服务只处理本领域的业务逻辑
### 2. 应用服务层Application Layer
#### `InvoiceApplicationService`更新
```go
type InvoiceApplicationServiceImpl struct {
invoiceRepo finance_repo.InvoiceApplicationRepository
userInvoiceInfoRepo finance_repo.UserInvoiceInfoRepository
userRepo user_repo.UserRepository
userAggregateService user_service.UserAggregateService // 新增:用户聚合服务
rechargeRecordRepo finance_repo.RechargeRecordRepository
walletRepo finance_repo.WalletRepository
invoiceDomainService services.InvoiceDomainService
invoiceAggregateService services.InvoiceAggregateService
userInvoiceInfoService services.UserInvoiceInfoService
storageService *storage.QiNiuStorageService
logger *zap.Logger
}
```
#### 跨域协调逻辑
```go
func (s *InvoiceApplicationServiceImpl) GetUserInvoiceInfo(ctx context.Context, userID string) (*dto.InvoiceInfoResponse, error) {
// 1. 通过用户聚合服务获取企业认证信息
user, err := s.userAggregateService.GetUserWithEnterpriseInfo(ctx, userID)
if err != nil {
return nil, fmt.Errorf("获取用户企业认证信息失败: %w", err)
}
// 2. 提取企业认证信息
var companyName, taxpayerID string
var companyNameReadOnly, taxpayerIDReadOnly bool
if user.EnterpriseInfo != nil {
companyName = user.EnterpriseInfo.CompanyName
taxpayerID = user.EnterpriseInfo.UnifiedSocialCode
companyNameReadOnly = true
taxpayerIDReadOnly = true
}
// 3. 调用领域服务,传入企业认证信息
userInvoiceInfo, err := s.userInvoiceInfoService.GetUserInvoiceInfoWithEnterpriseInfo(ctx, userID, companyName, taxpayerID)
if err != nil {
return nil, err
}
// 4. 构建响应DTO
return &dto.InvoiceInfoResponse{
CompanyName: userInvoiceInfo.CompanyName,
TaxpayerID: userInvoiceInfo.TaxpayerID,
// ... 其他字段
CompanyNameReadOnly: companyNameReadOnly,
TaxpayerIDReadOnly: taxpayerIDReadOnly,
}, nil
}
```
### 3. 依赖注入更新
#### 容器配置
```go
// 用户聚合服务
fx.Annotate(
user_service.NewUserAggregateService,
fx.ResultTags(`name:"userAggregateService"`),
),
// 用户开票信息服务移除userRepo依赖
fx.Annotate(
finance_service.NewUserInvoiceInfoService,
fx.ParamTags(
`name:"userInvoiceInfoRepo"`,
),
fx.ResultTags(`name:"userInvoiceInfoService"`),
),
// 发票应用服务添加userAggregateService依赖
fx.Annotate(
finance.NewInvoiceApplicationService,
fx.As(new(finance.InvoiceApplicationService)),
fx.ParamTags(
`name:"invoiceRepo"`,
`name:"userInvoiceInfoRepo"`,
`name:"userRepo"`,
`name:"userAggregateService"`, // 新增
`name:"rechargeRecordRepo"`,
`name:"walletRepo"`,
`name:"domainService"`,
`name:"aggregateService"`,
`name:"userInvoiceInfoService"`,
`name:"storageService"`,
`name:"logger"`,
),
),
```
## 架构优势
### 1. 符合DDD规范
- **领域边界清晰**:每个领域只处理自己的业务逻辑
- **依赖方向正确**:应用服务层负责跨域协调
- **聚合根隔离**:不同领域的聚合根不直接交互
### 2. 可维护性
- **职责分离**:领域服务专注于本领域逻辑
- **易于测试**:可以独立测试每个领域服务
- **扩展性好**:新增跨域功能时只需修改应用服务层
### 3. 业务逻辑清晰
- **数据流向明确**:企业认证信息 → 应用服务 → 开票信息
- **错误处理统一**:在应用服务层统一处理跨域错误
- **权限控制集中**:在应用服务层统一控制访问权限
## 工作流程
### 1. 获取开票信息流程
```
用户请求 → 应用服务层
调用UserAggregateService.GetUserWithEnterpriseInfo()
获取企业认证信息
调用UserInvoiceInfoService.GetUserInvoiceInfoWithEnterpriseInfo()
返回开票信息(包含企业认证信息)
```
### 2. 更新开票信息流程
```
用户请求 → 应用服务层
调用UserAggregateService.GetUserWithEnterpriseInfo()
验证企业认证状态
调用UserInvoiceInfoService.CreateOrUpdateUserInvoiceInfoWithEnterpriseInfo()
保存开票信息(企业认证信息自动填充)
```
## 技术实现要点
### 1. 接口设计
- **向后兼容**:保留原有的基础方法
- **功能扩展**:新增包含企业认证信息的方法
- **参数传递**:通过方法参数传递跨域数据
### 2. 错误处理
- **分层处理**:在应用服务层处理跨域错误
- **错误传播**:领域服务层错误向上传播
- **用户友好**:提供清晰的错误信息
### 3. 性能优化
- **减少查询**:应用服务层缓存企业认证信息
- **批量操作**:支持批量获取和更新
- **异步处理**:非关键路径支持异步处理
## 代码示例
### 1. 领域服务实现
```go
// GetUserInvoiceInfoWithEnterpriseInfo 获取用户开票信息(包含企业认证信息)
func (s *UserInvoiceInfoServiceImpl) GetUserInvoiceInfoWithEnterpriseInfo(ctx context.Context, userID string, companyName, taxpayerID string) (*entities.UserInvoiceInfo, error) {
info, err := s.userInvoiceInfoRepo.FindByUserID(ctx, userID)
if err != nil {
return nil, fmt.Errorf("获取用户开票信息失败: %w", err)
}
// 如果没有找到开票信息记录,创建新的实体
if info == nil {
info = &entities.UserInvoiceInfo{
ID: uuid.New().String(),
UserID: userID,
CompanyName: "",
TaxpayerID: "",
BankName: "",
BankAccount: "",
CompanyAddress: "",
CompanyPhone: "",
ReceivingEmail: "",
}
}
// 使用传入的企业认证信息填充公司名称和纳税人识别号
if companyName != "" {
info.CompanyName = companyName
}
if taxpayerID != "" {
info.TaxpayerID = taxpayerID
}
return info, nil
}
```
### 2. 应用服务实现
```go
func (s *InvoiceApplicationServiceImpl) UpdateUserInvoiceInfo(ctx context.Context, userID string, req UpdateInvoiceInfoRequest) error {
// 获取用户企业认证信息
user, err := s.userAggregateService.GetUserWithEnterpriseInfo(ctx, userID)
if err != nil {
return fmt.Errorf("获取用户企业认证信息失败: %w", err)
}
// 检查用户是否有企业认证信息
if user.EnterpriseInfo == nil {
return fmt.Errorf("用户未完成企业认证,无法创建开票信息")
}
// 创建开票信息对象
invoiceInfo := value_objects.NewInvoiceInfo(
"", // 公司名称将由服务层从企业认证信息中获取
"", // 纳税人识别号将由服务层从企业认证信息中获取
req.BankName,
req.BankAccount,
req.CompanyAddress,
req.CompanyPhone,
req.ReceivingEmail,
)
// 使用包含企业认证信息的方法
_, err = s.userInvoiceInfoService.CreateOrUpdateUserInvoiceInfoWithEnterpriseInfo(
ctx,
userID,
invoiceInfo,
user.EnterpriseInfo.CompanyName,
user.EnterpriseInfo.UnifiedSocialCode,
)
return err
}
```
## 总结
通过按照DDD规范重新设计我们实现了
1.**架构规范**严格遵循DDD的领域边界和依赖方向
2.**职责分离**:领域服务专注于本领域逻辑,应用服务负责跨域协调
3.**可维护性**:代码结构清晰,易于理解和维护
4.**可扩展性**:新增跨域功能时只需修改应用服务层
5.**业务逻辑**:企业认证信息自动填充功能完整实现
这种设计既满足了业务需求又符合DDD架构规范是一个优秀的架构实现。

View File

@@ -1,171 +0,0 @@
# Handler请求绑定方式更新总结
## 概述
根据用户要求将handler中的请求体参数绑定方式从`ShouldBindJSON`统一更新为使用`h.validator.BindAndValidate`,以保持代码风格的一致性和更好的验证处理。
## 主要变更
### 1. 更新的文件
#### `internal/infrastructure/http/handlers/finance_handler.go`
**更新的方法:**
1. **ApplyInvoice** - 申请开票
```go
// 更新前
if err := c.ShouldBindJSON(&req); err != nil {
h.responseBuilder.BadRequest(c, "请求参数错误", err)
return
}
// 更新后
if err := h.validator.BindAndValidate(c, &req); err != nil {
h.responseBuilder.BadRequest(c, "请求参数错误", err)
return
}
```
2. **UpdateUserInvoiceInfo** - 更新用户发票信息
```go
// 更新前
if err := c.ShouldBindJSON(&req); err != nil {
h.responseBuilder.BadRequest(c, "请求参数错误", err)
return
}
// 更新后
if err := h.validator.BindAndValidate(c, &req); err != nil {
h.responseBuilder.BadRequest(c, "请求参数错误", err)
return
}
```
3. **RejectInvoiceApplication** - 拒绝发票申请
```go
// 更新前
if err := c.ShouldBindJSON(&req); err != nil {
h.responseBuilder.BadRequest(c, "请求参数错误", err)
return
}
// 更新后
if err := h.validator.BindAndValidate(c, &req); err != nil {
h.responseBuilder.BadRequest(c, "请求参数错误", err)
return
}
```
#### `internal/infrastructure/http/handlers/api_handler.go`
**更新的方法:**
1. **AddWhiteListIP** - 添加白名单IP
```go
// 更新前
if err := c.ShouldBindJSON(&req); err != nil {
h.responseBuilder.BadRequest(c, "请求参数错误")
return
}
// 更新后
if err := h.validator.BindAndValidate(c, &req); err != nil {
h.responseBuilder.BadRequest(c, "请求参数错误")
return
}
```
### 2. 保持不变的文件
#### `internal/shared/validator/validator.go`
- `BindAndValidate`方法内部仍然使用`c.ShouldBindJSON(dto)`
- 这是正确的因为validator需要先绑定JSON数据然后再进行验证
- 这是validator的实现细节不需要修改
## 技术优势
### 1. 统一的验证处理
- 所有handler都使用相同的验证方式
- 统一的错误处理和响应格式
- 更好的代码一致性
### 2. 更好的错误信息
- `BindAndValidate`提供了更详细的验证错误信息
- 支持中文字段名显示
- 更友好的用户错误提示
### 3. 验证规则支持
- 支持自定义验证规则
- 支持字段翻译
- 支持复杂的业务验证逻辑
### 4. 代码维护性
- 统一的验证逻辑
- 便于后续验证规则的修改
- 减少重复代码
## 验证流程
### 1. 使用`h.validator.BindAndValidate`的流程
```
请求到达 → BindAndValidate → JSON绑定 → 结构体验证 → 返回结果
```
### 2. 错误处理流程
```
验证失败 → 格式化错误信息 → 返回BadRequest响应 → 前端显示错误
```
## 验证器功能
### 1. 支持的验证标签
- `required` - 必填字段
- `email` - 邮箱格式
- `min/max` - 长度限制
- `phone` - 手机号格式
- `strong_password` - 强密码
- `social_credit_code` - 统一社会信用代码
- `id_card` - 身份证号
- `price` - 价格格式
- `uuid` - UUID格式
- `url` - URL格式
- 等等...
### 2. 错误信息本地化
- 支持中文字段名
- 支持中文错误消息
- 支持自定义错误消息
## 兼容性
### 1. API接口保持不变
- 请求和响应格式完全一致
- 前端调用方式无需修改
- 向后兼容
### 2. 错误响应格式
- 保持原有的错误响应结构
- 错误信息更加详细和友好
- 支持字段级别的错误信息
## 后续建议
### 1. 代码审查
- 检查其他handler文件是否还有使用`ShouldBindJSON`的地方
- 确保所有新的handler都使用`BindAndValidate`
### 2. 测试验证
- 验证所有API接口的请求绑定是否正常工作
- 测试各种验证错误场景
- 确保错误信息显示正确
### 3. 文档更新
- 更新开发指南,说明使用`BindAndValidate`的最佳实践
- 更新API文档说明验证规则和错误格式
## 总结
通过这次更新我们成功统一了handler中的请求绑定方式使用`h.validator.BindAndValidate`替代了`ShouldBindJSON`。这样的更改带来了更好的代码一致性、更友好的错误处理和更强的验证能力同时保持了API的向后兼容性。
所有更改都经过了编译测试,确保没有引入任何错误。这为后续的开发工作奠定了良好的基础。

View File

@@ -44,17 +44,74 @@ cache:
cleanup_interval: 600s
max_size: 1000
# 🚀 日志系统配置 - 基于 Zap 官方推荐
logger:
level: "info"
format: "console"
output: "file"
log_dir: "logs"
max_size: 100
max_backups: 3
max_age: 7
compress: true
use_color: true
use_daily: false
# 基础配置
level: "info" # 日志级别: debug, info, warn, error, fatal, panic
format: "json" # 输出格式: json, console
output: "file" # 输出方式: stdout, stderr, file
log_dir: "logs" # 日志目录
use_daily: true # 是否按日分包
use_color: false # 是否使用彩色输出仅console格式有效
# 文件配置
max_size: 100 # 单个文件最大大小(MB)
max_backups: 5 # 最大备份文件数
max_age: 30 # 最大保留天数
compress: true # 是否压缩
# 高级功能
enable_level_separation: true # 是否启用按级别分文件
enable_request_logging: true # 是否启用请求日志
enable_performance_log: true # 是否启用性能日志
# 开发环境配置
development: true # 是否为开发环境
sampling: false # 是否启用采样
# 各级别配置(按级别分文件时使用)
level_configs:
debug:
max_size: 50 # 50MB
max_backups: 3
max_age: 7 # 7天
compress: true
info:
max_size: 100 # 100MB
max_backups: 5
max_age: 30 # 30天
compress: true
warn:
max_size: 100 # 100MB
max_backups: 5
max_age: 30 # 30天
compress: true
error:
max_size: 200 # 200MB
max_backups: 10
max_age: 90 # 90天
compress: true
fatal:
max_size: 100 # 100MB
max_backups: 10
max_age: 365 # 1年
compress: true
panic:
max_size: 100 # 100MB
max_backups: 10
max_age: 365 # 1年
compress: true
# 全面日志配置
comprehensive_logging:
enable_request_logging: true
enable_response_logging: true
enable_request_body_logging: true # 开发环境记录请求体
enable_error_logging: true
enable_business_logging: true
enable_performance_logging: true
max_body_size: 10240 # 10KB
exclude_paths: ["/health", "/metrics", "/favicon.ico", "/swagger"]
jwt:
secret: "JwT8xR4mN9vP2sL7kH3oB6yC1zA5uF0qE9tW"
@@ -107,45 +164,45 @@ ratelimit:
# 每日请求限制配置
daily_ratelimit:
max_requests_per_day: 200 # 每日最大请求次数
max_requests_per_ip: 10 # 每个IP每日最大请求次数
max_requests_per_day: 200 # 每日最大请求次数
max_requests_per_ip: 10 # 每个IP每日最大请求次数
key_prefix: "daily_limit" # Redis键前缀
ttl: 24h # 键过期时间
max_concurrent: 5 # 最大并发请求数
ttl: 24h # 键过期时间
max_concurrent: 5 # 最大并发请求数
# 安全配置
enable_ip_whitelist: false # 是否启用IP白名单
ip_whitelist: # IP白名单列表
- "192.168.1.*" # 内网IP段
- "10.0.0.*" # 内网IP段
- "127.0.0.1" # 本地回环
enable_ip_whitelist: false # 是否启用IP白名单
ip_whitelist: # IP白名单列表
- "192.168.1.*" # 内网IP段
- "10.0.0.*" # 内网IP段
- "127.0.0.1" # 本地回环
enable_ip_blacklist: true # 是否启用IP黑名单
ip_blacklist: # IP黑名单列表
- "0.0.0.0" # 无效IP
- "255.255.255.255" # 广播IP
enable_ip_blacklist: true # 是否启用IP黑名单
ip_blacklist: # IP黑名单列表
- "0.0.0.0" # 无效IP
- "255.255.255.255" # 广播IP
enable_user_agent: true # 是否检查User-Agent
blocked_user_agents: # 被阻止的User-Agent
- "bot" # 机器人
- "crawler" # 爬虫
- "spider" # 蜘蛛
- "scraper" # 抓取器
- "curl" # curl工具
- "wget" # wget工具
- "python" # Python脚本
- "java" # Java脚本
- "go-http-client" # Go HTTP客户端
enable_user_agent: true # 是否检查User-Agent
blocked_user_agents: # 被阻止的User-Agent
- "bot" # 机器人
- "crawler" # 爬虫
- "spider" # 蜘蛛
- "scraper" # 抓取器
- "curl" # curl工具
- "wget" # wget工具
- "python" # Python脚本
- "java" # Java脚本
- "go-http-client" # Go HTTP客户端
enable_referer: true # 是否检查Referer
allowed_referers: # 允许的Referer
- "https://console.tianyuanapi.com" # 天元API控制台
- "https://consoletest.tianyuanapi.com" # 天元API测试控制台
enable_referer: true # 是否检查Referer
allowed_referers: # 允许的Referer
- "https://console.tianyuanapi.com" # 天元API控制台
- "https://consoletest.tianyuanapi.com" # 天元API测试控制台
enable_proxy_check: true # 是否检查代理
enable_geo_block: false # 是否启用地理位置阻止
blocked_countries: # 被阻止的国家/地区
- "XX" # 示例国家代码
enable_proxy_check: true # 是否检查代理
enable_geo_block: false # 是否启用地理位置阻止
blocked_countries: # 被阻止的国家/地区
- "XX" # 示例国家代码
monitoring:
metrics_enabled: true
@@ -228,6 +285,32 @@ westdex:
secret_id: "449159"
secret_second_id: "296804"
# 西部数据日志配置
logging:
enabled: true
log_dir: "logs/external_services"
service_name: "westdex"
use_daily: true
enable_level_separation: true
# 各级别配置
level_configs:
info:
max_size: 100
max_backups: 5
max_age: 30
compress: true
error:
max_size: 200
max_backups: 10
max_age: 90
compress: true
warn:
max_size: 100
max_backups: 5
max_age: 30
compress: true
# ===========================================
# 🌍 羽山配置
# ===========================================
@@ -236,6 +319,32 @@ yushan:
api_key: "4c566c4a4b543164535455685655316c"
acct_id: "YSSJ843926726"
# 羽山日志配置
logging:
enabled: true
log_dir: "logs/external_services"
service_name: "yushan"
use_daily: true
enable_level_separation: true
# 各级别配置
level_configs:
info:
max_size: 100
max_backups: 5
max_age: 30
compress: true
error:
max_size: 200
max_backups: 10
max_age: 90
compress: true
warn:
max_size: 100
max_backups: 5
max_age: 30
compress: true
# ===========================================
# 💰 支付宝支付配置
# ===========================================
@@ -260,3 +369,38 @@ tianyancha:
alicloud:
host: "https://kzidcardv1.market.alicloudapi.com"
app_code: "d55b58829efb41c8aa8e86769cba4844"
# ===========================================
# 🔍 智查金控配置
# ===========================================
zhicha:
url: "https://www.zhichajinkong.com/dataMiddle/api/handle"
app_id: "4b78fff61ab8426f"
app_secret: "1128f01b94124ae899c2e9f2b1f37681"
encrypt_key: "af4ca0098e6a202a5c08c413ebd9fd62"
# 智查金控日志配置
logging:
enabled: true
log_dir: "logs/external_services"
service_name: "zhicha"
use_daily: true
enable_level_separation: true
# 各级别配置
level_configs:
info:
max_size: 100
max_backups: 5
max_age: 30
compress: true
error:
max_size: 200
max_backups: 10
max_age: 90
compress: true
warn:
max_size: 100
max_backups: 5
max_age: 30
compress: true

View File

@@ -15,19 +15,6 @@ database:
name: "tyapi_dev"
# ===========================================
# 📝 日志配置
# ===========================================
logger:
level: info
format: json
output: "console"
log_dir: "logs"
max_size: 100
max_backups: 5
max_age: 30
compress: true
use_daily: true
# ===========================================
# 🔐 JWT配置
# ===========================================
jwt:
@@ -113,3 +100,9 @@ wallet:
tianyancha:
base_url: http://open.api.tianyancha.com/services
api_key: e6a43dc9-786e-4a16-bb12-392b8201d8e2
# 智查金控配置示例
zhicha:
url: "http://proxy.tianyuanapi.com/dataMiddle/api/handle"
app_id: "4b78fff61ab8426f"
app_secret: "1128f01b94124ae899c2e9f2b1f37681"
encrypt_key: "af4ca0098e6a202a5c08c413ebd9fd62"

View File

@@ -34,63 +34,7 @@ redis:
port: "6379"
password: ""
db: 0
# ===========================================
# 📝 日志配置
# ===========================================
logger:
level: info
format: json
output: "file"
log_dir: "/app/logs"
max_size: 100
max_backups: 5
max_age: 30
compress: true
use_daily: true
# 启用按级别分文件
enable_level_separation: true
# 各级别日志文件配置
level_configs:
debug:
max_size: 50 # 50MB
max_backups: 3
max_age: 7 # 7天
compress: true
info:
max_size: 100 # 100MB
max_backups: 5
max_age: 30 # 30天
compress: true
warn:
max_size: 100 # 100MB
max_backups: 5
max_age: 30 # 30天
compress: true
error:
max_size: 200 # 200MB
max_backups: 10
max_age: 90 # 90天
compress: true
fatal:
max_size: 100 # 100MB
max_backups: 10
max_age: 365 # 1年
compress: true
panic:
max_size: 100 # 100MB
max_backups: 10
max_age: 365 # 1年
compress: true
# 生产环境全面日志配置
comprehensive_logging:
enable_request_logging: true
enable_response_logging: true
enable_request_body_logging: false # 生产环境不记录请求体(安全考虑)
enable_error_logging: true
enable_business_logging: true
enable_performance_logging: true
max_body_size: 10240 # 10KB
exclude_paths: ["/health", "/metrics", "/favicon.ico", "/swagger"]
# ===========================================
# 🔐 JWT配置
# ===========================================
@@ -139,14 +83,6 @@ esign:
client_type: "ALL"
redirect_url: "https://console.tianyuanapi.com/certification/callback/sign"
# ===========================================
# 🌍 西部数据配置
# ===========================================
westdex:
url: "http://proxy.tianyuanapi.com/api/invoke"
key: "121a1e41fc1690dd6b90afbcacd80cf4"
secret_id: "449159"
secret_second_id: "296804"
# ===========================================
# 💰 支付宝支付配置
# ===========================================
alipay:

View File

@@ -0,0 +1,453 @@
# 🚀 Zap 官方最佳实践日志系统指南
## 概述
本日志系统完全基于 [Zap 官方最佳实践](https://betterstack.com/community/guides/logging/go/zap/) 设计,使用 `zap.NewProduction()``zap.NewDevelopment()` 预设,提供高性能、结构化的日志记录。
## ✨ 核心特性
### 1. **基于 Zap 官方预设**
- 使用 `zap.NewProduction()` 生产环境预设
- 使用 `zap.NewDevelopment()` 开发环境预设
- 自动添加调用者信息、堆栈跟踪等
### 2. **全局日志器支持**
- 支持 `zap.ReplaceGlobals()` 全局替换
- 提供 `logger.L()``logger.GetGlobalLogger()` 访问
- 符合 Zap 官方推荐的使用方式
### 3. **强类型字段支持**
- 使用 `zap.String()`, `zap.Int()`, `zap.Error()` 等强类型字段
- 避免运行时类型错误
- 提供最佳性能
### 4. **上下文日志记录**
- 自动从上下文提取 `request_id`, `user_id`, `trace_id`
- 支持 `WithContext()` 方法
- 便于分布式系统追踪
## 🏗️ 架构设计
### 核心接口
```go
type Logger interface {
// 基础日志方法
Debug(msg string, fields ...zapcore.Field)
Info(msg string, fields ...zapcore.Field)
Warn(msg string, fields ...zapcore.Field)
Error(msg string, fields ...zapcore.Field)
Fatal(msg string, fields ...zapcore.Field)
Panic(msg string, fields ...zapcore.Field)
// 结构化日志方法
With(fields ...zapcore.Field) Logger
WithContext(ctx context.Context) Logger
Named(name string) Logger
// 同步和清理
Sync() error
Core() zapcore.Core
// 获取原生 Zap Logger
GetZapLogger() *zap.Logger
}
```
### 实现类型
1. **ZapLogger**: 标准日志器,基于 Zap 官方预设
2. **LevelLogger**: 级别分文件日志器,支持按级别分离
3. **全局日志器**: 通过 `zap.ReplaceGlobals()` 提供全局访问
## 🚀 使用方法
### 1. 基础使用
```go
package main
import (
"go.uber.org/zap"
"tyapi-server/internal/shared/logger"
)
func main() {
// 初始化全局日志器
config := logger.Config{
Development: true,
Output: "stdout",
Format: "console",
}
if err := logger.InitGlobalLogger(config); err != nil {
panic(err)
}
// 使用全局日志器
logger.L().Info("应用启动成功")
// 或者获取全局日志器
globalLogger := logger.GetGlobalLogger()
globalLogger.Info("使用全局日志器")
}
```
### 2. 依赖注入使用
```go
type ProductService struct {
logger logger.Logger
}
func NewProductService(logger logger.Logger) *ProductService {
return &ProductService{logger: logger}
}
func (s *ProductService) CreateProduct(ctx context.Context, product *Product) error {
// 记录操作日志
s.logger.Info("创建产品",
zap.String("product_id", product.ID),
zap.String("product_name", product.Name),
zap.String("user_id", product.CreatedBy),
)
// 业务逻辑...
return nil
}
```
### 3. 上下文日志记录
```go
func (s *ProductService) GetProduct(ctx context.Context, id string) (*Product, error) {
// 自动从上下文提取字段
logger := s.logger.WithContext(ctx)
logger.Info("获取产品信息",
zap.String("product_id", id),
zap.String("operation", "get_product"),
)
// 业务逻辑...
return product, nil
}
```
### 4. 结构化字段
```go
// 使用强类型字段
s.logger.Info("用户登录",
zap.String("username", "john_doe"),
zap.Int("user_id", 12345),
zap.String("ip_address", "192.168.1.100"),
zap.String("user_agent", r.UserAgent()),
zap.Time("login_time", time.Now()),
)
// 记录错误
if err != nil {
s.logger.Error("数据库操作失败",
zap.Error(err),
zap.String("operation", "create_user"),
zap.String("table", "users"),
)
}
```
### 5. 级别分文件日志
```go
// 配置启用级别分文件
config := logger.Config{
EnableLevelSeparation: true,
Output: "file",
LogDir: "logs",
UseDaily: true,
LevelConfigs: map[string]interface{}{
"debug": map[string]interface{}{
"max_size": 50,
"max_backups": 3,
"max_age": 7,
},
"error": map[string]interface{}{
"max_size": 200,
"max_backups": 10,
"max_age": 90,
},
},
}
// 创建级别分文件日志器
levelLogger, err := logger.NewLevelLogger(logger.LevelLoggerConfig{
BaseConfig: config,
EnableLevelSeparation: true,
LevelConfigs: convertLevelConfigs(config.LevelConfigs),
})
```
## 📁 日志文件结构
### 按级别分文件
```
logs/
├── 2024-01-01/
│ ├── debug.log # 调试日志
│ ├── info.log # 信息日志
│ ├── warn.log # 警告日志
│ ├── error.log # 错误日志
│ ├── fatal.log # 致命错误日志
│ └── panic.log # 恐慌错误日志
└── app.log # 主日志文件
```
### 按日期分包
```
logs/
├── 2024-01-01/
│ ├── app.log
│ └── error.log
├── 2024-01-02/
│ ├── app.log
│ └── error.log
└── app.log # 当前日期
```
## ⚙️ 配置选项
### 基础配置
```yaml
logger:
# 环境配置
development: true # 是否为开发环境
# 输出配置
output: "file" # 输出方式: stdout, stderr, file
format: "json" # 输出格式: json, console
log_dir: "logs" # 日志目录
# 文件配置
max_size: 100 # 单个文件最大大小(MB)
max_backups: 5 # 最大备份文件数
max_age: 30 # 最大保留天数
compress: true # 是否压缩
# 高级功能
use_daily: true # 是否按日分包
enable_level_separation: true # 是否启用按级别分文件
use_color: false # 是否使用彩色输出
```
### 级别配置
```yaml
logger:
level_configs:
debug:
max_size: 50 # 50MB
max_backups: 3 # 3个备份
max_age: 7 # 7天
compress: true
info:
max_size: 100 # 100MB
max_backups: 5 # 5个备份
max_age: 30 # 30天
compress: true
error:
max_size: 200 # 200MB
max_backups: 10 # 10个备份
max_age: 90 # 90天
compress: true
```
## 🔧 最佳实践
### 1. **使用强类型字段**
```go
// ✅ 推荐:使用强类型字段
logger.Info("用户操作",
zap.String("user_id", userID),
zap.String("action", "login"),
zap.Time("timestamp", time.Now()),
)
// ❌ 避免:使用 Any 字段
logger.Info("用户操作",
zap.Any("user_id", userID),
zap.Any("action", "login"),
zap.Any("timestamp", time.Now()),
)
```
### 2. **合理使用日志级别**
```go
// Debug: 详细的调试信息
logger.Debug("SQL查询", zap.String("query", sql))
// Info: 重要的业务事件
logger.Info("用户注册成功", zap.String("user_id", userID))
// Warn: 警告信息,不影响功能
logger.Warn("数据库连接池使用率过高", zap.Int("usage", 85))
// Error: 错误信息,功能受影响
logger.Error("数据库连接失败", zap.Error(err))
// Fatal: 致命错误,应用无法继续
logger.Fatal("配置文件加载失败", zap.Error(err))
```
### 3. **上下文信息提取**
```go
// 在中间件中设置上下文
func LoggingMiddleware(logger logger.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
// 生成请求ID
requestID := uuid.New().String()
// 设置上下文
ctx := context.WithValue(c.Request.Context(), "request_id", requestID)
ctx = context.WithValue(ctx, "user_id", getUserID(c))
ctx = context.WithValue(ctx, "trace_id", getTraceID(c))
c.Request = c.Request.WithContext(ctx)
// 记录请求日志
logger.WithContext(ctx).Info("收到请求",
zap.String("method", c.Request.Method),
zap.String("path", c.Request.URL.Path),
zap.String("client_ip", c.ClientIP()),
)
c.Next()
}
}
```
### 4. **性能优化**
```go
// ✅ 推荐:延迟计算
if logger.Core().Enabled(zapcore.DebugLevel) {
logger.Debug("调试信息", zap.String("data", expensiveOperation()))
}
// ❌ 避免:总是计算
logger.Debug("调试信息", zap.String("data", expensiveOperation()))
```
## 🚨 错误处理
### 1. **Panic 恢复**
```go
// 使用 panic 恢复中间件
func PanicRecoveryMiddleware(logger *zap.Logger) gin.HandlerFunc {
return gin.RecoveryWithWriter(&panicLogger{logger: logger})
}
type panicLogger struct {
logger *zap.Logger
}
func (pl *panicLogger) Write(p []byte) (n int, err error) {
pl.logger.Error("系统发生严重错误",
zap.String("error_type", "panic"),
zap.String("stack_trace", string(p)),
zap.String("timestamp", time.Now().Format("2006-01-02 15:04:05")),
)
return len(p), nil
}
```
### 2. **错误日志记录**
```go
// 记录错误详情
if err != nil {
logger.Error("操作失败",
zap.Error(err),
zap.String("operation", "create_user"),
zap.String("user_id", userID),
zap.String("stack_trace", string(debug.Stack())),
)
return err
}
```
## 📊 性能基准
基于 [Zap 官方基准测试](https://betterstack.com/community/guides/logging/go/zap/)
| 包 | 时间 | 相对于 Zap | 内存分配 |
|----|------|------------|----------|
| zap | 193 ns/op | +0% | 0 allocs/op |
| zap (sugared) | 227 ns/op | +18% | 1 allocs/op |
| zerolog | 81 ns/op | -58% | 0 allocs/op |
| slog | 322 ns/op | +67% | 0 allocs/op |
## 🔍 调试和故障排除
### 1. **检查日志级别**
```go
// 检查日志级别是否启用
if logger.Core().Enabled(zapcore.DebugLevel) {
logger.Debug("调试信息")
}
```
### 2. **同步日志**
```go
// 确保日志写入完成
defer logger.Sync()
// 或者在应用关闭时
func cleanup() {
logger.Sync()
}
```
### 3. **验证配置**
```go
// 验证日志器配置
config := logger.Config{
Development: true,
Output: "stdout",
Format: "console",
}
logger, err := logger.NewLogger(config)
if err != nil {
log.Fatalf("创建日志器失败: %v", err)
}
```
## 🎯 总结
本日志系统完全基于 Zap 官方最佳实践设计,具有以下优势:
1. **高性能**: 基于 Zap 的高性能实现
2. **官方推荐**: 使用 `zap.NewProduction()``zap.NewDevelopment()` 预设
3. **强类型**: 支持强类型字段,避免运行时错误
4. **结构化**: 支持结构化日志记录
5. **上下文**: 自动提取上下文信息
6. **灵活配置**: 支持文件输出、级别分离、按日分包等
7. **全局访问**: 支持全局日志器访问
通过合理使用,您将获得高性能、结构化的日志系统,满足生产环境的各种需求!
## 📚 参考资源
- [Zap 官方文档](https://pkg.go.dev/go.uber.org/zap)
- [Zap 最佳实践指南](https://betterstack.com/community/guides/logging/go/zap/)
- [Zap GitHub 仓库](https://github.com/uber-go/zap)

View File

@@ -0,0 +1,340 @@
# 🚀 新日志系统使用指南
## 概述
本项目已重新设计日志系统,完全基于 **Zap 官方最佳实践**,提供高性能、结构化的日志记录功能。
## ✨ 核心特性
### 🎯 **基于 Zap 官方推荐**
- 使用 `zap.Config` 和官方配置结构
- 支持 `zap.NewProductionConfig()``zap.NewDevelopmentConfig()`
- 完整的编码器配置和输出配置
### 📁 **灵活的日志输出**
- **控制台输出**: `stdout`, `stderr`
- **文件输出**: 支持日志轮转和压缩
- **按日分包**: 自动按日期创建目录
- **按级别分文件**: 不同级别写入不同文件
### 🔧 **智能配置**
- 根据环境自动选择最佳配置
- 支持选项模式配置
- 完整的默认值设置
## 🏗️ 架构设计
```
应用代码 → Logger接口 → LoggerFactory → Zap核心 → 输出目标
```
### **核心组件**
1. **`Logger` 接口**: 统一的日志接口
2. **`ZapLogger`**: 基于 Zap 的日志实现
3. **`LevelLogger`**: 级别分文件日志器
4. **`LoggerFactory`**: 日志器工厂,支持多种创建方式
## 📖 使用方法
### **1. 基础使用**
```go
import "tyapi-server/internal/shared/logger"
// 创建日志器
log, err := logger.NewLogger(logger.Config{
Level: "info",
Format: "json",
Output: "file",
LogDir: "logs",
UseDaily: true,
})
if err != nil {
panic(err)
}
// 记录日志
log.Info("应用启动成功",
logger.String("version", "1.0.0"),
logger.String("environment", "production"),
)
```
### **2. 使用日志工厂**
```go
// 创建工厂
factory := logger.NewLoggerFactory(logger.Config{
Level: "info",
Format: "json",
Output: "file",
Development: false, // 生产环境
})
// 创建生产环境日志器
prodLogger, err := factory.CreateProductionLogger()
if err != nil {
panic(err)
}
// 创建开发环境日志器
devLogger, err := factory.CreateDevelopmentLogger()
if err != nil {
panic(err)
}
// 根据环境自动选择
autoLogger, err := factory.CreateLoggerByEnvironment()
if err != nil {
panic(err)
}
```
### **3. 选项模式配置**
```go
// 使用选项模式
logger, err := factory.CreateLoggerWithOptions(
logger.WithLevel("debug"),
logger.WithFormat("console"),
logger.WithOutput("stdout"),
logger.WithDevelopment(true),
logger.WithColor(true),
)
```
### **4. 级别分文件日志器**
```go
// 创建级别分文件日志器
levelConfig := logger.LevelLoggerConfig{
BaseConfig: logger.Config{
Level: "info",
Format: "json",
Output: "file",
LogDir: "logs",
UseDaily: true,
},
EnableLevelSeparation: true,
LevelConfigs: map[zapcore.Level]logger.LevelFileConfig{
zapcore.InfoLevel: {
MaxSize: 100,
MaxBackups: 5,
MaxAge: 30,
Compress: true,
},
zapcore.ErrorLevel: {
MaxSize: 200,
MaxBackups: 10,
MaxAge: 90,
Compress: true,
},
},
}
levelLogger, err := logger.NewLevelLogger(levelConfig)
if err != nil {
panic(err)
}
// 不同级别的日志会写入不同文件
levelLogger.Info("这是一条信息日志") // 写入 logs/2024-01-01/info.log
levelLogger.Error("这是一条错误日志") // 写入 logs/2024-01-01/error.log
```
### **5. 结构化日志**
```go
// 使用 With 添加字段
userLogger := log.With(
logger.String("user_id", "12345"),
logger.String("action", "login"),
)
userLogger.Info("用户登录成功",
logger.String("ip", "192.168.1.1"),
logger.String("user_agent", "Mozilla/5.0..."),
)
// 使用 WithContext 从上下文提取字段
ctx := context.WithValue(context.Background(), "request_id", "req_123")
ctx = context.WithValue(ctx, "user_id", "user_456")
ctx = context.WithValue(ctx, "trace_id", "trace_789")
contextLogger := log.WithContext(ctx)
contextLogger.Info("处理请求",
logger.String("endpoint", "/api/users"),
logger.Int("status_code", 200),
)
```
### **6. 命名日志器**
```go
// 创建命名日志器
dbLogger := log.Named("database")
dbLogger.Info("数据库连接成功",
logger.String("host", "localhost"),
logger.String("database", "tyapi"),
)
apiLogger := log.Named("api")
apiLogger.Info("API 请求处理",
logger.String("method", "GET"),
logger.String("path", "/api/v1/users"),
)
```
## ⚙️ 配置说明
### **基础配置**
```yaml
logger:
# 基础配置
level: "info" # 日志级别
format: "json" # 输出格式
output: "file" # 输出方式
log_dir: "logs" # 日志目录
use_daily: true # 是否按日分包
use_color: false # 是否使用彩色输出
# 文件配置
max_size: 100 # 单个文件最大大小(MB)
max_backups: 5 # 最大备份文件数
max_age: 30 # 最大保留天数
compress: true # 是否压缩
# 高级功能
enable_level_separation: true # 是否启用按级别分文件
development: true # 是否为开发环境
```
### **级别配置**
```yaml
logger:
level_configs:
debug:
max_size: 50 # 50MB
max_backups: 3
max_age: 7 # 7天
compress: true
info:
max_size: 100 # 100MB
max_backups: 5
max_age: 30 # 30天
compress: true
error:
max_size: 200 # 200MB
max_backups: 10
max_age: 90 # 90天
compress: true
```
## 🔍 日志级别
### **级别说明**
- **`debug`**: 调试信息,开发环境使用
- **`info`**: 一般信息,记录应用状态
- **`warn`**: 警告信息,需要注意但不影响运行
- **`error`**: 错误信息,操作失败但可恢复
- **`fatal`**: 致命错误,应用无法继续运行
- **`panic`**: 恐慌错误,程序崩溃
### **级别选择建议**
- **开发环境**: `debug``info`
- **测试环境**: `info``warn`
- **生产环境**: `warn``error`
## 📊 日志格式
### **JSON 格式(推荐)**
```json
{
"level": "info",
"timestamp": "2024-01-01T12:00:00.000Z",
"logger": "main",
"caller": "main.go:25",
"message": "应用启动成功",
"version": "1.0.0",
"environment": "production"
}
```
### **Console 格式(开发环境)**
```
2024-01-01T12:00:00.000Z INFO main/main.go:25 应用启动成功 {"version": "1.0.0", "environment": "production"}
```
## 🚀 性能优化
### **Zap 官方推荐**
- 使用结构化字段而不是字符串拼接
- 避免在日志记录时进行复杂计算
- 合理设置日志级别,避免过度记录
### **最佳实践**
```go
// ✅ 推荐:使用结构化字段
log.Info("用户操作",
logger.String("user_id", userID),
logger.String("action", action),
logger.String("resource", resource),
)
// ❌ 不推荐:字符串拼接
log.Info(fmt.Sprintf("用户 %s 执行了 %s 操作,资源: %s", userID, action, resource))
```
## 🔧 故障排除
### **常见问题**
1. **日志文件未创建**
- 检查目录权限
- 确认配置中的 `log_dir` 路径
2. **日志级别不生效**
- 检查配置中的 `level`
- 确认日志器创建时使用了正确的配置
3. **按级别分文件不工作**
- 确认 `enable_level_separation: true`
- 检查 `level_configs` 配置
4. **按日分包不工作**
- 确认 `use_daily: true`
- 检查日期格式是否正确
### **调试技巧**
```go
// 启用调试模式
log.SetLevel(zapcore.DebugLevel)
// 检查日志器配置
if zapLogger, ok := log.(*logger.ZapLogger); ok {
config := zapLogger.GetConfig()
fmt.Printf("日志器配置: %+v\n", config)
}
```
## 📚 更多资源
- [Zap 官方文档](https://pkg.go.dev/go.uber.org/zap)
- [Zap 最佳实践](https://github.com/uber-go/zap/blob/master/FAQ.md)
- [结构化日志指南](https://github.com/uber-go/zap/blob/master/FAQ.md#q-how-do-i-choose-between-the-json-and-console-encoders)
## 🎉 总结
新的日志系统完全基于 Zap 官方最佳实践,提供了:
1. **高性能**: Zap 是 Go 生态中性能最好的日志库
2. **结构化**: 支持结构化字段,便于日志分析
3. **灵活性**: 支持多种输出方式和配置选项
4. **生产就绪**: 支持日志轮转、压缩、清理等生产环境需求
5. **官方推荐**: 完全按照 Zap 官方文档实现,确保最佳实践
使用新的日志系统,您将获得更好的性能、更清晰的日志结构和更强大的功能!

View File

@@ -0,0 +1,321 @@
# 📝 日志系统配置示例
## 概述
新的日志系统完全基于配置文件,所有配置都在 `config.yaml` 中设置,容器代码保持简洁。
## 🎯 完整配置示例
```yaml
# 🚀 日志系统配置 - 基于 Zap 官方推荐
logger:
# 基础配置
level: "info" # 日志级别: debug, info, warn, error, fatal, panic
format: "json" # 输出格式: json, console
output: "file" # 输出方式: stdout, stderr, file
log_dir: "logs" # 日志目录
use_daily: true # 是否按日分包
use_color: false # 是否使用彩色输出仅console格式有效
# 文件配置
max_size: 100 # 单个文件最大大小(MB)
max_backups: 5 # 最大备份文件数
max_age: 30 # 最大保留天数
compress: true # 是否压缩
# 高级功能
enable_level_separation: true # 是否启用按级别分文件
enable_request_logging: true # 是否启用请求日志
enable_performance_log: true # 是否启用性能日志
# 各级别配置(按级别分文件时使用)
level_configs:
debug:
max_size: 50 # 50MB
max_backups: 3
max_age: 7 # 7天
compress: true
info:
max_size: 100 # 100MB
max_backups: 5
max_age: 30 # 30天
compress: true
warn:
max_size: 100 # 100MB
max_backups: 5
max_age: 30 # 30天
compress: true
error:
max_size: 200 # 200MB
max_backups: 10
max_age: 90 # 90天
compress: true
fatal:
max_size: 100 # 100MB
max_backups: 10
max_age: 365 # 1年
compress: true
panic:
max_size: 100 # 100MB
max_backups: 10
max_age: 365 # 1年
compress: true
```
## 🔧 环境特定配置
### 开发环境配置
```yaml
# 开发环境 - 详细日志,控制台输出
logger:
level: "debug"
format: "console"
output: "stdout"
use_daily: false
use_color: true
enable_level_separation: false
development: true
```
### 生产环境配置
```yaml
# 生产环境 - 精简日志,文件输出
logger:
level: "warn"
format: "json"
output: "file"
log_dir: "/app/logs"
use_daily: true
use_color: false
enable_level_separation: true
development: false
# 生产环境文件配置
max_size: 200
max_backups: 10
max_age: 90
compress: true
# 生产环境级别配置
level_configs:
warn:
max_size: 200
max_backups: 10
max_age: 90
compress: true
error:
max_size: 500
max_backups: 20
max_age: 180
compress: true
fatal:
max_size: 100
max_backups: 10
max_age: 365
compress: true
```
## 📊 配置字段说明
### 基础配置字段
| 字段 | 类型 | 说明 | 默认值 |
|------|------|------|--------|
| `level` | string | 日志级别 | "info" |
| `format` | string | 输出格式 | "json" |
| `output` | string | 输出方式 | "stdout" |
| `log_dir` | string | 日志目录 | "logs" |
| `use_daily` | bool | 是否按日分包 | false |
| `use_color` | bool | 是否使用彩色输出 | false |
### 文件配置字段
| 字段 | 类型 | 说明 | 默认值 |
|------|------|------|--------|
| `max_size` | int | 单个文件最大大小(MB) | 100 |
| `max_backups` | int | 最大备份文件数 | 5 |
| `max_age` | int | 最大保留天数 | 30 |
| `compress` | bool | 是否压缩 | true |
### 高级功能字段
| 字段 | 类型 | 说明 | 默认值 |
|------|------|------|--------|
| `enable_level_separation` | bool | 是否启用按级别分文件 | false |
| `enable_request_logging` | bool | 是否启用请求日志 | false |
| `enable_performance_log` | bool | 是否启用性能日志 | false |
### 级别配置字段
| 字段 | 类型 | 说明 | 默认值 |
|------|------|------|--------|
| `max_size` | int | 单个文件最大大小(MB) | 无 |
| `max_backups` | int | 最大备份文件数 | 无 |
| `max_age` | int | 最大保留天数 | 无 |
| `compress` | bool | 是否压缩 | 无 |
## 🚀 配置最佳实践
### 1. 按环境配置
```yaml
# 开发环境
logger:
level: "debug"
format: "console"
output: "stdout"
use_color: true
enable_level_separation: false
# 测试环境
logger:
level: "info"
format: "json"
output: "file"
use_daily: true
enable_level_separation: true
# 生产环境
logger:
level: "warn"
format: "json"
output: "file"
use_daily: true
enable_level_separation: true
max_size: 200
max_backups: 10
max_age: 90
```
### 2. 按级别配置
```yaml
logger:
enable_level_separation: true
level_configs:
# 调试日志 - 小文件,短期保留
debug:
max_size: 50
max_backups: 3
max_age: 7
compress: true
# 信息日志 - 中等文件,中期保留
info:
max_size: 100
max_backups: 5
max_age: 30
compress: true
# 警告日志 - 中等文件,中期保留
warn:
max_size: 100
max_backups: 5
max_age: 30
compress: true
# 错误日志 - 大文件,长期保留
error:
max_size: 200
max_backups: 10
max_age: 90
compress: true
# 致命错误 - 中等文件,长期保留
fatal:
max_size: 100
max_backups: 10
max_age: 365
compress: true
```
### 3. 性能优化配置
```yaml
logger:
# 生产环境性能优化
level: "warn" # 只记录警告及以上级别
format: "json" # JSON格式便于日志分析
output: "file" # 文件输出避免控制台性能影响
use_daily: true # 按日分包便于管理
enable_level_separation: true # 按级别分文件便于分析
# 文件轮转优化
max_size: 200 # 较大的文件大小减少轮转频率
max_backups: 10 # 适中的备份数量
max_age: 90 # 90天保留期平衡存储和分析需求
compress: true # 启用压缩节省存储空间
```
## 🔍 配置验证
### 1. 必需字段
以下字段是必需的,如果未设置将使用默认值:
- `level`: 日志级别
- `format`: 输出格式
- `output`: 输出方式
### 2. 可选字段
以下字段是可选的,如果未设置将使用系统默认值:
- `log_dir`: 日志目录
- `use_daily`: 是否按日分包
- `use_color`: 是否使用彩色输出
- `max_size`: 文件大小限制
- `max_backups`: 备份文件数量
- `max_age`: 文件保留天数
- `compress`: 是否压缩
### 3. 条件字段
以下字段在特定条件下是必需的:
-`output: "file"` 时,建议设置 `log_dir`
-`enable_level_separation: true` 时,建议设置 `level_configs`
-`format: "console"` 时,`use_color` 才有效
## 📝 配置迁移指南
### 从旧配置迁移
如果您之前使用的是旧版本的日志配置,可以按照以下方式迁移:
#### 旧配置
```yaml
# 旧版本配置
logging:
level: "info"
file: "logs/app.log"
max_size: 100
max_backups: 3
```
#### 新配置
```yaml
# 新版本配置
logger:
level: "info"
output: "file"
log_dir: "logs"
max_size: 100
max_backups: 3
format: "json"
use_daily: false
```
## 🎉 总结
新的日志系统配置完全基于 `config.yaml` 文件,具有以下特点:
1. **配置集中**: 所有日志配置都在一个地方管理
2. **环境友好**: 支持不同环境的配置
3. **灵活性强**: 支持按级别分文件、按日分包等高级功能
4. **性能优化**: 基于 Zap 官方最佳实践
5. **易于维护**: 配置结构清晰,易于理解和修改
通过合理的配置,您可以获得高性能、结构化的日志系统,满足开发、测试和生产环境的各种需求!

View File

@@ -0,0 +1,383 @@
# 🚀 西部数据日志系统使用指南
## 概述
西部数据服务现在集成了完整的日志记录系统,支持请求、响应、错误和性能四种类型的日志记录,每种类型都有独立的日志文件。
## 📝 日志记录范围
### ✅ **会记录日志的操作**
- `CallAPI()` - 调用西部数据API
- `G05HZ01CallAPI()` - 调用G05HZ01 API
### ❌ **不会记录日志的操作**
- `Encrypt()` - 内部加密方法
- `Md5Encrypt()` - 内部MD5加密方法
**说明**: 加密解密是内部工具方法调用频率高且不涉及外部API因此不记录日志以提高性能。日志系统专注于记录外部API调用的完整生命周期。
## ✨ 核心特性
### 1. **四种日志类型**
- **请求日志**: 记录所有API请求的详细信息
- **响应日志**: 记录所有API响应的详细信息
- **错误日志**: 记录所有错误和异常情况
- **性能日志**: 记录请求耗时和性能指标
### 2. **日志文件分离**
```
logs/
├── westdex/
│ ├── 2024-01-01/
│ │ ├── request.log # 请求日志
│ │ ├── response.log # 响应日志
│ │ ├── error.log # 错误日志
│ │ └── performance.log # 性能日志
│ └── 2024-01-02/
│ ├── request.log
│ ├── response.log
│ ├── error.log
│ └── performance.log
```
### 3. **请求追踪**
- 每个请求都有唯一的请求ID
- 请求和响应日志通过请求ID关联
- 支持完整的请求链路追踪
### 4. **性能监控**
- 记录每个API调用的耗时
- 区分成功和失败的请求
- 提供性能分析数据
## 🏗️ 配置说明
### 配置文件设置
`config.yaml` 中添加西部数据日志配置:
```yaml
westdex:
url: "https://apimaster.westdex.com.cn/api/invoke"
key: "your-key"
secret_id: "your-secret-id"
secret_second_id: "your-secret-second-id"
# 西部数据日志配置
logging:
enabled: true # 启用日志记录
log_dir: "logs/westdex" # 日志目录
use_daily: true # 按日分包
enable_level_separation: true # 启用级别分离
# 各级别配置
level_configs:
request:
max_size: 100 # 100MB
max_backups: 5 # 5个备份
max_age: 30 # 30天
compress: true # 启用压缩
response:
max_size: 100
max_backups: 5
max_age: 30
compress: true
error:
max_size: 200 # 错误日志文件更大
max_backups: 10 # 更多备份
max_age: 90 # 保留更久
compress: true
performance:
max_size: 100
max_backups: 5
max_age: 30
compress: true
```
## 🚀 使用方法
### 1. 使用配置创建服务
```go
package main
import (
"tyapi-server/internal/config"
"tyapi-server/internal/infrastructure/external/westdex"
)
func main() {
// 加载配置
cfg := &config.Config{
// ... 配置内容
}
// 使用配置创建西部数据服务
service, err := westdex.NewWestDexServiceWithConfig(cfg)
if err != nil {
panic(err)
}
// 使用服务
resp, err := service.CallAPI("G05HZ01", map[string]interface{}{
"param1": "value1",
"param2": "value2",
})
if err != nil {
// 错误会自动记录到错误日志
log.Printf("API调用失败: %v", err)
return
}
// 成功响应会自动记录到响应日志
log.Printf("API调用成功: %s", string(resp))
}
```
### 2. 使用自定义日志配置
```go
// 创建自定义日志配置
loggingConfig := westdex.WestDexLoggingConfig{
Enabled: true,
LogDir: "logs/custom_westdex",
UseDaily: true,
EnableLevelSeparation: true,
LevelConfigs: map[string]westdex.WestDexLevelFileConfig{
"request": {
MaxSize: 50,
MaxBackups: 3,
MaxAge: 7,
Compress: true,
},
"error": {
MaxSize: 100,
MaxBackups: 5,
MaxAge: 30,
Compress: true,
},
},
}
// 使用自定义配置创建服务
service, err := westdex.NewWestDexServiceWithLogging(
"https://api.example.com",
"your-key",
"your-secret-id",
"your-secret-second-id",
loggingConfig,
)
```
## 📊 日志示例
### 请求日志示例
```json
{
"level": "info",
"msg": "西部数据API请求",
"request_id": "westdex_a1b2c3d4",
"api_code": "G05HZ01",
"url": "https://apimaster.westdex.com.cn/api/invoke/449159/G05HZ01?timestamp=1704096000000",
"request_data": {
"param1": "value1",
"param2": "value2"
},
"timestamp": "2024-01-01T12:00:00Z"
}
```
### 响应日志示例
```json
{
"level": "info",
"msg": "西部数据API响应",
"request_id": "westdex_a1b2c3d4",
"api_code": "G05HZ01",
"status_code": 200,
"response_data": "{\"code\":\"0000\",\"data\":\"...\",\"message\":\"success\"}",
"duration": "150ms",
"timestamp": "2024-01-01T12:00:01Z"
}
```
### 错误日志示例
```json
{
"level": "error",
"msg": "西部数据API错误",
"request_id": "westdex_a1b2c3d4",
"api_code": "G05HZ01",
"error": "数据源异常: 查询失败",
"request_data": {
"param1": "value1",
"param2": "value2"
},
"timestamp": "2024-01-01T12:00:01Z"
}
```
### 性能日志示例
```json
{
"level": "info",
"msg": "西部数据API性能",
"request_id": "westdex_a1b2c3d4",
"api_code": "G05HZ01",
"duration": "150ms",
"success": true,
"timestamp": "2024-01-01T12:00:01Z"
}
```
## 🔍 日志字段说明
### 通用字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `request_id` | string | 请求唯一标识符 |
| `api_code` | string | API代码 |
| `timestamp` | string | 时间戳ISO8601格式 |
### 请求日志字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `url` | string | 完整的请求URL |
| `request_data` | object | 请求数据 |
### 响应日志字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `status_code` | int | HTTP状态码 |
| `response_data` | string | 响应数据JSON字符串 |
| `duration` | duration | 请求耗时 |
### 错误日志字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `error` | error | 错误信息 |
| `request_data` | object | 原始请求数据 |
### 性能日志字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `duration` | duration | 请求耗时 |
| `success` | bool | 是否成功 |
## 🎯 最佳实践
### 1. **日志配置优化**
```yaml
# 生产环境配置
logging:
enabled: true
log_dir: "/var/log/westdex"
use_daily: true
enable_level_separation: true
level_configs:
request:
max_size: 200 # 更大的文件大小
max_backups: 10 # 更多备份
max_age: 90 # 保留更久
error:
max_size: 500 # 错误日志文件更大
max_backups: 20 # 更多备份
max_age: 180 # 保留更久
```
### 2. **日志分析**
```bash
# 查看请求日志
tail -f logs/westdex/2024-01-01/request.log
# 查看错误日志
tail -f logs/westdex/2024-01-01/error.log
# 查看性能日志
tail -f logs/westdex/2024-01-01/performance.log
# 统计API调用次数
grep -c "G05HZ01" logs/westdex/2024-01-01/request.log
# 统计错误率
grep -c "error" logs/westdex/2024-01-01/error.log
```
### 3. **性能监控**
```bash
# 查看平均响应时间
grep "duration" logs/westdex/2024-01-01/performance.log | \
awk -F'"duration":"' '{print $2}' | \
awk -F'"' '{print $1}' | \
awk -F'ms' '{sum+=$1; count++} END {print "平均响应时间: " sum/count "ms"}'
# 查看成功率
total=$(grep -c "success" logs/westdex/2024-01-01/performance.log)
success=$(grep -c '"success":true' logs/westdex/2024-01-01/performance.log)
echo "成功率: $((success * 100 / total))%"
```
## 🚨 注意事项
### 1. **日志文件管理**
- 定期清理旧日志文件
- 监控磁盘空间使用
- 配置合适的日志轮转策略
### 2. **敏感信息处理**
- 日志中可能包含敏感数据
- 确保日志文件访问权限
- 考虑日志脱敏需求
### 3. **性能影响**
- 日志记录会增加少量性能开销
- 异步日志记录可减少性能影响
- 合理配置日志级别
## 🔧 故障排除
### 1. **日志文件未创建**
- 检查日志目录权限
- 确认日志配置已启用
- 验证文件路径配置
### 2. **日志记录不完整**
- 检查日志器是否正确初始化
- 确认请求ID生成逻辑
- 验证错误处理流程
### 3. **性能问题**
- 检查日志文件大小和数量
- 确认日志轮转配置
- 监控磁盘I/O性能
## 🎉 总结
西部数据日志系统提供了完整的API调用追踪能力
1. **请求追踪**: 通过唯一请求ID关联请求和响应
2. **错误监控**: 记录所有错误和异常情况
3. **性能分析**: 提供详细的性能指标
4. **文件管理**: 支持按日期分包和级别分离
5. **配置灵活**: 支持自定义日志配置
通过合理使用这个日志系统,您可以:
- 快速定位API调用问题
- 监控系统性能和稳定性
- 分析API使用模式和趋势
- 提高问题排查效率
现在您的西部数据服务已经具备了完整的日志记录能力!🚀

View File

@@ -176,7 +176,11 @@ func (s *ApiApplicationServiceImpl) CallApi(ctx context.Context, cmd *commands.A
callContext := &processors.CallContext{
ContractCode: contractCode,
}
response, err := s.apiRequestService.PreprocessRequestApi(txCtx, cmd.ApiName, requestParams, &cmd.Options, callContext)
// 将transactionId放入ctx中供外部服务使用
ctxWithTransactionId := context.WithValue(txCtx, "transaction_id", transactionId)
response, err := s.apiRequestService.PreprocessRequestApi(ctxWithTransactionId, cmd.ApiName, requestParams, &cmd.Options, callContext)
if err != nil {
if errors.Is(err, processors.ErrDatasource) {
s.logger.Error("调用API失败", zap.Error(err))

View File

@@ -104,6 +104,29 @@ func (s *ProductApplicationServiceImpl) DeleteProduct(ctx context.Context, cmd *
func (s *ProductApplicationServiceImpl) ListProducts(ctx context.Context, filters map[string]interface{}, options interfaces.ListOptions) (*responses.ProductListResponse, error) {
// 检查是否有用户ID如果有则使用带订阅状态的方法
if userID, ok := filters["user_id"].(string); ok && userID != "" {
// 测试日志系统 - 主动记录错误信息
s.logger.Error("测试错误日志记录",
zap.String("method", "ListProducts"),
zap.String("error_type", "test_error"),
zap.String("error_message", "这是一个测试错误,用于验证日志系统"),
zap.Int("products_count", 0),
zap.Int64("total", 0),
zap.String("test_scenario", "主动错误记录测试"),
)
// 测试日志系统 - 记录一些信息
s.logger.Info("准备测试日志系统",
zap.String("method", "ListProducts"),
zap.Int("products_count", 0),
zap.Int64("total", 0),
)
// 测试日志系统 - 模拟空指针异常
s.logger.Warn("即将触发空指针异常进行测试")
// // 模拟空指针异常,用于测试
// var testPtr *int
// _ = *testPtr // 这里会触发空指针异常
return s.ListProductsWithSubscriptionStatus(ctx, filters, options)
}

View File

@@ -6,32 +6,33 @@ import (
// 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"`
AliPay AliPayConfig `mapstructure:"alipay"`
Yushan YushanConfig `mapstructure:"yushan"`
TianYanCha TianYanChaConfig `mapstructure:"tianyancha"`
Alicloud AlicloudConfig `mapstructure:"alicloud"`
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"`
AliPay AliPayConfig `mapstructure:"alipay"`
Yushan YushanConfig `mapstructure:"yushan"`
TianYanCha TianYanChaConfig `mapstructure:"tianyancha"`
Alicloud AlicloudConfig `mapstructure:"alicloud"`
}
// ServerConfig HTTP服务器配置
@@ -124,20 +125,20 @@ 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"` // 键过期时间
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"` // 最大并发请求数
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"` // 最大并发请求数
}
// MonitoringConfig 监控配置
@@ -210,14 +211,14 @@ type SMSRateLimit struct {
// 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"` // 控制台域名
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连接字符串
@@ -321,12 +322,60 @@ type AliPayRechargeBonusRule struct {
BonusAmount float64 `mapstructure:"bonus_amount"` // 赠送金额
}
// WestDexConfig WestDex配置
// WestDexConfig 西部数据配置
type WestDexConfig struct {
URL string `mapstructure:"url"`
Key string `mapstructure:"key"`
SecretId string `mapstructure:"secret_id"`
SecretSecondId string `mapstructure:"secret_second_id"`
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"`
}
// AliPayConfig 支付宝配置
@@ -344,6 +393,26 @@ 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 天眼查配置

View File

@@ -35,6 +35,7 @@ import (
"tyapi-server/internal/infrastructure/external/tianyancha"
"tyapi-server/internal/infrastructure/external/westdex"
"tyapi-server/internal/infrastructure/external/yushan"
"tyapi-server/internal/infrastructure/external/zhicha"
"tyapi-server/internal/infrastructure/http/handlers"
"tyapi-server/internal/infrastructure/http/routes"
shared_database "tyapi-server/internal/shared/database"
@@ -82,37 +83,44 @@ func NewContainer() *Container {
fx.Provide(
// 日志器 - 提供自定义Logger和*zap.Logger
func(cfg *config.Config) (logger.Logger, error) {
// 将 config.LoggerConfig 转换为 logger.Config
// 转换 LevelConfigs 类型
levelConfigs := make(map[string]interface{})
for key, value := range cfg.Logger.LevelConfigs {
levelConfigs[key] = value
}
logCfg := logger.Config{
Level: cfg.Logger.Level,
Format: cfg.Logger.Format,
Output: cfg.Logger.Output,
LogDir: cfg.Logger.LogDir,
MaxSize: cfg.Logger.MaxSize,
MaxBackups: cfg.Logger.MaxBackups,
MaxAge: cfg.Logger.MaxAge,
Compress: cfg.Logger.Compress,
UseDaily: cfg.Logger.UseDaily,
UseColor: cfg.Logger.UseColor,
EnableLevelSeparation: cfg.Logger.EnableLevelSeparation,
LevelConfigs: levelConfigs,
Development: cfg.App.Env == "development",
}
// 初始化全局日志器
if err := logger.InitGlobalLogger(logCfg); err != nil {
return nil, err
}
if cfg.Logger.EnableLevelSeparation {
// 使用按级别分文件的日志器
levelConfig := logger.LevelLoggerConfig{
BaseConfig: logger.Config{
Level: cfg.Logger.Level,
Format: cfg.Logger.Format,
Output: cfg.Logger.Output,
LogDir: cfg.Logger.LogDir,
MaxSize: cfg.Logger.MaxSize,
MaxBackups: cfg.Logger.MaxBackups,
MaxAge: cfg.Logger.MaxAge,
Compress: cfg.Logger.Compress,
UseDaily: cfg.Logger.UseDaily,
},
BaseConfig: logCfg,
EnableLevelSeparation: true,
LevelConfigs: convertLevelConfigs(cfg.Logger.LevelConfigs),
}
return logger.NewLevelLogger(levelConfig)
} else {
// 使用普通日志器
logCfg := logger.Config{
Level: cfg.Logger.Level,
Format: cfg.Logger.Format,
Output: cfg.Logger.Output,
LogDir: cfg.Logger.LogDir,
MaxSize: cfg.Logger.MaxSize,
MaxBackups: cfg.Logger.MaxBackups,
MaxAge: cfg.Logger.MaxAge,
Compress: cfg.Logger.Compress,
UseDaily: cfg.Logger.UseDaily,
}
return logger.NewLogger(logCfg)
}
},
@@ -130,9 +138,8 @@ func NewContainer() *Container {
return infoLogger
}
}
// 如果类型转换失败,创建一个默认的zap logger
defaultLogger, _ := zap.NewProduction()
return defaultLogger
// 如果类型转换失败,使用全局日志器
return logger.GetGlobalLogger()
},
),
@@ -304,19 +311,19 @@ func NewContainer() *Container {
sharedhttp.NewResponseBuilder,
validator.NewRequestValidator,
// WestDexService - 需要从配置中获取参数
func(cfg *config.Config) *westdex.WestDexService {
return westdex.NewWestDexService(
cfg.WestDex.URL,
cfg.WestDex.Key,
cfg.WestDex.SecretId,
cfg.WestDex.SecretSecondId,
)
func(cfg *config.Config) (*westdex.WestDexService, error) {
return westdex.NewWestDexServiceWithConfig(cfg)
},
// ZhichaService - 智查金控服务
func(cfg *config.Config) (*zhicha.ZhichaService, error) {
return zhicha.NewZhichaServiceWithConfig(cfg)
},
func(cfg *config.Config) *yushan.YushanService {
return yushan.NewYushanService(
cfg.Yushan.URL,
cfg.Yushan.APIKey,
cfg.Yushan.AcctID,
nil, // 暂时不传入logger使用无日志版本
)
},
// TianYanChaService - 天眼查服务
@@ -815,6 +822,7 @@ func convertLevelConfigs(configs map[string]config.LevelFileConfig) map[zapcore.
"panic": zapcore.PanicLevel,
}
// 只转换配置文件中存在的级别
for levelStr, config := range configs {
if level, exists := levelMap[levelStr]; exists {
result[level] = logger.LevelFileConfig{

View File

@@ -224,3 +224,30 @@ type COMENT01Req struct {
EntName string `json:"ent_name" validate:"required,min=1,validName"`
EntCode string `json:"ent_code" validate:"required,validUSCI"`
}
type JRZQ09J8Req struct {
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
IDCard string `json:"id_card" validate:"required,validIDCard"`
Name string `json:"name" validate:"required,min=1,validName"`
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
}
type FLXGDEA8Req struct {
IDCard string `json:"id_card" validate:"required,validIDCard"`
Name string `json:"name" validate:"required,min=1,validName"`
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
}
type FLXGDEA9Req struct {
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
IDCard string `json:"id_card" validate:"required,validIDCard"`
Name string `json:"name" validate:"required,min=1,validName"`
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
}
type JRZQ1D09Req struct {
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
IDCard string `json:"id_card" validate:"required,min=1,validIDCard"`
Name string `json:"name" validate:"required,min=1,validName"`
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
}

View File

@@ -18,6 +18,7 @@ import (
"tyapi-server/internal/infrastructure/external/tianyancha"
"tyapi-server/internal/infrastructure/external/westdex"
"tyapi-server/internal/infrastructure/external/yushan"
"tyapi-server/internal/infrastructure/external/zhicha"
"tyapi-server/internal/shared/interfaces"
)
@@ -44,6 +45,7 @@ func NewApiRequestService(
yushanService *yushan.YushanService,
tianYanChaService *tianyancha.TianYanChaService,
alicloudService *alicloud.AlicloudService,
zhichaService *zhicha.ZhichaService,
validator interfaces.RequestValidator,
productManagementService *services.ProductManagementService,
) *ApiRequestService {
@@ -51,7 +53,7 @@ func NewApiRequestService(
combService := comb.NewCombService(productManagementService)
// 创建处理器依赖容器
processorDeps := processors.NewProcessorDependencies(westDexService, yushanService, tianYanChaService, alicloudService, validator, combService)
processorDeps := processors.NewProcessorDependencies(westDexService, yushanService, tianYanChaService, alicloudService, zhichaService, validator, combService)
// 统一注册所有处理器
registerAllProcessors(combService)
@@ -88,12 +90,16 @@ func registerAllProcessors(combService *comb.CombService) {
"FLXG5B2E": flxg.ProcessFLXG5B2ERequest,
"FLXG0687": flxg.ProcessFLXG0687Request,
"FLXGBC21": flxg.ProcessFLXGBC21Request,
"FLXGDEA8": flxg.ProcessFLXGDEA8Request,
"FLXGDEA9": flxg.ProcessFLXGDEA9Request,
// JRZQ系列处理器
"JRZQ8203": jrzq.ProcessJRZQ8203Request,
"JRZQ0A03": jrzq.ProcessJRZQ0A03Request,
"JRZQ4AA8": jrzq.ProcessJRZQ4AA8Request,
"JRZQDCBE": jrzq.ProcessJRZQDCBERequest,
"JRZQ09J8": jrzq.ProcessJRZQ09J8Request,
"JRZQ1D09": jrzq.ProcessJRZQ1D09Request,
// QYGL系列处理器
"QYGL8261": qygl.ProcessQYGL8261Request,

View File

@@ -7,6 +7,7 @@ import (
"tyapi-server/internal/infrastructure/external/tianyancha"
"tyapi-server/internal/infrastructure/external/westdex"
"tyapi-server/internal/infrastructure/external/yushan"
"tyapi-server/internal/infrastructure/external/zhicha"
"tyapi-server/internal/shared/interfaces"
)
@@ -26,6 +27,7 @@ type ProcessorDependencies struct {
YushanService *yushan.YushanService
TianYanChaService *tianyancha.TianYanChaService
AlicloudService *alicloud.AlicloudService
ZhichaService *zhicha.ZhichaService
Validator interfaces.RequestValidator
CombService CombServiceInterface // Changed to interface to break import cycle
Options *commands.ApiCallOptions // 添加Options支持
@@ -38,6 +40,7 @@ func NewProcessorDependencies(
yushanService *yushan.YushanService,
tianYanChaService *tianyancha.TianYanChaService,
alicloudService *alicloud.AlicloudService,
zhichaService *zhicha.ZhichaService,
validator interfaces.RequestValidator,
combService CombServiceInterface, // Changed to interface
) *ProcessorDependencies {
@@ -46,6 +49,7 @@ func NewProcessorDependencies(
YushanService: yushanService,
TianYanChaService: tianYanChaService,
AlicloudService: alicloudService,
ZhichaService: zhichaService,
Validator: validator,
CombService: combService,
Options: nil, // 初始化为nil在调用时设置

View File

@@ -28,7 +28,7 @@ func ProcessFLXG0687Request(ctx context.Context, params []byte, deps *processors
"type": 3,
}
respBytes, err := deps.YushanService.CallAPI("RIS031", reqData)
respBytes, err := deps.YushanService.CallAPI(ctx, "RIS031", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)

View File

@@ -39,7 +39,7 @@ func ProcessFLXG0V3Bequest(ctx context.Context, params []byte, deps *processors.
},
}
respBytes, err := deps.WestDexService.CallAPI("G34BJ03", reqData)
respBytes, err := deps.WestDexService.CallAPI(ctx, "G34BJ03", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)

View File

@@ -50,7 +50,7 @@ func ProcessFLXG0V4BRequest(ctx context.Context, params []byte, deps *processors
},
}
log.Println("reqData", reqData)
respBytes, err := deps.WestDexService.CallAPI("G22SC01", reqData)
respBytes, err := deps.WestDexService.CallAPI(ctx, "G22SC01", reqData)
if err != nil {
// 数据源错误
if errors.Is(err, westdex.ErrDatasource) {

View File

@@ -45,7 +45,7 @@ func ProcessFLXG162ARequest(ctx context.Context, params []byte, deps *processors
},
}
respBytes, err := deps.WestDexService.CallAPI("G32BJ05", reqData)
respBytes, err := deps.WestDexService.CallAPI(ctx, "G32BJ05", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)

View File

@@ -54,7 +54,7 @@ func ProcessFLXG3D56Request(ctx context.Context, params []byte, deps *processors
reqData["data"].(map[string]interface{})["time_range"] = encryptedTimeRange
}
respBytes, err := deps.WestDexService.CallAPI("G26BJ05", reqData)
respBytes, err := deps.WestDexService.CallAPI(ctx, "G26BJ05", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)

View File

@@ -33,7 +33,7 @@ func ProcessFLXG54F5Request(ctx context.Context, params []byte, deps *processors
},
}
respBytes, err := deps.WestDexService.CallAPI("G03HZ01", reqData)
respBytes, err := deps.WestDexService.CallAPI(ctx,"G03HZ01", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)

View File

@@ -33,7 +33,7 @@ func ProcessFLXG5876Request(ctx context.Context, params []byte, deps *processors
},
}
respBytes, err := deps.WestDexService.CallAPI("G03XM02", reqData)
respBytes, err := deps.WestDexService.CallAPI(ctx, "G03XM02", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)

View File

@@ -48,7 +48,7 @@ func ProcessFLXG5B2ERequest(ctx context.Context, params []byte, deps *processors
},
}
respBytes, err := deps.WestDexService.CallAPI("G36SC01", reqData)
respBytes, err := deps.WestDexService.CallAPI(ctx, "G36SC01", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
// 如果有返回内容,优先解析返回内容

View File

@@ -28,7 +28,7 @@ func ProcessFLXG75FERequest(ctx context.Context, params []byte, deps *processors
"mobile": paramsDto.MobileNo,
}
respBytes, err := deps.WestDexService.CallAPI("FLXG75FE", reqData)
respBytes, err := deps.WestDexService.CallAPI(ctx,"FLXG75FE", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)

View File

@@ -48,7 +48,7 @@ func ProcessFLXG8A3FRequest(ctx context.Context, params []byte, deps *processors
},
}
respBytes, err := deps.WestDexService.CallAPI("G37SC01", reqData)
respBytes, err := deps.WestDexService.CallAPI(ctx, "G37SC01", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
// 如果有返回内容,优先解析返回内容

View File

@@ -45,7 +45,7 @@ func ProcessFLXG9687Request(ctx context.Context, params []byte, deps *processors
},
}
respBytes, err := deps.WestDexService.CallAPI("G31BJ05", reqData)
respBytes, err := deps.WestDexService.CallAPI(ctx, "G31BJ05", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)

View File

@@ -39,7 +39,7 @@ func ProcessFLXG970FRequest(ctx context.Context, params []byte, deps *processors
},
}
respBytes, err := deps.WestDexService.CallAPI("WEST00028", reqData)
respBytes, err := deps.WestDexService.CallAPI(ctx, "WEST00028", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)

View File

@@ -27,7 +27,7 @@ func ProcessFLXGBC21Request(ctx context.Context, params []byte, deps *processors
"mobile": paramsDto.MobileNo,
}
respBytes, err := deps.YushanService.CallAPI("MOB032", reqData)
respBytes, err := deps.YushanService.CallAPI(ctx, "MOB032", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)

View File

@@ -45,7 +45,7 @@ func ProcessFLXGC9D1Request(ctx context.Context, params []byte, deps *processors
},
}
respBytes, err := deps.WestDexService.CallAPI("G30BJ05", reqData)
respBytes, err := deps.WestDexService.CallAPI(ctx, "G30BJ05", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)

View File

@@ -39,7 +39,7 @@ func ProcessFLXGCA3DRequest(ctx context.Context, params []byte, deps *processors
},
}
respBytes, err := deps.WestDexService.CallAPI("G22BJ03", reqData)
respBytes, err := deps.WestDexService.CallAPI(ctx, "G22BJ03", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
if respBytes != nil {

View File

@@ -0,0 +1,57 @@
package flxg
import (
"context"
"encoding/json"
"errors"
"fmt"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/zhicha"
)
// ProcessFLXGDEA8Request FLXGDEA8 API处理方法
func ProcessFLXGDEA8Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.FLXGDEA8Req
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrInvalidParam, err)
}
encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name)
if err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
}
encryptedIDCard, err := deps.ZhichaService.Encrypt(paramsDto.IDCard)
if err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
}
reqData := map[string]interface{}{
"name": encryptedName,
"idCard": encryptedIDCard,
"authorized": paramsDto.Authorized,
}
respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI028", reqData)
if err != nil {
if errors.Is(err, zhicha.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)
} else {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
}
}
// 将响应数据转换为JSON字节
respBytes, err := json.Marshal(respData)
if err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
}
return respBytes, nil
}

View File

@@ -0,0 +1,56 @@
package flxg
import (
"context"
"encoding/json"
"errors"
"fmt"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/zhicha"
)
// ProcessFLXGDEA9Request FLXGDEA9 API处理方法
func ProcessFLXGDEA9Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.FLXGDEA9Req
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrInvalidParam, err)
}
encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name)
if err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
}
encryptedIDCard, err := deps.ZhichaService.Encrypt(paramsDto.IDCard)
if err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
}
reqData := map[string]interface{}{
"name": encryptedName,
"idCard": encryptedIDCard,
"authorized": paramsDto.Authorized,
}
respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI005", reqData)
if err != nil {
if errors.Is(err, zhicha.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)
} else {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
}
}
// 将响应数据转换为JSON字节
respBytes, err := json.Marshal(respData)
if err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
}
return respBytes, nil
}

View File

@@ -39,7 +39,7 @@ func ProcessFLXGDEC7Request(ctx context.Context, params []byte, deps *processors
},
}
respBytes, err := deps.WestDexService.CallAPI("G23BJ03", reqData)
respBytes, err := deps.WestDexService.CallAPI(ctx, "G23BJ03", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)

View File

@@ -39,7 +39,7 @@ func ProcessIVYZ0B03Request(ctx context.Context, params []byte, deps *processors
},
}
respBytes, err := deps.WestDexService.CallAPI("G17BJ02", reqData)
respBytes, err := deps.WestDexService.CallAPI(ctx, "G17BJ02", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)

View File

@@ -40,7 +40,7 @@ func ProcessIVYZ1C9DRequest(ctx context.Context, params []byte, deps *processors
},
}
respBytes, err := deps.WestDexService.CallAPI("G38SC02", reqData)
respBytes, err := deps.WestDexService.CallAPI(ctx, "G38SC02", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)

View File

@@ -25,7 +25,7 @@ func ProcessIVYZ2125Request(ctx context.Context, params []byte, deps *processors
// "mobile": paramsDto.Mobile,
// }
// respBytes, err := deps.WestDexService.CallAPI("IVYZ2125", reqData)
// respBytes, err := deps.WestDexService.CallAPI(ctx, "IVYZ2125", reqData)
// if err != nil {
// if errors.Is(err, westdex.ErrDatasource) {
// return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)

View File

@@ -37,7 +37,7 @@ func ProcessIVYZ385ERequest(ctx context.Context, params []byte, deps *processors
"gmsfzhm": encryptedIDCard,
}
respBytes, err := deps.WestDexService.CallAPI("WEST00020", reqData)
respBytes, err := deps.WestDexService.CallAPI(ctx, "WEST00020", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)

View File

@@ -39,7 +39,7 @@ func ProcessIVYZ4E8BRequest(ctx context.Context, params []byte, deps *processors
},
}
respBytes, err := deps.WestDexService.CallAPI("G09GZ02", reqData)
respBytes, err := deps.WestDexService.CallAPI(ctx, "G09GZ02", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)

View File

@@ -39,7 +39,7 @@ func ProcessIVYZ5733Request(ctx context.Context, params []byte, deps *processors
},
}
respBytes, err := deps.WestDexService.CallAPI("G09XM02", reqData)
respBytes, err := deps.WestDexService.CallAPI(ctx, "G09XM02", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)

View File

@@ -51,7 +51,7 @@ func ProcessIVYZ7F2ARequest(ctx context.Context, params []byte, deps *processors
},
}
respBytes, err := deps.WestDexService.CallAPI("G10GZ02", reqData)
respBytes, err := deps.WestDexService.CallAPI(ctx, "G10GZ02", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)

View File

@@ -51,7 +51,7 @@ func ProcessIVYZ9363Request(ctx context.Context, params []byte, deps *processors
},
}
respBytes, err := deps.WestDexService.CallAPI("G10XM02", reqData)
respBytes, err := deps.WestDexService.CallAPI(ctx, "G10XM02", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)

View File

@@ -39,7 +39,7 @@ func ProcessIVYZ9A2BRequest(ctx context.Context, params []byte, deps *processors
},
}
respBytes, err := deps.WestDexService.CallAPI("G11BJ06", reqData)
respBytes, err := deps.WestDexService.CallAPI(ctx, "G11BJ06", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)

View File

@@ -25,7 +25,7 @@ func ProcessIVYZADEERequest(ctx context.Context, params []byte, deps *processors
// "mobile": paramsDto.Mobile,
// }
// respBytes, err := deps.WestDexService.CallAPI("IVYZADEE", reqData)
// respBytes, err := deps.WestDexService.CallAPI(ctx, "IVYZADEE", reqData)
// if err != nil {
// if errors.Is(err, westdex.ErrDatasource) {
// return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)

View File

@@ -39,7 +39,7 @@ func ProcessIVYZGZ08Request(ctx context.Context, params []byte, deps *processors
},
}
respBytes, err := deps.WestDexService.CallAPI("G08SC02", reqData)
respBytes, err := deps.WestDexService.CallAPI(ctx, "G08SC02", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)

View File

@@ -0,0 +1,63 @@
package jrzq
import (
"context"
"encoding/json"
"errors"
"fmt"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/zhicha"
)
// ProcessJRZQ09J8Request JRZQ09J8 API处理方法
func ProcessJRZQ09J8Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.JRZQ09J8Req
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrInvalidParam, err)
}
encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name)
if err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
}
encryptedIDCard, err := deps.ZhichaService.Encrypt(paramsDto.IDCard)
if err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
}
encryptedMobileNo, err := deps.ZhichaService.Encrypt(paramsDto.MobileNo)
if err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
}
reqData := map[string]interface{}{
"name": encryptedName,
"idCard": encryptedIDCard,
"phone": encryptedMobileNo,
"authorized": paramsDto.Authorized,
}
respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI031", reqData)
if err != nil {
if errors.Is(err, zhicha.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)
} else {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
}
}
// 将响应数据转换为JSON字节
respBytes, err := json.Marshal(respData)
if err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
}
return respBytes, nil
}

View File

@@ -45,7 +45,7 @@ func ProcessJRZQ0A03Request(ctx context.Context, params []byte, deps *processors
},
}
respBytes, err := deps.WestDexService.CallAPI("G27BJ05", reqData)
respBytes, err := deps.WestDexService.CallAPI(ctx, "G27BJ05", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)

View File

@@ -0,0 +1,63 @@
package jrzq
import (
"context"
"encoding/json"
"errors"
"fmt"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/zhicha"
)
// ProcessJRZQ1D09Request JRZQ1D09 API处理方法
func ProcessJRZQ1D09Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.JRZQ1D09Req
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrInvalidParam, err)
}
encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name)
if err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
}
encryptedIDCard, err := deps.ZhichaService.Encrypt(paramsDto.IDCard)
if err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
}
encryptedMobileNo, err := deps.ZhichaService.Encrypt(paramsDto.MobileNo)
if err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
}
reqData := map[string]interface{}{
"name": encryptedName,
"idCard": encryptedIDCard,
"phone": encryptedMobileNo,
"authorized": paramsDto.Authorized,
}
respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI020", reqData)
if err != nil {
if errors.Is(err, zhicha.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)
} else {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
}
}
// 将响应数据转换为JSON字节
respBytes, err := json.Marshal(respData)
if err != nil {
return nil, fmt.Errorf("%s: %w", processors.ErrSystem, err)
}
return respBytes, nil
}

View File

@@ -45,7 +45,7 @@ func ProcessJRZQ4AA8Request(ctx context.Context, params []byte, deps *processors
},
}
respBytes, err := deps.WestDexService.CallAPI("G29BJ05", reqData)
respBytes, err := deps.WestDexService.CallAPI(ctx, "G29BJ05", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)

View File

@@ -45,7 +45,7 @@ func ProcessJRZQ8203Request(ctx context.Context, params []byte, deps *processors
},
}
respBytes, err := deps.WestDexService.CallAPI("G28BJ05", reqData)
respBytes, err := deps.WestDexService.CallAPI(ctx, "G28BJ05", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)

View File

@@ -51,7 +51,7 @@ func ProcessJRZQDCBERequest(ctx context.Context, params []byte, deps *processors
},
}
respBytes, err := deps.WestDexService.CallAPI("G20GZ01", reqData)
respBytes, err := deps.WestDexService.CallAPI(ctx, "G20GZ01", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)

View File

@@ -26,7 +26,7 @@ func ProcessQCXG7A2BRequest(ctx context.Context, params []byte, deps *processors
"cardNo": paramsDto.IDCard,
}
respBytes, err := deps.YushanService.CallAPI("CAR061", reqData)
respBytes, err := deps.YushanService.CallAPI(ctx, "CAR061", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)

View File

@@ -45,7 +45,7 @@ func ProcessQYGL2ACDRequest(ctx context.Context, params []byte, deps *processors
},
}
respBytes, err := deps.WestDexService.CallAPI("WEST00022", reqData)
respBytes, err := deps.WestDexService.CallAPI(ctx, "WEST00022", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)

View File

@@ -51,7 +51,7 @@ func ProcessQYGL45BDRequest(ctx context.Context, params []byte, deps *processors
},
}
respBytes, err := deps.WestDexService.CallAPI("WEST00021", reqData)
respBytes, err := deps.WestDexService.CallAPI(ctx, "WEST00021", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
if respBytes != nil {

View File

@@ -33,7 +33,7 @@ func ProcessQYGL6F2DRequest(ctx context.Context, params []byte, deps *processors
},
}
respBytes, err := deps.WestDexService.CallAPI("G05XM02", reqData)
respBytes, err := deps.WestDexService.CallAPI(ctx, "G05XM02", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)

View File

@@ -33,7 +33,7 @@ func ProcessQYGL8261Request(ctx context.Context, params []byte, deps *processors
},
}
respBytes, err := deps.WestDexService.CallAPI("Q03BJ03", reqData)
respBytes, err := deps.WestDexService.CallAPI(ctx, "Q03BJ03", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)

View File

@@ -49,7 +49,7 @@ func ProcessQYGL8271Request(ctx context.Context, params []byte, deps *processors
},
}
respBytes, err := deps.WestDexService.CallAPI("Q03SC01", reqData)
respBytes, err := deps.WestDexService.CallAPI(ctx, "Q03SC01", reqData)
if err != nil {
// 数据源错误
if errors.Is(err, westdex.ErrDatasource) {

View File

@@ -30,7 +30,7 @@ func ProcessQYGLB4C0Request(ctx context.Context, params []byte, deps *processors
"pid": encryptedIDCard,
}
respBytes, err := deps.WestDexService.G05HZ01CallAPI("G05HZ01", reqData)
respBytes, err := deps.WestDexService.G05HZ01CallAPI(ctx, "G05HZ01", reqData)
if err != nil {
// 数据源错误
if errors.Is(err, westdex.ErrDatasource) {

View File

@@ -46,7 +46,7 @@ func ProcessYYSY09CDRequest(ctx context.Context, params []byte, deps *processors
},
}
respBytes, err := deps.WestDexService.CallAPI("G16BJ02", reqData)
respBytes, err := deps.WestDexService.CallAPI(ctx, "G16BJ02", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)

View File

@@ -33,7 +33,7 @@ func ProcessYYSY4B21Request(ctx context.Context, params []byte, deps *processors
},
}
respBytes, err := deps.WestDexService.CallAPI("G25BJ02", reqData)
respBytes, err := deps.WestDexService.CallAPI(ctx, "G25BJ02", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)

View File

@@ -33,7 +33,7 @@ func ProcessYYSY4B37Request(ctx context.Context, params []byte, deps *processors
},
}
respBytes, err := deps.WestDexService.CallAPI("G02BJ02", reqData)
respBytes, err := deps.WestDexService.CallAPI(ctx, "G02BJ02", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)

View File

@@ -46,7 +46,7 @@ func ProcessYYSY6F2ERequest(ctx context.Context, params []byte, deps *processors
},
}
respBytes, err := deps.WestDexService.CallAPI("G15BJ02", reqData)
respBytes, err := deps.WestDexService.CallAPI(ctx, "G15BJ02", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)

View File

@@ -39,7 +39,7 @@ func ProcessYYSYD50FRequest(ctx context.Context, params []byte, deps *processors
},
}
respBytes, err := deps.WestDexService.CallAPI("G18BJ02", reqData)
respBytes, err := deps.WestDexService.CallAPI(ctx, "G18BJ02", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)

View File

@@ -34,7 +34,7 @@ func ProcessYYSYF7DBRequest(ctx context.Context, params []byte, deps *processors
},
}
respBytes, err := deps.WestDexService.CallAPI("G19BJ02", reqData)
respBytes, err := deps.WestDexService.CallAPI(ctx, "G19BJ02", reqData)
if err != nil {
if errors.Is(err, westdex.ErrDatasource) {
return nil, fmt.Errorf("%s: %w", processors.ErrDatasource, err)

View File

@@ -0,0 +1,63 @@
package westdex
import (
"tyapi-server/internal/config"
"tyapi-server/internal/shared/external_logger"
)
// NewWestDexServiceWithConfig 使用配置创建西部数据服务
func NewWestDexServiceWithConfig(cfg *config.Config) (*WestDexService, error) {
// 将配置类型转换为通用外部服务日志配置
loggingConfig := external_logger.ExternalServiceLoggingConfig{
Enabled: cfg.WestDex.Logging.Enabled,
LogDir: cfg.WestDex.Logging.LogDir,
ServiceName: "westdex",
UseDaily: cfg.WestDex.Logging.UseDaily,
EnableLevelSeparation: cfg.WestDex.Logging.EnableLevelSeparation,
LevelConfigs: make(map[string]external_logger.ExternalServiceLevelFileConfig),
}
// 转换级别配置
for key, value := range cfg.WestDex.Logging.LevelConfigs {
loggingConfig.LevelConfigs[key] = external_logger.ExternalServiceLevelFileConfig{
MaxSize: value.MaxSize,
MaxBackups: value.MaxBackups,
MaxAge: value.MaxAge,
Compress: value.Compress,
}
}
// 创建通用外部服务日志器
logger, err := external_logger.NewExternalServiceLogger(loggingConfig)
if err != nil {
return nil, err
}
// 创建西部数据服务
service := NewWestDexService(
cfg.WestDex.URL,
cfg.WestDex.Key,
cfg.WestDex.SecretID,
cfg.WestDex.SecretSecondID,
logger,
)
return service, nil
}
// NewWestDexServiceWithLogging 使用自定义日志配置创建西部数据服务
func NewWestDexServiceWithLogging(url, key, secretID, secretSecondID string, loggingConfig external_logger.ExternalServiceLoggingConfig) (*WestDexService, error) {
// 设置服务名称
loggingConfig.ServiceName = "westdex"
// 创建通用外部服务日志器
logger, err := external_logger.NewExternalServiceLogger(loggingConfig)
if err != nil {
return nil, err
}
// 创建西部数据服务
service := NewWestDexService(url, key, secretID, secretSecondID, logger)
return service, nil
}

View File

@@ -2,15 +2,18 @@ package westdex
import (
"bytes"
"context"
"crypto/md5"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"strconv"
"time"
"tyapi-server/internal/shared/crypto"
"tyapi-server/internal/shared/external_logger"
)
var (
@@ -26,6 +29,7 @@ type WestResp struct {
ErrorCode *int `json:"error_code"`
Reason string `json:"reason"`
}
type G05HZ01WestResp struct {
Message string `json:"message"`
Code string `json:"code"`
@@ -38,43 +42,91 @@ type G05HZ01WestResp struct {
type WestConfig struct {
Url string
Key string
SecretId string
SecretSecondId string
SecretID string
SecretSecondID string
}
type WestDexService struct {
config WestConfig
logger *external_logger.ExternalServiceLogger
}
// NewWestDexService 是一个构造函数,用于初始化 WestDexService
func NewWestDexService(url, key, secretId, secretSecondId string) *WestDexService {
func NewWestDexService(url, key, secretID, secretSecondID string, logger *external_logger.ExternalServiceLogger) *WestDexService {
return &WestDexService{
config: WestConfig{
Url: url,
Key: key,
SecretId: secretId,
SecretSecondId: secretSecondId,
SecretID: secretID,
SecretSecondID: secretSecondID,
},
logger: logger,
}
}
// CallAPI 调用西部数据的 API
func (w *WestDexService) CallAPI(code string, reqData map[string]interface{}) (resp []byte, err error) {
// 生成当前的13位时间戳
timestamp := strconv.FormatInt(time.Now().UnixNano()/int64(time.Millisecond), 10)
// generateRequestID 生成请求ID
func (w *WestDexService) generateRequestID() string {
timestamp := time.Now().UnixNano()
hash := md5.Sum([]byte(fmt.Sprintf("%d_%s", timestamp, w.config.Key)))
return fmt.Sprintf("westdex_%x", hash[:8])
}
// 构造请求URL
reqUrl := fmt.Sprintf("%s/%s/%s?timestamp=%s", w.config.Url, w.config.SecretId, code, timestamp)
// buildLogData 构建包含transactionId的日志数据
func (w *WestDexService) buildLogData(data map[string]interface{}, transactionID string) map[string]interface{} {
if transactionID == "" {
return data
}
logData := data
if logData == nil {
logData = make(map[string]interface{})
}
logData["transaction_id"] = transactionID
return logData
}
// buildRequestURL 构建请求URL
func (w *WestDexService) buildRequestURL(code string) string {
timestamp := strconv.FormatInt(time.Now().UnixNano()/int64(time.Millisecond), 10)
return fmt.Sprintf("%s/%s/%s?timestamp=%s", w.config.Url, w.config.SecretID, code, timestamp)
}
// CallAPI 调用西部数据的 API
func (w *WestDexService) CallAPI(ctx context.Context, code string, reqData map[string]interface{}) (resp []byte, err error) {
startTime := time.Now()
requestID := w.generateRequestID()
// 从ctx中获取transactionId
var transactionID string
if ctxTransactionID, ok := ctx.Value("transaction_id").(string); ok {
transactionID = ctxTransactionID
}
// 构建请求URL
reqUrl := w.buildRequestURL(code)
// 记录请求日志
if w.logger != nil {
w.logger.LogRequest(requestID, code, reqUrl, w.buildLogData(reqData, transactionID))
}
jsonData, marshalErr := json.Marshal(reqData)
if marshalErr != nil {
return nil, fmt.Errorf("%w: %s", ErrSystem, marshalErr.Error())
err = fmt.Errorf("%w: %s", ErrSystem, marshalErr.Error())
if w.logger != nil {
w.logger.LogError(requestID, code, err, w.buildLogData(reqData, transactionID))
}
return nil, err
}
// 创建HTTP POST请求
req, newRequestErr := http.NewRequest("POST", reqUrl, bytes.NewBuffer(jsonData))
req, newRequestErr := http.NewRequestWithContext(ctx, "POST", reqUrl, bytes.NewBuffer(jsonData))
if newRequestErr != nil {
return nil, fmt.Errorf("%w: %s", ErrSystem, newRequestErr.Error())
err = fmt.Errorf("%w: %s", ErrSystem, newRequestErr.Error())
if w.logger != nil {
w.logger.LogError(requestID, code, err, w.buildLogData(reqData, transactionID))
}
return nil, err
}
// 设置请求头
@@ -84,70 +136,150 @@ func (w *WestDexService) CallAPI(code string, reqData map[string]interface{}) (r
client := &http.Client{}
httpResp, clientDoErr := client.Do(req)
if clientDoErr != nil {
return nil, fmt.Errorf("%w: %s", ErrSystem, clientDoErr.Error())
err = fmt.Errorf("%w: %s", ErrSystem, clientDoErr.Error())
if w.logger != nil {
w.logger.LogError(requestID, code, err, w.buildLogData(reqData, transactionID))
}
return nil, err
}
defer func(Body io.ReadCloser) {
closeErr := Body.Close()
if closeErr != nil {
// 记录关闭错误
if w.logger != nil {
w.logger.LogError(requestID, code, fmt.Errorf("关闭响应体失败: %w", closeErr), w.buildLogData(reqData, transactionID))
}
}
}(httpResp.Body)
// 计算请求耗时
duration := time.Since(startTime)
// 检查请求是否成功
if httpResp.StatusCode == 200 {
// 读取响应体
bodyBytes, ReadErr := io.ReadAll(httpResp.Body)
if ReadErr != nil {
return nil, fmt.Errorf("%w: %s", ErrSystem, ReadErr.Error())
err = fmt.Errorf("%w: %s", ErrSystem, ReadErr.Error())
if w.logger != nil {
w.logger.LogError(requestID, code, err, w.buildLogData(reqData, transactionID))
}
return nil, err
}
// 记录响应日志
if w.logger != nil {
w.logger.LogResponse(requestID, code, httpResp.StatusCode, bodyBytes, duration)
}
// 手动调用 json.Unmarshal 触发自定义的 UnmarshalJSON 方法
var westDexResp WestResp
log.Println("westDexResp.ID", westDexResp.ID)
UnmarshalErr := json.Unmarshal(bodyBytes, &westDexResp)
if UnmarshalErr != nil {
return nil, UnmarshalErr
err = UnmarshalErr
if w.logger != nil {
w.logger.LogError(requestID, code, err, w.buildLogData(reqData, transactionID))
}
return nil, err
}
if westDexResp.Code != "00000" && westDexResp.Code != "200" && westDexResp.Code != "0" {
if westDexResp.Data == "" {
return nil, fmt.Errorf("%w: %s", ErrSystem, westDexResp.Message)
err = fmt.Errorf("%w: %s", ErrSystem, westDexResp.Message)
if w.logger != nil {
w.logger.LogError(requestID, code, err, w.buildLogData(reqData, transactionID))
}
return nil, err
}
decryptedData, DecryptErr := crypto.WestDexDecrypt(westDexResp.Data, w.config.Key)
if DecryptErr != nil {
return nil, fmt.Errorf("%w: %s", ErrSystem, DecryptErr.Error())
err = fmt.Errorf("%w: %s", ErrSystem, DecryptErr.Error())
if w.logger != nil {
w.logger.LogError(requestID, code, err, w.buildLogData(reqData, transactionID))
}
return nil, err
}
// 记录业务错误日志
if w.logger != nil {
w.logger.LogError(requestID, code, fmt.Errorf("%w: %s", ErrDatasource, westDexResp.Message), w.buildLogData(reqData, transactionID))
}
// 记录性能日志(失败)
// 注意:通用日志系统不包含性能日志功能
return decryptedData, fmt.Errorf("%w: %s", ErrDatasource, westDexResp.Message)
}
if westDexResp.Data == "" {
return nil, fmt.Errorf("%w: %s", ErrSystem, westDexResp.Message)
err = fmt.Errorf("%w: %s", ErrSystem, westDexResp.Message)
if w.logger != nil {
w.logger.LogError(requestID, code, err, w.buildLogData(reqData, transactionID))
}
return nil, err
}
decryptedData, DecryptErr := crypto.WestDexDecrypt(westDexResp.Data, w.config.Key)
if DecryptErr != nil {
return nil, fmt.Errorf("%w: %s", ErrSystem, DecryptErr.Error())
err = fmt.Errorf("%w: %s", ErrSystem, DecryptErr.Error())
if w.logger != nil {
w.logger.LogError(requestID, code, err, w.buildLogData(reqData, transactionID))
}
return nil, err
}
// 记录性能日志(成功)
// 注意:通用日志系统不包含性能日志功能
return decryptedData, nil
}
return nil, fmt.Errorf("%w: 西部请求失败Code: %d", ErrSystem, httpResp.StatusCode)
// 记录HTTP错误
err = fmt.Errorf("%w: 西部请求失败Code: %d", ErrSystem, httpResp.StatusCode)
if w.logger != nil {
w.logger.LogError(requestID, code, err, w.buildLogData(reqData, transactionID))
// 注意:通用日志系统不包含性能日志功能
}
return nil, err
}
// G05HZ01CallAPI 调用西部数据的 G05HZ01 API
func (w *WestDexService) G05HZ01CallAPI(code string, reqData map[string]interface{}) (resp []byte, err error) {
// 生成当前的13位时间戳
timestamp := strconv.FormatInt(time.Now().UnixNano()/int64(time.Millisecond), 10)
func (w *WestDexService) G05HZ01CallAPI(ctx context.Context, code string, reqData map[string]interface{}) (resp []byte, err error) {
startTime := time.Now()
requestID := w.generateRequestID()
// 构造请求URL
reqUrl := fmt.Sprintf("%s/%s/%s?timestamp=%s", w.config.Url, w.config.SecretSecondId, code, timestamp)
// 从ctx中获取transactionId
var transactionID string
if ctxTransactionID, ok := ctx.Value("transaction_id").(string); ok {
transactionID = ctxTransactionID
}
// 构建请求URL
reqUrl := fmt.Sprintf("%s/%s/%s?timestamp=%d", w.config.Url, w.config.SecretSecondID, code, time.Now().UnixNano()/int64(time.Millisecond))
// 记录请求日志
if w.logger != nil {
w.logger.LogRequest(requestID, code, reqUrl, w.buildLogData(reqData, transactionID))
}
jsonData, marshalErr := json.Marshal(reqData)
if marshalErr != nil {
return nil, fmt.Errorf("%w: %s", ErrSystem, marshalErr.Error())
err = fmt.Errorf("%w: %s", ErrSystem, marshalErr.Error())
if w.logger != nil {
w.logger.LogError(requestID, code, err, w.buildLogData(reqData, transactionID))
}
return nil, err
}
// 创建HTTP POST请求
req, newRequestErr := http.NewRequest("POST", reqUrl, bytes.NewBuffer(jsonData))
req, newRequestErr := http.NewRequestWithContext(ctx, "POST", reqUrl, bytes.NewBuffer(jsonData))
if newRequestErr != nil {
return nil, fmt.Errorf("%w: %s", ErrSystem, newRequestErr.Error())
err = fmt.Errorf("%w: %s", ErrSystem, newRequestErr.Error())
if w.logger != nil {
w.logger.LogError(requestID, code, err, w.buildLogData(reqData, transactionID))
}
return nil, err
}
// 设置请求头
@@ -157,38 +289,90 @@ func (w *WestDexService) G05HZ01CallAPI(code string, reqData map[string]interfac
client := &http.Client{}
httpResp, clientDoErr := client.Do(req)
if clientDoErr != nil {
return nil, fmt.Errorf("%w: %s", ErrSystem, clientDoErr.Error())
err = fmt.Errorf("%w: %s", ErrSystem, clientDoErr.Error())
if w.logger != nil {
w.logger.LogError(requestID, code, err, w.buildLogData(reqData, transactionID))
}
return nil, err
}
defer func(Body io.ReadCloser) {
closeErr := Body.Close()
if closeErr != nil {
// 忽略
// 记录关闭错误
if w.logger != nil {
w.logger.LogError(requestID, code, fmt.Errorf("关闭响应体失败: %w", closeErr), w.buildLogData(reqData, transactionID))
}
}
}(httpResp.Body)
// 计算请求耗时
duration := time.Since(startTime)
if httpResp.StatusCode == 200 {
bodyBytes, ReadErr := io.ReadAll(httpResp.Body)
if ReadErr != nil {
return nil, fmt.Errorf("%w: %s", ErrSystem, ReadErr.Error())
err = fmt.Errorf("%w: %s", ErrSystem, ReadErr.Error())
if w.logger != nil {
w.logger.LogError(requestID, code, err, w.buildLogData(reqData, transactionID))
}
return nil, err
}
// 记录响应日志
if w.logger != nil {
w.logger.LogResponse(requestID, code, httpResp.StatusCode, bodyBytes, duration)
}
var westDexResp G05HZ01WestResp
UnmarshalErr := json.Unmarshal(bodyBytes, &westDexResp)
if UnmarshalErr != nil {
return nil, fmt.Errorf("%w: %s", ErrSystem, UnmarshalErr.Error())
err = fmt.Errorf("%w: %s", ErrSystem, UnmarshalErr.Error())
if w.logger != nil {
w.logger.LogError(requestID, code, err, w.buildLogData(reqData, transactionID))
}
return nil, err
}
if westDexResp.Code != "0000" {
if westDexResp.Data == nil {
return nil, fmt.Errorf("%w: %s", ErrSystem, westDexResp.Message)
err = fmt.Errorf("%w: %s", ErrSystem, westDexResp.Message)
if w.logger != nil {
w.logger.LogError(requestID, code, err, w.buildLogData(reqData, transactionID))
}
return nil, err
} else {
// 记录业务错误日志
if w.logger != nil {
w.logger.LogError(requestID, code, fmt.Errorf("%w: %s", ErrSystem, string(westDexResp.Data)), w.buildLogData(reqData, transactionID))
}
// 记录性能日志(失败)
// 注意:通用日志系统不包含性能日志功能
return westDexResp.Data, fmt.Errorf("%w: %s", ErrSystem, string(westDexResp.Data))
}
}
if westDexResp.Data == nil {
return nil, fmt.Errorf("%w: %s", ErrSystem, westDexResp.Message)
err = fmt.Errorf("%w: %s", ErrSystem, westDexResp.Message)
if w.logger != nil {
w.logger.LogError(requestID, code, err, w.buildLogData(reqData, transactionID))
}
return nil, err
}
// 记录性能日志(成功)
// 注意:通用日志系统不包含性能日志功能
return westDexResp.Data, nil
} else {
return nil, fmt.Errorf("%w: 西部请求失败Code: %d", ErrSystem, httpResp.StatusCode)
// 记录HTTP错误
err = fmt.Errorf("%w: 西部请求失败Code: %d", ErrSystem, httpResp.StatusCode)
if w.logger != nil {
w.logger.LogError(requestID, code, err, w.buildLogData(reqData, transactionID))
// 注意:通用日志系统不包含性能日志功能
}
return nil, err
}
}
@@ -197,10 +381,12 @@ func (w *WestDexService) Encrypt(data string) (string, error) {
if err != nil {
return "", ErrSystem
}
return encryptedValue, nil
}
func (w *WestDexService) Md5Encrypt(data string) string {
return Md5Encrypt(data)
result := Md5Encrypt(data)
return result
}
func (w *WestDexService) GetConfig() WestConfig {

View File

@@ -0,0 +1,67 @@
package yushan
import (
"tyapi-server/internal/config"
"tyapi-server/internal/shared/external_logger"
)
// NewYushanServiceWithConfig 使用配置创建羽山服务
func NewYushanServiceWithConfig(cfg *config.Config) (*YushanService, error) {
// 将配置类型转换为通用外部服务日志配置
loggingConfig := external_logger.ExternalServiceLoggingConfig{
Enabled: cfg.Yushan.Logging.Enabled,
LogDir: cfg.Yushan.Logging.LogDir,
ServiceName: "yushan",
UseDaily: cfg.Yushan.Logging.UseDaily,
EnableLevelSeparation: cfg.Yushan.Logging.EnableLevelSeparation,
LevelConfigs: make(map[string]external_logger.ExternalServiceLevelFileConfig),
}
// 转换级别配置
for key, value := range cfg.Yushan.Logging.LevelConfigs {
loggingConfig.LevelConfigs[key] = external_logger.ExternalServiceLevelFileConfig{
MaxSize: value.MaxSize,
MaxBackups: value.MaxBackups,
MaxAge: value.MaxAge,
Compress: value.Compress,
}
}
// 创建通用外部服务日志器
logger, err := external_logger.NewExternalServiceLogger(loggingConfig)
if err != nil {
return nil, err
}
// 创建羽山服务
service := NewYushanService(
cfg.Yushan.URL,
cfg.Yushan.APIKey,
cfg.Yushan.AcctID,
logger,
)
return service, nil
}
// NewYushanServiceWithLogging 使用自定义日志配置创建羽山服务
func NewYushanServiceWithLogging(url, apiKey, acctID string, loggingConfig external_logger.ExternalServiceLoggingConfig) (*YushanService, error) {
// 设置服务名称
loggingConfig.ServiceName = "yushan"
// 创建通用外部服务日志器
logger, err := external_logger.NewExternalServiceLogger(loggingConfig)
if err != nil {
return nil, err
}
// 创建羽山服务
service := NewYushanService(url, apiKey, acctID, logger)
return service, nil
}
// NewYushanServiceSimple 创建简单的羽山服务(无日志)
func NewYushanServiceSimple(url, apiKey, acctID string) *YushanService {
return NewYushanService(url, apiKey, acctID, nil)
}

View File

@@ -2,8 +2,10 @@ package yushan
import (
"bytes"
"context"
"crypto/aes"
"crypto/cipher"
"crypto/md5"
"crypto/rand"
"encoding/base64"
"encoding/hex"
@@ -15,6 +17,8 @@ import (
"strings"
"time"
"tyapi-server/internal/shared/external_logger"
"github.com/tidwall/gjson"
)
@@ -32,21 +36,37 @@ type YushanConfig struct {
type YushanService struct {
config YushanConfig
logger *external_logger.ExternalServiceLogger
}
// NewWestDexService 是一个构造函数,用于初始化 WestDexService
func NewYushanService(url, apiKey, acctID string) *YushanService {
// NewYushanService 是一个构造函数,用于初始化 YushanService
func NewYushanService(url, apiKey, acctID string, logger *external_logger.ExternalServiceLogger) *YushanService {
return &YushanService{
config: YushanConfig{
URL: url,
ApiKey: apiKey,
AcctID: acctID,
},
logger: logger,
}
}
// CallAPI 调用西部数据的 API
func (y *YushanService) CallAPI(code string, params map[string]interface{}) (respBytes []byte, err error) {
// CallAPI 调用羽山数据的 API
func (y *YushanService) CallAPI(ctx context.Context, code string, params map[string]interface{}) (respBytes []byte, err error) {
startTime := time.Now()
requestID := y.generateRequestID()
// 从ctx中获取transactionId
var transactionID string
if ctxTransactionID, ok := ctx.Value("transaction_id").(string); ok {
transactionID = ctxTransactionID
}
// 记录请求日志
if y.logger != nil {
y.logger.LogRequest(requestID, code, y.config.URL, y.buildLogData(params, transactionID))
}
// 获取当前时间戳
unixMilliseconds := time.Now().UnixNano() / int64(time.Millisecond)
@@ -64,13 +84,21 @@ func (y *YushanService) CallAPI(code string, params map[string]interface{}) (res
// 将请求数据转换为 JSON 字节数组
messageBytes, err := json.Marshal(reqData)
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrSystem, err.Error())
err = fmt.Errorf("%w: %s", ErrSystem, err.Error())
if y.logger != nil {
y.logger.LogError(requestID, code, err, y.buildLogData(params, transactionID))
}
return nil, err
}
// 获取 API 密钥
key, err := hex.DecodeString(y.config.ApiKey)
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrSystem, err.Error())
err = fmt.Errorf("%w: %s", ErrSystem, err.Error())
if y.logger != nil {
y.logger.LogError(requestID, code, err, y.buildLogData(params, transactionID))
}
return nil, err
}
// 使用 AES CBC 加密请求数据
@@ -80,10 +108,16 @@ func (y *YushanService) CallAPI(code string, params map[string]interface{}) (res
content := base64.StdEncoding.EncodeToString(cipherText)
// 发起 HTTP 请求
client := &http.Client{}
req, err := http.NewRequest("POST", y.config.URL, strings.NewReader(content))
client := &http.Client{
Timeout: 20 * time.Second,
}
req, err := http.NewRequestWithContext(ctx, "POST", y.config.URL, strings.NewReader(content))
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrSystem, err.Error())
err = fmt.Errorf("%w: %s", ErrSystem, err.Error())
if y.logger != nil {
y.logger.LogError(requestID, code, err, y.buildLogData(params, transactionID))
}
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("ACCT_ID", y.config.AcctID)
@@ -91,13 +125,20 @@ func (y *YushanService) CallAPI(code string, params map[string]interface{}) (res
// 执行请求
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrSystem, err.Error())
err = fmt.Errorf("%w: %s", ErrSystem, err.Error())
if y.logger != nil {
y.logger.LogError(requestID, code, err, y.buildLogData(params, transactionID))
}
return nil, err
}
defer resp.Body.Close()
// 读取响应体
body, err := io.ReadAll(resp.Body)
if err != nil {
if y.logger != nil {
y.logger.LogError(requestID, code, err, y.buildLogData(params, transactionID))
}
return nil, err
}
@@ -108,12 +149,22 @@ func (y *YushanService) CallAPI(code string, params map[string]interface{}) (res
} else {
sDec, err := base64.StdEncoding.DecodeString(string(body))
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrSystem, err.Error())
err = fmt.Errorf("%w: %s", ErrSystem, err.Error())
if y.logger != nil {
y.logger.LogError(requestID, code, err, y.buildLogData(params, transactionID))
}
return nil, err
}
respData = y.AES_CBC_Decrypt(sDec, key)
}
retCode := gjson.GetBytes(respData, "retcode").String()
// 记录响应日志
if y.logger != nil {
duration := time.Since(startTime)
y.logger.LogResponse(requestID, code, resp.StatusCode, respData, duration)
}
if retCode == "100000" {
// retcode 为 100000表示查询为空
return nil, ErrNotFound
@@ -121,13 +172,41 @@ func (y *YushanService) CallAPI(code string, params map[string]interface{}) (res
// retcode 为 000000表示有数据返回 retdata
retData := gjson.GetBytes(respData, "retdata")
if !retData.Exists() {
return nil, fmt.Errorf("%w: %s", ErrDatasource, "羽山请求retdata为空")
err = fmt.Errorf("%w: %s", ErrDatasource, "羽山请求retdata为空")
if y.logger != nil {
y.logger.LogError(requestID, code, err, y.buildLogData(params, transactionID))
}
return nil, err
}
return []byte(retData.Raw), nil
} else {
return nil, fmt.Errorf("%w: %s", ErrDatasource, "羽山请求未知的状态码")
err = fmt.Errorf("%w: %s", ErrDatasource, "羽山请求未知的状态码")
if y.logger != nil {
y.logger.LogError(requestID, code, err, y.buildLogData(params, transactionID))
}
return nil, err
}
}
// generateRequestID 生成请求ID
func (y *YushanService) generateRequestID() string {
timestamp := time.Now().UnixNano()
hash := md5.Sum([]byte(fmt.Sprintf("%d_%s", timestamp, y.config.ApiKey)))
return fmt.Sprintf("yushan_%x", hash[:8])
}
// buildLogData 构建包含transactionId的日志数据
func (y *YushanService) buildLogData(data map[string]interface{}, transactionID string) map[string]interface{} {
if transactionID == "" {
return data
}
logData := data
if logData == nil {
logData = make(map[string]interface{})
}
logData["transaction_id"] = transactionID
return logData
}
// GenerateRandomString 生成一个32位的随机字符串订单号

View File

@@ -0,0 +1,83 @@
package yushan
import (
"testing"
"time"
)
func TestGenerateRequestID(t *testing.T) {
service := &YushanService{
config: YushanConfig{
ApiKey: "test_api_key_123",
},
}
id1 := service.generateRequestID()
// 等待一小段时间确保时间戳不同
time.Sleep(time.Millisecond)
id2 := service.generateRequestID()
if id1 == "" || id2 == "" {
t.Error("请求ID生成失败")
}
if id1 == id2 {
t.Error("不同时间生成的请求ID应该不同")
}
// 验证ID格式
if len(id1) < 20 { // yushan_ + 8位十六进制 + 其他
t.Errorf("请求ID长度不足实际: %s", id1)
}
}
func TestGenerateRandomString(t *testing.T) {
service := &YushanService{}
str1, err := service.GenerateRandomString()
if err != nil {
t.Fatalf("生成随机字符串失败: %v", err)
}
str2, err := service.GenerateRandomString()
if err != nil {
t.Fatalf("生成随机字符串失败: %v", err)
}
if str1 == "" || str2 == "" {
t.Error("随机字符串为空")
}
if str1 == str2 {
t.Error("两次生成的随机字符串应该不同")
}
// 验证长度16字节 = 32位十六进制字符
if len(str1) != 32 || len(str2) != 32 {
t.Error("随机字符串长度应该是32位")
}
}
func TestIsJSON(t *testing.T) {
testCases := []struct {
input string
expected bool
}{
{"{}", true},
{"[]", true},
{"{\"key\": \"value\"}", true},
{"[1, 2, 3]", true},
{"invalid json", false},
{"", false},
{"{invalid}", false},
}
for _, tc := range testCases {
result := IsJSON(tc.input)
if result != tc.expected {
t.Errorf("输入: %s, 期望: %v, 实际: %v", tc.input, tc.expected, result)
}
}
}

View File

@@ -0,0 +1,121 @@
package zhicha
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"encoding/hex"
"fmt"
)
const (
KEY_SIZE = 16 // AES-128, 16 bytes
)
// Encrypt 使用AES-128-CBC加密数据
// 对应Python示例中的encrypt函数
func Encrypt(data, key string) (string, error) {
// 将十六进制密钥转换为字节
binKey, err := hex.DecodeString(key)
if err != nil {
return "", fmt.Errorf("密钥格式错误: %w", err)
}
if len(binKey) < KEY_SIZE {
return "", fmt.Errorf("密钥长度不足,需要至少%d字节", KEY_SIZE)
}
// 从密钥前16个字符生成IV
iv := []byte(key[:KEY_SIZE])
// 创建AES加密器
block, err := aes.NewCipher(binKey)
if err != nil {
return "", fmt.Errorf("创建AES加密器失败: %w", err)
}
// 对数据进行PKCS7填充
paddedData := pkcs7Padding([]byte(data), aes.BlockSize)
// 创建CBC模式加密器
mode := cipher.NewCBCEncrypter(block, iv)
// 加密
ciphertext := make([]byte, len(paddedData))
mode.CryptBlocks(ciphertext, paddedData)
// 返回Base64编码结果
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
// Decrypt 使用AES-128-CBC解密数据
// 对应Python示例中的decrypt函数
func Decrypt(encryptedData, key string) (string, error) {
// 将十六进制密钥转换为字节
binKey, err := hex.DecodeString(key)
if err != nil {
return "", fmt.Errorf("密钥格式错误: %w", err)
}
if len(binKey) < KEY_SIZE {
return "", fmt.Errorf("密钥长度不足,需要至少%d字节", KEY_SIZE)
}
// 从密钥前16个字符生成IV
iv := []byte(key[:KEY_SIZE])
// 解码Base64数据
decodedData, err := base64.StdEncoding.DecodeString(encryptedData)
if err != nil {
return "", fmt.Errorf("Base64解码失败: %w", err)
}
// 检查数据长度是否为AES块大小的倍数
if len(decodedData) == 0 || len(decodedData)%aes.BlockSize != 0 {
return "", fmt.Errorf("加密数据长度无效,必须是%d字节的倍数", aes.BlockSize)
}
// 创建AES解密器
block, err := aes.NewCipher(binKey)
if err != nil {
return "", fmt.Errorf("创建AES解密器失败: %w", err)
}
// 创建CBC模式解密器
mode := cipher.NewCBCDecrypter(block, iv)
// 解密
plaintext := make([]byte, len(decodedData))
mode.CryptBlocks(plaintext, decodedData)
// 移除PKCS7填充
unpadded, err := pkcs7Unpadding(plaintext)
if err != nil {
return "", fmt.Errorf("移除填充失败: %w", err)
}
return string(unpadded), nil
}
// pkcs7Padding 使用PKCS7填充数据
func pkcs7Padding(src []byte, blockSize int) []byte {
padding := blockSize - len(src)%blockSize
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
return append(src, padtext...)
}
// pkcs7Unpadding 移除PKCS7填充
func pkcs7Unpadding(src []byte) ([]byte, error) {
length := len(src)
if length == 0 {
return nil, fmt.Errorf("数据为空")
}
unpadding := int(src[length-1])
if unpadding > length {
return nil, fmt.Errorf("填充长度无效")
}
return src[:length-unpadding], nil
}

View File

@@ -0,0 +1,170 @@
package zhicha
import (
"fmt"
)
// ZhichaError 智查金控服务错误
type ZhichaError struct {
Code string `json:"code"`
Message string `json:"message"`
}
// Error 实现error接口
func (e *ZhichaError) Error() string {
return fmt.Sprintf("智查金控错误 [%s]: %s", e.Code, e.Message)
}
// IsSuccess 检查是否成功
func (e *ZhichaError) IsSuccess() bool {
return e.Code == "200"
}
// IsNoRecord 检查是否查询无记录
func (e *ZhichaError) IsNoRecord() bool {
return e.Code == "201"
}
// IsBusinessError 检查是否是业务错误(非系统错误)
func (e *ZhichaError) IsBusinessError() bool {
return e.Code >= "302" && e.Code <= "320"
}
// IsSystemError 检查是否是系统错误
func (e *ZhichaError) IsSystemError() bool {
return e.Code == "500"
}
// IsAuthError 检查是否是认证相关错误
func (e *ZhichaError) IsAuthError() bool {
return e.Code == "304" || e.Code == "318" || e.Code == "319" || e.Code == "320"
}
// IsParamError 检查是否是参数相关错误
func (e *ZhichaError) IsParamError() bool {
return e.Code == "302" || e.Code == "303" || e.Code == "305" || e.Code == "306" || e.Code == "307" || e.Code == "316" || e.Code == "317"
}
// IsServiceError 检查是否是服务相关错误
func (e *ZhichaError) IsServiceError() bool {
return e.Code == "308" || e.Code == "309" || e.Code == "310" || e.Code == "311"
}
// IsUserError 检查是否是用户相关错误
func (e *ZhichaError) IsUserError() bool {
return e.Code == "312" || e.Code == "313" || e.Code == "314" || e.Code == "315"
}
// 预定义错误常量
var (
// 成功状态
ErrSuccess = &ZhichaError{Code: "200", Message: "请求成功"}
ErrNoRecord = &ZhichaError{Code: "201", Message: "查询无记录"}
// 业务参数错误
ErrBusinessParamMissing = &ZhichaError{Code: "302", Message: "业务参数缺失"}
ErrParamError = &ZhichaError{Code: "303", Message: "参数错误"}
ErrHeaderParamMissing = &ZhichaError{Code: "304", Message: "请求头参数缺失"}
ErrNameError = &ZhichaError{Code: "305", Message: "姓名错误"}
ErrPhoneError = &ZhichaError{Code: "306", Message: "手机号错误"}
ErrIDCardError = &ZhichaError{Code: "307", Message: "身份证号错误"}
// 服务相关错误
ErrServiceNotExist = &ZhichaError{Code: "308", Message: "服务不存在"}
ErrServiceNotEnabled = &ZhichaError{Code: "309", Message: "服务未开通"}
ErrInsufficientBalance = &ZhichaError{Code: "310", Message: "余额不足"}
ErrRemoteDataError = &ZhichaError{Code: "311", Message: "调用远程数据异常"}
// 用户相关错误
ErrUserNotExist = &ZhichaError{Code: "312", Message: "用户不存在"}
ErrUserStatusError = &ZhichaError{Code: "313", Message: "用户状态异常"}
ErrUserUnauthorized = &ZhichaError{Code: "314", Message: "用户未授权"}
ErrWhitelistError = &ZhichaError{Code: "315", Message: "白名单错误"}
// 时间戳和认证错误
ErrTimestampInvalid = &ZhichaError{Code: "316", Message: "timestamp不合法"}
ErrTimestampExpired = &ZhichaError{Code: "317", Message: "timestamp已过期"}
ErrSignVerifyFailed = &ZhichaError{Code: "318", Message: "验签失败"}
ErrDecryptFailed = &ZhichaError{Code: "319", Message: "解密失败"}
ErrUnauthorized = &ZhichaError{Code: "320", Message: "未授权"}
// 系统错误
ErrSystemError = &ZhichaError{Code: "500", Message: "系统异常,请联系管理员"}
)
// NewZhichaError 创建新的智查金控错误
func NewZhichaError(code, message string) *ZhichaError {
return &ZhichaError{
Code: code,
Message: message,
}
}
// NewZhichaErrorFromCode 根据状态码创建错误
func NewZhichaErrorFromCode(code string) *ZhichaError {
switch code {
case "200":
return ErrSuccess
case "201":
return ErrNoRecord
case "302":
return ErrBusinessParamMissing
case "303":
return ErrParamError
case "304":
return ErrHeaderParamMissing
case "305":
return ErrNameError
case "306":
return ErrPhoneError
case "307":
return ErrIDCardError
case "308":
return ErrServiceNotExist
case "309":
return ErrServiceNotEnabled
case "310":
return ErrInsufficientBalance
case "311":
return ErrRemoteDataError
case "312":
return ErrUserNotExist
case "313":
return ErrUserStatusError
case "314":
return ErrUserUnauthorized
case "315":
return ErrWhitelistError
case "316":
return ErrTimestampInvalid
case "317":
return ErrTimestampExpired
case "318":
return ErrSignVerifyFailed
case "319":
return ErrDecryptFailed
case "320":
return ErrUnauthorized
case "500":
return ErrSystemError
default:
return &ZhichaError{
Code: code,
Message: "未知错误",
}
}
}
// IsZhichaError 检查是否是智查金控错误
func IsZhichaError(err error) bool {
_, ok := err.(*ZhichaError)
return ok
}
// GetZhichaError 获取智查金控错误
func GetZhichaError(err error) *ZhichaError {
if zhichaErr, ok := err.(*ZhichaError); ok {
return zhichaErr
}
return nil
}

View File

@@ -0,0 +1,68 @@
package zhicha
import (
"tyapi-server/internal/config"
"tyapi-server/internal/shared/external_logger"
)
// NewZhichaServiceWithConfig 使用配置创建智查金控服务
func NewZhichaServiceWithConfig(cfg *config.Config) (*ZhichaService, error) {
// 将配置类型转换为通用外部服务日志配置
loggingConfig := external_logger.ExternalServiceLoggingConfig{
Enabled: cfg.Zhicha.Logging.Enabled,
LogDir: cfg.Zhicha.Logging.LogDir,
ServiceName: "zhicha",
UseDaily: cfg.Zhicha.Logging.UseDaily,
EnableLevelSeparation: cfg.Zhicha.Logging.EnableLevelSeparation,
LevelConfigs: make(map[string]external_logger.ExternalServiceLevelFileConfig),
}
// 转换级别配置
for key, value := range cfg.Zhicha.Logging.LevelConfigs {
loggingConfig.LevelConfigs[key] = external_logger.ExternalServiceLevelFileConfig{
MaxSize: value.MaxSize,
MaxBackups: value.MaxBackups,
MaxAge: value.MaxAge,
Compress: value.Compress,
}
}
// 创建通用外部服务日志器
logger, err := external_logger.NewExternalServiceLogger(loggingConfig)
if err != nil {
return nil, err
}
// 创建智查金控服务
service := NewZhichaService(
cfg.Zhicha.URL,
cfg.Zhicha.AppID,
cfg.Zhicha.AppSecret,
cfg.Zhicha.EncryptKey,
logger,
)
return service, nil
}
// NewZhichaServiceWithLogging 使用自定义日志配置创建智查金控服务
func NewZhichaServiceWithLogging(url, appID, appSecret, encryptKey string, loggingConfig external_logger.ExternalServiceLoggingConfig) (*ZhichaService, error) {
// 设置服务名称
loggingConfig.ServiceName = "zhicha"
// 创建通用外部服务日志器
logger, err := external_logger.NewExternalServiceLogger(loggingConfig)
if err != nil {
return nil, err
}
// 创建智查金控服务
service := NewZhichaService(url, appID, appSecret, encryptKey, logger)
return service, nil
}
// NewZhichaServiceSimple 创建简单的智查金控服务(无日志)
func NewZhichaServiceSimple(url, appID, appSecret, encryptKey string) *ZhichaService {
return NewZhichaService(url, appID, appSecret, encryptKey, nil)
}

View File

@@ -0,0 +1,318 @@
package zhicha
import (
"bytes"
"context"
"crypto/aes"
"crypto/cipher"
"crypto/md5"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strconv"
"time"
"tyapi-server/internal/shared/external_logger"
)
var (
ErrDatasource = errors.New("数据源异常")
ErrSystem = errors.New("系统异常")
)
type ZhichaResp struct {
Code string `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
Success bool `json:"success"`
}
type ZhichaConfig struct {
URL string
AppID string
AppSecret string
EncryptKey string
}
type ZhichaService struct {
config ZhichaConfig
logger *external_logger.ExternalServiceLogger
}
// NewZhichaService 是一个构造函数,用于初始化 ZhichaService
func NewZhichaService(url, appID, appSecret, encryptKey string, logger *external_logger.ExternalServiceLogger) *ZhichaService {
return &ZhichaService{
config: ZhichaConfig{
URL: url,
AppID: appID,
AppSecret: appSecret,
EncryptKey: encryptKey,
},
logger: logger,
}
}
// generateRequestID 生成请求ID
func (z *ZhichaService) generateRequestID() string {
timestamp := time.Now().UnixNano()
hash := md5.Sum([]byte(fmt.Sprintf("%d_%s", timestamp, z.config.AppID)))
return fmt.Sprintf("zhicha_%x", hash[:8])
}
// buildLogData 构建包含transactionId的日志数据
func (z *ZhichaService) buildLogData(data map[string]interface{}, transactionID string) map[string]interface{} {
if transactionID == "" {
return data
}
logData := data
if logData == nil {
logData = make(map[string]interface{})
}
logData["transaction_id"] = transactionID
return logData
}
// generateSign 生成签名
func (z *ZhichaService) generateSign(timestamp int64) string {
// 第一步对app_secret进行MD5加密
encryptedSecret := fmt.Sprintf("%x", md5.Sum([]byte(z.config.AppSecret)))
// 第二步将加密后的密钥和时间戳拼接再次MD5加密
signStr := encryptedSecret + strconv.FormatInt(timestamp, 10)
sign := fmt.Sprintf("%x", md5.Sum([]byte(signStr)))
return sign
}
// CallAPI 调用智查金控的 API
func (z *ZhichaService) CallAPI(ctx context.Context, proID string, params map[string]interface{}) (data interface{}, err error) {
startTime := time.Now()
requestID := z.generateRequestID()
timestamp := time.Now().Unix()
// 从ctx中获取transactionId
var transactionID string
if ctxTransactionID, ok := ctx.Value("transaction_id").(string); ok {
transactionID = ctxTransactionID
}
// 记录请求日志
if z.logger != nil {
z.logger.LogRequest(requestID, "handle", z.config.URL, z.buildLogData(params, transactionID))
}
jsonData, marshalErr := json.Marshal(params)
if marshalErr != nil {
err = fmt.Errorf("%w: %s", ErrSystem, marshalErr.Error())
if z.logger != nil {
z.logger.LogError(requestID, "handle", err, z.buildLogData(params, transactionID))
}
return nil, err
}
// 创建HTTP POST请求
req, err := http.NewRequestWithContext(ctx, "POST", z.config.URL, bytes.NewBuffer(jsonData))
if err != nil {
err = fmt.Errorf("%w: %s", ErrSystem, err.Error())
if z.logger != nil {
z.logger.LogError(requestID, "handle", err, z.buildLogData(params, transactionID))
}
return nil, err
}
// 设置请求头
req.Header.Set("Content-Type", "application/json")
req.Header.Set("appId", z.config.AppID)
req.Header.Set("proId", proID)
req.Header.Set("timestamp", strconv.FormatInt(timestamp, 10))
req.Header.Set("sign", z.generateSign(timestamp))
// 创建HTTP客户端
client := &http.Client{
Timeout: 20 * time.Second,
}
// 发送请求
response, err := client.Do(req)
if err != nil {
err = fmt.Errorf("%w: %s", ErrSystem, err.Error())
if z.logger != nil {
z.logger.LogError(requestID, "handle", err, z.buildLogData(params, transactionID))
}
return nil, err
}
defer response.Body.Close()
// 读取响应
respBody, err := io.ReadAll(response.Body)
if err != nil {
err = fmt.Errorf("%w: %s", ErrSystem, err.Error())
if z.logger != nil {
z.logger.LogError(requestID, "handle", err, z.buildLogData(params, transactionID))
}
return nil, err
}
// 记录响应日志
if z.logger != nil {
duration := time.Since(startTime)
z.logger.LogResponse(requestID, "handle", response.StatusCode, respBody, duration)
}
// 检查HTTP状态码
if response.StatusCode != http.StatusOK {
err = fmt.Errorf("%w: HTTP状态码 %d", ErrDatasource, response.StatusCode)
if z.logger != nil {
z.logger.LogError(requestID, "handle", err, z.buildLogData(params, transactionID))
}
return nil, err
}
// 解析响应
var zhichaResp ZhichaResp
if err := json.Unmarshal(respBody, &zhichaResp); err != nil {
err = fmt.Errorf("%w: 响应解析失败: %s", ErrSystem, err.Error())
if z.logger != nil {
z.logger.LogError(requestID, "handle", err, z.buildLogData(params, transactionID))
}
return nil, err
}
// 检查业务状态码
if zhichaResp.Code != "200" && zhichaResp.Code != "201" {
// 创建智查金控错误用于日志记录
zhichaErr := NewZhichaErrorFromCode(zhichaResp.Code)
if zhichaErr.Code == "未知错误" {
zhichaErr.Message = zhichaResp.Message
}
// 记录智查金控的详细错误信息到日志
if z.logger != nil {
z.logger.LogError(requestID, "handle", zhichaErr, z.buildLogData(params, transactionID))
}
// 对外统一返回数据源异常错误
return nil, ErrDatasource
}
// 返回data字段
return zhichaResp.Data, nil
}
// Encrypt 使用配置的加密密钥对数据进行AES-128-CBC加密
func (z *ZhichaService) Encrypt(data string) (string, error) {
if z.config.EncryptKey == "" {
return "", fmt.Errorf("加密密钥未配置")
}
// 将十六进制密钥转换为字节
binKey, err := hex.DecodeString(z.config.EncryptKey)
if err != nil {
return "", fmt.Errorf("密钥格式错误: %w", err)
}
if len(binKey) < 16 { // AES-128, 16 bytes
return "", fmt.Errorf("密钥长度不足需要至少16字节")
}
// 从密钥前16个字符生成IV
iv := []byte(z.config.EncryptKey[:16])
// 创建AES加密器
block, err := aes.NewCipher(binKey)
if err != nil {
return "", fmt.Errorf("创建AES加密器失败: %w", err)
}
// 对数据进行PKCS7填充
paddedData := z.pkcs7Padding([]byte(data), aes.BlockSize)
// 创建CBC模式加密器
mode := cipher.NewCBCEncrypter(block, iv)
// 加密
ciphertext := make([]byte, len(paddedData))
mode.CryptBlocks(ciphertext, paddedData)
// 返回Base64编码结果
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
// Decrypt 使用配置的加密密钥对数据进行AES-128-CBC解密
func (z *ZhichaService) Decrypt(encryptedData string) (string, error) {
if z.config.EncryptKey == "" {
return "", fmt.Errorf("加密密钥未配置")
}
// 将十六进制密钥转换为字节
binKey, err := hex.DecodeString(z.config.EncryptKey)
if err != nil {
return "", fmt.Errorf("密钥格式错误: %w", err)
}
if len(binKey) < 16 { // AES-128, 16 bytes
return "", fmt.Errorf("密钥长度不足需要至少16字节")
}
// 从密钥前16个字符生成IV
iv := []byte(z.config.EncryptKey[:16])
// 解码Base64数据
decodedData, err := base64.StdEncoding.DecodeString(encryptedData)
if err != nil {
return "", fmt.Errorf("Base64解码失败: %w", err)
}
// 检查数据长度是否为AES块大小的倍数
if len(decodedData) == 0 || len(decodedData)%aes.BlockSize != 0 {
return "", fmt.Errorf("加密数据长度无效,必须是%d字节的倍数", aes.BlockSize)
}
// 创建AES解密器
block, err := aes.NewCipher(binKey)
if err != nil {
return "", fmt.Errorf("创建AES解密器失败: %w", err)
}
// 创建CBC模式解密器
mode := cipher.NewCBCDecrypter(block, iv)
// 解密
plaintext := make([]byte, len(decodedData))
mode.CryptBlocks(plaintext, decodedData)
// 移除PKCS7填充
unpadded, err := z.pkcs7Unpadding(plaintext)
if err != nil {
return "", fmt.Errorf("移除填充失败: %w", err)
}
return string(unpadded), nil
}
// pkcs7Padding 使用PKCS7填充数据
func (z *ZhichaService) pkcs7Padding(src []byte, blockSize int) []byte {
padding := blockSize - len(src)%blockSize
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
return append(src, padtext...)
}
// pkcs7Unpadding 移除PKCS7填充
func (z *ZhichaService) pkcs7Unpadding(src []byte) ([]byte, error) {
length := len(src)
if length == 0 {
return nil, fmt.Errorf("数据为空")
}
unpadding := int(src[length-1])
if unpadding > length {
return nil, fmt.Errorf("填充长度无效")
}
return src[:length-unpadding], nil
}

View File

@@ -0,0 +1,698 @@
package zhicha
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"testing"
"time"
)
func TestGenerateSign(t *testing.T) {
service := &ZhichaService{
config: ZhichaConfig{
AppSecret: "test_secret_123",
},
}
timestamp := int64(1640995200) // 2022-01-01 00:00:00
sign := service.generateSign(timestamp)
if sign == "" {
t.Error("签名生成失败,签名为空")
}
// 验证签名长度MD5是32位十六进制
if len(sign) != 32 {
t.Errorf("签名长度错误期望32位实际%d位", len(sign))
}
// 验证相同参数生成相同签名
sign2 := service.generateSign(timestamp)
if sign != sign2 {
t.Error("相同参数生成的签名不一致")
}
}
func TestEncryptDecrypt(t *testing.T) {
// 测试密钥32位十六进制
key := "1234567890abcdef1234567890abcdef"
// 测试数据
testData := "这是一个测试数据包含中文和English"
// 加密
encrypted, err := Encrypt(testData, key)
if err != nil {
t.Fatalf("加密失败: %v", err)
}
if encrypted == "" {
t.Error("加密结果为空")
}
// 解密
decrypted, err := Decrypt(encrypted, key)
if err != nil {
t.Fatalf("解密失败: %v", err)
}
if decrypted != testData {
t.Errorf("解密结果不匹配,期望: %s, 实际: %s", testData, decrypted)
}
}
func TestEncryptWithInvalidKey(t *testing.T) {
// 测试无效密钥
invalidKeys := []string{
"", // 空密钥
"123", // 太短
"invalid_key_string", // 非十六进制
"1234567890abcdef", // 16位不足32位
}
testData := "test data"
for _, key := range invalidKeys {
_, err := Encrypt(testData, key)
if err == nil {
t.Errorf("使用无效密钥 %s 应该返回错误", key)
}
}
}
func TestDecryptWithInvalidData(t *testing.T) {
key := "1234567890abcdef1234567890abcdef"
// 测试无效的加密数据
invalidData := []string{
"", // 空数据
"invalid_base64", // 无效的Base64
"dGVzdA==", // 有效的Base64但不是AES加密数据
}
for _, data := range invalidData {
_, err := Decrypt(data, key)
if err == nil {
t.Errorf("使用无效数据 %s 应该返回错误", data)
}
}
}
func TestPKCS7Padding(t *testing.T) {
testCases := []struct {
input string
blockSize int
expected int
}{
{"", 16, 16},
{"a", 16, 16},
{"ab", 16, 16},
{"abc", 16, 16},
{"abcd", 16, 16},
{"abcde", 16, 16},
{"abcdef", 16, 16},
{"abcdefg", 16, 16},
{"abcdefgh", 16, 16},
{"abcdefghi", 16, 16},
{"abcdefghij", 16, 16},
{"abcdefghijk", 16, 16},
{"abcdefghijkl", 16, 16},
{"abcdefghijklm", 16, 16},
{"abcdefghijklmn", 16, 16},
{"abcdefghijklmno", 16, 16},
{"abcdefghijklmnop", 16, 16},
}
for _, tc := range testCases {
padded := pkcs7Padding([]byte(tc.input), tc.blockSize)
if len(padded)%tc.blockSize != 0 {
t.Errorf("输入: %s, 期望块大小倍数,实际: %d", tc.input, len(padded))
}
// 测试移除填充
unpadded, err := pkcs7Unpadding(padded)
if err != nil {
t.Errorf("移除填充失败: %v", err)
}
if string(unpadded) != tc.input {
t.Errorf("输入: %s, 期望: %s, 实际: %s", tc.input, tc.input, string(unpadded))
}
}
}
func TestGenerateRequestID(t *testing.T) {
service := &ZhichaService{
config: ZhichaConfig{
AppID: "test_app_id",
},
}
id1 := service.generateRequestID()
// 等待一小段时间确保时间戳不同
time.Sleep(time.Millisecond)
id2 := service.generateRequestID()
if id1 == "" || id2 == "" {
t.Error("请求ID生成失败")
}
if id1 == id2 {
t.Error("不同时间生成的请求ID应该不同")
}
// 验证ID格式
if len(id1) < 20 { // zhicha_ + 8位十六进制 + 其他
t.Errorf("请求ID长度不足实际: %s", id1)
}
}
func TestCallAPISuccess(t *testing.T) {
// 创建测试服务
service := &ZhichaService{
config: ZhichaConfig{
URL: "http://proxy.tianyuanapi.com/dataMiddle/api/handle",
AppID: "4b78fff61ab8426f",
AppSecret: "1128f01b94124ae899c2e9f2b1f37681",
EncryptKey: "af4ca0098e6a202a5c08c413ebd9fd62",
},
logger: nil, // 测试时不使用日志
}
// 测试参数
idCardEncrypted, err := service.Encrypt("45212220000827423X")
if err != nil {
t.Fatalf("加密身份证号失败: %v", err)
}
nameEncrypted, err := service.Encrypt("张荣宏")
if err != nil {
t.Fatalf("加密姓名失败: %v", err)
}
params := map[string]interface{}{
"idCard": idCardEncrypted,
"name": nameEncrypted,
"authorized": "1",
}
// 创建带超时的context
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// 调用API
data, err := service.CallAPI(ctx, "ZCI001", params)
// 注意这是真实API调用可能会因为网络、认证等原因失败
// 我们主要测试方法调用是否正常不强制要求API返回成功
if err != nil {
// 如果是网络错误或认证错误,这是正常的
t.Logf("API调用返回错误: %v", err)
return
}
// 如果成功,验证响应
if data == nil {
t.Error("响应数据为空")
return
}
// 将data转换为字符串进行显示
var dataStr string
if str, ok := data.(string); ok {
dataStr = str
} else {
// 如果不是字符串尝试JSON序列化
if dataBytes, err := json.Marshal(data); err == nil {
dataStr = string(dataBytes)
} else {
dataStr = fmt.Sprintf("%v", data)
}
}
t.Logf("API调用成功响应内容: %s", dataStr)
}
func TestCallAPIWithInvalidURL(t *testing.T) {
// 创建使用无效URL的服务
service := &ZhichaService{
config: ZhichaConfig{
URL: "https://invalid-url-that-does-not-exist.com/api",
AppID: "test_app_id",
AppSecret: "test_app_secret",
EncryptKey: "test_encrypt_key",
},
logger: nil,
}
params := map[string]interface{}{
"test": "data",
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// 应该返回错误
_, err := service.CallAPI(ctx, "test_pro_id", params)
if err == nil {
t.Error("使用无效URL应该返回错误")
}
t.Logf("预期的错误: %v", err)
}
func TestCallAPIWithContextCancellation(t *testing.T) {
service := &ZhichaService{
config: ZhichaConfig{
URL: "https://www.zhichajinkong.com/dataMiddle/api/handle",
AppID: "4b78fff61ab8426f",
AppSecret: "1128f01b94124ae899c2e9f2b1f37681",
EncryptKey: "af4ca0098e6a202a5c08c413ebd9fd62",
},
logger: nil,
}
params := map[string]interface{}{
"test": "data",
}
// 创建可取消的context
ctx, cancel := context.WithCancel(context.Background())
// 立即取消
cancel()
// 应该返回context取消错误
_, err := service.CallAPI(ctx, "test_pro_id", params)
if err == nil {
t.Error("context取消后应该返回错误")
}
// 检查是否是context取消错误
if err != context.Canceled && !strings.Contains(err.Error(), "context") {
t.Errorf("期望context相关错误实际: %v", err)
}
t.Logf("Context取消错误: %v", err)
}
func TestCallAPIWithTimeout(t *testing.T) {
service := &ZhichaService{
config: ZhichaConfig{
URL: "https://www.zhichajinkong.com/dataMiddle/api/handle",
AppID: "4b78fff61ab8426f",
AppSecret: "1128f01b94124ae899c2e9f2b1f37681",
EncryptKey: "af4ca0098e6a202a5c08c413ebd9fd62",
},
logger: nil,
}
params := map[string]interface{}{
"test": "data",
}
// 创建很短的超时
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond)
defer cancel()
// 应该因为超时而失败
_, err := service.CallAPI(ctx, "test_pro_id", params)
if err == nil {
t.Error("超时后应该返回错误")
}
// 检查是否是超时错误
if err != context.DeadlineExceeded && !strings.Contains(err.Error(), "timeout") && !strings.Contains(err.Error(), "deadline") {
t.Errorf("期望超时相关错误,实际: %v", err)
}
t.Logf("超时错误: %v", err)
}
func TestCallAPIRequestHeaders(t *testing.T) {
// 这个测试验证请求头是否正确设置
// 由于我们不能直接访问HTTP请求我们通过日志或其他方式来验证
service := &ZhichaService{
config: ZhichaConfig{
URL: "https://www.zhichajinkong.com/dataMiddle/api/handle",
AppID: "test_app_id",
AppSecret: "test_app_secret",
EncryptKey: "test_encrypt_key",
},
logger: nil,
}
params := map[string]interface{}{
"test": "headers",
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// 调用API可能会失败但我们主要测试请求头设置
_, err := service.CallAPI(ctx, "test_pro_id", params)
// 验证签名生成是否正确
timestamp := time.Now().Unix()
sign := service.generateSign(timestamp)
if sign == "" {
t.Error("签名生成失败")
}
if len(sign) != 32 {
t.Errorf("签名长度错误期望32位实际%d位", len(sign))
}
t.Logf("签名生成成功: %s", sign)
t.Logf("API调用结果: %v", err)
}
func TestZhichaErrorHandling(t *testing.T) {
// 测试核心错误类型
testCases := []struct {
name string
code string
message string
expectedErr *ZhichaError
}{
{
name: "成功状态",
code: "200",
message: "请求成功",
expectedErr: ErrSuccess,
},
{
name: "查询无记录",
code: "201",
message: "查询无记录",
expectedErr: ErrNoRecord,
},
{
name: "手机号错误",
code: "306",
message: "手机号错误",
expectedErr: ErrPhoneError,
},
{
name: "姓名错误",
code: "305",
message: "姓名错误",
expectedErr: ErrNameError,
},
{
name: "身份证号错误",
code: "307",
message: "身份证号错误",
expectedErr: ErrIDCardError,
},
{
name: "余额不足",
code: "310",
message: "余额不足",
expectedErr: ErrInsufficientBalance,
},
{
name: "用户不存在",
code: "312",
message: "用户不存在",
expectedErr: ErrUserNotExist,
},
{
name: "系统异常",
code: "500",
message: "系统异常,请联系管理员",
expectedErr: ErrSystemError,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// 测试从状态码创建错误
err := NewZhichaErrorFromCode(tc.code)
if err.Code != tc.expectedErr.Code {
t.Errorf("期望错误码 %s实际 %s", tc.expectedErr.Code, err.Code)
}
if err.Message != tc.expectedErr.Message {
t.Errorf("期望错误消息 %s实际 %s", tc.expectedErr.Message, err.Message)
}
})
}
}
func TestZhichaErrorHelpers(t *testing.T) {
// 测试错误类型判断函数
err := NewZhichaError("302", "业务参数缺失")
// 测试IsZhichaError
if !IsZhichaError(err) {
t.Error("IsZhichaError应该返回true")
}
// 测试GetZhichaError
zhichaErr := GetZhichaError(err)
if zhichaErr == nil {
t.Error("GetZhichaError应该返回非nil值")
}
if zhichaErr.Code != "302" {
t.Errorf("期望错误码302实际%s", zhichaErr.Code)
}
// 测试普通错误
normalErr := fmt.Errorf("普通错误")
if IsZhichaError(normalErr) {
t.Error("普通错误不应该被识别为智查金控错误")
}
if GetZhichaError(normalErr) != nil {
t.Error("普通错误的GetZhichaError应该返回nil")
}
}
func TestZhichaErrorString(t *testing.T) {
// 测试错误字符串格式
err := NewZhichaError("304", "请求头参数缺失")
expectedStr := "智查金控错误 [304]: 请求头参数缺失"
if err.Error() != expectedStr {
t.Errorf("期望错误字符串 %s实际 %s", expectedStr, err.Error())
}
}
func TestErrorsIsFunctionality(t *testing.T) {
// 测试 errors.Is() 功能是否正常工作
// 创建各种错误
testCases := []struct {
name string
err error
expected error
shouldMatch bool
}{
{
name: "手机号错误匹配",
err: ErrPhoneError,
expected: ErrPhoneError,
shouldMatch: true,
},
{
name: "姓名错误匹配",
err: ErrNameError,
expected: ErrNameError,
shouldMatch: true,
},
{
name: "身份证号错误匹配",
err: ErrIDCardError,
expected: ErrIDCardError,
shouldMatch: true,
},
{
name: "余额不足错误匹配",
err: ErrInsufficientBalance,
expected: ErrInsufficientBalance,
shouldMatch: true,
},
{
name: "用户不存在错误匹配",
err: ErrUserNotExist,
expected: ErrUserNotExist,
shouldMatch: true,
},
{
name: "系统错误匹配",
err: ErrSystemError,
expected: ErrSystemError,
shouldMatch: true,
},
{
name: "不同错误不匹配",
err: ErrPhoneError,
expected: ErrNameError,
shouldMatch: false,
},
{
name: "手机号错误与身份证号错误不匹配",
err: ErrPhoneError,
expected: ErrIDCardError,
shouldMatch: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// 使用 errors.Is() 进行判断
if errors.Is(tc.err, tc.expected) != tc.shouldMatch {
if tc.shouldMatch {
t.Errorf("期望 errors.Is(%v, %v) 返回 true", tc.err, tc.expected)
} else {
t.Errorf("期望 errors.Is(%v, %v) 返回 false", tc.err, tc.expected)
}
}
})
}
}
func TestErrorsIsInSwitch(t *testing.T) {
// 测试在 switch 语句中使用 errors.Is()
// 模拟API调用返回手机号错误
err := ErrPhoneError
// 使用 switch 语句进行错误判断
var result string
switch {
case errors.Is(err, ErrSuccess):
result = "请求成功"
case errors.Is(err, ErrNoRecord):
result = "查询无记录"
case errors.Is(err, ErrPhoneError):
result = "手机号格式错误"
case errors.Is(err, ErrNameError):
result = "姓名格式错误"
case errors.Is(err, ErrIDCardError):
result = "身份证号格式错误"
case errors.Is(err, ErrHeaderParamMissing):
result = "请求头参数缺失"
case errors.Is(err, ErrInsufficientBalance):
result = "余额不足"
case errors.Is(err, ErrUserNotExist):
result = "用户不存在"
case errors.Is(err, ErrUserUnauthorized):
result = "用户未授权"
case errors.Is(err, ErrSystemError):
result = "系统异常"
default:
result = "未知错误"
}
// 验证结果
expected := "手机号格式错误"
if result != expected {
t.Errorf("期望结果 %s实际 %s", expected, result)
}
t.Logf("Switch语句错误判断结果: %s", result)
}
func TestServiceEncryptDecrypt(t *testing.T) {
// 创建测试服务
service := &ZhichaService{
config: ZhichaConfig{
URL: "https://test.com",
AppID: "test_app_id",
AppSecret: "test_app_secret",
EncryptKey: "af4ca0098e6a202a5c08c413ebd9fd62",
},
logger: nil,
}
// 测试数据
testData := "Hello, 智查金控!"
// 测试加密
encrypted, err := service.Encrypt(testData)
if err != nil {
t.Fatalf("加密失败: %v", err)
}
if encrypted == "" {
t.Error("加密结果为空")
}
if encrypted == testData {
t.Error("加密结果与原文相同")
}
t.Logf("原文: %s", testData)
t.Logf("加密后: %s", encrypted)
// 测试解密
decrypted, err := service.Decrypt(encrypted)
if err != nil {
t.Fatalf("解密失败: %v", err)
}
if decrypted != testData {
t.Errorf("解密结果不匹配,期望: %s实际: %s", testData, decrypted)
}
t.Logf("解密后: %s", decrypted)
}
func TestEncryptWithoutKey(t *testing.T) {
// 创建没有加密密钥的服务
service := &ZhichaService{
config: ZhichaConfig{
URL: "https://test.com",
AppID: "test_app_id",
AppSecret: "test_app_secret",
// 没有设置 EncryptKey
},
logger: nil,
}
// 应该返回错误
_, err := service.Encrypt("test data")
if err == nil {
t.Error("没有加密密钥时应该返回错误")
}
if !strings.Contains(err.Error(), "加密密钥未配置") {
t.Errorf("期望错误包含'加密密钥未配置',实际: %v", err)
}
t.Logf("预期的错误: %v", err)
}
func TestDecryptWithoutKey(t *testing.T) {
// 创建没有加密密钥的服务
service := &ZhichaService{
config: ZhichaConfig{
URL: "https://test.com",
AppID: "test_app_id",
AppSecret: "test_app_secret",
// 没有设置 EncryptKey
},
logger: nil,
}
// 应该返回错误
_, err := service.Decrypt("test encrypted data")
if err == nil {
t.Error("没有加密密钥时应该返回错误")
}
if !strings.Contains(err.Error(), "加密密钥未配置") {
t.Errorf("期望错误包含'加密密钥未配置',实际: %v", err)
}
t.Logf("预期的错误: %v", err)
}

View File

@@ -0,0 +1,264 @@
# 通用外部服务日志系统
## 概述
这是一个为外部服务(如 westdex、zhicha、yushan 等)提供统一日志记录功能的通用系统。所有外部服务共享相同的日志基础架构,但保持各自独立的日志文件目录。
## 设计目标
1. **代码复用**: 避免重复的日志实现代码
2. **统一格式**: 所有外部服务使用相同的日志格式
3. **独立存储**: 每个服务的日志存储在独立目录中
4. **灵活配置**: 支持每个服务独立的日志配置
5. **易于扩展**: 新增外部服务时只需简单配置
## 架构特点
### 1. 共享核心
- 统一的日志接口
- 相同的日志格式
- 一致的配置结构
- 共用的文件轮转策略
### 2. 服务分离
- 每个服务有独立的日志目录
- 通过 `service` 字段区分来源
- 可独立配置每个服务的日志参数
- 支持按级别分离日志文件
### 3. 配置灵活
- 支持从配置文件读取
- 支持自定义配置创建
- 支持简单模式(无日志)
- 支持日志级别分离
## 已集成的服务
### 1. WestDex (西部数据)
- 服务名称: `westdex`
- 日志目录: `logs/external_services/westdex/`
- 主要功能: 企业信息查询
### 2. Zhicha (智查金控)
- 服务名称: `zhicha`
- 日志目录: `logs/external_services/zhicha/`
- 主要功能: 企业信息查询、AES加密
### 3. Yushan (羽山)
- 服务名称: `yushan`
- 日志目录: `logs/external_services/yushan/`
- 主要功能: 企业信息查询、AES加密
## 日志格式
所有服务的日志都包含以下标准字段:
```json
{
"level": "INFO",
"timestamp": "2024-01-01T12:00:00Z",
"msg": "服务名 API请求",
"service": "服务名",
"request_id": "服务名_唯一ID",
"api_code": "API代码",
"url": "请求URL",
"params": "请求参数",
"status_code": "响应状态码",
"response": "响应内容",
"duration": "请求耗时",
"error": "错误信息"
}
```
## 配置结构
```yaml
# 外部服务日志根目录
external_services_log_dir: "./logs/external_services"
# 各服务配置
westdex:
logging:
enabled: true
log_dir: "./logs/external_services"
service_name: "westdex"
use_daily: true # 启用按天分隔
enable_level_separation: true # 启用级别分离
level_configs:
info: { max_size: 100, max_backups: 3, max_age: 28, compress: true }
error: { max_size: 200, max_backups: 10, max_age: 90, compress: true }
warn: { max_size: 100, max_backups: 3, max_age: 28, compress: true }
zhicha:
logging:
enabled: true
log_dir: "./logs/external_services"
service_name: "zhicha"
use_daily: true # 启用按天分隔
enable_level_separation: true # 启用级别分离
level_configs:
info: { max_size: 100, max_backups: 3, max_age: 28, compress: true }
error: { max_size: 200, max_backups: 10, max_age: 90, compress: true }
warn: { max_size: 100, max_backups: 3, max_age: 28, compress: true }
yushan:
logging:
enabled: true
log_dir: "./logs/external_services"
service_name: "yushan"
use_daily: true # 启用按天分隔
enable_level_separation: true # 启用级别分离
level_configs:
info: { max_size: 100, max_backups: 3, max_age: 28, compress: true }
error: { max_size: 200, max_backups: 10, max_age: 90, compress: true }
warn: { max_size: 100, max_backups: 3, max_age: 28, compress: true }
```
## 使用方法
### 1. 从配置创建服务
```go
// 推荐方式:从配置文件创建
westdexService, err := westdex.NewWestDexServiceWithConfig(cfg)
zhichaService, err := zhicha.NewZhichaServiceWithConfig(cfg)
yushanService, err := yushan.NewYushanServiceWithConfig(cfg)
```
### 2. 自定义日志配置
```go
loggingConfig := external_logger.ExternalServiceLoggingConfig{
Enabled: true,
LogDir: "./logs/external_services",
ServiceName: "custom_service",
EnableLevelSeparation: true,
// ... 其他配置
}
service := NewCustomServiceWithLogging(url, key, secret, loggingConfig)
```
### 3. 简单模式(无日志)
```go
// 创建无日志的服务实例
service := NewServiceSimple(url, key, secret)
```
## 日志级别
### 1. INFO 级别
- API 请求日志
- API 响应日志
- 一般信息日志
### 2. WARN 级别
- 警告信息
- 非致命错误
### 3. ERROR 级别
- API 调用错误
- 系统异常
- 业务逻辑错误
## 文件轮转策略
### 1. 按大小+时间混合分隔
系统支持两种日志分隔策略:
#### 按天分隔(推荐)
- **UseDaily**: 设置为 `true` 时启用
- 每天创建新的日期目录:`logs/westdex/2024-01-01/`
- 在日期目录下按级别分隔:`westdex_info.log``westdex_error.log``westdex_warn.log`
- 自动清理过期的日期目录
#### 传统方式
- **UseDaily**: 设置为 `false` 时使用
- 直接在服务目录下按级别分隔:`logs/westdex/westdex_info.log`
### 2. 文件轮转配置
每个日志级别都支持以下轮转配置:
- **MaxSize**: 单个文件最大大小MB
- **MaxBackups**: 最大备份文件数
- **MaxAge**: 最大保留天数
- **Compress**: 是否压缩旧文件
### 3. 目录结构示例
```
logs/
├── westdex/
│ ├── 2024-01-01/
│ │ ├── westdex_info.log
│ │ ├── westdex_error.log
│ │ └── westdex_warn.log
│ ├── 2024-01-02/
│ │ ├── westdex_info.log
│ │ ├── westdex_error.log
│ │ └── westdex_warn.log
│ └── westdex_info.log (回退文件)
├── zhicha/
│ ├── 2024-01-01/
│ │ ├── zhicha_info.log
│ │ ├── zhicha_error.log
│ │ └── zhicha_warn.log
│ └── zhicha_info.log (回退文件)
└── yushan/
├── 2024-01-01/
│ ├── yushan_info.log
│ ├── yushan_error.log
│ └── yushan_warn.log
└── yushan_info.log (回退文件)
```
## 扩展新服务
要添加新的外部服务,只需:
1. 在服务中使用 `external_logger.ExternalServiceLogger`
2. 设置合适的 `ServiceName`
3. 使用统一的日志接口
4. 在配置文件中添加相应的日志配置
```go
// 新服务示例
func NewCustomService(config CustomConfig) *CustomService {
loggingConfig := external_logger.ExternalServiceLoggingConfig{
ServiceName: "custom_service",
LogDir: config.LogDir,
// ... 其他配置
}
logger, _ := external_logger.NewExternalServiceLogger(loggingConfig)
return &CustomService{
config: config,
logger: logger,
}
}
```
## 优势总结
1. **维护简便**: 只需维护一套日志代码
2. **格式统一**: 所有服务使用相同的日志格式
3. **配置灵活**: 支持每个服务独立的配置
4. **易于扩展**: 新增服务只需简单配置
5. **性能优化**: 共享的日志基础设施
6. **监控友好**: 统一的日志格式便于监控和分析
7. **智能分隔**: 支持按大小+时间混合分隔策略
8. **自动清理**: 自动清理过期的日志目录,节省磁盘空间
9. **故障回退**: 如果按天分隔失败,自动回退到传统方式
## 注意事项
1. 确保日志目录有足够的磁盘空间
2. 定期清理过期的日志文件
3. 监控日志文件大小,避免磁盘空间不足
4. 在生产环境中建议启用日志压缩
5. 根据业务需求调整日志保留策略
6. 启用按天分隔时,确保系统时间准确
7. 监控自动清理任务的执行情况
8. 建议在生产环境中设置合理的 `MaxAge` 值,避免日志文件过多

View File

@@ -0,0 +1,286 @@
# 通用外部服务日志系统使用示例
## 概述
这个通用的外部服务日志系统允许 westdex 和 zhicha 服务共享相同的日志基础架构,但保持各自独立的日志文件目录。
## 目录结构
使用共享日志系统后,日志目录结构如下:
```
logs/
├── external_services/ # 外部服务日志根目录
│ ├── westdex/ # westdex 服务日志
│ │ ├── westdex_info.log
│ │ ├── westdex_error.log
│ │ └── westdex_warn.log
│ ├── zhicha/ # zhicha 服务日志
│ │ ├── zhicha_info.log
│ │ ├── zhicha_error.log
│ │ └── zhicha_warn.log
│ └── yushan/ # yushan 服务日志
│ ├── yushan_info.log
│ ├── yushan_error.log
│ └── yushan_warn.log
```
## 配置示例
### 1. 在 config.yaml 中配置
```yaml
# 外部服务日志根目录
external_services_log_dir: "./logs/external_services"
# westdex 配置
westdex:
url: "https://api.westdex.com"
key: "your_key"
secret_id: "your_secret_id"
secret_second_id: "your_secret_second_id"
logging:
enabled: true
log_dir: "./logs/external_services" # 使用共享根目录
enable_level_separation: true
level_configs:
info:
max_size: 100
max_backups: 3
max_age: 28
compress: true
error:
max_size: 100
max_backups: 3
max_age: 28
compress: true
warn:
max_size: 100
max_backups: 3
max_age: 28
compress: true
# zhicha 配置
zhicha:
url: "https://www.zhichajinkong.com/dataMiddle/api/handle"
app_id: "your_app_id"
app_secret: "your_app_secret"
pro_id: "your_pro_id"
logging:
enabled: true
log_dir: "./logs/external_services" # 使用共享根目录
enable_level_separation: true
level_configs:
info:
max_size: 100
max_backups: 3
max_age: 28
compress: true
error:
max_size: 100
max_backups: 3
max_age: 28
compress: true
warn:
max_size: 100
max_backups: 3
max_age: 28
compress: true
# yushan 配置
yushan:
url: "https://api.yushan.com"
api_key: "your_api_key"
acct_id: "your_acct_id"
logging:
enabled: true
log_dir: "./logs/external_services" # 使用共享根目录
enable_level_separation: true
level_configs:
info:
max_size: 100
max_backups: 3
max_age: 28
compress: true
error:
max_size: 100
max_backups: 3
max_age: 28
compress: true
warn:
max_size: 100
max_backups: 3
max_age: 28
compress: true
```
## 使用方法
### 1. 创建 WestDex 服务
```go
import "tyapi-server/internal/infrastructure/external/westdex"
// 从配置创建(推荐)
westdexService, err := westdex.NewWestDexServiceWithConfig(cfg)
if err != nil {
log.Fatal(err)
}
// 使用自定义日志配置
loggingConfig := external_logger.ExternalServiceLoggingConfig{
Enabled: true,
LogDir: "./logs/external_services",
ServiceName: "westdex", // 会自动设置
EnableLevelSeparation: true,
// ... 其他配置
}
westdexService, err := westdex.NewWestDexServiceWithLogging(
"url", "key", "secretID", "secretSecondID",
loggingConfig,
)
```
### 2. 创建 Zhicha 服务
```go
import "tyapi-server/internal/infrastructure/external/zhicha"
// 从配置创建(推荐)
zhichaService, err := zhicha.NewZhichaServiceWithConfig(cfg)
if err != nil {
log.Fatal(err)
}
// 使用自定义日志配置
loggingConfig := external_logger.ExternalServiceLoggingConfig{
Enabled: true,
LogDir: "./logs/external_services",
ServiceName: "zhicha", // 会自动设置
EnableLevelSeparation: true,
// ... 其他配置
}
zhichaService, err := zhicha.NewZhichaServiceWithLogging(
"url", "appID", "appSecret", "proID",
loggingConfig,
)
```
### 3. 创建 Yushan 服务
```go
import "tyapi-server/internal/infrastructure/external/yushan"
// 从配置创建(推荐)
yushanService, err := yushan.NewYushanServiceWithConfig(cfg)
if err != nil {
log.Fatal(err)
}
// 使用自定义日志配置
loggingConfig := external_logger.ExternalServiceLoggingConfig{
Enabled: true,
LogDir: "./logs/external_services",
ServiceName: "yushan", // 会自动设置
EnableLevelSeparation: true,
// ... 其他配置
}
yushanService, err := yushan.NewYushanServiceWithLogging(
"url", "apiKey", "acctID",
loggingConfig,
)
```
## 日志内容示例
### WestDex 日志内容
```json
{
"level": "INFO",
"timestamp": "2024-01-01T12:00:00Z",
"msg": "westdex API请求",
"service": "westdex",
"request_id": "westdex_12345678",
"api_code": "G05HZ01",
"url": "https://api.westdex.com/G05HZ01",
"params": {...}
}
```
### Zhicha 日志内容
```json
{
"level": "INFO",
"timestamp": "2024-01-01T12:00:00Z",
"msg": "zhicha API请求",
"service": "zhicha",
"request_id": "zhicha_87654321",
"api_code": "handle",
"url": "https://www.zhichajinkong.com/dataMiddle/api/handle",
"params": {...}
}
```
### Yushan 日志内容
```json
{
"level": "INFO",
"timestamp": "2024-01-01T12:00:00Z",
"msg": "yushan API请求",
"service": "yushan",
"request_id": "yushan_12345678",
"api_code": "G05HZ01",
"url": "https://api.yushan.com",
"params": {...}
}
```
## 优势
### 1. 代码复用
- 相同的日志基础架构
- 统一的日志格式
- 相同的配置结构
### 2. 维护简便
- 只需维护一套日志代码
- 统一的日志级别管理
- 统一的文件轮转策略
### 3. 清晰分离
- 每个服务有独立的日志目录
- 通过 `service` 字段区分来源
- 可独立配置每个服务的日志参数
### 4. 扩展性
- 易于添加新的外部服务
- 统一的日志接口
- 灵活的配置选项
## 添加新的外部服务
要添加新的外部服务(如 TianYanCha只需
1. 在服务中使用 `external_logger.ExternalServiceLogger`
2. 设置合适的 `ServiceName`
3. 使用统一的日志接口
```go
// 新服务示例
func NewTianYanChaService(config TianYanChaConfig) *TianYanChaService {
loggingConfig := external_logger.ExternalServiceLoggingConfig{
ServiceName: "tianyancha",
LogDir: config.LogDir,
// ... 其他配置
}
logger, _ := external_logger.NewExternalServiceLogger(loggingConfig)
return &TianYanChaService{
config: config,
logger: logger,
}
}
```
这样新服务的日志会自动保存到 `logs/external_services/tianyancha/` 目录。

View File

@@ -0,0 +1,315 @@
package external_logger
import (
"fmt"
"os"
"path/filepath"
"time"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2"
)
// ExternalServiceLoggingConfig 外部服务日志配置
type ExternalServiceLoggingConfig struct {
Enabled bool `yaml:"enabled"`
LogDir string `yaml:"log_dir"`
ServiceName string `yaml:"service_name"` // 服务名称,用于区分日志目录
UseDaily bool `yaml:"use_daily"`
EnableLevelSeparation bool `yaml:"enable_level_separation"`
LevelConfigs map[string]ExternalServiceLevelFileConfig `yaml:"level_configs"`
}
// ExternalServiceLevelFileConfig 外部服务级别文件配置
type ExternalServiceLevelFileConfig struct {
MaxSize int `yaml:"max_size"`
MaxBackups int `yaml:"max_backups"`
MaxAge int `yaml:"max_age"`
Compress bool `yaml:"compress"`
}
// ExternalServiceLogger 外部服务日志器
type ExternalServiceLogger struct {
logger *zap.Logger
config ExternalServiceLoggingConfig
serviceName string
}
// NewExternalServiceLogger 创建外部服务日志器
func NewExternalServiceLogger(config ExternalServiceLoggingConfig) (*ExternalServiceLogger, error) {
if !config.Enabled {
return &ExternalServiceLogger{
logger: zap.NewNop(),
serviceName: config.ServiceName,
}, nil
}
// 根据服务名称创建独立的日志目录
serviceLogDir := filepath.Join(config.LogDir, config.ServiceName)
// 确保日志目录存在
if err := os.MkdirAll(serviceLogDir, 0755); err != nil {
return nil, fmt.Errorf("创建日志目录失败: %w", err)
}
// 创建基础配置
zapConfig := zap.NewProductionConfig()
zapConfig.OutputPaths = []string{"stdout"}
zapConfig.ErrorOutputPaths = []string{"stderr"}
// 创建基础logger
baseLogger, err := zapConfig.Build()
if err != nil {
return nil, fmt.Errorf("创建基础logger失败: %w", err)
}
// 如果启用级别分离,创建文件输出
if config.EnableLevelSeparation {
core := createSeparatedCore(serviceLogDir, config)
baseLogger = zap.New(core)
}
// 创建日志器实例
logger := &ExternalServiceLogger{
logger: baseLogger,
config: config,
serviceName: config.ServiceName,
}
// 如果启用按天分隔,启动定时清理任务
if config.UseDaily {
go logger.startCleanupTask()
}
return logger, nil
}
// createSeparatedCore 创建分离的日志核心
func createSeparatedCore(logDir string, config ExternalServiceLoggingConfig) zapcore.Core {
// 创建编码器
encoderConfig := zap.NewProductionEncoderConfig()
encoderConfig.TimeKey = "timestamp"
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
// 创建不同级别的文件输出
infoWriter := createFileWriter(logDir, "info", config.LevelConfigs["info"], config.ServiceName, config.UseDaily)
errorWriter := createFileWriter(logDir, "error", config.LevelConfigs["error"], config.ServiceName, config.UseDaily)
warnWriter := createFileWriter(logDir, "warn", config.LevelConfigs["warn"], config.ServiceName, config.UseDaily)
// 修复:创建真正的级别分离核心
// 使用自定义的LevelEnabler来确保每个Core只处理特定级别的日志
infoCore := zapcore.NewCore(
zapcore.NewJSONEncoder(encoderConfig),
zapcore.AddSync(infoWriter),
&levelEnabler{minLevel: zapcore.InfoLevel, maxLevel: zapcore.InfoLevel}, // 只接受INFO级别
)
errorCore := zapcore.NewCore(
zapcore.NewJSONEncoder(encoderConfig),
zapcore.AddSync(errorWriter),
&levelEnabler{minLevel: zapcore.ErrorLevel, maxLevel: zapcore.ErrorLevel}, // 只接受ERROR级别
)
warnCore := zapcore.NewCore(
zapcore.NewJSONEncoder(encoderConfig),
zapcore.AddSync(warnWriter),
&levelEnabler{minLevel: zapcore.WarnLevel, maxLevel: zapcore.WarnLevel}, // 只接受WARN级别
)
// 使用 zapcore.NewTee 合并核心,现在每个核心只会处理自己级别的日志
return zapcore.NewTee(infoCore, errorCore, warnCore)
}
// levelEnabler 自定义级别过滤器,确保只接受指定级别的日志
type levelEnabler struct {
minLevel zapcore.Level
maxLevel zapcore.Level
}
// Enabled 实现 zapcore.LevelEnabler 接口
func (l *levelEnabler) Enabled(level zapcore.Level) bool {
return level >= l.minLevel && level <= l.maxLevel
}
// createFileWriter 创建文件写入器
func createFileWriter(logDir, level string, config ExternalServiceLevelFileConfig, serviceName string, useDaily bool) *lumberjack.Logger {
// 使用默认配置如果未指定
if config.MaxSize == 0 {
config.MaxSize = 100
}
if config.MaxBackups == 0 {
config.MaxBackups = 3
}
if config.MaxAge == 0 {
config.MaxAge = 28
}
// 构建文件名
var filename string
if useDaily {
// 按天分隔logs/westdex/2024-01-01/westdex_info.log
date := time.Now().Format("2006-01-02")
dateDir := filepath.Join(logDir, date)
// 确保日期目录存在
if err := os.MkdirAll(dateDir, 0755); err != nil {
// 如果创建日期目录失败,回退到根目录
filename = filepath.Join(logDir, fmt.Sprintf("%s_%s.log", serviceName, level))
} else {
filename = filepath.Join(dateDir, fmt.Sprintf("%s_%s.log", serviceName, level))
}
} else {
// 传统方式logs/westdex/westdex_info.log
filename = filepath.Join(logDir, fmt.Sprintf("%s_%s.log", serviceName, level))
}
return &lumberjack.Logger{
Filename: filename,
MaxSize: config.MaxSize,
MaxBackups: config.MaxBackups,
MaxAge: config.MaxAge,
Compress: config.Compress,
}
}
// LogRequest 记录请求日志
func (e *ExternalServiceLogger) LogRequest(requestID, apiCode string, url interface{}, params interface{}) {
e.logger.Info(fmt.Sprintf("%s API请求", e.serviceName),
zap.String("service", e.serviceName),
zap.String("request_id", requestID),
zap.String("api_code", apiCode),
zap.Any("url", url),
zap.Any("params", params),
zap.String("timestamp", time.Now().Format(time.RFC3339)),
)
}
// LogResponse 记录响应日志
func (e *ExternalServiceLogger) LogResponse(requestID, apiCode string, statusCode int, response []byte, duration time.Duration) {
e.logger.Info(fmt.Sprintf("%s API响应", e.serviceName),
zap.String("service", e.serviceName),
zap.String("request_id", requestID),
zap.String("api_code", apiCode),
zap.Int("status_code", statusCode),
zap.String("response", string(response)),
zap.Duration("duration", duration),
zap.String("timestamp", time.Now().Format(time.RFC3339)),
)
}
// LogError 记录错误日志
func (e *ExternalServiceLogger) LogError(requestID, apiCode string, err error, params interface{}) {
e.logger.Error(fmt.Sprintf("%s API错误", e.serviceName),
zap.String("service", e.serviceName),
zap.String("request_id", requestID),
zap.String("api_code", apiCode),
zap.Error(err),
zap.Any("params", params),
zap.String("timestamp", time.Now().Format(time.RFC3339)),
)
}
// LogInfo 记录信息日志
func (e *ExternalServiceLogger) LogInfo(message string, fields ...zap.Field) {
allFields := []zap.Field{zap.String("service", e.serviceName)}
allFields = append(allFields, fields...)
e.logger.Info(message, allFields...)
}
// LogWarn 记录警告日志
func (e *ExternalServiceLogger) LogWarn(message string, fields ...zap.Field) {
allFields := []zap.Field{zap.String("service", e.serviceName)}
allFields = append(allFields, fields...)
e.logger.Warn(message, allFields...)
}
// LogErrorWithFields 记录带字段的错误日志
func (e *ExternalServiceLogger) LogErrorWithFields(message string, fields ...zap.Field) {
allFields := []zap.Field{zap.String("service", e.serviceName)}
allFields = append(allFields, fields...)
e.logger.Error(message, allFields...)
}
// Sync 同步日志
func (e *ExternalServiceLogger) Sync() error {
return e.logger.Sync()
}
// CleanupOldDateDirs 清理过期的日期目录
func (e *ExternalServiceLogger) CleanupOldDateDirs() error {
if !e.config.UseDaily {
return nil
}
logDir := filepath.Join(e.config.LogDir, e.serviceName)
// 读取日志目录
entries, err := os.ReadDir(logDir)
if err != nil {
return fmt.Errorf("读取日志目录失败: %w", err)
}
// 计算过期时间基于配置的MaxAge
maxAge := 28 // 默认28天
if errorConfig, exists := e.config.LevelConfigs["error"]; exists && errorConfig.MaxAge > 0 {
maxAge = errorConfig.MaxAge
}
cutoffTime := time.Now().AddDate(0, 0, -maxAge)
for _, entry := range entries {
if !entry.IsDir() {
continue
}
// 尝试解析目录名为日期
dirName := entry.Name()
dirTime, err := time.Parse("2006-01-02", dirName)
if err != nil {
// 如果不是日期格式的目录,跳过
continue
}
// 检查是否过期
if dirTime.Before(cutoffTime) {
dirPath := filepath.Join(logDir, dirName)
if err := os.RemoveAll(dirPath); err != nil {
return fmt.Errorf("删除过期目录失败 %s: %w", dirPath, err)
}
}
}
return nil
}
// startCleanupTask 启动定时清理任务
func (e *ExternalServiceLogger) startCleanupTask() {
// 每天凌晨2点执行清理
ticker := time.NewTicker(24 * time.Hour)
defer ticker.Stop()
// 等待到下一个凌晨2点
now := time.Now()
next := time.Date(now.Year(), now.Month(), now.Day()+1, 2, 0, 0, 0, now.Location())
time.Sleep(time.Until(next))
// 立即执行一次清理
if err := e.CleanupOldDateDirs(); err != nil {
// 记录清理错误这里使用标准输出因为logger可能还未初始化
fmt.Printf("清理过期日志目录失败: %v\n", err)
}
// 定时执行清理
for range ticker.C {
if err := e.CleanupOldDateDirs(); err != nil {
fmt.Printf("清理过期日志目录失败: %v\n", err)
}
}
}
// GetServiceName 获取服务名称
func (e *ExternalServiceLogger) GetServiceName() string {
return e.serviceName
}

View File

@@ -0,0 +1,147 @@
package logger
import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
// LoggerFactory 日志器工厂 - 基于 Zap 官方推荐
type LoggerFactory struct {
config Config
}
// NewLoggerFactory 创建日志器工厂
func NewLoggerFactory(config Config) *LoggerFactory {
return &LoggerFactory{
config: config,
}
}
// CreateLogger 创建普通日志器
func (f *LoggerFactory) CreateLogger() (Logger, error) {
return NewLogger(f.config)
}
// CreateProductionLogger 创建生产环境日志器 - 使用 Zap 官方推荐
func (f *LoggerFactory) CreateProductionLogger() (*zap.Logger, error) {
// 使用 Zap 官方的生产环境预设
logger, err := zap.NewProduction(
zap.AddCaller(),
zap.AddCallerSkip(1),
zap.AddStacktrace(zapcore.ErrorLevel),
)
if err != nil {
return nil, err
}
// 如果配置为文件输出,需要手动设置 Core
if f.config.Output == "file" {
writeSyncer, err := createFileWriteSyncer(f.config)
if err != nil {
return nil, err
}
// 创建新的 Core 并替换
encoder := getEncoder(f.config.Format, f.config)
level := getLogLevel(f.config.Level)
core := zapcore.NewCore(encoder, writeSyncer, level)
logger = zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1), zap.AddStacktrace(zapcore.ErrorLevel))
}
return logger, nil
}
// CreateDevelopmentLogger 创建开发环境日志器 - 使用 Zap 官方推荐
func (f *LoggerFactory) CreateDevelopmentLogger() (*zap.Logger, error) {
// 使用 Zap 官方的开发环境预设
logger, err := zap.NewDevelopment(
zap.AddCaller(),
zap.AddCallerSkip(1),
zap.AddStacktrace(zapcore.ErrorLevel),
)
if err != nil {
return nil, err
}
// 如果配置为文件输出,需要手动设置 Core
if f.config.Output == "file" {
writeSyncer, err := createFileWriteSyncer(f.config)
if err != nil {
return nil, err
}
// 创建新的 Core 并替换
encoder := getEncoder(f.config.Format, f.config)
level := getLogLevel(f.config.Level)
core := zapcore.NewCore(encoder, writeSyncer, level)
logger = zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1), zap.AddStacktrace(zapcore.ErrorLevel))
}
return logger, nil
}
// CreateCustomLogger 创建自定义配置日志器
func (f *LoggerFactory) CreateCustomLogger() (*zap.Logger, error) {
// 根据环境选择预设
if f.config.Development {
return f.CreateDevelopmentLogger()
}
return f.CreateProductionLogger()
}
// CreateLoggerByEnvironment 根据环境创建合适的日志器
func (f *LoggerFactory) CreateLoggerByEnvironment() (*zap.Logger, error) {
if f.config.Development {
return f.CreateDevelopmentLogger()
}
return f.CreateProductionLogger()
}
// CreateLoggerWithOptions 使用选项模式创建日志器
func (f *LoggerFactory) CreateLoggerWithOptions(options ...LoggerOption) (*zap.Logger, error) {
// 应用选项
for _, option := range options {
option(&f.config)
}
// 创建日志器
return f.CreateLoggerByEnvironment()
}
// LoggerOption 日志器选项函数
type LoggerOption func(*Config)
// WithLevel 设置日志级别
func WithLevel(level string) LoggerOption {
return func(config *Config) {
config.Level = level
}
}
// WithFormat 设置日志格式
func WithFormat(format string) LoggerOption {
return func(config *Config) {
config.Format = format
}
}
// WithOutput 设置输出目标
func WithOutput(output string) LoggerOption {
return func(config *Config) {
config.Output = output
}
}
// WithDevelopment 设置是否为开发环境
func WithDevelopment(development bool) LoggerOption {
return func(config *Config) {
config.Development = development
}
}
// WithColor 设置是否使用彩色输出
func WithColor(useColor bool) LoggerOption {
return func(config *Config) {
config.UseColor = useColor
}
}

View File

@@ -2,8 +2,6 @@ package logger
import (
"context"
"fmt"
"os"
"path/filepath"
"time"
@@ -12,303 +10,219 @@ import (
"gopkg.in/natefinch/lumberjack.v2"
)
// LevelLoggerConfig 按级别分文件日志配置
// LevelLogger 级别分文件日志器 - 基于 Zap 官方推荐
type LevelLogger struct {
logger *zap.Logger
levelLoggers map[zapcore.Level]*zap.Logger
config LevelLoggerConfig
}
// LevelLoggerConfig 级别分文件日志器配置
type LevelLoggerConfig struct {
BaseConfig Config
// 是否启用按级别分文件
BaseConfig Config
EnableLevelSeparation bool
// 各级别日志文件配置
LevelConfigs map[zapcore.Level]LevelFileConfig
LevelConfigs map[zapcore.Level]LevelFileConfig
}
// LevelFileConfig 单个级别文件配置
type LevelFileConfig struct {
MaxSize int // 单个文件最大大小(MB)
MaxBackups int // 最大备份文件数
MaxAge int // 最大保留天数
Compress bool // 是否压缩
MaxSize int `mapstructure:"max_size"`
MaxBackups int `mapstructure:"max_backups"`
MaxAge int `mapstructure:"max_age"`
Compress bool `mapstructure:"compress"`
}
// LevelLogger 级别分文件日志器
type LevelLogger struct {
loggers map[zapcore.Level]*zap.Logger
config LevelLoggerConfig
}
// NewLevelLogger 创建级别分文件日志器
func NewLevelLogger(config LevelLoggerConfig) (Logger, error) {
// 根据环境创建基础日志器
var baseLogger *zap.Logger
var err error
// NewLevelLogger 创建按级别分文件的日志器
func NewLevelLogger(config LevelLoggerConfig) (*LevelLogger, error) {
if !config.EnableLevelSeparation {
// 如果不启用按级别分文件,使用普通日志器
normalLogger, err := NewLogger(config.BaseConfig)
if err != nil {
return nil, err
}
// 转换为LevelLogger格式
zapLogger := normalLogger.(*ZapLogger).GetZapLogger()
return &LevelLogger{
loggers: map[zapcore.Level]*zap.Logger{
zapcore.DebugLevel: zapLogger,
zapcore.InfoLevel: zapLogger,
zapcore.WarnLevel: zapLogger,
zapcore.ErrorLevel: zapLogger,
zapcore.FatalLevel: zapLogger,
zapcore.PanicLevel: zapLogger,
},
config: config,
}, nil
}
// 设置默认级别配置
if config.LevelConfigs == nil {
config.LevelConfigs = getDefaultLevelConfigs()
}
// 确保日志目录存在
if err := os.MkdirAll(config.BaseConfig.LogDir, 0755); err != nil {
return nil, fmt.Errorf("创建日志目录失败: %w", err)
}
// 为每个级别创建独立的日志器
loggers := make(map[zapcore.Level]*zap.Logger)
for level := range config.LevelConfigs {
logger, err := createLevelLogger(level, config)
if err != nil {
return nil, fmt.Errorf("创建级别日志器失败 [%s]: %w", level.String(), err)
}
loggers[level] = logger
}
return &LevelLogger{
loggers: loggers,
config: config,
}, nil
}
// getDefaultLevelConfigs 获取默认级别配置
func getDefaultLevelConfigs() map[zapcore.Level]LevelFileConfig {
return map[zapcore.Level]LevelFileConfig{
zapcore.DebugLevel: {
MaxSize: 50, // 50MB
MaxBackups: 3,
MaxAge: 7, // 7天
Compress: true,
},
zapcore.InfoLevel: {
MaxSize: 100, // 100MB
MaxBackups: 5,
MaxAge: 30, // 30天
Compress: true,
},
zapcore.WarnLevel: {
MaxSize: 100, // 100MB
MaxBackups: 5,
MaxAge: 30, // 30天
Compress: true,
},
zapcore.ErrorLevel: {
MaxSize: 200, // 200MB
MaxBackups: 10,
MaxAge: 90, // 90天
Compress: true,
},
zapcore.FatalLevel: {
MaxSize: 100, // 100MB
MaxBackups: 10,
MaxAge: 365, // 1年
Compress: true,
},
zapcore.PanicLevel: {
MaxSize: 100, // 100MB
MaxBackups: 10,
MaxAge: 365, // 1年
Compress: true,
},
}
}
// createLevelLogger 为单个级别创建日志器
func createLevelLogger(level zapcore.Level, config LevelLoggerConfig) (*zap.Logger, error) {
levelConfig := config.LevelConfigs[level]
// 创建编码器
encoderConfig := getEncoderConfig()
var encoder zapcore.Encoder
if config.BaseConfig.Format == "json" {
encoder = zapcore.NewJSONEncoder(encoderConfig)
if config.BaseConfig.Development {
baseLogger, err = zap.NewDevelopment(
zap.AddCaller(),
zap.AddCallerSkip(1),
zap.AddStacktrace(zapcore.ErrorLevel),
)
} else {
encoder = zapcore.NewConsoleEncoder(encoderConfig)
baseLogger, err = zap.NewProduction(
zap.AddCaller(),
zap.AddCallerSkip(1),
zap.AddStacktrace(zapcore.ErrorLevel),
)
}
// 创建文件输出
writeSyncer, err := createLevelFileWriteSyncer(level, config.BaseConfig, levelConfig)
if err != nil {
return nil, err
}
// 创建核心
core := zapcore.NewCore(encoder, writeSyncer, level)
// 创建级别分文件的日志器
levelLogger := &LevelLogger{
logger: baseLogger,
levelLoggers: make(map[zapcore.Level]*zap.Logger),
config: config,
}
// 创建日志器
logger := zap.New(core, zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel))
// 为每个级别创建专门的日志器
if config.EnableLevelSeparation {
levelLogger.createLevelLoggers()
}
return logger, nil
return levelLogger, nil
}
// createLevelFileWriteSyncer 创建级别文件输出同步
func createLevelFileWriteSyncer(level zapcore.Level, baseConfig Config, levelConfig LevelFileConfig) (zapcore.WriteSyncer, error) {
// 构建日志文件路径
// createLevelLoggers 创建级别的日志
func (l *LevelLogger) createLevelLoggers() {
levels := []zapcore.Level{
zapcore.DebugLevel,
zapcore.InfoLevel,
zapcore.WarnLevel,
zapcore.ErrorLevel,
zapcore.FatalLevel,
zapcore.PanicLevel,
}
for _, level := range levels {
// 获取该级别的配置
levelConfig, exists := l.config.LevelConfigs[level]
if !exists {
// 如果没有配置,使用默认配置
levelConfig = LevelFileConfig{
MaxSize: 100,
MaxBackups: 5,
MaxAge: 30,
Compress: true,
}
}
// 创建该级别的文件输出
writeSyncer := l.createLevelWriteSyncer(level, levelConfig)
// 创建编码器
encoder := getEncoder(l.config.BaseConfig.Format, l.config.BaseConfig)
// 创建 Core
core := zapcore.NewCore(encoder, writeSyncer, level)
// 创建该级别的日志器
levelLogger := zap.New(core,
zap.AddCaller(),
zap.AddCallerSkip(1),
zap.AddStacktrace(zapcore.ErrorLevel),
)
l.levelLoggers[level] = levelLogger
}
}
// createLevelWriteSyncer 创建级别特定的文件输出同步器
func (l *LevelLogger) createLevelWriteSyncer(level zapcore.Level, config LevelFileConfig) zapcore.WriteSyncer {
// 构建文件路径
var logFilePath string
if baseConfig.UseDaily {
// 按日分包logs/2024-01-01/error.log
today := time.Now().Format("2006-01-02")
dailyDir := filepath.Join(baseConfig.LogDir, today)
if err := os.MkdirAll(dailyDir, 0755); err != nil {
return nil, fmt.Errorf("创建日期目录失败: %w", err)
}
logFilePath = filepath.Join(dailyDir, fmt.Sprintf("%s.log", level.String()))
if l.config.BaseConfig.UseDaily {
// 按日分包logs/2024-01-01/debug.log
date := time.Now().Format("2006-01-02")
levelName := level.String()
logFilePath = filepath.Join(l.config.BaseConfig.LogDir, date, levelName+".log")
} else {
// 传统方式logs/error.log
logFilePath = filepath.Join(baseConfig.LogDir, fmt.Sprintf("%s.log", level.String()))
// 传统方式logs/debug.log
levelName := level.String()
logFilePath = filepath.Join(l.config.BaseConfig.LogDir, levelName+".log")
}
// 创建lumberjack日志轮转器
lumberJackLogger := &lumberjack.Logger{
// 创建 lumberjack 日志轮转器
rotator := &lumberjack.Logger{
Filename: logFilePath,
MaxSize: levelConfig.MaxSize,
MaxBackups: levelConfig.MaxBackups,
MaxAge: levelConfig.MaxAge,
Compress: levelConfig.Compress,
MaxSize: config.MaxSize,
MaxBackups: config.MaxBackups,
MaxAge: config.MaxAge,
Compress: config.Compress,
}
return zapcore.AddSync(lumberJackLogger), nil
}
// Debug 调试日志
func (l *LevelLogger) Debug(msg string, fields ...zapcore.Field) {
if logger, exists := l.loggers[zapcore.DebugLevel]; exists {
logger.Debug(msg, fields...)
}
}
// Info 信息日志
func (l *LevelLogger) Info(msg string, fields ...zapcore.Field) {
if logger, exists := l.loggers[zapcore.InfoLevel]; exists {
logger.Info(msg, fields...)
}
}
// Warn 警告日志
func (l *LevelLogger) Warn(msg string, fields ...zapcore.Field) {
if logger, exists := l.loggers[zapcore.WarnLevel]; exists {
logger.Warn(msg, fields...)
}
}
// Error 错误日志
func (l *LevelLogger) Error(msg string, fields ...zapcore.Field) {
if logger, exists := l.loggers[zapcore.ErrorLevel]; exists {
logger.Error(msg, fields...)
}
}
// Fatal 致命错误日志
func (l *LevelLogger) Fatal(msg string, fields ...zapcore.Field) {
if logger, exists := l.loggers[zapcore.FatalLevel]; exists {
logger.Fatal(msg, fields...)
}
}
// Panic 恐慌日志
func (l *LevelLogger) Panic(msg string, fields ...zapcore.Field) {
if logger, exists := l.loggers[zapcore.PanicLevel]; exists {
logger.Panic(msg, fields...)
}
}
// With 添加字段
func (l *LevelLogger) With(fields ...zapcore.Field) Logger {
// 为每个级别创建带字段的日志器
newLoggers := make(map[zapcore.Level]*zap.Logger)
for level, logger := range l.loggers {
newLoggers[level] = logger.With(fields...)
}
return &LevelLogger{
loggers: newLoggers,
config: l.config,
}
}
// WithContext 从上下文添加字段
func (l *LevelLogger) WithContext(ctx context.Context) Logger {
// 从上下文中提取常用字段
fields := []zapcore.Field{}
if traceID := getTraceIDFromContextLevel(ctx); traceID != "" {
fields = append(fields, zap.String("trace_id", traceID))
}
if userID := getUserIDFromContextLevel(ctx); userID != "" {
fields = append(fields, zap.String("user_id", userID))
}
if requestID := getRequestIDFromContextLevel(ctx); requestID != "" {
fields = append(fields, zap.String("request_id", requestID))
}
return l.With(fields...)
}
// Sync 同步日志
func (l *LevelLogger) Sync() error {
var lastErr error
for _, logger := range l.loggers {
if err := logger.Sync(); err != nil {
lastErr = err
}
}
return lastErr
return zapcore.AddSync(rotator)
}
// GetLevelLogger 获取指定级别的日志器
func (l *LevelLogger) GetLevelLogger(level zapcore.Level) *zap.Logger {
if logger, exists := l.loggers[level]; exists {
if logger, exists := l.levelLoggers[level]; exists {
return logger
}
return nil
return l.logger
}
// GetLoggers 获取所有级别的日志器
func (l *LevelLogger) GetLoggers() map[zapcore.Level]*zap.Logger {
return l.loggers
// 实现 Logger 接口
func (l *LevelLogger) Debug(msg string, fields ...zapcore.Field) {
if logger := l.GetLevelLogger(zapcore.DebugLevel); logger != nil {
logger.Debug(msg, fields...)
}
}
// 辅助函数从logger.go复制
func getTraceIDFromContextLevel(ctx context.Context) string {
if traceID := ctx.Value("trace_id"); traceID != nil {
if id, ok := traceID.(string); ok {
return id
func (l *LevelLogger) Info(msg string, fields ...zapcore.Field) {
if logger := l.GetLevelLogger(zapcore.InfoLevel); logger != nil {
logger.Info(msg, fields...)
}
}
func (l *LevelLogger) Warn(msg string, fields ...zapcore.Field) {
if logger := l.GetLevelLogger(zapcore.WarnLevel); logger != nil {
logger.Warn(msg, fields...)
}
}
func (l *LevelLogger) Error(msg string, fields ...zapcore.Field) {
if logger := l.GetLevelLogger(zapcore.ErrorLevel); logger != nil {
logger.Error(msg, fields...)
}
}
func (l *LevelLogger) Fatal(msg string, fields ...zapcore.Field) {
if logger := l.GetLevelLogger(zapcore.FatalLevel); logger != nil {
logger.Fatal(msg, fields...)
}
}
func (l *LevelLogger) Panic(msg string, fields ...zapcore.Field) {
if logger := l.GetLevelLogger(zapcore.PanicLevel); logger != nil {
logger.Panic(msg, fields...)
}
}
func (l *LevelLogger) With(fields ...zapcore.Field) Logger {
// 为所有级别添加字段
for level, logger := range l.levelLoggers {
l.levelLoggers[level] = logger.With(fields...)
}
return l
}
func (l *LevelLogger) WithContext(ctx context.Context) Logger {
// 从上下文提取字段
fields := extractFieldsFromContext(ctx)
return l.With(fields...)
}
func (l *LevelLogger) Named(name string) Logger {
// 为所有级别添加名称
for level, logger := range l.levelLoggers {
l.levelLoggers[level] = logger.Named(name)
}
return l
}
func (l *LevelLogger) Sync() error {
// 同步所有级别的日志器
for _, logger := range l.levelLoggers {
if err := logger.Sync(); err != nil {
return err
}
}
return ""
return l.logger.Sync()
}
func getUserIDFromContextLevel(ctx context.Context) string {
if userID := ctx.Value("user_id"); userID != nil {
if id, ok := userID.(string); ok {
return id
}
}
return ""
func (l *LevelLogger) Core() zapcore.Core {
return l.logger.Core()
}
func getRequestIDFromContextLevel(ctx context.Context) string {
if requestID := ctx.Value("request_id"); requestID != nil {
if id, ok := requestID.(string); ok {
return id
}
}
return ""
func (l *LevelLogger) GetZapLogger() *zap.Logger {
return l.logger
}

View File

@@ -2,8 +2,6 @@ package logger
import (
"context"
"fmt"
"os"
"path/filepath"
"time"
@@ -12,8 +10,9 @@ import (
"gopkg.in/natefinch/lumberjack.v2"
)
// Logger 日志接口
// Logger 日志接口 - 基于 Zap 官方推荐
type Logger interface {
// 基础日志方法
Debug(msg string, fields ...zapcore.Field)
Info(msg string, fields ...zapcore.Field)
Warn(msg string, fields ...zapcore.Field)
@@ -21,271 +20,303 @@ type Logger interface {
Fatal(msg string, fields ...zapcore.Field)
Panic(msg string, fields ...zapcore.Field)
// 结构化日志方法
With(fields ...zapcore.Field) Logger
WithContext(ctx context.Context) Logger
Named(name string) Logger
// 同步和清理
Sync() error
Core() zapcore.Core
// 获取原生 Zap Logger用于高级功能
GetZapLogger() *zap.Logger
}
// ZapLogger Zap日志实现
// Config 日志配置 - 基于 Zap 官方配置结构
type Config struct {
// 基础配置
Level string `mapstructure:"level"` // 日志级别
Format string `mapstructure:"format"` // 输出格式 (json/console)
Output string `mapstructure:"output"` // 输出方式 (stdout/stderr/file)
LogDir string `mapstructure:"log_dir"` // 日志目录
UseDaily bool `mapstructure:"use_daily"` // 是否按日分包
UseColor bool `mapstructure:"use_color"` // 是否使用彩色输出仅console格式
// 文件配置
MaxSize int `mapstructure:"max_size"` // 单个文件最大大小(MB)
MaxBackups int `mapstructure:"max_backups"` // 最大备份文件数
MaxAge int `mapstructure:"max_age"` // 最大保留天数
Compress bool `mapstructure:"compress"` // 是否压缩
// 高级功能
EnableLevelSeparation bool `mapstructure:"enable_level_separation"` // 是否启用按级别分文件
LevelConfigs map[string]interface{} `mapstructure:"level_configs"` // 各级别配置(使用 interface{} 避免循环依赖)
EnableRequestLogging bool `mapstructure:"enable_request_logging"` // 是否启用请求日志
EnablePerformanceLog bool `mapstructure:"enable_performance_log"` // 是否启用性能日志
// 开发环境配置
Development bool `mapstructure:"development"` // 是否为开发环境
Sampling bool `mapstructure:"sampling"` // 是否启用采样
}
// ZapLogger Zap日志实现 - 基于官方推荐
type ZapLogger struct {
logger *zap.Logger
}
// Config 日志配置
type Config struct {
Level string
Format string
Output string
LogDir string // 日志目录
MaxSize int // 单个文件最大大小(MB)
MaxBackups int // 最大备份文件数
MaxAge int // 最大保留天数
Compress bool // 是否压缩
UseDaily bool // 是否按日分包
}
// NewLogger 创建新的日志实例
// NewLogger 创建新的日志实例 - 使用 Zap 官方推荐的方式
func NewLogger(config Config) (Logger, error) {
// 设置日志级别
level, err := zapcore.ParseLevel(config.Level)
var logger *zap.Logger
var err error
// 根据环境创建合适的日志器
if config.Development {
logger, err = zap.NewDevelopment(
zap.AddCaller(),
zap.AddCallerSkip(1),
zap.AddStacktrace(zapcore.ErrorLevel),
)
} else {
logger, err = zap.NewProduction(
zap.AddCaller(),
zap.AddCallerSkip(1),
zap.AddStacktrace(zapcore.ErrorLevel),
)
}
if err != nil {
return nil, fmt.Errorf("无效的日志级别: %w", err)
return nil, err
}
// 配置编码器
var encoder zapcore.Encoder
encoderConfig := getEncoderConfig()
switch config.Format {
case "json":
encoder = zapcore.NewJSONEncoder(encoderConfig)
case "console":
encoder = zapcore.NewConsoleEncoder(encoderConfig)
default:
encoder = zapcore.NewJSONEncoder(encoderConfig)
}
// 配置输出
var writeSyncer zapcore.WriteSyncer
switch config.Output {
case "stdout":
writeSyncer = zapcore.AddSync(os.Stdout)
case "stderr":
writeSyncer = zapcore.AddSync(os.Stderr)
case "file":
writeSyncer, err = createFileWriteSyncer(config)
// 如果配置为文件输出,需要手动设置 Core
if config.Output == "file" {
writeSyncer, err := createFileWriteSyncer(config)
if err != nil {
return nil, fmt.Errorf("创建文件输出失败: %w", err)
return nil, err
}
default:
writeSyncer = zapcore.AddSync(os.Stdout)
// 创建新的 Core 并替换
encoder := getEncoder(config.Format, config)
level := getLogLevel(config.Level)
core := zapcore.NewCore(encoder, writeSyncer, level)
logger = zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1), zap.AddStacktrace(zapcore.ErrorLevel))
}
// 创建核心
core := zapcore.NewCore(encoder, writeSyncer, level)
// 创建logger
logger := zap.New(core, zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel))
return &ZapLogger{
logger: logger,
}, nil
}
// createFileWriteSyncer 创建文件输出同步器
func createFileWriteSyncer(config Config) (zapcore.WriteSyncer, error) {
// 设置默认日志目录
if config.LogDir == "" {
config.LogDir = "logs"
}
// 实现 Logger 接口
func (z *ZapLogger) Debug(msg string, fields ...zapcore.Field) {
z.logger.Debug(msg, fields...)
}
// 确保日志目录存在
if err := os.MkdirAll(config.LogDir, 0755); err != nil {
return nil, fmt.Errorf("创建日志目录失败: %w", err)
}
func (z *ZapLogger) Info(msg string, fields ...zapcore.Field) {
z.logger.Info(msg, fields...)
}
// 设置默认值
if config.MaxSize == 0 {
config.MaxSize = 100 // 默认100MB
}
if config.MaxBackups == 0 {
config.MaxBackups = 3 // 默认3个备份
}
if config.MaxAge == 0 {
config.MaxAge = 7 // 默认7天
}
func (z *ZapLogger) Warn(msg string, fields ...zapcore.Field) {
z.logger.Warn(msg, fields...)
}
// 构建日志文件路径
var logFilePath string
if config.UseDaily {
// 按日分包logs/2024-01-01/app.log
today := time.Now().Format("2006-01-02")
dailyDir := filepath.Join(config.LogDir, today)
if err := os.MkdirAll(dailyDir, 0755); err != nil {
return nil, fmt.Errorf("创建日期目录失败: %w", err)
}
logFilePath = filepath.Join(dailyDir, "app.log")
func (z *ZapLogger) Error(msg string, fields ...zapcore.Field) {
z.logger.Error(msg, fields...)
}
func (z *ZapLogger) Fatal(msg string, fields ...zapcore.Field) {
z.logger.Fatal(msg, fields...)
}
func (z *ZapLogger) Panic(msg string, fields ...zapcore.Field) {
z.logger.Panic(msg, fields...)
}
func (z *ZapLogger) With(fields ...zapcore.Field) Logger {
return &ZapLogger{logger: z.logger.With(fields...)}
}
func (z *ZapLogger) WithContext(ctx context.Context) Logger {
// 从上下文提取字段
fields := extractFieldsFromContext(ctx)
return &ZapLogger{logger: z.logger.With(fields...)}
}
func (z *ZapLogger) Named(name string) Logger {
return &ZapLogger{logger: z.logger.Named(name)}
}
func (z *ZapLogger) Sync() error {
return z.logger.Sync()
}
func (z *ZapLogger) Core() zapcore.Core {
return z.logger.Core()
}
func (z *ZapLogger) GetZapLogger() *zap.Logger {
return z.logger
}
// 全局日志器 - 基于 Zap 官方推荐
var globalLogger *zap.Logger
// InitGlobalLogger 初始化全局日志器
func InitGlobalLogger(config Config) error {
var logger *zap.Logger
var err error
// 根据环境创建合适的日志器
if config.Development {
logger, err = zap.NewDevelopment(
zap.AddCaller(),
zap.AddCallerSkip(1),
zap.AddStacktrace(zapcore.ErrorLevel),
)
} else {
// 传统方式logs/app.log
logFilePath = filepath.Join(config.LogDir, "app.log")
logger, err = zap.NewProduction(
zap.AddCaller(),
zap.AddCallerSkip(1),
zap.AddStacktrace(zapcore.ErrorLevel),
)
}
// 创建lumberjack日志轮转器
lumberJackLogger := &lumberjack.Logger{
Filename: logFilePath,
MaxSize: config.MaxSize, // 单个文件最大大小(MB)
MaxBackups: config.MaxBackups, // 最大备份文件数
MaxAge: config.MaxAge, // 最大保留天数
Compress: config.Compress, // 是否压缩
if err != nil {
return err
}
return zapcore.AddSync(lumberJackLogger), nil
}
// getEncoderConfig 获取编码器配置
func getEncoderConfig() zapcore.EncoderConfig {
return zapcore.EncoderConfig{
TimeKey: "timestamp",
LevelKey: "level",
NameKey: "logger",
CallerKey: "caller",
FunctionKey: zapcore.OmitKey,
MessageKey: "message",
StacktraceKey: "stacktrace",
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: zapcore.LowercaseLevelEncoder,
EncodeTime: zapcore.ISO8601TimeEncoder,
EncodeDuration: zapcore.StringDurationEncoder,
EncodeCaller: zapcore.ShortCallerEncoder,
}
}
// Debug 调试日志
func (l *ZapLogger) Debug(msg string, fields ...zapcore.Field) {
l.logger.Debug(msg, fields...)
}
// Info 信息日志
func (l *ZapLogger) Info(msg string, fields ...zapcore.Field) {
l.logger.Info(msg, fields...)
}
// Warn 警告日志
func (l *ZapLogger) Warn(msg string, fields ...zapcore.Field) {
l.logger.Warn(msg, fields...)
}
// Error 错误日志
func (l *ZapLogger) Error(msg string, fields ...zapcore.Field) {
l.logger.Error(msg, fields...)
}
// Fatal 致命错误日志
func (l *ZapLogger) Fatal(msg string, fields ...zapcore.Field) {
l.logger.Fatal(msg, fields...)
}
// Panic 恐慌日志
func (l *ZapLogger) Panic(msg string, fields ...zapcore.Field) {
l.logger.Panic(msg, fields...)
}
// With 添加字段
func (l *ZapLogger) With(fields ...zapcore.Field) Logger {
return &ZapLogger{
logger: l.logger.With(fields...),
}
}
// WithContext 从上下文添加字段
func (l *ZapLogger) WithContext(ctx context.Context) Logger {
// 从上下文中提取常用字段
fields := []zapcore.Field{}
if traceID := getTraceIDFromContext(ctx); traceID != "" {
fields = append(fields, zap.String("trace_id", traceID))
}
if userID := getUserIDFromContext(ctx); userID != "" {
fields = append(fields, zap.String("user_id", userID))
}
if requestID := getRequestIDFromContext(ctx); requestID != "" {
fields = append(fields, zap.String("request_id", requestID))
}
return l.With(fields...)
}
// Sync 同步日志
func (l *ZapLogger) Sync() error {
return l.logger.Sync()
}
// GetZapLogger 获取内部的zap.Logger实例
func (l *ZapLogger) GetZapLogger() *zap.Logger {
return l.logger
}
// getTraceIDFromContext 从上下文获取追踪ID
func getTraceIDFromContext(ctx context.Context) string {
if traceID := ctx.Value("trace_id"); traceID != nil {
if id, ok := traceID.(string); ok {
return id
// 如果配置为文件输出,需要手动设置 Core
if config.Output == "file" {
writeSyncer, err := createFileWriteSyncer(config)
if err != nil {
return err
}
// 创建新的 Core 并替换
encoder := getEncoder(config.Format, config)
level := getLogLevel(config.Level)
core := zapcore.NewCore(encoder, writeSyncer, level)
logger = zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1), zap.AddStacktrace(zapcore.ErrorLevel))
}
return ""
// 替换全局日志器
zap.ReplaceGlobals(logger)
globalLogger = logger
return nil
}
// getUserIDFromContext 从上下文获取用户ID
func getUserIDFromContext(ctx context.Context) string {
if userID := ctx.Value("user_id"); userID != nil {
if id, ok := userID.(string); ok {
return id
}
// GetGlobalLogger 获取全局日志器
func GetGlobalLogger() *zap.Logger {
if globalLogger == nil {
// 如果没有初始化,使用默认的生产环境配置
globalLogger = zap.Must(zap.NewProduction())
}
return ""
return globalLogger
}
// getRequestIDFromContext 从上下文获取请求ID
func getRequestIDFromContext(ctx context.Context) string {
// L 获取全局日志器Zap 官方推荐的方式)
func L() *zap.Logger {
return zap.L()
}
// 辅助函数
func getLogLevel(level string) zapcore.Level {
switch level {
case "debug":
return zapcore.DebugLevel
case "info":
return zapcore.InfoLevel
case "warn":
return zapcore.WarnLevel
case "error":
return zapcore.ErrorLevel
case "fatal":
return zapcore.FatalLevel
case "panic":
return zapcore.PanicLevel
default:
return zapcore.InfoLevel
}
}
func getEncoder(format string, config Config) zapcore.Encoder {
encoderConfig := getEncoderConfig(config)
if format == "console" {
return zapcore.NewConsoleEncoder(encoderConfig)
}
return zapcore.NewJSONEncoder(encoderConfig)
}
func getEncoderConfig(config Config) zapcore.EncoderConfig {
encoderConfig := zap.NewProductionEncoderConfig()
if config.Development {
encoderConfig = zap.NewDevelopmentEncoderConfig()
}
// 自定义时间格式
encoderConfig.TimeKey = "timestamp"
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
// 自定义级别格式
encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
// 自定义调用者格式
encoderConfig.EncodeCaller = zapcore.ShortCallerEncoder
return encoderConfig
}
func createFileWriteSyncer(config Config) (zapcore.WriteSyncer, error) {
// 使用 lumberjack 进行日志轮转
rotator := &lumberjack.Logger{
Filename: getLogFilePath(config),
MaxSize: config.MaxSize,
MaxBackups: config.MaxBackups,
MaxAge: config.MaxAge,
Compress: config.Compress,
}
return zapcore.AddSync(rotator), nil
}
func getLogFilePath(config Config) string {
if config.UseDaily {
// 按日期分包
date := time.Now().Format("2006-01-02")
return filepath.Join(config.LogDir, date, "app.log")
}
return filepath.Join(config.LogDir, "app.log")
}
func extractFieldsFromContext(ctx context.Context) []zapcore.Field {
var fields []zapcore.Field
// 提取请求ID
if requestID := ctx.Value("request_id"); requestID != nil {
if id, ok := requestID.(string); ok {
return id
fields = append(fields, zap.String("request_id", id))
}
}
return ""
}
// Field 创建日志字段的便捷函数
func String(key, val string) zapcore.Field {
return zap.String(key, val)
}
// 提取用户ID
if userID := ctx.Value("user_id"); userID != nil {
if id, ok := userID.(string); ok {
fields = append(fields, zap.String("user_id", id))
}
}
func Int(key string, val int) zapcore.Field {
return zap.Int(key, val)
}
// 提取跟踪ID
if traceID := ctx.Value("trace_id"); traceID != nil {
if id, ok := traceID.(string); ok {
fields = append(fields, zap.String("trace_id", id))
}
}
func Int64(key string, val int64) zapcore.Field {
return zap.Int64(key, val)
}
func Float64(key string, val float64) zapcore.Field {
return zap.Float64(key, val)
}
func Bool(key string, val bool) zapcore.Field {
return zap.Bool(key, val)
}
func Error(err error) zapcore.Field {
return zap.Error(err)
}
func Any(key string, val interface{}) zapcore.Field {
return zap.Any(key, val)
}
func Duration(key string, val interface{}) zapcore.Field {
return zap.Any(key, val)
return fields
}