From 267ff92998dc57923739138ef7bb022338223b53 Mon Sep 17 00:00:00 2001 From: liangzai <2440983361@qq.com> Date: Mon, 25 Aug 2025 15:44:06 +0800 Subject: [PATCH] =?UTF-8?q?add=20JRZQ09J8=E3=80=81FLXGDEA8=E3=80=81FLXGDEA?= =?UTF-8?q?9=E3=80=81JRZQ1D09=20add=20external=5Fservices=20log?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DDD规范企业认证信息自动填充实现总结.md | 288 -------- Handler请求绑定方式更新总结.md | 171 ----- config.yaml | 238 ++++-- configs/env.development.yaml | 19 +- configs/env.production.yaml | 66 +- docs/Zap官方最佳实践日志系统指南.md | 453 ++++++++++++ docs/新日志系统使用指南.md | 340 +++++++++ docs/日志系统配置示例.md | 321 ++++++++ docs/西部数据日志系统使用指南.md | 383 ++++++++++ .../api/api_application_service.go | 6 +- .../product_application_service_impl.go | 23 + internal/config/config.go | 169 +++-- internal/container/container.go | 72 +- internal/domains/api/dto/api_request_dto.go | 27 + .../api/services/api_request_service.go | 8 +- .../api/services/processors/dependencies.go | 4 + .../processors/flxg/flxg0687_processor.go | 2 +- .../processors/flxg/flxg0v3b_processor.go | 2 +- .../processors/flxg/flxg0v4b_processor.go | 2 +- .../processors/flxg/flxg162a_processor.go | 2 +- .../processors/flxg/flxg3d56_processor.go | 2 +- .../processors/flxg/flxg54f5_processor.go | 2 +- .../processors/flxg/flxg5876_processor.go | 2 +- .../processors/flxg/flxg5b2e_processor.go | 2 +- .../processors/flxg/flxg75fe_processor.go | 2 +- .../processors/flxg/flxg8a3f_processor.go | 2 +- .../processors/flxg/flxg9687_processor.go | 2 +- .../processors/flxg/flxg970f_processor.go | 2 +- .../processors/flxg/flxgbc21_processor.go | 2 +- .../processors/flxg/flxgc9d1_processor.go | 2 +- .../processors/flxg/flxgca3d_processor.go | 2 +- .../processors/flxg/flxgdea8_processor.go | 57 ++ .../processors/flxg/flxgdea9_processor.go | 56 ++ .../processors/flxg/flxgdec7_processor.go | 2 +- .../processors/ivyz/ivyz0b03_processor.go | 2 +- .../processors/ivyz/ivyz1c9d_processor.go | 2 +- .../processors/ivyz/ivyz2125_processor.go | 2 +- .../processors/ivyz/ivyz385e_processor.go | 2 +- .../processors/ivyz/ivyz4e8b_processor.go | 2 +- .../processors/ivyz/ivyz5733_processor.go | 2 +- .../processors/ivyz/ivyz7f2a_processor.go | 2 +- .../processors/ivyz/ivyz9363_processor.go | 2 +- .../processors/ivyz/ivyz9a2b_processor.go | 2 +- .../processors/ivyz/ivyzadee_processor.go | 2 +- .../processors/ivyz/ivyzgz08_processor.go | 2 +- .../processors/jrzq/jrzq09j8_processor.go | 63 ++ .../processors/jrzq/jrzq0a03_processor.go | 2 +- .../processors/jrzq/jrzq1d09_processor.go | 63 ++ .../processors/jrzq/jrzq4aa8_processor.go | 2 +- .../processors/jrzq/jrzq8203_processor.go | 2 +- .../processors/jrzq/jrzqdcbe_processor.go | 2 +- .../processors/qcxg/qcxg7a2b_processor.go | 2 +- .../processors/qygl/qygl2acd_processor.go | 2 +- .../processors/qygl/qygl45bd_processor.go | 2 +- .../processors/qygl/qygl6f2d_processor.go | 2 +- .../processors/qygl/qygl8261_processor.go | 2 +- .../processors/qygl/qygl8271_processor.go | 2 +- .../processors/qygl/qyglb4c0_processor.go | 2 +- .../processors/yysy/yysy09cd_processor.go | 2 +- .../processors/yysy/yysy4b21_processor.go | 2 +- .../processors/yysy/yysy4b37_processor.go | 2 +- .../processors/yysy/yysy6f2e_processor.go | 2 +- .../processors/yysy/yysyd50f_processor.go | 2 +- .../processors/yysy/yysyf7db_processor.go | 2 +- .../external/westdex/westdex_factory.go | 63 ++ .../external/westdex/westdex_service.go | 268 ++++++- .../external/yushan/yushan_factory.go | 67 ++ .../external/yushan/yushan_service.go | 105 ++- .../external/yushan/yushan_test.go | 83 +++ .../infrastructure/external/zhicha/crypto.go | 121 +++ .../external/zhicha/zhicha_errors.go | 170 +++++ .../external/zhicha/zhicha_factory.go | 68 ++ .../external/zhicha/zhicha_service.go | 318 ++++++++ .../external/zhicha/zhicha_test.go | 698 ++++++++++++++++++ internal/shared/external_logger/README.md | 264 +++++++ .../shared/external_logger/example_usage.md | 286 +++++++ .../shared/external_logger/external_logger.go | 315 ++++++++ internal/shared/logger/factory.go | 147 ++++ internal/shared/logger/level_logger.go | 432 +++++------ internal/shared/logger/logger.go | 489 ++++++------ 80 files changed, 5555 insertions(+), 1254 deletions(-) delete mode 100644 DDD规范企业认证信息自动填充实现总结.md delete mode 100644 Handler请求绑定方式更新总结.md create mode 100644 docs/Zap官方最佳实践日志系统指南.md create mode 100644 docs/新日志系统使用指南.md create mode 100644 docs/日志系统配置示例.md create mode 100644 docs/西部数据日志系统使用指南.md create mode 100644 internal/domains/api/services/processors/flxg/flxgdea8_processor.go create mode 100644 internal/domains/api/services/processors/flxg/flxgdea9_processor.go create mode 100644 internal/domains/api/services/processors/jrzq/jrzq09j8_processor.go create mode 100644 internal/domains/api/services/processors/jrzq/jrzq1d09_processor.go create mode 100644 internal/infrastructure/external/westdex/westdex_factory.go create mode 100644 internal/infrastructure/external/yushan/yushan_factory.go create mode 100644 internal/infrastructure/external/yushan/yushan_test.go create mode 100644 internal/infrastructure/external/zhicha/crypto.go create mode 100644 internal/infrastructure/external/zhicha/zhicha_errors.go create mode 100644 internal/infrastructure/external/zhicha/zhicha_factory.go create mode 100644 internal/infrastructure/external/zhicha/zhicha_service.go create mode 100644 internal/infrastructure/external/zhicha/zhicha_test.go create mode 100644 internal/shared/external_logger/README.md create mode 100644 internal/shared/external_logger/example_usage.md create mode 100644 internal/shared/external_logger/external_logger.go create mode 100644 internal/shared/logger/factory.go diff --git a/DDD规范企业认证信息自动填充实现总结.md b/DDD规范企业认证信息自动填充实现总结.md deleted file mode 100644 index 2a0ce11..0000000 --- a/DDD规范企业认证信息自动填充实现总结.md +++ /dev/null @@ -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架构规范,是一个优秀的架构实现。 \ No newline at end of file diff --git a/Handler请求绑定方式更新总结.md b/Handler请求绑定方式更新总结.md deleted file mode 100644 index 4a9d3fb..0000000 --- a/Handler请求绑定方式更新总结.md +++ /dev/null @@ -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的向后兼容性。 - -所有更改都经过了编译测试,确保没有引入任何错误。这为后续的开发工作奠定了良好的基础。 \ No newline at end of file diff --git a/config.yaml b/config.yaml index bd7ea91..3a8cc60 100644 --- a/config.yaml +++ b/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_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_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_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_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_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 diff --git a/configs/env.development.yaml b/configs/env.development.yaml index b3d2754..cc352db 100644 --- a/configs/env.development.yaml +++ b/configs/env.development.yaml @@ -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" \ No newline at end of file diff --git a/configs/env.production.yaml b/configs/env.production.yaml index c256ea6..b0d53b2 100644 --- a/configs/env.production.yaml +++ b/configs/env.production.yaml @@ -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: diff --git a/docs/Zap官方最佳实践日志系统指南.md b/docs/Zap官方最佳实践日志系统指南.md new file mode 100644 index 0000000..788f6d8 --- /dev/null +++ b/docs/Zap官方最佳实践日志系统指南.md @@ -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) diff --git a/docs/新日志系统使用指南.md b/docs/新日志系统使用指南.md new file mode 100644 index 0000000..336ca44 --- /dev/null +++ b/docs/新日志系统使用指南.md @@ -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 官方文档实现,确保最佳实践 + +使用新的日志系统,您将获得更好的性能、更清晰的日志结构和更强大的功能! diff --git a/docs/日志系统配置示例.md b/docs/日志系统配置示例.md new file mode 100644 index 0000000..4747f19 --- /dev/null +++ b/docs/日志系统配置示例.md @@ -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. **易于维护**: 配置结构清晰,易于理解和修改 + +通过合理的配置,您可以获得高性能、结构化的日志系统,满足开发、测试和生产环境的各种需求! diff --git a/docs/西部数据日志系统使用指南.md b/docs/西部数据日志系统使用指南.md new file mode 100644 index 0000000..e944148 --- /dev/null +++ b/docs/西部数据日志系统使用指南.md @@ -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使用模式和趋势 +- 提高问题排查效率 + +现在您的西部数据服务已经具备了完整的日志记录能力!🚀 diff --git a/internal/application/api/api_application_service.go b/internal/application/api/api_application_service.go index 51d5fd0..ea04a40 100644 --- a/internal/application/api/api_application_service.go +++ b/internal/application/api/api_application_service.go @@ -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)) diff --git a/internal/application/product/product_application_service_impl.go b/internal/application/product/product_application_service_impl.go index 4836a6a..9cb853f 100644 --- a/internal/application/product/product_application_service_impl.go +++ b/internal/application/product/product_application_service_impl.go @@ -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) } diff --git a/internal/config/config.go b/internal/config/config.go index 8d732ef..0965899 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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 天眼查配置 diff --git a/internal/container/container.go b/internal/container/container.go index 2d026a5..8ba7ff5 100644 --- a/internal/container/container.go +++ b/internal/container/container.go @@ -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{ diff --git a/internal/domains/api/dto/api_request_dto.go b/internal/domains/api/dto/api_request_dto.go index 0f8a483..ca6062d 100644 --- a/internal/domains/api/dto/api_request_dto.go +++ b/internal/domains/api/dto/api_request_dto.go @@ -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"` +} diff --git a/internal/domains/api/services/api_request_service.go b/internal/domains/api/services/api_request_service.go index 5933065..85b5543 100644 --- a/internal/domains/api/services/api_request_service.go +++ b/internal/domains/api/services/api_request_service.go @@ -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, diff --git a/internal/domains/api/services/processors/dependencies.go b/internal/domains/api/services/processors/dependencies.go index 2be9d08..843e6b6 100644 --- a/internal/domains/api/services/processors/dependencies.go +++ b/internal/domains/api/services/processors/dependencies.go @@ -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,在调用时设置 diff --git a/internal/domains/api/services/processors/flxg/flxg0687_processor.go b/internal/domains/api/services/processors/flxg/flxg0687_processor.go index 68dce5f..d3d61d4 100644 --- a/internal/domains/api/services/processors/flxg/flxg0687_processor.go +++ b/internal/domains/api/services/processors/flxg/flxg0687_processor.go @@ -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) diff --git a/internal/domains/api/services/processors/flxg/flxg0v3b_processor.go b/internal/domains/api/services/processors/flxg/flxg0v3b_processor.go index 53166a6..870803b 100644 --- a/internal/domains/api/services/processors/flxg/flxg0v3b_processor.go +++ b/internal/domains/api/services/processors/flxg/flxg0v3b_processor.go @@ -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) diff --git a/internal/domains/api/services/processors/flxg/flxg0v4b_processor.go b/internal/domains/api/services/processors/flxg/flxg0v4b_processor.go index fa22dac..e7e077f 100644 --- a/internal/domains/api/services/processors/flxg/flxg0v4b_processor.go +++ b/internal/domains/api/services/processors/flxg/flxg0v4b_processor.go @@ -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) { diff --git a/internal/domains/api/services/processors/flxg/flxg162a_processor.go b/internal/domains/api/services/processors/flxg/flxg162a_processor.go index 8f44a1c..c356b83 100644 --- a/internal/domains/api/services/processors/flxg/flxg162a_processor.go +++ b/internal/domains/api/services/processors/flxg/flxg162a_processor.go @@ -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) diff --git a/internal/domains/api/services/processors/flxg/flxg3d56_processor.go b/internal/domains/api/services/processors/flxg/flxg3d56_processor.go index 4ded777..5a9192d 100644 --- a/internal/domains/api/services/processors/flxg/flxg3d56_processor.go +++ b/internal/domains/api/services/processors/flxg/flxg3d56_processor.go @@ -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) diff --git a/internal/domains/api/services/processors/flxg/flxg54f5_processor.go b/internal/domains/api/services/processors/flxg/flxg54f5_processor.go index b3a7f8c..89452a2 100644 --- a/internal/domains/api/services/processors/flxg/flxg54f5_processor.go +++ b/internal/domains/api/services/processors/flxg/flxg54f5_processor.go @@ -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) diff --git a/internal/domains/api/services/processors/flxg/flxg5876_processor.go b/internal/domains/api/services/processors/flxg/flxg5876_processor.go index 6404d62..f023574 100644 --- a/internal/domains/api/services/processors/flxg/flxg5876_processor.go +++ b/internal/domains/api/services/processors/flxg/flxg5876_processor.go @@ -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) diff --git a/internal/domains/api/services/processors/flxg/flxg5b2e_processor.go b/internal/domains/api/services/processors/flxg/flxg5b2e_processor.go index 1b5a10f..61fcfb0 100644 --- a/internal/domains/api/services/processors/flxg/flxg5b2e_processor.go +++ b/internal/domains/api/services/processors/flxg/flxg5b2e_processor.go @@ -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) { // 如果有返回内容,优先解析返回内容 diff --git a/internal/domains/api/services/processors/flxg/flxg75fe_processor.go b/internal/domains/api/services/processors/flxg/flxg75fe_processor.go index 3f48490..355c9dc 100644 --- a/internal/domains/api/services/processors/flxg/flxg75fe_processor.go +++ b/internal/domains/api/services/processors/flxg/flxg75fe_processor.go @@ -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) diff --git a/internal/domains/api/services/processors/flxg/flxg8a3f_processor.go b/internal/domains/api/services/processors/flxg/flxg8a3f_processor.go index 6f8afe5..ac292a8 100644 --- a/internal/domains/api/services/processors/flxg/flxg8a3f_processor.go +++ b/internal/domains/api/services/processors/flxg/flxg8a3f_processor.go @@ -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) { // 如果有返回内容,优先解析返回内容 diff --git a/internal/domains/api/services/processors/flxg/flxg9687_processor.go b/internal/domains/api/services/processors/flxg/flxg9687_processor.go index 862b4ca..a9ec345 100644 --- a/internal/domains/api/services/processors/flxg/flxg9687_processor.go +++ b/internal/domains/api/services/processors/flxg/flxg9687_processor.go @@ -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) diff --git a/internal/domains/api/services/processors/flxg/flxg970f_processor.go b/internal/domains/api/services/processors/flxg/flxg970f_processor.go index d29cfb6..093f924 100644 --- a/internal/domains/api/services/processors/flxg/flxg970f_processor.go +++ b/internal/domains/api/services/processors/flxg/flxg970f_processor.go @@ -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) diff --git a/internal/domains/api/services/processors/flxg/flxgbc21_processor.go b/internal/domains/api/services/processors/flxg/flxgbc21_processor.go index d0116bf..af35b7a 100644 --- a/internal/domains/api/services/processors/flxg/flxgbc21_processor.go +++ b/internal/domains/api/services/processors/flxg/flxgbc21_processor.go @@ -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) diff --git a/internal/domains/api/services/processors/flxg/flxgc9d1_processor.go b/internal/domains/api/services/processors/flxg/flxgc9d1_processor.go index e3689e4..cd92b34 100644 --- a/internal/domains/api/services/processors/flxg/flxgc9d1_processor.go +++ b/internal/domains/api/services/processors/flxg/flxgc9d1_processor.go @@ -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) diff --git a/internal/domains/api/services/processors/flxg/flxgca3d_processor.go b/internal/domains/api/services/processors/flxg/flxgca3d_processor.go index f39cd47..1d1e9a3 100644 --- a/internal/domains/api/services/processors/flxg/flxgca3d_processor.go +++ b/internal/domains/api/services/processors/flxg/flxgca3d_processor.go @@ -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 { diff --git a/internal/domains/api/services/processors/flxg/flxgdea8_processor.go b/internal/domains/api/services/processors/flxg/flxgdea8_processor.go new file mode 100644 index 0000000..0033e4d --- /dev/null +++ b/internal/domains/api/services/processors/flxg/flxgdea8_processor.go @@ -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 +} diff --git a/internal/domains/api/services/processors/flxg/flxgdea9_processor.go b/internal/domains/api/services/processors/flxg/flxgdea9_processor.go new file mode 100644 index 0000000..4eff8af --- /dev/null +++ b/internal/domains/api/services/processors/flxg/flxgdea9_processor.go @@ -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 +} diff --git a/internal/domains/api/services/processors/flxg/flxgdec7_processor.go b/internal/domains/api/services/processors/flxg/flxgdec7_processor.go index e2a390e..9024a8f 100644 --- a/internal/domains/api/services/processors/flxg/flxgdec7_processor.go +++ b/internal/domains/api/services/processors/flxg/flxgdec7_processor.go @@ -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) diff --git a/internal/domains/api/services/processors/ivyz/ivyz0b03_processor.go b/internal/domains/api/services/processors/ivyz/ivyz0b03_processor.go index 1f40ae3..9d4cf14 100644 --- a/internal/domains/api/services/processors/ivyz/ivyz0b03_processor.go +++ b/internal/domains/api/services/processors/ivyz/ivyz0b03_processor.go @@ -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) diff --git a/internal/domains/api/services/processors/ivyz/ivyz1c9d_processor.go b/internal/domains/api/services/processors/ivyz/ivyz1c9d_processor.go index 32dce0e..b708726 100644 --- a/internal/domains/api/services/processors/ivyz/ivyz1c9d_processor.go +++ b/internal/domains/api/services/processors/ivyz/ivyz1c9d_processor.go @@ -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) diff --git a/internal/domains/api/services/processors/ivyz/ivyz2125_processor.go b/internal/domains/api/services/processors/ivyz/ivyz2125_processor.go index b6b1a3a..cb36d3a 100644 --- a/internal/domains/api/services/processors/ivyz/ivyz2125_processor.go +++ b/internal/domains/api/services/processors/ivyz/ivyz2125_processor.go @@ -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) diff --git a/internal/domains/api/services/processors/ivyz/ivyz385e_processor.go b/internal/domains/api/services/processors/ivyz/ivyz385e_processor.go index c237a48..338d736 100644 --- a/internal/domains/api/services/processors/ivyz/ivyz385e_processor.go +++ b/internal/domains/api/services/processors/ivyz/ivyz385e_processor.go @@ -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) diff --git a/internal/domains/api/services/processors/ivyz/ivyz4e8b_processor.go b/internal/domains/api/services/processors/ivyz/ivyz4e8b_processor.go index f08b6ed..4609171 100644 --- a/internal/domains/api/services/processors/ivyz/ivyz4e8b_processor.go +++ b/internal/domains/api/services/processors/ivyz/ivyz4e8b_processor.go @@ -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) diff --git a/internal/domains/api/services/processors/ivyz/ivyz5733_processor.go b/internal/domains/api/services/processors/ivyz/ivyz5733_processor.go index b885d61..5fb2d69 100644 --- a/internal/domains/api/services/processors/ivyz/ivyz5733_processor.go +++ b/internal/domains/api/services/processors/ivyz/ivyz5733_processor.go @@ -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) diff --git a/internal/domains/api/services/processors/ivyz/ivyz7f2a_processor.go b/internal/domains/api/services/processors/ivyz/ivyz7f2a_processor.go index 5c57ea8..0b12f74 100644 --- a/internal/domains/api/services/processors/ivyz/ivyz7f2a_processor.go +++ b/internal/domains/api/services/processors/ivyz/ivyz7f2a_processor.go @@ -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) diff --git a/internal/domains/api/services/processors/ivyz/ivyz9363_processor.go b/internal/domains/api/services/processors/ivyz/ivyz9363_processor.go index 77c6413..b4a55b4 100644 --- a/internal/domains/api/services/processors/ivyz/ivyz9363_processor.go +++ b/internal/domains/api/services/processors/ivyz/ivyz9363_processor.go @@ -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) diff --git a/internal/domains/api/services/processors/ivyz/ivyz9a2b_processor.go b/internal/domains/api/services/processors/ivyz/ivyz9a2b_processor.go index 0f3330f..2f29e41 100644 --- a/internal/domains/api/services/processors/ivyz/ivyz9a2b_processor.go +++ b/internal/domains/api/services/processors/ivyz/ivyz9a2b_processor.go @@ -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) diff --git a/internal/domains/api/services/processors/ivyz/ivyzadee_processor.go b/internal/domains/api/services/processors/ivyz/ivyzadee_processor.go index 425f5fc..3f15513 100644 --- a/internal/domains/api/services/processors/ivyz/ivyzadee_processor.go +++ b/internal/domains/api/services/processors/ivyz/ivyzadee_processor.go @@ -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) diff --git a/internal/domains/api/services/processors/ivyz/ivyzgz08_processor.go b/internal/domains/api/services/processors/ivyz/ivyzgz08_processor.go index 976baf8..7947b1d 100644 --- a/internal/domains/api/services/processors/ivyz/ivyzgz08_processor.go +++ b/internal/domains/api/services/processors/ivyz/ivyzgz08_processor.go @@ -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) diff --git a/internal/domains/api/services/processors/jrzq/jrzq09j8_processor.go b/internal/domains/api/services/processors/jrzq/jrzq09j8_processor.go new file mode 100644 index 0000000..0d9d05e --- /dev/null +++ b/internal/domains/api/services/processors/jrzq/jrzq09j8_processor.go @@ -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 +} diff --git a/internal/domains/api/services/processors/jrzq/jrzq0a03_processor.go b/internal/domains/api/services/processors/jrzq/jrzq0a03_processor.go index 5aed224..e4aad18 100644 --- a/internal/domains/api/services/processors/jrzq/jrzq0a03_processor.go +++ b/internal/domains/api/services/processors/jrzq/jrzq0a03_processor.go @@ -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) diff --git a/internal/domains/api/services/processors/jrzq/jrzq1d09_processor.go b/internal/domains/api/services/processors/jrzq/jrzq1d09_processor.go new file mode 100644 index 0000000..cc03352 --- /dev/null +++ b/internal/domains/api/services/processors/jrzq/jrzq1d09_processor.go @@ -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 +} diff --git a/internal/domains/api/services/processors/jrzq/jrzq4aa8_processor.go b/internal/domains/api/services/processors/jrzq/jrzq4aa8_processor.go index 6ec8cd8..6525874 100644 --- a/internal/domains/api/services/processors/jrzq/jrzq4aa8_processor.go +++ b/internal/domains/api/services/processors/jrzq/jrzq4aa8_processor.go @@ -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) diff --git a/internal/domains/api/services/processors/jrzq/jrzq8203_processor.go b/internal/domains/api/services/processors/jrzq/jrzq8203_processor.go index cb54fb1..100a6c3 100644 --- a/internal/domains/api/services/processors/jrzq/jrzq8203_processor.go +++ b/internal/domains/api/services/processors/jrzq/jrzq8203_processor.go @@ -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) diff --git a/internal/domains/api/services/processors/jrzq/jrzqdcbe_processor.go b/internal/domains/api/services/processors/jrzq/jrzqdcbe_processor.go index 85d4f0a..bf55b2b 100644 --- a/internal/domains/api/services/processors/jrzq/jrzqdcbe_processor.go +++ b/internal/domains/api/services/processors/jrzq/jrzqdcbe_processor.go @@ -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) diff --git a/internal/domains/api/services/processors/qcxg/qcxg7a2b_processor.go b/internal/domains/api/services/processors/qcxg/qcxg7a2b_processor.go index eec5d32..fa34f49 100644 --- a/internal/domains/api/services/processors/qcxg/qcxg7a2b_processor.go +++ b/internal/domains/api/services/processors/qcxg/qcxg7a2b_processor.go @@ -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) diff --git a/internal/domains/api/services/processors/qygl/qygl2acd_processor.go b/internal/domains/api/services/processors/qygl/qygl2acd_processor.go index 4948ae2..d7fa26d 100644 --- a/internal/domains/api/services/processors/qygl/qygl2acd_processor.go +++ b/internal/domains/api/services/processors/qygl/qygl2acd_processor.go @@ -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) diff --git a/internal/domains/api/services/processors/qygl/qygl45bd_processor.go b/internal/domains/api/services/processors/qygl/qygl45bd_processor.go index 59d1aa7..7227d46 100644 --- a/internal/domains/api/services/processors/qygl/qygl45bd_processor.go +++ b/internal/domains/api/services/processors/qygl/qygl45bd_processor.go @@ -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 { diff --git a/internal/domains/api/services/processors/qygl/qygl6f2d_processor.go b/internal/domains/api/services/processors/qygl/qygl6f2d_processor.go index 3a4d901..13998d5 100644 --- a/internal/domains/api/services/processors/qygl/qygl6f2d_processor.go +++ b/internal/domains/api/services/processors/qygl/qygl6f2d_processor.go @@ -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) diff --git a/internal/domains/api/services/processors/qygl/qygl8261_processor.go b/internal/domains/api/services/processors/qygl/qygl8261_processor.go index 7ed83c8..44be04d 100644 --- a/internal/domains/api/services/processors/qygl/qygl8261_processor.go +++ b/internal/domains/api/services/processors/qygl/qygl8261_processor.go @@ -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) diff --git a/internal/domains/api/services/processors/qygl/qygl8271_processor.go b/internal/domains/api/services/processors/qygl/qygl8271_processor.go index 267e456..19408fc 100644 --- a/internal/domains/api/services/processors/qygl/qygl8271_processor.go +++ b/internal/domains/api/services/processors/qygl/qygl8271_processor.go @@ -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) { diff --git a/internal/domains/api/services/processors/qygl/qyglb4c0_processor.go b/internal/domains/api/services/processors/qygl/qyglb4c0_processor.go index 0fdd6ad..c4c9f95 100644 --- a/internal/domains/api/services/processors/qygl/qyglb4c0_processor.go +++ b/internal/domains/api/services/processors/qygl/qyglb4c0_processor.go @@ -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) { diff --git a/internal/domains/api/services/processors/yysy/yysy09cd_processor.go b/internal/domains/api/services/processors/yysy/yysy09cd_processor.go index 2fa053e..07c4304 100644 --- a/internal/domains/api/services/processors/yysy/yysy09cd_processor.go +++ b/internal/domains/api/services/processors/yysy/yysy09cd_processor.go @@ -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) diff --git a/internal/domains/api/services/processors/yysy/yysy4b21_processor.go b/internal/domains/api/services/processors/yysy/yysy4b21_processor.go index 9a1f646..bbcab30 100644 --- a/internal/domains/api/services/processors/yysy/yysy4b21_processor.go +++ b/internal/domains/api/services/processors/yysy/yysy4b21_processor.go @@ -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) diff --git a/internal/domains/api/services/processors/yysy/yysy4b37_processor.go b/internal/domains/api/services/processors/yysy/yysy4b37_processor.go index 2810572..0a3bfe5 100644 --- a/internal/domains/api/services/processors/yysy/yysy4b37_processor.go +++ b/internal/domains/api/services/processors/yysy/yysy4b37_processor.go @@ -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) diff --git a/internal/domains/api/services/processors/yysy/yysy6f2e_processor.go b/internal/domains/api/services/processors/yysy/yysy6f2e_processor.go index fb4d2fc..41b3170 100644 --- a/internal/domains/api/services/processors/yysy/yysy6f2e_processor.go +++ b/internal/domains/api/services/processors/yysy/yysy6f2e_processor.go @@ -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) diff --git a/internal/domains/api/services/processors/yysy/yysyd50f_processor.go b/internal/domains/api/services/processors/yysy/yysyd50f_processor.go index f4d453f..3f2a91d 100644 --- a/internal/domains/api/services/processors/yysy/yysyd50f_processor.go +++ b/internal/domains/api/services/processors/yysy/yysyd50f_processor.go @@ -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) diff --git a/internal/domains/api/services/processors/yysy/yysyf7db_processor.go b/internal/domains/api/services/processors/yysy/yysyf7db_processor.go index 4e6755d..94ce29e 100644 --- a/internal/domains/api/services/processors/yysy/yysyf7db_processor.go +++ b/internal/domains/api/services/processors/yysy/yysyf7db_processor.go @@ -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) diff --git a/internal/infrastructure/external/westdex/westdex_factory.go b/internal/infrastructure/external/westdex/westdex_factory.go new file mode 100644 index 0000000..c6e6a2a --- /dev/null +++ b/internal/infrastructure/external/westdex/westdex_factory.go @@ -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 +} diff --git a/internal/infrastructure/external/westdex/westdex_service.go b/internal/infrastructure/external/westdex/westdex_service.go index 2f590c2..ff4021c 100644 --- a/internal/infrastructure/external/westdex/westdex_service.go +++ b/internal/infrastructure/external/westdex/westdex_service.go @@ -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() + + // 从ctx中获取transactionId + var transactionID string + if ctxTransactionID, ok := ctx.Value("transaction_id").(string); ok { + transactionID = ctxTransactionID + } - // 构造请求URL - reqUrl := fmt.Sprintf("%s/%s/%s?timestamp=%s", w.config.Url, w.config.SecretSecondId, code, timestamp) + // 构建请求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,12 +381,14 @@ 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 { return w.config -} \ No newline at end of file +} diff --git a/internal/infrastructure/external/yushan/yushan_factory.go b/internal/infrastructure/external/yushan/yushan_factory.go new file mode 100644 index 0000000..b55f656 --- /dev/null +++ b/internal/infrastructure/external/yushan/yushan_factory.go @@ -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) +} diff --git a/internal/infrastructure/external/yushan/yushan_service.go b/internal/infrastructure/external/yushan/yushan_service.go index 813460c..3814129 100644 --- a/internal/infrastructure/external/yushan/yushan_service.go +++ b/internal/infrastructure/external/yushan/yushan_service.go @@ -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位的随机字符串订单号 diff --git a/internal/infrastructure/external/yushan/yushan_test.go b/internal/infrastructure/external/yushan/yushan_test.go new file mode 100644 index 0000000..4920443 --- /dev/null +++ b/internal/infrastructure/external/yushan/yushan_test.go @@ -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) + } + } +} diff --git a/internal/infrastructure/external/zhicha/crypto.go b/internal/infrastructure/external/zhicha/crypto.go new file mode 100644 index 0000000..13bd170 --- /dev/null +++ b/internal/infrastructure/external/zhicha/crypto.go @@ -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 +} diff --git a/internal/infrastructure/external/zhicha/zhicha_errors.go b/internal/infrastructure/external/zhicha/zhicha_errors.go new file mode 100644 index 0000000..f95d822 --- /dev/null +++ b/internal/infrastructure/external/zhicha/zhicha_errors.go @@ -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 +} diff --git a/internal/infrastructure/external/zhicha/zhicha_factory.go b/internal/infrastructure/external/zhicha/zhicha_factory.go new file mode 100644 index 0000000..c3e7d4d --- /dev/null +++ b/internal/infrastructure/external/zhicha/zhicha_factory.go @@ -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) +} diff --git a/internal/infrastructure/external/zhicha/zhicha_service.go b/internal/infrastructure/external/zhicha/zhicha_service.go new file mode 100644 index 0000000..9e53dc2 --- /dev/null +++ b/internal/infrastructure/external/zhicha/zhicha_service.go @@ -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 +} diff --git a/internal/infrastructure/external/zhicha/zhicha_test.go b/internal/infrastructure/external/zhicha/zhicha_test.go new file mode 100644 index 0000000..6d58eb2 --- /dev/null +++ b/internal/infrastructure/external/zhicha/zhicha_test.go @@ -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) +} diff --git a/internal/shared/external_logger/README.md b/internal/shared/external_logger/README.md new file mode 100644 index 0000000..bed15ff --- /dev/null +++ b/internal/shared/external_logger/README.md @@ -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` 值,避免日志文件过多 + diff --git a/internal/shared/external_logger/example_usage.md b/internal/shared/external_logger/example_usage.md new file mode 100644 index 0000000..51e6cce --- /dev/null +++ b/internal/shared/external_logger/example_usage.md @@ -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/` 目录。 diff --git a/internal/shared/external_logger/external_logger.go b/internal/shared/external_logger/external_logger.go new file mode 100644 index 0000000..447dca5 --- /dev/null +++ b/internal/shared/external_logger/external_logger.go @@ -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 +} diff --git a/internal/shared/logger/factory.go b/internal/shared/logger/factory.go new file mode 100644 index 0000000..8597f9d --- /dev/null +++ b/internal/shared/logger/factory.go @@ -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 + } +} diff --git a/internal/shared/logger/level_logger.go b/internal/shared/logger/level_logger.go index a5d8267..36e8442 100644 --- a/internal/shared/logger/level_logger.go +++ b/internal/shared/logger/level_logger.go @@ -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 "" -} \ No newline at end of file +func (l *LevelLogger) GetZapLogger() *zap.Logger { + return l.logger +} diff --git a/internal/shared/logger/logger.go b/internal/shared/logger/logger.go index 1fd0532..37afe9e 100644 --- a/internal/shared/logger/logger.go +++ b/internal/shared/logger/logger.go @@ -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) -} - -func Int(key string, val int) zapcore.Field { - return zap.Int(key, val) -} - -func Int64(key string, val int64) zapcore.Field { - return zap.Int64(key, val) -} - -func Float64(key string, val float64) zapcore.Field { - return zap.Float64(key, val) -} - -func Bool(key string, val bool) zapcore.Field { - return zap.Bool(key, val) -} - -func Error(err error) zapcore.Field { - return zap.Error(err) -} - -func Any(key string, val interface{}) zapcore.Field { - return zap.Any(key, val) -} - -func Duration(key string, val interface{}) zapcore.Field { - return zap.Any(key, val) + + // 提取用户ID + if userID := ctx.Value("user_id"); userID != nil { + if id, ok := userID.(string); ok { + fields = append(fields, zap.String("user_id", id)) + } + } + + // 提取跟踪ID + if traceID := ctx.Value("trace_id"); traceID != nil { + if id, ok := traceID.(string); ok { + fields = append(fields, zap.String("trace_id", id)) + } + } + + return fields }