add JRZQ09J8、FLXGDEA8、FLXGDEA9、JRZQ1D09
add external_services log
This commit is contained in:
@@ -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架构规范,是一个优秀的架构实现。
|
||||
@@ -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的向后兼容性。
|
||||
|
||||
所有更改都经过了编译测试,确保没有引入任何错误。这为后续的开发工作奠定了良好的基础。
|
||||
228
config.yaml
228
config.yaml
@@ -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
|
||||
|
||||
@@ -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"
|
||||
@@ -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:
|
||||
|
||||
453
docs/Zap官方最佳实践日志系统指南.md
Normal file
453
docs/Zap官方最佳实践日志系统指南.md
Normal 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)
|
||||
340
docs/新日志系统使用指南.md
Normal file
340
docs/新日志系统使用指南.md
Normal 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 官方文档实现,确保最佳实践
|
||||
|
||||
使用新的日志系统,您将获得更好的性能、更清晰的日志结构和更强大的功能!
|
||||
321
docs/日志系统配置示例.md
Normal file
321
docs/日志系统配置示例.md
Normal 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. **易于维护**: 配置结构清晰,易于理解和修改
|
||||
|
||||
通过合理的配置,您可以获得高性能、结构化的日志系统,满足开发、测试和生产环境的各种需求!
|
||||
383
docs/西部数据日志系统使用指南.md
Normal file
383
docs/西部数据日志系统使用指南.md
Normal 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使用模式和趋势
|
||||
- 提高问题排查效率
|
||||
|
||||
现在您的西部数据服务已经具备了完整的日志记录能力!🚀
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 天眼查配置
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,在调用时设置
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
// 如果有返回内容,优先解析返回内容
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
// 如果有返回内容,优先解析返回内容
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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, ¶msDto); 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
|
||||
}
|
||||
@@ -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, ¶msDto); 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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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, ¶msDto); 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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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, ¶msDto); 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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
63
internal/infrastructure/external/westdex/westdex_factory.go
vendored
Normal file
63
internal/infrastructure/external/westdex/westdex_factory.go
vendored
Normal 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
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
67
internal/infrastructure/external/yushan/yushan_factory.go
vendored
Normal file
67
internal/infrastructure/external/yushan/yushan_factory.go
vendored
Normal 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)
|
||||
}
|
||||
@@ -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位的随机字符串订单号
|
||||
|
||||
83
internal/infrastructure/external/yushan/yushan_test.go
vendored
Normal file
83
internal/infrastructure/external/yushan/yushan_test.go
vendored
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
121
internal/infrastructure/external/zhicha/crypto.go
vendored
Normal file
121
internal/infrastructure/external/zhicha/crypto.go
vendored
Normal 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
|
||||
}
|
||||
170
internal/infrastructure/external/zhicha/zhicha_errors.go
vendored
Normal file
170
internal/infrastructure/external/zhicha/zhicha_errors.go
vendored
Normal 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
|
||||
}
|
||||
68
internal/infrastructure/external/zhicha/zhicha_factory.go
vendored
Normal file
68
internal/infrastructure/external/zhicha/zhicha_factory.go
vendored
Normal 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)
|
||||
}
|
||||
318
internal/infrastructure/external/zhicha/zhicha_service.go
vendored
Normal file
318
internal/infrastructure/external/zhicha/zhicha_service.go
vendored
Normal 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
|
||||
}
|
||||
698
internal/infrastructure/external/zhicha/zhicha_test.go
vendored
Normal file
698
internal/infrastructure/external/zhicha/zhicha_test.go
vendored
Normal 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)
|
||||
}
|
||||
264
internal/shared/external_logger/README.md
Normal file
264
internal/shared/external_logger/README.md
Normal 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` 值,避免日志文件过多
|
||||
|
||||
286
internal/shared/external_logger/example_usage.md
Normal file
286
internal/shared/external_logger/example_usage.md
Normal 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/` 目录。
|
||||
315
internal/shared/external_logger/external_logger.go
Normal file
315
internal/shared/external_logger/external_logger.go
Normal 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
|
||||
}
|
||||
147
internal/shared/logger/factory.go
Normal file
147
internal/shared/logger/factory.go
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user