diff --git a/docs/Certification域DDD重构规范.md b/docs/Certification域DDD重构规范.md new file mode 100644 index 0000000..4c68648 --- /dev/null +++ b/docs/Certification域DDD重构规范.md @@ -0,0 +1,348 @@ +# Certification域DDD重构规范文档 + +## 📋 概述 + +本文档规范了certification域(企业认证域)的DDD架构重构方案,目标是实现清晰的职责划分、完善的状态管理、以及高度可维护的代码结构。 + +## 🎯 重构目标 + +### 业务目标 +- 企业认证流程通过e签宝API实现 +- 异步回调驱动的状态转换机制 +- 简化状态设计,专注核心业务节点 +- 已签署合同即代表认证完成 + +### 技术目标 +- 符合DDD架构原则的分层设计 +- 清晰的职责边界和依赖关系 +- 基于CachedBaseRepositoryImpl的仓储实现 +- 完善的事件驱动架构 +- 强类型的状态机设计 + +## 🏗️ 架构设计 + +### 分层架构图 +``` +┌─────────────────────────────────────────────────┐ +│ 表现层 (API) │ +│ HTTP Controllers + Handlers │ +└─────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────┐ +│ 应用服务层 │ +│ 用例协调 + DTO转换 + 事务管理 │ +└─────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────┐ +│ 领域服务层 │ +│ 工作流编排 + e签宝集成 + 回调处理 + 状态机 │ +└─────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────┐ +│ 领域模型层 │ +│ 聚合根 + 实体 + 值对象 + 领域规则 │ +└─────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────┐ +│ 基础设施层 │ +│ 仓储实现 + 外部服务 + 缓存 + 事件发布 │ +└─────────────────────────────────────────────────┘ +``` + +## 📊 状态设计 + +### 状态枚举定义 +```go +const ( + // 主流程状态 + StatusPending = "pending" // 待认证 + StatusInfoSubmitted = "info_submitted" // 已提交企业信息 + StatusEnterpriseVerified = "enterprise_verified" // 已企业认证 + StatusContractApplied = "contract_applied" // 已申请签署合同 + StatusContractSigned = "contract_signed" // 已签署合同(认证完成) + + // 失败状态 + StatusInfoRejected = "info_rejected" // 企业信息被拒绝 + StatusContractRejected = "contract_rejected" // 合同被拒签 + StatusContractExpired = "contract_expired" // 合同签署超时 +) +``` + +### 状态转换规则 +| 当前状态 | 目标状态 | 触发条件 | 操作者 | +|---------|---------|----------|-------| +| Pending | InfoSubmitted | 用户提交企业信息 | USER | +| InfoSubmitted | EnterpriseVerified | e签宝认证成功回调 | SYSTEM | +| InfoSubmitted | InfoRejected | e签宝认证失败回调 | SYSTEM | +| EnterpriseVerified | ContractApplied | 用户申请合同 | USER | +| ContractApplied | ContractSigned | e签宝签署成功回调 | SYSTEM | +| ContractApplied | ContractRejected | e签宝拒签回调 | SYSTEM | +| ContractApplied | ContractExpired | 签署超时 | SYSTEM | +| InfoRejected | InfoSubmitted | 用户重新提交 | USER | +| ContractRejected | EnterpriseVerified | 重置状态 | SYSTEM | +| ContractExpired | EnterpriseVerified | 重置状态 | SYSTEM | + +## 📁 文件结构规范 + +### 目录结构 +``` +internal/domains/certification/ +├── entities/ # 实体层 +│ ├── certification.go # 认证聚合根 +│ ├── enterprise_info_submit_record.go # 企业信息提交记录 +│ ├── esign_contract_generate_record.go # 合同生成记录 +│ ├── esign_contract_sign_record.go # 合同签署记录 +│ └── value_objects/ # 值对象 +│ ├── enterprise_info.go # 企业信息 +│ └── contract_info.go # 合同信息 +├── enums/ # 枚举定义 +│ ├── certification_status.go # 认证状态 +│ ├── failure_reason.go # 失败原因 +│ └── actor_type.go # 操作者类型 +├── specifications/ # 规格模式 +│ └── certification_specifications.go # 认证规格 +├── services/ # 领域服务 +│ ├── certification_aggregate_service.go # 聚合服务 +│ ├── enterprise_verification_service.go # 企业验证服务 +│ ├── contract_management_service.go # 合同管理服务 +│ ├── certification_workflow_orchestrator.go # 工作流编排器 +│ ├── state_machine/ # 状态机 +│ │ ├── certification_state_machine.go # 状态机核心 +│ │ ├── state_config.go # 状态配置 +│ │ └── esign_callback_handler.go # 回调处理器 +│ └── factories/ # 工厂模式 +│ └── certification_factory.go # 认证工厂 +├── repositories/ # 仓储接口 +│ ├── certification_command_repository.go # 命令仓储 +│ ├── certification_query_repository.go # 查询仓储 +│ ├── enterprise_info_repository.go # 企业信息仓储 +│ ├── contract_record_repository.go # 合同记录仓储 +│ └── queries/ # 查询对象 +│ ├── certification_queries.go # 认证查询 +│ └── list_certifications_query.go # 列表查询 +└── events/ # 领域事件 + ├── certification_events.go # 事件定义 + └── event_handlers/ # 事件处理器 + ├── certification_event_handler.go # 认证事件处理 + └── notification_event_handler.go # 通知处理 +``` + +## 🔧 各层接口规范 + +### 1. 聚合根接口 (certification.go) +```go +type Certification struct { + // 聚合根必须实现的核心方法 + + // 状态转换 + CanTransitionTo(targetStatus CertificationStatus, actor ActorType) bool + TransitionTo(targetStatus CertificationStatus, actor ActorType, reason string) error + + // 业务操作 + SubmitEnterpriseInfo(info EnterpriseInfo) error + ApplyContract() error + HandleEsignCallback(callbackType string, success bool, data map[string]interface{}) error + + // 业务规则验证 + ValidateBusinessRules() error + ValidateInvariance() error + + // 查询方法 + GetProgress() int + IsUserActionRequired() bool + GetAvailableActions() []string +} +``` + +### 2. 领域服务接口规范 + +#### 聚合服务 +```go +type CertificationAggregateService interface { + // 聚合管理 + CreateCertification(ctx context.Context, userID string) (*entities.Certification, error) + LoadCertification(ctx context.Context, id string) (*entities.Certification, error) + SaveCertification(ctx context.Context, cert *entities.Certification) error + + // 状态管理 + TransitionState(ctx context.Context, cert *entities.Certification, targetStatus enums.CertificationStatus, actor enums.ActorType, metadata map[string]interface{}) error +} +``` + +#### 工作流编排器 +```go +type CertificationWorkflowOrchestrator interface { + // 用户操作用例 + SubmitEnterpriseInfo(ctx context.Context, cmd *SubmitEnterpriseInfoCommand) (*WorkflowResult, error) + ApplyContract(ctx context.Context, cmd *ApplyContractCommand) (*WorkflowResult, error) + + // e签宝回调处理 + HandleEnterpriseVerificationCallback(ctx context.Context, cmd *EsignCallbackCommand) error + HandleContractSignCallback(ctx context.Context, cmd *EsignCallbackCommand) error + + // 异常处理 + HandleFailure(ctx context.Context, certificationID string, failureType string, reason string) error + RetryOperation(ctx context.Context, certificationID string, operation string) error +} +``` + +### 3. 仓储接口规范 + +#### 命令仓储 +```go +type CertificationCommandRepository interface { + Create(ctx context.Context, cert entities.Certification) error + Update(ctx context.Context, cert entities.Certification) error + UpdateStatus(ctx context.Context, id string, status enums.CertificationStatus) error + Delete(ctx context.Context, id string) error + + // 事务支持 + WithTx(tx interfaces.Transaction) CertificationCommandRepository +} +``` + +#### 查询仓储 +```go +type CertificationQueryRepository interface { + GetByID(ctx context.Context, id string) (*entities.Certification, error) + GetByUserID(ctx context.Context, userID string) (*entities.Certification, error) + List(ctx context.Context, query *queries.ListCertificationsQuery) ([]*entities.Certification, int64, error) + + // 业务查询 + GetPendingCertifications(ctx context.Context) ([]*entities.Certification, error) + GetExpiredContracts(ctx context.Context) ([]*entities.Certification, error) + GetStatistics(ctx context.Context, period TimePeriod) (*CertificationStats, error) +} +``` + +### 4. 应用服务接口规范 +```go +type CertificationApplicationService interface { + // 用户操作用例 + CreateCertification(ctx context.Context, cmd *commands.CreateCertificationCommand) (*responses.CertificationResponse, error) + SubmitEnterpriseInfo(ctx context.Context, cmd *commands.SubmitEnterpriseInfoCommand) (*responses.CertificationResponse, error) + ApplyContract(ctx context.Context, cmd *commands.ApplyContractCommand) (*responses.ContractSignUrlResponse, error) + + // 查询用例 + GetCertification(ctx context.Context, query *queries.GetCertificationQuery) (*responses.CertificationResponse, error) + ListCertifications(ctx context.Context, query *queries.ListCertificationsQuery) (*responses.CertificationListResponse, error) + + // e签宝回调处理 + HandleEsignCallback(ctx context.Context, cmd *commands.EsignCallbackCommand) error +} +``` + +## 🔄 业务流程规范 + +### 主要业务流程 + +#### 流程1:用户提交企业信息 +``` +HTTP Request → Application Service → Workflow Orchestrator → Aggregate Service → Domain Entity → State Machine → Repository → Event Publishing +``` + +#### 流程2:e签宝企业认证回调 +``` +HTTP Callback → Application Service → Workflow Orchestrator → Esign Callback Handler → Aggregate Service → State Machine → Repository → Event Publishing +``` + +#### 流程3:用户申请合同签署 +``` +HTTP Request → Application Service → Workflow Orchestrator → Contract Management Service → Aggregate Service → State Machine → Repository → Return Sign URL +``` + +#### 流程4:e签宝合同签署回调 +``` +HTTP Callback → Application Service → Workflow Orchestrator → Esign Callback Handler → Aggregate Service → State Machine → Repository → Event Publishing +``` + +## 📝 编码规范 + +### 命名规范 +- **接口**: 以`Interface`或不带后缀结尾,如`CertificationRepository` +- **实现类**: 以`Impl`结尾,如`CertificationRepositoryImpl` +- **领域服务**: 以`Service`结尾,如`CertificationAggregateService` +- **DTO**: 以`Command`、`Query`、`Response`结尾 +- **事件**: 以`Event`结尾,如`CertificationCreatedEvent` + +### 错误处理规范 +- 使用领域特定的错误类型 +- 错误消息必须使用中文 +- 包含足够的上下文信息 +- 支持错误链追踪 + +### 日志规范 +- 关键业务操作必须记录日志 +- 日志消息使用中文 +- 包含关键业务标识符(用户ID、认证ID等) +- 使用结构化日志格式 + +### 缓存规范 +- 查询仓储使用CachedBaseRepositoryImpl +- 根据查询频率选择合适的缓存策略 +- 写操作自动失效相关缓存 +- 支持手动缓存刷新 + +## 🧪 测试规范 + +### 单元测试 +- 每个领域服务必须有单元测试 +- 状态机转换逻辑必须全覆盖测试 +- 业务规则验证必须有测试 +- Mock外部依赖 + +### 集成测试 +- e签宝回调处理的集成测试 +- 完整业务流程的端到端测试 +- 数据库操作的集成测试 + +## 📊 监控规范 + +### 业务指标 +- 认证申请创建数量 +- 企业认证成功/失败率 +- 合同签署成功/失败率 +- 各状态的停留时间 + +### 技术指标 +- 缓存命中率 +- API响应时间 +- 数据库查询性能 +- 错误率和类型分布 + +## 🚀 部署规范 + +### 配置管理 +- 环境相关配置外部化 +- e签宝API配置独立管理 +- 缓存配置可调整 +- 日志级别可配置 + +### 数据迁移 +- 状态枚举的兼容性迁移 +- 历史数据的清理策略 +- 缓存预热脚本 + +## 📋 验收标准 + +### 功能验收 +- [ ] 完整的企业认证流程正常运行 +- [ ] e签宝回调正确处理 +- [ ] 状态转换严格按照规则执行 +- [ ] 异常情况正确处理和恢复 + +### 技术验收 +- [ ] 各层职责清晰,无逻辑泄漏 +- [ ] 代码符合DDD架构原则 +- [ ] 缓存策略有效,性能提升明显 +- [ ] 事件驱动架构正常工作 + +### 质量验收 +- [ ] 单元测试覆盖率 > 80% +- [ ] 集成测试通过 +- [ ] 代码质量检查通过 +- [ ] 性能指标达标 + +--- + +本规范文档将指导整个certification域的重构实施,确保重构后的代码具有良好的可维护性、可扩展性和健壮性。 \ No newline at end of file diff --git a/internal/application/certification/certification_application_service.go b/internal/application/certification/certification_application_service.go index c5c2161..53d474c 100644 --- a/internal/application/certification/certification_application_service.go +++ b/internal/application/certification/certification_application_service.go @@ -9,25 +9,49 @@ import ( ) // CertificationApplicationService 认证应用服务接口 +// 负责用例协调,提供精简的应用层接口 type CertificationApplicationService interface { - // 认证状态查询 - GetCertificationStatus(ctx context.Context, query *queries.GetCertificationStatusQuery) (*responses.CertificationResponse, error) - GetCertificationDetails(ctx context.Context, query *queries.GetCertificationDetailsQuery) (*responses.CertificationResponse, error) - GetCertificationProgress(ctx context.Context, userID string) (map[string]interface{}, error) + // ================ 用户操作用例 ================ - // 企业信息管理 + // 创建认证申请 + CreateCertification(ctx context.Context, cmd *commands.CreateCertificationCommand) (*responses.CertificationResponse, error) + + // 提交企业信息 SubmitEnterpriseInfo(ctx context.Context, cmd *commands.SubmitEnterpriseInfoCommand) (*responses.CertificationResponse, error) - // 企业认证 - GetEnterpriseAuthURL(ctx context.Context, userID string) (*responses.EnterpriseAuthURLResponse, error) + // 申请合同签署 + ApplyContract(ctx context.Context, cmd *commands.ApplyContractCommand) (*responses.ContractSignUrlResponse, error) - // 合同管理 - ApplyContract(ctx context.Context, userID string) (*responses.CertificationResponse, error) - GetContractSignURL(ctx context.Context, cmd *commands.GetContractSignURLCommand) (*responses.ContractSignURLResponse, error) -} + // 重试失败操作 + RetryOperation(ctx context.Context, cmd *commands.RetryOperationCommand) (*responses.CertificationResponse, error) + + // ================ 查询用例 ================ + + // 获取认证详情 + GetCertification(ctx context.Context, query *queries.GetCertificationQuery) (*responses.CertificationResponse, error) + + // 获取用户认证列表 + GetUserCertifications(ctx context.Context, query *queries.GetUserCertificationsQuery) (*responses.CertificationListResponse, error) + + // 获取认证列表(管理员) + ListCertifications(ctx context.Context, query *queries.ListCertificationsQuery) (*responses.CertificationListResponse, error) + + // 搜索认证 + SearchCertifications(ctx context.Context, query *queries.SearchCertificationsQuery) (*responses.CertificationListResponse, error) + + // 获取认证统计 + GetCertificationStatistics(ctx context.Context, query *queries.GetCertificationStatisticsQuery) (*responses.CertificationStatisticsResponse, error) + + // ================ e签宝回调处理 ================ -// EsignCallbackApplicationService e签宝回调应用服务接口 -type EsignCallbackApplicationService interface { // 处理e签宝回调 - HandleCallback(ctx context.Context, callbackData map[string]interface{}, headers map[string]string, queryParams map[string]string) error + HandleEsignCallback(ctx context.Context, cmd *commands.EsignCallbackCommand) (*responses.CallbackResponse, error) + + // ================ 管理员操作 ================ + + // 手动状态转换(管理员) + ForceTransitionStatus(ctx context.Context, cmd *commands.ForceTransitionStatusCommand) (*responses.CertificationResponse, error) + + // 获取系统监控数据 + GetSystemMonitoring(ctx context.Context, query *queries.GetSystemMonitoringQuery) (*responses.SystemMonitoringResponse, error) } diff --git a/internal/application/certification/certification_application_service_impl.go b/internal/application/certification/certification_application_service_impl.go index 8e1dea1..098c4b3 100644 --- a/internal/application/certification/certification_application_service_impl.go +++ b/internal/application/certification/certification_application_service_impl.go @@ -2,454 +2,730 @@ package certification import ( "context" - "errors" "fmt" - "time" - - "go.uber.org/zap" - "gorm.io/gorm" "tyapi-server/internal/application/certification/dto/commands" "tyapi-server/internal/application/certification/dto/queries" "tyapi-server/internal/application/certification/dto/responses" "tyapi-server/internal/domains/certification/entities" "tyapi-server/internal/domains/certification/enums" + "tyapi-server/internal/domains/certification/repositories" "tyapi-server/internal/domains/certification/services" - user_entities "tyapi-server/internal/domains/user/entities" - user_services "tyapi-server/internal/domains/user/services" - "tyapi-server/internal/shared/database" - esign_service "tyapi-server/internal/shared/esign" + "tyapi-server/internal/domains/certification/services/state_machine" + + "go.uber.org/zap" ) // CertificationApplicationServiceImpl 认证应用服务实现 -// 负责业务流程编排、事务管理、数据转换,不直接操作仓库 +// 负责用例协调,DTO转换,是应用层的核心组件 type CertificationApplicationServiceImpl struct { - certManagementService *services.CertificationManagementService - certWorkflowService *services.CertificationWorkflowService - certificationEsignService *services.CertificationEsignService - enterpriseService *user_services.EnterpriseService - esignService *esign_service.Client - enterpriseRecordService *services.EnterpriseInfoSubmitRecordService - smsCodeService *user_services.SMSCodeService - txManager *database.TransactionManager - logger *zap.Logger + // 领域服务依赖 + aggregateService services.CertificationAggregateService + workflowOrchestrator services.CertificationWorkflowOrchestrator + + // 仓储依赖 + queryRepository repositories.CertificationQueryRepository + + // 基础设施依赖 + callbackHandler *state_machine.EsignCallbackHandler + logger *zap.Logger } // NewCertificationApplicationService 创建认证应用服务 func NewCertificationApplicationService( - certManagementService *services.CertificationManagementService, - certWorkflowService *services.CertificationWorkflowService, - certificationEsignService *services.CertificationEsignService, - enterpriseService *user_services.EnterpriseService, - esignService *esign_service.Client, - enterpriseRecordService *services.EnterpriseInfoSubmitRecordService, - smsCodeService *user_services.SMSCodeService, - txManager *database.TransactionManager, + aggregateService services.CertificationAggregateService, + workflowOrchestrator services.CertificationWorkflowOrchestrator, + queryRepository repositories.CertificationQueryRepository, + callbackHandler *state_machine.EsignCallbackHandler, logger *zap.Logger, ) CertificationApplicationService { return &CertificationApplicationServiceImpl{ - certManagementService: certManagementService, - certWorkflowService: certWorkflowService, - certificationEsignService: certificationEsignService, - enterpriseService: enterpriseService, - esignService: esignService, - enterpriseRecordService: enterpriseRecordService, - smsCodeService: smsCodeService, - txManager: txManager, - logger: logger, + aggregateService: aggregateService, + workflowOrchestrator: workflowOrchestrator, + queryRepository: queryRepository, + callbackHandler: callbackHandler, + logger: logger, } } +// ================ 用户操作用例 ================ + +// CreateCertification 创建认证申请 +func (s *CertificationApplicationServiceImpl) CreateCertification( + ctx context.Context, + cmd *commands.CreateCertificationCommand, +) (*responses.CertificationResponse, error) { + s.logger.Info("开始创建认证申请", zap.String("user_id", cmd.UserID)) + + // 1. 调用聚合服务创建认证 + cert, err := s.aggregateService.CreateCertification(ctx, cmd.UserID) + if err != nil { + s.logger.Error("创建认证申请失败", zap.Error(err), zap.String("user_id", cmd.UserID)) + return nil, fmt.Errorf("创建认证申请失败: %w", err) + } + + // 2. 转换为响应DTO + response := s.convertToResponse(cert) + + s.logger.Info("认证申请创建成功", + zap.String("user_id", cmd.UserID), + zap.String("certification_id", cert.ID)) + + return response, nil +} + // SubmitEnterpriseInfo 提交企业信息 -// 业务流程:1. 验证企业信息 2. 创建或获取认证申请 3. 使用事务执行状态转换和创建记录 -func (s *CertificationApplicationServiceImpl) SubmitEnterpriseInfo(ctx context.Context, cmd *commands.SubmitEnterpriseInfoCommand) (*responses.CertificationResponse, error) { - // 1. 验证短信验证码 - if err := s.smsCodeService.VerifyCode(ctx, cmd.LegalPersonPhone, cmd.VerificationCode, user_entities.SMSSceneCertification); err != nil { - return nil, fmt.Errorf("验证码错误或已过期") +func (s *CertificationApplicationServiceImpl) SubmitEnterpriseInfo( + ctx context.Context, + cmd *commands.SubmitEnterpriseInfoCommand, +) (*responses.CertificationResponse, error) { + s.logger.Info("开始提交企业信息", + zap.String("certification_id", cmd.CertificationID), + zap.String("user_id", cmd.UserID)) + + // 1. 构建工作流命令 + workflowCmd := &services.SubmitEnterpriseInfoCommand{ + CertificationID: cmd.CertificationID, + UserID: cmd.UserID, + EnterpriseInfo: cmd.EnterpriseInfo, } - // 2. 验证企业信息(检查统一社会信用代码是否已存在) - exists, err := s.enterpriseService.CheckUnifiedSocialCodeExists(ctx, cmd.UnifiedSocialCode, "") + // 2. 执行工作流 + workflowResult, err := s.workflowOrchestrator.SubmitEnterpriseInfo(ctx, workflowCmd) if err != nil { - return nil, fmt.Errorf("检查企业信息失败: %w", err) - } - if exists { - return nil, fmt.Errorf("统一社会信用代码已存在") + s.logger.Error("提交企业信息失败", zap.Error(err)) + return nil, fmt.Errorf("提交企业信息失败: %w", err) } - // 3. 获取或创建认证申请 - certification, err := s.certManagementService.GetCertificationByUserID(ctx, cmd.UserID) + // 3. 加载最新的认证信息 + cert, err := s.aggregateService.LoadCertification(ctx, cmd.CertificationID) if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - certification, err = s.certManagementService.CreateCertification(ctx, cmd.UserID) - if err != nil { - return nil, fmt.Errorf("创建认证申请失败: %w", err) - } - } else { - return nil, fmt.Errorf("获取认证申请失败: %w", err) + s.logger.Error("加载认证信息失败", zap.Error(err)) + return nil, fmt.Errorf("加载认证信息失败: %w", err) + } + + // 4. 转换为响应DTO + response := s.convertToResponse(cert) + + // 5. 添加工作流结果信息 + if workflowResult.Data != nil { + response.Metadata = workflowResult.Data + } + + s.logger.Info("企业信息提交成功", zap.String("certification_id", cmd.CertificationID)) + return response, nil +} + +// ApplyContract 申请合同签署 +func (s *CertificationApplicationServiceImpl) ApplyContract( + ctx context.Context, + cmd *commands.ApplyContractCommand, +) (*responses.ContractSignUrlResponse, error) { + s.logger.Info("开始申请合同签署", + zap.String("certification_id", cmd.CertificationID), + zap.String("user_id", cmd.UserID)) + + // 1. 构建工作流命令 + workflowCmd := &services.ApplyContractCommand{ + CertificationID: cmd.CertificationID, + UserID: cmd.UserID, + } + + // 2. 执行工作流 + workflowResult, err := s.workflowOrchestrator.ApplyContract(ctx, workflowCmd) + if err != nil { + s.logger.Error("申请合同签署失败", zap.Error(err)) + return nil, fmt.Errorf("申请合同签署失败: %w", err) + } + + // 3. 从工作流结果提取签署URL信息 + signURL, _ := workflowResult.Data["contract_sign_url"].(string) + contractURL, _ := workflowResult.Data["contract_url"].(string) + nextAction, _ := workflowResult.Data["next_action"].(string) + + // 4. 构建响应 + response := responses.NewContractSignUrlResponse( + cmd.CertificationID, + signURL, + contractURL, + nextAction, + workflowResult.Message, + ) + + s.logger.Info("合同申请成功", zap.String("certification_id", cmd.CertificationID)) + return response, nil +} + +// RetryOperation 重试失败操作 +func (s *CertificationApplicationServiceImpl) RetryOperation( + ctx context.Context, + cmd *commands.RetryOperationCommand, +) (*responses.CertificationResponse, error) { + s.logger.Info("开始重试操作", + zap.String("certification_id", cmd.CertificationID), + zap.String("operation", cmd.Operation)) + + // 1. 执行重试工作流 + workflowResult, err := s.workflowOrchestrator.RetryOperation(ctx, cmd.CertificationID, cmd.Operation) + if err != nil { + s.logger.Error("重试操作失败", zap.Error(err)) + return nil, fmt.Errorf("重试操作失败: %w", err) + } + + // 2. 加载最新的认证信息 + cert, err := s.aggregateService.LoadCertification(ctx, cmd.CertificationID) + if err != nil { + s.logger.Error("加载认证信息失败", zap.Error(err)) + return nil, fmt.Errorf("加载认证信息失败: %w", err) + } + + // 3. 转换为响应DTO + response := s.convertToResponse(cert) + + // 4. 添加重试结果信息 + if workflowResult.Data != nil { + response.Metadata = workflowResult.Data + } + + s.logger.Info("重试操作成功", zap.String("certification_id", cmd.CertificationID)) + return response, nil +} + +// ================ 查询用例 ================ + +// GetCertification 获取认证详情 +func (s *CertificationApplicationServiceImpl) GetCertification( + ctx context.Context, + query *queries.GetCertificationQuery, +) (*responses.CertificationResponse, error) { + s.logger.Debug("获取认证详情", zap.String("certification_id", query.CertificationID)) + + // 1. 从查询仓储获取认证信息 + cert, err := s.queryRepository.GetByID(ctx, query.CertificationID) + if err != nil { + s.logger.Error("获取认证信息失败", zap.Error(err)) + return nil, fmt.Errorf("认证信息不存在: %w", err) + } + + // 2. 权限验证(如果提供了用户ID) + if query.UserID != "" && cert.UserID != query.UserID { + return nil, fmt.Errorf("无权限访问此认证信息") + } + + // 3. 转换为响应DTO + response := s.convertToResponse(cert) + + return response, nil +} + +// GetUserCertifications 获取用户认证列表 +func (s *CertificationApplicationServiceImpl) GetUserCertifications( + ctx context.Context, + query *queries.GetUserCertificationsQuery, +) (*responses.CertificationListResponse, error) { + s.logger.Debug("获取用户认证列表", zap.String("user_id", query.UserID)) + + // 1. 转换为通用列表查询对象 + domainQuery := &queries.ListCertificationsQuery{ + Page: query.Page, + PageSize: query.PageSize, + UserID: query.UserID, + Status: query.Status, + } + + // 根据包含选项设置状态过滤 + if !query.IncludeCompleted && !query.IncludeFailed { + // 只显示进行中的认证 + domainQuery.Statuses = []enums.CertificationStatus{ + enums.StatusPending, + enums.StatusInfoSubmitted, + enums.StatusEnterpriseVerified, + enums.StatusContractApplied, } } - // 4. 创建记录 - if certification.Status != enums.StatusPending && certification.Status != enums.StatusInfoSubmitted { - return nil, fmt.Errorf("当前状态不允许提交企业信息") - } - _, err = s.enterpriseRecordService.CreateEnterpriseInfoSubmitRecord(ctx, cmd.UserID, cmd.CompanyName, cmd.UnifiedSocialCode, cmd.LegalPersonName, cmd.LegalPersonID, cmd.LegalPersonPhone) - if err != nil { - return nil, fmt.Errorf("企业信息提交失败: %w", err) - } - s.logger.Info("企业信息提交成功", - zap.String("user_id", cmd.UserID), - zap.String("certification_id", certification.ID), - zap.String("company_name", cmd.CompanyName), - ) + // 转换为领域查询对象 + listQuery := domainQuery.ToDomainQuery() - // 转换状态 - err = s.certWorkflowService.SubmitEnterpriseInfo(ctx, certification.ID) + // 2. 执行查询 + certs, total, err := s.queryRepository.List(ctx, listQuery) if err != nil { - return nil, fmt.Errorf("转换状态失败: %w", err) + s.logger.Error("查询用户认证列表失败", zap.Error(err)) + return nil, fmt.Errorf("查询用户认证列表失败: %w", err) } - // 5. 检查企业是否已经认证 - hasCertification, err := s.certManagementService.CheckCertification(ctx, cmd.CompanyName, cmd.UnifiedSocialCode) - if err != nil { - return nil, fmt.Errorf("检查企业认证状态失败: %w", err) + // 3. 转换为响应DTO + items := make([]*responses.CertificationResponse, len(certs)) + for i, cert := range certs { + items[i] = s.convertToResponse(cert) } - if hasCertification { - // 如果企业已经认证,则直接完成认证 - err = s.completeEnterpriseAuth(ctx, certification) + + // 4. 构建列表响应 + response := responses.NewCertificationListResponse(items, total, query.Page, query.PageSize) + + return response, nil +} + +// ListCertifications 获取认证列表(管理员) +func (s *CertificationApplicationServiceImpl) ListCertifications( + ctx context.Context, + query *queries.ListCertificationsQuery, +) (*responses.CertificationListResponse, error) { + s.logger.Debug("获取认证列表(管理员)") + + // 1. 转换为领域查询对象 + domainQuery := query.ToDomainQuery() + + // 2. 执行查询 + certs, total, err := s.queryRepository.List(ctx, domainQuery) + if err != nil { + s.logger.Error("查询认证列表失败", zap.Error(err)) + return nil, fmt.Errorf("查询认证列表失败: %w", err) + } + + // 3. 转换为响应DTO + items := make([]*responses.CertificationResponse, len(certs)) + for i, cert := range certs { + items[i] = s.convertToResponse(cert) + } + + // 4. 构建列表响应 + response := responses.NewCertificationListResponse(items, total, query.Page, query.PageSize) + + return response, nil +} + +// SearchCertifications 搜索认证 +func (s *CertificationApplicationServiceImpl) SearchCertifications( + ctx context.Context, + query *queries.SearchCertificationsQuery, +) (*responses.CertificationListResponse, error) { + s.logger.Debug("搜索认证", zap.String("keyword", query.Keyword)) + + // 1. 转换为领域查询对象 + domainQuery := query.ToDomainQuery() + + // 2. 根据搜索字段选择不同的搜索方法 + var certs []*entities.Certification + var total int64 + var err error + + if len(domainQuery.SearchFields) == 1 { + switch domainQuery.SearchFields[0] { + case "company_name": + certs, err = s.queryRepository.SearchByCompanyName(ctx, domainQuery.Keyword, domainQuery.GetLimit()) + total = int64(len(certs)) // 简化实现,实际应该有单独的计数方法 + case "legal_person_name": + certs, err = s.queryRepository.SearchByLegalPerson(ctx, domainQuery.Keyword, domainQuery.GetLimit()) + total = int64(len(certs)) + default: + // 通用搜索,这里简化为按公司名搜索 + certs, err = s.queryRepository.SearchByCompanyName(ctx, domainQuery.Keyword, domainQuery.GetLimit()) + total = int64(len(certs)) + } + } else { + // 多字段搜索,这里简化为按公司名搜索 + certs, err = s.queryRepository.SearchByCompanyName(ctx, domainQuery.Keyword, domainQuery.GetLimit()) + total = int64(len(certs)) + } + + if err != nil { + s.logger.Error("搜索认证失败", zap.Error(err)) + return nil, fmt.Errorf("搜索认证失败: %w", err) + } + + // 3. 转换为响应DTO + items := make([]*responses.CertificationResponse, len(certs)) + for i, cert := range certs { + items[i] = s.convertToResponse(cert) + } + + // 4. 构建列表响应 + response := responses.NewCertificationListResponse(items, total, query.Page, query.PageSize) + + return response, nil +} + +// GetCertificationStatistics 获取认证统计 +func (s *CertificationApplicationServiceImpl) GetCertificationStatistics( + ctx context.Context, + query *queries.GetCertificationStatisticsQuery, +) (*responses.CertificationStatisticsResponse, error) { + s.logger.Debug("获取认证统计", zap.String("period", query.Period)) + + // 1. 转换为领域查询对象 + domainQuery := query.ToDomainQuery() + + // 2. 验证查询参数 + if err := domainQuery.Validate(); err != nil { + return nil, fmt.Errorf("统计查询参数无效: %w", err) + } + + // 3. 确定时间周期 + var period repositories.CertificationTimePeriod + switch query.Period { + case "daily": + period = repositories.PeriodDaily + case "weekly": + period = repositories.PeriodWeekly + case "monthly": + period = repositories.PeriodMonthly + case "yearly": + period = repositories.PeriodYearly + default: + period = repositories.PeriodDaily + } + + // 4. 获取统计数据 + stats, err := s.queryRepository.GetStatistics(ctx, period) + if err != nil { + s.logger.Error("获取认证统计失败", zap.Error(err)) + return nil, fmt.Errorf("获取认证统计失败: %w", err) + } + + // 5. 获取进度统计(如果需要) + var progressStats *repositories.CertificationProgressStats + if query.IncludeProgressStats { + progressStats, err = s.queryRepository.GetProgressStatistics(ctx) if err != nil { - return nil, fmt.Errorf("完成企业认证失败: %w", err) + s.logger.Warn("获取进度统计失败", zap.Error(err)) } } - // 6. 重新获取认证申请数据 - certification, err = s.certManagementService.GetCertificationByID(ctx, certification.ID) - if err != nil { - return nil, fmt.Errorf("获取认证申请失败: %w", err) - } - return s.buildCertificationResponse(certification), nil -} - -// GetEnterpriseAuthURL 获取企业认证链接 -// 业务流程:1. 获取认证申请 2. 获取企业信息提交记录 3. 生成e签宝认证文件 4. 返回认证链接 -func (s *CertificationApplicationServiceImpl) GetEnterpriseAuthURL(ctx context.Context, userID string) (*responses.EnterpriseAuthURLResponse, error) { - // 1. 获取认证申请 - certification, err := s.certManagementService.GetCertificationByUserID(ctx, userID) - if err != nil { - return nil, fmt.Errorf("用户尚未创建认证申请: %w", err) - } - - // 2. 检查认证状态 - if certification.Status != enums.StatusInfoSubmitted { - return nil, fmt.Errorf("当前状态不允许进行企业认证") - } - - // 3. 获取企业信息提交记录 - enterpriseRecord, err := s.enterpriseRecordService.GetLatestByUserID(ctx, userID) - if err != nil { - return nil, fmt.Errorf("获取企业信息失败: %w", err) - } - - // 4. 生成e签宝认证文件 - authReq := &esign_service.EnterpriseAuthRequest{ - CompanyName: enterpriseRecord.CompanyName, - UnifiedSocialCode: enterpriseRecord.UnifiedSocialCode, - LegalPersonName: enterpriseRecord.LegalPersonName, - LegalPersonID: enterpriseRecord.LegalPersonID, - TransactorName: enterpriseRecord.LegalPersonName, - TransactorMobile: enterpriseRecord.LegalPersonPhone, - TransactorID: enterpriseRecord.LegalPersonID, - } - - authResp, err := s.esignService.GenerateEnterpriseAuth(authReq) - if err != nil { - s.logger.Error("生成企业认证文件失败", - zap.String("user_id", userID), - zap.String("certification_id", certification.ID), - zap.Error(err), - ) - return nil, fmt.Errorf("生成企业认证文件失败: %w", err) - } - - s.logger.Info("获取企业认证链接成功", - zap.String("user_id", userID), - zap.String("certification_id", certification.ID), - zap.String("esign_flow_id", authResp.AuthFlowID), - ) - - return &responses.EnterpriseAuthURLResponse{ - AuthURL: authResp.AuthURL, - ShortURL: authResp.AuthShortURL, - ExpireAt: time.Now().AddDate(0, 0, 7).Format(time.RFC3339), - }, nil -} - -// ApplyContract 申请合同文件 -// 业务流程:1. 获取认证申请 2. 获取企业信息 3. 生成e签宝合同文件 -func (s *CertificationApplicationServiceImpl) ApplyContract(ctx context.Context, userID string) (*responses.CertificationResponse, error) { - // 1. 获取认证申请 - certification, err := s.certManagementService.GetCertificationByUserID(ctx, userID) - if err != nil { - return nil, fmt.Errorf("用户尚未创建认证申请: %w", err) - } - - // 2. 获取企业信息 - enterpriseInfo, err := s.enterpriseService.GetEnterpriseInfo(ctx, userID) - if err != nil { - return nil, fmt.Errorf("获取企业信息失败: %w", err) - } - - // 4. 生成e签宝合同 - components := map[string]string{ - "JFQY": "海南学宇思网络科技有限公司", - "JFFR": "刘福思", - "YFQY": enterpriseInfo.CompanyName, - "YFFR": enterpriseInfo.LegalPersonName, - "QDRQ": time.Now().Format("2006-01-02"), - } - _, err = s.certificationEsignService.FillTemplate(ctx, certification, components) - if err != nil { - return nil, fmt.Errorf("生成e签宝合同失败: %w", err) - } - // 6. 重新获取更新后的认证申请数据 - updatedCertification, err := s.certManagementService.GetCertificationByID(ctx, certification.ID) - if err != nil { - return nil, err - } - return s.buildCertificationResponse(updatedCertification), nil -} - -// GetContractSignURL 获取合同签署链接 -// 业务流程:1. 获取认证申请 2. 获取企业信息 3. 获取签署链接 -func (s *CertificationApplicationServiceImpl) GetContractSignURL(ctx context.Context, cmd *commands.GetContractSignURLCommand) (*responses.ContractSignURLResponse, error) { - // 1. 获取认证申请 - certification, err := s.certManagementService.GetCertificationByUserID(ctx, cmd.UserID) - if err != nil { - return nil, fmt.Errorf("用户尚未创建认证申请: %w", err) - } - - // 2. 检查认证状态 - if certification.Status != enums.StatusContractApplied { - return nil, fmt.Errorf("当前状态不允许获取签署链接") - } - - // 3. 获取企业信息 - enterpriseInfo, err := s.enterpriseService.GetEnterpriseInfo(ctx, cmd.UserID) - if err != nil { - return nil, fmt.Errorf("获取企业信息失败: %w", err) - } - - if certification.ContractFileID == "" { - return nil, fmt.Errorf("请先申请合同文件") - } - - // 5. 发起签署 - signRecord, err := s.certificationEsignService.InitiateSign(ctx, certification, enterpriseInfo) - if err != nil { - return nil, fmt.Errorf("获取签署链接失败: %w", err) - } - // 转换状态 - err = s.certWorkflowService.ApplyContract(ctx, certification.ID) - if err != nil { - return nil, fmt.Errorf("转换状态失败: %w", err) - } - // 6. 计算过期时间(7天后) - expireAt := time.Now().AddDate(0, 0, 7).Format(time.RFC3339) - s.logger.Info("获取签署链接成功", - zap.String("user_id", cmd.UserID), - zap.String("certification_id", certification.ID), - zap.String("sign_flow_id", signRecord.EsignFlowID), - ) - - return &responses.ContractSignURLResponse{ - SignURL: signRecord.SignURL, - ShortURL: signRecord.SignShortURL, - SignFlowID: signRecord.EsignFlowID, - ExpireAt: expireAt, - }, nil -} - -// CompleteContractSign 完成合同签署 -// 业务流程:1. 获取认证申请 2. 完成合同签署 3. 自动完成认证 -func (s *CertificationApplicationServiceImpl) CompleteContractSign(ctx context.Context, cmd *commands.CompleteContractSignCommand) error { - // 1. 获取认证申请 - certification, err := s.certManagementService.GetCertificationByUserID(ctx, cmd.UserID) - if err != nil { - return fmt.Errorf("用户尚未创建认证申请: %w", err) - } - if certification.Status != enums.StatusContractApplied { - return fmt.Errorf("当前状态不允许完成合同签署") - } - // 2. 完成合同签署(状态转换) - if err := s.certWorkflowService.CompleteContractSign(ctx, certification.ID, cmd.ContractURL); err != nil { - return err - } - - // 3. 重新获取认证申请 - updatedCertification, err := s.certManagementService.GetCertificationByID(ctx, certification.ID) - if err != nil { - return err - } - - // 4. 如果合同已签署,自动完成认证 - if updatedCertification.Status == enums.StatusContractSigned { - if err := s.certWorkflowService.CompleteCertification(ctx, certification.ID); err != nil { - return err - } - } - - s.logger.Info("合同签署完成", - zap.String("user_id", cmd.UserID), - zap.String("certification_id", certification.ID), - zap.String("contract_url", cmd.ContractURL), - ) - - return nil -} - -// GetCertificationStatus 获取认证状态 -// 业务流程:1. 获取认证申请 2. 构建响应数据 -func (s *CertificationApplicationServiceImpl) GetCertificationStatus(ctx context.Context, query *queries.GetCertificationStatusQuery) (*responses.CertificationResponse, error) { - // 1. 获取认证申请 - certification, err := s.certManagementService.GetCertificationByUserID(ctx, query.UserID) - if err != nil { - // 如果用户没有认证申请,返回一个表示未开始的状态 - if errors.Is(err, gorm.ErrRecordNotFound) { - return &responses.CertificationResponse{ - ID: "", - UserID: query.UserID, - Status: "not_started", - StatusName: "未开始认证", - Progress: 0, - IsUserActionRequired: true, - InfoSubmittedAt: nil, - EnterpriseVerifiedAt: nil, - ContractAppliedAt: nil, - ContractSignedAt: nil, - CompletedAt: nil, - ContractURL: "", - CreatedAt: time.Time{}, - UpdatedAt: time.Time{}, - }, nil - } - return nil, err - } - - // 2. 构建响应 - return s.buildCertificationResponse(certification), nil -} - -// GetCertificationDetails 获取认证详情 -// 业务流程:1. 获取认证申请 2. 获取企业信息 3. 构建响应数据 -func (s *CertificationApplicationServiceImpl) GetCertificationDetails(ctx context.Context, query *queries.GetCertificationDetailsQuery) (*responses.CertificationResponse, error) { - // 1. 获取认证申请 - certification, err := s.certManagementService.GetCertificationByUserID(ctx, query.UserID) - if err != nil { - // 如果用户没有认证申请,返回错误 - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fmt.Errorf("用户尚未创建认证申请") - } - return nil, err - } - - // 2. 构建响应 - response := s.buildCertificationResponse(certification) - - // 3. 添加企业信息 - if certification.UserID != "" { - enterpriseInfo, err := s.enterpriseService.GetEnterpriseInfo(ctx, certification.UserID) - if err == nil && enterpriseInfo != nil { - response.Enterprise = s.buildEnterpriseInfoResponse(enterpriseInfo) - } + // 6. 构建响应 + response := &responses.CertificationStatisticsResponse{ + Period: query.Period, + TimeRange: domainQuery.GetTimeRange(), + Statistics: stats, + ProgressStats: progressStats, + GeneratedAt: domainQuery.StartDate, + Charts: s.generateChartsData(stats, progressStats), } return response, nil } -// GetCertificationProgress 获取认证进度 -// 业务流程:1. 获取认证申请 2. 获取进度信息 -func (s *CertificationApplicationServiceImpl) GetCertificationProgress(ctx context.Context, userID string) (map[string]interface{}, error) { - // 1. 获取认证申请 - certification, err := s.certManagementService.GetCertificationByUserID(ctx, userID) +// ================ e签宝回调处理 ================ + +// HandleEsignCallback 处理e签宝回调 +func (s *CertificationApplicationServiceImpl) HandleEsignCallback( + ctx context.Context, + cmd *commands.EsignCallbackCommand, +) (*responses.CallbackResponse, error) { + s.logger.Info("开始处理e签宝回调", + zap.String("certification_id", cmd.CertificationID), + zap.String("callback_type", cmd.CallbackType)) + + // 1. 解析回调数据 + callbackData, err := s.callbackHandler.ParseCallbackData(cmd.RawData) if err != nil { - // 如果用户没有认证申请,返回未开始状态 - if errors.Is(err, gorm.ErrRecordNotFound) { - return map[string]interface{}{ - "certification_id": "", - "user_id": userID, - "current_status": "not_started", - "status_name": "未开始认证", - "progress_percentage": 0, - "is_user_action_required": true, - "next_valid_statuses": []string{"pending"}, - "message": "用户尚未开始认证流程", - "created_at": nil, - "updated_at": nil, - }, nil - } - return nil, err + s.logger.Error("解析回调数据失败", zap.Error(err)) + return responses.NewCallbackResponse(false, cmd.CertificationID, cmd.CallbackType, + fmt.Sprintf("解析回调数据失败: %s", err.Error())), err } - // 2. 获取认证进度 - return s.certManagementService.GetCertificationProgress(ctx, certification.ID) -} - -// buildCertificationResponse 构建认证响应 -func (s *CertificationApplicationServiceImpl) buildCertificationResponse(certification *entities.Certification) *responses.CertificationResponse { - return &responses.CertificationResponse{ - ID: certification.ID, - UserID: certification.UserID, - Status: certification.Status, - StatusName: certification.GetStatusName(), - Progress: certification.GetProgressPercentage(), - IsUserActionRequired: certification.IsUserActionRequired(), - InfoSubmittedAt: certification.InfoSubmittedAt, - EnterpriseVerifiedAt: certification.EnterpriseVerifiedAt, - ContractAppliedAt: certification.ContractAppliedAt, - ContractSignedAt: certification.ContractSignedAt, - CompletedAt: certification.CompletedAt, - ContractURL: certification.ContractURL, - CreatedAt: certification.CreatedAt, - UpdatedAt: certification.UpdatedAt, + // 2. 构建工作流回调命令 + workflowCmd := &services.EsignCallbackCommand{ + CertificationID: cmd.CertificationID, + CallbackType: cmd.CallbackType, + CallbackData: callbackData, } -} -// buildEnterpriseInfoResponse 构建企业信息响应 -func (s *CertificationApplicationServiceImpl) buildEnterpriseInfoResponse(enterpriseInfo *user_entities.EnterpriseInfo) *responses.EnterpriseInfoResponse { - return &responses.EnterpriseInfoResponse{ - ID: enterpriseInfo.ID, - CompanyName: enterpriseInfo.CompanyName, - UnifiedSocialCode: enterpriseInfo.UnifiedSocialCode, - LegalPersonName: enterpriseInfo.LegalPersonName, - LegalPersonID: enterpriseInfo.LegalPersonID, - CreatedAt: enterpriseInfo.CreatedAt, - UpdatedAt: enterpriseInfo.UpdatedAt, + // 3. 根据回调类型分发处理 + var workflowResult *services.WorkflowResult + switch cmd.CallbackType { + case "auth_result": + workflowResult, err = s.workflowOrchestrator.HandleEnterpriseVerificationCallback(ctx, workflowCmd) + case "sign_result": + workflowResult, err = s.workflowOrchestrator.HandleContractSignCallback(ctx, workflowCmd) + default: + err = fmt.Errorf("不支持的回调类型: %s", cmd.CallbackType) } -} -// 企业认证成功后操作 -func (s *CertificationApplicationServiceImpl) completeEnterpriseAuth(ctx context.Context, certification *entities.Certification) error { - err := s.txManager.ExecuteInTx(ctx, func(txCtx context.Context) error { - // 1. 获取企业信息提交记录 - enterpriseRecord, err := s.enterpriseRecordService.GetLatestByUserID(txCtx, certification.UserID) - if err != nil { - return fmt.Errorf("获取企业信息失败: %w", err) - } - // 2. 转换状态 - if err := s.certWorkflowService.CompleteEnterpriseVerification(txCtx, certification.ID); err != nil { - return err - } - // 3. 创建企业信息 - _, err = s.enterpriseService.CreateEnterpriseInfo(txCtx, certification.UserID, enterpriseRecord.CompanyName, enterpriseRecord.UnifiedSocialCode, enterpriseRecord.LegalPersonName, enterpriseRecord.LegalPersonID) - if err != nil { - s.logger.Warn("创建用户企业信息失败", zap.Error(err)) - return err - } - return nil - }) if err != nil { - return fmt.Errorf("完成企业认证失败: %w", err) + s.logger.Error("处理回调失败", zap.Error(err)) + return responses.NewCallbackResponse(false, cmd.CertificationID, cmd.CallbackType, + fmt.Sprintf("处理回调失败: %s", err.Error())), err + } + + // 4. 构建成功响应 + response := responses.NewCallbackResponse(true, cmd.CertificationID, cmd.CallbackType, workflowResult.Message) + + // 5. 设置状态转换信息 + if workflowResult.StateTransition != nil { + response.StateTransition = workflowResult.StateTransition + response.OldStatus = workflowResult.StateTransition.OldStatus + response.NewStatus = workflowResult.StateTransition.NewStatus + } + + s.logger.Info("e签宝回调处理成功", zap.String("certification_id", cmd.CertificationID)) + return response, nil +} + +// ================ 管理员操作 ================ + +// ForceTransitionStatus 强制状态转换(管理员) +func (s *CertificationApplicationServiceImpl) ForceTransitionStatus( + ctx context.Context, + cmd *commands.ForceTransitionStatusCommand, +) (*responses.CertificationResponse, error) { + s.logger.Info("开始强制状态转换", + zap.String("certification_id", cmd.CertificationID), + zap.String("admin_id", cmd.AdminID), + zap.String("target_status", string(cmd.TargetStatus))) + + // 1. 权限验证(这里简化,实际应该有更复杂的权限验证) + // TODO: 实现管理员权限验证逻辑 + + // 2. 执行状态转换 + result, err := s.aggregateService.TransitionState( + ctx, + cmd.CertificationID, + cmd.TargetStatus, + enums.ActorTypeAdmin, + cmd.AdminID, + cmd.Reason, + map[string]interface{}{ + "force": cmd.Force, + }, + ) + if err != nil { + s.logger.Error("强制状态转换失败", zap.Error(err)) + return nil, fmt.Errorf("强制状态转换失败: %w", err) + } + + // 3. 加载最新的认证信息 + cert, err := s.aggregateService.LoadCertification(ctx, cmd.CertificationID) + if err != nil { + s.logger.Error("加载认证信息失败", zap.Error(err)) + return nil, fmt.Errorf("加载认证信息失败: %w", err) + } + + // 4. 转换为响应DTO + response := s.convertToResponse(cert) + + // 5. 添加状态转换信息 + if result != nil { + response.Metadata = map[string]interface{}{ + "state_transition": result, + "admin_operation": true, + } + } + + s.logger.Info("强制状态转换成功", zap.String("certification_id", cmd.CertificationID)) + return response, nil +} + +// GetSystemMonitoring 获取系统监控数据 +func (s *CertificationApplicationServiceImpl) GetSystemMonitoring( + ctx context.Context, + query *queries.GetSystemMonitoringQuery, +) (*responses.SystemMonitoringResponse, error) { + s.logger.Debug("获取系统监控数据", zap.String("time_range", query.TimeRange)) + + // 1. 获取基础统计数据 + stats, err := s.queryRepository.GetStatistics(ctx, repositories.PeriodDaily) + if err != nil { + s.logger.Error("获取基础统计失败", zap.Error(err)) + return nil, fmt.Errorf("获取系统监控数据失败: %w", err) + } + + // 2. 构建监控指标 + metrics := make(map[string]interface{}) + + if query.ShouldIncludeMetric("certification_count") { + metrics["certification_count"] = stats.TotalCertifications + } + if query.ShouldIncludeMetric("success_rate") { + metrics["success_rate"] = stats.SuccessRate + } + if query.ShouldIncludeMetric("failure_rate") { + metrics["failure_rate"] = 1.0 - stats.SuccessRate + } + if query.ShouldIncludeMetric("avg_processing_time") { + metrics["avg_processing_time"] = stats.AvgProcessingTime.String() + } + if query.ShouldIncludeMetric("status_distribution") { + metrics["status_distribution"] = stats.StatusDistribution + } + + // 3. 生成系统警告 + alerts := s.generateSystemAlerts(stats) + + // 4. 评估系统健康状态 + systemHealth := s.evaluateSystemHealth(stats, alerts) + + // 5. 构建响应 + response := &responses.SystemMonitoringResponse{ + TimeRange: query.TimeRange, + Metrics: metrics, + Alerts: alerts, + SystemHealth: systemHealth, + LastUpdatedAt: stats.StartDate, + } + + return response, nil +} + +// ================ 辅助方法 ================ + +// convertToResponse 转换实体为响应DTO +func (s *CertificationApplicationServiceImpl) convertToResponse(cert *entities.Certification) *responses.CertificationResponse { + response := &responses.CertificationResponse{ + ID: cert.ID, + UserID: cert.UserID, + Status: cert.Status, + StatusName: enums.GetStatusName(cert.Status), + Progress: cert.GetProgress(), + CreatedAt: cert.CreatedAt, + UpdatedAt: cert.UpdatedAt, + InfoSubmittedAt: cert.InfoSubmittedAt, + EnterpriseVerifiedAt: cert.EnterpriseVerifiedAt, + ContractAppliedAt: cert.ContractAppliedAt, + ContractSignedAt: cert.ContractSignedAt, + IsCompleted: cert.IsCompleted(), + IsFailed: enums.IsFailureStatus(cert.Status), + IsUserActionRequired: cert.IsUserActionRequired(), + NextAction: enums.GetUserActionHint(cert.Status), + AvailableActions: cert.GetAvailableActions(), + RetryCount: cert.RetryCount, + Metadata: make(map[string]interface{}), + } + + // 设置企业信息(从认证实体中构建) + // TODO: 这里需要从企业信息服务或其他地方获取完整的企业信息 + // response.EnterpriseInfo = cert.EnterpriseInfo + + // 设置合同信息(从认证实体中构建) + if cert.ContractFileID != "" || cert.EsignFlowID != "" { + // TODO: 从认证实体字段构建合同信息值对象 + // response.ContractInfo = &value_objects.ContractInfo{...} + } + + // 设置失败信息 + if enums.IsFailureStatus(cert.Status) { + response.FailureReason = cert.FailureReason + response.FailureReasonName = enums.GetFailureReasonName(cert.FailureReason) + response.FailureMessage = cert.FailureMessage + response.CanRetry = enums.IsRetryable(cert.FailureReason) + } + + return response +} + +// generateChartsData 生成图表数据 +func (s *CertificationApplicationServiceImpl) generateChartsData( + stats *repositories.CertificationStatistics, + progressStats *repositories.CertificationProgressStats, +) map[string]interface{} { + charts := make(map[string]interface{}) + + // 状态分布饼图 + if stats.StatusDistribution != nil { + statusChart := make(map[string]interface{}) + statusChart["type"] = "pie" + statusChart["data"] = stats.StatusDistribution + charts["status_distribution"] = statusChart + } + + // 失败原因分布 + if stats.FailureDistribution != nil { + failureChart := make(map[string]interface{}) + failureChart["type"] = "bar" + failureChart["data"] = stats.FailureDistribution + charts["failure_distribution"] = failureChart + } + + // 进度分布 + if progressStats != nil && progressStats.ProgressDistribution != nil { + progressChart := make(map[string]interface{}) + progressChart["type"] = "histogram" + progressChart["data"] = progressStats.ProgressDistribution + charts["progress_distribution"] = progressChart + } + + return charts +} + +// generateSystemAlerts 生成系统警告 +func (s *CertificationApplicationServiceImpl) generateSystemAlerts(stats *repositories.CertificationStatistics) []responses.SystemAlert { + var alerts []responses.SystemAlert + + // 成功率警告 + if stats.SuccessRate < 0.8 { + level := "warning" + if stats.SuccessRate < 0.6 { + level = "critical" + } + + alert := responses.SystemAlert{ + Level: level, + Type: "success_rate_low", + Message: fmt.Sprintf("认证成功率过低:%.1f%%", stats.SuccessRate*100), + Metric: "success_rate", + Value: stats.SuccessRate, + Threshold: 0.8, + CreatedAt: stats.StartDate, + } + alerts = append(alerts, alert) + } + + // 处理时间警告 + if stats.AvgProcessingTime.Hours() > 24 { + alert := responses.SystemAlert{ + Level: "warning", + Type: "processing_time_high", + Message: fmt.Sprintf("平均处理时间过长:%.1f小时", stats.AvgProcessingTime.Hours()), + Metric: "avg_processing_time", + Value: stats.AvgProcessingTime.String(), + Threshold: "24h", + CreatedAt: stats.StartDate, + } + alerts = append(alerts, alert) + } + + return alerts +} + +// evaluateSystemHealth 评估系统健康状态 +func (s *CertificationApplicationServiceImpl) evaluateSystemHealth( + stats *repositories.CertificationStatistics, + alerts []responses.SystemAlert, +) responses.SystemHealthStatus { + overall := "healthy" + components := map[string]string{ + "certification_service": "healthy", + "esign_integration": "healthy", + "database": "healthy", + "state_machine": "healthy", + } + + // 根据警告判断健康状态 + for _, alert := range alerts { + if alert.Level == "critical" { + overall = "critical" + break + } else if alert.Level == "warning" && overall == "healthy" { + overall = "warning" + } + } + + // 根据统计数据评估组件状态 + if stats.SuccessRate < 0.6 { + components["certification_service"] = "critical" + components["esign_integration"] = "warning" + } else if stats.SuccessRate < 0.8 { + components["certification_service"] = "warning" + } + + return responses.SystemHealthStatus{ + Overall: overall, + Components: components, + LastCheck: stats.StartDate, + Details: map[string]interface{}{ + "total_certifications": stats.TotalCertifications, + "success_rate": stats.SuccessRate, + "avg_processing_time": stats.AvgProcessingTime.String(), + }, } - return nil } diff --git a/internal/application/certification/dto/commands/certification_commands.go b/internal/application/certification/dto/commands/certification_commands.go index fed3c41..b507f61 100644 --- a/internal/application/certification/dto/commands/certification_commands.go +++ b/internal/application/certification/dto/commands/certification_commands.go @@ -1,21 +1,50 @@ package commands -// SubmitEnterpriseInfoCommand 提交企业信息命令 -// 用于用户提交企业四要素信息,完成企业信息验证 -// 如果用户没有认证申请,系统会自动创建 -type SubmitEnterpriseInfoCommand struct { - UserID string `json:"-" comment:"用户唯一标识,从JWT token获取,不在JSON中暴露"` - CompanyName string `json:"company_name" binding:"required,min=2,max=100" comment:"企业名称,如:北京科技有限公司"` - UnifiedSocialCode string `json:"unified_social_code" binding:"required,social_credit_code" comment:"统一社会信用代码,18位企业唯一标识,如:91110000123456789X"` - LegalPersonName string `json:"legal_person_name" binding:"required,min=2,max=20" comment:"法定代表人姓名,如:张三"` - LegalPersonID string `json:"legal_person_id" binding:"required,id_card" comment:"法定代表人身份证号码,18位,如:110101199001011234"` - LegalPersonPhone string `json:"legal_person_phone" binding:"required,phone" comment:"法定代表人手机号,11位,如:13800138000"` - VerificationCode string `json:"verification_code" binding:"required,len=6" comment:"验证码"` +import ( + "tyapi-server/internal/domains/certification/entities/value_objects" + "tyapi-server/internal/domains/certification/enums" +) + +// CreateCertificationCommand 创建认证申请命令 +type CreateCertificationCommand struct { + UserID string `json:"-"` } -// CompleteContractSignCommand 完成合同签署命令 -// 用于用户完成合同签署,提交合同URL -type CompleteContractSignCommand struct { - UserID string `json:"-" comment:"用户唯一标识,从JWT token获取,不在JSON中暴露"` - ContractURL string `json:"contract_url" binding:"required,url,min=10,max=500" comment:"合同签署后的URL地址"` +// ApplyContractCommand 申请合同命令 +type ApplyContractCommand struct { + CertificationID string `json:"certification_id" validate:"required"` + UserID string `json:"user_id" validate:"required"` +} + +// RetryOperationCommand 重试操作命令 +type RetryOperationCommand struct { + CertificationID string `json:"certification_id" validate:"required"` + UserID string `json:"user_id" validate:"required"` + Operation string `json:"operation" validate:"required,oneof=enterprise_verification contract_application"` + Reason string `json:"reason,omitempty"` +} + +// EsignCallbackCommand e签宝回调命令 +type EsignCallbackCommand struct { + CertificationID string `json:"certification_id" validate:"required"` + CallbackType string `json:"callback_type" validate:"required,oneof=auth_result sign_result flow_status"` + RawData string `json:"raw_data" validate:"required"` + Headers map[string]string `json:"headers,omitempty"` + QueryParams map[string]string `json:"query_params,omitempty"` +} + +// ForceTransitionStatusCommand 强制状态转换命令(管理员) +type ForceTransitionStatusCommand struct { + CertificationID string `json:"certification_id" validate:"required"` + AdminID string `json:"admin_id" validate:"required"` + TargetStatus enums.CertificationStatus `json:"target_status" validate:"required"` + Reason string `json:"reason" validate:"required"` + Force bool `json:"force,omitempty"` // 是否强制执行,跳过业务规则验证 +} + +// SubmitEnterpriseInfoCommand 提交企业信息命令 +type SubmitEnterpriseInfoCommand struct { + CertificationID string `json:"certification_id" validate:"required"` + UserID string `json:"-" validate:"required"` + EnterpriseInfo *value_objects.EnterpriseInfo `json:"enterprise_info" validate:"required"` } diff --git a/internal/application/certification/dto/queries/certification_queries.go b/internal/application/certification/dto/queries/certification_queries.go index 5751346..1b6c9ae 100644 --- a/internal/application/certification/dto/queries/certification_queries.go +++ b/internal/application/certification/dto/queries/certification_queries.go @@ -1,13 +1,185 @@ package queries -// GetCertificationStatusQuery 获取认证状态查询 -// 用于查询用户当前认证申请的进度状态 -type GetCertificationStatusQuery struct { - UserID string `json:"user_id" binding:"required,uuid" comment:"用户唯一标识,用于查询该用户的认证申请状态"` +import ( + "time" + + "tyapi-server/internal/domains/certification/enums" + domainQueries "tyapi-server/internal/domains/certification/repositories/queries" +) + +// GetCertificationQuery 获取认证详情查询 +type GetCertificationQuery struct { + CertificationID string `json:"certification_id" validate:"required"` + UserID string `json:"user_id,omitempty"` // 用于权限验证 } -// GetCertificationDetailsQuery 获取认证详情查询 -// 用于查询用户认证申请的详细信息,包括所有相关记录 -type GetCertificationDetailsQuery struct { - UserID string `json:"user_id" binding:"required,uuid" comment:"用户唯一标识,用于查询该用户的认证申请详细信息"` +// GetUserCertificationsQuery 获取用户认证列表查询 +type GetUserCertificationsQuery struct { + UserID string `json:"user_id" validate:"required"` + Status enums.CertificationStatus `json:"status,omitempty"` + IncludeCompleted bool `json:"include_completed,omitempty"` + IncludeFailed bool `json:"include_failed,omitempty"` + Page int `json:"page"` + PageSize int `json:"page_size"` +} + +// ToDomainQuery 转换为领域查询对象 +func (q *GetUserCertificationsQuery) ToDomainQuery() *domainQueries.UserCertificationsQuery { + domainQuery := &domainQueries.UserCertificationsQuery{ + UserID: q.UserID, + Status: q.Status, + IncludeCompleted: q.IncludeCompleted, + IncludeFailed: q.IncludeFailed, + Page: q.Page, + PageSize: q.PageSize, + } + domainQuery.DefaultValues() + return domainQuery +} + +// ListCertificationsQuery 认证列表查询(管理员) +type ListCertificationsQuery struct { + Page int `json:"page"` + PageSize int `json:"page_size"` + SortBy string `json:"sort_by"` + SortOrder string `json:"sort_order"` + UserID string `json:"user_id,omitempty"` + Status enums.CertificationStatus `json:"status,omitempty"` + Statuses []enums.CertificationStatus `json:"statuses,omitempty"` + FailureReason enums.FailureReason `json:"failure_reason,omitempty"` + CreatedAfter *time.Time `json:"created_after,omitempty"` + CreatedBefore *time.Time `json:"created_before,omitempty"` + CompanyName string `json:"company_name,omitempty"` + LegalPersonName string `json:"legal_person_name,omitempty"` + SearchKeyword string `json:"search_keyword,omitempty"` +} + +// ToDomainQuery 转换为领域查询对象 +func (q *ListCertificationsQuery) ToDomainQuery() *domainQueries.ListCertificationsQuery { + domainQuery := &domainQueries.ListCertificationsQuery{ + Page: q.Page, + PageSize: q.PageSize, + SortBy: q.SortBy, + SortOrder: q.SortOrder, + UserID: q.UserID, + Status: q.Status, + Statuses: q.Statuses, + FailureReason: q.FailureReason, + CreatedAfter: q.CreatedAfter, + CreatedBefore: q.CreatedBefore, + CompanyName: q.CompanyName, + LegalPersonName: q.LegalPersonName, + SearchKeyword: q.SearchKeyword, + } + domainQuery.DefaultValues() + return domainQuery +} + +// SearchCertificationsQuery 搜索认证查询 +type SearchCertificationsQuery struct { + Keyword string `json:"keyword" validate:"required,min=2"` + SearchFields []string `json:"search_fields,omitempty"` + Statuses []enums.CertificationStatus `json:"statuses,omitempty"` + UserID string `json:"user_id,omitempty"` + Page int `json:"page"` + PageSize int `json:"page_size"` + SortBy string `json:"sort_by"` + SortOrder string `json:"sort_order"` + ExactMatch bool `json:"exact_match,omitempty"` +} + +// ToDomainQuery 转换为领域查询对象 +func (q *SearchCertificationsQuery) ToDomainQuery() *domainQueries.SearchCertificationsQuery { + domainQuery := &domainQueries.SearchCertificationsQuery{ + Keyword: q.Keyword, + SearchFields: q.SearchFields, + Statuses: q.Statuses, + UserID: q.UserID, + Page: q.Page, + PageSize: q.PageSize, + SortBy: q.SortBy, + SortOrder: q.SortOrder, + ExactMatch: q.ExactMatch, + } + domainQuery.DefaultValues() + return domainQuery +} + +// GetCertificationStatisticsQuery 认证统计查询 +type GetCertificationStatisticsQuery struct { + StartDate time.Time `json:"start_date" validate:"required"` + EndDate time.Time `json:"end_date" validate:"required"` + Period string `json:"period" validate:"oneof=daily weekly monthly yearly"` + GroupBy []string `json:"group_by,omitempty"` + UserIDs []string `json:"user_ids,omitempty"` + Statuses []enums.CertificationStatus `json:"statuses,omitempty"` + IncludeProgressStats bool `json:"include_progress_stats,omitempty"` + IncludeRetryStats bool `json:"include_retry_stats,omitempty"` + IncludeTimeStats bool `json:"include_time_stats,omitempty"` +} + +// ToDomainQuery 转换为领域查询对象 +func (q *GetCertificationStatisticsQuery) ToDomainQuery() *domainQueries.CertificationStatisticsQuery { + return &domainQueries.CertificationStatisticsQuery{ + StartDate: q.StartDate, + EndDate: q.EndDate, + Period: q.Period, + GroupBy: q.GroupBy, + UserIDs: q.UserIDs, + Statuses: q.Statuses, + IncludeProgressStats: q.IncludeProgressStats, + IncludeRetryStats: q.IncludeRetryStats, + IncludeTimeStats: q.IncludeTimeStats, + } +} + +// GetSystemMonitoringQuery 系统监控查询 +type GetSystemMonitoringQuery struct { + TimeRange string `json:"time_range" validate:"oneof=1h 6h 24h 7d 30d"` + Metrics []string `json:"metrics,omitempty"` // 指定要获取的指标类型 +} + +// GetAvailableMetrics 获取可用的监控指标 +func (q *GetSystemMonitoringQuery) GetAvailableMetrics() []string { + return []string{ + "certification_count", + "success_rate", + "failure_rate", + "avg_processing_time", + "status_distribution", + "retry_count", + "esign_callback_success_rate", + } +} + +// GetTimeRangeDuration 获取时间范围对应的持续时间 +func (q *GetSystemMonitoringQuery) GetTimeRangeDuration() time.Duration { + switch q.TimeRange { + case "1h": + return time.Hour + case "6h": + return 6 * time.Hour + case "24h": + return 24 * time.Hour + case "7d": + return 7 * 24 * time.Hour + case "30d": + return 30 * 24 * time.Hour + default: + return 24 * time.Hour // 默认24小时 + } +} + +// ShouldIncludeMetric 检查是否应该包含指定指标 +func (q *GetSystemMonitoringQuery) ShouldIncludeMetric(metric string) bool { + if len(q.Metrics) == 0 { + return true // 如果没有指定,包含所有指标 + } + + for _, m := range q.Metrics { + if m == metric { + return true + } + } + return false } diff --git a/internal/application/certification/dto/responses/certification_responses.go b/internal/application/certification/dto/responses/certification_responses.go index 696f075..5f7435f 100644 --- a/internal/application/certification/dto/responses/certification_responses.go +++ b/internal/application/certification/dto/responses/certification_responses.go @@ -1,45 +1,235 @@ package responses import ( + "fmt" "time" + "tyapi-server/internal/domains/certification/entities/value_objects" "tyapi-server/internal/domains/certification/enums" + "tyapi-server/internal/domains/certification/repositories" + "tyapi-server/internal/domains/certification/services/state_machine" ) // CertificationResponse 认证响应 type CertificationResponse struct { - ID string `json:"id"` - UserID string `json:"user_id"` - Status enums.CertificationStatus `json:"status"` - StatusName string `json:"status_name"` - Progress int `json:"progress"` - IsUserActionRequired bool `json:"is_user_action_required"` - InfoSubmittedAt *time.Time `json:"info_submitted_at,omitempty"` - EnterpriseVerifiedAt *time.Time `json:"enterprise_verified_at,omitempty"` - ContractAppliedAt *time.Time `json:"contract_applied_at,omitempty"` - ContractSignedAt *time.Time `json:"contract_signed_at,omitempty"` - CompletedAt *time.Time `json:"completed_at,omitempty"` - Enterprise *EnterpriseInfoResponse `json:"enterprise,omitempty"` - ContractURL string `json:"contract_url,omitempty"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID string `json:"id"` + UserID string `json:"user_id"` + Status enums.CertificationStatus `json:"status"` + StatusName string `json:"status_name"` + Progress int `json:"progress"` + + // 企业信息 + EnterpriseInfo *value_objects.EnterpriseInfo `json:"enterprise_info,omitempty"` + + // 合同信息 + ContractInfo *value_objects.ContractInfo `json:"contract_info,omitempty"` + + // 时间戳 + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + InfoSubmittedAt *time.Time `json:"info_submitted_at,omitempty"` + EnterpriseVerifiedAt *time.Time `json:"enterprise_verified_at,omitempty"` + ContractAppliedAt *time.Time `json:"contract_applied_at,omitempty"` + ContractSignedAt *time.Time `json:"contract_signed_at,omitempty"` + + // 业务状态 + IsCompleted bool `json:"is_completed"` + IsFailed bool `json:"is_failed"` + IsUserActionRequired bool `json:"is_user_action_required"` + + // 失败信息 + FailureReason enums.FailureReason `json:"failure_reason,omitempty"` + FailureReasonName string `json:"failure_reason_name,omitempty"` + FailureMessage string `json:"failure_message,omitempty"` + CanRetry bool `json:"can_retry,omitempty"` + RetryCount int `json:"retry_count,omitempty"` + + // 用户操作提示 + NextAction string `json:"next_action,omitempty"` + AvailableActions []string `json:"available_actions,omitempty"` + + // 元数据 + Metadata map[string]interface{} `json:"metadata,omitempty"` } -// EnterpriseInfoResponse 企业信息响应 -type EnterpriseInfoResponse struct { - ID string `json:"id"` - CompanyName string `json:"company_name"` - UnifiedSocialCode string `json:"unified_social_code"` - LegalPersonName string `json:"legal_person_name"` - LegalPersonID string `json:"legal_person_id"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` +// CertificationListResponse 认证列表响应 +type CertificationListResponse struct { + Items []*CertificationResponse `json:"items"` + Total int64 `json:"total"` + Page int `json:"page"` + PageSize int `json:"page_size"` + TotalPages int `json:"total_pages"` } -// EnterpriseAuthURLResponse 企业认证链接响应 -type EnterpriseAuthURLResponse struct { - EsignFlowID string `json:"esign_flow_id"` // e签宝认证流程ID - AuthURL string `json:"auth_url"` // 认证链接 - ShortURL string `json:"short_url"` // 短链接 - ExpireAt string `json:"expire_at"` // 过期时间 +// ContractSignUrlResponse 合同签署URL响应 +type ContractSignUrlResponse struct { + CertificationID string `json:"certification_id"` + ContractSignURL string `json:"contract_sign_url"` + ContractURL string `json:"contract_url,omitempty"` + ExpireAt *time.Time `json:"expire_at,omitempty"` + NextAction string `json:"next_action"` + Message string `json:"message"` +} + +// CallbackResponse 回调响应 +type CallbackResponse struct { + Success bool `json:"success"` + CertificationID string `json:"certification_id"` + CallbackType string `json:"callback_type"` + ProcessedAt time.Time `json:"processed_at"` + OldStatus enums.CertificationStatus `json:"old_status,omitempty"` + NewStatus enums.CertificationStatus `json:"new_status,omitempty"` + Message string `json:"message"` + StateTransition *state_machine.StateTransitionResult `json:"state_transition,omitempty"` +} + +// CertificationStatisticsResponse 认证统计响应 +type CertificationStatisticsResponse struct { + Period string `json:"period"` + TimeRange string `json:"time_range"` + Statistics *repositories.CertificationStatistics `json:"statistics"` + ProgressStats *repositories.CertificationProgressStats `json:"progress_stats,omitempty"` + Charts map[string]interface{} `json:"charts,omitempty"` + GeneratedAt time.Time `json:"generated_at"` +} + +// SystemMonitoringResponse 系统监控响应 +type SystemMonitoringResponse struct { + TimeRange string `json:"time_range"` + Metrics map[string]interface{} `json:"metrics"` + Alerts []SystemAlert `json:"alerts,omitempty"` + SystemHealth SystemHealthStatus `json:"system_health"` + LastUpdatedAt time.Time `json:"last_updated_at"` +} + +// SystemAlert 系统警告 +type SystemAlert struct { + Level string `json:"level"` // info, warning, error, critical + Type string `json:"type"` // 警告类型 + Message string `json:"message"` // 警告消息 + Metric string `json:"metric"` // 相关指标 + Value interface{} `json:"value"` // 当前值 + Threshold interface{} `json:"threshold"` // 阈值 + CreatedAt time.Time `json:"created_at"` + Metadata map[string]interface{} `json:"metadata,omitempty"` +} + +// SystemHealthStatus 系统健康状态 +type SystemHealthStatus struct { + Overall string `json:"overall"` // healthy, warning, critical + Components map[string]string `json:"components"` // 各组件状态 + LastCheck time.Time `json:"last_check"` + Details map[string]interface{} `json:"details,omitempty"` +} + +// ================ 响应构建辅助方法 ================ + +// NewCertificationListResponse 创建认证列表响应 +func NewCertificationListResponse(items []*CertificationResponse, total int64, page, pageSize int) *CertificationListResponse { + totalPages := int((total + int64(pageSize) - 1) / int64(pageSize)) + if totalPages == 0 { + totalPages = 1 + } + + return &CertificationListResponse{ + Items: items, + Total: total, + Page: page, + PageSize: pageSize, + TotalPages: totalPages, + } +} + +// NewContractSignUrlResponse 创建合同签署URL响应 +func NewContractSignUrlResponse(certificationID, signURL, contractURL, nextAction, message string) *ContractSignUrlResponse { + response := &ContractSignUrlResponse{ + CertificationID: certificationID, + ContractSignURL: signURL, + ContractURL: contractURL, + NextAction: nextAction, + Message: message, + } + + // 设置过期时间(默认24小时) + expireAt := time.Now().Add(24 * time.Hour) + response.ExpireAt = &expireAt + + return response +} + +// NewCallbackResponse 创建回调响应 +func NewCallbackResponse(success bool, certificationID, callbackType, message string) *CallbackResponse { + return &CallbackResponse{ + Success: success, + CertificationID: certificationID, + CallbackType: callbackType, + ProcessedAt: time.Now(), + Message: message, + } +} + +// NewSystemAlert 创建系统警告 +func NewSystemAlert(level, alertType, message, metric string, value, threshold interface{}) *SystemAlert { + return &SystemAlert{ + Level: level, + Type: alertType, + Message: message, + Metric: metric, + Value: value, + Threshold: threshold, + CreatedAt: time.Now(), + Metadata: make(map[string]interface{}), + } +} + +// IsSuccess 检查响应是否成功 +func (r *CallbackResponse) IsSuccess() bool { + return r.Success +} + +// HasStateTransition 检查是否有状态转换 +func (r *CallbackResponse) HasStateTransition() bool { + return r.StateTransition != nil +} + +// GetStatusChange 获取状态变更描述 +func (r *CallbackResponse) GetStatusChange() string { + if !r.HasStateTransition() { + return "" + } + + if r.OldStatus == r.NewStatus { + return "状态无变化" + } + + return fmt.Sprintf("从 %s 转换为 %s", + enums.GetStatusName(r.OldStatus), + enums.GetStatusName(r.NewStatus)) +} + +// IsHealthy 检查系统是否健康 +func (r *SystemMonitoringResponse) IsHealthy() bool { + return r.SystemHealth.Overall == "healthy" +} + +// GetCriticalAlerts 获取严重警告 +func (r *SystemMonitoringResponse) GetCriticalAlerts() []*SystemAlert { + var criticalAlerts []*SystemAlert + for i := range r.Alerts { + if r.Alerts[i].Level == "critical" { + criticalAlerts = append(criticalAlerts, &r.Alerts[i]) + } + } + return criticalAlerts +} + +// HasAlerts 检查是否有警告 +func (r *SystemMonitoringResponse) HasAlerts() bool { + return len(r.Alerts) > 0 +} + +// GetMetricValue 获取指标值 +func (r *SystemMonitoringResponse) GetMetricValue(metric string) (interface{}, bool) { + value, exists := r.Metrics[metric] + return value, exists } diff --git a/internal/application/certification/esign_callback_application_service_impl.go b/internal/application/certification/esign_callback_application_service_impl.go deleted file mode 100644 index 826142e..0000000 --- a/internal/application/certification/esign_callback_application_service_impl.go +++ /dev/null @@ -1,385 +0,0 @@ -package certification - -import ( - "context" - "crypto/hmac" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "fmt" - "sort" - "strings" - - "go.uber.org/zap" - - "tyapi-server/internal/domains/certification/entities" - "tyapi-server/internal/domains/certification/enums" - "tyapi-server/internal/domains/certification/services" - user_services "tyapi-server/internal/domains/user/services" - "tyapi-server/internal/shared/database" - esign_service "tyapi-server/internal/shared/esign" -) - -// EsignCallbackData e签宝回调数据结构 -type EsignCallbackData struct { - Action string `json:"action"` - Timestamp int64 `json:"timestamp"` - AuthFlowId string `json:"authFlowId,omitempty"` - SignFlowId string `json:"signFlowId,omitempty"` - CustomBizNum string `json:"customBizNum,omitempty"` - SignOrder int `json:"signOrder,omitempty"` - OperateTime int64 `json:"operateTime,omitempty"` - SignResult int `json:"signResult,omitempty"` - ResultDescription string `json:"resultDescription,omitempty"` - AuthType string `json:"authType,omitempty"` - SignFlowStatus string `json:"signFlowStatus,omitempty"` - Operator *EsignOperator `json:"operator,omitempty"` - PsnInfo *EsignPsnInfo `json:"psnInfo,omitempty"` - Organization *EsignOrganization `json:"organization,omitempty"` -} - -// EsignOperator 签署人信息 -type EsignOperator struct { - PsnId string `json:"psnId"` - PsnAccount *EsignPsnAccount `json:"psnAccount"` -} - -// EsignPsnInfo 个人认证信息 -type EsignPsnInfo struct { - PsnId string `json:"psnId"` - PsnAccount *EsignPsnAccount `json:"psnAccount"` -} - -// EsignPsnAccount 个人账户信息 -type EsignPsnAccount struct { - AccountMobile string `json:"accountMobile"` - AccountEmail string `json:"accountEmail"` -} - -// EsignOrganization 企业信息 -type EsignOrganization struct { - OrgName string `json:"orgName"` - // 可以根据需要添加更多企业信息字段 -} - -// EsignCallbackApplicationServiceImpl e签宝回调应用服务实现 -type EsignCallbackApplicationServiceImpl struct { - certManagementService *services.CertificationManagementService - certWorkflowService *services.CertificationWorkflowService - certificationEsignService *services.CertificationEsignService - enterpriseService *user_services.EnterpriseService - esignService *esign_service.Client - enterpriseRecordService *services.EnterpriseInfoSubmitRecordService - txManager *database.TransactionManager - logger *zap.Logger -} - -// NewEsignCallbackApplicationService 创建e签宝回调应用服务 -func NewEsignCallbackApplicationService( - certManagementService *services.CertificationManagementService, - certWorkflowService *services.CertificationWorkflowService, - certificationEsignService *services.CertificationEsignService, - enterpriseService *user_services.EnterpriseService, - esignService *esign_service.Client, - enterpriseRecordService *services.EnterpriseInfoSubmitRecordService, - txManager *database.TransactionManager, - logger *zap.Logger, -) EsignCallbackApplicationService { - return &EsignCallbackApplicationServiceImpl{ - certManagementService: certManagementService, - certWorkflowService: certWorkflowService, - certificationEsignService: certificationEsignService, - enterpriseService: enterpriseService, - esignService: esignService, - enterpriseRecordService: enterpriseRecordService, - txManager: txManager, - logger: logger, - } -} - -// HandleCallback 处理e签宝回调 -func (s *EsignCallbackApplicationServiceImpl) HandleCallback(ctx context.Context, callbackData map[string]interface{}, headers map[string]string, queryParams map[string]string) error { - s.logger.Info("开始处理e签宝回调", zap.Any("callback_data", callbackData)) - - // 1. 验签 - if err := s.verifySignature(callbackData, headers, queryParams); err != nil { - s.logger.Error("e签宝回调验签失败", zap.Error(err)) - return fmt.Errorf("验签失败: %w", err) - } - - // 2. 解析回调数据为结构体 - var callback EsignCallbackData - jsonBytes, err := json.Marshal(callbackData) - if err != nil { - return fmt.Errorf("序列化回调数据失败: %w", err) - } - - if err := json.Unmarshal(jsonBytes, &callback); err != nil { - return fmt.Errorf("解析回调数据失败: %w", err) - } - - // 3. 记录回调信息 - s.logger.Info("e签宝回调信息解析", - zap.String("action", callback.Action), - zap.String("auth_flow_id", callback.AuthFlowId), - zap.String("sign_flow_id", callback.SignFlowId), - zap.String("auth_type", callback.AuthType), - zap.String("sign_flow_status", callback.SignFlowStatus), - zap.Int64("timestamp", callback.Timestamp), - ) - - // 4. 根据回调类型处理业务逻辑 - switch callback.Action { - case "AUTH_PASS": - // 只处理企业认证通过 - if callback.AuthType == "ORG" { - return s.handleEnterpriseAuthPass(ctx, &callback) - } - s.logger.Info("忽略非企业认证通过回调", zap.String("auth_type", callback.AuthType)) - return nil - case "AUTH_FAIL": - // 只处理企业认证失败 - if callback.AuthType == "ORG" { - return s.handleEnterpriseAuthFail(ctx, &callback) - } - s.logger.Info("忽略非企业认证失败回调", zap.String("auth_type", callback.AuthType)) - return nil - case "SIGN_FLOW_COMPLETE": - // 合同签署流程完成 - return s.handleContractSignFlowComplete(ctx, &callback) - default: - s.logger.Info("忽略未知的回调动作", zap.String("action", callback.Action)) - return nil - } -} - -// verifySignature 验证e签宝回调签名 -func (s *EsignCallbackApplicationServiceImpl) verifySignature(callbackData map[string]interface{}, headers map[string]string, queryParams map[string]string) error { - // 1. 获取签名相关参数 - signature, ok := headers["X-Tsign-Open-Signature"] - if !ok { - return fmt.Errorf("缺少签名头: X-Tsign-Open-Signature") - } - - timestamp, ok := headers["X-Tsign-Open-Timestamp"] - if !ok { - return fmt.Errorf("缺少时间戳头: X-Tsign-Open-Timestamp") - } - - // 2. 构建查询参数字符串 - var queryKeys []string - for key := range queryParams { - queryKeys = append(queryKeys, key) - } - sort.Strings(queryKeys) // 按ASCII码升序排序 - - var queryValues []string - for _, key := range queryKeys { - queryValues = append(queryValues, queryParams[key]) - } - queryString := strings.Join(queryValues, "") - - // 3. 获取请求体数据 - bodyData, err := s.getRequestBodyString(callbackData) - if err != nil { - return fmt.Errorf("获取请求体数据失败: %w", err) - } - - // 4. 构建验签数据 - data := timestamp + queryString + bodyData - - // 5. 计算签名 - expectedSignature := s.calculateSignature(data, s.esignService.GetConfig().AppSecret) - - // 6. 比较签名 - if strings.ToLower(expectedSignature) != strings.ToLower(signature) { - s.logger.Error("签名验证失败", - zap.String("expected", strings.ToLower(expectedSignature)), - zap.String("received", strings.ToLower(signature)), - zap.String("data", data), - ) - return fmt.Errorf("签名验证失败") - } - - s.logger.Info("e签宝回调验签成功") - return nil -} - -// calculateSignature 计算HMAC-SHA256签名 -func (s *EsignCallbackApplicationServiceImpl) calculateSignature(data, secret string) string { - h := hmac.New(sha256.New, []byte(secret)) - h.Write([]byte(data)) - return strings.ToUpper(hex.EncodeToString(h.Sum(nil))) -} - -// getRequestBodyString 获取请求体字符串 -func (s *EsignCallbackApplicationServiceImpl) getRequestBodyString(callbackData map[string]interface{}) (string, error) { - // 将map转换为JSON字符串 - jsonBytes, err := json.Marshal(callbackData) - if err != nil { - return "", fmt.Errorf("JSON序列化失败: %w", err) - } - return string(jsonBytes), nil -} - -// handleEnterpriseAuthPass 处理企业认证通过回调 -func (s *EsignCallbackApplicationServiceImpl) handleEnterpriseAuthPass(ctx context.Context, callback *EsignCallbackData) error { - s.logger.Info("处理企业认证通过回调") - - if callback.Organization == nil { - return fmt.Errorf("回调数据中缺少organization字段") - } - - if callback.AuthFlowId == "" { - return fmt.Errorf("回调数据中缺少authFlowId字段") - } - - // 查找对应的认证申请 - certification, err := s.certManagementService.GetCertificationByAuthFlowID(ctx, callback.AuthFlowId) - if err != nil { - return fmt.Errorf("查找认证申请失败: %w", err) - } - if certification.Status != enums.StatusInfoSubmitted { - s.logger.Warn("当前状态不允许完成企业认证", zap.String("status", string(certification.Status))) - return nil - } - if err := s.completeEnterpriseAuth(ctx, certification); err != nil { - return fmt.Errorf("完成企业认证失败: %w", err) - } - - s.logger.Info("企业认证通过处理完成", - zap.String("user_id", certification.UserID), - zap.String("certification_id", certification.ID), - zap.String("org_name", callback.Organization.OrgName), - ) - - return nil -} - -// handleEnterpriseAuthFail 处理企业认证失败回调 -func (s *EsignCallbackApplicationServiceImpl) handleEnterpriseAuthFail(ctx context.Context, callback *EsignCallbackData) error { - s.logger.Info("处理企业认证失败回调") - - if callback.Organization == nil { - return fmt.Errorf("回调数据中缺少organization字段") - } - - // 暂时忽略 - - return nil -} - -// handleContractSignFlowComplete 处理合同签署流程完成回调 -func (s *EsignCallbackApplicationServiceImpl) handleContractSignFlowComplete(ctx context.Context, callback *EsignCallbackData) error { - s.logger.Info("处理合同签署流程完成回调") - - if callback.SignFlowId == "" { - return fmt.Errorf("回调数据中缺少signFlowId字段") - } - - if callback.SignFlowStatus == "" { - return fmt.Errorf("回调数据中缺少signFlowStatus字段") - } - - // 查找对应的认证申请 - certification, err := s.certManagementService.GetCertificationByEsignFlowID(ctx, callback.SignFlowId) - if err != nil { - return fmt.Errorf("查找认证申请失败: %w", err) - } - - // 根据签署流程状态处理 - switch callback.SignFlowStatus { - case "2": // 已完成(所有签署方完成签署) - s.logger.Info("合同签署流程已完成,所有签署方完成签署") - - // 完成合同签署 - if err := s.certWorkflowService.CompleteContractSign(ctx, certification.ID, "所有签署方完成签署"); err != nil { - return fmt.Errorf("完成合同签署失败: %w", err) - } - - // 自动完成认证 - if err := s.certWorkflowService.CompleteCertification(ctx, certification.ID); err != nil { - return fmt.Errorf("完成认证失败: %w", err) - } - - s.logger.Info("合同签署流程完成处理成功", - zap.String("user_id", certification.UserID), - zap.String("certification_id", certification.ID), - zap.String("sign_flow_id", callback.SignFlowId), - zap.String("sign_flow_status", callback.SignFlowStatus), - ) - - case "3": // 已撤销(发起方撤销签署任务) - s.logger.Info("合同签署流程已撤销") - - // 可以在这里添加撤销处理逻辑 - s.logger.Info("合同签署流程撤销处理完成", - zap.String("user_id", certification.UserID), - zap.String("certification_id", certification.ID), - zap.String("sign_flow_id", callback.SignFlowId), - zap.String("sign_flow_status", callback.SignFlowStatus), - ) - - // 暂无撤销业务逻辑 - - case "5": // 已过期(签署截止日到期后触发) - s.logger.Info("合同签署流程已过期") - - // 可以在这里添加过期处理逻辑 - s.logger.Info("合同签署流程过期处理完成", - zap.String("user_id", certification.UserID), - zap.String("certification_id", certification.ID), - zap.String("sign_flow_id", callback.SignFlowId), - zap.String("sign_flow_status", callback.SignFlowStatus), - ) - - // 暂无过期业务逻辑 - - case "7": // 已拒签(签署方拒绝签署) - s.logger.Info("合同签署流程已拒签") - - // 可以在这里添加拒签处理逻辑 - s.logger.Info("合同签署流程拒签处理完成", - zap.String("user_id", certification.UserID), - zap.String("certification_id", certification.ID), - zap.String("sign_flow_id", callback.SignFlowId), - zap.String("sign_flow_status", callback.SignFlowStatus), - ) - - default: - s.logger.Warn("未知的签署流程状态", - zap.String("sign_flow_status", callback.SignFlowStatus), - zap.String("sign_flow_id", callback.SignFlowId), - ) - - // 暂无拒签业务逻辑 - } - - return nil -} - -// 企业认证成功后操作 -func (s *EsignCallbackApplicationServiceImpl) completeEnterpriseAuth(ctx context.Context, certification *entities.Certification) error { - err := s.txManager.ExecuteInTx(ctx, func(txCtx context.Context) error { - // 1. 获取企业信息提交记录 - enterpriseRecord, err := s.enterpriseRecordService.GetLatestByUserID(txCtx, certification.UserID) - if err != nil { - return fmt.Errorf("获取企业信息失败: %w", err) - } - // 2. 转换状态 - if err := s.certWorkflowService.CompleteEnterpriseVerification(txCtx, certification.ID); err != nil { - return err - } - // 3. 创建企业信息 - _, err = s.enterpriseService.CreateEnterpriseInfo(txCtx, certification.UserID, enterpriseRecord.CompanyName, enterpriseRecord.UnifiedSocialCode, enterpriseRecord.LegalPersonName, enterpriseRecord.LegalPersonID) - if err != nil { - s.logger.Warn("创建用户企业信息失败", zap.Error(err)) - return err - } - return nil - }) - if err != nil { - return fmt.Errorf("完成企业认证失败: %w", err) - } - return nil -} diff --git a/internal/container/container.go b/internal/container/container.go index 9ce1463..7cd9c68 100644 --- a/internal/container/container.go +++ b/internal/container/container.go @@ -15,7 +15,6 @@ import ( "tyapi-server/internal/application/user" "tyapi-server/internal/config" domain_certification_repo "tyapi-server/internal/domains/certification/repositories" - certification_service "tyapi-server/internal/domains/certification/services" domain_finance_repo "tyapi-server/internal/domains/finance/repositories" finance_service "tyapi-server/internal/domains/finance/services" domain_product_repo "tyapi-server/internal/domains/product/repositories" @@ -251,25 +250,15 @@ func NewContainer() *Container { // 仓储层 - 认证域 fx.Provide( - // 认证申请仓储 + // 认证命令仓储 fx.Annotate( - certification_repo.NewGormCertificationRepository, - fx.As(new(domain_certification_repo.CertificationRepository)), + certification_repo.NewGormCertificationCommandRepository, + fx.As(new(domain_certification_repo.CertificationCommandRepository)), ), - // 企业信息提交记录仓储 + // 认证查询仓储 fx.Annotate( - certification_repo.NewGormEnterpriseInfoSubmitRecordRepository, - fx.As(new(domain_certification_repo.EnterpriseInfoSubmitRecordRepository)), - ), - // e签宝生成合同记录仓储 - fx.Annotate( - certification_repo.NewGormEsignContractGenerateRecordRepository, - fx.As(new(domain_certification_repo.EsignContractGenerateRecordRepository)), - ), - // e签宝签署合同记录仓储 - fx.Annotate( - certification_repo.NewGormEsignContractSignRecordRepository, - fx.As(new(domain_certification_repo.EsignContractSignRecordRepository)), + certification_repo.NewGormCertificationQueryRepository, + fx.As(new(domain_certification_repo.CertificationQueryRepository)), ), ), @@ -314,11 +303,7 @@ func NewContainer() *Container { user_service.NewEnterpriseService, product_service.NewProductManagementService, product_service.NewProductSubscriptionService, - certification_service.NewCertificationManagementService, - certification_service.NewCertificationWorkflowService, - certification_service.NewCertificationStateMachine, - certification_service.NewEnterpriseInfoSubmitRecordService, - certification_service.NewCertificationEsignService, + // 认证域的领域服务已经整合到应用服务中 finance_service.NewFinanceService, ), @@ -334,11 +319,6 @@ func NewContainer() *Container { certification.NewCertificationApplicationService, fx.As(new(certification.CertificationApplicationService)), ), - // e签宝回调应用服务 - 绑定到接口 - fx.Annotate( - certification.NewEsignCallbackApplicationService, - fx.As(new(certification.EsignCallbackApplicationService)), - ), // 财务应用服务 - 绑定到接口 fx.Annotate( finance.NewFinanceApplicationService, diff --git a/internal/domains/certification/entities/certification.go b/internal/domains/certification/entities/certification.go index be56281..5e325bc 100644 --- a/internal/domains/certification/entities/certification.go +++ b/internal/domains/certification/entities/certification.go @@ -1,43 +1,55 @@ package entities import ( + "errors" + "fmt" "time" + "tyapi-server/internal/domains/certification/entities/value_objects" "tyapi-server/internal/domains/certification/enums" "github.com/google/uuid" - "gorm.io/gorm" ) -// Certification 认证申请实体 -// 这是企业认证流程的核心实体,负责管理整个认证申请的生命周期 +// Certification 认证聚合根 +// 这是企业认证流程的核心聚合根,封装了完整的认证业务逻辑和状态管理 type Certification struct { - // 基础信息 + // === 基础信息 === ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"认证申请唯一标识"` UserID string `gorm:"type:varchar(36);not null;index" json:"user_id" comment:"申请用户ID"` Status enums.CertificationStatus `gorm:"type:varchar(50);not null;index" json:"status" comment:"当前认证状态"` - // 流程节点时间戳 - 记录每个关键步骤的完成时间 + // === 流程时间戳 - 记录每个关键步骤的完成时间 === InfoSubmittedAt *time.Time `json:"info_submitted_at,omitempty" comment:"企业信息提交时间"` EnterpriseVerifiedAt *time.Time `json:"enterprise_verified_at,omitempty" comment:"企业认证完成时间"` ContractAppliedAt *time.Time `json:"contract_applied_at,omitempty" comment:"合同申请时间"` ContractSignedAt *time.Time `json:"contract_signed_at,omitempty" comment:"合同签署完成时间"` - CompletedAt *time.Time `json:"completed_at,omitempty" comment:"认证完成时间"` - // 合同信息 - 电子合同相关链接 + // === e签宝相关信息 === + AuthFlowID string `gorm:"type:varchar(500)" json:"auth_flow_id,omitempty" comment:"企业认证流程ID"` ContractFileID string `gorm:"type:varchar(500)" json:"contract_file_id,omitempty" comment:"合同文件ID"` EsignFlowID string `gorm:"type:varchar(500)" json:"esign_flow_id,omitempty" comment:"签署流程ID"` ContractURL string `gorm:"type:varchar(500)" json:"contract_url,omitempty" comment:"合同文件访问链接"` ContractSignURL string `gorm:"type:varchar(500)" json:"contract_sign_url,omitempty" comment:"合同签署链接"` - // 认证信息 - AuthFlowID string `gorm:"type:varchar(500)" json:"auth_flow_id,omitempty" comment:"认证流程ID"` + // === 失败信息 === + FailureReason enums.FailureReason `gorm:"type:varchar(100)" json:"failure_reason,omitempty" comment:"失败原因"` + FailureMessage string `gorm:"type:text" json:"failure_message,omitempty" comment:"失败详细信息"` + RetryCount int `gorm:"default:0" json:"retry_count" comment:"重试次数"` - // 时间戳字段 + // === 审计信息 === + LastTransitionAt *time.Time `json:"last_transition_at,omitempty" comment:"最后状态转换时间"` + LastTransitionBy enums.ActorType `gorm:"type:varchar(20)" json:"last_transition_by,omitempty" comment:"最后操作者类型"` + LastTransitionActor string `gorm:"type:varchar(100)" json:"last_transition_actor,omitempty" comment:"最后操作者ID"` + + // === 系统字段 === CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at" comment:"创建时间"` UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at" comment:"更新时间"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-" comment:"软删除时间"` + + // === 领域事件 (不持久化) === + domainEvents []interface{} `gorm:"-" json:"-"` } // TableName 指定数据库表名 @@ -50,98 +62,622 @@ func (c *Certification) BeforeCreate(tx *gorm.DB) error { if c.ID == "" { c.ID = uuid.New().String() } + + // 设置初始状态 + if c.Status == "" { + c.Status = enums.StatusPending + } + return nil } -// IsStatusChangeable 检查状态是否可以变更 -func (c *Certification) IsStatusChangeable() bool { - return !enums.IsFinalStatus(c.Status) +// ================ 工厂方法 ================ + +// NewCertification 创建新的认证申请 +func NewCertification(userID string) (*Certification, error) { + if userID == "" { + return nil, errors.New("用户ID不能为空") + } + + certification := &Certification{ + ID: uuid.New().String(), + UserID: userID, + Status: enums.StatusPending, + RetryCount: 0, + domainEvents: make([]interface{}, 0), + } + + // 添加领域事件 + certification.addDomainEvent(&CertificationCreatedEvent{ + CertificationID: certification.ID, + UserID: userID, + CreatedAt: time.Now(), + }) + + return certification, nil } -// GetStatusName 获取状态名称 -func (c *Certification) GetStatusName() string { +// ================ 状态转换方法 ================ + +// CanTransitionTo 检查是否可以转换到目标状态 +func (c *Certification) CanTransitionTo(targetStatus enums.CertificationStatus, actor enums.ActorType) (bool, string) { + // 检查状态转换规则 + if !enums.CanTransitionTo(c.Status, targetStatus) { + return false, fmt.Sprintf("不允许从 %s 转换到 %s", enums.GetStatusName(c.Status), enums.GetStatusName(targetStatus)) + } + + // 检查操作者权限 + if !c.validateActorPermission(targetStatus, actor) { + return false, fmt.Sprintf("%s 无权执行此状态转换", enums.GetActorTypeName(actor)) + } + + // 检查业务规则 + if err := c.validateBusinessRules(targetStatus, actor); err != nil { + return false, err.Error() + } + + return true, "" +} + +// TransitionTo 执行状态转换 +func (c *Certification) TransitionTo(targetStatus enums.CertificationStatus, actor enums.ActorType, actorID string, reason string) error { + // 验证转换合法性 + canTransition, message := c.CanTransitionTo(targetStatus, actor) + if !canTransition { + return fmt.Errorf("状态转换失败: %s", message) + } + + oldStatus := c.Status + + // 执行状态转换 + c.Status = targetStatus + c.updateTimestampByStatus(targetStatus) + c.updateTransitionAudit(actor, actorID) + + // 清除失败信息(如果转换到成功状态) + if !enums.IsFailureStatus(targetStatus) { + c.clearFailureInfo() + } + + // 添加状态转换事件 + c.addDomainEvent(&CertificationStatusChangedEvent{ + CertificationID: c.ID, + UserID: c.UserID, + FromStatus: oldStatus, + ToStatus: targetStatus, + Actor: actor, + ActorID: actorID, + Reason: reason, + TransitionedAt: time.Now(), + }) + + return nil +} + +// ================ 业务操作方法 ================ + +// SubmitEnterpriseInfo 提交企业信息 +func (c *Certification) SubmitEnterpriseInfo(enterpriseInfo *value_objects.EnterpriseInfo) error { + // 验证当前状态 + if c.Status != enums.StatusPending && c.Status != enums.StatusInfoRejected { + return fmt.Errorf("当前状态 %s 不允许提交企业信息", enums.GetStatusName(c.Status)) + } + + // 验证企业信息 + if err := enterpriseInfo.Validate(); err != nil { + return fmt.Errorf("企业信息验证失败: %w", err) + } + + // 状态转换 + if err := c.TransitionTo(enums.StatusInfoSubmitted, enums.ActorTypeUser, c.UserID, "用户提交企业信息"); err != nil { + return err + } + + // 添加业务事件 + c.addDomainEvent(&EnterpriseInfoSubmittedEvent{ + CertificationID: c.ID, + UserID: c.UserID, + EnterpriseInfo: enterpriseInfo, + SubmittedAt: time.Now(), + }) + + return nil +} + +// HandleEnterpriseVerificationCallback 处理企业认证回调 +func (c *Certification) HandleEnterpriseVerificationCallback(success bool, authFlowID string, failureReason enums.FailureReason, message string) error { + // 验证当前状态 + if c.Status != enums.StatusInfoSubmitted { + return fmt.Errorf("当前状态 %s 不允许处理企业认证回调", enums.GetStatusName(c.Status)) + } + + c.AuthFlowID = authFlowID + + if success { + // 认证成功 + if err := c.TransitionTo(enums.StatusEnterpriseVerified, enums.ActorTypeEsign, "esign_system", "企业认证成功"); err != nil { + return err + } + + c.addDomainEvent(&EnterpriseVerificationSuccessEvent{ + CertificationID: c.ID, + UserID: c.UserID, + AuthFlowID: authFlowID, + VerifiedAt: time.Now(), + }) + } else { + // 认证失败 + c.setFailureInfo(failureReason, message) + + if err := c.TransitionTo(enums.StatusInfoRejected, enums.ActorTypeEsign, "esign_system", "企业认证失败"); err != nil { + return err + } + + c.addDomainEvent(&EnterpriseVerificationFailedEvent{ + CertificationID: c.ID, + UserID: c.UserID, + AuthFlowID: authFlowID, + FailureReason: failureReason, + FailureMessage: message, + FailedAt: time.Now(), + }) + } + + return nil +} + +// ApplyContract 申请合同签署 +func (c *Certification) ApplyContract() error { + // 验证当前状态 + if c.Status != enums.StatusEnterpriseVerified { + return fmt.Errorf("当前状态 %s 不允许申请合同", enums.GetStatusName(c.Status)) + } + + // 状态转换 + if err := c.TransitionTo(enums.StatusContractApplied, enums.ActorTypeUser, c.UserID, "用户申请合同签署"); err != nil { + return err + } + + // 添加业务事件 + c.addDomainEvent(&ContractAppliedEvent{ + CertificationID: c.ID, + UserID: c.UserID, + AppliedAt: time.Now(), + }) + + return nil +} + +// UpdateContractInfo 更新合同信息 +func (c *Certification) UpdateContractInfo(contractInfo *value_objects.ContractInfo) error { + // 验证合同信息 + if err := contractInfo.Validate(); err != nil { + return fmt.Errorf("合同信息验证失败: %w", err) + } + + // 更新合同相关字段 + c.ContractFileID = contractInfo.ContractFileID + c.EsignFlowID = contractInfo.EsignFlowID + c.ContractURL = contractInfo.ContractURL + c.ContractSignURL = contractInfo.ContractSignURL + + return nil +} + +// HandleContractSignCallback 处理合同签署回调 +func (c *Certification) HandleContractSignCallback(success bool, contractURL string, failureReason enums.FailureReason, message string) error { + // 验证当前状态 + if c.Status != enums.StatusContractApplied { + return fmt.Errorf("当前状态 %s 不允许处理合同签署回调", enums.GetStatusName(c.Status)) + } + + if success { + // 签署成功 - 认证完成 + c.ContractURL = contractURL + + if err := c.TransitionTo(enums.StatusContractSigned, enums.ActorTypeEsign, "esign_system", "合同签署成功,认证完成"); err != nil { + return err + } + + c.addDomainEvent(&ContractSignedEvent{ + CertificationID: c.ID, + UserID: c.UserID, + ContractURL: contractURL, + SignedAt: time.Now(), + }) + + c.addDomainEvent(&CertificationCompletedEvent{ + CertificationID: c.ID, + UserID: c.UserID, + CompletedAt: time.Now(), + }) + } else { + // 签署失败 + c.setFailureInfo(failureReason, message) + + var targetStatus enums.CertificationStatus + if failureReason == enums.FailureReasonContractExpired { + targetStatus = enums.StatusContractExpired + } else { + targetStatus = enums.StatusContractRejected + } + + if err := c.TransitionTo(targetStatus, enums.ActorTypeEsign, "esign_system", "合同签署失败"); err != nil { + return err + } + + c.addDomainEvent(&ContractSignFailedEvent{ + CertificationID: c.ID, + UserID: c.UserID, + FailureReason: failureReason, + FailureMessage: message, + FailedAt: time.Now(), + }) + } + + return nil +} + +// RetryFromFailure 从失败状态重试 +func (c *Certification) RetryFromFailure(actor enums.ActorType, actorID string) error { + if !enums.IsFailureStatus(c.Status) { + return errors.New("当前状态不是失败状态,无需重试") + } + + // 检查重试次数限制 + if c.RetryCount >= 3 { + return errors.New("已达到最大重试次数限制") + } + + // 检查失败原因是否可重试 + if !enums.IsRetryable(c.FailureReason) { + return fmt.Errorf("失败原因 %s 不支持重试", enums.GetFailureReasonName(c.FailureReason)) + } + + var targetStatus enums.CertificationStatus + var reason string + + switch c.Status { + case enums.StatusInfoRejected: + targetStatus = enums.StatusInfoSubmitted + reason = "重新提交企业信息" + case enums.StatusContractRejected, enums.StatusContractExpired: + targetStatus = enums.StatusEnterpriseVerified + reason = "重置状态,准备重新申请合同" + default: + return fmt.Errorf("不支持从状态 %s 重试", enums.GetStatusName(c.Status)) + } + + // 增加重试次数 + c.RetryCount++ + + // 状态转换 + if err := c.TransitionTo(targetStatus, actor, actorID, reason); err != nil { + return err + } + + // 添加重试事件 + c.addDomainEvent(&CertificationRetryEvent{ + CertificationID: c.ID, + UserID: c.UserID, + FromStatus: c.Status, + ToStatus: targetStatus, + RetryCount: c.RetryCount, + RetriedAt: time.Now(), + }) + + return nil +} + +// ================ 查询方法 ================ + +// GetProgress 获取认证进度百分比 +func (c *Certification) GetProgress() int { + return enums.GetProgressPercentage(c.Status) +} + +// IsUserActionRequired 是否需要用户操作 +func (c *Certification) IsUserActionRequired() bool { + return enums.IsUserActionRequired(c.Status) +} + +// GetCurrentStatusName 获取当前状态名称 +func (c *Certification) GetCurrentStatusName() string { return enums.GetStatusName(c.Status) } -// IsFinalStatus 判断是否为最终状态 +// GetUserActionHint 获取用户操作提示 +func (c *Certification) GetUserActionHint() string { + return enums.GetUserActionHint(c.Status) +} + +// GetAvailableActions 获取当前可执行的操作 +func (c *Certification) GetAvailableActions() []string { + actions := make([]string, 0) + + switch c.Status { + case enums.StatusPending: + actions = append(actions, "submit_enterprise_info") + case enums.StatusEnterpriseVerified: + actions = append(actions, "apply_contract") + case enums.StatusInfoRejected, enums.StatusContractRejected, enums.StatusContractExpired: + if enums.IsRetryable(c.FailureReason) && c.RetryCount < 3 { + actions = append(actions, "retry") + } + } + + return actions +} + +// IsFinalStatus 是否为最终状态 func (c *Certification) IsFinalStatus() bool { return enums.IsFinalStatus(c.Status) } -// GetStatusCategory 获取状态分类 -func (c *Certification) GetStatusCategory() string { - return enums.GetStatusCategory(c.Status) -} - -// GetStatusPriority 获取状态优先级 -func (c *Certification) GetStatusPriority() int { - return enums.GetStatusPriority(c.Status) -} - -// GetProgressPercentage 获取进度百分比 -func (c *Certification) GetProgressPercentage() int { - progressMap := map[enums.CertificationStatus]int{ - enums.StatusPending: 0, - enums.StatusInfoSubmitted: 20, - enums.StatusEnterpriseVerified: 40, - enums.StatusContractApplied: 60, - enums.StatusContractSigned: 80, - enums.StatusCompleted: 100, - } - - if progress, exists := progressMap[c.Status]; exists { - return progress - } - return 0 -} - -// IsUserActionRequired 检查是否需要用户操作 -func (c *Certification) IsUserActionRequired() bool { - userActionRequired := map[enums.CertificationStatus]bool{ - enums.StatusPending: true, - enums.StatusInfoSubmitted: true, - enums.StatusEnterpriseVerified: true, - enums.StatusContractApplied: true, - enums.StatusContractSigned: false, - enums.StatusCompleted: false, - } - - if required, exists := userActionRequired[c.Status]; exists { - return required - } - return false +// IsCompleted 是否已完成认证 +func (c *Certification) IsCompleted() bool { + return c.Status == enums.StatusContractSigned } // GetNextValidStatuses 获取下一个有效状态 func (c *Certification) GetNextValidStatuses() []enums.CertificationStatus { - nextStatusMap := map[enums.CertificationStatus][]enums.CertificationStatus{ - enums.StatusPending: {enums.StatusInfoSubmitted}, - enums.StatusInfoSubmitted: {enums.StatusEnterpriseVerified, enums.StatusInfoSubmitted}, // 可以重新提交 - enums.StatusEnterpriseVerified: {enums.StatusContractApplied}, - enums.StatusContractApplied: {enums.StatusContractSigned}, - enums.StatusContractSigned: {enums.StatusCompleted}, - enums.StatusCompleted: {}, - } - - if nextStatuses, exists := nextStatusMap[c.Status]; exists { - return nextStatuses - } - return []enums.CertificationStatus{} + return enums.GetNextValidStatuses(c.Status) } -// CanTransitionTo 检查是否可以转换到指定状态 -func (c *Certification) CanTransitionTo(targetStatus enums.CertificationStatus, isUser bool) (bool, string) { - nextStatuses := c.GetNextValidStatuses() +// GetFailureInfo 获取失败信息 +func (c *Certification) GetFailureInfo() (enums.FailureReason, string) { + return c.FailureReason, c.FailureMessage +} - for _, nextStatus := range nextStatuses { - if nextStatus == targetStatus { - // 检查权限 - if isUser && !c.IsUserActionRequired() { - return false, "当前状态不需要用户操作" - } - return true, "" +// ================ 业务规则验证 ================ + +// ValidateBusinessRules 验证业务规则 +func (c *Certification) ValidateBusinessRules() error { + // 基础验证 + if c.UserID == "" { + return errors.New("用户ID不能为空") + } + + if !enums.IsValidStatus(c.Status) { + return fmt.Errorf("无效的认证状态: %s", c.Status) + } + + // 状态相关验证 + switch c.Status { + case enums.StatusEnterpriseVerified: + if c.AuthFlowID == "" { + return errors.New("企业认证状态下必须有认证流程ID") + } + case enums.StatusContractApplied: + if c.AuthFlowID == "" { + return errors.New("合同申请状态下必须有企业认证流程ID") + } + case enums.StatusContractSigned: + if c.ContractFileID == "" || c.EsignFlowID == "" { + return errors.New("合同签署状态下必须有完整的合同信息") } } - return false, "不支持的状态转换" + // 失败状态验证 + if enums.IsFailureStatus(c.Status) { + if c.FailureReason == "" { + return errors.New("失败状态下必须有失败原因") + } + if !enums.IsValidFailureReason(c.FailureReason) { + return fmt.Errorf("无效的失败原因: %s", c.FailureReason) + } + } + + return nil +} + +// validateActorPermission 验证操作者权限 +func (c *Certification) validateActorPermission(targetStatus enums.CertificationStatus, actor enums.ActorType) bool { + // 定义状态转换的权限规则 + permissions := map[enums.CertificationStatus][]enums.ActorType{ + enums.StatusInfoSubmitted: {enums.ActorTypeUser}, + enums.StatusEnterpriseVerified: {enums.ActorTypeEsign, enums.ActorTypeSystem, enums.ActorTypeAdmin}, + enums.StatusInfoRejected: {enums.ActorTypeEsign, enums.ActorTypeSystem, enums.ActorTypeAdmin}, + enums.StatusContractApplied: {enums.ActorTypeUser}, + enums.StatusContractSigned: {enums.ActorTypeEsign, enums.ActorTypeSystem, enums.ActorTypeAdmin}, + enums.StatusContractRejected: {enums.ActorTypeEsign, enums.ActorTypeSystem, enums.ActorTypeAdmin}, + enums.StatusContractExpired: {enums.ActorTypeEsign, enums.ActorTypeSystem, enums.ActorTypeAdmin}, + } + + allowedActors, exists := permissions[targetStatus] + if !exists { + return false + } + + for _, allowedActor := range allowedActors { + if actor == allowedActor { + return true + } + } + + return false +} + +// validateBusinessRules 验证业务规则 +func (c *Certification) validateBusinessRules(targetStatus enums.CertificationStatus, actor enums.ActorType) error { + // 用户操作验证 + if actor == enums.ActorTypeUser { + switch targetStatus { + case enums.StatusInfoSubmitted: + // 用户提交企业信息时的验证 + if c.Status != enums.StatusPending && c.Status != enums.StatusInfoRejected { + return fmt.Errorf("当前状态 %s 不允许提交企业信息", enums.GetStatusName(c.Status)) + } + case enums.StatusContractApplied: + // 用户申请合同时的验证 + if c.Status != enums.StatusEnterpriseVerified { + return fmt.Errorf("必须先完成企业认证才能申请合同") + } + if c.AuthFlowID == "" { + return errors.New("缺少企业认证流程ID") + } + } + } + + // e签宝回调验证 + if actor == enums.ActorTypeEsign { + switch targetStatus { + case enums.StatusEnterpriseVerified, enums.StatusInfoRejected: + if c.Status != enums.StatusInfoSubmitted { + return fmt.Errorf("当前状态 %s 不允许处理企业认证回调", enums.GetStatusName(c.Status)) + } + case enums.StatusContractSigned, enums.StatusContractRejected, enums.StatusContractExpired: + if c.Status != enums.StatusContractApplied { + return fmt.Errorf("当前状态 %s 不允许处理合同签署回调", enums.GetStatusName(c.Status)) + } + } + } + + return nil +} + +// ================ 辅助方法 ================ + +// updateTimestampByStatus 根据状态更新对应的时间戳 +func (c *Certification) updateTimestampByStatus(status enums.CertificationStatus) { + now := time.Now() + + switch status { + case enums.StatusInfoSubmitted: + c.InfoSubmittedAt = &now + case enums.StatusEnterpriseVerified: + c.EnterpriseVerifiedAt = &now + case enums.StatusContractApplied: + c.ContractAppliedAt = &now + case enums.StatusContractSigned: + c.ContractSignedAt = &now + } +} + +// updateTransitionAudit 更新状态转换审计信息 +func (c *Certification) updateTransitionAudit(actor enums.ActorType, actorID string) { + now := time.Now() + c.LastTransitionAt = &now + c.LastTransitionBy = actor + c.LastTransitionActor = actorID +} + +// setFailureInfo 设置失败信息 +func (c *Certification) setFailureInfo(reason enums.FailureReason, message string) { + c.FailureReason = reason + c.FailureMessage = message +} + +// clearFailureInfo 清除失败信息 +func (c *Certification) clearFailureInfo() { + c.FailureReason = "" + c.FailureMessage = "" +} + +// ================ 领域事件管理 ================ + +// addDomainEvent 添加领域事件 +func (c *Certification) addDomainEvent(event interface{}) { + if c.domainEvents == nil { + c.domainEvents = make([]interface{}, 0) + } + c.domainEvents = append(c.domainEvents, event) +} + +// GetDomainEvents 获取领域事件 +func (c *Certification) GetDomainEvents() []interface{} { + return c.domainEvents +} + +// ClearDomainEvents 清除领域事件 +func (c *Certification) ClearDomainEvents() { + c.domainEvents = make([]interface{}, 0) +} + +// ================ 领域事件定义 ================ + +// CertificationCreatedEvent 认证创建事件 +type CertificationCreatedEvent struct { + CertificationID string `json:"certification_id"` + UserID string `json:"user_id"` + CreatedAt time.Time `json:"created_at"` +} + +// CertificationStatusChangedEvent 认证状态变更事件 +type CertificationStatusChangedEvent struct { + CertificationID string `json:"certification_id"` + UserID string `json:"user_id"` + FromStatus enums.CertificationStatus `json:"from_status"` + ToStatus enums.CertificationStatus `json:"to_status"` + Actor enums.ActorType `json:"actor"` + ActorID string `json:"actor_id"` + Reason string `json:"reason"` + TransitionedAt time.Time `json:"transitioned_at"` +} + +// EnterpriseInfoSubmittedEvent 企业信息提交事件 +type EnterpriseInfoSubmittedEvent struct { + CertificationID string `json:"certification_id"` + UserID string `json:"user_id"` + EnterpriseInfo *value_objects.EnterpriseInfo `json:"enterprise_info"` + SubmittedAt time.Time `json:"submitted_at"` +} + +// EnterpriseVerificationSuccessEvent 企业认证成功事件 +type EnterpriseVerificationSuccessEvent struct { + CertificationID string `json:"certification_id"` + UserID string `json:"user_id"` + AuthFlowID string `json:"auth_flow_id"` + VerifiedAt time.Time `json:"verified_at"` +} + +// EnterpriseVerificationFailedEvent 企业认证失败事件 +type EnterpriseVerificationFailedEvent struct { + CertificationID string `json:"certification_id"` + UserID string `json:"user_id"` + AuthFlowID string `json:"auth_flow_id"` + FailureReason enums.FailureReason `json:"failure_reason"` + FailureMessage string `json:"failure_message"` + FailedAt time.Time `json:"failed_at"` +} + +// ContractAppliedEvent 合同申请事件 +type ContractAppliedEvent struct { + CertificationID string `json:"certification_id"` + UserID string `json:"user_id"` + AppliedAt time.Time `json:"applied_at"` +} + +// ContractSignedEvent 合同签署成功事件 +type ContractSignedEvent struct { + CertificationID string `json:"certification_id"` + UserID string `json:"user_id"` + ContractURL string `json:"contract_url"` + SignedAt time.Time `json:"signed_at"` +} + +// ContractSignFailedEvent 合同签署失败事件 +type ContractSignFailedEvent struct { + CertificationID string `json:"certification_id"` + UserID string `json:"user_id"` + FailureReason enums.FailureReason `json:"failure_reason"` + FailureMessage string `json:"failure_message"` + FailedAt time.Time `json:"failed_at"` +} + +// CertificationCompletedEvent 认证完成事件 +type CertificationCompletedEvent struct { + CertificationID string `json:"certification_id"` + UserID string `json:"user_id"` + CompletedAt time.Time `json:"completed_at"` +} + +// CertificationRetryEvent 认证重试事件 +type CertificationRetryEvent struct { + CertificationID string `json:"certification_id"` + UserID string `json:"user_id"` + FromStatus enums.CertificationStatus `json:"from_status"` + ToStatus enums.CertificationStatus `json:"to_status"` + RetryCount int `json:"retry_count"` + RetriedAt time.Time `json:"retried_at"` } diff --git a/internal/domains/certification/entities/enterprise_info_submit_record.go b/internal/domains/certification/entities/enterprise_info_submit_record.go index 3afa13a..4b96e53 100644 --- a/internal/domains/certification/entities/enterprise_info_submit_record.go +++ b/internal/domains/certification/entities/enterprise_info_submit_record.go @@ -9,9 +9,9 @@ import ( // EnterpriseInfoSubmitRecord 企业信息提交记录 type EnterpriseInfoSubmitRecord struct { - ID string `json:"id" gorm:"primaryKey;type:varchar(36)"` - UserID string `json:"user_id" gorm:"type:varchar(36);not null;index"` - + ID string `json:"id" gorm:"primaryKey;type:varchar(36)"` + UserID string `json:"user_id" gorm:"type:varchar(36);not null;index"` + // 企业信息 CompanyName string `json:"company_name" gorm:"type:varchar(200);not null"` UnifiedSocialCode string `json:"unified_social_code" gorm:"type:varchar(50);not null;index"` @@ -19,12 +19,12 @@ type EnterpriseInfoSubmitRecord struct { LegalPersonID string `json:"legal_person_id" gorm:"type:varchar(50);not null"` LegalPersonPhone string `json:"legal_person_phone" gorm:"type:varchar(50);not null"` // 提交状态 - Status string `json:"status" gorm:"type:varchar(20);not null;default:'submitted'"` // submitted, verified, failed - SubmitAt time.Time `json:"submit_at" gorm:"not null"` - VerifiedAt *time.Time `json:"verified_at"` - FailedAt *time.Time `json:"failed_at"` - FailureReason string `json:"failure_reason" gorm:"type:text"` - + Status string `json:"status" gorm:"type:varchar(20);not null;default:'submitted'"` // submitted, verified, failed + SubmitAt time.Time `json:"submit_at" gorm:"not null"` + VerifiedAt *time.Time `json:"verified_at"` + FailedAt *time.Time `json:"failed_at"` + FailureReason string `json:"failure_reason" gorm:"type:text"` + // 系统字段 CreatedAt time.Time `json:"created_at" gorm:"not null"` UpdatedAt time.Time `json:"updated_at" gorm:"not null"` @@ -80,4 +80,4 @@ func (r *EnterpriseInfoSubmitRecord) IsVerified() bool { // IsFailed 检查是否验证失败 func (r *EnterpriseInfoSubmitRecord) IsFailed() bool { return r.Status == "failed" -} \ No newline at end of file +} diff --git a/internal/domains/certification/entities/value_objects/contract_info.go b/internal/domains/certification/entities/value_objects/contract_info.go new file mode 100644 index 0000000..41fea3b --- /dev/null +++ b/internal/domains/certification/entities/value_objects/contract_info.go @@ -0,0 +1,516 @@ +package value_objects + +import ( + "errors" + "fmt" + "regexp" + "strings" + "time" +) + +// ContractInfo 合同信息值对象 +// 封装电子合同相关的核心信息,包含合同状态和签署流程管理 +type ContractInfo struct { + // 合同基本信息 + ContractFileID string `json:"contract_file_id"` // 合同文件ID + EsignFlowID string `json:"esign_flow_id"` // e签宝签署流程ID + ContractURL string `json:"contract_url"` // 合同文件访问链接 + ContractSignURL string `json:"contract_sign_url"` // 合同签署链接 + + // 合同元数据 + ContractTitle string `json:"contract_title"` // 合同标题 + ContractVersion string `json:"contract_version"` // 合同版本 + TemplateID string `json:"template_id"` // 模板ID + + // 签署相关信息 + SignerAccount string `json:"signer_account"` // 签署人账号 + SignerName string `json:"signer_name"` // 签署人姓名 + TransactorPhone string `json:"transactor_phone"` // 经办人手机号 + TransactorName string `json:"transactor_name"` // 经办人姓名 + TransactorIDCardNum string `json:"transactor_id_card_num"` // 经办人身份证号 + + // 时间信息 + GeneratedAt *time.Time `json:"generated_at,omitempty"` // 合同生成时间 + SignFlowCreatedAt *time.Time `json:"sign_flow_created_at,omitempty"` // 签署流程创建时间 + SignedAt *time.Time `json:"signed_at,omitempty"` // 签署完成时间 + ExpiresAt *time.Time `json:"expires_at,omitempty"` // 签署链接过期时间 + + // 状态信息 + Status string `json:"status"` // 合同状态 + SignProgress int `json:"sign_progress"` // 签署进度 + + // 附加信息 + Metadata map[string]interface{} `json:"metadata,omitempty"` // 元数据 +} + +// ContractStatus 合同状态常量 +const ( + ContractStatusDraft = "draft" // 草稿 + ContractStatusGenerated = "generated" // 已生成 + ContractStatusSigning = "signing" // 签署中 + ContractStatusSigned = "signed" // 已签署 + ContractStatusExpired = "expired" // 已过期 + ContractStatusRejected = "rejected" // 被拒绝 + ContractStatusCancelled = "cancelled" // 已取消 +) + +// NewContractInfo 创建合同信息值对象 +func NewContractInfo(contractFileID, esignFlowID, contractURL, contractSignURL string) (*ContractInfo, error) { + info := &ContractInfo{ + ContractFileID: strings.TrimSpace(contractFileID), + EsignFlowID: strings.TrimSpace(esignFlowID), + ContractURL: strings.TrimSpace(contractURL), + ContractSignURL: strings.TrimSpace(contractSignURL), + Status: ContractStatusGenerated, + SignProgress: 0, + Metadata: make(map[string]interface{}), + } + + if err := info.Validate(); err != nil { + return nil, fmt.Errorf("合同信息验证失败: %w", err) + } + + return info, nil +} + +// Validate 验证合同信息的完整性和格式 +func (c *ContractInfo) Validate() error { + if err := c.validateContractFileID(); err != nil { + return err + } + + if err := c.validateEsignFlowID(); err != nil { + return err + } + + if err := c.validateContractURL(); err != nil { + return err + } + + if err := c.validateContractSignURL(); err != nil { + return err + } + + if err := c.validateSignerInfo(); err != nil { + return err + } + + if err := c.validateStatus(); err != nil { + return err + } + + return nil +} + +// validateContractFileID 验证合同文件ID +func (c *ContractInfo) validateContractFileID() error { + if c.ContractFileID == "" { + return errors.New("合同文件ID不能为空") + } + + // 简单的格式验证 + if len(c.ContractFileID) < 10 { + return errors.New("合同文件ID格式不正确") + } + + return nil +} + +// validateEsignFlowID 验证e签宝流程ID +func (c *ContractInfo) validateEsignFlowID() error { + if c.EsignFlowID == "" { + return errors.New("e签宝流程ID不能为空") + } + + // 简单的格式验证 + if len(c.EsignFlowID) < 10 { + return errors.New("e签宝流程ID格式不正确") + } + + return nil +} + +// validateContractURL 验证合同访问链接 +func (c *ContractInfo) validateContractURL() error { + if c.ContractURL == "" { + return errors.New("合同访问链接不能为空") + } + + // URL格式验证 + urlPattern := `^https?://.*` + matched, err := regexp.MatchString(urlPattern, c.ContractURL) + if err != nil { + return fmt.Errorf("合同访问链接格式验证错误: %w", err) + } + + if !matched { + return errors.New("合同访问链接格式不正确,必须以http://或https://开头") + } + + return nil +} + +// validateContractSignURL 验证合同签署链接 +func (c *ContractInfo) validateContractSignURL() error { + if c.ContractSignURL == "" { + return errors.New("合同签署链接不能为空") + } + + // URL格式验证 + urlPattern := `^https?://.*` + matched, err := regexp.MatchString(urlPattern, c.ContractSignURL) + if err != nil { + return fmt.Errorf("合同签署链接格式验证错误: %w", err) + } + + if !matched { + return errors.New("合同签署链接格式不正确,必须以http://或https://开头") + } + + return nil +} + +// validateSignerInfo 验证签署人信息 +func (c *ContractInfo) validateSignerInfo() error { + // 如果有签署人信息,进行验证 + if c.SignerAccount != "" || c.SignerName != "" || c.TransactorPhone != "" { + if c.SignerAccount == "" { + return errors.New("签署人账号不能为空") + } + + if c.SignerName == "" { + return errors.New("签署人姓名不能为空") + } + + if c.TransactorPhone != "" { + // 手机号格式验证 + phonePattern := `^1[3-9]\d{9}$` + matched, err := regexp.MatchString(phonePattern, c.TransactorPhone) + if err != nil { + return fmt.Errorf("经办人手机号格式验证错误: %w", err) + } + + if !matched { + return errors.New("经办人手机号格式不正确") + } + } + + if c.TransactorIDCardNum != "" { + // 身份证号格式验证 + idPattern := `^[1-9]\d{5}(19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$` + matched, err := regexp.MatchString(idPattern, c.TransactorIDCardNum) + if err != nil { + return fmt.Errorf("经办人身份证号格式验证错误: %w", err) + } + + if !matched { + return errors.New("经办人身份证号格式不正确") + } + } + } + + return nil +} + +// validateStatus 验证合同状态 +func (c *ContractInfo) validateStatus() error { + validStatuses := []string{ + ContractStatusDraft, + ContractStatusGenerated, + ContractStatusSigning, + ContractStatusSigned, + ContractStatusExpired, + ContractStatusRejected, + ContractStatusCancelled, + } + + for _, status := range validStatuses { + if c.Status == status { + return nil + } + } + + return fmt.Errorf("无效的合同状态: %s", c.Status) +} + +// SetSignerInfo 设置签署人信息 +func (c *ContractInfo) SetSignerInfo(signerAccount, signerName, transactorPhone, transactorName, transactorIDCardNum string) error { + c.SignerAccount = strings.TrimSpace(signerAccount) + c.SignerName = strings.TrimSpace(signerName) + c.TransactorPhone = strings.TrimSpace(transactorPhone) + c.TransactorName = strings.TrimSpace(transactorName) + c.TransactorIDCardNum = strings.TrimSpace(transactorIDCardNum) + + return c.validateSignerInfo() +} + +// UpdateStatus 更新合同状态 +func (c *ContractInfo) UpdateStatus(status string) error { + oldStatus := c.Status + c.Status = status + + if err := c.validateStatus(); err != nil { + c.Status = oldStatus // 回滚 + return err + } + + // 根据状态更新进度 + c.updateProgressByStatus() + + return nil +} + +// updateProgressByStatus 根据状态更新进度 +func (c *ContractInfo) updateProgressByStatus() { + progressMap := map[string]int{ + ContractStatusDraft: 0, + ContractStatusGenerated: 25, + ContractStatusSigning: 50, + ContractStatusSigned: 100, + ContractStatusExpired: 50, + ContractStatusRejected: 50, + ContractStatusCancelled: 0, + } + + if progress, exists := progressMap[c.Status]; exists { + c.SignProgress = progress + } +} + +// MarkAsSigning 标记为签署中 +func (c *ContractInfo) MarkAsSigning() error { + c.Status = ContractStatusSigning + c.SignProgress = 50 + now := time.Now() + c.SignFlowCreatedAt = &now + + return nil +} + +// MarkAsSigned 标记为已签署 +func (c *ContractInfo) MarkAsSigned() error { + c.Status = ContractStatusSigned + c.SignProgress = 100 + now := time.Now() + c.SignedAt = &now + + return nil +} + +// MarkAsExpired 标记为已过期 +func (c *ContractInfo) MarkAsExpired() error { + c.Status = ContractStatusExpired + now := time.Now() + c.ExpiresAt = &now + + return nil +} + +// MarkAsRejected 标记为被拒绝 +func (c *ContractInfo) MarkAsRejected() error { + c.Status = ContractStatusRejected + c.SignProgress = 50 + + return nil +} + +// IsExpired 检查合同是否已过期 +func (c *ContractInfo) IsExpired() bool { + if c.ExpiresAt == nil { + return false + } + + return time.Now().After(*c.ExpiresAt) +} + +// IsSigned 检查合同是否已签署 +func (c *ContractInfo) IsSigned() bool { + return c.Status == ContractStatusSigned +} + +// CanSign 检查是否可以签署 +func (c *ContractInfo) CanSign() bool { + return c.Status == ContractStatusGenerated || c.Status == ContractStatusSigning +} + +// GetStatusName 获取状态的中文名称 +func (c *ContractInfo) GetStatusName() string { + statusNames := map[string]string{ + ContractStatusDraft: "草稿", + ContractStatusGenerated: "已生成", + ContractStatusSigning: "签署中", + ContractStatusSigned: "已签署", + ContractStatusExpired: "已过期", + ContractStatusRejected: "被拒绝", + ContractStatusCancelled: "已取消", + } + + if name, exists := statusNames[c.Status]; exists { + return name + } + return c.Status +} + +// GetDisplayTitle 获取显示用的合同标题 +func (c *ContractInfo) GetDisplayTitle() string { + if c.ContractTitle != "" { + return c.ContractTitle + } + return "企业认证服务合同" +} + +// GetMaskedSignerAccount 获取脱敏的签署人账号 +func (c *ContractInfo) GetMaskedSignerAccount() string { + if len(c.SignerAccount) <= 6 { + return c.SignerAccount + } + + // 保留前3位和后3位,中间用*替代 + return c.SignerAccount[:3] + "***" + c.SignerAccount[len(c.SignerAccount)-3:] +} + +// GetMaskedTransactorPhone 获取脱敏的经办人手机号 +func (c *ContractInfo) GetMaskedTransactorPhone() string { + if len(c.TransactorPhone) != 11 { + return c.TransactorPhone + } + + // 保留前3位和后4位,中间用*替代 + return c.TransactorPhone[:3] + "****" + c.TransactorPhone[7:] +} + +// GetMaskedTransactorIDCardNum 获取脱敏的经办人身份证号 +func (c *ContractInfo) GetMaskedTransactorIDCardNum() string { + if len(c.TransactorIDCardNum) != 18 { + return c.TransactorIDCardNum + } + + // 保留前6位和后4位,中间用*替代 + return c.TransactorIDCardNum[:6] + "********" + c.TransactorIDCardNum[14:] +} + +// AddMetadata 添加元数据 +func (c *ContractInfo) AddMetadata(key string, value interface{}) { + if c.Metadata == nil { + c.Metadata = make(map[string]interface{}) + } + c.Metadata[key] = value +} + +// GetMetadata 获取元数据 +func (c *ContractInfo) GetMetadata(key string) (interface{}, bool) { + if c.Metadata == nil { + return nil, false + } + value, exists := c.Metadata[key] + return value, exists +} + +// Equals 比较两个合同信息是否相等 +func (c *ContractInfo) Equals(other *ContractInfo) bool { + if other == nil { + return false + } + + return c.ContractFileID == other.ContractFileID && + c.EsignFlowID == other.EsignFlowID && + c.Status == other.Status +} + +// Clone 创建合同信息的副本 +func (c *ContractInfo) Clone() *ContractInfo { + cloned := &ContractInfo{ + ContractFileID: c.ContractFileID, + EsignFlowID: c.EsignFlowID, + ContractURL: c.ContractURL, + ContractSignURL: c.ContractSignURL, + ContractTitle: c.ContractTitle, + ContractVersion: c.ContractVersion, + TemplateID: c.TemplateID, + SignerAccount: c.SignerAccount, + SignerName: c.SignerName, + TransactorPhone: c.TransactorPhone, + TransactorName: c.TransactorName, + TransactorIDCardNum: c.TransactorIDCardNum, + Status: c.Status, + SignProgress: c.SignProgress, + } + + // 复制时间字段 + if c.GeneratedAt != nil { + generatedAt := *c.GeneratedAt + cloned.GeneratedAt = &generatedAt + } + if c.SignFlowCreatedAt != nil { + signFlowCreatedAt := *c.SignFlowCreatedAt + cloned.SignFlowCreatedAt = &signFlowCreatedAt + } + if c.SignedAt != nil { + signedAt := *c.SignedAt + cloned.SignedAt = &signedAt + } + if c.ExpiresAt != nil { + expiresAt := *c.ExpiresAt + cloned.ExpiresAt = &expiresAt + } + + // 复制元数据 + if c.Metadata != nil { + cloned.Metadata = make(map[string]interface{}) + for k, v := range c.Metadata { + cloned.Metadata[k] = v + } + } + + return cloned +} + +// String 返回合同信息的字符串表示 +func (c *ContractInfo) String() string { + return fmt.Sprintf("合同信息[文件ID:%s, 流程ID:%s, 状态:%s, 进度:%d%%]", + c.ContractFileID, + c.EsignFlowID, + c.GetStatusName(), + c.SignProgress) +} + +// ToMap 转换为map格式(用于序列化) +func (c *ContractInfo) ToMap() map[string]interface{} { + result := map[string]interface{}{ + "contract_file_id": c.ContractFileID, + "esign_flow_id": c.EsignFlowID, + "contract_url": c.ContractURL, + "contract_sign_url": c.ContractSignURL, + "contract_title": c.ContractTitle, + "contract_version": c.ContractVersion, + "template_id": c.TemplateID, + "signer_account": c.SignerAccount, + "signer_name": c.SignerName, + "transactor_phone": c.TransactorPhone, + "transactor_name": c.TransactorName, + "transactor_id_card_num": c.TransactorIDCardNum, + "status": c.Status, + "sign_progress": c.SignProgress, + } + + // 添加时间字段 + if c.GeneratedAt != nil { + result["generated_at"] = c.GeneratedAt + } + if c.SignFlowCreatedAt != nil { + result["sign_flow_created_at"] = c.SignFlowCreatedAt + } + if c.SignedAt != nil { + result["signed_at"] = c.SignedAt + } + if c.ExpiresAt != nil { + result["expires_at"] = c.ExpiresAt + } + + // 添加元数据 + if c.Metadata != nil { + result["metadata"] = c.Metadata + } + + return result +} \ No newline at end of file diff --git a/internal/domains/certification/entities/value_objects/enterprise_info.go b/internal/domains/certification/entities/value_objects/enterprise_info.go new file mode 100644 index 0000000..6cce267 --- /dev/null +++ b/internal/domains/certification/entities/value_objects/enterprise_info.go @@ -0,0 +1,352 @@ +package value_objects + +import ( + "errors" + "fmt" + "regexp" + "strings" +) + +// EnterpriseInfo 企业信息值对象 +// 封装企业认证所需的核心信息,包含完整的业务规则验证 +type EnterpriseInfo struct { + // 企业基本信息 + CompanyName string `json:"company_name"` // 企业名称 + UnifiedSocialCode string `json:"unified_social_code"` // 统一社会信用代码 + + // 法定代表人信息 + LegalPersonName string `json:"legal_person_name"` // 法定代表人姓名 + LegalPersonID string `json:"legal_person_id"` // 法定代表人身份证号 + LegalPersonPhone string `json:"legal_person_phone"` // 法定代表人手机号 + + // 企业详细信息 + RegisteredAddress string `json:"registered_address"` // 注册地址 + BusinessScope string `json:"business_scope"` // 经营范围 + RegisteredCapital string `json:"registered_capital"` // 注册资本 + EstablishmentDate string `json:"establishment_date"` // 成立日期 +} + +// NewEnterpriseInfo 创建企业信息值对象 +func NewEnterpriseInfo(companyName, unifiedSocialCode, legalPersonName, legalPersonID, legalPersonPhone string) (*EnterpriseInfo, error) { + info := &EnterpriseInfo{ + CompanyName: strings.TrimSpace(companyName), + UnifiedSocialCode: strings.TrimSpace(unifiedSocialCode), + LegalPersonName: strings.TrimSpace(legalPersonName), + LegalPersonID: strings.TrimSpace(legalPersonID), + LegalPersonPhone: strings.TrimSpace(legalPersonPhone), + } + + if err := info.Validate(); err != nil { + return nil, fmt.Errorf("企业信息验证失败: %w", err) + } + + return info, nil +} + +// Validate 验证企业信息的完整性和格式 +func (e *EnterpriseInfo) Validate() error { + if err := e.validateCompanyName(); err != nil { + return err + } + + if err := e.validateUnifiedSocialCode(); err != nil { + return err + } + + if err := e.validateLegalPersonName(); err != nil { + return err + } + + if err := e.validateLegalPersonID(); err != nil { + return err + } + + if err := e.validateLegalPersonPhone(); err != nil { + return err + } + + return nil +} + +// validateCompanyName 验证企业名称 +func (e *EnterpriseInfo) validateCompanyName() error { + if e.CompanyName == "" { + return errors.New("企业名称不能为空") + } + + if len(e.CompanyName) < 2 { + return errors.New("企业名称长度不能少于2个字符") + } + + if len(e.CompanyName) > 100 { + return errors.New("企业名称长度不能超过100个字符") + } + + // 检查是否包含非法字符 + invalidChars := []string{"`", "~", "!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "+", "=", "{", "}", "[", "]", "\\", "|", ";", ":", "'", "\"", "<", ">", ",", ".", "?", "/"} + for _, char := range invalidChars { + if strings.Contains(e.CompanyName, char) { + return fmt.Errorf("企业名称不能包含特殊字符: %s", char) + } + } + + return nil +} + +// validateUnifiedSocialCode 验证统一社会信用代码 +func (e *EnterpriseInfo) validateUnifiedSocialCode() error { + if e.UnifiedSocialCode == "" { + return errors.New("统一社会信用代码不能为空") + } + + // 统一社会信用代码格式验证(18位数字和字母) + pattern := `^[0-9A-HJ-NPQRTUWXY]{2}[0-9]{6}[0-9A-HJ-NPQRTUWXY]{10}$` + matched, err := regexp.MatchString(pattern, e.UnifiedSocialCode) + if err != nil { + return fmt.Errorf("统一社会信用代码格式验证错误: %w", err) + } + + if !matched { + return errors.New("统一社会信用代码格式不正确,应为18位数字和字母组合") + } + + return nil +} + +// validateLegalPersonName 验证法定代表人姓名 +func (e *EnterpriseInfo) validateLegalPersonName() error { + if e.LegalPersonName == "" { + return errors.New("法定代表人姓名不能为空") + } + + if len(e.LegalPersonName) < 2 { + return errors.New("法定代表人姓名长度不能少于2个字符") + } + + if len(e.LegalPersonName) > 50 { + return errors.New("法定代表人姓名长度不能超过50个字符") + } + + // 中文姓名格式验证 + pattern := `^[\u4e00-\u9fa5·]+$` + matched, err := regexp.MatchString(pattern, e.LegalPersonName) + if err != nil { + return fmt.Errorf("法定代表人姓名格式验证错误: %w", err) + } + + if !matched { + return errors.New("法定代表人姓名只能包含中文字符和间隔号") + } + + return nil +} + +// validateLegalPersonID 验证法定代表人身份证号 +func (e *EnterpriseInfo) validateLegalPersonID() error { + if e.LegalPersonID == "" { + return errors.New("法定代表人身份证号不能为空") + } + + // 身份证号格式验证(18位) + if len(e.LegalPersonID) != 18 { + return errors.New("身份证号必须为18位") + } + + pattern := `^[1-9]\d{5}(19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$` + matched, err := regexp.MatchString(pattern, e.LegalPersonID) + if err != nil { + return fmt.Errorf("身份证号格式验证错误: %w", err) + } + + if !matched { + return errors.New("身份证号格式不正确") + } + + // 身份证号校验码验证 + if !e.validateIDChecksum() { + return errors.New("身份证号校验码错误") + } + + return nil +} + +// validateIDChecksum 验证身份证号校验码 +func (e *EnterpriseInfo) validateIDChecksum() bool { + if len(e.LegalPersonID) != 18 { + return false + } + + // 加权因子 + weights := []int{7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2} + // 校验码对应表 + checkCodes := []string{"1", "0", "X", "9", "8", "7", "6", "5", "4", "3", "2"} + + sum := 0 + for i := 0; i < 17; i++ { + digit := int(e.LegalPersonID[i] - '0') + sum += digit * weights[i] + } + + checkCodeIndex := sum % 11 + expectedCheckCode := checkCodes[checkCodeIndex] + actualCheckCode := strings.ToUpper(string(e.LegalPersonID[17])) + + return expectedCheckCode == actualCheckCode +} + +// validateLegalPersonPhone 验证法定代表人手机号 +func (e *EnterpriseInfo) validateLegalPersonPhone() error { + if e.LegalPersonPhone == "" { + return errors.New("法定代表人手机号不能为空") + } + + // 手机号格式验证(11位数字,1开头) + pattern := `^1[3-9]\d{9}$` + matched, err := regexp.MatchString(pattern, e.LegalPersonPhone) + if err != nil { + return fmt.Errorf("手机号格式验证错误: %w", err) + } + + if !matched { + return errors.New("手机号格式不正确,应为11位数字且以1开头") + } + + return nil +} + +// IsComplete 检查企业信息是否完整 +func (e *EnterpriseInfo) IsComplete() bool { + return e.CompanyName != "" && + e.UnifiedSocialCode != "" && + e.LegalPersonName != "" && + e.LegalPersonID != "" && + e.LegalPersonPhone != "" +} + +// IsDetailComplete 检查企业详细信息是否完整 +func (e *EnterpriseInfo) IsDetailComplete() bool { + return e.IsComplete() && + e.RegisteredAddress != "" && + e.BusinessScope != "" && + e.RegisteredCapital != "" && + e.EstablishmentDate != "" +} + +// GetDisplayName 获取显示用的企业名称 +func (e *EnterpriseInfo) GetDisplayName() string { + if e.CompanyName == "" { + return "未知企业" + } + return e.CompanyName +} + +// GetMaskedUnifiedSocialCode 获取脱敏的统一社会信用代码 +func (e *EnterpriseInfo) GetMaskedUnifiedSocialCode() string { + if len(e.UnifiedSocialCode) != 18 { + return e.UnifiedSocialCode + } + + // 保留前6位和后4位,中间用*替代 + return e.UnifiedSocialCode[:6] + "********" + e.UnifiedSocialCode[14:] +} + +// GetMaskedLegalPersonID 获取脱敏的法定代表人身份证号 +func (e *EnterpriseInfo) GetMaskedLegalPersonID() string { + if len(e.LegalPersonID) != 18 { + return e.LegalPersonID + } + + // 保留前6位和后4位,中间用*替代 + return e.LegalPersonID[:6] + "********" + e.LegalPersonID[14:] +} + +// GetMaskedLegalPersonPhone 获取脱敏的法定代表人手机号 +func (e *EnterpriseInfo) GetMaskedLegalPersonPhone() string { + if len(e.LegalPersonPhone) != 11 { + return e.LegalPersonPhone + } + + // 保留前3位和后4位,中间用*替代 + return e.LegalPersonPhone[:3] + "****" + e.LegalPersonPhone[7:] +} + +// Equals 比较两个企业信息是否相等 +func (e *EnterpriseInfo) Equals(other *EnterpriseInfo) bool { + if other == nil { + return false + } + + return e.CompanyName == other.CompanyName && + e.UnifiedSocialCode == other.UnifiedSocialCode && + e.LegalPersonName == other.LegalPersonName && + e.LegalPersonID == other.LegalPersonID && + e.LegalPersonPhone == other.LegalPersonPhone +} + +// Clone 创建企业信息的副本 +func (e *EnterpriseInfo) Clone() *EnterpriseInfo { + return &EnterpriseInfo{ + CompanyName: e.CompanyName, + UnifiedSocialCode: e.UnifiedSocialCode, + LegalPersonName: e.LegalPersonName, + LegalPersonID: e.LegalPersonID, + LegalPersonPhone: e.LegalPersonPhone, + RegisteredAddress: e.RegisteredAddress, + BusinessScope: e.BusinessScope, + RegisteredCapital: e.RegisteredCapital, + EstablishmentDate: e.EstablishmentDate, + } +} + +// String 返回企业信息的字符串表示 +func (e *EnterpriseInfo) String() string { + return fmt.Sprintf("企业信息[名称:%s, 信用代码:%s, 法人:%s]", + e.CompanyName, + e.GetMaskedUnifiedSocialCode(), + e.LegalPersonName) +} + +// ToMap 转换为map格式(用于序列化) +func (e *EnterpriseInfo) ToMap() map[string]interface{} { + return map[string]interface{}{ + "company_name": e.CompanyName, + "unified_social_code": e.UnifiedSocialCode, + "legal_person_name": e.LegalPersonName, + "legal_person_id": e.LegalPersonID, + "legal_person_phone": e.LegalPersonPhone, + "registered_address": e.RegisteredAddress, + "business_scope": e.BusinessScope, + "registered_capital": e.RegisteredCapital, + "establishment_date": e.EstablishmentDate, + } +} + +// FromMap 从map格式创建企业信息(用于反序列化) +func FromMap(data map[string]interface{}) (*EnterpriseInfo, error) { + getString := func(key string) string { + if val, exists := data[key]; exists { + if str, ok := val.(string); ok { + return strings.TrimSpace(str) + } + } + return "" + } + + info := &EnterpriseInfo{ + CompanyName: getString("company_name"), + UnifiedSocialCode: getString("unified_social_code"), + LegalPersonName: getString("legal_person_name"), + LegalPersonID: getString("legal_person_id"), + LegalPersonPhone: getString("legal_person_phone"), + RegisteredAddress: getString("registered_address"), + BusinessScope: getString("business_scope"), + RegisteredCapital: getString("registered_capital"), + EstablishmentDate: getString("establishment_date"), + } + + if err := info.Validate(); err != nil { + return nil, fmt.Errorf("从Map创建企业信息失败: %w", err) + } + + return info, nil +} \ No newline at end of file diff --git a/internal/domains/certification/enums/actor_type.go b/internal/domains/certification/enums/actor_type.go new file mode 100644 index 0000000..f608e95 --- /dev/null +++ b/internal/domains/certification/enums/actor_type.go @@ -0,0 +1,215 @@ +package enums + +// ActorType 操作者类型枚举 +type ActorType string + +const ( + // === 操作者类型 === + ActorTypeUser ActorType = "user" // 用户操作 + ActorTypeSystem ActorType = "system" // 系统操作 + ActorTypeAdmin ActorType = "admin" // 管理员操作 + ActorTypeEsign ActorType = "esign" // e签宝回调操作 +) + +// AllActorTypes 所有操作者类型列表 +var AllActorTypes = []ActorType{ + ActorTypeUser, + ActorTypeSystem, + ActorTypeAdmin, + ActorTypeEsign, +} + +// IsValidActorType 检查操作者类型是否有效 +func IsValidActorType(actorType ActorType) bool { + for _, validType := range AllActorTypes { + if actorType == validType { + return true + } + } + return false +} + +// GetActorTypeName 获取操作者类型的中文名称 +func GetActorTypeName(actorType ActorType) string { + typeNames := map[ActorType]string{ + ActorTypeUser: "用户", + ActorTypeSystem: "系统", + ActorTypeAdmin: "管理员", + ActorTypeEsign: "e签宝", + } + + if name, exists := typeNames[actorType]; exists { + return name + } + return string(actorType) +} + +// GetActorTypeDescription 获取操作者类型描述 +func GetActorTypeDescription(actorType ActorType) string { + descriptions := map[ActorType]string{ + ActorTypeUser: "用户主动操作", + ActorTypeSystem: "系统自动操作", + ActorTypeAdmin: "管理员操作", + ActorTypeEsign: "e签宝回调操作", + } + + if desc, exists := descriptions[actorType]; exists { + return desc + } + return string(actorType) +} + +// IsAutomatedActor 判断是否为自动化操作者 +func IsAutomatedActor(actorType ActorType) bool { + automatedActors := map[ActorType]bool{ + ActorTypeUser: false, // 用户手动操作 + ActorTypeSystem: true, // 系统自动操作 + ActorTypeAdmin: false, // 管理员手动操作 + ActorTypeEsign: true, // e签宝自动回调 + } + + if automated, exists := automatedActors[actorType]; exists { + return automated + } + return false +} + +// IsHumanActor 判断是否为人工操作者 +func IsHumanActor(actorType ActorType) bool { + return !IsAutomatedActor(actorType) +} + +// GetActorTypePriority 获取操作者类型优先级(用于日志排序等) +func GetActorTypePriority(actorType ActorType) int { + priorities := map[ActorType]int{ + ActorTypeUser: 1, // 用户操作最重要 + ActorTypeAdmin: 2, // 管理员操作次之 + ActorTypeEsign: 3, // e签宝回调 + ActorTypeSystem: 4, // 系统操作最后 + } + + if priority, exists := priorities[actorType]; exists { + return priority + } + return 999 +} + +// GetPermissionLevel 获取权限级别 +func GetPermissionLevel(actorType ActorType) int { + levels := map[ActorType]int{ + ActorTypeUser: 1, // 普通用户权限 + ActorTypeSystem: 2, // 系统权限 + ActorTypeEsign: 2, // e签宝权限(与系统同级) + ActorTypeAdmin: 3, // 管理员最高权限 + } + + if level, exists := levels[actorType]; exists { + return level + } + return 0 +} + +// CanPerformAction 检查操作者是否可以执行指定操作 +func CanPerformAction(actorType ActorType, action string) bool { + permissions := map[ActorType][]string{ + ActorTypeUser: { + "submit_enterprise_info", // 提交企业信息 + "apply_contract", // 申请合同 + "view_certification", // 查看认证信息 + }, + ActorTypeSystem: { + "auto_transition", // 自动状态转换 + "system_maintenance", // 系统维护 + "data_cleanup", // 数据清理 + }, + ActorTypeAdmin: { + "manual_transition", // 手动状态转换 + "view_all_certifications", // 查看所有认证 + "system_configuration", // 系统配置 + "user_management", // 用户管理 + }, + ActorTypeEsign: { + "callback_notification", // 回调通知 + "status_update", // 状态更新 + "verification_result", // 验证结果 + }, + } + + if actions, exists := permissions[actorType]; exists { + for _, permittedAction := range actions { + if permittedAction == action { + return true + } + } + } + return false +} + +// GetAllowedActions 获取操作者允许执行的所有操作 +func GetAllowedActions(actorType ActorType) []string { + permissions := map[ActorType][]string{ + ActorTypeUser: { + "submit_enterprise_info", + "apply_contract", + "view_certification", + }, + ActorTypeSystem: { + "auto_transition", + "system_maintenance", + "data_cleanup", + }, + ActorTypeAdmin: { + "manual_transition", + "view_all_certifications", + "system_configuration", + "user_management", + }, + ActorTypeEsign: { + "callback_notification", + "status_update", + "verification_result", + }, + } + + if actions, exists := permissions[actorType]; exists { + return actions + } + return []string{} +} + +// GetActorTypeFromContext 从上下文推断操作者类型 +func GetActorTypeFromContext(context map[string]interface{}) ActorType { + // 检查是否为e签宝回调 + if _, exists := context["esign_callback"]; exists { + return ActorTypeEsign + } + + // 检查是否为管理员操作 + if isAdmin, exists := context["is_admin"]; exists && isAdmin.(bool) { + return ActorTypeAdmin + } + + // 检查是否为用户操作 + if userID, exists := context["user_id"]; exists && userID != nil { + return ActorTypeUser + } + + // 默认为系统操作 + return ActorTypeSystem +} + +// FormatActorInfo 格式化操作者信息 +func FormatActorInfo(actorType ActorType, actorID string) string { + switch actorType { + case ActorTypeUser: + return "用户(" + actorID + ")" + case ActorTypeAdmin: + return "管理员(" + actorID + ")" + case ActorTypeSystem: + return "系统" + case ActorTypeEsign: + return "e签宝回调" + default: + return string(actorType) + "(" + actorID + ")" + } +} \ No newline at end of file diff --git a/internal/domains/certification/enums/certification_status.go b/internal/domains/certification/enums/certification_status.go index 59d556a..3aff4a4 100644 --- a/internal/domains/certification/enums/certification_status.go +++ b/internal/domains/certification/enums/certification_status.go @@ -4,23 +4,50 @@ package enums type CertificationStatus string const ( - // 主流程状态 - StatusPending CertificationStatus = "pending" // 待认证 - StatusInfoSubmitted CertificationStatus = "info_submitted" // 已提交企业信息 - StatusEnterpriseVerified CertificationStatus = "enterprise_verified" // 已企业认证 - StatusContractApplied CertificationStatus = "contract_applied" // 已申请签署合同 - StatusContractSigned CertificationStatus = "contract_signed" // 已签署合同 - StatusCompleted CertificationStatus = "completed" // 认证完成 + // === 主流程状态 === + StatusPending CertificationStatus = "pending" // 待认证 + StatusInfoSubmitted CertificationStatus = "info_submitted" // 已提交企业信息 + StatusEnterpriseVerified CertificationStatus = "enterprise_verified" // 已企业认证 + StatusContractApplied CertificationStatus = "contract_applied" // 已申请签署合同 + StatusContractSigned CertificationStatus = "contract_signed" // 已签署合同(认证完成) + + // === 失败状态 === + StatusInfoRejected CertificationStatus = "info_rejected" // 企业信息被拒绝 + StatusContractRejected CertificationStatus = "contract_rejected" // 合同被拒签 + StatusContractExpired CertificationStatus = "contract_expired" // 合同签署超时 ) +// AllStatuses 所有有效状态列表 +var AllStatuses = []CertificationStatus{ + StatusPending, + StatusInfoSubmitted, + StatusEnterpriseVerified, + StatusContractApplied, + StatusContractSigned, + StatusInfoRejected, + StatusContractRejected, + StatusContractExpired, +} + +// MainFlowStatuses 主流程状态列表 +var MainFlowStatuses = []CertificationStatus{ + StatusPending, + StatusInfoSubmitted, + StatusEnterpriseVerified, + StatusContractApplied, + StatusContractSigned, +} + +// FailureStatuses 失败状态列表 +var FailureStatuses = []CertificationStatus{ + StatusInfoRejected, + StatusContractRejected, + StatusContractExpired, +} + // IsValidStatus 检查状态是否有效 func IsValidStatus(status CertificationStatus) bool { - validStatuses := []CertificationStatus{ - StatusPending, StatusInfoSubmitted, StatusEnterpriseVerified, - StatusContractApplied, StatusContractSigned, StatusCompleted, - } - - for _, validStatus := range validStatuses { + for _, validStatus := range AllStatuses { if status == validStatus { return true } @@ -31,12 +58,14 @@ func IsValidStatus(status CertificationStatus) bool { // GetStatusName 获取状态的中文名称 func GetStatusName(status CertificationStatus) string { statusNames := map[CertificationStatus]string{ - StatusPending: "待认证", - StatusInfoSubmitted: "已提交企业信息", + StatusPending: "待认证", + StatusInfoSubmitted: "已提交企业信息", StatusEnterpriseVerified: "已企业认证", - StatusContractApplied: "已申请合同", - StatusContractSigned: "已签署合同", - StatusCompleted: "认证完成", + StatusContractApplied: "已申请签署合同", + StatusContractSigned: "认证完成", + StatusInfoRejected: "企业信息被拒绝", + StatusContractRejected: "合同被拒签", + StatusContractExpired: "合同签署超时", } if name, exists := statusNames[status]; exists { @@ -47,32 +76,51 @@ func GetStatusName(status CertificationStatus) string { // IsFinalStatus 判断是否为最终状态 func IsFinalStatus(status CertificationStatus) bool { - return status == StatusCompleted + return status == StatusContractSigned +} + +// IsFailureStatus 判断是否为失败状态 +func IsFailureStatus(status CertificationStatus) bool { + for _, failureStatus := range FailureStatuses { + if status == failureStatus { + return true + } + } + return false +} + +// IsMainFlowStatus 判断是否为主流程状态 +func IsMainFlowStatus(status CertificationStatus) bool { + for _, mainStatus := range MainFlowStatuses { + if status == mainStatus { + return true + } + } + return false } // GetStatusCategory 获取状态分类 func GetStatusCategory(status CertificationStatus) string { - switch status { - case StatusPending: - return "initial" - case StatusInfoSubmitted, StatusEnterpriseVerified, StatusContractApplied, StatusContractSigned: - return "processing" - case StatusCompleted: - return "completed" - default: - return "unknown" + if IsMainFlowStatus(status) { + return "主流程" } + if IsFailureStatus(status) { + return "失败状态" + } + return "未知" } // GetStatusPriority 获取状态优先级(用于排序) func GetStatusPriority(status CertificationStatus) int { priorities := map[CertificationStatus]int{ - StatusPending: 0, - StatusInfoSubmitted: 1, - StatusEnterpriseVerified: 2, - StatusContractApplied: 3, - StatusContractSigned: 4, - StatusCompleted: 5, + StatusPending: 1, + StatusInfoSubmitted: 2, + StatusEnterpriseVerified: 3, + StatusContractApplied: 4, + StatusContractSigned: 5, + StatusInfoRejected: 6, + StatusContractRejected: 7, + StatusContractExpired: 8, } if priority, exists := priorities[status]; exists { @@ -80,3 +128,131 @@ func GetStatusPriority(status CertificationStatus) int { } return 999 } + +// GetProgressPercentage 获取进度百分比 +func GetProgressPercentage(status CertificationStatus) int { + progressMap := map[CertificationStatus]int{ + StatusPending: 0, + StatusInfoSubmitted: 25, + StatusEnterpriseVerified: 50, + StatusContractApplied: 75, + StatusContractSigned: 100, + StatusInfoRejected: 25, + StatusContractRejected: 75, + StatusContractExpired: 75, + } + + if progress, exists := progressMap[status]; exists { + return progress + } + return 0 +} + +// IsUserActionRequired 检查是否需要用户操作 +func IsUserActionRequired(status CertificationStatus) bool { + userActionRequired := map[CertificationStatus]bool{ + StatusPending: true, // 需要提交企业信息 + StatusInfoSubmitted: false, // 等待系统验证 + StatusEnterpriseVerified: true, // 需要申请合同 + StatusContractApplied: true, // 需要签署合同 + StatusContractSigned: false, // 已完成 + StatusInfoRejected: true, // 需要重新提交 + StatusContractRejected: true, // 需要重新申请 + StatusContractExpired: true, // 需要重新申请 + } + + if required, exists := userActionRequired[status]; exists { + return required + } + return false +} + +// GetUserActionHint 获取用户操作提示 +func GetUserActionHint(status CertificationStatus) string { + hints := map[CertificationStatus]string{ + StatusPending: "请提交企业信息", + StatusInfoSubmitted: "系统正在验证企业信息,请稍候", + StatusEnterpriseVerified: "企业认证完成,请申请签署合同", + StatusContractApplied: "请在规定时间内完成合同签署", + StatusContractSigned: "认证已完成", + StatusInfoRejected: "企业信息验证失败,请修正后重新提交", + StatusContractRejected: "合同签署被拒绝,可重新申请", + StatusContractExpired: "合同签署已超时,请重新申请", + } + + if hint, exists := hints[status]; exists { + return hint + } + return "" +} + +// GetNextValidStatuses 获取当前状态的下一个有效状态列表 +func GetNextValidStatuses(currentStatus CertificationStatus) []CertificationStatus { + nextStatusMap := map[CertificationStatus][]CertificationStatus{ + StatusPending: { + StatusInfoSubmitted, + }, + StatusInfoSubmitted: { + StatusEnterpriseVerified, + StatusInfoRejected, + }, + StatusEnterpriseVerified: { + StatusContractApplied, + }, + StatusContractApplied: { + StatusContractSigned, + StatusContractRejected, + StatusContractExpired, + }, + StatusContractSigned: { + // 最终状态,无后续状态 + }, + StatusInfoRejected: { + StatusInfoSubmitted, // 可以重新提交 + }, + StatusContractRejected: { + StatusEnterpriseVerified, // 重置到企业认证状态 + }, + StatusContractExpired: { + StatusEnterpriseVerified, // 重置到企业认证状态 + }, + } + + if nextStatuses, exists := nextStatusMap[currentStatus]; exists { + return nextStatuses + } + return []CertificationStatus{} +} + +// CanTransitionTo 检查是否可以从当前状态转换到目标状态 +func CanTransitionTo(currentStatus, targetStatus CertificationStatus) bool { + validNextStatuses := GetNextValidStatuses(currentStatus) + for _, validStatus := range validNextStatuses { + if validStatus == targetStatus { + return true + } + } + return false +} + +// GetTransitionReason 获取状态转换的原因描述 +func GetTransitionReason(from, to CertificationStatus) string { + transitionReasons := map[string]string{ + string(StatusPending) + "->" + string(StatusInfoSubmitted): "用户提交企业信息", + string(StatusInfoSubmitted) + "->" + string(StatusEnterpriseVerified): "e签宝企业认证成功", + string(StatusInfoSubmitted) + "->" + string(StatusInfoRejected): "e签宝企业认证失败", + string(StatusEnterpriseVerified) + "->" + string(StatusContractApplied): "用户申请签署合同", + string(StatusContractApplied) + "->" + string(StatusContractSigned): "e签宝合同签署成功", + string(StatusContractApplied) + "->" + string(StatusContractRejected): "用户拒绝签署合同", + string(StatusContractApplied) + "->" + string(StatusContractExpired): "合同签署超时", + string(StatusInfoRejected) + "->" + string(StatusInfoSubmitted): "用户重新提交企业信息", + string(StatusContractRejected) + "->" + string(StatusEnterpriseVerified): "重置状态,准备重新申请", + string(StatusContractExpired) + "->" + string(StatusEnterpriseVerified): "重置状态,准备重新申请", + } + + key := string(from) + "->" + string(to) + if reason, exists := transitionReasons[key]; exists { + return reason + } + return "未知转换" +} diff --git a/internal/domains/certification/enums/failure_reason.go b/internal/domains/certification/enums/failure_reason.go new file mode 100644 index 0000000..c593a3d --- /dev/null +++ b/internal/domains/certification/enums/failure_reason.go @@ -0,0 +1,270 @@ +package enums + +// FailureReason 失败原因枚举 +type FailureReason string + +const ( + // === 企业信息验证失败原因 === + FailureReasonEnterpriseNotExists FailureReason = "enterprise_not_exists" // 企业不存在 + FailureReasonEnterpriseInfoMismatch FailureReason = "enterprise_info_mismatch" // 企业信息不匹配 + FailureReasonEnterpriseStatusAbnormal FailureReason = "enterprise_status_abnormal" // 企业状态异常 + FailureReasonLegalPersonMismatch FailureReason = "legal_person_mismatch" // 法定代表人信息不匹配 + FailureReasonEsignVerificationFailed FailureReason = "esign_verification_failed" // e签宝验证失败 + FailureReasonInvalidDocument FailureReason = "invalid_document" // 证件信息无效 + + // === 合同签署失败原因 === + FailureReasonContractRejectedByUser FailureReason = "contract_rejected_by_user" // 用户拒绝签署 + FailureReasonContractExpired FailureReason = "contract_expired" // 合同签署超时 + FailureReasonSignProcessFailed FailureReason = "sign_process_failed" // 签署流程失败 + FailureReasonContractGenFailed FailureReason = "contract_gen_failed" // 合同生成失败 + FailureReasonEsignFlowError FailureReason = "esign_flow_error" // e签宝流程错误 + + // === 系统错误原因 === + FailureReasonSystemError FailureReason = "system_error" // 系统错误 + FailureReasonNetworkError FailureReason = "network_error" // 网络错误 + FailureReasonTimeout FailureReason = "timeout" // 操作超时 + FailureReasonUnknownError FailureReason = "unknown_error" // 未知错误 +) + +// AllFailureReasons 所有失败原因列表 +var AllFailureReasons = []FailureReason{ + // 企业信息验证失败 + FailureReasonEnterpriseNotExists, + FailureReasonEnterpriseInfoMismatch, + FailureReasonEnterpriseStatusAbnormal, + FailureReasonLegalPersonMismatch, + FailureReasonEsignVerificationFailed, + FailureReasonInvalidDocument, + + // 合同签署失败 + FailureReasonContractRejectedByUser, + FailureReasonContractExpired, + FailureReasonSignProcessFailed, + FailureReasonContractGenFailed, + FailureReasonEsignFlowError, + + // 系统错误 + FailureReasonSystemError, + FailureReasonNetworkError, + FailureReasonTimeout, + FailureReasonUnknownError, +} + +// EnterpriseVerificationFailureReasons 企业验证失败原因列表 +var EnterpriseVerificationFailureReasons = []FailureReason{ + FailureReasonEnterpriseNotExists, + FailureReasonEnterpriseInfoMismatch, + FailureReasonEnterpriseStatusAbnormal, + FailureReasonLegalPersonMismatch, + FailureReasonEsignVerificationFailed, + FailureReasonInvalidDocument, +} + +// ContractSignFailureReasons 合同签署失败原因列表 +var ContractSignFailureReasons = []FailureReason{ + FailureReasonContractRejectedByUser, + FailureReasonContractExpired, + FailureReasonSignProcessFailed, + FailureReasonContractGenFailed, + FailureReasonEsignFlowError, +} + +// SystemErrorReasons 系统错误原因列表 +var SystemErrorReasons = []FailureReason{ + FailureReasonSystemError, + FailureReasonNetworkError, + FailureReasonTimeout, + FailureReasonUnknownError, +} + +// IsValidFailureReason 检查失败原因是否有效 +func IsValidFailureReason(reason FailureReason) bool { + for _, validReason := range AllFailureReasons { + if reason == validReason { + return true + } + } + return false +} + +// GetFailureReasonName 获取失败原因的中文名称 +func GetFailureReasonName(reason FailureReason) string { + reasonNames := map[FailureReason]string{ + // 企业信息验证失败 + FailureReasonEnterpriseNotExists: "企业不存在", + FailureReasonEnterpriseInfoMismatch: "企业信息不匹配", + FailureReasonEnterpriseStatusAbnormal: "企业状态异常", + FailureReasonLegalPersonMismatch: "法定代表人信息不匹配", + FailureReasonEsignVerificationFailed: "e签宝验证失败", + FailureReasonInvalidDocument: "证件信息无效", + + // 合同签署失败 + FailureReasonContractRejectedByUser: "用户拒绝签署", + FailureReasonContractExpired: "合同签署超时", + FailureReasonSignProcessFailed: "签署流程失败", + FailureReasonContractGenFailed: "合同生成失败", + FailureReasonEsignFlowError: "e签宝流程错误", + + // 系统错误 + FailureReasonSystemError: "系统错误", + FailureReasonNetworkError: "网络错误", + FailureReasonTimeout: "操作超时", + FailureReasonUnknownError: "未知错误", + } + + if name, exists := reasonNames[reason]; exists { + return name + } + return string(reason) +} + +// GetFailureReasonCategory 获取失败原因分类 +func GetFailureReasonCategory(reason FailureReason) string { + categories := map[FailureReason]string{ + // 企业信息验证失败 + FailureReasonEnterpriseNotExists: "企业验证", + FailureReasonEnterpriseInfoMismatch: "企业验证", + FailureReasonEnterpriseStatusAbnormal: "企业验证", + FailureReasonLegalPersonMismatch: "企业验证", + FailureReasonEsignVerificationFailed: "企业验证", + FailureReasonInvalidDocument: "企业验证", + + // 合同签署失败 + FailureReasonContractRejectedByUser: "合同签署", + FailureReasonContractExpired: "合同签署", + FailureReasonSignProcessFailed: "合同签署", + FailureReasonContractGenFailed: "合同签署", + FailureReasonEsignFlowError: "合同签署", + + // 系统错误 + FailureReasonSystemError: "系统错误", + FailureReasonNetworkError: "系统错误", + FailureReasonTimeout: "系统错误", + FailureReasonUnknownError: "系统错误", + } + + if category, exists := categories[reason]; exists { + return category + } + return "未知" +} + +// IsEnterpriseVerificationFailure 判断是否为企业验证失败 +func IsEnterpriseVerificationFailure(reason FailureReason) bool { + for _, verifyReason := range EnterpriseVerificationFailureReasons { + if reason == verifyReason { + return true + } + } + return false +} + +// IsContractSignFailure 判断是否为合同签署失败 +func IsContractSignFailure(reason FailureReason) bool { + for _, signReason := range ContractSignFailureReasons { + if reason == signReason { + return true + } + } + return false +} + +// IsSystemError 判断是否为系统错误 +func IsSystemError(reason FailureReason) bool { + for _, systemReason := range SystemErrorReasons { + if reason == systemReason { + return true + } + } + return false +} + +// GetSuggestedAction 获取建议的后续操作 +func GetSuggestedAction(reason FailureReason) string { + actions := map[FailureReason]string{ + // 企业信息验证失败 + FailureReasonEnterpriseNotExists: "请检查企业名称和统一社会信用代码是否正确", + FailureReasonEnterpriseInfoMismatch: "请核对企业信息是否与工商登记信息一致", + FailureReasonEnterpriseStatusAbnormal: "请确认企业状态正常,如有疑问请联系客服", + FailureReasonLegalPersonMismatch: "请核对法定代表人信息是否正确", + FailureReasonEsignVerificationFailed: "请稍后重试,如持续失败请联系客服", + FailureReasonInvalidDocument: "请检查证件信息是否有效", + + // 合同签署失败 + FailureReasonContractRejectedByUser: "您可以重新申请签署合同", + FailureReasonContractExpired: "请重新申请签署合同", + FailureReasonSignProcessFailed: "请重新尝试签署,如持续失败请联系客服", + FailureReasonContractGenFailed: "系统正在处理,请稍后重试", + FailureReasonEsignFlowError: "请稍后重试,如持续失败请联系客服", + + // 系统错误 + FailureReasonSystemError: "系统暂时不可用,请稍后重试", + FailureReasonNetworkError: "网络连接异常,请检查网络后重试", + FailureReasonTimeout: "操作超时,请重新尝试", + FailureReasonUnknownError: "发生未知错误,请联系客服", + } + + if action, exists := actions[reason]; exists { + return action + } + return "请联系客服处理" +} + +// IsRetryable 判断是否可以重试 +func IsRetryable(reason FailureReason) bool { + retryableReasons := map[FailureReason]bool{ + // 企业信息验证失败 - 用户数据问题,可重试 + FailureReasonEnterpriseNotExists: true, + FailureReasonEnterpriseInfoMismatch: true, + FailureReasonEnterpriseStatusAbnormal: false, // 企业状态问题,需要外部解决 + FailureReasonLegalPersonMismatch: true, + FailureReasonEsignVerificationFailed: true, // 可能是临时问题 + FailureReasonInvalidDocument: true, + + // 合同签署失败 + FailureReasonContractRejectedByUser: true, // 用户可以改变主意 + FailureReasonContractExpired: true, // 可以重新申请 + FailureReasonSignProcessFailed: true, // 可能是临时问题 + FailureReasonContractGenFailed: true, // 可能是临时问题 + FailureReasonEsignFlowError: true, // 可能是临时问题 + + // 系统错误 - 大部分可重试 + FailureReasonSystemError: true, + FailureReasonNetworkError: true, + FailureReasonTimeout: true, + FailureReasonUnknownError: false, // 未知错误,不建议自动重试 + } + + if retryable, exists := retryableReasons[reason]; exists { + return retryable + } + return false +} + +// GetRetrySuggestion 获取重试建议 +func GetRetrySuggestion(reason FailureReason) string { + if !IsRetryable(reason) { + return "此问题不建议重试,请联系客服处理" + } + + suggestions := map[FailureReason]string{ + FailureReasonEnterpriseNotExists: "请修正企业信息后重新提交", + FailureReasonEnterpriseInfoMismatch: "请核对企业信息后重新提交", + FailureReasonLegalPersonMismatch: "请确认法定代表人信息后重新提交", + FailureReasonEsignVerificationFailed: "请稍后重新尝试", + FailureReasonInvalidDocument: "请检查证件信息后重新提交", + FailureReasonContractRejectedByUser: "如需要可重新申请合同", + FailureReasonContractExpired: "请重新申请合同签署", + FailureReasonSignProcessFailed: "请重新尝试签署", + FailureReasonContractGenFailed: "请稍后重新申请", + FailureReasonEsignFlowError: "请稍后重新尝试", + FailureReasonSystemError: "请稍后重试", + FailureReasonNetworkError: "请检查网络连接后重试", + FailureReasonTimeout: "请重新尝试操作", + } + + if suggestion, exists := suggestions[reason]; exists { + return suggestion + } + return "请重新尝试操作" +} \ No newline at end of file diff --git a/internal/domains/certification/repositories/certification_command_repository.go b/internal/domains/certification/repositories/certification_command_repository.go new file mode 100644 index 0000000..4383118 --- /dev/null +++ b/internal/domains/certification/repositories/certification_command_repository.go @@ -0,0 +1,29 @@ +package repositories + +import ( + "context" + "tyapi-server/internal/domains/certification/entities" + "tyapi-server/internal/domains/certification/enums" + "tyapi-server/internal/shared/interfaces" +) + +// CertificationCommandRepository 认证命令仓储接口 +// 专门处理认证数据的变更操作,符合CQRS模式 +type CertificationCommandRepository interface { + // 基础CRUD操作 + Create(ctx context.Context, cert entities.Certification) error + Update(ctx context.Context, cert entities.Certification) error + Delete(ctx context.Context, id string) error + + // 业务特定的更新操作 + UpdateStatus(ctx context.Context, id string, status enums.CertificationStatus) error + UpdateAuthFlowID(ctx context.Context, id string, authFlowID string) error + UpdateContractInfo(ctx context.Context, id string, contractFileID, esignFlowID, contractURL, contractSignURL string) error + UpdateFailureInfo(ctx context.Context, id string, reason enums.FailureReason, message string) error + + // 批量操作 + BatchUpdateStatus(ctx context.Context, ids []string, status enums.CertificationStatus) error + + // 事务支持 + WithTx(tx interfaces.Transaction) CertificationCommandRepository +} \ No newline at end of file diff --git a/internal/domains/certification/repositories/certification_query_repository.go b/internal/domains/certification/repositories/certification_query_repository.go new file mode 100644 index 0000000..0e013c9 --- /dev/null +++ b/internal/domains/certification/repositories/certification_query_repository.go @@ -0,0 +1,122 @@ +package repositories + +import ( + "context" + "time" + + "tyapi-server/internal/domains/certification/entities" + "tyapi-server/internal/domains/certification/enums" + "tyapi-server/internal/domains/certification/repositories/queries" +) + +// CertificationQueryRepository 认证查询仓储接口 +// 专门处理认证数据的查询操作,符合CQRS模式 +type CertificationQueryRepository interface { + // 基础查询操作 + GetByID(ctx context.Context, id string) (*entities.Certification, error) + GetByUserID(ctx context.Context, userID string) (*entities.Certification, error) + Exists(ctx context.Context, id string) (bool, error) + + // 列表查询 + List(ctx context.Context, query *queries.ListCertificationsQuery) ([]*entities.Certification, int64, error) + ListByUserIDs(ctx context.Context, userIDs []string) ([]*entities.Certification, error) + ListByStatus(ctx context.Context, status enums.CertificationStatus, limit int) ([]*entities.Certification, error) + + // 业务查询 + FindByAuthFlowID(ctx context.Context, authFlowID string) (*entities.Certification, error) + FindByEsignFlowID(ctx context.Context, esignFlowID string) (*entities.Certification, error) + ListPendingRetry(ctx context.Context, maxRetryCount int) ([]*entities.Certification, error) + GetPendingCertifications(ctx context.Context) ([]*entities.Certification, error) + GetExpiredContracts(ctx context.Context) ([]*entities.Certification, error) + GetCertificationsByDateRange(ctx context.Context, startDate, endDate time.Time) ([]*entities.Certification, error) + GetUserActiveCertification(ctx context.Context, userID string) (*entities.Certification, error) + + // 统计查询 + GetStatistics(ctx context.Context, period CertificationTimePeriod) (*CertificationStatistics, error) + CountByStatus(ctx context.Context, status enums.CertificationStatus) (int64, error) + CountByFailureReason(ctx context.Context, reason enums.FailureReason) (int64, error) + GetProgressStatistics(ctx context.Context) (*CertificationProgressStats, error) + + // 搜索查询 + SearchByCompanyName(ctx context.Context, companyName string, limit int) ([]*entities.Certification, error) + SearchByLegalPerson(ctx context.Context, legalPersonName string, limit int) ([]*entities.Certification, error) + + // 缓存相关 + InvalidateCache(ctx context.Context, keys ...string) error + RefreshCache(ctx context.Context, certificationID string) error +} + +// CertificationTimePeriod 时间周期枚举 +type CertificationTimePeriod string + +const ( + PeriodDaily CertificationTimePeriod = "daily" + PeriodWeekly CertificationTimePeriod = "weekly" + PeriodMonthly CertificationTimePeriod = "monthly" + PeriodYearly CertificationTimePeriod = "yearly" +) + +// CertificationStatistics 认证统计信息 +type CertificationStatistics struct { + Period CertificationTimePeriod `json:"period"` + StartDate time.Time `json:"start_date"` + EndDate time.Time `json:"end_date"` + + // 总体统计 + TotalCertifications int64 `json:"total_certifications"` + CompletedCount int64 `json:"completed_count"` + FailedCount int64 `json:"failed_count"` + InProgressCount int64 `json:"in_progress_count"` + + // 状态分布 + StatusDistribution map[enums.CertificationStatus]int64 `json:"status_distribution"` + + // 失败原因分布 + FailureDistribution map[enums.FailureReason]int64 `json:"failure_distribution"` + + // 成功率统计 + SuccessRate float64 `json:"success_rate"` + EnterpriseVerifyRate float64 `json:"enterprise_verify_rate"` + ContractSignRate float64 `json:"contract_sign_rate"` + + // 时间统计 + AvgProcessingTime time.Duration `json:"avg_processing_time"` + AvgVerificationTime time.Duration `json:"avg_verification_time"` + AvgSigningTime time.Duration `json:"avg_signing_time"` + + // 重试统计 + RetryStats *CertificationRetryStats `json:"retry_stats"` +} + +// CertificationProgressStats 进度统计信息 +type CertificationProgressStats struct { + StatusProgress map[enums.CertificationStatus]int64 `json:"status_progress"` + ProgressDistribution map[int]int64 `json:"progress_distribution"` // key: progress percentage + + // 各阶段耗时统计 + StageTimeStats map[string]*CertificationStageTimeInfo `json:"stage_time_stats"` +} + +// CertificationStageTimeInfo 阶段耗时信息 +type CertificationStageTimeInfo struct { + StageName string `json:"stage_name"` + AverageTime time.Duration `json:"average_time"` + MinTime time.Duration `json:"min_time"` + MaxTime time.Duration `json:"max_time"` + SampleCount int64 `json:"sample_count"` +} + +// CertificationRetryStats 重试统计信息 +type CertificationRetryStats struct { + TotalRetries int64 `json:"total_retries"` + SuccessfulRetries int64 `json:"successful_retries"` + FailedRetries int64 `json:"failed_retries"` + RetrySuccessRate float64 `json:"retry_success_rate"` + + // 各阶段重试统计 + EnterpriseRetries int64 `json:"enterprise_retries"` + ContractRetries int64 `json:"contract_retries"` + + // 重试原因分布 + RetryReasonStats map[enums.FailureReason]int64 `json:"retry_reason_stats"` +} diff --git a/internal/domains/certification/repositories/certification_repository_interface.go b/internal/domains/certification/repositories/certification_repository_interface.go deleted file mode 100644 index f51f643..0000000 --- a/internal/domains/certification/repositories/certification_repository_interface.go +++ /dev/null @@ -1,91 +0,0 @@ -package repositories - -import ( - "context" - "tyapi-server/internal/domains/certification/entities" - "tyapi-server/internal/domains/certification/repositories/queries" - "tyapi-server/internal/shared/interfaces" -) - -// CertificationStats 认证统计信息 -type CertificationStats struct { - TotalCertifications int64 - PendingCertifications int64 - CompletedCertifications int64 - TodaySubmissions int64 -} - -// CertificationRepository 认证申请仓储接口 -type CertificationRepository interface { - interfaces.Repository[entities.Certification] - - // 基础查询 - 直接使用实体 - GetByUserID(ctx context.Context, userID string) (*entities.Certification, error) - GetByStatus(ctx context.Context, status string) ([]*entities.Certification, error) - GetPendingCertifications(ctx context.Context) ([]*entities.Certification, error) - GetByAuthFlowID(ctx context.Context, authFlowID string) (entities.Certification, error) - GetByEsignFlowID(ctx context.Context, esignFlowID string) (entities.Certification, error) - // 复杂查询 - 使用查询参数 - ListCertifications(ctx context.Context, query *queries.ListCertificationsQuery) ([]*entities.Certification, int64, error) - - // 业务操作 - UpdateStatus(ctx context.Context, certificationID string, status string) error - - // 统计信息 - GetStats(ctx context.Context) (*CertificationStats, error) - GetStatsByDateRange(ctx context.Context, startDate, endDate string) (*CertificationStats, error) -} - -// EnterpriseInfoSubmitRecordRepository 企业信息提交记录仓储接口 -type EnterpriseInfoSubmitRecordRepository interface { - interfaces.Repository[entities.EnterpriseInfoSubmitRecord] - - // 基础查询 - GetByUserID(ctx context.Context, userID string) ([]*entities.EnterpriseInfoSubmitRecord, error) - GetLatestByUserID(ctx context.Context, userID string) (*entities.EnterpriseInfoSubmitRecord, error) - - // 复杂查询 - ListRecords(ctx context.Context, query *queries.ListEnterpriseInfoSubmitRecordsQuery) ([]*entities.EnterpriseInfoSubmitRecord, int64, error) - - // 业务操作 - UpdateStatus(ctx context.Context, recordID string, status string, reason string) error -} - -// EsignContractGenerateRecordRepository e签宝生成合同记录仓储接口 -type EsignContractGenerateRecordRepository interface { - interfaces.Repository[entities.EsignContractGenerateRecord] - - // 基础查询 - GetByCertificationID(ctx context.Context, certificationID string) (*entities.EsignContractGenerateRecord, error) - GetByUserID(ctx context.Context, userID string) ([]*entities.EsignContractGenerateRecord, error) - GetLatestByCertificationID(ctx context.Context, certificationID string) (*entities.EsignContractGenerateRecord, error) - - // 复杂查询 - ListRecords(ctx context.Context, query *queries.ListEsignContractGenerateRecordsQuery) ([]*entities.EsignContractGenerateRecord, int64, error) - - // 业务操作 - UpdateStatus(ctx context.Context, recordID string, status string, reason string) error - UpdateSuccessInfo(ctx context.Context, recordID, esignFlowID, contractFileID, contractURL string) error - IncrementRetry(ctx context.Context, recordID string) error -} - -// EsignContractSignRecordRepository e签宝签署合同记录仓储接口 -type EsignContractSignRecordRepository interface { - interfaces.Repository[entities.EsignContractSignRecord] - - // 基础查询 - GetByCertificationID(ctx context.Context, certificationID string) (*entities.EsignContractSignRecord, error) - GetByUserID(ctx context.Context, userID string) ([]*entities.EsignContractSignRecord, error) - GetLatestByCertificationID(ctx context.Context, certificationID string) (*entities.EsignContractSignRecord, error) - GetByGenerateRecordID(ctx context.Context, generateRecordID string) (*entities.EsignContractSignRecord, error) - - // 复杂查询 - ListRecords(ctx context.Context, query *queries.ListEsignContractSignRecordsQuery) ([]*entities.EsignContractSignRecord, int64, error) - - // 业务操作 - UpdateStatus(ctx context.Context, recordID string, status string, reason string) error - UpdateSuccessInfo(ctx context.Context, recordID, signedFileURL string) error - SetSignURL(ctx context.Context, recordID, signURL string) error - IncrementRetry(ctx context.Context, recordID string) error - MarkExpiredRecords(ctx context.Context) error -} diff --git a/internal/domains/certification/repositories/queries/certification_queries.go b/internal/domains/certification/repositories/queries/certification_queries.go index b610c7c..33bde18 100644 --- a/internal/domains/certification/repositories/queries/certification_queries.go +++ b/internal/domains/certification/repositories/queries/certification_queries.go @@ -1,62 +1,279 @@ package queries -import "tyapi-server/internal/domains/certification/enums" +import ( + "fmt" + "time" -// ListCertificationsQuery 认证申请列表查询参数 + "tyapi-server/internal/domains/certification/enums" +) + +// GetCertificationQuery 获取单个认证查询 +type GetCertificationQuery struct { + ID string `json:"id" validate:"required"` + UserID string `json:"user_id,omitempty"` // 可选的用户ID,用于权限验证 +} + +// ListCertificationsQuery 认证列表查询 type ListCertificationsQuery struct { - Page int `json:"page"` - PageSize int `json:"page_size"` - UserID string `json:"user_id"` - Status enums.CertificationStatus `json:"status"` - StartDate string `json:"start_date"` - EndDate string `json:"end_date"` - EnterpriseName string `json:"enterprise_name"` + // 分页参数 + Page int `json:"page" validate:"min=1"` + PageSize int `json:"page_size" validate:"min=1,max=100"` + + // 排序参数 + SortBy string `json:"sort_by"` // 排序字段: created_at, updated_at, status, progress + SortOrder string `json:"sort_order"` // 排序方向: asc, desc + + // 过滤条件 + UserID string `json:"user_id,omitempty"` + Status enums.CertificationStatus `json:"status,omitempty"` + Statuses []enums.CertificationStatus `json:"statuses,omitempty"` + FailureReason enums.FailureReason `json:"failure_reason,omitempty"` + + // 时间范围过滤 + CreatedAfter *time.Time `json:"created_after,omitempty"` + CreatedBefore *time.Time `json:"created_before,omitempty"` + UpdatedAfter *time.Time `json:"updated_after,omitempty"` + UpdatedBefore *time.Time `json:"updated_before,omitempty"` + + // 企业信息过滤 + CompanyName string `json:"company_name,omitempty"` + LegalPersonName string `json:"legal_person_name,omitempty"` + + // 业务状态过滤 + IsCompleted *bool `json:"is_completed,omitempty"` + IsFailed *bool `json:"is_failed,omitempty"` + IsUserActionRequired *bool `json:"is_user_action_required,omitempty"` + + // 高级过滤 + MinRetryCount *int `json:"min_retry_count,omitempty"` + MaxRetryCount *int `json:"max_retry_count,omitempty"` + MinProgress *int `json:"min_progress,omitempty"` + MaxProgress *int `json:"max_progress,omitempty"` + + // 搜索参数 + SearchKeyword string `json:"search_keyword,omitempty"` // 通用搜索关键词 + + // 包含关联数据 + IncludeMetadata bool `json:"include_metadata,omitempty"` } -// ListEnterprisesQuery 企业信息列表查询参数 -type ListEnterprisesQuery struct { - Page int `json:"page"` - PageSize int `json:"page_size"` - UserID string `json:"user_id"` - EnterpriseName string `json:"enterprise_name"` - LicenseNumber string `json:"license_number"` - LegalPersonName string `json:"legal_person_name"` - StartDate string `json:"start_date"` - EndDate string `json:"end_date"` +// DefaultValues 设置默认值 +func (q *ListCertificationsQuery) DefaultValues() { + if q.Page <= 0 { + q.Page = 1 + } + if q.PageSize <= 0 { + q.PageSize = 20 + } + if q.SortBy == "" { + q.SortBy = "created_at" + } + if q.SortOrder == "" { + q.SortOrder = "desc" + } } -// ListEnterpriseInfoSubmitRecordsQuery 企业信息提交记录列表查询参数 -type ListEnterpriseInfoSubmitRecordsQuery struct { - Page int `json:"page"` - PageSize int `json:"page_size"` - CertificationID string `json:"certification_id"` - UserID string `json:"user_id"` - Status string `json:"status"` - CompanyName string `json:"company_name"` - StartDate string `json:"start_date"` - EndDate string `json:"end_date"` +// GetOffset 计算分页偏移量 +func (q *ListCertificationsQuery) GetOffset() int { + return (q.Page - 1) * q.PageSize } -// ListEsignContractGenerateRecordsQuery e签宝生成合同记录列表查询参数 -type ListEsignContractGenerateRecordsQuery struct { - Page int `json:"page"` - PageSize int `json:"page_size"` - CertificationID string `json:"certification_id"` - UserID string `json:"user_id"` - Status string `json:"status"` - ContractType string `json:"contract_type"` - StartDate string `json:"start_date"` - EndDate string `json:"end_date"` +// GetLimit 获取查询限制数量 +func (q *ListCertificationsQuery) GetLimit() int { + return q.PageSize } -// ListEsignContractSignRecordsQuery e签宝签署合同记录列表查询参数 -type ListEsignContractSignRecordsQuery struct { - Page int `json:"page"` - PageSize int `json:"page_size"` - CertificationID string `json:"certification_id"` - UserID string `json:"user_id"` - Status string `json:"status"` - SignerName string `json:"signer_name"` - StartDate string `json:"start_date"` - EndDate string `json:"end_date"` +// HasTimeFilter 检查是否有时间过滤条件 +func (q *ListCertificationsQuery) HasTimeFilter() bool { + return q.CreatedAfter != nil || q.CreatedBefore != nil || + q.UpdatedAfter != nil || q.UpdatedBefore != nil +} + +// HasStatusFilter 检查是否有状态过滤条件 +func (q *ListCertificationsQuery) HasStatusFilter() bool { + return q.Status != "" || len(q.Statuses) > 0 +} + +// HasSearchFilter 检查是否有搜索过滤条件 +func (q *ListCertificationsQuery) HasSearchFilter() bool { + return q.CompanyName != "" || q.LegalPersonName != "" || q.SearchKeyword != "" +} + +// GetSearchFields 获取搜索字段映射 +func (q *ListCertificationsQuery) GetSearchFields() map[string]string { + fields := make(map[string]string) + + if q.CompanyName != "" { + fields["company_name"] = q.CompanyName + } + if q.LegalPersonName != "" { + fields["legal_person_name"] = q.LegalPersonName + } + if q.SearchKeyword != "" { + fields["keyword"] = q.SearchKeyword + } + + return fields +} + +// CertificationStatisticsQuery 认证统计查询 +type CertificationStatisticsQuery struct { + // 时间范围 + StartDate time.Time `json:"start_date" validate:"required"` + EndDate time.Time `json:"end_date" validate:"required"` + + // 统计周期 + Period string `json:"period" validate:"oneof=daily weekly monthly yearly"` + + // 分组维度 + GroupBy []string `json:"group_by,omitempty"` // status, failure_reason, user_type, date + + // 过滤条件 + UserIDs []string `json:"user_ids,omitempty"` + Statuses []enums.CertificationStatus `json:"statuses,omitempty"` + + // 统计类型 + IncludeProgressStats bool `json:"include_progress_stats,omitempty"` + IncludeRetryStats bool `json:"include_retry_stats,omitempty"` + IncludeTimeStats bool `json:"include_time_stats,omitempty"` +} + +// Validate 验证统计查询参数 +func (q *CertificationStatisticsQuery) Validate() error { + if q.EndDate.Before(q.StartDate) { + return fmt.Errorf("结束时间不能早于开始时间") + } + + // 检查时间范围是否合理(不超过1年) + if q.EndDate.Sub(q.StartDate) > 365*24*time.Hour { + return fmt.Errorf("查询时间范围不能超过1年") + } + + return nil +} + +// GetTimeRange 获取时间范围描述 +func (q *CertificationStatisticsQuery) GetTimeRange() string { + return fmt.Sprintf("%s 到 %s", + q.StartDate.Format("2006-01-02"), + q.EndDate.Format("2006-01-02")) +} + +// SearchCertificationsQuery 搜索认证查询 +type SearchCertificationsQuery struct { + // 搜索关键词 + Keyword string `json:"keyword" validate:"required,min=2"` + + // 搜索字段 + SearchFields []string `json:"search_fields,omitempty"` // company_name, legal_person_name, unified_social_code + + // 过滤条件 + Statuses []enums.CertificationStatus `json:"statuses,omitempty"` + UserID string `json:"user_id,omitempty"` + + // 分页参数 + Page int `json:"page" validate:"min=1"` + PageSize int `json:"page_size" validate:"min=1,max=50"` + + // 排序参数 + SortBy string `json:"sort_by"` + SortOrder string `json:"sort_order"` + + // 搜索选项 + ExactMatch bool `json:"exact_match,omitempty"` // 是否精确匹配 + IgnoreCase bool `json:"ignore_case,omitempty"` // 是否忽略大小写 +} + +// DefaultValues 设置搜索查询默认值 +func (q *SearchCertificationsQuery) DefaultValues() { + if q.Page <= 0 { + q.Page = 1 + } + if q.PageSize <= 0 { + q.PageSize = 10 + } + if q.SortBy == "" { + q.SortBy = "created_at" + } + if q.SortOrder == "" { + q.SortOrder = "desc" + } + if len(q.SearchFields) == 0 { + q.SearchFields = []string{"company_name", "legal_person_name"} + } + + // 默认忽略大小写 + q.IgnoreCase = true +} + +// GetLimit 获取查询限制数量 +func (q *SearchCertificationsQuery) GetLimit() int { + return q.PageSize +} + +// GetSearchPattern 获取搜索模式 +func (q *SearchCertificationsQuery) GetSearchPattern() string { + if q.ExactMatch { + return q.Keyword + } + + // 模糊搜索,添加通配符 + return "%" + q.Keyword + "%" +} + +// UserCertificationsQuery 用户认证查询 +type UserCertificationsQuery struct { + UserID string `json:"user_id" validate:"required"` + + // 状态过滤 + Status enums.CertificationStatus `json:"status,omitempty"` + IncludeCompleted bool `json:"include_completed,omitempty"` + IncludeFailed bool `json:"include_failed,omitempty"` + + // 时间过滤 + After *time.Time `json:"after,omitempty"` + Before *time.Time `json:"before,omitempty"` + + // 分页 + Page int `json:"page"` + PageSize int `json:"page_size"` + + // 排序 + SortBy string `json:"sort_by"` + SortOrder string `json:"sort_order"` +} + +// DefaultValues 设置用户认证查询默认值 +func (q *UserCertificationsQuery) DefaultValues() { + if q.Page <= 0 { + q.Page = 1 + } + if q.PageSize <= 0 { + q.PageSize = 10 + } + if q.SortBy == "" { + q.SortBy = "created_at" + } + if q.SortOrder == "" { + q.SortOrder = "desc" + } +} + +// ShouldIncludeStatus 检查是否应该包含指定状态 +func (q *UserCertificationsQuery) ShouldIncludeStatus(status enums.CertificationStatus) bool { + // 如果指定了特定状态,只返回该状态 + if q.Status != "" { + return status == q.Status + } + + // 根据包含选项决定 + if enums.IsFinalStatus(status) && !q.IncludeCompleted { + return false + } + + if enums.IsFailureStatus(status) && !q.IncludeFailed { + return false + } + + return true } diff --git a/internal/domains/certification/services/certification_aggregate_service.go b/internal/domains/certification/services/certification_aggregate_service.go new file mode 100644 index 0000000..943b079 --- /dev/null +++ b/internal/domains/certification/services/certification_aggregate_service.go @@ -0,0 +1,384 @@ +package services + +import ( + "context" + "fmt" + + "tyapi-server/internal/domains/certification/entities" + "tyapi-server/internal/domains/certification/enums" + "tyapi-server/internal/domains/certification/repositories" + "tyapi-server/internal/domains/certification/services/state_machine" + + "go.uber.org/zap" +) + +// CertificationAggregateService 认证聚合服务接口 +// 负责认证聚合根的生命周期管理和状态转换协调 +type CertificationAggregateService interface { + // 聚合根管理 + CreateCertification(ctx context.Context, userID string) (*entities.Certification, error) + LoadCertification(ctx context.Context, certificationID string) (*entities.Certification, error) + SaveCertification(ctx context.Context, cert *entities.Certification) error + + // 状态转换管理 + TransitionState(ctx context.Context, certificationID string, targetStatus enums.CertificationStatus, actor enums.ActorType, actorID string, reason string, metadata map[string]interface{}) (*state_machine.StateTransitionResult, error) + ValidateStateTransition(ctx context.Context, certificationID string, targetStatus enums.CertificationStatus, actor enums.ActorType) error + + // 业务规则验证 + ValidateBusinessRules(ctx context.Context, cert *entities.Certification) error + CheckInvariance(ctx context.Context, cert *entities.Certification) error + + // 查询方法 + GetStateInfo(status enums.CertificationStatus) *state_machine.StateConfig + GetValidTransitions(ctx context.Context, certificationID string, actor enums.ActorType) ([]*state_machine.StateTransitionRule, error) +} + +// CertificationAggregateServiceImpl 认证聚合服务实现 +type CertificationAggregateServiceImpl struct { + commandRepo repositories.CertificationCommandRepository + queryRepo repositories.CertificationQueryRepository + stateMachine *state_machine.CertificationStateMachine + logger *zap.Logger +} + +// NewCertificationAggregateService 创建认证聚合服务 +func NewCertificationAggregateService( + commandRepo repositories.CertificationCommandRepository, + queryRepo repositories.CertificationQueryRepository, + stateMachine *state_machine.CertificationStateMachine, + logger *zap.Logger, +) CertificationAggregateService { + return &CertificationAggregateServiceImpl{ + commandRepo: commandRepo, + queryRepo: queryRepo, + stateMachine: stateMachine, + logger: logger, + } +} + +// ================ 聚合根管理 ================ + +// CreateCertification 创建认证申请 +func (s *CertificationAggregateServiceImpl) CreateCertification(ctx context.Context, userID string) (*entities.Certification, error) { + s.logger.Info("创建认证申请", zap.String("user_id", userID)) + + // 1. 检查用户是否已有认证申请 + existingCert, err := s.queryRepo.GetByUserID(ctx, userID) + if err == nil && existingCert != nil { + // 检查现有认证的状态 + if !existingCert.IsFinalStatus() { + return nil, fmt.Errorf("用户已有进行中的认证申请,请先完成或取消现有申请") + } + + s.logger.Info("用户已有完成的认证申请,允许创建新申请", + zap.String("user_id", userID), + zap.String("existing_cert_id", existingCert.ID), + zap.String("existing_status", string(existingCert.Status))) + } + + // 2. 创建新的认证聚合根 + cert, err := entities.NewCertification(userID) + if err != nil { + s.logger.Error("创建认证实体失败", zap.Error(err), zap.String("user_id", userID)) + return nil, fmt.Errorf("创建认证实体失败: %w", err) + } + + // 3. 验证业务规则 + if err := s.ValidateBusinessRules(ctx, cert); err != nil { + s.logger.Error("认证业务规则验证失败", zap.Error(err)) + return nil, fmt.Errorf("业务规则验证失败: %w", err) + } + + // 4. 保存聚合根 + if err := s.SaveCertification(ctx, cert); err != nil { + s.logger.Error("保存认证申请失败", zap.Error(err)) + return nil, fmt.Errorf("保存认证申请失败: %w", err) + } + + s.logger.Info("认证申请创建成功", + zap.String("user_id", userID), + zap.String("certification_id", cert.ID)) + + return cert, nil +} + +// LoadCertification 加载认证聚合根 +func (s *CertificationAggregateServiceImpl) LoadCertification(ctx context.Context, certificationID string) (*entities.Certification, error) { + s.logger.Debug("加载认证聚合根", zap.String("certification_id", certificationID)) + + // 从查询仓储加载 + cert, err := s.queryRepo.GetByID(ctx, certificationID) + if err != nil { + s.logger.Error("加载认证聚合根失败", zap.Error(err), zap.String("certification_id", certificationID)) + return nil, fmt.Errorf("认证申请不存在: %w", err) + } + + // 验证聚合根完整性 + if err := s.CheckInvariance(ctx, cert); err != nil { + s.logger.Error("认证聚合根完整性验证失败", zap.Error(err)) + return nil, fmt.Errorf("认证数据完整性验证失败: %w", err) + } + + return cert, nil +} + +// SaveCertification 保存认证聚合根 +func (s *CertificationAggregateServiceImpl) SaveCertification(ctx context.Context, cert *entities.Certification) error { + s.logger.Debug("保存认证聚合根", zap.String("certification_id", cert.ID)) + + // 1. 验证业务规则 + if err := s.ValidateBusinessRules(ctx, cert); err != nil { + return fmt.Errorf("业务规则验证失败: %w", err) + } + + // 2. 检查聚合根是否存在 + exists, err := s.queryRepo.Exists(ctx, cert.ID) + if err != nil { + return fmt.Errorf("检查认证存在性失败: %w", err) + } + + // 3. 保存到命令仓储 + if exists { + err = s.commandRepo.Update(ctx, *cert) + if err != nil { + s.logger.Error("更新认证聚合根失败", zap.Error(err)) + return fmt.Errorf("更新认证失败: %w", err) + } + } else { + err = s.commandRepo.Create(ctx, *cert) + if err != nil { + s.logger.Error("创建认证聚合根失败", zap.Error(err)) + return fmt.Errorf("创建认证失败: %w", err) + } + } + + s.logger.Debug("认证聚合根保存成功", zap.String("certification_id", cert.ID)) + return nil +} + +// ================ 状态转换管理 ================ + +// TransitionState 执行状态转换 +func (s *CertificationAggregateServiceImpl) TransitionState( + ctx context.Context, + certificationID string, + targetStatus enums.CertificationStatus, + actor enums.ActorType, + actorID string, + reason string, + metadata map[string]interface{}, +) (*state_machine.StateTransitionResult, error) { + s.logger.Info("执行状态转换", + zap.String("certification_id", certificationID), + zap.String("target_status", string(targetStatus)), + zap.String("actor", string(actor)), + zap.String("actor_id", actorID)) + + // 构建状态转换请求 + req := &state_machine.StateTransitionRequest{ + CertificationID: certificationID, + TargetStatus: targetStatus, + Actor: actor, + ActorID: actorID, + Reason: reason, + Context: metadata, + AllowRollback: true, + } + + // 执行状态转换 + result, err := s.stateMachine.ExecuteTransition(ctx, req) + if err != nil { + s.logger.Error("状态转换执行失败", + zap.String("certification_id", certificationID), + zap.Error(err)) + return result, err + } + + s.logger.Info("状态转换执行成功", + zap.String("certification_id", certificationID), + zap.String("from_status", string(result.OldStatus)), + zap.String("to_status", string(result.NewStatus))) + + return result, nil +} + +// ValidateStateTransition 验证状态转换 +func (s *CertificationAggregateServiceImpl) ValidateStateTransition( + ctx context.Context, + certificationID string, + targetStatus enums.CertificationStatus, + actor enums.ActorType, +) error { + // 加载认证聚合根 + cert, err := s.LoadCertification(ctx, certificationID) + if err != nil { + return err + } + + // 检查是否可以转换 + canTransition, message := s.stateMachine.CanTransition(cert, targetStatus, actor) + if !canTransition { + return fmt.Errorf("状态转换验证失败: %s", message) + } + + return nil +} + +// ================ 业务规则验证 ================ + +// ValidateBusinessRules 验证业务规则 +func (s *CertificationAggregateServiceImpl) ValidateBusinessRules(ctx context.Context, cert *entities.Certification) error { + s.logger.Debug("验证认证业务规则", zap.String("certification_id", cert.ID)) + + // 1. 实体内部业务规则验证 + if err := cert.ValidateBusinessRules(); err != nil { + return fmt.Errorf("实体业务规则验证失败: %w", err) + } + + // 2. 跨聚合根业务规则验证 + if err := s.validateCrossAggregateRules(ctx, cert); err != nil { + return fmt.Errorf("跨聚合根业务规则验证失败: %w", err) + } + + // 3. 领域级业务规则验证 + if err := s.validateDomainRules(ctx, cert); err != nil { + return fmt.Errorf("领域业务规则验证失败: %w", err) + } + + return nil +} + +// CheckInvariance 检查聚合根不变量 +func (s *CertificationAggregateServiceImpl) CheckInvariance(ctx context.Context, cert *entities.Certification) error { + s.logger.Debug("检查认证聚合根不变量", zap.String("certification_id", cert.ID)) + + // 1. 基础不变量检查 + if cert.ID == "" { + return fmt.Errorf("认证ID不能为空") + } + + if cert.UserID == "" { + return fmt.Errorf("用户ID不能为空") + } + + if !enums.IsValidStatus(cert.Status) { + return fmt.Errorf("无效的认证状态: %s", cert.Status) + } + + // 2. 状态相关不变量检查 + if err := s.validateStatusInvariance(cert); err != nil { + return err + } + + // 3. 时间戳不变量检查 + if err := s.validateTimestampInvariance(cert); err != nil { + return err + } + + return nil +} + +// ================ 查询方法 ================ + +// GetStateInfo 获取状态信息 +func (s *CertificationAggregateServiceImpl) GetStateInfo(status enums.CertificationStatus) *state_machine.StateConfig { + return s.stateMachine.GetStateInfo(status) +} + +// GetValidTransitions 获取有效的状态转换 +func (s *CertificationAggregateServiceImpl) GetValidTransitions( + ctx context.Context, + certificationID string, + actor enums.ActorType, +) ([]*state_machine.StateTransitionRule, error) { + // 加载认证聚合根 + cert, err := s.LoadCertification(ctx, certificationID) + if err != nil { + return nil, err + } + + // 获取有效转换 + transitions := s.stateMachine.GetValidTransitions(cert, actor) + return transitions, nil +} + +// ================ 私有方法 ================ + +// validateCrossAggregateRules 验证跨聚合根业务规则 +func (s *CertificationAggregateServiceImpl) validateCrossAggregateRules(ctx context.Context, cert *entities.Certification) error { + // TODO: 实现跨聚合根业务规则验证 + // 例如:检查用户是否有权限申请认证、检查企业信息是否已被其他用户使用等 + return nil +} + +// validateDomainRules 验证领域级业务规则 +func (s *CertificationAggregateServiceImpl) validateDomainRules(ctx context.Context, cert *entities.Certification) error { + // TODO: 实现领域级业务规则验证 + // 例如:检查认证流程是否符合法规要求、检查时间窗口限制等 + return nil +} + +// validateStatusInvariance 验证状态相关不变量 +func (s *CertificationAggregateServiceImpl) validateStatusInvariance(cert *entities.Certification) error { + switch cert.Status { + case enums.StatusEnterpriseVerified: + if cert.AuthFlowID == "" { + return fmt.Errorf("企业认证状态下必须有认证流程ID") + } + if cert.EnterpriseVerifiedAt == nil { + return fmt.Errorf("企业认证状态下必须有认证完成时间") + } + + case enums.StatusContractApplied: + if cert.AuthFlowID == "" { + return fmt.Errorf("合同申请状态下必须有企业认证流程ID") + } + if cert.ContractAppliedAt == nil { + return fmt.Errorf("合同申请状态下必须有合同申请时间") + } + + case enums.StatusContractSigned: + if cert.ContractFileID == "" || cert.EsignFlowID == "" { + return fmt.Errorf("合同签署状态下必须有完整的合同信息") + } + if cert.ContractSignedAt == nil { + return fmt.Errorf("合同签署状态下必须有签署完成时间") + } + } + + // 失败状态检查 + if enums.IsFailureStatus(cert.Status) { + if cert.FailureReason == "" { + return fmt.Errorf("失败状态下必须有失败原因") + } + if !enums.IsValidFailureReason(cert.FailureReason) { + return fmt.Errorf("无效的失败原因: %s", cert.FailureReason) + } + } + + return nil +} + +// validateTimestampInvariance 验证时间戳不变量 +func (s *CertificationAggregateServiceImpl) validateTimestampInvariance(cert *entities.Certification) error { + // 检查时间戳的逻辑顺序 + if cert.InfoSubmittedAt != nil && cert.EnterpriseVerifiedAt != nil { + if cert.InfoSubmittedAt.After(*cert.EnterpriseVerifiedAt) { + return fmt.Errorf("企业信息提交时间不能晚于企业认证时间") + } + } + + if cert.EnterpriseVerifiedAt != nil && cert.ContractAppliedAt != nil { + if cert.EnterpriseVerifiedAt.After(*cert.ContractAppliedAt) { + return fmt.Errorf("企业认证时间不能晚于合同申请时间") + } + } + + if cert.ContractAppliedAt != nil && cert.ContractSignedAt != nil { + if cert.ContractAppliedAt.After(*cert.ContractSignedAt) { + return fmt.Errorf("合同申请时间不能晚于合同签署时间") + } + } + + return nil +} diff --git a/internal/domains/certification/services/certification_esign_service.go b/internal/domains/certification/services/certification_esign_service.go deleted file mode 100644 index 0f3f148..0000000 --- a/internal/domains/certification/services/certification_esign_service.go +++ /dev/null @@ -1,126 +0,0 @@ -package services - -import ( - "context" - "fmt" - - "go.uber.org/zap" - - cert_entities "tyapi-server/internal/domains/certification/entities" - "tyapi-server/internal/domains/certification/repositories" - user_entities "tyapi-server/internal/domains/user/entities" - "tyapi-server/internal/shared/esign" -) - -// CertificationEsignService 负责与e签宝相关的认证业务逻辑 -type CertificationEsignService struct { - certRepo repositories.CertificationRepository - esignClient *esign.Client - esignContractGenerateRecordRepo repositories.EsignContractGenerateRecordRepository - esignContractSignRecordRepo repositories.EsignContractSignRecordRepository - logger *zap.Logger -} - -// NewCertificationEsignService 创建CertificationEsignService实例 -func NewCertificationEsignService( - certRepo repositories.CertificationRepository, - esignClient *esign.Client, - esignContractGenerateRecordRepo repositories.EsignContractGenerateRecordRepository, - logger *zap.Logger, -) *CertificationEsignService { - return &CertificationEsignService{ - certRepo: certRepo, - esignClient: esignClient, - esignContractGenerateRecordRepo: esignContractGenerateRecordRepo, - logger: logger, - } -} - -// FillTemplate 生成合同文件(e签宝模板填充) -func (s *CertificationEsignService) FillTemplate(ctx context.Context, certification *cert_entities.Certification, components map[string]string) (*esign.FillTemplate, error) { - resp, err := s.esignClient.FillTemplate(components) - record := &cert_entities.EsignContractGenerateRecord{ - CertificationID: certification.ID, - UserID: certification.UserID, - } - if err != nil { - s.logger.Error("生成合同文件失败", zap.Any("components", components), zap.Error(err)) - record.Status = "failed" - } else { - record.TemplateID = resp.TemplateID - record.ContractName = resp.FileName - record.ContractFileID = resp.FileID - record.ContractURL = resp.FileDownloadUrl - record.Status = "success" - record.FillTime = &resp.FillTime - } - if _, createErr := s.esignContractGenerateRecordRepo.Create(ctx, *record); createErr != nil { - s.logger.Error("创建合同生成记录失败", zap.Error(createErr)) - if err == nil { - return nil, fmt.Errorf("创建合同生成记录失败: %w", createErr) - } - } - if err != nil { - return nil, fmt.Errorf("生成合同文件失败: %w", err) - } - - certification.ContractURL = resp.FileDownloadUrl - certification.ContractFileID = resp.FileID - err = s.certRepo.Update(ctx, *certification) - if err != nil { - s.logger.Error("更新认证申请失败", zap.Error(err)) - return nil, fmt.Errorf("更新认证申请失败: %w", err) - } - s.logger.Info("生成合同文件成功", zap.String("template_id", resp.TemplateID), zap.String("file_id", resp.FileID)) - return resp, nil -} - -// 发起签署 -func (s *CertificationEsignService) InitiateSign(ctx context.Context, certification *cert_entities.Certification, enterpriseInfo *user_entities.EnterpriseInfo) (*cert_entities.EsignContractSignRecord, error) { - - // 发起签署流程 - flowID, err := s.esignClient.CreateSignFlow(&esign.CreateSignFlowRequest{ - FileID: certification.ContractFileID, - SignerAccount: enterpriseInfo.UnifiedSocialCode, - SignerName: enterpriseInfo.CompanyName, - TransactorPhone: enterpriseInfo.LegalPersonPhone, - TransactorName: enterpriseInfo.LegalPersonName, - TransactorIDCardNum: enterpriseInfo.LegalPersonID, - }) - if err != nil { - s.logger.Error("获取签署链接失败", - zap.String("user_id", enterpriseInfo.UserID), - zap.Error(err), - ) - return nil, fmt.Errorf("获取签署链接失败: %w", err) - } - signURL, shortURL, err := s.esignClient.GetSignURL(flowID, enterpriseInfo.UnifiedSocialCode, enterpriseInfo.CompanyName) - if err != nil { - s.logger.Error("获取签署链接失败", zap.Error(err)) - return nil, fmt.Errorf("获取签署链接失败: %w", err) - } - esignContractSignRecord := cert_entities.NewEsignContractSignRecord( - certification.ID, - enterpriseInfo.UserID, - flowID, - certification.ContractFileID, - enterpriseInfo.UnifiedSocialCode, - enterpriseInfo.LegalPersonPhone, - enterpriseInfo.LegalPersonID, - signURL, - shortURL, - ) - signContractSignRecord, err := s.esignContractSignRecordRepo.Create(ctx, *esignContractSignRecord) - if err != nil { - s.logger.Error("创建签署记录失败", zap.Error(err)) - return nil, fmt.Errorf("创建签署记录失败: %w", err) - } - certification.EsignFlowID = signContractSignRecord.EsignFlowID - certification.ContractSignURL = signContractSignRecord.SignShortURL // 记录的是短链接 - err = s.certRepo.Update(ctx, *certification) - if err != nil { - s.logger.Error("更新认证申请失败", zap.Error(err)) - return nil, fmt.Errorf("更新认证申请失败: %w", err) - } - return &signContractSignRecord, nil -} diff --git a/internal/domains/certification/services/certification_management_service.go b/internal/domains/certification/services/certification_management_service.go deleted file mode 100644 index ae28870..0000000 --- a/internal/domains/certification/services/certification_management_service.go +++ /dev/null @@ -1,153 +0,0 @@ -package services - -import ( - "context" - "fmt" - - "tyapi-server/internal/domains/certification/entities" - "tyapi-server/internal/domains/certification/enums" - "tyapi-server/internal/domains/certification/repositories" - esign_service "tyapi-server/internal/shared/esign" - - "go.uber.org/zap" -) - -// CertificationManagementService 认证管理领域服务 -// 负责认证申请的生命周期管理,包括创建、状态转换、进度查询等 -type CertificationManagementService struct { - certRepo repositories.CertificationRepository - esignService *esign_service.Client - stateMachine *CertificationStateMachine - logger *zap.Logger -} - -// NewCertificationManagementService 创建认证管理领域服务 -func NewCertificationManagementService( - certRepo repositories.CertificationRepository, - esignService *esign_service.Client, - stateMachine *CertificationStateMachine, - logger *zap.Logger, -) *CertificationManagementService { - return &CertificationManagementService{ - certRepo: certRepo, - esignService: esignService, - stateMachine: stateMachine, - logger: logger, - } -} - -// CreateCertification 创建认证申请 -func (s *CertificationManagementService) CreateCertification(ctx context.Context, userID string) (*entities.Certification, error) { - // 检查用户是否已有认证申请 - existingCert, err := s.certRepo.GetByUserID(ctx, userID) - if err == nil && existingCert != nil { - return nil, fmt.Errorf("用户已有认证申请") - } - - certification := &entities.Certification{ - UserID: userID, - Status: enums.StatusPending, - } - - createdCert, err := s.certRepo.Create(ctx, *certification) - if err != nil { - s.logger.Error("创建认证申请失败", zap.Error(err)) - return nil, fmt.Errorf("创建认证申请失败: %w", err) - } - certification = &createdCert - - s.logger.Info("认证申请创建成功", - zap.String("certification_id", certification.ID), - zap.String("user_id", userID), - ) - - return certification, nil -} - -// GetCertificationByUserID 根据用户ID获取认证申请 -func (s *CertificationManagementService) GetCertificationByUserID(ctx context.Context, userID string) (*entities.Certification, error) { - return s.certRepo.GetByUserID(ctx, userID) -} - -// GetCertificationByID 根据ID获取认证申请 -func (s *CertificationManagementService) GetCertificationByID(ctx context.Context, certificationID string) (*entities.Certification, error) { - cert, err := s.certRepo.GetByID(ctx, certificationID) - if err != nil { - return nil, err - } - return &cert, nil -} - -// GetCertificationByAuthFlowID 根据认证流程ID获取认证申请 -func (s *CertificationManagementService) GetCertificationByAuthFlowID(ctx context.Context, authFlowID string) (*entities.Certification, error) { - cert, err := s.certRepo.GetByAuthFlowID(ctx, authFlowID) - if err != nil { - return nil, err - } - return &cert, nil -} - -// 根据签署流程ID获取认证申请 -func (s *CertificationManagementService) GetCertificationByEsignFlowID(ctx context.Context, esignFlowID string) (*entities.Certification, error) { - cert, err := s.certRepo.GetByEsignFlowID(ctx, esignFlowID) - if err != nil { - return nil, err - } - return &cert, nil -} -// GetCertificationProgress 获取认证进度信息 -func (s *CertificationManagementService) GetCertificationProgress(ctx context.Context, certificationID string) (map[string]interface{}, error) { - cert, err := s.certRepo.GetByID(ctx, certificationID) - if err != nil { - return nil, fmt.Errorf("认证申请不存在: %w", err) - } - - progress := map[string]interface{}{ - "certification_id": cert.ID, - "user_id": cert.UserID, - "current_status": cert.Status, - "progress_percentage": cert.GetProgressPercentage(), - "is_user_action_required": cert.IsUserActionRequired(), - "next_valid_statuses": cert.GetNextValidStatuses(), - "created_at": cert.CreatedAt, - "updated_at": cert.UpdatedAt, - } - - // 添加时间节点信息 - if cert.InfoSubmittedAt != nil { - progress["info_submitted_at"] = cert.InfoSubmittedAt - } - if cert.EnterpriseVerifiedAt != nil { - progress["enterprise_verified_at"] = cert.EnterpriseVerifiedAt - } - if cert.ContractAppliedAt != nil { - progress["contract_applied_at"] = cert.ContractAppliedAt - } - if cert.ContractSignedAt != nil { - progress["contract_signed_at"] = cert.ContractSignedAt - } - if cert.CompletedAt != nil { - progress["completed_at"] = cert.CompletedAt - } - - return progress, nil -} - -// 通过e签宝检查是否有过认证 -func (s *CertificationManagementService) CheckCertification(ctx context.Context, companyName string, unifiedSocialCode string) (bool, error) { - // 查询企业是否已经过认证 - queryOrgIdentityInfo := &esign_service.QueryOrgIdentityRequest{ - OrgName: companyName, - OrgIDCardNum: unifiedSocialCode, - OrgIDCardType: esign_service.OrgIDCardTypeUSCC, - } - queryOrgIdentityResponse, err := s.esignService.QueryOrgIdentityInfo(queryOrgIdentityInfo) - if err != nil { - return false, fmt.Errorf("查询机构认证信息失败: %w", err) - } - if queryOrgIdentityResponse.Data.RealnameStatus == 1 { - s.logger.Info("该企业已进行过认证", zap.String("company_name", companyName), zap.String("unified_social_code", unifiedSocialCode)) - return true, nil - } - return false, nil -} diff --git a/internal/domains/certification/services/certification_workflow_orchestrator.go b/internal/domains/certification/services/certification_workflow_orchestrator.go new file mode 100644 index 0000000..f75cfb7 --- /dev/null +++ b/internal/domains/certification/services/certification_workflow_orchestrator.go @@ -0,0 +1,636 @@ +package services + +import ( + "context" + "fmt" + "time" + + "tyapi-server/internal/domains/certification/entities" + "tyapi-server/internal/domains/certification/entities/value_objects" + "tyapi-server/internal/domains/certification/enums" + "tyapi-server/internal/domains/certification/services/state_machine" + + "go.uber.org/zap" +) + +// WorkflowResult 工作流执行结果 +type WorkflowResult struct { + Success bool `json:"success"` + CertificationID string `json:"certification_id"` + CurrentStatus enums.CertificationStatus `json:"current_status"` + Message string `json:"message"` + Data map[string]interface{} `json:"data,omitempty"` + StateTransition *state_machine.StateTransitionResult `json:"state_transition,omitempty"` + ExecutedAt time.Time `json:"executed_at"` +} + +// SubmitEnterpriseInfoCommand 提交企业信息命令 +type SubmitEnterpriseInfoCommand struct { + CertificationID string `json:"certification_id"` + UserID string `json:"user_id"` + EnterpriseInfo *value_objects.EnterpriseInfo `json:"enterprise_info"` +} + +// ApplyContractCommand 申请合同命令 +type ApplyContractCommand struct { + CertificationID string `json:"certification_id"` + UserID string `json:"user_id"` +} + +// EsignCallbackCommand e签宝回调命令 +type EsignCallbackCommand struct { + CertificationID string `json:"certification_id"` + CallbackType string `json:"callback_type"` // "auth_result" | "sign_result" | "flow_status" + CallbackData *state_machine.EsignCallbackData `json:"callback_data"` +} + +// CertificationWorkflowOrchestrator 认证工作流编排器接口 +// 负责编排认证业务流程,协调各个领域服务的协作 +type CertificationWorkflowOrchestrator interface { + // 用户操作用例 + SubmitEnterpriseInfo(ctx context.Context, cmd *SubmitEnterpriseInfoCommand) (*WorkflowResult, error) + ApplyContract(ctx context.Context, cmd *ApplyContractCommand) (*WorkflowResult, error) + + // e签宝回调处理 + HandleEnterpriseVerificationCallback(ctx context.Context, cmd *EsignCallbackCommand) (*WorkflowResult, error) + HandleContractSignCallback(ctx context.Context, cmd *EsignCallbackCommand) (*WorkflowResult, error) + + // 异常处理 + HandleFailure(ctx context.Context, certificationID string, failureType string, reason string) (*WorkflowResult, error) + RetryOperation(ctx context.Context, certificationID string, operation string) (*WorkflowResult, error) + + // 查询操作 + GetWorkflowStatus(ctx context.Context, certificationID string) (*WorkflowResult, error) +} + +// CertificationWorkflowOrchestratorImpl 认证工作流编排器实现 +type CertificationWorkflowOrchestratorImpl struct { + aggregateService CertificationAggregateService + callbackHandler *state_machine.EsignCallbackHandler + logger *zap.Logger +} + +// NewCertificationWorkflowOrchestrator 创建认证工作流编排器 +func NewCertificationWorkflowOrchestrator( + aggregateService CertificationAggregateService, + callbackHandler *state_machine.EsignCallbackHandler, + logger *zap.Logger, +) CertificationWorkflowOrchestrator { + return &CertificationWorkflowOrchestratorImpl{ + aggregateService: aggregateService, + callbackHandler: callbackHandler, + logger: logger, + } +} + +// ================ 用户操作用例 ================ + +// SubmitEnterpriseInfo 用户提交企业信息 +func (o *CertificationWorkflowOrchestratorImpl) SubmitEnterpriseInfo( + ctx context.Context, + cmd *SubmitEnterpriseInfoCommand, +) (*WorkflowResult, error) { + o.logger.Info("开始处理企业信息提交", + zap.String("certification_id", cmd.CertificationID), + zap.String("user_id", cmd.UserID)) + + // 1. 验证命令完整性 + if err := o.validateSubmitEnterpriseInfoCommand(cmd); err != nil { + return o.createFailureResult(cmd.CertificationID, "", fmt.Sprintf("命令验证失败: %s", err.Error())), err + } + + // 2. 加载认证聚合根 + cert, err := o.aggregateService.LoadCertification(ctx, cmd.CertificationID) + if err != nil { + return o.createFailureResult(cmd.CertificationID, "", fmt.Sprintf("加载认证信息失败: %s", err.Error())), err + } + + // 3. 验证业务前置条件 + if err := o.validateEnterpriseInfoSubmissionPreconditions(cert, cmd.UserID); err != nil { + return o.createFailureResult(cmd.CertificationID, cert.Status, err.Error()), err + } + + // 4. 执行状态转换 + metadata := map[string]interface{}{ + "enterprise_info": cmd.EnterpriseInfo, + "user_id": cmd.UserID, + } + + result, err := o.aggregateService.TransitionState( + ctx, + cmd.CertificationID, + enums.StatusInfoSubmitted, + enums.ActorTypeUser, + cmd.UserID, + "用户提交企业信息", + metadata, + ) + if err != nil { + o.logger.Error("企业信息提交状态转换失败", zap.Error(err)) + return o.createFailureResult(cmd.CertificationID, cert.Status, fmt.Sprintf("状态转换失败: %s", err.Error())), err + } + + // 5. 执行后续处理(如调用e签宝API) + if err := o.triggerEnterpriseVerification(ctx, cmd.CertificationID, cmd.EnterpriseInfo); err != nil { + o.logger.Warn("触发企业认证失败", zap.Error(err)) + // 这里不返回错误,因为状态已经成功转换,e签宝调用失败可以通过重试机制处理 + } + + // 6. 构建成功结果 + return o.createSuccessResult(cmd.CertificationID, enums.StatusInfoSubmitted, "企业信息提交成功", map[string]interface{}{ + "enterprise_info": cmd.EnterpriseInfo, + "next_action": "等待企业认证结果", + }, result), nil +} + +// ApplyContract 用户申请合同签署 +func (o *CertificationWorkflowOrchestratorImpl) ApplyContract( + ctx context.Context, + cmd *ApplyContractCommand, +) (*WorkflowResult, error) { + o.logger.Info("开始处理合同申请", + zap.String("certification_id", cmd.CertificationID), + zap.String("user_id", cmd.UserID)) + + // 1. 验证命令完整性 + if err := o.validateApplyContractCommand(cmd); err != nil { + return o.createFailureResult(cmd.CertificationID, "", fmt.Sprintf("命令验证失败: %s", err.Error())), err + } + + // 2. 加载认证聚合根 + cert, err := o.aggregateService.LoadCertification(ctx, cmd.CertificationID) + if err != nil { + return o.createFailureResult(cmd.CertificationID, "", fmt.Sprintf("加载认证信息失败: %s", err.Error())), err + } + + // 3. 验证业务前置条件 + if err := o.validateContractApplicationPreconditions(cert, cmd.UserID); err != nil { + return o.createFailureResult(cmd.CertificationID, cert.Status, err.Error()), err + } + + // 4. 执行状态转换 + result, err := o.aggregateService.TransitionState( + ctx, + cmd.CertificationID, + enums.StatusContractApplied, + enums.ActorTypeUser, + cmd.UserID, + "用户申请合同签署", + map[string]interface{}{}, + ) + if err != nil { + o.logger.Error("合同申请状态转换失败", zap.Error(err)) + return o.createFailureResult(cmd.CertificationID, cert.Status, fmt.Sprintf("状态转换失败: %s", err.Error())), err + } + + // 5. 生成合同和签署链接 + contractInfo, err := o.generateContractAndSignURL(ctx, cmd.CertificationID, cert) + if err != nil { + o.logger.Error("生成合同失败", zap.Error(err)) + // 需要回滚状态 + return o.createFailureResult(cmd.CertificationID, cert.Status, fmt.Sprintf("生成合同失败: %s", err.Error())), err + } + + // 6. 构建成功结果 + return o.createSuccessResult(cmd.CertificationID, enums.StatusContractApplied, "合同申请成功", map[string]interface{}{ + "contract_sign_url": contractInfo.ContractSignURL, + "contract_url": contractInfo.ContractURL, + "next_action": "请在规定时间内完成合同签署", + }, result), nil +} + +// ================ e签宝回调处理 ================ + +// HandleEnterpriseVerificationCallback 处理企业认证回调 +func (o *CertificationWorkflowOrchestratorImpl) HandleEnterpriseVerificationCallback( + ctx context.Context, + cmd *EsignCallbackCommand, +) (*WorkflowResult, error) { + o.logger.Info("开始处理企业认证回调", + zap.String("certification_id", cmd.CertificationID), + zap.String("callback_type", cmd.CallbackType)) + + // 1. 验证回调数据 + if err := o.callbackHandler.ValidateCallbackData(cmd.CallbackData); err != nil { + return o.createFailureResult(cmd.CertificationID, "", fmt.Sprintf("回调数据验证失败: %s", err.Error())), err + } + + // 2. 加载认证聚合根 + cert, err := o.aggregateService.LoadCertification(ctx, cmd.CertificationID) + if err != nil { + return o.createFailureResult(cmd.CertificationID, "", fmt.Sprintf("加载认证信息失败: %s", err.Error())), err + } + + // 3. 验证回调处理前置条件 + if cert.Status != enums.StatusInfoSubmitted { + return o.createFailureResult(cmd.CertificationID, cert.Status, + fmt.Sprintf("当前状态 %s 不允许处理企业认证回调", enums.GetStatusName(cert.Status))), + fmt.Errorf("无效的状态转换") + } + + // 4. 处理回调 + err = o.callbackHandler.HandleCallback(ctx, cmd.CertificationID, cmd.CallbackData) + if err != nil { + o.logger.Error("处理企业认证回调失败", zap.Error(err)) + return o.createFailureResult(cmd.CertificationID, cert.Status, fmt.Sprintf("回调处理失败: %s", err.Error())), err + } + + // 5. 重新加载认证信息获取最新状态 + updatedCert, err := o.aggregateService.LoadCertification(ctx, cmd.CertificationID) + if err != nil { + return o.createFailureResult(cmd.CertificationID, cert.Status, "加载更新后的认证信息失败"), err + } + + // 6. 构建结果 + message := "企业认证回调处理成功" + data := map[string]interface{}{ + "auth_flow_id": cmd.CallbackData.FlowID, + "status": cmd.CallbackData.Status, + } + + if updatedCert.Status == enums.StatusEnterpriseVerified { + message = "企业认证成功" + data["next_action"] = "可以申请合同签署" + } else if updatedCert.Status == enums.StatusInfoRejected { + message = "企业认证失败" + data["next_action"] = "请修正企业信息后重新提交" + data["failure_reason"] = enums.GetFailureReasonName(updatedCert.FailureReason) + data["failure_message"] = updatedCert.FailureMessage + } + + return o.createSuccessResult(cmd.CertificationID, updatedCert.Status, message, data, nil), nil +} + +// HandleContractSignCallback 处理合同签署回调 +func (o *CertificationWorkflowOrchestratorImpl) HandleContractSignCallback( + ctx context.Context, + cmd *EsignCallbackCommand, +) (*WorkflowResult, error) { + o.logger.Info("开始处理合同签署回调", + zap.String("certification_id", cmd.CertificationID), + zap.String("callback_type", cmd.CallbackType)) + + // 1. 验证回调数据 + if err := o.callbackHandler.ValidateCallbackData(cmd.CallbackData); err != nil { + return o.createFailureResult(cmd.CertificationID, "", fmt.Sprintf("回调数据验证失败: %s", err.Error())), err + } + + // 2. 加载认证聚合根 + cert, err := o.aggregateService.LoadCertification(ctx, cmd.CertificationID) + if err != nil { + return o.createFailureResult(cmd.CertificationID, "", fmt.Sprintf("加载认证信息失败: %s", err.Error())), err + } + + // 3. 验证回调处理前置条件 + if cert.Status != enums.StatusContractApplied { + return o.createFailureResult(cmd.CertificationID, cert.Status, + fmt.Sprintf("当前状态 %s 不允许处理合同签署回调", enums.GetStatusName(cert.Status))), + fmt.Errorf("无效的状态转换") + } + + // 4. 处理回调 + err = o.callbackHandler.HandleCallback(ctx, cmd.CertificationID, cmd.CallbackData) + if err != nil { + o.logger.Error("处理合同签署回调失败", zap.Error(err)) + return o.createFailureResult(cmd.CertificationID, cert.Status, fmt.Sprintf("回调处理失败: %s", err.Error())), err + } + + // 5. 重新加载认证信息获取最新状态 + updatedCert, err := o.aggregateService.LoadCertification(ctx, cmd.CertificationID) + if err != nil { + return o.createFailureResult(cmd.CertificationID, cert.Status, "加载更新后的认证信息失败"), err + } + + // 6. 构建结果 + message := "合同签署回调处理成功" + data := map[string]interface{}{ + "esign_flow_id": cmd.CallbackData.FlowID, + "status": cmd.CallbackData.Status, + } + + if updatedCert.Status == enums.StatusContractSigned { + message = "认证完成" + data["next_action"] = "认证流程已完成" + data["contract_url"] = updatedCert.ContractURL + } else if enums.IsFailureStatus(updatedCert.Status) { + message = "合同签署失败" + data["next_action"] = "可以重新申请合同签署" + data["failure_reason"] = enums.GetFailureReasonName(updatedCert.FailureReason) + data["failure_message"] = updatedCert.FailureMessage + } + + return o.createSuccessResult(cmd.CertificationID, updatedCert.Status, message, data, nil), nil +} + +// ================ 异常处理 ================ + +// HandleFailure 处理业务失败 +func (o *CertificationWorkflowOrchestratorImpl) HandleFailure( + ctx context.Context, + certificationID string, + failureType string, + reason string, +) (*WorkflowResult, error) { + o.logger.Info("开始处理业务失败", + zap.String("certification_id", certificationID), + zap.String("failure_type", failureType), + zap.String("reason", reason)) + + // 1. 加载认证聚合根 + cert, err := o.aggregateService.LoadCertification(ctx, certificationID) + if err != nil { + return o.createFailureResult(certificationID, "", fmt.Sprintf("加载认证信息失败: %s", err.Error())), err + } + + // 2. 根据失败类型执行相应处理 + var targetStatus enums.CertificationStatus + var failureReason enums.FailureReason + + switch failureType { + case "enterprise_verification_failed": + targetStatus = enums.StatusInfoRejected + failureReason = enums.FailureReasonEsignVerificationFailed + case "contract_sign_failed": + targetStatus = enums.StatusContractRejected + failureReason = enums.FailureReasonSignProcessFailed + case "contract_expired": + targetStatus = enums.StatusContractExpired + failureReason = enums.FailureReasonContractExpired + default: + return o.createFailureResult(certificationID, cert.Status, fmt.Sprintf("未知的失败类型: %s", failureType)), + fmt.Errorf("未知的失败类型") + } + + // 3. 执行状态转换 + metadata := map[string]interface{}{ + "failure_reason": failureReason, + "failure_message": reason, + } + + result, err := o.aggregateService.TransitionState( + ctx, + certificationID, + targetStatus, + enums.ActorTypeSystem, + "failure_handler", + fmt.Sprintf("系统处理失败: %s", reason), + metadata, + ) + if err != nil { + return o.createFailureResult(certificationID, cert.Status, fmt.Sprintf("失败处理状态转换失败: %s", err.Error())), err + } + + return o.createSuccessResult(certificationID, targetStatus, "失败处理完成", map[string]interface{}{ + "failure_type": failureType, + "failure_reason": enums.GetFailureReasonName(failureReason), + "can_retry": enums.IsRetryable(failureReason), + }, result), nil +} + +// RetryOperation 重试操作 +func (o *CertificationWorkflowOrchestratorImpl) RetryOperation( + ctx context.Context, + certificationID string, + operation string, +) (*WorkflowResult, error) { + o.logger.Info("开始重试操作", + zap.String("certification_id", certificationID), + zap.String("operation", operation)) + + // 1. 加载认证聚合根 + cert, err := o.aggregateService.LoadCertification(ctx, certificationID) + if err != nil { + return o.createFailureResult(certificationID, "", fmt.Sprintf("加载认证信息失败: %s", err.Error())), err + } + + // 2. 检查是否可以重试 + if !enums.IsFailureStatus(cert.Status) { + return o.createFailureResult(certificationID, cert.Status, "当前状态不是失败状态,无需重试"), + fmt.Errorf("不需要重试") + } + + if !enums.IsRetryable(cert.FailureReason) { + return o.createFailureResult(certificationID, cert.Status, + fmt.Sprintf("失败原因 %s 不支持重试", enums.GetFailureReasonName(cert.FailureReason))), + fmt.Errorf("不支持重试") + } + + if cert.RetryCount >= 3 { + return o.createFailureResult(certificationID, cert.Status, "已达到最大重试次数限制"), + fmt.Errorf("超过重试限制") + } + + // 3. 根据操作类型执行重试 + var targetStatus enums.CertificationStatus + var reason string + + switch operation { + case "enterprise_verification": + if cert.Status != enums.StatusInfoRejected { + return o.createFailureResult(certificationID, cert.Status, "当前状态不支持企业认证重试"), + fmt.Errorf("无效的重试操作") + } + targetStatus = enums.StatusInfoSubmitted + reason = "重新提交企业信息" + case "contract_application": + if cert.Status != enums.StatusContractRejected && cert.Status != enums.StatusContractExpired { + return o.createFailureResult(certificationID, cert.Status, "当前状态不支持合同申请重试"), + fmt.Errorf("无效的重试操作") + } + targetStatus = enums.StatusEnterpriseVerified + reason = "重置状态,准备重新申请合同" + default: + return o.createFailureResult(certificationID, cert.Status, fmt.Sprintf("不支持的重试操作: %s", operation)), + fmt.Errorf("不支持的重试操作") + } + + // 4. 执行状态转换 + result, err := o.aggregateService.TransitionState( + ctx, + certificationID, + targetStatus, + enums.ActorTypeSystem, + "retry_handler", + reason, + map[string]interface{}{}, + ) + if err != nil { + return o.createFailureResult(certificationID, cert.Status, fmt.Sprintf("重试状态转换失败: %s", err.Error())), err + } + + return o.createSuccessResult(certificationID, targetStatus, "重试操作成功", map[string]interface{}{ + "retry_operation": operation, + "retry_count": cert.RetryCount + 1, + "next_action": o.getNextActionForStatus(targetStatus), + }, result), nil +} + +// ================ 查询操作 ================ + +// GetWorkflowStatus 获取工作流状态 +func (o *CertificationWorkflowOrchestratorImpl) GetWorkflowStatus( + ctx context.Context, + certificationID string, +) (*WorkflowResult, error) { + // 加载认证聚合根 + cert, err := o.aggregateService.LoadCertification(ctx, certificationID) + if err != nil { + return o.createFailureResult(certificationID, "", fmt.Sprintf("加载认证信息失败: %s", err.Error())), err + } + + // 构建状态信息 + data := map[string]interface{}{ + "status": cert.Status, + "status_name": enums.GetStatusName(cert.Status), + "progress": cert.GetProgress(), + "is_final": cert.IsFinalStatus(), + "is_completed": cert.IsCompleted(), + "user_action_required": cert.IsUserActionRequired(), + "next_action": o.getNextActionForStatus(cert.Status), + "available_actions": cert.GetAvailableActions(), + } + + // 添加失败信息(如果存在) + if enums.IsFailureStatus(cert.Status) { + data["failure_reason"] = enums.GetFailureReasonName(cert.FailureReason) + data["failure_message"] = cert.FailureMessage + data["can_retry"] = enums.IsRetryable(cert.FailureReason) + data["retry_count"] = cert.RetryCount + } + + // 添加时间戳信息 + if cert.InfoSubmittedAt != nil { + data["info_submitted_at"] = cert.InfoSubmittedAt + } + if cert.EnterpriseVerifiedAt != nil { + data["enterprise_verified_at"] = cert.EnterpriseVerifiedAt + } + if cert.ContractAppliedAt != nil { + data["contract_applied_at"] = cert.ContractAppliedAt + } + if cert.ContractSignedAt != nil { + data["contract_signed_at"] = cert.ContractSignedAt + } + + return o.createSuccessResult(certificationID, cert.Status, "工作流状态查询成功", data, nil), nil +} + +// ================ 辅助方法 ================ + +// validateSubmitEnterpriseInfoCommand 验证提交企业信息命令 +func (o *CertificationWorkflowOrchestratorImpl) validateSubmitEnterpriseInfoCommand(cmd *SubmitEnterpriseInfoCommand) error { + if cmd.CertificationID == "" { + return fmt.Errorf("认证ID不能为空") + } + if cmd.UserID == "" { + return fmt.Errorf("用户ID不能为空") + } + if cmd.EnterpriseInfo == nil { + return fmt.Errorf("企业信息不能为空") + } + return cmd.EnterpriseInfo.Validate() +} + +// validateApplyContractCommand 验证申请合同命令 +func (o *CertificationWorkflowOrchestratorImpl) validateApplyContractCommand(cmd *ApplyContractCommand) error { + if cmd.CertificationID == "" { + return fmt.Errorf("认证ID不能为空") + } + if cmd.UserID == "" { + return fmt.Errorf("用户ID不能为空") + } + return nil +} + +// validateEnterpriseInfoSubmissionPreconditions 验证企业信息提交前置条件 +func (o *CertificationWorkflowOrchestratorImpl) validateEnterpriseInfoSubmissionPreconditions(cert *entities.Certification, userID string) error { + if cert.UserID != userID { + return fmt.Errorf("用户无权限操作此认证申请") + } + if cert.Status != enums.StatusPending && cert.Status != enums.StatusInfoRejected { + return fmt.Errorf("当前状态 %s 不允许提交企业信息", enums.GetStatusName(cert.Status)) + } + return nil +} + +// validateContractApplicationPreconditions 验证合同申请前置条件 +func (o *CertificationWorkflowOrchestratorImpl) validateContractApplicationPreconditions(cert *entities.Certification, userID string) error { + if cert.UserID != userID { + return fmt.Errorf("用户无权限操作此认证申请") + } + if cert.Status != enums.StatusEnterpriseVerified { + return fmt.Errorf("必须先完成企业认证才能申请合同") + } + if cert.AuthFlowID == "" { + return fmt.Errorf("缺少企业认证流程ID") + } + return nil +} + +// triggerEnterpriseVerification 触发企业认证 +func (o *CertificationWorkflowOrchestratorImpl) triggerEnterpriseVerification(ctx context.Context, certificationID string, enterpriseInfo *value_objects.EnterpriseInfo) error { + // TODO: 调用e签宝API进行企业认证 + o.logger.Info("触发企业认证", + zap.String("certification_id", certificationID), + zap.String("company_name", enterpriseInfo.CompanyName)) + return nil +} + +// generateContractAndSignURL 生成合同和签署链接 +func (o *CertificationWorkflowOrchestratorImpl) generateContractAndSignURL(ctx context.Context, certificationID string, cert *entities.Certification) (*value_objects.ContractInfo, error) { + // TODO: 调用e签宝API生成合同和签署链接 + o.logger.Info("生成合同和签署链接", zap.String("certification_id", certificationID)) + + // 临时返回模拟数据 + contractInfo, err := value_objects.NewContractInfo( + "contract_file_"+certificationID, + "esign_flow_"+certificationID, + "https://example.com/contract/"+certificationID, + "https://example.com/sign/"+certificationID, + ) + if err != nil { + return nil, err + } + + return contractInfo, nil +} + +// getNextActionForStatus 获取状态对应的下一步操作提示 +func (o *CertificationWorkflowOrchestratorImpl) getNextActionForStatus(status enums.CertificationStatus) string { + return enums.GetUserActionHint(status) +} + +// createSuccessResult 创建成功结果 +func (o *CertificationWorkflowOrchestratorImpl) createSuccessResult( + certificationID string, + status enums.CertificationStatus, + message string, + data map[string]interface{}, + stateTransition *state_machine.StateTransitionResult, +) *WorkflowResult { + return &WorkflowResult{ + Success: true, + CertificationID: certificationID, + CurrentStatus: status, + Message: message, + Data: data, + StateTransition: stateTransition, + ExecutedAt: time.Now(), + } +} + +// createFailureResult 创建失败结果 +func (o *CertificationWorkflowOrchestratorImpl) createFailureResult( + certificationID string, + status enums.CertificationStatus, + message string, +) *WorkflowResult { + return &WorkflowResult{ + Success: false, + CertificationID: certificationID, + CurrentStatus: status, + Message: message, + Data: map[string]interface{}{}, + ExecutedAt: time.Now(), + } +} diff --git a/internal/domains/certification/services/certification_workflow_service.go b/internal/domains/certification/services/certification_workflow_service.go deleted file mode 100644 index cbb7028..0000000 --- a/internal/domains/certification/services/certification_workflow_service.go +++ /dev/null @@ -1,162 +0,0 @@ -package services - -import ( - "context" - "fmt" - - "go.uber.org/zap" - - "tyapi-server/internal/domains/certification/enums" - "tyapi-server/internal/domains/certification/repositories" -) - -// CertificationWorkflowService 认证工作流领域服务 -// 负责认证流程的状态转换和业务逻辑处理 -type CertificationWorkflowService struct { - certRepo repositories.CertificationRepository - stateMachine *CertificationStateMachine - logger *zap.Logger -} - -// NewCertificationWorkflowService 创建认证工作流领域服务 -func NewCertificationWorkflowService( - certRepo repositories.CertificationRepository, - stateMachine *CertificationStateMachine, - logger *zap.Logger, -) *CertificationWorkflowService { - return &CertificationWorkflowService{ - certRepo: certRepo, - stateMachine: stateMachine, - logger: logger, - } -} - -// SubmitEnterpriseInfo 提交企业信息 -func (s *CertificationWorkflowService) SubmitEnterpriseInfo(ctx context.Context, certificationID string) error { - cert, err := s.certRepo.GetByID(ctx, certificationID) - if err != nil { - return fmt.Errorf("认证申请不存在: %w", err) - } - - // 检查当前状态是否可以提交企业信息 - if cert.Status != enums.StatusPending && cert.Status != enums.StatusInfoSubmitted { - return fmt.Errorf("当前状态不允许提交企业信息") - } - - // 使用状态机转换状态 - if err := s.stateMachine.TransitionTo(ctx, certificationID, enums.StatusInfoSubmitted, true, false, nil); err != nil { - return fmt.Errorf("状态转换失败: %w", err) - } - - s.logger.Info("企业信息提交成功", - zap.String("certification_id", certificationID), - zap.String("user_id", cert.UserID), - ) - - return nil -} - -// CompleteEnterpriseVerification 完成企业认证 -func (s *CertificationWorkflowService) CompleteEnterpriseVerification(ctx context.Context, certificationID string) error { - cert, err := s.certRepo.GetByID(ctx, certificationID) - if err != nil { - return fmt.Errorf("认证申请不存在: %w", err) - } - - // 检查当前状态是否可以完成企业认证 - if cert.Status != enums.StatusInfoSubmitted { - return fmt.Errorf("当前状态不允许完成企业认证") - } - - // 使用状态机转换状态 - if err := s.stateMachine.TransitionTo(ctx, certificationID, enums.StatusEnterpriseVerified, true, false, nil); err != nil { - return fmt.Errorf("状态转换失败: %w", err) - } - - s.logger.Info("企业认证完成", - zap.String("certification_id", certificationID), - zap.String("user_id", cert.UserID), - ) - - return nil -} - -// ApplyContract 申请签署合同 -func (s *CertificationWorkflowService) ApplyContract(ctx context.Context, certificationID string) error { - cert, err := s.certRepo.GetByID(ctx, certificationID) - if err != nil { - return fmt.Errorf("认证申请不存在: %w", err) - } - - // 检查当前状态是否可以申请签署合同 - if cert.Status != enums.StatusEnterpriseVerified { - return fmt.Errorf("当前状态不允许申请签署合同") - } - - // 使用状态机转换状态 - if err := s.stateMachine.TransitionTo(ctx, certificationID, enums.StatusContractApplied, true, false, nil); err != nil { - return fmt.Errorf("状态转换失败: %w", err) - } - - s.logger.Info("签署合同申请成功", - zap.String("certification_id", certificationID), - zap.String("user_id", cert.UserID), - ) - - return nil -} - -// CompleteContractSign 完成合同签署 -func (s *CertificationWorkflowService) CompleteContractSign(ctx context.Context, certificationID, contractURL string) error { - cert, err := s.certRepo.GetByID(ctx, certificationID) - if err != nil { - return fmt.Errorf("认证申请不存在: %w", err) - } - - // 检查当前状态是否可以签署 - if cert.Status != enums.StatusContractApplied { - return fmt.Errorf("当前状态不允许签署") - } - - // 准备签署元数据 - metadata := map[string]interface{}{ - "contract_url": contractURL, - } - - // 使用状态机转换状态 - if err := s.stateMachine.TransitionTo(ctx, certificationID, enums.StatusContractSigned, true, false, metadata); err != nil { - return fmt.Errorf("状态转换失败: %w", err) - } - - s.logger.Info("合同签署完成", - zap.String("certification_id", certificationID), - zap.String("user_id", cert.UserID), - ) - - return nil -} - -// CompleteCertification 完成认证 -func (s *CertificationWorkflowService) CompleteCertification(ctx context.Context, certificationID string) error { - cert, err := s.certRepo.GetByID(ctx, certificationID) - if err != nil { - return fmt.Errorf("认证申请不存在: %w", err) - } - - // 检查当前状态是否可以完成 - if cert.Status != enums.StatusContractSigned { - return fmt.Errorf("当前状态不允许完成认证") - } - - // 使用状态机转换状态 - if err := s.stateMachine.TransitionTo(ctx, certificationID, enums.StatusCompleted, false, false, nil); err != nil { - return fmt.Errorf("状态转换失败: %w", err) - } - - s.logger.Info("认证完成", - zap.String("certification_id", certificationID), - zap.String("user_id", cert.UserID), - ) - - return nil -} diff --git a/internal/domains/certification/services/enterprise_info_submit_record_service.go b/internal/domains/certification/services/enterprise_info_submit_record_service.go deleted file mode 100644 index 5a1acbf..0000000 --- a/internal/domains/certification/services/enterprise_info_submit_record_service.go +++ /dev/null @@ -1,180 +0,0 @@ -package services - -import ( - "context" - "fmt" - - "go.uber.org/zap" - - "tyapi-server/internal/domains/certification/entities" - "tyapi-server/internal/domains/certification/repositories" -) - -// EnterpriseInfoSubmitRecordService 企业信息提交记录领域服务 -// 负责企业信息提交记录的业务逻辑处理 -type EnterpriseInfoSubmitRecordService struct { - enterpriseRecordRepo repositories.EnterpriseInfoSubmitRecordRepository - logger *zap.Logger -} - -// NewEnterpriseInfoSubmitRecordService 创建企业信息提交记录领域服务 -func NewEnterpriseInfoSubmitRecordService( - enterpriseRecordRepo repositories.EnterpriseInfoSubmitRecordRepository, - logger *zap.Logger, -) *EnterpriseInfoSubmitRecordService { - return &EnterpriseInfoSubmitRecordService{ - enterpriseRecordRepo: enterpriseRecordRepo, - logger: logger, - } -} - -// CreateEnterpriseInfoSubmitRecord 创建企业信息提交记录 -func (s *EnterpriseInfoSubmitRecordService) CreateEnterpriseInfoSubmitRecord( - ctx context.Context, - userID string, - companyName string, - unifiedSocialCode string, - legalPersonName string, - legalPersonID string, - legalPersonPhone string, -) (*entities.EnterpriseInfoSubmitRecord, error) { - // 创建企业信息提交记录实体 - record := entities.NewEnterpriseInfoSubmitRecord( - userID, - companyName, - unifiedSocialCode, - legalPersonName, - legalPersonID, - legalPersonPhone, - ) - - // 保存到仓储 - createdRecord, err := s.enterpriseRecordRepo.Create(ctx, *record) - if err != nil { - s.logger.Error("创建企业信息提交记录失败", - zap.String("user_id", userID), - zap.Error(err)) - return nil, fmt.Errorf("创建企业信息提交记录失败: %w", err) - } - - s.logger.Info("企业信息提交记录创建成功", - zap.String("record_id", createdRecord.ID), - zap.String("user_id", userID), - zap.String("company_name", companyName)) - - return &createdRecord, nil -} - -// GetLatestByUserID 根据用户ID获取最新的企业信息提交记录 -func (s *EnterpriseInfoSubmitRecordService) GetLatestByUserID(ctx context.Context, userID string) (*entities.EnterpriseInfoSubmitRecord, error) { - record, err := s.enterpriseRecordRepo.GetLatestByUserID(ctx, userID) - if err != nil { - s.logger.Error("获取企业信息提交记录失败", - zap.String("user_id", userID), - zap.Error(err)) - return nil, fmt.Errorf("获取企业信息提交记录失败: %w", err) - } - - return record, nil -} - -// UpdateEnterpriseInfoSubmitRecord 更新企业信息提交记录 -func (s *EnterpriseInfoSubmitRecordService) UpdateEnterpriseInfoSubmitRecord(ctx context.Context, record *entities.EnterpriseInfoSubmitRecord) error { - err := s.enterpriseRecordRepo.Update(ctx, *record) - if err != nil { - s.logger.Error("更新企业信息提交记录失败", - zap.String("record_id", record.ID), - zap.Error(err)) - return fmt.Errorf("更新企业信息提交记录失败: %w", err) - } - - s.logger.Info("企业信息提交记录更新成功", - zap.String("record_id", record.ID), - zap.String("status", record.Status)) - - return nil -} - -// MarkAsVerified 标记企业信息为已验证 -func (s *EnterpriseInfoSubmitRecordService) MarkAsVerified(ctx context.Context, recordID string) error { - record, err := s.enterpriseRecordRepo.GetByID(ctx, recordID) - if err != nil { - return fmt.Errorf("获取企业信息提交记录失败: %w", err) - } - - record.MarkAsVerified() - err = s.enterpriseRecordRepo.Update(ctx, record) - if err != nil { - s.logger.Error("标记企业信息为已验证失败", - zap.String("record_id", recordID), - zap.Error(err)) - return fmt.Errorf("标记企业信息为已验证失败: %w", err) - } - - s.logger.Info("企业信息标记为已验证成功", - zap.String("record_id", recordID)) - - return nil -} - -// UpdateVerificationStatus 更新企业信息验证状态 -func (s *EnterpriseInfoSubmitRecordService) UpdateVerificationStatus(ctx context.Context, userID string, isVerified bool, reason string) error { - // 获取用户最新的企业信息提交记录 - record, err := s.enterpriseRecordRepo.GetLatestByUserID(ctx, userID) - if err != nil { - return fmt.Errorf("获取企业信息提交记录失败: %w", err) - } - - // 更新验证状态 - if isVerified { - record.MarkAsVerified() - } else { - record.MarkAsFailed(reason) - } - - // 保存更新 - err = s.enterpriseRecordRepo.Update(ctx, *record) - if err != nil { - s.logger.Error("更新企业信息验证状态失败", - zap.String("user_id", userID), - zap.Bool("is_verified", isVerified), - zap.Error(err)) - return fmt.Errorf("更新企业信息验证状态失败: %w", err) - } - - s.logger.Info("企业信息验证状态更新成功", - zap.String("user_id", userID), - zap.Bool("is_verified", isVerified), - zap.String("reason", reason)) - - return nil -} - -// DeleteEnterpriseInfoSubmitRecord 删除企业信息提交记录 -func (s *EnterpriseInfoSubmitRecordService) DeleteEnterpriseInfoSubmitRecord(ctx context.Context, recordID string) error { - err := s.enterpriseRecordRepo.Delete(ctx, recordID) - if err != nil { - s.logger.Error("删除企业信息提交记录失败", - zap.String("record_id", recordID), - zap.Error(err)) - return fmt.Errorf("删除企业信息提交记录失败: %w", err) - } - - s.logger.Info("企业信息提交记录删除成功", - zap.String("record_id", recordID)) - - return nil -} - -// GetByUserID 根据用户ID获取企业信息提交记录列表 -func (s *EnterpriseInfoSubmitRecordService) GetByUserID(ctx context.Context, userID string) ([]*entities.EnterpriseInfoSubmitRecord, error) { - records, err := s.enterpriseRecordRepo.GetByUserID(ctx, userID) - if err != nil { - s.logger.Error("获取用户企业信息提交记录失败", - zap.String("user_id", userID), - zap.Error(err)) - return nil, fmt.Errorf("获取用户企业信息提交记录失败: %w", err) - } - - return records, nil -} \ No newline at end of file diff --git a/internal/domains/certification/services/state_config.go b/internal/domains/certification/services/state_config.go index 01b63e8..ae8d6ef 100644 --- a/internal/domains/certification/services/state_config.go +++ b/internal/domains/certification/services/state_config.go @@ -97,18 +97,9 @@ func (manager *CertificationStateManager) initStateConfigs() { IsAdminActionRequired: false, TimestampField: "ContractSignedAt", Description: "合同已签署", - NextValidStatuses: []enums.CertificationStatus{enums.StatusCompleted}, - }, - { - Status: enums.StatusCompleted, - Name: "认证完成", - ProgressPercentage: 100, - IsUserActionRequired: false, - IsAdminActionRequired: false, - TimestampField: "CompletedAt", - Description: "认证流程已完成", NextValidStatuses: []enums.CertificationStatus{}, }, + // 已完成状态已合并到StatusContractSigned中 } // 转换配置 @@ -168,17 +159,7 @@ func (manager *CertificationStateManager) initStateConfigs() { RequiresValidation: true, Description: "用户签署合同", }, - // 完成认证 - { - From: enums.StatusContractSigned, - To: enums.StatusCompleted, - Action: "complete", - ActionName: "完成认证", - AllowUser: false, - AllowAdmin: false, - RequiresValidation: false, - Description: "系统自动完成认证", - }, + // 合同签署即为认证完成,无需额外状态转换 } // 构建映射 diff --git a/internal/domains/certification/services/state_machine.go b/internal/domains/certification/services/state_machine.go deleted file mode 100644 index a729c2d..0000000 --- a/internal/domains/certification/services/state_machine.go +++ /dev/null @@ -1,258 +0,0 @@ -package services - -import ( - "context" - "fmt" - "time" - - "tyapi-server/internal/domains/certification/entities" - "tyapi-server/internal/domains/certification/enums" - "tyapi-server/internal/domains/certification/repositories" - - "go.uber.org/zap" -) - -// CertificationStateMachine 认证状态机 -type CertificationStateMachine struct { - stateManager *CertificationStateManager - certRepo repositories.CertificationRepository - logger *zap.Logger -} - -// NewCertificationStateMachine 创建认证状态机 -func NewCertificationStateMachine( - certRepo repositories.CertificationRepository, - logger *zap.Logger, -) *CertificationStateMachine { - return &CertificationStateMachine{ - stateManager: NewCertificationStateManager(), - certRepo: certRepo, - logger: logger, - } -} - -// CanTransition 检查是否可以转换到指定状态 -func (sm *CertificationStateMachine) CanTransition( - from enums.CertificationStatus, - to enums.CertificationStatus, - isUser bool, - isAdmin bool, -) (bool, string) { - return sm.stateManager.CanTransition(from, to, isUser, isAdmin) -} - -// TransitionTo 执行状态转换 -func (sm *CertificationStateMachine) TransitionTo( - ctx context.Context, - certificationID string, - targetStatus enums.CertificationStatus, - isUser bool, - isAdmin bool, - metadata map[string]interface{}, -) error { - // 获取当前认证记录 - cert, err := sm.certRepo.GetByID(ctx, certificationID) - if err != nil { - return fmt.Errorf("获取认证记录失败: %w", err) - } - - // 检查是否可以转换 - canTransition, reason := sm.CanTransition(cert.Status, targetStatus, isUser, isAdmin) - if !canTransition { - return fmt.Errorf("状态转换失败: %s", reason) - } - - // 更新状态和时间戳 - oldStatus := cert.Status - cert.Status = targetStatus - sm.updateTimestamp(&cert, targetStatus) - - // 更新其他字段 - sm.updateCertificationFields(&cert, targetStatus, metadata) - - // 保存到数据库 - if err := sm.certRepo.Update(ctx, cert); err != nil { - return fmt.Errorf("保存状态转换失败: %w", err) - } - - sm.logger.Info("认证状态转换成功", - zap.String("certification_id", certificationID), - zap.String("from_status", string(oldStatus)), - zap.String("to_status", string(targetStatus)), - zap.Bool("is_user", isUser), - zap.Bool("is_admin", isAdmin), - ) - - return nil -} - -// updateTimestamp 更新对应的时间戳字段 -func (sm *CertificationStateMachine) updateTimestamp(cert *entities.Certification, status enums.CertificationStatus) { - stateConfig := sm.stateManager.GetStateConfig(status) - if stateConfig == nil || stateConfig.TimestampField == "" { - return - } - - now := time.Now() - - switch stateConfig.TimestampField { - case "InfoSubmittedAt": - cert.InfoSubmittedAt = &now - case "EnterpriseVerifiedAt": - cert.EnterpriseVerifiedAt = &now - case "ContractAppliedAt": - cert.ContractAppliedAt = &now - case "ContractSignedAt": - cert.ContractSignedAt = &now - case "CompletedAt": - cert.CompletedAt = &now - } -} - -// updateCertificationFields 根据状态更新认证记录的其他字段 -func (sm *CertificationStateMachine) updateCertificationFields( - cert *entities.Certification, - status enums.CertificationStatus, - metadata map[string]interface{}, -) { - switch status { - case enums.StatusContractSigned: - if contractURL, ok := metadata["contract_url"].(string); ok { - cert.ContractURL = contractURL - } - } -} - -// GetValidNextStatuses 获取当前状态可以转换到的下一个状态列表 -func (sm *CertificationStateMachine) GetValidNextStatuses( - currentStatus enums.CertificationStatus, - isUser bool, - isAdmin bool, -) []enums.CertificationStatus { - return sm.stateManager.GetNextValidStatuses(currentStatus) -} - -// GetTransitionAction 获取状态转换对应的操作名称 -func (sm *CertificationStateMachine) GetTransitionAction( - from enums.CertificationStatus, - to enums.CertificationStatus, -) string { - transitions := sm.stateManager.GetTransitionConfigs(from) - for _, transition := range transitions { - if transition.To == to { - return transition.Action - } - } - return "" -} - -// GetTransitionActionName 获取状态转换对应的操作中文名称 -func (sm *CertificationStateMachine) GetTransitionActionName( - from enums.CertificationStatus, - to enums.CertificationStatus, -) string { - transitions := sm.stateManager.GetTransitionConfigs(from) - for _, transition := range transitions { - if transition.To == to { - return transition.ActionName - } - } - return "" -} - -// GetStateConfig 获取状态配置 -func (sm *CertificationStateMachine) GetStateConfig(status enums.CertificationStatus) *StateConfig { - return sm.stateManager.GetStateConfig(status) -} - -// GetProgressPercentage 获取进度百分比 -func (sm *CertificationStateMachine) GetProgressPercentage(status enums.CertificationStatus) int { - return sm.stateManager.GetProgressPercentage(status) -} - -// IsUserActionRequired 检查是否需要用户操作 -func (sm *CertificationStateMachine) IsUserActionRequired(status enums.CertificationStatus) bool { - return sm.stateManager.IsUserActionRequired(status) -} - -// IsAdminActionRequired 检查是否需要管理员操作 -func (sm *CertificationStateMachine) IsAdminActionRequired(status enums.CertificationStatus) bool { - return sm.stateManager.IsAdminActionRequired(status) -} - -// GetTransitionHistory 获取状态转换历史 -func (sm *CertificationStateMachine) GetTransitionHistory(ctx context.Context, certificationID string) ([]map[string]interface{}, error) { - cert, err := sm.certRepo.GetByID(ctx, certificationID) - if err != nil { - return nil, fmt.Errorf("获取认证记录失败: %w", err) - } - - history := []map[string]interface{}{} - - // 添加创建时间 - history = append(history, map[string]interface{}{ - "status": "CREATED", - "timestamp": cert.CreatedAt, - "action": "create", - "performer": "system", - "metadata": map[string]interface{}{}, - }) - - // 添加各个时间节点的状态转换 - if cert.InfoSubmittedAt != nil { - history = append(history, map[string]interface{}{ - "status": string(enums.StatusInfoSubmitted), - "timestamp": *cert.InfoSubmittedAt, - "action": "submit_info", - "performer": "user", - "metadata": map[string]interface{}{}, - }) - } - - if cert.EnterpriseVerifiedAt != nil { - history = append(history, map[string]interface{}{ - "status": string(enums.StatusEnterpriseVerified), - "timestamp": *cert.EnterpriseVerifiedAt, - "action": "enterprise_verify", - "performer": "user", - "metadata": map[string]interface{}{}, - }) - } - - if cert.ContractAppliedAt != nil { - history = append(history, map[string]interface{}{ - "status": string(enums.StatusContractApplied), - "timestamp": *cert.ContractAppliedAt, - "action": "apply_contract", - "performer": "user", - "metadata": map[string]interface{}{}, - }) - } - - if cert.ContractSignedAt != nil { - metadata := map[string]interface{}{} - if cert.ContractURL != "" { - metadata["contract_url"] = cert.ContractURL - } - - history = append(history, map[string]interface{}{ - "status": string(enums.StatusContractSigned), - "timestamp": *cert.ContractSignedAt, - "action": "sign_contract", - "performer": "user", - "metadata": metadata, - }) - } - - if cert.CompletedAt != nil { - history = append(history, map[string]interface{}{ - "status": string(enums.StatusCompleted), - "timestamp": *cert.CompletedAt, - "action": "complete", - "performer": "system", - "metadata": map[string]interface{}{}, - }) - } - - return history, nil -} diff --git a/internal/domains/certification/services/state_machine/certification_state_machine.go b/internal/domains/certification/services/state_machine/certification_state_machine.go new file mode 100644 index 0000000..212cf33 --- /dev/null +++ b/internal/domains/certification/services/state_machine/certification_state_machine.go @@ -0,0 +1,455 @@ +package state_machine + +import ( + "context" + "fmt" + "time" + + "tyapi-server/internal/domains/certification/entities" + "tyapi-server/internal/domains/certification/enums" + "tyapi-server/internal/domains/certification/repositories" + + "go.uber.org/zap" +) + +// CertificationStateMachine 认证状态机 +// 负责管理认证流程的状态转换、业务规则验证和事件发布 +type CertificationStateMachine struct { + configManager *StateConfigManager + repository repositories.CertificationCommandRepository + eventPublisher interface{} // TODO: 使用 interfaces.EventPublisher + logger *zap.Logger +} + +// NewCertificationStateMachine 创建认证状态机 +func NewCertificationStateMachine( + repository repositories.CertificationCommandRepository, + eventPublisher interface{}, // TODO: 使用 interfaces.EventPublisher + logger *zap.Logger, +) *CertificationStateMachine { + return &CertificationStateMachine{ + configManager: NewStateConfigManager(), + repository: repository, + eventPublisher: eventPublisher, + logger: logger, + } +} + +// StateTransitionRequest 状态转换请求 +type StateTransitionRequest struct { + CertificationID string `json:"certification_id"` + TargetStatus enums.CertificationStatus `json:"target_status"` + Actor enums.ActorType `json:"actor"` + ActorID string `json:"actor_id"` + Reason string `json:"reason"` + Context map[string]interface{} `json:"context"` + AllowRollback bool `json:"allow_rollback"` +} + +// StateTransitionResult 状态转换结果 +type StateTransitionResult struct { + Success bool `json:"success"` + OldStatus enums.CertificationStatus `json:"old_status"` + NewStatus enums.CertificationStatus `json:"new_status"` + Message string `json:"message"` + TransitionedAt time.Time `json:"transitioned_at"` + Events []interface{} `json:"events,omitempty"` +} + +// CanTransition 检查是否可以执行状态转换 +func (sm *CertificationStateMachine) CanTransition( + cert *entities.Certification, + targetStatus enums.CertificationStatus, + actor enums.ActorType, +) (bool, string) { + // 1. 检查基本状态转换规则 + canTransition, message := sm.configManager.CanTransition(cert.Status, targetStatus, actor) + if !canTransition { + return false, message + } + + // 2. 检查认证实体的业务规则 + if canTransition, message := cert.CanTransitionTo(targetStatus, actor); !canTransition { + return false, message + } + + // 3. 检查是否为最终状态 + if cert.IsFinalStatus() { + return false, "认证已完成,无法进行状态转换" + } + + return true, "" +} + +// ExecuteTransition 执行状态转换 +func (sm *CertificationStateMachine) ExecuteTransition( + ctx context.Context, + req *StateTransitionRequest, +) (*StateTransitionResult, error) { + sm.logger.Info("开始执行状态转换", + zap.String("certification_id", req.CertificationID), + zap.String("target_status", string(req.TargetStatus)), + zap.String("actor", string(req.Actor)), + zap.String("actor_id", req.ActorID)) + + // 1. 加载认证聚合根 + cert, err := sm.loadCertification(ctx, req.CertificationID) + if err != nil { + return sm.createFailureResult(cert.Status, req.TargetStatus, fmt.Sprintf("加载认证信息失败: %s", err.Error())), err + } + + oldStatus := cert.Status + + // 2. 验证转换合法性 + if canTransition, message := sm.CanTransition(cert, req.TargetStatus, req.Actor); !canTransition { + return sm.createFailureResult(oldStatus, req.TargetStatus, message), fmt.Errorf("状态转换验证失败: %s", message) + } + + // 3. 验证业务规则 + if err := sm.validateBusinessRules(cert, req); err != nil { + return sm.createFailureResult(oldStatus, req.TargetStatus, fmt.Sprintf("业务规则验证失败: %s", err.Error())), err + } + + // 4. 执行状态转换 + if err := cert.TransitionTo(req.TargetStatus, req.Actor, req.ActorID, req.Reason); err != nil { + return sm.createFailureResult(oldStatus, req.TargetStatus, fmt.Sprintf("状态转换执行失败: %s", err.Error())), err + } + + // 5. 保存到数据库 + if err := sm.repository.Update(ctx, *cert); err != nil { + // 如果保存失败,需要回滚状态 + sm.logger.Error("状态转换保存失败,尝试回滚", + zap.String("certification_id", req.CertificationID), + zap.Error(err)) + + if req.AllowRollback { + if rollbackErr := sm.rollbackStateTransition(ctx, cert, oldStatus, req.Actor, req.ActorID); rollbackErr != nil { + sm.logger.Error("状态回滚失败", zap.Error(rollbackErr)) + } + } + + return sm.createFailureResult(oldStatus, req.TargetStatus, fmt.Sprintf("保存状态转换失败: %s", err.Error())), err + } + + // 6. 发布领域事件 + events := cert.GetDomainEvents() + for _, event := range events { + // TODO: 实现事件发布 + // if err := sm.eventPublisher.PublishEvent(ctx, event); err != nil { + // sm.logger.Error("发布领域事件失败", + // zap.String("certification_id", req.CertificationID), + // zap.Error(err)) + // } + sm.logger.Info("领域事件待发布", + zap.String("certification_id", req.CertificationID), + zap.Any("event", event)) + } + + // 7. 清理领域事件 + cert.ClearDomainEvents() + + // 8. 记录成功日志 + sm.logger.Info("状态转换执行成功", + zap.String("certification_id", req.CertificationID), + zap.String("from_status", string(oldStatus)), + zap.String("to_status", string(req.TargetStatus)), + zap.String("actor", string(req.Actor))) + + // 9. 返回成功结果 + return &StateTransitionResult{ + Success: true, + OldStatus: oldStatus, + NewStatus: req.TargetStatus, + Message: "状态转换成功", + TransitionedAt: time.Now(), + Events: events, + }, nil +} + +// GetValidTransitions 获取有效的状态转换 +func (sm *CertificationStateMachine) GetValidTransitions( + cert *entities.Certification, + actor enums.ActorType, +) []*StateTransitionRule { + return sm.configManager.GetAllowedTransitions(cert.Status, actor) +} + +// GetStateInfo 获取状态信息 +func (sm *CertificationStateMachine) GetStateInfo(status enums.CertificationStatus) *StateConfig { + return sm.configManager.GetStateConfig(status) +} + +// ValidateBusinessRules 验证业务规则 +func (sm *CertificationStateMachine) ValidateBusinessRules( + cert *entities.Certification, + req *StateTransitionRequest, +) error { + return sm.validateBusinessRules(cert, req) +} + +// IsUserActionRequired 检查是否需要用户操作 +func (sm *CertificationStateMachine) IsUserActionRequired(status enums.CertificationStatus) bool { + return sm.configManager.IsUserActionRequired(status) +} + +// GetProgressPercentage 获取进度百分比 +func (sm *CertificationStateMachine) GetProgressPercentage(status enums.CertificationStatus) int { + return sm.configManager.GetStateProgress(status) +} + +// ================ 私有方法 ================ + +// loadCertification 加载认证聚合根 +func (sm *CertificationStateMachine) loadCertification(ctx context.Context, certificationID string) (*entities.Certification, error) { + // 这里需要通过查询仓储获取认证信息 + // 由于当前只有命令仓储,这里使用简单的方法 + // 在实际实现中,应该使用查询仓储 + cert := &entities.Certification{ID: certificationID} + + // TODO: 实现从查询仓储加载认证信息 + // cert, err := sm.queryRepository.GetByID(ctx, certificationID) + // if err != nil { + // return nil, fmt.Errorf("认证信息不存在: %w", err) + // } + + return cert, nil +} + +// validateBusinessRules 验证业务规则 +func (sm *CertificationStateMachine) validateBusinessRules( + cert *entities.Certification, + req *StateTransitionRequest, +) error { + // 获取转换规则 + rule := sm.configManager.GetTransitionRule(cert.Status, req.TargetStatus) + if rule == nil { + return fmt.Errorf("找不到状态转换规则") + } + + // 如果不需要验证,直接返回 + if !rule.RequiresValidation { + return nil + } + + // 构建验证上下文 + context := make(map[string]interface{}) + + // 添加认证基本信息 + context["certification_id"] = cert.ID + context["user_id"] = cert.UserID + context["current_status"] = string(cert.Status) + context["retry_count"] = cert.RetryCount + context["auth_flow_id"] = cert.AuthFlowID + + // 添加请求中的上下文信息 + for key, value := range req.Context { + context[key] = value + } + + // 执行业务规则验证 + return sm.configManager.ValidateBusinessRules(rule, context) +} + +// rollbackStateTransition 回滚状态转换 +func (sm *CertificationStateMachine) rollbackStateTransition( + ctx context.Context, + cert *entities.Certification, + originalStatus enums.CertificationStatus, + actor enums.ActorType, + actorID string, +) error { + sm.logger.Info("开始回滚状态转换", + zap.String("certification_id", cert.ID), + zap.String("original_status", string(originalStatus)), + zap.String("current_status", string(cert.Status))) + + // 直接设置回原状态(跳过业务规则验证) + cert.Status = originalStatus + + // 更新审计信息 + now := time.Now() + cert.LastTransitionAt = &now + cert.LastTransitionBy = actor + cert.LastTransitionActor = actorID + + // 保存回滚结果 + if err := sm.repository.Update(ctx, *cert); err != nil { + return fmt.Errorf("保存回滚状态失败: %w", err) + } + + sm.logger.Info("状态转换回滚成功", + zap.String("certification_id", cert.ID), + zap.String("rollback_to_status", string(originalStatus))) + + return nil +} + +// createFailureResult 创建失败结果 +func (sm *CertificationStateMachine) createFailureResult( + oldStatus, targetStatus enums.CertificationStatus, + message string, +) *StateTransitionResult { + return &StateTransitionResult{ + Success: false, + OldStatus: oldStatus, + NewStatus: targetStatus, + Message: message, + TransitionedAt: time.Now(), + Events: []interface{}{}, + } +} + +// ================ 状态转换快捷方法 ================ + +// TransitionToInfoSubmitted 转换到已提交企业信息状态 +func (sm *CertificationStateMachine) TransitionToInfoSubmitted( + ctx context.Context, + certificationID string, + actor enums.ActorType, + actorID string, + enterpriseInfo interface{}, +) (*StateTransitionResult, error) { + req := &StateTransitionRequest{ + CertificationID: certificationID, + TargetStatus: enums.StatusInfoSubmitted, + Actor: actor, + ActorID: actorID, + Reason: "用户提交企业信息", + Context: map[string]interface{}{ + "enterprise_info": enterpriseInfo, + }, + AllowRollback: true, + } + + return sm.ExecuteTransition(ctx, req) +} + +// TransitionToEnterpriseVerified 转换到已企业认证状态 +func (sm *CertificationStateMachine) TransitionToEnterpriseVerified( + ctx context.Context, + certificationID string, + authFlowID string, +) (*StateTransitionResult, error) { + req := &StateTransitionRequest{ + CertificationID: certificationID, + TargetStatus: enums.StatusEnterpriseVerified, + Actor: enums.ActorTypeEsign, + ActorID: "esign_system", + Reason: "e签宝企业认证成功", + Context: map[string]interface{}{ + "auth_flow_id": authFlowID, + }, + AllowRollback: false, + } + + return sm.ExecuteTransition(ctx, req) +} + +// TransitionToInfoRejected 转换到企业信息被拒绝状态 +func (sm *CertificationStateMachine) TransitionToInfoRejected( + ctx context.Context, + certificationID string, + failureReason enums.FailureReason, + failureMessage string, +) (*StateTransitionResult, error) { + req := &StateTransitionRequest{ + CertificationID: certificationID, + TargetStatus: enums.StatusInfoRejected, + Actor: enums.ActorTypeEsign, + ActorID: "esign_system", + Reason: "e签宝企业认证失败", + Context: map[string]interface{}{ + "failure_reason": failureReason, + "failure_message": failureMessage, + }, + AllowRollback: false, + } + + return sm.ExecuteTransition(ctx, req) +} + +// TransitionToContractApplied 转换到已申请合同状态 +func (sm *CertificationStateMachine) TransitionToContractApplied( + ctx context.Context, + certificationID string, + actor enums.ActorType, + actorID string, +) (*StateTransitionResult, error) { + req := &StateTransitionRequest{ + CertificationID: certificationID, + TargetStatus: enums.StatusContractApplied, + Actor: actor, + ActorID: actorID, + Reason: "用户申请合同签署", + Context: map[string]interface{}{}, + AllowRollback: true, + } + + return sm.ExecuteTransition(ctx, req) +} + +// TransitionToContractSigned 转换到已签署合同状态(认证完成) +func (sm *CertificationStateMachine) TransitionToContractSigned( + ctx context.Context, + certificationID string, + contractURL string, +) (*StateTransitionResult, error) { + req := &StateTransitionRequest{ + CertificationID: certificationID, + TargetStatus: enums.StatusContractSigned, + Actor: enums.ActorTypeEsign, + ActorID: "esign_system", + Reason: "e签宝合同签署成功,认证完成", + Context: map[string]interface{}{ + "contract_url": contractURL, + }, + AllowRollback: false, + } + + return sm.ExecuteTransition(ctx, req) +} + +// TransitionToContractRejected 转换到合同被拒签状态 +func (sm *CertificationStateMachine) TransitionToContractRejected( + ctx context.Context, + certificationID string, + failureReason enums.FailureReason, + failureMessage string, +) (*StateTransitionResult, error) { + req := &StateTransitionRequest{ + CertificationID: certificationID, + TargetStatus: enums.StatusContractRejected, + Actor: enums.ActorTypeEsign, + ActorID: "esign_system", + Reason: "合同签署失败", + Context: map[string]interface{}{ + "failure_reason": failureReason, + "failure_message": failureMessage, + }, + AllowRollback: false, + } + + return sm.ExecuteTransition(ctx, req) +} + +// TransitionToContractExpired 转换到合同签署超时状态 +func (sm *CertificationStateMachine) TransitionToContractExpired( + ctx context.Context, + certificationID string, + failureMessage string, +) (*StateTransitionResult, error) { + req := &StateTransitionRequest{ + CertificationID: certificationID, + TargetStatus: enums.StatusContractExpired, + Actor: enums.ActorTypeSystem, + ActorID: "timeout_monitor", + Reason: "合同签署超时", + Context: map[string]interface{}{ + "failure_reason": enums.FailureReasonContractExpired, + "failure_message": failureMessage, + }, + AllowRollback: false, + } + + return sm.ExecuteTransition(ctx, req) +} \ No newline at end of file diff --git a/internal/domains/certification/services/state_machine/esign_callback_handler.go b/internal/domains/certification/services/state_machine/esign_callback_handler.go new file mode 100644 index 0000000..b3084d1 --- /dev/null +++ b/internal/domains/certification/services/state_machine/esign_callback_handler.go @@ -0,0 +1,391 @@ +package state_machine + +import ( + "context" + "encoding/json" + "fmt" + + "tyapi-server/internal/domains/certification/enums" + + "go.uber.org/zap" +) + +// EsignCallbackData e签宝回调数据结构 +type EsignCallbackData struct { + // 基础信息 + Action string `json:"action"` // 回调动作类型 + FlowID string `json:"flow_id"` // 流程ID + AccountID string `json:"account_id"` // 账户ID + Status string `json:"status"` // 状态 + Message string `json:"message"` // 消息 + Timestamp int64 `json:"timestamp"` // 时间戳 + + // 扩展数据 + Data map[string]interface{} `json:"data,omitempty"` // 扩展数据 + OriginalData string `json:"original_data"` // 原始回调数据 +} + +// EsignCallbackHandler e签宝回调处理器 +// 负责处理e签宝的异步回调,将外部回调转换为内部状态转换 +type EsignCallbackHandler struct { + stateMachine *CertificationStateMachine + logger *zap.Logger +} + +// NewEsignCallbackHandler 创建e签宝回调处理器 +func NewEsignCallbackHandler( + stateMachine *CertificationStateMachine, + logger *zap.Logger, +) *EsignCallbackHandler { + return &EsignCallbackHandler{ + stateMachine: stateMachine, + logger: logger, + } +} + +// HandleCallback 处理e签宝回调 +func (h *EsignCallbackHandler) HandleCallback( + ctx context.Context, + certificationID string, + callbackData *EsignCallbackData, +) error { + h.logger.Info("接收到e签宝回调", + zap.String("certification_id", certificationID), + zap.String("action", callbackData.Action), + zap.String("flow_id", callbackData.FlowID), + zap.String("status", callbackData.Status)) + + // 根据动作类型分发处理 + switch callbackData.Action { + case "auth_result": + return h.handleAuthResult(ctx, certificationID, callbackData) + case "sign_result": + return h.handleSignResult(ctx, certificationID, callbackData) + case "flow_status": + return h.handleFlowStatus(ctx, certificationID, callbackData) + default: + h.logger.Warn("未知的e签宝回调动作", + zap.String("certification_id", certificationID), + zap.String("action", callbackData.Action)) + return fmt.Errorf("未知的回调动作: %s", callbackData.Action) + } +} + +// handleAuthResult 处理企业认证结果回调 +func (h *EsignCallbackHandler) handleAuthResult( + ctx context.Context, + certificationID string, + callbackData *EsignCallbackData, +) error { + h.logger.Info("处理企业认证结果回调", + zap.String("certification_id", certificationID), + zap.String("flow_id", callbackData.FlowID), + zap.String("status", callbackData.Status)) + + switch callbackData.Status { + case "success", "verified", "completed": + // 认证成功 + _, err := h.stateMachine.TransitionToEnterpriseVerified( + ctx, + certificationID, + callbackData.FlowID, + ) + return err + + case "failed", "rejected", "error": + // 认证失败 + failureReason := h.parseAuthFailureReason(callbackData) + _, err := h.stateMachine.TransitionToInfoRejected( + ctx, + certificationID, + failureReason, + callbackData.Message, + ) + return err + + default: + h.logger.Warn("未知的企业认证状态", + zap.String("certification_id", certificationID), + zap.String("status", callbackData.Status)) + return fmt.Errorf("未知的认证状态: %s", callbackData.Status) + } +} + +// handleSignResult 处理合同签署结果回调 +func (h *EsignCallbackHandler) handleSignResult( + ctx context.Context, + certificationID string, + callbackData *EsignCallbackData, +) error { + h.logger.Info("处理合同签署结果回调", + zap.String("certification_id", certificationID), + zap.String("flow_id", callbackData.FlowID), + zap.String("status", callbackData.Status)) + + switch callbackData.Status { + case "signed", "completed", "success": + // 签署成功 + contractURL := h.extractContractURL(callbackData) + _, err := h.stateMachine.TransitionToContractSigned( + ctx, + certificationID, + contractURL, + ) + return err + + case "rejected", "refused": + // 用户拒绝签署 + _, err := h.stateMachine.TransitionToContractRejected( + ctx, + certificationID, + enums.FailureReasonContractRejectedByUser, + callbackData.Message, + ) + return err + + case "expired", "timeout": + // 签署超时 + _, err := h.stateMachine.TransitionToContractExpired( + ctx, + certificationID, + fmt.Sprintf("合同签署超时: %s", callbackData.Message), + ) + return err + + case "failed", "error": + // 签署失败 + failureReason := h.parseSignFailureReason(callbackData) + _, err := h.stateMachine.TransitionToContractRejected( + ctx, + certificationID, + failureReason, + callbackData.Message, + ) + return err + + default: + h.logger.Warn("未知的合同签署状态", + zap.String("certification_id", certificationID), + zap.String("status", callbackData.Status)) + return fmt.Errorf("未知的签署状态: %s", callbackData.Status) + } +} + +// handleFlowStatus 处理流程状态回调 +func (h *EsignCallbackHandler) handleFlowStatus( + ctx context.Context, + certificationID string, + callbackData *EsignCallbackData, +) error { + h.logger.Info("处理流程状态回调", + zap.String("certification_id", certificationID), + zap.String("flow_id", callbackData.FlowID), + zap.String("status", callbackData.Status)) + + // 流程状态回调主要用于监控和日志记录 + // 实际的状态转换由具体的auth_result和sign_result处理 + + switch callbackData.Status { + case "started", "processing", "in_progress": + h.logger.Info("e签宝流程进行中", + zap.String("certification_id", certificationID), + zap.String("flow_id", callbackData.FlowID)) + + case "paused", "suspended": + h.logger.Warn("e签宝流程被暂停", + zap.String("certification_id", certificationID), + zap.String("flow_id", callbackData.FlowID), + zap.String("message", callbackData.Message)) + + case "cancelled", "terminated": + h.logger.Warn("e签宝流程被取消", + zap.String("certification_id", certificationID), + zap.String("flow_id", callbackData.FlowID), + zap.String("message", callbackData.Message)) + + default: + h.logger.Info("收到其他流程状态", + zap.String("certification_id", certificationID), + zap.String("status", callbackData.Status)) + } + + return nil +} + +// parseAuthFailureReason 解析企业认证失败原因 +func (h *EsignCallbackHandler) parseAuthFailureReason(callbackData *EsignCallbackData) enums.FailureReason { + // 根据e签宝返回的错误信息解析失败原因 + message := callbackData.Message + + // 检查扩展数据中的错误码 + if errorCode, exists := callbackData.Data["error_code"]; exists { + switch errorCode { + case "ENTERPRISE_NOT_FOUND", "ORG_NOT_EXISTS": + return enums.FailureReasonEnterpriseNotExists + case "INFO_MISMATCH", "ORG_INFO_ERROR": + return enums.FailureReasonEnterpriseInfoMismatch + case "STATUS_ABNORMAL", "ORG_STATUS_ERROR": + return enums.FailureReasonEnterpriseStatusAbnormal + case "LEGAL_PERSON_ERROR", "LEGAL_REP_ERROR": + return enums.FailureReasonLegalPersonMismatch + case "DOCUMENT_INVALID", "ID_CARD_ERROR": + return enums.FailureReasonInvalidDocument + } + } + + // 根据错误消息文本判断 + if message != "" { + if h.containsKeywords(message, []string{"企业不存在", "机构不存在", "not found"}) { + return enums.FailureReasonEnterpriseNotExists + } + if h.containsKeywords(message, []string{"信息不匹配", "信息错误", "mismatch"}) { + return enums.FailureReasonEnterpriseInfoMismatch + } + if h.containsKeywords(message, []string{"状态异常", "status abnormal"}) { + return enums.FailureReasonEnterpriseStatusAbnormal + } + if h.containsKeywords(message, []string{"法定代表人", "legal person", "法人"}) { + return enums.FailureReasonLegalPersonMismatch + } + if h.containsKeywords(message, []string{"证件", "身份证", "document", "id card"}) { + return enums.FailureReasonInvalidDocument + } + } + + // 默认返回e签宝验证失败 + return enums.FailureReasonEsignVerificationFailed +} + +// parseSignFailureReason 解析合同签署失败原因 +func (h *EsignCallbackHandler) parseSignFailureReason(callbackData *EsignCallbackData) enums.FailureReason { + // 根据e签宝返回的错误信息解析失败原因 + message := callbackData.Message + + // 检查扩展数据中的错误码 + if errorCode, exists := callbackData.Data["error_code"]; exists { + switch errorCode { + case "USER_REJECTED", "SIGN_REJECTED": + return enums.FailureReasonContractRejectedByUser + case "FLOW_EXPIRED", "SIGN_EXPIRED": + return enums.FailureReasonContractExpired + case "FLOW_ERROR", "SIGN_PROCESS_ERROR": + return enums.FailureReasonSignProcessFailed + case "ESIGN_ERROR", "SYSTEM_ERROR": + return enums.FailureReasonEsignFlowError + } + } + + // 根据错误消息文本判断 + if message != "" { + if h.containsKeywords(message, []string{"拒绝", "rejected", "refused"}) { + return enums.FailureReasonContractRejectedByUser + } + if h.containsKeywords(message, []string{"过期", "超时", "expired", "timeout"}) { + return enums.FailureReasonContractExpired + } + if h.containsKeywords(message, []string{"流程错误", "process error", "flow error"}) { + return enums.FailureReasonSignProcessFailed + } + } + + // 默认返回e签宝流程错误 + return enums.FailureReasonEsignFlowError +} + +// extractContractURL 提取合同URL +func (h *EsignCallbackHandler) extractContractURL(callbackData *EsignCallbackData) string { + // 优先从扩展数据中获取 + if contractURL, exists := callbackData.Data["contract_url"]; exists { + if url, ok := contractURL.(string); ok && url != "" { + return url + } + } + + if downloadURL, exists := callbackData.Data["download_url"]; exists { + if url, ok := downloadURL.(string); ok && url != "" { + return url + } + } + + if fileURL, exists := callbackData.Data["file_url"]; exists { + if url, ok := fileURL.(string); ok && url != "" { + return url + } + } + + // 如果没有找到URL,返回空字符串 + h.logger.Warn("未能从回调数据中提取合同URL", + zap.Any("callback_data", callbackData.Data)) + + return "" +} + +// containsKeywords 检查文本是否包含关键词 +func (h *EsignCallbackHandler) containsKeywords(text string, keywords []string) bool { + for _, keyword := range keywords { + if len(text) >= len(keyword) { + for i := 0; i <= len(text)-len(keyword); i++ { + if text[i:i+len(keyword)] == keyword { + return true + } + } + } + } + return false +} + +// ValidateCallbackData 验证回调数据 +func (h *EsignCallbackHandler) ValidateCallbackData(callbackData *EsignCallbackData) error { + if callbackData == nil { + return fmt.Errorf("回调数据不能为空") + } + + if callbackData.Action == "" { + return fmt.Errorf("回调动作不能为空") + } + + if callbackData.FlowID == "" { + return fmt.Errorf("流程ID不能为空") + } + + if callbackData.Status == "" { + return fmt.Errorf("状态不能为空") + } + + return nil +} + +// ParseCallbackData 解析原始回调数据 +func (h *EsignCallbackHandler) ParseCallbackData(rawData string) (*EsignCallbackData, error) { + var callbackData EsignCallbackData + + if err := json.Unmarshal([]byte(rawData), &callbackData); err != nil { + h.logger.Error("解析e签宝回调数据失败", zap.Error(err), zap.String("raw_data", rawData)) + return nil, fmt.Errorf("解析回调数据失败: %w", err) + } + + // 保存原始数据 + callbackData.OriginalData = rawData + + // 验证数据完整性 + if err := h.ValidateCallbackData(&callbackData); err != nil { + return nil, fmt.Errorf("回调数据验证失败: %w", err) + } + + return &callbackData, nil +} + +// GetCallbackType 获取回调类型描述 +func (h *EsignCallbackHandler) GetCallbackType(action string) string { + types := map[string]string{ + "auth_result": "企业认证结果", + "sign_result": "合同签署结果", + "flow_status": "流程状态更新", + } + + if typeName, exists := types[action]; exists { + return typeName + } + + return "未知类型" +} \ No newline at end of file diff --git a/internal/domains/certification/services/state_machine/state_config.go b/internal/domains/certification/services/state_machine/state_config.go new file mode 100644 index 0000000..4b94f06 --- /dev/null +++ b/internal/domains/certification/services/state_machine/state_config.go @@ -0,0 +1,438 @@ +package state_machine + +import ( + "fmt" + "tyapi-server/internal/domains/certification/enums" +) + +// StateConfig 状态配置 +type StateConfig struct { + Status enums.CertificationStatus `json:"status"` + Name string `json:"name"` + ProgressPercentage int `json:"progress_percentage"` + IsUserActionRequired bool `json:"is_user_action_required"` + IsSystemAction bool `json:"is_system_action"` + TimestampField string `json:"timestamp_field,omitempty"` + Description string `json:"description"` + NextValidStatuses []enums.CertificationStatus `json:"next_valid_statuses"` + AllowedActors []enums.ActorType `json:"allowed_actors"` +} + +// StateTransitionRule 状态转换规则 +type StateTransitionRule struct { + FromStatus enums.CertificationStatus `json:"from_status"` + ToStatus enums.CertificationStatus `json:"to_status"` + TransitionName string `json:"transition_name"` + AllowedActors []enums.ActorType `json:"allowed_actors"` + RequiresValidation bool `json:"requires_validation"` + Description string `json:"description"` + BusinessRules []string `json:"business_rules"` +} + +// StateConfigManager 状态配置管理器 +type StateConfigManager struct { + stateConfigs map[enums.CertificationStatus]*StateConfig + transitionRules map[string]*StateTransitionRule // key: "from_status->to_status" + actorPermissions map[enums.ActorType][]string // actor允许的操作 +} + +// NewStateConfigManager 创建状态配置管理器 +func NewStateConfigManager() *StateConfigManager { + manager := &StateConfigManager{ + stateConfigs: make(map[enums.CertificationStatus]*StateConfig), + transitionRules: make(map[string]*StateTransitionRule), + actorPermissions: make(map[enums.ActorType][]string), + } + + manager.initializeStateConfigs() + manager.initializeTransitionRules() + manager.initializeActorPermissions() + + return manager +} + +// initializeStateConfigs 初始化状态配置 +func (m *StateConfigManager) initializeStateConfigs() { + configs := []*StateConfig{ + { + Status: enums.StatusPending, + Name: "待认证", + ProgressPercentage: 0, + IsUserActionRequired: true, + IsSystemAction: false, + Description: "等待用户提交企业信息", + NextValidStatuses: []enums.CertificationStatus{enums.StatusInfoSubmitted}, + AllowedActors: []enums.ActorType{enums.ActorTypeUser}, + }, + { + Status: enums.StatusInfoSubmitted, + Name: "已提交企业信息", + ProgressPercentage: 25, + IsUserActionRequired: false, + IsSystemAction: true, + TimestampField: "InfoSubmittedAt", + Description: "企业信息已提交,等待e签宝验证", + NextValidStatuses: []enums.CertificationStatus{enums.StatusEnterpriseVerified, enums.StatusInfoRejected}, + AllowedActors: []enums.ActorType{enums.ActorTypeEsign, enums.ActorTypeSystem}, + }, + { + Status: enums.StatusEnterpriseVerified, + Name: "已企业认证", + ProgressPercentage: 50, + IsUserActionRequired: true, + IsSystemAction: false, + TimestampField: "EnterpriseVerifiedAt", + Description: "企业认证完成,用户可申请合同", + NextValidStatuses: []enums.CertificationStatus{enums.StatusContractApplied}, + AllowedActors: []enums.ActorType{enums.ActorTypeUser}, + }, + { + Status: enums.StatusContractApplied, + Name: "已申请签署合同", + ProgressPercentage: 75, + IsUserActionRequired: true, + IsSystemAction: true, + TimestampField: "ContractAppliedAt", + Description: "合同已生成,等待用户签署", + NextValidStatuses: []enums.CertificationStatus{enums.StatusContractSigned, enums.StatusContractRejected, enums.StatusContractExpired}, + AllowedActors: []enums.ActorType{enums.ActorTypeEsign, enums.ActorTypeSystem}, + }, + { + Status: enums.StatusContractSigned, + Name: "认证完成", + ProgressPercentage: 100, + IsUserActionRequired: false, + IsSystemAction: false, + TimestampField: "ContractSignedAt", + Description: "认证流程已完成", + NextValidStatuses: []enums.CertificationStatus{}, + AllowedActors: []enums.ActorType{}, + }, + // 失败状态 + { + Status: enums.StatusInfoRejected, + Name: "企业信息被拒绝", + ProgressPercentage: 25, + IsUserActionRequired: true, + IsSystemAction: false, + Description: "企业信息验证失败,需要重新提交", + NextValidStatuses: []enums.CertificationStatus{enums.StatusInfoSubmitted}, + AllowedActors: []enums.ActorType{enums.ActorTypeUser}, + }, + { + Status: enums.StatusContractRejected, + Name: "合同被拒签", + ProgressPercentage: 75, + IsUserActionRequired: true, + IsSystemAction: false, + Description: "用户拒绝签署合同", + NextValidStatuses: []enums.CertificationStatus{enums.StatusEnterpriseVerified}, + AllowedActors: []enums.ActorType{enums.ActorTypeUser, enums.ActorTypeSystem}, + }, + { + Status: enums.StatusContractExpired, + Name: "合同签署超时", + ProgressPercentage: 75, + IsUserActionRequired: true, + IsSystemAction: false, + Description: "合同签署链接已过期", + NextValidStatuses: []enums.CertificationStatus{enums.StatusEnterpriseVerified}, + AllowedActors: []enums.ActorType{enums.ActorTypeUser, enums.ActorTypeSystem}, + }, + } + + for _, config := range configs { + m.stateConfigs[config.Status] = config + } +} + +// initializeTransitionRules 初始化状态转换规则 +func (m *StateConfigManager) initializeTransitionRules() { + rules := []*StateTransitionRule{ + // 用户提交企业信息 + { + FromStatus: enums.StatusPending, + ToStatus: enums.StatusInfoSubmitted, + TransitionName: "submit_enterprise_info", + AllowedActors: []enums.ActorType{enums.ActorTypeUser}, + RequiresValidation: true, + Description: "用户提交企业信息", + BusinessRules: []string{"enterprise_info_complete", "enterprise_info_valid"}, + }, + // e签宝企业认证成功 + { + FromStatus: enums.StatusInfoSubmitted, + ToStatus: enums.StatusEnterpriseVerified, + TransitionName: "enterprise_verification_success", + AllowedActors: []enums.ActorType{enums.ActorTypeEsign, enums.ActorTypeSystem}, + RequiresValidation: false, + Description: "e签宝企业认证成功", + BusinessRules: []string{"auth_flow_id_exists"}, + }, + // e签宝企业认证失败 + { + FromStatus: enums.StatusInfoSubmitted, + ToStatus: enums.StatusInfoRejected, + TransitionName: "enterprise_verification_failed", + AllowedActors: []enums.ActorType{enums.ActorTypeEsign, enums.ActorTypeSystem}, + RequiresValidation: false, + Description: "e签宝企业认证失败", + BusinessRules: []string{"failure_reason_provided"}, + }, + // 用户申请合同 + { + FromStatus: enums.StatusEnterpriseVerified, + ToStatus: enums.StatusContractApplied, + TransitionName: "apply_contract", + AllowedActors: []enums.ActorType{enums.ActorTypeUser}, + RequiresValidation: true, + Description: "用户申请合同签署", + BusinessRules: []string{"enterprise_verified", "auth_flow_id_exists"}, + }, + // e签宝合同签署成功 + { + FromStatus: enums.StatusContractApplied, + ToStatus: enums.StatusContractSigned, + TransitionName: "contract_sign_success", + AllowedActors: []enums.ActorType{enums.ActorTypeEsign, enums.ActorTypeSystem}, + RequiresValidation: false, + Description: "e签宝合同签署成功", + BusinessRules: []string{"contract_info_complete"}, + }, + // 合同签署失败 + { + FromStatus: enums.StatusContractApplied, + ToStatus: enums.StatusContractRejected, + TransitionName: "contract_sign_rejected", + AllowedActors: []enums.ActorType{enums.ActorTypeEsign, enums.ActorTypeSystem}, + RequiresValidation: false, + Description: "用户拒绝签署合同", + BusinessRules: []string{"failure_reason_provided"}, + }, + // 合同签署超时 + { + FromStatus: enums.StatusContractApplied, + ToStatus: enums.StatusContractExpired, + TransitionName: "contract_sign_expired", + AllowedActors: []enums.ActorType{enums.ActorTypeEsign, enums.ActorTypeSystem}, + RequiresValidation: false, + Description: "合同签署超时", + BusinessRules: []string{"failure_reason_provided"}, + }, + // 重新提交企业信息 + { + FromStatus: enums.StatusInfoRejected, + ToStatus: enums.StatusInfoSubmitted, + TransitionName: "resubmit_enterprise_info", + AllowedActors: []enums.ActorType{enums.ActorTypeUser}, + RequiresValidation: true, + Description: "用户重新提交企业信息", + BusinessRules: []string{"enterprise_info_complete", "enterprise_info_valid", "retry_limit_check"}, + }, + // 从合同失败状态恢复 + { + FromStatus: enums.StatusContractRejected, + ToStatus: enums.StatusEnterpriseVerified, + TransitionName: "reset_from_contract_rejected", + AllowedActors: []enums.ActorType{enums.ActorTypeSystem, enums.ActorTypeUser}, + RequiresValidation: false, + Description: "从合同拒签状态恢复", + BusinessRules: []string{"retry_limit_check"}, + }, + { + FromStatus: enums.StatusContractExpired, + ToStatus: enums.StatusEnterpriseVerified, + TransitionName: "reset_from_contract_expired", + AllowedActors: []enums.ActorType{enums.ActorTypeSystem, enums.ActorTypeUser}, + RequiresValidation: false, + Description: "从合同超时状态恢复", + BusinessRules: []string{"retry_limit_check"}, + }, + } + + for _, rule := range rules { + key := string(rule.FromStatus) + "->" + string(rule.ToStatus) + m.transitionRules[key] = rule + } +} + +// initializeActorPermissions 初始化操作者权限 +func (m *StateConfigManager) initializeActorPermissions() { + m.actorPermissions = map[enums.ActorType][]string{ + enums.ActorTypeUser: { + "submit_enterprise_info", + "apply_contract", + "view_certification", + "retry_from_failure", + }, + enums.ActorTypeSystem: { + "auto_transition", + "system_recovery", + "timeout_handling", + "data_cleanup", + }, + enums.ActorTypeEsign: { + "verification_callback", + "sign_callback", + "status_notification", + }, + enums.ActorTypeAdmin: { + "manual_intervention", + "force_transition", + "view_all_certifications", + "system_configuration", + }, + } +} + +// GetStateConfig 获取状态配置 +func (m *StateConfigManager) GetStateConfig(status enums.CertificationStatus) *StateConfig { + return m.stateConfigs[status] +} + +// GetTransitionRule 获取状态转换规则 +func (m *StateConfigManager) GetTransitionRule(fromStatus, toStatus enums.CertificationStatus) *StateTransitionRule { + key := string(fromStatus) + "->" + string(toStatus) + return m.transitionRules[key] +} + +// CanTransition 检查是否可以执行状态转换 +func (m *StateConfigManager) CanTransition(fromStatus, toStatus enums.CertificationStatus, actor enums.ActorType) (bool, string) { + // 获取转换规则 + rule := m.GetTransitionRule(fromStatus, toStatus) + if rule == nil { + return false, "不支持的状态转换" + } + + // 检查操作者权限 + allowed := false + for _, allowedActor := range rule.AllowedActors { + if actor == allowedActor { + allowed = true + break + } + } + + if !allowed { + return false, "操作者无权限执行此转换" + } + + return true, "" +} + +// GetAllowedTransitions 获取指定状态下允许的转换 +func (m *StateConfigManager) GetAllowedTransitions(fromStatus enums.CertificationStatus, actor enums.ActorType) []*StateTransitionRule { + var allowedTransitions []*StateTransitionRule + + config := m.GetStateConfig(fromStatus) + if config == nil { + return allowedTransitions + } + + for _, toStatus := range config.NextValidStatuses { + if canTransition, _ := m.CanTransition(fromStatus, toStatus, actor); canTransition { + if rule := m.GetTransitionRule(fromStatus, toStatus); rule != nil { + allowedTransitions = append(allowedTransitions, rule) + } + } + } + + return allowedTransitions +} + +// GetActorPermissions 获取操作者权限 +func (m *StateConfigManager) GetActorPermissions(actor enums.ActorType) []string { + if permissions, exists := m.actorPermissions[actor]; exists { + return permissions + } + return []string{} +} + +// HasPermission 检查操作者是否有指定权限 +func (m *StateConfigManager) HasPermission(actor enums.ActorType, permission string) bool { + permissions := m.GetActorPermissions(actor) + for _, p := range permissions { + if p == permission { + return true + } + } + return false +} + +// ValidateBusinessRules 验证业务规则 +func (m *StateConfigManager) ValidateBusinessRules(rule *StateTransitionRule, context map[string]interface{}) error { + for _, businessRule := range rule.BusinessRules { + if err := m.validateSingleBusinessRule(businessRule, context); err != nil { + return err + } + } + return nil +} + +// validateSingleBusinessRule 验证单个业务规则 +func (m *StateConfigManager) validateSingleBusinessRule(ruleName string, context map[string]interface{}) error { + switch ruleName { + case "enterprise_info_complete": + if enterpriseInfo, exists := context["enterprise_info"]; !exists || enterpriseInfo == nil { + return fmt.Errorf("企业信息不能为空") + } + case "enterprise_info_valid": + // 这里可以添加更复杂的企业信息验证逻辑 + return nil + case "auth_flow_id_exists": + if authFlowID, exists := context["auth_flow_id"]; !exists || authFlowID == "" { + return fmt.Errorf("认证流程ID不能为空") + } + case "failure_reason_provided": + if reason, exists := context["failure_reason"]; !exists || reason == "" { + return fmt.Errorf("失败原因不能为空") + } + case "enterprise_verified": + if status, exists := context["current_status"]; !exists || status != string(enums.StatusEnterpriseVerified) { + return fmt.Errorf("企业必须先完成认证") + } + case "contract_info_complete": + if contractInfo, exists := context["contract_info"]; !exists || contractInfo == nil { + return fmt.Errorf("合同信息不能为空") + } + case "retry_limit_check": + if retryCount, exists := context["retry_count"]; exists { + if count, ok := retryCount.(int); ok && count >= 3 { + return fmt.Errorf("已达到最大重试次数限制") + } + } + } + return nil +} + +// GetStateProgress 获取状态进度信息 +func (m *StateConfigManager) GetStateProgress(status enums.CertificationStatus) int { + if config := m.GetStateConfig(status); config != nil { + return config.ProgressPercentage + } + return 0 +} + +// IsUserActionRequired 检查是否需要用户操作 +func (m *StateConfigManager) IsUserActionRequired(status enums.CertificationStatus) bool { + if config := m.GetStateConfig(status); config != nil { + return config.IsUserActionRequired + } + return false +} + +// IsSystemAction 检查是否为系统操作状态 +func (m *StateConfigManager) IsSystemAction(status enums.CertificationStatus) bool { + if config := m.GetStateConfig(status); config != nil { + return config.IsSystemAction + } + return false +} + +// GetTimestampField 获取状态对应的时间戳字段 +func (m *StateConfigManager) GetTimestampField(status enums.CertificationStatus) string { + if config := m.GetStateConfig(status); config != nil { + return config.TimestampField + } + return "" +} \ No newline at end of file diff --git a/internal/domains/certification/value_objects/enterprise_info.go b/internal/domains/certification/value_objects/enterprise_info.go new file mode 100644 index 0000000..99f50e6 --- /dev/null +++ b/internal/domains/certification/value_objects/enterprise_info.go @@ -0,0 +1,181 @@ +package value_objects + +import ( + "fmt" + "regexp" + "strings" +) + +// EnterpriseInfo 企业信息值对象 +// 负责封装和验证企业认证相关的信息 +type EnterpriseInfo struct { + CompanyName string `json:"company_name" validate:"required,min=2,max=100"` + UnifiedSocialCode string `json:"unified_social_code" validate:"required,len=18"` + LegalPersonName string `json:"legal_person_name" validate:"required,min=2,max=50"` + LegalPersonID string `json:"legal_person_id" validate:"required,len=18"` + LegalPersonPhone string `json:"legal_person_phone" validate:"required,mobile"` +} + +// NewEnterpriseInfo 创建企业信息值对象 +func NewEnterpriseInfo( + companyName, unifiedSocialCode, legalPersonName, legalPersonID, legalPersonPhone string, +) (*EnterpriseInfo, error) { + // 清理输入数据 + info := &EnterpriseInfo{ + CompanyName: strings.TrimSpace(companyName), + UnifiedSocialCode: strings.TrimSpace(unifiedSocialCode), + LegalPersonName: strings.TrimSpace(legalPersonName), + LegalPersonID: strings.TrimSpace(legalPersonID), + LegalPersonPhone: strings.TrimSpace(legalPersonPhone), + } + + // 验证数据 + if err := info.Validate(); err != nil { + return nil, err + } + + return info, nil +} + +// Validate 验证企业信息 +func (e *EnterpriseInfo) Validate() error { + // 验证公司名称 + if e.CompanyName == "" { + return fmt.Errorf("公司名称不能为空") + } + if len(e.CompanyName) < 2 || len(e.CompanyName) > 100 { + return fmt.Errorf("公司名称长度应在2-100个字符之间") + } + + // 验证统一社会信用代码 + if err := e.validateUnifiedSocialCode(); err != nil { + return err + } + + // 验证法人姓名 + if e.LegalPersonName == "" { + return fmt.Errorf("法人姓名不能为空") + } + if len(e.LegalPersonName) < 2 || len(e.LegalPersonName) > 50 { + return fmt.Errorf("法人姓名长度应在2-50个字符之间") + } + + // 验证法人身份证 + if err := e.validateLegalPersonID(); err != nil { + return err + } + + // 验证法人手机号 + if err := e.validateLegalPersonPhone(); err != nil { + return err + } + + return nil +} + +// validateUnifiedSocialCode 验证统一社会信用代码 +func (e *EnterpriseInfo) validateUnifiedSocialCode() error { + if e.UnifiedSocialCode == "" { + return fmt.Errorf("统一社会信用代码不能为空") + } + + if len(e.UnifiedSocialCode) != 18 { + return fmt.Errorf("统一社会信用代码应为18位") + } + + // 检查格式:应为数字和大写字母组合 + matched, _ := regexp.MatchString(`^[0-9A-Z]{18}$`, e.UnifiedSocialCode) + if !matched { + return fmt.Errorf("统一社会信用代码格式不正确") + } + + // TODO: 可以添加更严格的校验算法 + + return nil +} + +// validateLegalPersonID 验证法人身份证号 +func (e *EnterpriseInfo) validateLegalPersonID() error { + if e.LegalPersonID == "" { + return fmt.Errorf("法人身份证号不能为空") + } + + if len(e.LegalPersonID) != 18 { + return fmt.Errorf("法人身份证号应为18位") + } + + // 检查格式:前17位为数字,最后一位为数字或X + matched, _ := regexp.MatchString(`^\d{17}[\dX]$`, e.LegalPersonID) + if !matched { + return fmt.Errorf("法人身份证号格式不正确") + } + + // TODO: 可以添加身份证校验算法 + + return nil +} + +// validateLegalPersonPhone 验证法人手机号 +func (e *EnterpriseInfo) validateLegalPersonPhone() error { + if e.LegalPersonPhone == "" { + return fmt.Errorf("法人手机号不能为空") + } + + // 检查中国大陆手机号格式 + matched, _ := regexp.MatchString(`^1[3-9]\d{9}$`, e.LegalPersonPhone) + if !matched { + return fmt.Errorf("法人手机号格式不正确") + } + + return nil +} + +// ================ 业务方法 ================ + +// GetDisplayName 获取显示名称 +func (e *EnterpriseInfo) GetDisplayName() string { + return fmt.Sprintf("%s(%s)", e.CompanyName, e.LegalPersonName) +} + +// IsSame 判断是否为同一企业信息 +func (e *EnterpriseInfo) IsSame(other *EnterpriseInfo) bool { + if other == nil { + return false + } + + return e.UnifiedSocialCode == other.UnifiedSocialCode && + e.LegalPersonID == other.LegalPersonID +} + +// GetMaskedPhone 获取脱敏手机号 +func (e *EnterpriseInfo) GetMaskedPhone() string { + if len(e.LegalPersonPhone) != 11 { + return e.LegalPersonPhone + } + return e.LegalPersonPhone[:3] + "****" + e.LegalPersonPhone[7:] +} + +// GetMaskedIDNumber 获取脱敏身份证号 +func (e *EnterpriseInfo) GetMaskedIDNumber() string { + if len(e.LegalPersonID) != 18 { + return e.LegalPersonID + } + return e.LegalPersonID[:6] + "******" + e.LegalPersonID[14:] +} + +// ToMap 转换为Map格式 +func (e *EnterpriseInfo) ToMap() map[string]string { + return map[string]string{ + "company_name": e.CompanyName, + "unified_social_code": e.UnifiedSocialCode, + "legal_person_name": e.LegalPersonName, + "legal_person_id": e.LegalPersonID, + "legal_person_phone": e.LegalPersonPhone, + } +} + +// String 字符串表示 +func (e *EnterpriseInfo) String() string { + return fmt.Sprintf("企业信息[公司=%s, 法人=%s, 信用代码=%s]", + e.CompanyName, e.LegalPersonName, e.UnifiedSocialCode) +} \ No newline at end of file diff --git a/internal/infrastructure/database/repositories/certification/gorm_certification_command_repository.go b/internal/infrastructure/database/repositories/certification/gorm_certification_command_repository.go new file mode 100644 index 0000000..40daaaa --- /dev/null +++ b/internal/infrastructure/database/repositories/certification/gorm_certification_command_repository.go @@ -0,0 +1,370 @@ +package certification + +import ( + "context" + "fmt" + "time" + + "go.uber.org/zap" + "gorm.io/gorm" + + "tyapi-server/internal/domains/certification/entities" + "tyapi-server/internal/domains/certification/enums" + "tyapi-server/internal/domains/certification/repositories" + "tyapi-server/internal/shared/database" + "tyapi-server/internal/shared/interfaces" +) + +// ================ 常量定义 ================ + +const ( + // 表名常量 + CertificationsTable = "certifications" + + // 缓存时间常量 + CacheTTLPrimaryQuery = 30 * time.Minute // 主键查询缓存时间 + CacheTTLBusinessQuery = 15 * time.Minute // 业务查询缓存时间 + CacheTTLUserQuery = 10 * time.Minute // 用户相关查询缓存时间 + CacheTTLWarmupLong = 30 * time.Minute // 预热长期缓存 + CacheTTLWarmupMedium = 15 * time.Minute // 预热中期缓存 + + // 缓存键模式常量 + CachePatternTable = "gorm_cache:certifications:*" + CachePatternUser = "certification:user_id:*" +) + +// ================ Repository 实现 ================ + +// GormCertificationCommandRepository 认证命令仓储GORM实现 +// +// 特性说明: +// - 基于 CachedBaseRepositoryImpl 实现自动缓存管理 +// - 支持多级缓存策略(主键查询30分钟,业务查询15分钟) +// - 自动缓存失效:写操作时自动清理相关缓存 +// - 智能缓存选择:根据查询复杂度自动选择缓存策略 +// - 内置监控支持:提供缓存统计和性能监控 +type GormCertificationCommandRepository struct { + *database.CachedBaseRepositoryImpl +} + +// 编译时检查接口实现 +var _ repositories.CertificationCommandRepository = (*GormCertificationCommandRepository)(nil) + +// NewGormCertificationCommandRepository 创建认证命令仓储 +// +// 参数: +// - db: GORM数据库连接实例 +// - logger: 日志记录器 +// +// 返回: +// - repositories.CertificationCommandRepository: 仓储接口实现 +func NewGormCertificationCommandRepository(db *gorm.DB, logger *zap.Logger) repositories.CertificationCommandRepository { + return &GormCertificationCommandRepository{ + CachedBaseRepositoryImpl: database.NewCachedBaseRepositoryImpl(db, logger, CertificationsTable), + } +} + +// ================ 基础CRUD操作 ================ + +// Create 创建认证 +// +// 业务说明: +// - 创建新的认证申请 +// - 自动触发相关缓存失效 +// +// 参数: +// - ctx: 上下文 +// - cert: 认证实体 +// +// 返回: +// - error: 创建失败时的错误信息 +func (r *GormCertificationCommandRepository) Create(ctx context.Context, cert entities.Certification) error { + r.GetLogger().Info("创建认证申请", + zap.String("user_id", cert.UserID), + zap.String("status", string(cert.Status))) + + return r.CreateEntity(ctx, &cert) +} + +// Update 更新认证 +// +// 缓存影响: +// - GORM缓存插件会自动失效相关缓存 +// - 无需手动管理缓存一致性 +// +// 参数: +// - ctx: 上下文 +// - cert: 认证实体 +// +// 返回: +// - error: 更新失败时的错误信息 +func (r *GormCertificationCommandRepository) Update(ctx context.Context, cert entities.Certification) error { + r.GetLogger().Info("更新认证", + zap.String("id", cert.ID), + zap.String("status", string(cert.Status))) + + return r.UpdateEntity(ctx, &cert) +} + +// Delete 删除认证 +// +// 参数: +// - ctx: 上下文 +// - id: 认证ID +// +// 返回: +// - error: 删除失败时的错误信息 +func (r *GormCertificationCommandRepository) Delete(ctx context.Context, id string) error { + r.GetLogger().Info("删除认证", zap.String("id", id)) + return r.DeleteEntity(ctx, id, &entities.Certification{}) +} + +// ================ 业务特定的更新操作 ================ + +// UpdateStatus 更新认证状态 +// +// 业务说明: +// - 更新认证的状态 +// - 自动更新时间戳 +// +// 缓存影响: +// - GORM缓存插件会自动失效表相关的缓存 +// - 状态更新会影响列表查询和统计结果 +// +// 参数: +// - ctx: 上下文 +// - id: 认证ID +// - status: 新状态 +// +// 返回: +// - error: 更新失败时的错误信息 +func (r *GormCertificationCommandRepository) UpdateStatus(ctx context.Context, id string, status enums.CertificationStatus) error { + r.GetLogger().Info("更新认证状态", + zap.String("id", id), + zap.String("status", string(status))) + + updates := map[string]interface{}{ + "status": status, + "updated_at": time.Now(), + } + + return r.GetDB(ctx).Model(&entities.Certification{}). + Where("id = ?", id). + Updates(updates).Error +} + +// UpdateAuthFlowID 更新认证流程ID +// +// 业务说明: +// - 记录e签宝企业认证流程ID +// - 用于回调处理和状态跟踪 +// +// 参数: +// - ctx: 上下文 +// - id: 认证ID +// - authFlowID: 认证流程ID +// +// 返回: +// - error: 更新失败时的错误信息 +func (r *GormCertificationCommandRepository) UpdateAuthFlowID(ctx context.Context, id string, authFlowID string) error { + r.GetLogger().Info("更新认证流程ID", + zap.String("id", id), + zap.String("auth_flow_id", authFlowID)) + + updates := map[string]interface{}{ + "auth_flow_id": authFlowID, + "updated_at": time.Now(), + } + + return r.GetDB(ctx).Model(&entities.Certification{}). + Where("id = ?", id). + Updates(updates).Error +} + +// UpdateContractInfo 更新合同信息 +// +// 业务说明: +// - 记录合同相关的ID和URL信息 +// - 用于合同管理和用户下载 +// +// 参数: +// - ctx: 上下文 +// - id: 认证ID +// - contractFileID: 合同文件ID +// - esignFlowID: e签宝流程ID +// - contractURL: 合同URL +// - contractSignURL: 合同签署URL +// +// 返回: +// - error: 更新失败时的错误信息 +func (r *GormCertificationCommandRepository) UpdateContractInfo(ctx context.Context, id string, contractFileID, esignFlowID, contractURL, contractSignURL string) error { + r.GetLogger().Info("更新合同信息", + zap.String("id", id), + zap.String("contract_file_id", contractFileID), + zap.String("esign_flow_id", esignFlowID)) + + updates := map[string]interface{}{ + "contract_file_id": contractFileID, + "esign_flow_id": esignFlowID, + "contract_url": contractURL, + "contract_sign_url": contractSignURL, + "updated_at": time.Now(), + } + + return r.GetDB(ctx).Model(&entities.Certification{}). + Where("id = ?", id). + Updates(updates).Error +} + +// UpdateFailureInfo 更新失败信息 +// +// 业务说明: +// - 记录认证失败的原因和详细信息 +// - 用于错误分析和用户提示 +// +// 参数: +// - ctx: 上下文 +// - id: 认证ID +// - reason: 失败原因 +// - message: 失败详细信息 +// +// 返回: +// - error: 更新失败时的错误信息 +func (r *GormCertificationCommandRepository) UpdateFailureInfo(ctx context.Context, id string, reason enums.FailureReason, message string) error { + r.GetLogger().Info("更新失败信息", + zap.String("id", id), + zap.String("reason", string(reason)), + zap.String("message", message)) + + updates := map[string]interface{}{ + "failure_reason": reason, + "failure_message": message, + "updated_at": time.Now(), + } + + return r.GetDB(ctx).Model(&entities.Certification{}). + Where("id = ?", id). + Updates(updates).Error +} + +// ================ 批量操作 ================ + +// BatchUpdateStatus 批量更新状态 +// +// 业务说明: +// - 批量更新多个认证的状态 +// - 适用于管理员批量操作 +// +// 参数: +// - ctx: 上下文 +// - ids: 认证ID列表 +// - status: 新状态 +// +// 返回: +// - error: 更新失败时的错误信息 +func (r *GormCertificationCommandRepository) BatchUpdateStatus(ctx context.Context, ids []string, status enums.CertificationStatus) error { + if len(ids) == 0 { + return fmt.Errorf("批量更新状态:ID列表不能为空") + } + + r.GetLogger().Info("批量更新认证状态", + zap.Strings("ids", ids), + zap.String("status", string(status))) + + updates := map[string]interface{}{ + "status": status, + "updated_at": time.Now(), + } + + result := r.GetDB(ctx).Model(&entities.Certification{}). + Where("id IN ?", ids). + Updates(updates) + + if result.Error != nil { + return fmt.Errorf("批量更新认证状态失败: %w", result.Error) + } + + r.GetLogger().Info("批量更新完成", zap.Int64("affected_rows", result.RowsAffected)) + return nil +} + +// ================ 事务支持 ================ + +// WithTx 使用事务 +// +// 业务说明: +// - 返回支持事务的仓储实例 +// - 用于复杂业务操作的事务一致性保证 +// +// 参数: +// - tx: 事务对象 +// +// 返回: +// - repositories.CertificationCommandRepository: 支持事务的仓储实例 +func (r *GormCertificationCommandRepository) WithTx(tx interfaces.Transaction) repositories.CertificationCommandRepository { + // 获取事务的底层*gorm.DB + txDB := tx.GetDB() + if gormDB, ok := txDB.(*gorm.DB); ok { + return &GormCertificationCommandRepository{ + CachedBaseRepositoryImpl: database.NewCachedBaseRepositoryImpl(gormDB, r.GetLogger(), CertificationsTable), + } + } + + r.GetLogger().Warn("不支持的事务类型,返回原始仓储") + return r +} + +// ================ 缓存管理方法 ================ + +// WarmupCache 预热认证缓存 +// +// 业务说明: +// - 系统启动时预热常用查询的缓存 +// - 提升首次访问的响应速度 +// +// 预热策略: +// - 活跃认证:30分钟长期缓存 +// - 最近创建:15分钟中期缓存 +func (r *GormCertificationCommandRepository) WarmupCache(ctx context.Context) error { + r.GetLogger().Info("开始预热认证缓存") + + queries := []database.WarmupQuery{ + { + Name: "active_certifications", + TTL: CacheTTLWarmupLong, + Dest: &[]entities.Certification{}, + }, + { + Name: "recent_certifications", + TTL: CacheTTLWarmupMedium, + Dest: &[]entities.Certification{}, + }, + } + + return r.WarmupCommonQueries(ctx, queries) +} + +// RefreshCache 刷新认证缓存 +// +// 业务说明: +// - 手动刷新认证相关的所有缓存 +// - 适用于数据迁移或批量更新后的缓存清理 +func (r *GormCertificationCommandRepository) RefreshCache(ctx context.Context) error { + r.GetLogger().Info("刷新认证缓存") + return r.CachedBaseRepositoryImpl.RefreshCache(ctx, CachePatternTable) +} + +// GetCacheStats 获取缓存统计信息 +// +// 返回当前Repository的缓存使用统计,包括: +// - 基础缓存信息(命中率、键数量等) +// - 特定的缓存模式列表 +// - 性能指标 +func (r *GormCertificationCommandRepository) GetCacheStats() map[string]interface{} { + stats := r.GetCacheInfo() + stats["specific_patterns"] = []string{ + CachePatternTable, + CachePatternUser, + } + return stats +} \ No newline at end of file diff --git a/internal/infrastructure/database/repositories/certification/gorm_certification_query_repository.go b/internal/infrastructure/database/repositories/certification/gorm_certification_query_repository.go new file mode 100644 index 0000000..7cfe032 --- /dev/null +++ b/internal/infrastructure/database/repositories/certification/gorm_certification_query_repository.go @@ -0,0 +1,514 @@ +package certification + +import ( + "context" + "fmt" + "strings" + "time" + + "tyapi-server/internal/domains/certification/entities" + "tyapi-server/internal/domains/certification/enums" + "tyapi-server/internal/domains/certification/repositories" + "tyapi-server/internal/domains/certification/repositories/queries" + "tyapi-server/internal/shared/database" + + "go.uber.org/zap" + "gorm.io/gorm" +) + +// ================ 常量定义 ================ + +const ( + // 缓存时间常量 + QueryCacheTTLPrimaryQuery = 30 * time.Minute // 主键查询缓存时间 + QueryCacheTTLBusinessQuery = 15 * time.Minute // 业务查询缓存时间 + QueryCacheTTLUserQuery = 10 * time.Minute // 用户相关查询缓存时间 + QueryCacheTTLSearchQuery = 2 * time.Minute // 搜索查询缓存时间 + QueryCacheTTLActiveRecords = 5 * time.Minute // 活跃记录查询缓存时间 + QueryCacheTTLWarmupLong = 30 * time.Minute // 预热长期缓存 + QueryCacheTTLWarmupMedium = 15 * time.Minute // 预热中期缓存 + + // 缓存键模式常量 + QueryCachePatternTable = "gorm_cache:certifications:*" + QueryCachePatternUser = "certification:user_id:*" +) + +// ================ Repository 实现 ================ + +// GormCertificationQueryRepository 认证查询仓储GORM实现 +// +// 特性说明: +// - 基于 CachedBaseRepositoryImpl 实现自动缓存管理 +// - 支持多级缓存策略(主键查询30分钟,业务查询15分钟,搜索2分钟) +// - 自动缓存失效:写操作时自动清理相关缓存 +// - 智能缓存选择:根据查询复杂度自动选择缓存策略 +// - 内置监控支持:提供缓存统计和性能监控 +type GormCertificationQueryRepository struct { + *database.CachedBaseRepositoryImpl +} + +// 编译时检查接口实现 +var _ repositories.CertificationQueryRepository = (*GormCertificationQueryRepository)(nil) + +// NewGormCertificationQueryRepository 创建认证查询仓储 +// +// 参数: +// - db: GORM数据库连接实例 +// - logger: 日志记录器 +// +// 返回: +// - repositories.CertificationQueryRepository: 仓储接口实现 +func NewGormCertificationQueryRepository( + db *gorm.DB, + logger *zap.Logger, +) repositories.CertificationQueryRepository { + return &GormCertificationQueryRepository{ + CachedBaseRepositoryImpl: database.NewCachedBaseRepositoryImpl(db, logger, CertificationsTable), + } +} + +// ================ 基础查询操作 ================ + +// GetByID 根据ID获取认证 +// +// 缓存策略: +// - 使用智能主键查询,自动缓存30分钟 +// - 主键查询命中率高,适合长期缓存 +// +// 参数: +// - ctx: 上下文 +// - id: 认证ID +// +// 返回: +// - *entities.Certification: 查询到的认证,未找到时返回nil +// - error: 查询失败时的错误信息 +func (r *GormCertificationQueryRepository) GetByID(ctx context.Context, id string) (*entities.Certification, error) { + var cert entities.Certification + if err := r.SmartGetByID(ctx, id, &cert); err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("认证记录不存在") + } + return nil, fmt.Errorf("查询认证记录失败: %w", err) + } + return &cert, nil +} + +// GetByUserID 根据用户ID获取认证 +// +// 缓存策略: +// - 业务查询,缓存15分钟 +// - 用户查询频率较高,适合中期缓存 +// +// 参数: +// - ctx: 上下文 +// - userID: 用户ID +// +// 返回: +// - *entities.Certification: 查询到的认证,未找到时返回nil +// - error: 查询失败时的错误信息 +func (r *GormCertificationQueryRepository) GetByUserID(ctx context.Context, userID string) (*entities.Certification, error) { + var cert entities.Certification + err := r.SmartGetByField(ctx, &cert, "user_id", userID, QueryCacheTTLUserQuery) + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("用户尚未创建认证申请") + } + return nil, fmt.Errorf("查询用户认证记录失败: %w", err) + } + return &cert, nil +} + +// Exists 检查认证是否存在 +func (r *GormCertificationQueryRepository) Exists(ctx context.Context, id string) (bool, error) { + return r.ExistsEntity(ctx, id, &entities.Certification{}) +} + +// ================ 列表查询 ================ + +// List 分页列表查询 +// +// 缓存策略: +// - 搜索查询:短期缓存2分钟(避免频繁数据库查询但保证实时性) +// - 常规列表:智能缓存(根据查询复杂度自动选择缓存策略) +// +// 参数: +// - ctx: 上下文 +// - query: 列表查询条件 +// +// 返回: +// - []*entities.Certification: 查询结果列表 +// - int64: 总记录数 +// - error: 查询失败时的错误信息 +func (r *GormCertificationQueryRepository) List(ctx context.Context, query *queries.ListCertificationsQuery) ([]*entities.Certification, int64, error) { + db := r.GetDB(ctx).Model(&entities.Certification{}) + + // 应用过滤条件 + if query.UserID != "" { + db = db.Where("user_id = ?", query.UserID) + } + if query.Status != "" { + db = db.Where("status = ?", query.Status) + } + if len(query.Statuses) > 0 { + db = db.Where("status IN ?", query.Statuses) + } + + // 获取总数 + var total int64 + if err := db.Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("查询认证总数失败: %w", err) + } + + // 应用排序和分页 + if query.SortBy != "" { + orderClause := query.SortBy + if query.SortOrder != "" { + orderClause += " " + strings.ToUpper(query.SortOrder) + } + db = db.Order(orderClause) + } else { + db = db.Order("created_at DESC") + } + + offset := (query.Page - 1) * query.PageSize + db = db.Offset(offset).Limit(query.PageSize) + + // 执行查询 + var certifications []*entities.Certification + if err := db.Find(&certifications).Error; err != nil { + return nil, 0, fmt.Errorf("查询认证列表失败: %w", err) + } + + return certifications, total, nil +} + +// ListByUserIDs 根据用户ID列表查询 +func (r *GormCertificationQueryRepository) ListByUserIDs(ctx context.Context, userIDs []string) ([]*entities.Certification, error) { + if len(userIDs) == 0 { + return []*entities.Certification{}, nil + } + + var certifications []*entities.Certification + if err := r.GetDB(ctx).Where("user_id IN ?", userIDs).Find(&certifications).Error; err != nil { + return nil, fmt.Errorf("根据用户ID列表查询认证失败: %w", err) + } + + return certifications, nil +} + +// ListByStatus 根据状态查询 +func (r *GormCertificationQueryRepository) ListByStatus(ctx context.Context, status enums.CertificationStatus, limit int) ([]*entities.Certification, error) { + db := r.GetDB(ctx).Where("status = ?", status) + if limit > 0 { + db = db.Limit(limit) + } + + var certifications []*entities.Certification + if err := db.Find(&certifications).Error; err != nil { + return nil, fmt.Errorf("根据状态查询认证失败: %w", err) + } + + return certifications, nil +} + +// ================ 业务查询 ================ + +// FindByAuthFlowID 根据认证流程ID查询 +// +// 缓存策略: +// - 业务查询,缓存15分钟 +// - 回调查询频率较高 +// +// 参数: +// - ctx: 上下文 +// - authFlowID: 认证流程ID +// +// 返回: +// - *entities.Certification: 查询到的认证,未找到时返回nil +// - error: 查询失败时的错误信息 +func (r *GormCertificationQueryRepository) FindByAuthFlowID(ctx context.Context, authFlowID string) (*entities.Certification, error) { + var cert entities.Certification + err := r.SmartGetByField(ctx, &cert, "auth_flow_id", authFlowID, QueryCacheTTLBusinessQuery) + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("认证流程不存在") + } + return nil, fmt.Errorf("根据认证流程ID查询失败: %w", err) + } + return &cert, nil +} + +// FindByEsignFlowID 根据e签宝流程ID查询 +// +// 缓存策略: +// - 业务查询,缓存15分钟 +// - 回调查询频率较高 +// +// 参数: +// - ctx: 上下文 +// - esignFlowID: e签宝流程ID +// +// 返回: +// - *entities.Certification: 查询到的认证,未找到时返回nil +// - error: 查询失败时的错误信息 +func (r *GormCertificationQueryRepository) FindByEsignFlowID(ctx context.Context, esignFlowID string) (*entities.Certification, error) { + var cert entities.Certification + err := r.SmartGetByField(ctx, &cert, "esign_flow_id", esignFlowID, QueryCacheTTLBusinessQuery) + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("e签宝流程不存在") + } + return nil, fmt.Errorf("根据e签宝流程ID查询失败: %w", err) + } + return &cert, nil +} + +// ListPendingRetry 查询待重试的认证 +// +// 缓存策略: +// - 管理查询,不缓存保证数据实时性 +// +// 参数: +// - ctx: 上下文 +// - maxRetryCount: 最大重试次数 +// +// 返回: +// - []*entities.Certification: 待重试的认证列表 +// - error: 查询失败时的错误信息 +func (r *GormCertificationQueryRepository) ListPendingRetry(ctx context.Context, maxRetryCount int) ([]*entities.Certification, error) { + var certifications []*entities.Certification + err := r.WithoutCache().GetDB(ctx). + Where("status IN ? AND retry_count < ?", + []enums.CertificationStatus{ + enums.StatusInfoRejected, + enums.StatusContractRejected, + enums.StatusContractExpired, + }, + maxRetryCount). + Order("created_at ASC"). + Find(&certifications).Error + + if err != nil { + return nil, fmt.Errorf("查询待重试认证失败: %w", err) + } + + return certifications, nil +} + +// GetPendingCertifications 获取待处理认证 +func (r *GormCertificationQueryRepository) GetPendingCertifications(ctx context.Context) ([]*entities.Certification, error) { + var certifications []*entities.Certification + err := r.WithoutCache().GetDB(ctx). + Where("status IN ?", []enums.CertificationStatus{ + enums.StatusPending, + enums.StatusInfoSubmitted, + }). + Order("created_at ASC"). + Find(&certifications).Error + + if err != nil { + return nil, fmt.Errorf("查询待处理认证失败: %w", err) + } + + return certifications, nil +} + +// GetExpiredContracts 获取过期合同 +func (r *GormCertificationQueryRepository) GetExpiredContracts(ctx context.Context) ([]*entities.Certification, error) { + var certifications []*entities.Certification + err := r.WithoutCache().GetDB(ctx). + Where("status = ?", enums.StatusContractExpired). + Order("updated_at DESC"). + Find(&certifications).Error + + if err != nil { + return nil, fmt.Errorf("查询过期合同失败: %w", err) + } + + return certifications, nil +} + +// GetCertificationsByDateRange 根据日期范围获取认证 +func (r *GormCertificationQueryRepository) GetCertificationsByDateRange(ctx context.Context, startDate, endDate time.Time) ([]*entities.Certification, error) { + var certifications []*entities.Certification + err := r.GetDB(ctx). + Where("created_at BETWEEN ? AND ?", startDate, endDate). + Order("created_at DESC"). + Find(&certifications).Error + + if err != nil { + return nil, fmt.Errorf("根据日期范围查询认证失败: %w", err) + } + + return certifications, nil +} + +// GetUserActiveCertification 获取用户当前活跃认证 +func (r *GormCertificationQueryRepository) GetUserActiveCertification(ctx context.Context, userID string) (*entities.Certification, error) { + var cert entities.Certification + err := r.GetDB(ctx). + Where("user_id = ? AND status NOT IN ?", userID, []enums.CertificationStatus{ + enums.StatusContractSigned, + enums.StatusInfoRejected, + enums.StatusContractRejected, + enums.StatusContractExpired, + }). + First(&cert).Error + + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("用户没有活跃的认证申请") + } + return nil, fmt.Errorf("查询用户活跃认证失败: %w", err) + } + + return &cert, nil +} + +// ================ 统计查询 ================ + +// GetStatistics 获取统计数据 +func (r *GormCertificationQueryRepository) GetStatistics(ctx context.Context, period repositories.CertificationTimePeriod) (*repositories.CertificationStatistics, error) { + now := time.Now() + var startDate time.Time + + switch period { + case repositories.PeriodDaily: + startDate = now.AddDate(0, 0, -1) + case repositories.PeriodWeekly: + startDate = now.AddDate(0, 0, -7) + case repositories.PeriodMonthly: + startDate = now.AddDate(0, -1, 0) + case repositories.PeriodYearly: + startDate = now.AddDate(-1, 0, 0) + default: + startDate = now.AddDate(0, 0, -7) + } + + // 使用短期缓存进行统计查询 + var totalCount int64 + r.WithShortCache().GetDB(ctx).Model(&entities.Certification{}). + Where("created_at BETWEEN ? AND ?", startDate, now). + Count(&totalCount) + + var completedCount int64 + r.WithShortCache().GetDB(ctx).Model(&entities.Certification{}). + Where("created_at BETWEEN ? AND ? AND status = ?", startDate, now, enums.StatusContractSigned). + Count(&completedCount) + + successRate := float64(0) + if totalCount > 0 { + successRate = float64(completedCount) / float64(totalCount) + } + + return &repositories.CertificationStatistics{ + Period: period, + StartDate: startDate, + EndDate: now, + TotalCertifications: totalCount, + CompletedCount: completedCount, + SuccessRate: successRate, + StatusDistribution: make(map[enums.CertificationStatus]int64), + FailureDistribution: make(map[enums.FailureReason]int64), + AvgProcessingTime: 24 * time.Hour, // 简化实现 + }, nil +} + +// CountByStatus 按状态统计 +func (r *GormCertificationQueryRepository) CountByStatus(ctx context.Context, status enums.CertificationStatus) (int64, error) { + var count int64 + if err := r.WithShortCache().GetDB(ctx).Model(&entities.Certification{}).Where("status = ?", status).Count(&count).Error; err != nil { + return 0, fmt.Errorf("按状态统计认证失败: %w", err) + } + return count, nil +} + +// CountByFailureReason 按失败原因统计 +func (r *GormCertificationQueryRepository) CountByFailureReason(ctx context.Context, reason enums.FailureReason) (int64, error) { + var count int64 + if err := r.WithShortCache().GetDB(ctx).Model(&entities.Certification{}).Where("failure_reason = ?", reason).Count(&count).Error; err != nil { + return 0, fmt.Errorf("按失败原因统计认证失败: %w", err) + } + return count, nil +} + +// GetProgressStatistics 获取进度统计 +func (r *GormCertificationQueryRepository) GetProgressStatistics(ctx context.Context) (*repositories.CertificationProgressStats, error) { + // 简化实现 + return &repositories.CertificationProgressStats{ + StatusProgress: make(map[enums.CertificationStatus]int64), + ProgressDistribution: make(map[int]int64), + StageTimeStats: make(map[string]*repositories.CertificationStageTimeInfo), + }, nil +} + +// SearchByCompanyName 按公司名搜索 +func (r *GormCertificationQueryRepository) SearchByCompanyName(ctx context.Context, companyName string, limit int) ([]*entities.Certification, error) { + // 简化实现,暂时返回空结果 + r.GetLogger().Warn("按公司名搜索功能待实现,需要企业信息服务支持") + return []*entities.Certification{}, nil +} + +// SearchByLegalPerson 按法人搜索 +func (r *GormCertificationQueryRepository) SearchByLegalPerson(ctx context.Context, legalPersonName string, limit int) ([]*entities.Certification, error) { + // 简化实现,暂时返回空结果 + r.GetLogger().Warn("按法人搜索功能待实现,需要企业信息服务支持") + return []*entities.Certification{}, nil +} + +// InvalidateCache 清除缓存 +func (r *GormCertificationQueryRepository) InvalidateCache(ctx context.Context, keys ...string) error { + // 简化实现,暂不处理缓存 + return nil +} + +// RefreshCache 刷新缓存 +func (r *GormCertificationQueryRepository) RefreshCache(ctx context.Context, certificationID string) error { + // 简化实现,暂不处理缓存 + return nil +} + +// ================ 缓存管理方法 ================ + +// WarmupCache 预热认证查询缓存 +// +// 业务说明: +// - 系统启动时预热常用查询的缓存 +// - 提升首次访问的响应速度 +// +// 预热策略: +// - 活跃认证:30分钟长期缓存 +// - 待处理认证:15分钟中期缓存 +func (r *GormCertificationQueryRepository) WarmupCache(ctx context.Context) error { + r.GetLogger().Info("开始预热认证查询缓存") + + queries := []database.WarmupQuery{ + { + Name: "active_certifications", + TTL: QueryCacheTTLWarmupLong, + Dest: &[]entities.Certification{}, + }, + { + Name: "pending_certifications", + TTL: QueryCacheTTLWarmupMedium, + Dest: &[]entities.Certification{}, + }, + } + + return r.WarmupCommonQueries(ctx, queries) +} + +// GetCacheStats 获取缓存统计信息 +// +// 返回当前Repository的缓存使用统计,包括: +// - 基础缓存信息(命中率、键数量等) +// - 特定的缓存模式列表 +// - 性能指标 +func (r *GormCertificationQueryRepository) GetCacheStats() map[string]interface{} { + stats := r.GetCacheInfo() + stats["specific_patterns"] = []string{ + QueryCachePatternTable, + QueryCachePatternUser, + } + return stats +} \ No newline at end of file diff --git a/internal/infrastructure/database/repositories/certification/gorm_certification_repository.go b/internal/infrastructure/database/repositories/certification/gorm_certification_repository.go deleted file mode 100644 index 7aec5fd..0000000 --- a/internal/infrastructure/database/repositories/certification/gorm_certification_repository.go +++ /dev/null @@ -1,370 +0,0 @@ -package repositories - -import ( - "context" - "errors" - "time" - - "go.uber.org/zap" - "gorm.io/gorm" - - "tyapi-server/internal/domains/certification/entities" - "tyapi-server/internal/domains/certification/repositories" - "tyapi-server/internal/domains/certification/repositories/queries" - "tyapi-server/internal/shared/interfaces" -) - -// GormCertificationRepository GORM认证仓储实现 -type GormCertificationRepository struct { - db *gorm.DB - logger *zap.Logger -} - -// 编译时检查接口实现 -var _ repositories.CertificationRepository = (*GormCertificationRepository)(nil) - -// NewGormCertificationRepository 创建GORM认证仓储 -func NewGormCertificationRepository(db *gorm.DB, logger *zap.Logger) repositories.CertificationRepository { - return &GormCertificationRepository{ - db: db, - logger: logger, - } -} - -// ================ 基础CRUD操作 ================ - -// Create 创建认证申请 -func (r *GormCertificationRepository) Create(ctx context.Context, cert entities.Certification) (entities.Certification, error) { - r.logger.Info("创建认证申请", zap.String("user_id", cert.UserID)) - err := r.db.WithContext(ctx).Create(&cert).Error - return cert, err -} - -// GetByID 根据ID获取认证申请 -func (r *GormCertificationRepository) GetByID(ctx context.Context, id string) (entities.Certification, error) { - var cert entities.Certification - err := r.db.WithContext(ctx).Where("id = ?", id).First(&cert).Error - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return entities.Certification{}, gorm.ErrRecordNotFound - } - return entities.Certification{}, err - } - return cert, err -} - -// GetByAuthFlowID 根据认证流程ID获取认证申请 -func (r *GormCertificationRepository) GetByAuthFlowID(ctx context.Context, authFlowID string) (entities.Certification, error) { - var cert entities.Certification - err := r.db.WithContext(ctx).Where("auth_flow_id = ?", authFlowID).First(&cert).Error - return cert, err -} - -// GetByEsignFlowID 根据签署流程ID获取认证申请 -func (r *GormCertificationRepository) GetByEsignFlowID(ctx context.Context, esignFlowID string) (entities.Certification, error) { - var cert entities.Certification - err := r.db.WithContext(ctx).Where("esign_flow_id = ?", esignFlowID).First(&cert).Error - return cert, err -} - -// Update 更新认证申请 -func (r *GormCertificationRepository) Update(ctx context.Context, cert entities.Certification) error { - r.logger.Info("更新认证申请", zap.String("id", cert.ID)) - return r.db.WithContext(ctx).Save(&cert).Error -} - -// Delete 删除认证申请 -func (r *GormCertificationRepository) Delete(ctx context.Context, id string) error { - r.logger.Info("删除认证申请", zap.String("id", id)) - return r.db.WithContext(ctx).Delete(&entities.Certification{}, "id = ?", id).Error -} - -// SoftDelete 软删除认证申请 -func (r *GormCertificationRepository) SoftDelete(ctx context.Context, id string) error { - r.logger.Info("软删除认证申请", zap.String("id", id)) - return r.db.WithContext(ctx).Delete(&entities.Certification{}, "id = ?", id).Error -} - -// Restore 恢复认证申请 -func (r *GormCertificationRepository) Restore(ctx context.Context, id string) error { - r.logger.Info("恢复认证申请", zap.String("id", id)) - return r.db.WithContext(ctx).Unscoped().Model(&entities.Certification{}).Where("id = ?", id).Update("deleted_at", nil).Error -} - -// Count 统计认证申请数量 -func (r *GormCertificationRepository) Count(ctx context.Context, options interfaces.CountOptions) (int64, error) { - var count int64 - query := r.db.WithContext(ctx).Model(&entities.Certification{}) - - if options.Filters != nil { - for key, value := range options.Filters { - query = query.Where(key+" = ?", value) - } - } - - if options.Search != "" { - query = query.Where("user_id LIKE ?", "%"+options.Search+"%") - } - - err := query.Count(&count).Error - return count, err -} - -// Exists 检查认证申请是否存在 -func (r *GormCertificationRepository) Exists(ctx context.Context, id string) (bool, error) { - var count int64 - err := r.db.WithContext(ctx).Model(&entities.Certification{}).Where("id = ?", id).Count(&count).Error - return count > 0, err -} - -// CreateBatch 批量创建认证申请 -func (r *GormCertificationRepository) CreateBatch(ctx context.Context, certs []entities.Certification) error { - r.logger.Info("批量创建认证申请", zap.Int("count", len(certs))) - return r.db.WithContext(ctx).Create(&certs).Error -} - -// GetByIDs 根据ID列表获取认证申请 -func (r *GormCertificationRepository) GetByIDs(ctx context.Context, ids []string) ([]entities.Certification, error) { - var certs []entities.Certification - err := r.db.WithContext(ctx).Where("id IN ?", ids).Find(&certs).Error - return certs, err -} - -// UpdateBatch 批量更新认证申请 -func (r *GormCertificationRepository) UpdateBatch(ctx context.Context, certs []entities.Certification) error { - r.logger.Info("批量更新认证申请", zap.Int("count", len(certs))) - return r.db.WithContext(ctx).Save(&certs).Error -} - -// DeleteBatch 批量删除认证申请 -func (r *GormCertificationRepository) DeleteBatch(ctx context.Context, ids []string) error { - r.logger.Info("批量删除认证申请", zap.Strings("ids", ids)) - return r.db.WithContext(ctx).Delete(&entities.Certification{}, "id IN ?", ids).Error -} - -// List 获取认证申请列表 -func (r *GormCertificationRepository) List(ctx context.Context, options interfaces.ListOptions) ([]entities.Certification, error) { - var certs []entities.Certification - query := r.db.WithContext(ctx).Model(&entities.Certification{}) - - if options.Filters != nil { - for key, value := range options.Filters { - query = query.Where(key+" = ?", value) - } - } - - if options.Search != "" { - query = query.Where("user_id LIKE ?", "%"+options.Search+"%") - } - - if options.Sort != "" { - order := "ASC" - if options.Order != "" { - order = options.Order - } - query = query.Order(options.Sort + " " + order) - } - - if options.Page > 0 && options.PageSize > 0 { - offset := (options.Page - 1) * options.PageSize - query = query.Offset(offset).Limit(options.PageSize) - } - - return certs, query.Find(&certs).Error -} - -// WithTx 使用事务 -func (r *GormCertificationRepository) WithTx(tx interface{}) interfaces.Repository[entities.Certification] { - if gormTx, ok := tx.(*gorm.DB); ok { - return &GormCertificationRepository{ - db: gormTx, - logger: r.logger, - } - } - return r -} - -// ================ 业务方法 ================ - -// ListCertifications 获取认证申请列表(带分页和筛选) -func (r *GormCertificationRepository) ListCertifications(ctx context.Context, query *queries.ListCertificationsQuery) ([]*entities.Certification, int64, error) { - var certs []entities.Certification - var total int64 - - dbQuery := r.db.WithContext(ctx).Model(&entities.Certification{}) - - // 应用筛选条件 - if query.UserID != "" { - dbQuery = dbQuery.Where("user_id = ?", query.UserID) - } - if query.Status != "" { - dbQuery = dbQuery.Where("status = ?", query.Status) - } - if query.StartDate != "" { - dbQuery = dbQuery.Where("created_at >= ?", query.StartDate) - } - if query.EndDate != "" { - dbQuery = dbQuery.Where("created_at <= ?", query.EndDate) - } - if query.EnterpriseName != "" { - // 简化企业名称查询,暂时不关联企业表 - dbQuery = dbQuery.Where("user_id IN (SELECT user_id FROM enterprise_infos WHERE company_name LIKE ?)", "%"+query.EnterpriseName+"%") - } - - // 统计总数 - if err := dbQuery.Count(&total).Error; err != nil { - return nil, 0, err - } - - // 应用分页 - if query.Page > 0 && query.PageSize > 0 { - offset := (query.Page - 1) * query.PageSize - dbQuery = dbQuery.Offset(offset).Limit(query.PageSize) - } - - // 排序 - dbQuery = dbQuery.Order("created_at DESC") - - // 执行查询 - if err := dbQuery.Find(&certs).Error; err != nil { - return nil, 0, err - } - - // 转换为指针切片 - var result []*entities.Certification - for i := range certs { - result = append(result, &certs[i]) - } - - return result, total, nil -} - -// GetByUserID 根据用户ID获取认证申请 -func (r *GormCertificationRepository) GetByUserID(ctx context.Context, userID string) (*entities.Certification, error) { - var cert entities.Certification - err := r.db.WithContext(ctx).Where("user_id = ?", userID).First(&cert).Error - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, gorm.ErrRecordNotFound - } - return nil, err - } - return &cert, nil -} - -// GetByStatus 根据状态获取认证申请列表 -func (r *GormCertificationRepository) GetByStatus(ctx context.Context, status string) ([]*entities.Certification, error) { - var certs []entities.Certification - err := r.db.WithContext(ctx).Where("status = ?", status).Find(&certs).Error - if err != nil { - return nil, err - } - - var result []*entities.Certification - for i := range certs { - result = append(result, &certs[i]) - } - return result, nil -} - -// UpdateStatus 更新认证状态 -func (r *GormCertificationRepository) UpdateStatus(ctx context.Context, certificationID string, status string) error { - r.logger.Info("更新认证状态", - zap.String("certification_id", certificationID), - zap.String("status", status), - ) - - updates := map[string]interface{}{ - "status": status, - "updated_at": time.Now(), - } - - // 根据状态更新时间戳 - switch status { - case "info_submitted": - updates["info_submitted_at"] = time.Now() - case "enterprise_verified": - updates["enterprise_verified_at"] = time.Now() - case "contract_applied": - updates["contract_applied_at"] = time.Now() - case "contract_signed": - updates["contract_signed_at"] = time.Now() - case "completed": - updates["completed_at"] = time.Now() - } - - return r.db.WithContext(ctx).Model(&entities.Certification{}). - Where("id = ?", certificationID). - Updates(updates).Error -} - -// GetPendingCertifications 获取待处理的认证申请 -func (r *GormCertificationRepository) GetPendingCertifications(ctx context.Context) ([]*entities.Certification, error) { - return r.GetByStatus(ctx, "pending") -} - -// GetStats 获取认证统计信息 -func (r *GormCertificationRepository) GetStats(ctx context.Context) (*repositories.CertificationStats, error) { - var stats repositories.CertificationStats - - // 总认证数 - if err := r.db.WithContext(ctx).Model(&entities.Certification{}).Count(&stats.TotalCertifications).Error; err != nil { - return nil, err - } - - // 待处理认证数 - if err := r.db.WithContext(ctx).Model(&entities.Certification{}).Where("status = ?", "pending").Count(&stats.PendingCertifications).Error; err != nil { - return nil, err - } - - // 已完成认证数 - if err := r.db.WithContext(ctx).Model(&entities.Certification{}).Where("status = ?", "completed").Count(&stats.CompletedCertifications).Error; err != nil { - return nil, err - } - - // 今日提交数 - today := time.Now().Format("2006-01-02") - if err := r.db.WithContext(ctx).Model(&entities.Certification{}). - Where("DATE(created_at) = ?", today). - Count(&stats.TodaySubmissions).Error; err != nil { - return nil, err - } - - return &stats, nil -} - -// GetStatsByDateRange 根据日期范围获取认证统计信息 -func (r *GormCertificationRepository) GetStatsByDateRange(ctx context.Context, startDate, endDate string) (*repositories.CertificationStats, error) { - var stats repositories.CertificationStats - - // 总认证数 - if err := r.db.WithContext(ctx).Model(&entities.Certification{}). - Where("created_at BETWEEN ? AND ?", startDate, endDate). - Count(&stats.TotalCertifications).Error; err != nil { - return nil, err - } - - // 待处理认证数 - if err := r.db.WithContext(ctx).Model(&entities.Certification{}). - Where("status = ? AND created_at BETWEEN ? AND ?", "pending", startDate, endDate). - Count(&stats.PendingCertifications).Error; err != nil { - return nil, err - } - - // 已完成认证数 - if err := r.db.WithContext(ctx).Model(&entities.Certification{}). - Where("status = ? AND created_at BETWEEN ? AND ?", "completed", startDate, endDate). - Count(&stats.CompletedCertifications).Error; err != nil { - return nil, err - } - - // 今日提交数 - today := time.Now().Format("2006-01-02") - if err := r.db.WithContext(ctx).Model(&entities.Certification{}). - Where("DATE(created_at) = ?", today). - Count(&stats.TodaySubmissions).Error; err != nil { - return nil, err - } - - return &stats, nil -} diff --git a/internal/infrastructure/database/repositories/certification/gorm_enterprise_info_submit_record_repository.go b/internal/infrastructure/database/repositories/certification/gorm_enterprise_info_submit_record_repository.go deleted file mode 100644 index 7ce1187..0000000 --- a/internal/infrastructure/database/repositories/certification/gorm_enterprise_info_submit_record_repository.go +++ /dev/null @@ -1,596 +0,0 @@ -package repositories - -import ( - "context" - "time" - - "go.uber.org/zap" - "gorm.io/gorm" - - "tyapi-server/internal/domains/certification/entities" - "tyapi-server/internal/domains/certification/repositories" - "tyapi-server/internal/domains/certification/repositories/queries" - "tyapi-server/internal/shared/database" - "tyapi-server/internal/shared/interfaces" -) - -// ================ 常量定义 ================ - -const ( - // 表名常量 - EnterpriseInfoSubmitRecordsTable = "enterprise_info_submit_records" - - // 缓存时间常量 - CacheTTLPrimaryQuery = 30 * time.Minute // 主键查询缓存时间 - CacheTTLBusinessQuery = 15 * time.Minute // 业务查询缓存时间 - CacheTTLUserQuery = 10 * time.Minute // 用户相关查询缓存时间 - CacheTTLSearchQuery = 2 * time.Minute // 搜索查询缓存时间 - CacheTTLActiveRecords = 5 * time.Minute // 活跃记录查询缓存时间 - CacheTTLWarmupLong = 30 * time.Minute // 预热长期缓存 - CacheTTLWarmupMedium = 15 * time.Minute // 预热中期缓存 - - // 缓存键模式常量 - CachePatternTable = "gorm_cache:enterprise_info_submit_records:*" - CachePatternCertification = "enterprise_info:certification_id:*" - CachePatternUser = "enterprise_info:user_id:*" - - // 业务常量 - StatusActive = "active" - StatusPending = "pending" -) - -// ================ Repository 实现 ================ - -// GormEnterpriseInfoSubmitRecordRepository 企业信息提交记录GORM仓储实现 -// -// 特性说明: -// - 基于 CachedBaseRepositoryImpl 实现自动缓存管理 -// - 支持多级缓存策略(主键查询30分钟,业务查询15分钟,搜索2分钟) -// - 自动缓存失效:写操作时自动清理相关缓存 -// - 智能缓存选择:根据查询复杂度自动选择缓存策略 -// - 内置监控支持:提供缓存统计和性能监控 -type GormEnterpriseInfoSubmitRecordRepository struct { - *database.CachedBaseRepositoryImpl -} - -// 编译时检查接口实现 -var _ repositories.EnterpriseInfoSubmitRecordRepository = (*GormEnterpriseInfoSubmitRecordRepository)(nil) - -// NewGormEnterpriseInfoSubmitRecordRepository 创建企业信息提交记录GORM仓储 -// -// 参数: -// - db: GORM数据库连接实例 -// - logger: 日志记录器 -// -// 返回: -// - repositories.EnterpriseInfoSubmitRecordRepository: 仓储接口实现 -func NewGormEnterpriseInfoSubmitRecordRepository(db *gorm.DB, logger *zap.Logger) repositories.EnterpriseInfoSubmitRecordRepository { - return &GormEnterpriseInfoSubmitRecordRepository{ - CachedBaseRepositoryImpl: database.NewCachedBaseRepositoryImpl(db, logger, EnterpriseInfoSubmitRecordsTable), - } -} - -// ================ Repository[T] 接口实现 ================ - -// Create 创建企业信息提交记录 -// -// 业务说明: -// - 创建新的企业信息提交记录 -// - 自动触发相关缓存失效 -// -// 参数: -// - ctx: 上下文 -// - record: 要创建的记录实体 -// -// 返回: -// - entities.EnterpriseInfoSubmitRecord: 创建后的记录(包含生成的ID) -// - error: 创建失败时的错误信息 -func (r *GormEnterpriseInfoSubmitRecordRepository) Create(ctx context.Context, record entities.EnterpriseInfoSubmitRecord) (entities.EnterpriseInfoSubmitRecord, error) { - r.GetLogger().Info("创建企业信息提交记录", - zap.String("user_id", record.UserID), - zap.String("company_name", record.CompanyName)) - - err := r.CreateEntity(ctx, &record) - return record, err -} - -// GetByID 根据ID获取企业信息提交记录 -// -// 缓存策略: -// - 使用智能主键查询,自动缓存30分钟 -// - 主键查询命中率高,适合长期缓存 -// -// 参数: -// - ctx: 上下文 -// - id: 记录ID -// -// 返回: -// - entities.EnterpriseInfoSubmitRecord: 查询到的记录 -// - error: 查询失败或记录不存在时的错误 -func (r *GormEnterpriseInfoSubmitRecordRepository) GetByID(ctx context.Context, id string) (entities.EnterpriseInfoSubmitRecord, error) { - var record entities.EnterpriseInfoSubmitRecord - err := r.SmartGetByID(ctx, id, &record) - return record, err -} - -// Update 更新企业信息提交记录 -// -// 缓存影响: -// - GORM缓存插件会自动失效相关缓存 -// - 无需手动管理缓存一致性 -// -// 参数: -// - ctx: 上下文 -// - record: 要更新的记录实体 -// -// 返回: -// - error: 更新失败时的错误信息 -func (r *GormEnterpriseInfoSubmitRecordRepository) Update(ctx context.Context, record entities.EnterpriseInfoSubmitRecord) error { - r.GetLogger().Info("更新企业信息提交记录", - zap.String("id", record.ID), - zap.String("company_name", record.CompanyName)) - - return r.UpdateEntity(ctx, &record) -} - -// CreateBatch 批量创建企业信息提交记录 -func (r *GormEnterpriseInfoSubmitRecordRepository) CreateBatch(ctx context.Context, records []entities.EnterpriseInfoSubmitRecord) error { - r.GetLogger().Info("批量创建企业信息提交记录", zap.Int("count", len(records))) - return r.CreateBatchEntity(ctx, &records) -} - -// GetByIDs 根据ID列表获取企业信息提交记录 -func (r *GormEnterpriseInfoSubmitRecordRepository) GetByIDs(ctx context.Context, ids []string) ([]entities.EnterpriseInfoSubmitRecord, error) { - var records []entities.EnterpriseInfoSubmitRecord - err := r.GetEntitiesByIDs(ctx, ids, &records) - return records, err -} - -// UpdateBatch 批量更新企业信息提交记录 -func (r *GormEnterpriseInfoSubmitRecordRepository) UpdateBatch(ctx context.Context, records []entities.EnterpriseInfoSubmitRecord) error { - r.GetLogger().Info("批量更新企业信息提交记录", zap.Int("count", len(records))) - return r.UpdateBatchEntity(ctx, &records) -} - -// DeleteBatch 批量删除企业信息提交记录 -func (r *GormEnterpriseInfoSubmitRecordRepository) DeleteBatch(ctx context.Context, ids []string) error { - r.GetLogger().Info("批量删除企业信息提交记录", zap.Strings("ids", ids)) - return r.DeleteBatchEntity(ctx, ids, &entities.EnterpriseInfoSubmitRecord{}) -} - -// List 获取企业信息提交记录列表 -// -// 缓存策略: -// - 搜索查询:短期缓存2分钟(避免频繁数据库查询但保证实时性) -// - 常规列表:智能缓存(根据查询复杂度自动选择缓存策略) -// -// 参数: -// - ctx: 上下文 -// - options: 列表查询选项(分页、排序、筛选、搜索等) -// -// 返回: -// - []entities.EnterpriseInfoSubmitRecord: 查询结果列表 -// - error: 查询失败时的错误信息 -func (r *GormEnterpriseInfoSubmitRecordRepository) List(ctx context.Context, options interfaces.ListOptions) ([]entities.EnterpriseInfoSubmitRecord, error) { - var records []entities.EnterpriseInfoSubmitRecord - - // 搜索查询:使用短期缓存避免频繁数据库查询 - if options.Search != "" { - return r.handleSearchQuery(ctx, options) - } - - // 常规列表:使用智能缓存策略 - err := r.SmartList(ctx, &records, options) - return records, err -} - -// ================ BaseRepository 接口实现 ================ - -// Delete 删除企业信息提交记录 -func (r *GormEnterpriseInfoSubmitRecordRepository) Delete(ctx context.Context, id string) error { - return r.DeleteEntity(ctx, id, &entities.EnterpriseInfoSubmitRecord{}) -} - -// Exists 检查企业信息提交记录是否存在 -func (r *GormEnterpriseInfoSubmitRecordRepository) Exists(ctx context.Context, id string) (bool, error) { - return r.ExistsEntity(ctx, id, &entities.EnterpriseInfoSubmitRecord{}) -} - -// Count 统计企业信息提交记录数量 -// -// 缓存策略: -// - 搜索统计:使用自定义搜索逻辑,不缓存保证准确性 -// - 常规统计:使用基础实现的缓存策略 -func (r *GormEnterpriseInfoSubmitRecordRepository) Count(ctx context.Context, options interfaces.CountOptions) (int64, error) { - if options.Search != "" { - return r.CountWhere(ctx, &entities.EnterpriseInfoSubmitRecord{}, - "company_name LIKE ? OR unified_social_code LIKE ? OR legal_person_name LIKE ?", - "%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%") - } - return r.CountWithOptions(ctx, &entities.EnterpriseInfoSubmitRecord{}, options) -} - -// SoftDelete 软删除企业信息提交记录 -func (r *GormEnterpriseInfoSubmitRecordRepository) SoftDelete(ctx context.Context, id string) error { - return r.SoftDeleteEntity(ctx, id, &entities.EnterpriseInfoSubmitRecord{}) -} - -// Restore 恢复企业信息提交记录 -func (r *GormEnterpriseInfoSubmitRecordRepository) Restore(ctx context.Context, id string) error { - return r.RestoreEntity(ctx, id, &entities.EnterpriseInfoSubmitRecord{}) -} - -// ================ 业务专用查询方法 ================ - -// GetByCertificationID 根据认证ID获取企业信息提交记录 -// -// 业务说明: -// - 每个认证流程对应一个企业信息提交记录 -// - 适用于认证流程中查询企业信息 -// -// 缓存策略: -// - 业务查询,缓存15分钟 -// - 认证ID查询频率较高,适合中期缓存 -// -// 参数: -// - ctx: 上下文 -// - certificationID: 认证流程ID -// -// 返回: -// - *entities.EnterpriseInfoSubmitRecord: 查询到的记录,未找到时返回nil -// - error: 查询失败时的错误信息 -func (r *GormEnterpriseInfoSubmitRecordRepository) GetByCertificationID(ctx context.Context, certificationID string) (*entities.EnterpriseInfoSubmitRecord, error) { - var record entities.EnterpriseInfoSubmitRecord - err := r.SmartGetByField(ctx, &record, "certification_id", certificationID, CacheTTLBusinessQuery) - if err != nil { - return nil, err - } - return &record, nil -} - -// GetByUserID 根据用户ID获取企业信息提交记录列表 -// -// 业务说明: -// - 获取某用户的所有企业信息提交记录 -// - 按创建时间倒序排列,最新的在前 -// -// 缓存策略: -// - 用户相关查询,使用中期缓存10分钟 -// - 用户查询频率中等,适合中期缓存 -// -// 参数: -// - ctx: 上下文 -// - userID: 用户ID -// -// 返回: -// - []*entities.EnterpriseInfoSubmitRecord: 查询结果列表 -// - error: 查询失败时的错误信息 -func (r *GormEnterpriseInfoSubmitRecordRepository) GetByUserID(ctx context.Context, userID string) ([]*entities.EnterpriseInfoSubmitRecord, error) { - var records []*entities.EnterpriseInfoSubmitRecord - - err := r.WithMediumCache().GetDB(ctx). - Where("user_id = ?", userID). - Order("created_at DESC"). - Find(&records).Error - - return records, err -} - -// GetLatestByUserID 根据用户ID获取最新的企业信息提交记录 -// -// 业务说明: -// - 获取用户最新提交的企业信息记录 -// - 适用于用户中心显示最新提交状态 -// -// 缓存策略: -// - 最新记录查询,缓存15分钟 -// - 用户中心访问频率较高 -func (r *GormEnterpriseInfoSubmitRecordRepository) GetLatestByUserID(ctx context.Context, userID string) (*entities.EnterpriseInfoSubmitRecord, error) { - var record entities.EnterpriseInfoSubmitRecord - err := r.GetDB(ctx). - Where("user_id = ?", userID). - Order("created_at DESC"). - First(&record).Error - if err != nil { - return nil, err - } - return &record, nil -} - -// ================ 业务状态管理方法 ================ - -// UpdateStatus 更新企业信息提交记录状态 -// -// 业务说明: -// - 更新记录的审核状态和备注信息 -// - 适用于管理员审核流程 -// -// 缓存影响: -// - GORM缓存插件会自动失效表相关的缓存 -// - 状态更新会影响列表查询和统计结果 -// -// 参数: -// - ctx: 上下文 -// - recordID: 记录ID -// - status: 新状态 -// - reason: 状态变更原因或备注 -// -// 返回: -// - error: 更新失败时的错误信息 -func (r *GormEnterpriseInfoSubmitRecordRepository) UpdateStatus(ctx context.Context, recordID string, status string, reason string) error { - r.GetLogger().Info("更新企业信息提交记录状态", - zap.String("record_id", recordID), - zap.String("status", status), - zap.String("reason", reason)) - - updates := map[string]interface{}{ - "status": status, - "updated_at": time.Now(), - } - - if reason != "" { - updates["reason"] = reason - } - - return r.GetDB(ctx).Model(&entities.EnterpriseInfoSubmitRecord{}). - Where("id = ?", recordID). - Updates(updates).Error -} - -// ================ 高级查询方法 ================ - -// ListRecords 获取企业信息提交记录列表(带分页和高级筛选) -// -// 业务说明: -// - 管理员后台的高级查询接口 -// - 支持多维度筛选和分页 -// -// 参数: -// - ctx: 上下文 -// - query: 高级查询条件 -// -// 返回: -// - []*entities.EnterpriseInfoSubmitRecord: 查询结果 -// - int64: 总记录数 -// - error: 查询失败时的错误 -func (r *GormEnterpriseInfoSubmitRecordRepository) ListRecords(ctx context.Context, query *queries.ListEnterpriseInfoSubmitRecordsQuery) ([]*entities.EnterpriseInfoSubmitRecord, int64, error) { - var records []*entities.EnterpriseInfoSubmitRecord - var total int64 - - dbQuery := r.GetDB(ctx).Model(&entities.EnterpriseInfoSubmitRecord{}) - - // 构建查询条件 - dbQuery = r.buildQueryConditions(dbQuery, query) - - // 统计总数 - if err := dbQuery.Count(&total).Error; err != nil { - return nil, 0, err - } - - // 应用分页和排序 - dbQuery = r.applyPaginationAndSorting(dbQuery, query) - - // 查询数据 - if err := dbQuery.Find(&records).Error; err != nil { - return nil, 0, err - } - - return records, total, nil -} - -// ================ 缓存管理方法 ================ - -// GetActiveRecords 获取活跃记录 -// -// 业务说明: -// - 获取状态为活跃的企业信息提交记录 -// - 适用于仪表板统计 -// -// 缓存策略: -// - 活跃记录查询使用短期缓存5分钟 -// - 活跃状态变化较频繁,使用短期缓存 -func (r *GormEnterpriseInfoSubmitRecordRepository) GetActiveRecords(ctx context.Context) ([]entities.EnterpriseInfoSubmitRecord, error) { - var records []entities.EnterpriseInfoSubmitRecord - err := r.WithShortCache().FindWithCache(ctx, &records, CacheTTLActiveRecords, "status = ?", StatusActive) - return records, err -} - -// GetPendingRecords 获取待审核记录 -// -// 业务说明: -// - 获取待审核的企业信息提交记录 -// - 适用于管理员工作台 -// -// 缓存策略: -// - 禁用缓存,保证数据实时性 -// - 待审核状态需要实时准确的数据 -func (r *GormEnterpriseInfoSubmitRecordRepository) GetPendingRecords(ctx context.Context) ([]entities.EnterpriseInfoSubmitRecord, error) { - var records []entities.EnterpriseInfoSubmitRecord - - db := r.WithoutCache().GetDB(ctx) - err := db.Where("status = ?", StatusPending). - Order("created_at ASC"). - Find(&records).Error - - return records, err -} - -// WarmupCache 预热企业信息提交记录缓存 -// -// 业务说明: -// - 系统启动时预热常用查询的缓存 -// - 提升首次访问的响应速度 -// -// 预热策略: -// - 活跃记录:30分钟长期缓存 -// - 最近提交:15分钟中期缓存 -func (r *GormEnterpriseInfoSubmitRecordRepository) WarmupCache(ctx context.Context) error { - r.GetLogger().Info("开始预热企业信息提交记录缓存") - - queries := []database.WarmupQuery{ - { - Name: "active_records", - TTL: CacheTTLWarmupLong, - Dest: &[]entities.EnterpriseInfoSubmitRecord{}, - }, - { - Name: "recent_submissions", - TTL: CacheTTLWarmupMedium, - Dest: &[]entities.EnterpriseInfoSubmitRecord{}, - }, - } - - return r.WarmupCommonQueries(ctx, queries) -} - -// RefreshRecordCache 刷新记录缓存 -// -// 业务说明: -// - 手动刷新企业信息提交记录相关的所有缓存 -// - 适用于数据迁移或批量更新后的缓存清理 -func (r *GormEnterpriseInfoSubmitRecordRepository) RefreshRecordCache(ctx context.Context) error { - r.GetLogger().Info("刷新企业信息提交记录缓存") - return r.RefreshCache(ctx, CachePatternTable) -} - -// GetCacheStats 获取缓存统计信息 -// -// 返回当前Repository的缓存使用统计,包括: -// - 基础缓存信息(命中率、键数量等) -// - 特定的缓存模式列表 -// - 性能指标 -func (r *GormEnterpriseInfoSubmitRecordRepository) GetCacheStats() map[string]interface{} { - stats := r.GetCacheInfo() - stats["specific_patterns"] = []string{ - CachePatternTable, - CachePatternCertification, - CachePatternUser, - } - return stats -} - -// ================ 私有辅助方法 ================ - -// handleSearchQuery 处理搜索查询 -// -// 业务逻辑: -// - 支持按企业名称、统一社会信用代码、法定代表人姓名搜索 -// - 使用短期缓存避免频繁数据库查询 -// -// 参数: -// - ctx: 上下文 -// - options: 查询选项 -// -// 返回: -// - []entities.EnterpriseInfoSubmitRecord: 搜索结果 -// - error: 查询失败时的错误 -func (r *GormEnterpriseInfoSubmitRecordRepository) handleSearchQuery(ctx context.Context, options interfaces.ListOptions) ([]entities.EnterpriseInfoSubmitRecord, error) { - var records []entities.EnterpriseInfoSubmitRecord - - db := r.GetDB(ctx). - Set("cache:enabled", true). - Set("cache:ttl", CacheTTLSearchQuery). - Model(&entities.EnterpriseInfoSubmitRecord{}) - - // 应用筛选条件 - if options.Filters != nil { - for key, value := range options.Filters { - db = db.Where(key+" = ?", value) - } - } - - // 企业信息特定的搜索逻辑 - db = db.Where("company_name LIKE ? OR unified_social_code LIKE ? OR legal_person_name LIKE ?", - "%"+options.Search+"%", "%"+options.Search+"%", "%"+options.Search+"%") - - // 应用预加载 - for _, include := range options.Include { - db = db.Preload(include) - } - - // 应用排序 - db = r.applySorting(db, options) - - // 应用分页 - db = r.applyPagination(db, options) - - return records, db.Find(&records).Error -} - -// buildQueryConditions 构建查询条件 -// -// 参数: -// - db: GORM数据库查询对象 -// - query: 查询条件 -// -// 返回: -// - *gorm.DB: 应用查询条件后的数据库对象 -func (r *GormEnterpriseInfoSubmitRecordRepository) buildQueryConditions(db *gorm.DB, query *queries.ListEnterpriseInfoSubmitRecordsQuery) *gorm.DB { - if query.UserID != "" { - db = db.Where("user_id = ?", query.UserID) - } - if query.Status != "" { - db = db.Where("status = ?", query.Status) - } - if query.StartDate != "" { - db = db.Where("created_at >= ?", query.StartDate) - } - if query.EndDate != "" { - db = db.Where("created_at <= ?", query.EndDate) - } - return db -} - -// applyPaginationAndSorting 应用分页和排序 -// -// 参数: -// - db: GORM数据库查询对象 -// - query: 查询条件 -// -// 返回: -// - *gorm.DB: 应用分页和排序后的数据库对象 -func (r *GormEnterpriseInfoSubmitRecordRepository) applyPaginationAndSorting(db *gorm.DB, query *queries.ListEnterpriseInfoSubmitRecordsQuery) *gorm.DB { - // 应用分页 - offset := (query.Page - 1) * query.PageSize - db = db.Offset(offset).Limit(query.PageSize) - - // 默认排序 - db = db.Order("created_at DESC") - - return db -} - -// applySorting 应用排序规则 -// -// 参数: -// - db: GORM数据库查询对象 -// - options: 查询选项 -// -// 返回: -// - *gorm.DB: 应用排序后的数据库对象 -func (r *GormEnterpriseInfoSubmitRecordRepository) applySorting(db *gorm.DB, options interfaces.ListOptions) *gorm.DB { - if options.Sort != "" { - order := "ASC" - if options.Order == "desc" || options.Order == "DESC" { - order = "DESC" - } - return db.Order(options.Sort + " " + order) - } - return db.Order("created_at DESC") -} - -// applyPagination 应用分页规则 -// -// 参数: -// - db: GORM数据库查询对象 -// - options: 查询选项 -// -// 返回: -// - *gorm.DB: 应用分页后的数据库对象 -func (r *GormEnterpriseInfoSubmitRecordRepository) applyPagination(db *gorm.DB, options interfaces.ListOptions) *gorm.DB { - if options.Page > 0 && options.PageSize > 0 { - offset := (options.Page - 1) * options.PageSize - return db.Offset(offset).Limit(options.PageSize) - } - return db -} diff --git a/internal/infrastructure/database/repositories/certification/gorm_esign_contract_generate_record_repository.go b/internal/infrastructure/database/repositories/certification/gorm_esign_contract_generate_record_repository.go deleted file mode 100644 index 178482a..0000000 --- a/internal/infrastructure/database/repositories/certification/gorm_esign_contract_generate_record_repository.go +++ /dev/null @@ -1,315 +0,0 @@ -package repositories - -import ( - "context" - "time" - - "go.uber.org/zap" - "gorm.io/gorm" - - "tyapi-server/internal/domains/certification/entities" - "tyapi-server/internal/domains/certification/repositories" - "tyapi-server/internal/domains/certification/repositories/queries" - "tyapi-server/internal/shared/interfaces" -) - -// GormEsignContractGenerateRecordRepository e签宝生成合同记录GORM仓储实现 -type GormEsignContractGenerateRecordRepository struct { - db *gorm.DB - logger *zap.Logger -} - -// 编译时检查接口实现 -var _ repositories.EsignContractGenerateRecordRepository = (*GormEsignContractGenerateRecordRepository)(nil) - -// NewGormEsignContractGenerateRecordRepository 创建e签宝生成合同记录GORM仓储 -func NewGormEsignContractGenerateRecordRepository(db *gorm.DB, logger *zap.Logger) repositories.EsignContractGenerateRecordRepository { - return &GormEsignContractGenerateRecordRepository{ - db: db, - logger: logger, - } -} - -// ================ 基础CRUD操作 ================ - -// Create 创建e签宝生成合同记录 -func (r *GormEsignContractGenerateRecordRepository) Create(ctx context.Context, record entities.EsignContractGenerateRecord) (entities.EsignContractGenerateRecord, error) { - r.logger.Info("创建e签宝生成合同记录", zap.String("certification_id", record.CertificationID)) - err := r.db.WithContext(ctx).Create(&record).Error - return record, err -} - -// GetByID 根据ID获取e签宝生成合同记录 -func (r *GormEsignContractGenerateRecordRepository) GetByID(ctx context.Context, id string) (entities.EsignContractGenerateRecord, error) { - var record entities.EsignContractGenerateRecord - err := r.db.WithContext(ctx).Where("id = ?", id).First(&record).Error - return record, err -} - -// Update 更新e签宝生成合同记录 -func (r *GormEsignContractGenerateRecordRepository) Update(ctx context.Context, record entities.EsignContractGenerateRecord) error { - r.logger.Info("更新e签宝生成合同记录", zap.String("id", record.ID)) - return r.db.WithContext(ctx).Save(&record).Error -} - -// Delete 删除e签宝生成合同记录 -func (r *GormEsignContractGenerateRecordRepository) Delete(ctx context.Context, id string) error { - r.logger.Info("删除e签宝生成合同记录", zap.String("id", id)) - return r.db.WithContext(ctx).Delete(&entities.EsignContractGenerateRecord{}, "id = ?", id).Error -} - -// SoftDelete 软删除e签宝生成合同记录 -func (r *GormEsignContractGenerateRecordRepository) SoftDelete(ctx context.Context, id string) error { - r.logger.Info("软删除e签宝生成合同记录", zap.String("id", id)) - return r.db.WithContext(ctx).Delete(&entities.EsignContractGenerateRecord{}, "id = ?", id).Error -} - -// Restore 恢复e签宝生成合同记录 -func (r *GormEsignContractGenerateRecordRepository) Restore(ctx context.Context, id string) error { - r.logger.Info("恢复e签宝生成合同记录", zap.String("id", id)) - return r.db.WithContext(ctx).Unscoped().Model(&entities.EsignContractGenerateRecord{}).Where("id = ?", id).Update("deleted_at", nil).Error -} - -// Count 统计e签宝生成合同记录数量 -func (r *GormEsignContractGenerateRecordRepository) Count(ctx context.Context, options interfaces.CountOptions) (int64, error) { - var count int64 - query := r.db.WithContext(ctx).Model(&entities.EsignContractGenerateRecord{}) - - if options.Filters != nil { - for key, value := range options.Filters { - query = query.Where(key+" = ?", value) - } - } - - if options.Search != "" { - query = query.Where("contract_name LIKE ?", "%"+options.Search+"%") - } - - err := query.Count(&count).Error - return count, err -} - -// Exists 检查e签宝生成合同记录是否存在 -func (r *GormEsignContractGenerateRecordRepository) Exists(ctx context.Context, id string) (bool, error) { - var count int64 - err := r.db.WithContext(ctx).Model(&entities.EsignContractGenerateRecord{}).Where("id = ?", id).Count(&count).Error - return count > 0, err -} - -// CreateBatch 批量创建e签宝生成合同记录 -func (r *GormEsignContractGenerateRecordRepository) CreateBatch(ctx context.Context, records []entities.EsignContractGenerateRecord) error { - r.logger.Info("批量创建e签宝生成合同记录", zap.Int("count", len(records))) - return r.db.WithContext(ctx).Create(&records).Error -} - -// GetByIDs 根据ID列表获取e签宝生成合同记录 -func (r *GormEsignContractGenerateRecordRepository) GetByIDs(ctx context.Context, ids []string) ([]entities.EsignContractGenerateRecord, error) { - var records []entities.EsignContractGenerateRecord - err := r.db.WithContext(ctx).Where("id IN ?", ids).Find(&records).Error - return records, err -} - -// UpdateBatch 批量更新e签宝生成合同记录 -func (r *GormEsignContractGenerateRecordRepository) UpdateBatch(ctx context.Context, records []entities.EsignContractGenerateRecord) error { - r.logger.Info("批量更新e签宝生成合同记录", zap.Int("count", len(records))) - return r.db.WithContext(ctx).Save(&records).Error -} - -// DeleteBatch 批量删除e签宝生成合同记录 -func (r *GormEsignContractGenerateRecordRepository) DeleteBatch(ctx context.Context, ids []string) error { - r.logger.Info("批量删除e签宝生成合同记录", zap.Strings("ids", ids)) - return r.db.WithContext(ctx).Delete(&entities.EsignContractGenerateRecord{}, "id IN ?", ids).Error -} - -// List 获取e签宝生成合同记录列表 -func (r *GormEsignContractGenerateRecordRepository) List(ctx context.Context, options interfaces.ListOptions) ([]entities.EsignContractGenerateRecord, error) { - var records []entities.EsignContractGenerateRecord - query := r.db.WithContext(ctx).Model(&entities.EsignContractGenerateRecord{}) - - if options.Filters != nil { - for key, value := range options.Filters { - query = query.Where(key+" = ?", value) - } - } - - if options.Search != "" { - query = query.Where("contract_name LIKE ?", "%"+options.Search+"%") - } - - if options.Sort != "" { - order := "ASC" - if options.Order != "" { - order = options.Order - } - query = query.Order(options.Sort + " " + order) - } - - if options.Page > 0 && options.PageSize > 0 { - offset := (options.Page - 1) * options.PageSize - query = query.Offset(offset).Limit(options.PageSize) - } - - return records, query.Find(&records).Error -} - -// WithTx 使用事务 -func (r *GormEsignContractGenerateRecordRepository) WithTx(tx interface{}) interfaces.Repository[entities.EsignContractGenerateRecord] { - if gormTx, ok := tx.(*gorm.DB); ok { - return &GormEsignContractGenerateRecordRepository{ - db: gormTx, - logger: r.logger, - } - } - return r -} - -// ================ 业务方法 ================ - -// GetByCertificationID 根据认证ID获取e签宝生成合同记录 -func (r *GormEsignContractGenerateRecordRepository) GetByCertificationID(ctx context.Context, certificationID string) (*entities.EsignContractGenerateRecord, error) { - var record entities.EsignContractGenerateRecord - err := r.db.WithContext(ctx).Where("certification_id = ?", certificationID).First(&record).Error - if err != nil { - return nil, err - } - return &record, nil -} - -// GetByUserID 根据用户ID获取e签宝生成合同记录列表 -func (r *GormEsignContractGenerateRecordRepository) GetByUserID(ctx context.Context, userID string) ([]*entities.EsignContractGenerateRecord, error) { - var records []entities.EsignContractGenerateRecord - err := r.db.WithContext(ctx).Where("user_id = ?", userID).Find(&records).Error - if err != nil { - return nil, err - } - - var result []*entities.EsignContractGenerateRecord - for i := range records { - result = append(result, &records[i]) - } - return result, nil -} - -// GetLatestByCertificationID 根据认证ID获取最新的e签宝生成合同记录 -func (r *GormEsignContractGenerateRecordRepository) GetLatestByCertificationID(ctx context.Context, certificationID string) (*entities.EsignContractGenerateRecord, error) { - var record entities.EsignContractGenerateRecord - err := r.db.WithContext(ctx).Where("certification_id = ?", certificationID).Order("created_at DESC").First(&record).Error - if err != nil { - return nil, err - } - return &record, nil -} - -// ListRecords 获取e签宝生成合同记录列表(带分页和筛选) -func (r *GormEsignContractGenerateRecordRepository) ListRecords(ctx context.Context, query *queries.ListEsignContractGenerateRecordsQuery) ([]*entities.EsignContractGenerateRecord, int64, error) { - var records []entities.EsignContractGenerateRecord - var total int64 - - dbQuery := r.db.WithContext(ctx).Model(&entities.EsignContractGenerateRecord{}) - - // 应用筛选条件 - if query.CertificationID != "" { - dbQuery = dbQuery.Where("certification_id = ?", query.CertificationID) - } - if query.UserID != "" { - dbQuery = dbQuery.Where("user_id = ?", query.UserID) - } - if query.Status != "" { - dbQuery = dbQuery.Where("status = ?", query.Status) - } - if query.ContractType != "" { - dbQuery = dbQuery.Where("contract_type = ?", query.ContractType) - } - if query.StartDate != "" { - dbQuery = dbQuery.Where("created_at >= ?", query.StartDate) - } - if query.EndDate != "" { - dbQuery = dbQuery.Where("created_at <= ?", query.EndDate) - } - - // 统计总数 - if err := dbQuery.Count(&total).Error; err != nil { - return nil, 0, err - } - - // 应用分页 - if query.Page > 0 && query.PageSize > 0 { - offset := (query.Page - 1) * query.PageSize - dbQuery = dbQuery.Offset(offset).Limit(query.PageSize) - } - - // 排序 - dbQuery = dbQuery.Order("created_at DESC") - - // 执行查询 - if err := dbQuery.Find(&records).Error; err != nil { - return nil, 0, err - } - - // 转换为指针切片 - var result []*entities.EsignContractGenerateRecord - for i := range records { - result = append(result, &records[i]) - } - - return result, total, nil -} - -// UpdateStatus 更新状态 -func (r *GormEsignContractGenerateRecordRepository) UpdateStatus(ctx context.Context, recordID string, status string, reason string) error { - r.logger.Info("更新e签宝生成合同记录状态", - zap.String("record_id", recordID), - zap.String("status", status), - ) - - updates := map[string]interface{}{ - "status": status, - "updated_at": time.Now(), - } - - // 根据状态设置相应的时间戳 - switch status { - case "generating": - // 不需要额外时间戳 - case "success": - updates["generated_at"] = time.Now() - case "failed": - updates["failed_at"] = time.Now() - updates["failure_reason"] = reason - } - - return r.db.WithContext(ctx).Model(&entities.EsignContractGenerateRecord{}). - Where("id = ?", recordID). - Updates(updates).Error -} - -// UpdateSuccessInfo 更新成功信息 -func (r *GormEsignContractGenerateRecordRepository) UpdateSuccessInfo(ctx context.Context, recordID, esignFlowID, contractFileID, contractURL string) error { - r.logger.Info("更新e签宝生成合同记录成功信息", - zap.String("record_id", recordID), - zap.String("esign_flow_id", esignFlowID), - ) - - now := time.Now() - updates := map[string]interface{}{ - "status": "success", - "esign_flow_id": esignFlowID, - "contract_file_id": contractFileID, - "contract_url": contractURL, - "generated_at": &now, - "updated_at": now, - } - - return r.db.WithContext(ctx).Model(&entities.EsignContractGenerateRecord{}). - Where("id = ?", recordID). - Updates(updates).Error -} - -// IncrementRetry 增加重试次数 -func (r *GormEsignContractGenerateRecordRepository) IncrementRetry(ctx context.Context, recordID string) error { - r.logger.Info("增加e签宝生成合同记录重试次数", zap.String("record_id", recordID)) - - return r.db.WithContext(ctx).Model(&entities.EsignContractGenerateRecord{}). - Where("id = ?", recordID). - UpdateColumn("retry_count", gorm.Expr("retry_count + 1")).Error -} \ No newline at end of file diff --git a/internal/infrastructure/database/repositories/certification/gorm_esign_contract_sign_record_repository.go b/internal/infrastructure/database/repositories/certification/gorm_esign_contract_sign_record_repository.go deleted file mode 100644 index 4b9db34..0000000 --- a/internal/infrastructure/database/repositories/certification/gorm_esign_contract_sign_record_repository.go +++ /dev/null @@ -1,356 +0,0 @@ -package repositories - -import ( - "context" - "time" - - "go.uber.org/zap" - "gorm.io/gorm" - - "tyapi-server/internal/domains/certification/entities" - "tyapi-server/internal/domains/certification/repositories" - "tyapi-server/internal/domains/certification/repositories/queries" - "tyapi-server/internal/shared/interfaces" -) - -// GormEsignContractSignRecordRepository e签宝签署合同记录GORM仓储实现 -type GormEsignContractSignRecordRepository struct { - db *gorm.DB - logger *zap.Logger -} - -// 编译时检查接口实现 -var _ repositories.EsignContractSignRecordRepository = (*GormEsignContractSignRecordRepository)(nil) - -// NewGormEsignContractSignRecordRepository 创建e签宝签署合同记录GORM仓储 -func NewGormEsignContractSignRecordRepository(db *gorm.DB, logger *zap.Logger) repositories.EsignContractSignRecordRepository { - return &GormEsignContractSignRecordRepository{ - db: db, - logger: logger, - } -} - -// ================ 基础CRUD操作 ================ - -// Create 创建e签宝签署合同记录 -func (r *GormEsignContractSignRecordRepository) Create(ctx context.Context, record entities.EsignContractSignRecord) (entities.EsignContractSignRecord, error) { - r.logger.Info("创建e签宝签署合同记录", zap.String("certification_id", record.CertificationID)) - err := r.db.WithContext(ctx).Create(&record).Error - return record, err -} - -// GetByID 根据ID获取e签宝签署合同记录 -func (r *GormEsignContractSignRecordRepository) GetByID(ctx context.Context, id string) (entities.EsignContractSignRecord, error) { - var record entities.EsignContractSignRecord - err := r.db.WithContext(ctx).Where("id = ?", id).First(&record).Error - return record, err -} - -// Update 更新e签宝签署合同记录 -func (r *GormEsignContractSignRecordRepository) Update(ctx context.Context, record entities.EsignContractSignRecord) error { - r.logger.Info("更新e签宝签署合同记录", zap.String("id", record.ID)) - return r.db.WithContext(ctx).Save(&record).Error -} - -// Delete 删除e签宝签署合同记录 -func (r *GormEsignContractSignRecordRepository) Delete(ctx context.Context, id string) error { - r.logger.Info("删除e签宝签署合同记录", zap.String("id", id)) - return r.db.WithContext(ctx).Delete(&entities.EsignContractSignRecord{}, "id = ?", id).Error -} - -// SoftDelete 软删除e签宝签署合同记录 -func (r *GormEsignContractSignRecordRepository) SoftDelete(ctx context.Context, id string) error { - r.logger.Info("软删除e签宝签署合同记录", zap.String("id", id)) - return r.db.WithContext(ctx).Delete(&entities.EsignContractSignRecord{}, "id = ?", id).Error -} - -// Restore 恢复e签宝签署合同记录 -func (r *GormEsignContractSignRecordRepository) Restore(ctx context.Context, id string) error { - r.logger.Info("恢复e签宝签署合同记录", zap.String("id", id)) - return r.db.WithContext(ctx).Unscoped().Model(&entities.EsignContractSignRecord{}).Where("id = ?", id).Update("deleted_at", nil).Error -} - -// Count 统计e签宝签署合同记录数量 -func (r *GormEsignContractSignRecordRepository) Count(ctx context.Context, options interfaces.CountOptions) (int64, error) { - var count int64 - query := r.db.WithContext(ctx).Model(&entities.EsignContractSignRecord{}) - - if options.Filters != nil { - for key, value := range options.Filters { - query = query.Where(key+" = ?", value) - } - } - - if options.Search != "" { - query = query.Where("signer_name LIKE ?", "%"+options.Search+"%") - } - - err := query.Count(&count).Error - return count, err -} - -// Exists 检查e签宝签署合同记录是否存在 -func (r *GormEsignContractSignRecordRepository) Exists(ctx context.Context, id string) (bool, error) { - var count int64 - err := r.db.WithContext(ctx).Model(&entities.EsignContractSignRecord{}).Where("id = ?", id).Count(&count).Error - return count > 0, err -} - -// CreateBatch 批量创建e签宝签署合同记录 -func (r *GormEsignContractSignRecordRepository) CreateBatch(ctx context.Context, records []entities.EsignContractSignRecord) error { - r.logger.Info("批量创建e签宝签署合同记录", zap.Int("count", len(records))) - return r.db.WithContext(ctx).Create(&records).Error -} - -// GetByIDs 根据ID列表获取e签宝签署合同记录 -func (r *GormEsignContractSignRecordRepository) GetByIDs(ctx context.Context, ids []string) ([]entities.EsignContractSignRecord, error) { - var records []entities.EsignContractSignRecord - err := r.db.WithContext(ctx).Where("id IN ?", ids).Find(&records).Error - return records, err -} - -// UpdateBatch 批量更新e签宝签署合同记录 -func (r *GormEsignContractSignRecordRepository) UpdateBatch(ctx context.Context, records []entities.EsignContractSignRecord) error { - r.logger.Info("批量更新e签宝签署合同记录", zap.Int("count", len(records))) - return r.db.WithContext(ctx).Save(&records).Error -} - -// DeleteBatch 批量删除e签宝签署合同记录 -func (r *GormEsignContractSignRecordRepository) DeleteBatch(ctx context.Context, ids []string) error { - r.logger.Info("批量删除e签宝签署合同记录", zap.Strings("ids", ids)) - return r.db.WithContext(ctx).Delete(&entities.EsignContractSignRecord{}, "id IN ?", ids).Error -} - -// List 获取e签宝签署合同记录列表 -func (r *GormEsignContractSignRecordRepository) List(ctx context.Context, options interfaces.ListOptions) ([]entities.EsignContractSignRecord, error) { - var records []entities.EsignContractSignRecord - query := r.db.WithContext(ctx).Model(&entities.EsignContractSignRecord{}) - - if options.Filters != nil { - for key, value := range options.Filters { - query = query.Where(key+" = ?", value) - } - } - - if options.Search != "" { - query = query.Where("signer_name LIKE ?", "%"+options.Search+"%") - } - - if options.Sort != "" { - order := "ASC" - if options.Order != "" { - order = options.Order - } - query = query.Order(options.Sort + " " + order) - } - - if options.Page > 0 && options.PageSize > 0 { - offset := (options.Page - 1) * options.PageSize - query = query.Offset(offset).Limit(options.PageSize) - } - - return records, query.Find(&records).Error -} - -// WithTx 使用事务 -func (r *GormEsignContractSignRecordRepository) WithTx(tx interface{}) interfaces.Repository[entities.EsignContractSignRecord] { - if gormTx, ok := tx.(*gorm.DB); ok { - return &GormEsignContractSignRecordRepository{ - db: gormTx, - logger: r.logger, - } - } - return r -} - -// ================ 业务方法 ================ - -// GetByCertificationID 根据认证ID获取e签宝签署合同记录 -func (r *GormEsignContractSignRecordRepository) GetByCertificationID(ctx context.Context, certificationID string) (*entities.EsignContractSignRecord, error) { - var record entities.EsignContractSignRecord - err := r.db.WithContext(ctx).Where("certification_id = ?", certificationID).First(&record).Error - if err != nil { - return nil, err - } - return &record, nil -} - -// GetByUserID 根据用户ID获取e签宝签署合同记录列表 -func (r *GormEsignContractSignRecordRepository) GetByUserID(ctx context.Context, userID string) ([]*entities.EsignContractSignRecord, error) { - var records []entities.EsignContractSignRecord - err := r.db.WithContext(ctx).Where("user_id = ?", userID).Find(&records).Error - if err != nil { - return nil, err - } - - var result []*entities.EsignContractSignRecord - for i := range records { - result = append(result, &records[i]) - } - return result, nil -} - -// GetLatestByCertificationID 根据认证ID获取最新的e签宝签署合同记录 -func (r *GormEsignContractSignRecordRepository) GetLatestByCertificationID(ctx context.Context, certificationID string) (*entities.EsignContractSignRecord, error) { - var record entities.EsignContractSignRecord - err := r.db.WithContext(ctx).Where("certification_id = ?", certificationID).Order("created_at DESC").First(&record).Error - if err != nil { - return nil, err - } - return &record, nil -} - -// GetByGenerateRecordID 根据生成记录ID获取e签宝签署合同记录 -func (r *GormEsignContractSignRecordRepository) GetByGenerateRecordID(ctx context.Context, generateRecordID string) (*entities.EsignContractSignRecord, error) { - var record entities.EsignContractSignRecord - err := r.db.WithContext(ctx).Where("generate_record_id = ?", generateRecordID).First(&record).Error - if err != nil { - return nil, err - } - return &record, nil -} - -// ListRecords 获取e签宝签署合同记录列表(带分页和筛选) -func (r *GormEsignContractSignRecordRepository) ListRecords(ctx context.Context, query *queries.ListEsignContractSignRecordsQuery) ([]*entities.EsignContractSignRecord, int64, error) { - var records []entities.EsignContractSignRecord - var total int64 - - dbQuery := r.db.WithContext(ctx).Model(&entities.EsignContractSignRecord{}) - - // 应用筛选条件 - if query.CertificationID != "" { - dbQuery = dbQuery.Where("certification_id = ?", query.CertificationID) - } - if query.UserID != "" { - dbQuery = dbQuery.Where("user_id = ?", query.UserID) - } - if query.Status != "" { - dbQuery = dbQuery.Where("status = ?", query.Status) - } - if query.SignerName != "" { - dbQuery = dbQuery.Where("signer_name LIKE ?", "%"+query.SignerName+"%") - } - if query.StartDate != "" { - dbQuery = dbQuery.Where("created_at >= ?", query.StartDate) - } - if query.EndDate != "" { - dbQuery = dbQuery.Where("created_at <= ?", query.EndDate) - } - - // 统计总数 - if err := dbQuery.Count(&total).Error; err != nil { - return nil, 0, err - } - - // 应用分页 - if query.Page > 0 && query.PageSize > 0 { - offset := (query.Page - 1) * query.PageSize - dbQuery = dbQuery.Offset(offset).Limit(query.PageSize) - } - - // 排序 - dbQuery = dbQuery.Order("created_at DESC") - - // 执行查询 - if err := dbQuery.Find(&records).Error; err != nil { - return nil, 0, err - } - - // 转换为指针切片 - var result []*entities.EsignContractSignRecord - for i := range records { - result = append(result, &records[i]) - } - - return result, total, nil -} - -// UpdateStatus 更新状态 -func (r *GormEsignContractSignRecordRepository) UpdateStatus(ctx context.Context, recordID string, status string, reason string) error { - r.logger.Info("更新e签宝签署合同记录状态", - zap.String("record_id", recordID), - zap.String("status", status), - ) - - updates := map[string]interface{}{ - "status": status, - "updated_at": time.Now(), - } - - // 根据状态设置相应的时间戳 - switch status { - case "signing": - // 不需要额外时间戳 - case "success": - updates["signed_at"] = time.Now() - case "failed": - updates["failed_at"] = time.Now() - updates["failure_reason"] = reason - case "expired": - updates["expired_at"] = time.Now() - } - - return r.db.WithContext(ctx).Model(&entities.EsignContractSignRecord{}). - Where("id = ?", recordID). - Updates(updates).Error -} - -// UpdateSuccessInfo 更新成功信息 -func (r *GormEsignContractSignRecordRepository) UpdateSuccessInfo(ctx context.Context, recordID, signedFileURL string) error { - r.logger.Info("更新e签宝签署合同记录成功信息", - zap.String("record_id", recordID), - ) - - now := time.Now() - updates := map[string]interface{}{ - "status": "success", - "signed_file_url": signedFileURL, - "signed_at": &now, - "updated_at": now, - } - - return r.db.WithContext(ctx).Model(&entities.EsignContractSignRecord{}). - Where("id = ?", recordID). - Updates(updates).Error -} - -// SetSignURL 设置签署链接 -func (r *GormEsignContractSignRecordRepository) SetSignURL(ctx context.Context, recordID, signURL string) error { - r.logger.Info("设置e签宝签署合同记录签署链接", - zap.String("record_id", recordID), - ) - - updates := map[string]interface{}{ - "sign_url": signURL, - "updated_at": time.Now(), - } - - return r.db.WithContext(ctx).Model(&entities.EsignContractSignRecord{}). - Where("id = ?", recordID). - Updates(updates).Error -} - -// IncrementRetry 增加重试次数 -func (r *GormEsignContractSignRecordRepository) IncrementRetry(ctx context.Context, recordID string) error { - r.logger.Info("增加e签宝签署合同记录重试次数", zap.String("record_id", recordID)) - - return r.db.WithContext(ctx).Model(&entities.EsignContractSignRecord{}). - Where("id = ?", recordID). - UpdateColumn("retry_count", gorm.Expr("retry_count + 1")).Error -} - -// MarkExpiredRecords 标记过期的记录 -func (r *GormEsignContractSignRecordRepository) MarkExpiredRecords(ctx context.Context) error { - r.logger.Info("标记过期的e签宝签署合同记录") - - now := time.Now() - updates := map[string]interface{}{ - "status": "expired", - "expired_at": &now, - "updated_at": now, - } - - return r.db.WithContext(ctx).Model(&entities.EsignContractSignRecord{}). - Where("status = ? AND expired_at < ?", "pending", now). - Updates(updates).Error -} \ No newline at end of file diff --git a/internal/infrastructure/events/certification_event_publisher.go b/internal/infrastructure/events/certification_event_publisher.go new file mode 100644 index 0000000..261da51 --- /dev/null +++ b/internal/infrastructure/events/certification_event_publisher.go @@ -0,0 +1,291 @@ +package events + +import ( + "context" + "time" + + "go.uber.org/zap" + + "tyapi-server/internal/shared/interfaces" +) + +// ================ 常量定义 ================ + +const ( + // 事件类型 + EventTypeCertificationCreated = "certification.created" + EventTypeEnterpriseInfoSubmitted = "certification.enterprise_info_submitted" + EventTypeEnterpriseVerificationCompleted = "certification.enterprise_verification_completed" + EventTypeContractGenerated = "certification.contract_generated" + EventTypeContractSigned = "certification.contract_signed" + EventTypeCertificationCompleted = "certification.completed" + EventTypeCertificationFailed = "certification.failed" + EventTypeStatusTransitioned = "certification.status_transitioned" + + // 重试配置 + MaxRetries = 3 + RetryDelay = 5 * time.Second +) + +// ================ 事件结构 ================ + +// CertificationEventData 认证事件数据结构 +type CertificationEventData struct { + EventType string `json:"event_type"` + CertificationID string `json:"certification_id"` + UserID string `json:"user_id"` + Data map[string]interface{} `json:"data"` + Timestamp time.Time `json:"timestamp"` + Version string `json:"version"` +} + +// ================ 事件发布器实现 ================ + +// CertificationEventPublisher 认证事件发布器实现 +// +// 职责: +// - 发布认证域相关的事件 +// - 支持异步发布和重试机制 +// - 提供事件持久化能力 +// - 集成监控和日志 +type CertificationEventPublisher struct { + eventBus interfaces.EventBus + logger *zap.Logger +} + +// NewCertificationEventPublisher 创建认证事件发布器 +func NewCertificationEventPublisher( + eventBus interfaces.EventBus, + logger *zap.Logger, +) *CertificationEventPublisher { + return &CertificationEventPublisher{ + eventBus: eventBus, + logger: logger, + } +} + +// ================ 事件发布方法 ================ + +// PublishCertificationCreated 发布认证创建事件 +func (p *CertificationEventPublisher) PublishCertificationCreated( + ctx context.Context, + certificationID, userID string, + data map[string]interface{}, +) error { + eventData := &CertificationEventData{ + EventType: EventTypeCertificationCreated, + CertificationID: certificationID, + UserID: userID, + Data: data, + Timestamp: time.Now(), + Version: "1.0", + } + + return p.publishEventData(ctx, eventData) +} + +// PublishEnterpriseInfoSubmitted 发布企业信息提交事件 +func (p *CertificationEventPublisher) PublishEnterpriseInfoSubmitted( + ctx context.Context, + certificationID, userID string, + data map[string]interface{}, +) error { + eventData := &CertificationEventData{ + EventType: EventTypeEnterpriseInfoSubmitted, + CertificationID: certificationID, + UserID: userID, + Data: data, + Timestamp: time.Now(), + Version: "1.0", + } + + return p.publishEventData(ctx, eventData) +} + +// PublishEnterpriseVerificationCompleted 发布企业认证完成事件 +func (p *CertificationEventPublisher) PublishEnterpriseVerificationCompleted( + ctx context.Context, + certificationID, userID string, + data map[string]interface{}, +) error { + eventData := &CertificationEventData{ + EventType: EventTypeEnterpriseVerificationCompleted, + CertificationID: certificationID, + UserID: userID, + Data: data, + Timestamp: time.Now(), + Version: "1.0", + } + + return p.publishEventData(ctx, eventData) +} + +// PublishContractGenerated 发布合同生成事件 +func (p *CertificationEventPublisher) PublishContractGenerated( + ctx context.Context, + certificationID, userID string, + data map[string]interface{}, +) error { + eventData := &CertificationEventData{ + EventType: EventTypeContractGenerated, + CertificationID: certificationID, + UserID: userID, + Data: data, + Timestamp: time.Now(), + Version: "1.0", + } + + return p.publishEventData(ctx, eventData) +} + +// PublishContractSigned 发布合同签署事件 +func (p *CertificationEventPublisher) PublishContractSigned( + ctx context.Context, + certificationID, userID string, + data map[string]interface{}, +) error { + eventData := &CertificationEventData{ + EventType: EventTypeContractSigned, + CertificationID: certificationID, + UserID: userID, + Data: data, + Timestamp: time.Now(), + Version: "1.0", + } + + return p.publishEventData(ctx, eventData) +} + +// PublishCertificationCompleted 发布认证完成事件 +func (p *CertificationEventPublisher) PublishCertificationCompleted( + ctx context.Context, + certificationID, userID string, + data map[string]interface{}, +) error { + eventData := &CertificationEventData{ + EventType: EventTypeCertificationCompleted, + CertificationID: certificationID, + UserID: userID, + Data: data, + Timestamp: time.Now(), + Version: "1.0", + } + + return p.publishEventData(ctx, eventData) +} + +// PublishCertificationFailed 发布认证失败事件 +func (p *CertificationEventPublisher) PublishCertificationFailed( + ctx context.Context, + certificationID, userID string, + data map[string]interface{}, +) error { + eventData := &CertificationEventData{ + EventType: EventTypeCertificationFailed, + CertificationID: certificationID, + UserID: userID, + Data: data, + Timestamp: time.Now(), + Version: "1.0", + } + + return p.publishEventData(ctx, eventData) +} + +// PublishStatusTransitioned 发布状态转换事件 +func (p *CertificationEventPublisher) PublishStatusTransitioned( + ctx context.Context, + certificationID, userID string, + data map[string]interface{}, +) error { + eventData := &CertificationEventData{ + EventType: EventTypeStatusTransitioned, + CertificationID: certificationID, + UserID: userID, + Data: data, + Timestamp: time.Now(), + Version: "1.0", + } + + return p.publishEventData(ctx, eventData) +} + +// ================ 内部实现 ================ + +// publishEventData 发布事件数据(带重试机制) +func (p *CertificationEventPublisher) publishEventData(ctx context.Context, eventData *CertificationEventData) error { + p.logger.Info("发布认证事件", + zap.String("event_type", eventData.EventType), + zap.String("certification_id", eventData.CertificationID), + zap.Time("timestamp", eventData.Timestamp)) + + // 尝试发布事件,带重试机制 + var lastErr error + for attempt := 0; attempt <= MaxRetries; attempt++ { + if attempt > 0 { + // 指数退避重试 + delay := time.Duration(attempt) * RetryDelay + p.logger.Warn("事件发布重试", + zap.String("event_type", eventData.EventType), + zap.Int("attempt", attempt), + zap.Duration("delay", delay)) + + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(delay): + // 继续重试 + } + } + + // 简化的事件发布:直接记录日志 + p.logger.Info("模拟事件发布", + zap.String("event_type", eventData.EventType), + zap.String("certification_id", eventData.CertificationID), + zap.Any("data", eventData.Data)) + + // TODO: 这里可以集成真正的事件总线 + // if err := p.eventBus.Publish(ctx, eventData); err != nil { + // lastErr = err + // continue + // } + + // 发布成功 + p.logger.Info("事件发布成功", + zap.String("event_type", eventData.EventType), + zap.String("certification_id", eventData.CertificationID)) + return nil + } + + // 理论上不会到达这里,因为简化实现总是成功 + return lastErr +} + +// ================ 事件处理器注册 ================ + +// RegisterEventHandlers 注册事件处理器 +func (p *CertificationEventPublisher) RegisterEventHandlers() error { + // TODO: 注册具体的事件处理器 + // 例如:发送通知、更新统计数据、触发后续流程等 + + p.logger.Info("认证事件处理器已注册") + return nil +} + +// ================ 工具方法 ================ + +// CreateEventData 创建事件数据 +func CreateEventData(eventType, certificationID, userID string, data map[string]interface{}) map[string]interface{} { + if data == nil { + data = make(map[string]interface{}) + } + + return map[string]interface{}{ + "event_type": eventType, + "certification_id": certificationID, + "user_id": userID, + "data": data, + "timestamp": time.Now(), + "version": "1.0", + } +} \ No newline at end of file diff --git a/internal/infrastructure/external/esign/certification_esign_service.go b/internal/infrastructure/external/esign/certification_esign_service.go new file mode 100644 index 0000000..20c2ea5 --- /dev/null +++ b/internal/infrastructure/external/esign/certification_esign_service.go @@ -0,0 +1,295 @@ +package esign + +import ( + "context" + "fmt" + "time" + + "go.uber.org/zap" + + "tyapi-server/internal/domains/certification/enums" + "tyapi-server/internal/domains/certification/repositories" + "tyapi-server/internal/domains/certification/value_objects" + "tyapi-server/internal/shared/esign" +) + +// ================ 常量定义 ================ + +const ( + // 企业认证超时时间 + EnterpriseAuthTimeout = 30 * time.Minute + + // 合同签署超时时间 + ContractSignTimeout = 7 * 24 * time.Hour // 7天 + + // 回调重试次数 + MaxCallbackRetries = 3 +) + +// ================ 服务实现 ================ + +// CertificationEsignService 认证e签宝服务实现 +// +// 业务职责: +// - 处理企业认证流程 +// - 处理合同生成和签署 +// - 处理e签宝回调 +// - 管理认证状态更新 +type CertificationEsignService struct { + esignClient *esign.Client + commandRepo repositories.CertificationCommandRepository + queryRepo repositories.CertificationQueryRepository + logger *zap.Logger +} + +// NewCertificationEsignService 创建认证e签宝服务 +func NewCertificationEsignService( + esignClient *esign.Client, + commandRepo repositories.CertificationCommandRepository, + queryRepo repositories.CertificationQueryRepository, + logger *zap.Logger, +) *CertificationEsignService { + return &CertificationEsignService{ + esignClient: esignClient, + commandRepo: commandRepo, + queryRepo: queryRepo, + logger: logger, + } +} + +// ================ 企业认证流程 ================ + +// StartEnterpriseAuth 开始企业认证 +// +// 业务流程: +// 1. 调用e签宝企业认证API +// 2. 更新认证记录的auth_flow_id +// 3. 更新状态为企业认证中 +// +// 参数: +// - ctx: 上下文 +// - certificationID: 认证ID +// - enterpriseInfo: 企业信息 +// +// 返回: +// - authURL: 认证URL +// - error: 错误信息 +func (s *CertificationEsignService) StartEnterpriseAuth( + ctx context.Context, + certificationID string, + enterpriseInfo *value_objects.EnterpriseInfo, +) (string, error) { + s.logger.Info("开始企业认证", + zap.String("certification_id", certificationID), + zap.String("company_name", enterpriseInfo.CompanyName)) + + // TODO: 实现e签宝企业认证API调用 + // 暂时使用模拟响应 + authFlowID := fmt.Sprintf("auth_%s_%d", certificationID, time.Now().Unix()) + authURL := fmt.Sprintf("https://esign.example.com/auth/%s", authFlowID) + + s.logger.Info("模拟调用e签宝企业认证API", + zap.String("auth_flow_id", authFlowID), + zap.String("auth_url", authURL)) + + // 更新认证记录 + if err := s.commandRepo.UpdateAuthFlowID(ctx, certificationID, authFlowID); err != nil { + s.logger.Error("更新认证流程ID失败", zap.Error(err)) + return "", fmt.Errorf("更新认证流程ID失败: %w", err) + } + + s.logger.Info("企业认证启动成功", + zap.String("certification_id", certificationID), + zap.String("auth_flow_id", authFlowID)) + + return authURL, nil +} + +// HandleEnterpriseAuthCallback 处理企业认证回调 +// +// 业务流程: +// 1. 根据回调信息查找认证记录 +// 2. 根据回调状态更新认证状态 +// 3. 如果成功,继续合同生成流程 +// +// 参数: +// - ctx: 上下文 +// - authFlowID: 认证流程ID +// - success: 是否成功 +// - message: 回调消息 +// +// 返回: +// - error: 错误信息 +func (s *CertificationEsignService) HandleEnterpriseAuthCallback( + ctx context.Context, + authFlowID string, + success bool, + message string, +) error { + s.logger.Info("处理企业认证回调", + zap.String("auth_flow_id", authFlowID), + zap.Bool("success", success)) + + // 查找认证记录 + cert, err := s.queryRepo.FindByAuthFlowID(ctx, authFlowID) + if err != nil { + s.logger.Error("根据认证流程ID查找认证记录失败", zap.Error(err)) + return fmt.Errorf("查找认证记录失败: %w", err) + } + + if success { + // 企业认证成功,更新状态 + if err := s.commandRepo.UpdateStatus(ctx, cert.ID, enums.StatusEnterpriseVerified); err != nil { + s.logger.Error("更新认证状态失败", zap.Error(err)) + return fmt.Errorf("更新认证状态失败: %w", err) + } + + s.logger.Info("企业认证成功", zap.String("certification_id", cert.ID)) + } else { + // 企业认证失败,更新状态 + if err := s.commandRepo.UpdateStatus(ctx, cert.ID, enums.StatusInfoRejected); err != nil { + s.logger.Error("更新认证状态失败", zap.Error(err)) + return fmt.Errorf("更新认证状态失败: %w", err) + } + + s.logger.Info("企业认证失败", zap.String("certification_id", cert.ID), zap.String("reason", message)) + } + + return nil +} + +// ================ 合同管理流程 ================ + +// GenerateContract 生成认证合同 +// +// 业务流程: +// 1. 调用e签宝合同生成API +// 2. 更新认证记录的合同信息 +// 3. 更新状态为合同已生成 +// +// 参数: +// - ctx: 上下文 +// - certificationID: 认证ID +// +// 返回: +// - contractSignURL: 合同签署URL +// - error: 错误信息 +func (s *CertificationEsignService) GenerateContract( + ctx context.Context, + certificationID string, +) (string, error) { + s.logger.Info("生成认证合同", zap.String("certification_id", certificationID)) + + // TODO: 实现e签宝合同生成API调用 + // 暂时使用模拟响应 + contractFileID := fmt.Sprintf("contract_%s_%d", certificationID, time.Now().Unix()) + esignFlowID := fmt.Sprintf("flow_%s_%d", certificationID, time.Now().Unix()) + contractURL := fmt.Sprintf("https://esign.example.com/contract/%s", contractFileID) + contractSignURL := fmt.Sprintf("https://esign.example.com/sign/%s", esignFlowID) + + s.logger.Info("模拟调用e签宝合同生成API", + zap.String("contract_file_id", contractFileID), + zap.String("esign_flow_id", esignFlowID)) + + // 更新认证记录 + if err := s.commandRepo.UpdateContractInfo( + ctx, + certificationID, + contractFileID, + esignFlowID, + contractURL, + contractSignURL, + ); err != nil { + s.logger.Error("更新合同信息失败", zap.Error(err)) + return "", fmt.Errorf("更新合同信息失败: %w", err) + } + + // 更新状态 + if err := s.commandRepo.UpdateStatus(ctx, certificationID, enums.StatusContractApplied); err != nil { + s.logger.Error("更新认证状态失败", zap.Error(err)) + return "", fmt.Errorf("更新认证状态失败: %w", err) + } + + s.logger.Info("认证合同生成成功", + zap.String("certification_id", certificationID), + zap.String("contract_file_id", contractFileID)) + + return contractSignURL, nil +} + +// HandleContractSignCallback 处理合同签署回调 +// +// 业务流程: +// 1. 根据回调信息查找认证记录 +// 2. 根据回调状态更新认证状态 +// 3. 如果成功,认证流程完成 +// +// 参数: +// - ctx: 上下文 +// - esignFlowID: e签宝流程ID +// - success: 是否成功 +// - signedFileURL: 已签署文件URL +// +// 返回: +// - error: 错误信息 +func (s *CertificationEsignService) HandleContractSignCallback( + ctx context.Context, + esignFlowID string, + success bool, + signedFileURL string, +) error { + s.logger.Info("处理合同签署回调", + zap.String("esign_flow_id", esignFlowID), + zap.Bool("success", success)) + + // 查找认证记录 + cert, err := s.queryRepo.FindByEsignFlowID(ctx, esignFlowID) + if err != nil { + s.logger.Error("根据e签宝流程ID查找认证记录失败", zap.Error(err)) + return fmt.Errorf("查找认证记录失败: %w", err) + } + + if success { + // 合同签署成功,认证完成 + if err := s.commandRepo.UpdateStatus(ctx, cert.ID, enums.StatusContractSigned); err != nil { + s.logger.Error("更新认证状态失败", zap.Error(err)) + return fmt.Errorf("更新认证状态失败: %w", err) + } + + s.logger.Info("认证流程完成", zap.String("certification_id", cert.ID)) + } else { + // 合同签署失败 + if err := s.commandRepo.UpdateStatus(ctx, cert.ID, enums.StatusContractRejected); err != nil { + s.logger.Error("更新认证状态失败", zap.Error(err)) + return fmt.Errorf("更新认证状态失败: %w", err) + } + + s.logger.Info("合同签署失败", zap.String("certification_id", cert.ID)) + } + + return nil +} + +// ================ 辅助方法 ================ + +// GetContractSignURL 获取合同签署URL +// +// 参数: +// - ctx: 上下文 +// - certificationID: 认证ID +// +// 返回: +// - signURL: 签署URL +// - error: 错误信息 +func (s *CertificationEsignService) GetContractSignURL(ctx context.Context, certificationID string) (string, error) { + cert, err := s.queryRepo.GetByID(ctx, certificationID) + if err != nil { + return "", fmt.Errorf("获取认证信息失败: %w", err) + } + + if cert.ContractSignURL == "" { + return "", fmt.Errorf("合同签署URL尚未生成") + } + + return cert.ContractSignURL, nil +} \ No newline at end of file diff --git a/internal/infrastructure/http/handlers/certification_handler.go b/internal/infrastructure/http/handlers/certification_handler.go index 1df8cce..c9d8661 100644 --- a/internal/infrastructure/http/handlers/certification_handler.go +++ b/internal/infrastructure/http/handlers/certification_handler.go @@ -1,10 +1,6 @@ package handlers import ( - "bytes" - "io" - "time" - "github.com/gin-gonic/gin" "go.uber.org/zap" @@ -12,157 +8,150 @@ import ( "tyapi-server/internal/application/certification/dto/commands" "tyapi-server/internal/application/certification/dto/queries" "tyapi-server/internal/shared/interfaces" + "tyapi-server/internal/shared/middleware" ) -// CertificationHandler 认证处理器 -// 负责处理HTTP请求,参数验证,调用应用服务,返回HTTP响应 +// CertificationHandler 认证HTTP处理器 type CertificationHandler struct { - certAppService certification.CertificationApplicationService - esignCallbackService certification.EsignCallbackApplicationService - response interfaces.ResponseBuilder - validator interfaces.RequestValidator - logger *zap.Logger + appService certification.CertificationApplicationService + response interfaces.ResponseBuilder + validator interfaces.RequestValidator + logger *zap.Logger + jwtAuth *middleware.JWTAuthMiddleware } // NewCertificationHandler 创建认证处理器 func NewCertificationHandler( - certAppService certification.CertificationApplicationService, - esignCallbackService certification.EsignCallbackApplicationService, + appService certification.CertificationApplicationService, response interfaces.ResponseBuilder, validator interfaces.RequestValidator, logger *zap.Logger, + jwtAuth *middleware.JWTAuthMiddleware, ) *CertificationHandler { return &CertificationHandler{ - certAppService: certAppService, - esignCallbackService: esignCallbackService, - response: response, - validator: validator, - logger: logger, + appService: appService, + response: response, + validator: validator, + logger: logger, + jwtAuth: jwtAuth, } } -// GetCertificationStatus 获取认证状态 -// @Summary 获取认证状态 -// @Description 获取当前用户的认证状态信息,包括认证进度、当前状态等 -// @Tags 企业认证 +// ================ 认证申请管理 ================ + +// CreateCertification 创建认证申请 +// @Summary 创建认证申请 +// @Description 为用户创建企业认证申请 +// @Tags 认证管理 // @Accept json // @Produce json // @Security Bearer -// @Success 200 {object} map[string]interface{} "获取认证状态成功" -// @Failure 401 {object} map[string]interface{} "用户未登录" +// @Param request body commands.CreateCertificationCommand true "创建认证申请请求" +// @Success 201 {object} responses.CertificationResponse "认证申请创建成功" +// @Failure 400 {object} map[string]interface{} "请求参数错误" +// @Failure 401 {object} map[string]interface{} "未认证" // @Failure 500 {object} map[string]interface{} "服务器内部错误" -// @Router /api/v1/certification/status [get] -func (h *CertificationHandler) GetCertificationStatus(c *gin.Context) { - userID := c.GetString("user_id") - if userID == "" { +// @Router /api/v1/certifications [post] +func (h *CertificationHandler) CreateCertification(c *gin.Context) { + var cmd commands.CreateCertificationCommand + cmd.UserID = h.getCurrentUserID(c) + if cmd.UserID == "" { h.response.Unauthorized(c, "用户未登录") return } - query := &queries.GetCertificationStatusQuery{ - UserID: userID, - } - - result, err := h.certAppService.GetCertificationStatus(c.Request.Context(), query) + result, err := h.appService.CreateCertification(c.Request.Context(), &cmd) if err != nil { - h.logger.Error("获取认证状态失败", zap.Error(err)) + h.logger.Error("创建认证申请失败", zap.Error(err), zap.String("user_id", cmd.UserID)) h.response.BadRequest(c, err.Error()) return } - h.response.Success(c, result, "获取认证状态成功") + h.response.Created(c, result, "认证申请创建成功") } -// GetCertificationDetails 获取认证详情 +// GetCertification 获取认证详情 // @Summary 获取认证详情 -// @Description 获取当前用户的详细认证信息,包括企业信息、认证记录等 -// @Tags 企业认证 +// @Description 根据认证ID获取认证详情 +// @Tags 认证管理 // @Accept json // @Produce json // @Security Bearer -// @Success 200 {object} map[string]interface{} "获取认证详情成功" -// @Failure 401 {object} map[string]interface{} "用户未登录" +// @Param id path string true "认证ID" +// @Success 200 {object} responses.CertificationResponse "获取认证详情成功" +// @Failure 400 {object} map[string]interface{} "请求参数错误" +// @Failure 401 {object} map[string]interface{} "未认证" +// @Failure 404 {object} map[string]interface{} "认证记录不存在" // @Failure 500 {object} map[string]interface{} "服务器内部错误" -// @Router /api/v1/certification/details [get] -func (h *CertificationHandler) GetCertificationDetails(c *gin.Context) { - userID := c.GetString("user_id") +// @Router /api/v1/certifications/{id} [get] +func (h *CertificationHandler) GetCertification(c *gin.Context) { + userID := h.getCurrentUserID(c) if userID == "" { h.response.Unauthorized(c, "用户未登录") return } - query := &queries.GetCertificationDetailsQuery{ - UserID: userID, + certificationID := c.Param("id") + if certificationID == "" { + h.response.BadRequest(c, "认证ID不能为空") + return } - result, err := h.certAppService.GetCertificationDetails(c.Request.Context(), query) + query := &queries.GetCertificationQuery{ + CertificationID: certificationID, + UserID: userID, + } + + result, err := h.appService.GetCertification(c.Request.Context(), query) if err != nil { - h.logger.Error("获取认证详情失败", zap.Error(err)) - h.response.BadRequest(c, err.Error()) + h.logger.Error("获取认证详情失败", zap.Error(err), zap.String("certification_id", certificationID)) + h.response.NotFound(c, "认证记录不存在") return } h.response.Success(c, result, "获取认证详情成功") } -// GetCertificationProgress 获取认证进度 -// @Summary 获取认证进度 -// @Description 获取当前用户的认证进度百分比和下一步操作提示 -// @Tags 企业认证 -// @Accept json -// @Produce json -// @Security Bearer -// @Success 200 {object} map[string]interface{} "获取认证进度成功" -// @Failure 401 {object} map[string]interface{} "用户未登录" -// @Failure 500 {object} map[string]interface{} "服务器内部错误" -// @Router /api/v1/certification/progress [get] -func (h *CertificationHandler) GetCertificationProgress(c *gin.Context) { - userID := c.GetString("user_id") - if userID == "" { - h.response.Unauthorized(c, "用户未登录") - return - } - - result, err := h.certAppService.GetCertificationProgress(c.Request.Context(), userID) - if err != nil { - h.logger.Error("获取认证进度失败", zap.Error(err)) - h.response.BadRequest(c, err.Error()) - return - } - - h.response.Success(c, result, "获取认证进度成功") -} +// ================ 企业信息管理 ================ // SubmitEnterpriseInfo 提交企业信息 // @Summary 提交企业信息 -// @Description 提交企业四要素信息(企业名称、统一社会信用代码、法定代表人姓名、法定代表人身份证),完成企业信息验证。如果用户没有认证申请,系统会自动创建 -// @Tags 企业认证 +// @Description 提交企业认证所需的企业信息 +// @Tags 认证管理 // @Accept json // @Produce json // @Security Bearer -// @Param request body commands.SubmitEnterpriseInfoCommand true "企业信息提交请求" -// @Success 200 {object} map[string]interface{} "企业信息提交成功" -// @Failure 400 {object} map[string]interface{} "请求参数错误或验证码无效" -// @Failure 401 {object} map[string]interface{} "用户未登录" +// @Param id path string true "认证ID" +// @Param request body commands.SubmitEnterpriseInfoCommand true "提交企业信息请求" +// @Success 200 {object} responses.CertificationResponse "企业信息提交成功" +// @Failure 400 {object} map[string]interface{} "请求参数错误" +// @Failure 401 {object} map[string]interface{} "未认证" +// @Failure 404 {object} map[string]interface{} "认证记录不存在" // @Failure 500 {object} map[string]interface{} "服务器内部错误" -// @Router /api/v1/certification/submit-enterprise-info [post] +// @Router /api/v1/certifications/{id}/enterprise-info [post] func (h *CertificationHandler) SubmitEnterpriseInfo(c *gin.Context) { - userID := c.GetString("user_id") + userID := h.getCurrentUserID(c) if userID == "" { h.response.Unauthorized(c, "用户未登录") return } + certificationID := c.Param("id") + if certificationID == "" { + h.response.BadRequest(c, "认证ID不能为空") + return + } + var cmd commands.SubmitEnterpriseInfoCommand if err := h.validator.BindAndValidate(c, &cmd); err != nil { return } - + cmd.CertificationID = certificationID cmd.UserID = userID - result, err := h.certAppService.SubmitEnterpriseInfo(c.Request.Context(), &cmd) + result, err := h.appService.SubmitEnterpriseInfo(c.Request.Context(), &cmd) if err != nil { - h.logger.Error("提交企业信息失败", zap.Error(err)) + h.logger.Error("提交企业信息失败", zap.Error(err), zap.String("certification_id", certificationID)) h.response.BadRequest(c, err.Error()) return } @@ -170,57 +159,38 @@ func (h *CertificationHandler) SubmitEnterpriseInfo(c *gin.Context) { h.response.Success(c, result, "企业信息提交成功") } -// GetEnterpriseAuthURL 获取企业认证链接 -// @Summary 获取企业认证链接 -// @Description 获取e签宝企业认证链接,用户可通过该链接完成企业认证 -// @Tags 企业认证 +// ================ 合同管理 ================ + +// ApplyContract 申请合同签署 +// @Summary 申请合同签署 +// @Description 申请企业认证合同签署 +// @Tags 认证管理 // @Accept json // @Produce json // @Security Bearer -// @Success 200 {object} map[string]interface{} "获取企业认证链接成功" -// @Failure 401 {object} map[string]interface{} "用户未登录" -// @Failure 400 {object} map[string]interface{} "企业信息未提交或认证状态异常" +// @Param request body commands.ApplyContractCommand true "申请合同请求" +// @Success 200 {object} responses.ContractSignUrlResponse "合同申请成功" +// @Failure 400 {object} map[string]interface{} "请求参数错误" +// @Failure 401 {object} map[string]interface{} "未认证" +// @Failure 404 {object} map[string]interface{} "认证记录不存在" // @Failure 500 {object} map[string]interface{} "服务器内部错误" -// @Router /api/v1/certification/enterprise-auth-url [get] -func (h *CertificationHandler) GetEnterpriseAuthURL(c *gin.Context) { - userID := c.GetString("user_id") - if userID == "" { - h.response.Unauthorized(c, "用户未登录") - return - } - - result, err := h.certAppService.GetEnterpriseAuthURL(c.Request.Context(), userID) - if err != nil { - h.logger.Error("获取企业认证链接失败", zap.Error(err)) - h.response.BadRequest(c, err.Error()) - return - } - - h.response.Success(c, result, "获取企业认证链接成功") -} - -// ApplyContract 申请合同 -// @Summary 申请合同 -// @Description 为企业认证用户申请合同,生成合同文档 -// @Tags 企业认证 -// @Accept json -// @Produce json -// @Security Bearer -// @Success 200 {object} map[string]interface{} "合同申请成功" -// @Failure 401 {object} map[string]interface{} "用户未登录" -// @Failure 400 {object} map[string]interface{} "企业认证未完成或合同申请失败" -// @Failure 500 {object} map[string]interface{} "服务器内部错误" -// @Router /api/v1/certification/apply-contract [post] +// @Router /api/v1/certifications/apply-contract [post] func (h *CertificationHandler) ApplyContract(c *gin.Context) { - userID := c.GetString("user_id") + userID := h.getCurrentUserID(c) if userID == "" { h.response.Unauthorized(c, "用户未登录") return } - result, err := h.certAppService.ApplyContract(c.Request.Context(), userID) + var cmd commands.ApplyContractCommand + if err := h.validator.BindAndValidate(c, &cmd); err != nil { + return + } + cmd.UserID = userID + + result, err := h.appService.ApplyContract(c.Request.Context(), &cmd) if err != nil { - h.logger.Error("申请合同失败", zap.Error(err)) + h.logger.Error("申请合同失败", zap.Error(err), zap.String("certification_id", cmd.CertificationID)) h.response.BadRequest(c, err.Error()) return } @@ -228,161 +198,282 @@ func (h *CertificationHandler) ApplyContract(c *gin.Context) { h.response.Success(c, result, "合同申请成功") } -// GetContractSignURL 获取合同签署链接 -// @Summary 获取合同签署链接 -// @Description 获取e签宝合同签署链接,用户可通过该链接完成合同签署 -// @Tags 企业认证 +// ================ 重试操作 ================ + +// RetryOperation 重试操作 +// @Summary 重试操作 +// @Description 重试失败的企业认证或合同申请操作 +// @Tags 认证管理 // @Accept json // @Produce json // @Security Bearer -// @Success 200 {object} map[string]interface{} "获取合同签署链接成功" -// @Failure 401 {object} map[string]interface{} "用户未登录" -// @Failure 400 {object} map[string]interface{} "合同未申请或签署状态异常" +// @Param request body commands.RetryOperationCommand true "重试操作请求" +// @Success 200 {object} responses.CertificationResponse "重试操作成功" +// @Failure 400 {object} map[string]interface{} "请求参数错误" +// @Failure 401 {object} map[string]interface{} "未认证" +// @Failure 404 {object} map[string]interface{} "认证记录不存在" // @Failure 500 {object} map[string]interface{} "服务器内部错误" -// @Router /api/v1/certification/contract-sign-url [get] -func (h *CertificationHandler) GetContractSignURL(c *gin.Context) { - userID := c.GetString("user_id") +// @Router /api/v1/certifications/retry [post] +func (h *CertificationHandler) RetryOperation(c *gin.Context) { + userID := h.getCurrentUserID(c) if userID == "" { h.response.Unauthorized(c, "用户未登录") return } - cmd := &commands.GetContractSignURLCommand{ - UserID: userID, + var cmd commands.RetryOperationCommand + if err := h.validator.BindAndValidate(c, &cmd); err != nil { + return } + cmd.UserID = userID - result, err := h.certAppService.GetContractSignURL(c.Request.Context(), cmd) + result, err := h.appService.RetryOperation(c.Request.Context(), &cmd) if err != nil { - h.logger.Error("获取合同签署链接失败", zap.Error(err)) + h.logger.Error("重试操作失败", zap.Error(err), zap.String("certification_id", cmd.CertificationID)) h.response.BadRequest(c, err.Error()) return } - h.response.Success(c, result, "获取合同签署链接成功") + h.response.Success(c, result, "重试操作成功") } -// EsignCallback e签宝回调 -// @Summary e签宝回调接口 -// @Description 接收e签宝认证和签署的回调通知 -// @Tags 企业认证 +// ================ 查询操作 ================ + +// GetUserCertifications 获取用户认证列表 +// @Summary 获取用户认证列表 +// @Description 获取当前用户的认证申请列表 +// @Tags 认证管理 // @Accept json // @Produce json -// @Success 200 {object} map[string]interface{} "回调处理成功" -// @Failure 400 {object} map[string]interface{} "回调参数错误" +// @Security Bearer +// @Param status query string false "认证状态" +// @Param include_completed query bool false "是否包含已完成" +// @Param include_failed query bool false "是否包含失败" +// @Param page query int false "页码" default(1) +// @Param page_size query int false "每页数量" default(10) +// @Success 200 {object} responses.CertificationListResponse "获取用户认证列表成功" +// @Failure 401 {object} map[string]interface{} "未认证" // @Failure 500 {object} map[string]interface{} "服务器内部错误" -// @Router /api/v1/certification/esign-callback [post] -func (h *CertificationHandler) EsignCallback(c *gin.Context) { - // 记录请求基本信息 - h.logger.Info("收到e签宝回调请求", - zap.String("method", c.Request.Method), - zap.String("url", c.Request.URL.String()), - zap.String("remote_addr", c.ClientIP()), - zap.String("user_agent", c.GetHeader("User-Agent")), - ) - - // 记录所有请求头 - headers := make(map[string]string) - for key, values := range c.Request.Header { - if len(values) > 0 { - headers[key] = values[0] - } - } - h.logger.Info("回调请求头信息", zap.Any("headers", headers)) - - // 记录URL查询参数 - queryParams := make(map[string]string) - for key, values := range c.Request.URL.Query() { - if len(values) > 0 { - queryParams[key] = values[0] - } - } - if len(queryParams) > 0 { - h.logger.Info("回调URL查询参数", zap.Any("query_params", queryParams)) +// @Router /api/v1/certifications/user [get] +func (h *CertificationHandler) GetUserCertifications(c *gin.Context) { + userID := h.getCurrentUserID(c) + if userID == "" { + h.response.Unauthorized(c, "用户未登录") + return } - // 读取并记录请求体 - var requestBody interface{} - var callbackData map[string]interface{} - if c.Request.Body != nil { - // 读取请求体 - bodyBytes, err := c.GetRawData() - if err != nil { - h.logger.Error("读取回调请求体失败", zap.Error(err)) - h.response.BadRequest(c, "读取请求体失败") - return - } + var query queries.GetUserCertificationsQuery + if err := h.validator.BindAndValidate(c, &query); err != nil { + return + } + query.UserID = userID - // 尝试解析为JSON - if err := c.ShouldBindJSON(&callbackData); err == nil { - requestBody = callbackData - } else { - // 如果不是JSON,记录原始字符串 - requestBody = string(bodyBytes) - h.logger.Error("回调请求体不是有效的JSON格式", zap.Error(err)) - h.response.BadRequest(c, "请求体格式错误") - return - } - h.logger.Info("回调请求体内容", zap.Any("body", requestBody)) - - // 重新设置请求体,以便后续处理 - c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) + result, err := h.appService.GetUserCertifications(c.Request.Context(), &query) + if err != nil { + h.logger.Error("获取用户认证列表失败", zap.Error(err), zap.String("user_id", userID)) + h.response.BadRequest(c, err.Error()) + return } - // 记录Content-Type - contentType := c.GetHeader("Content-Type") - h.logger.Info("回调请求Content-Type", zap.String("content_type", contentType)) - - // 记录Content-Length - contentLength := c.GetHeader("Content-Length") - if contentLength != "" { - h.logger.Info("回调请求Content-Length", zap.String("content_length", contentLength)) - } - - // 记录时间戳 - h.logger.Info("回调请求时间", - zap.Time("request_time", time.Now()), - zap.String("request_id", c.GetHeader("X-Request-ID")), - ) - - // 记录完整的请求信息摘要 - h.logger.Info("e签宝回调完整信息摘要", - zap.String("method", c.Request.Method), - zap.String("url", c.Request.URL.String()), - zap.String("client_ip", c.ClientIP()), - zap.String("content_type", contentType), - zap.Any("headers", headers), - zap.Any("query_params", queryParams), - zap.Any("body", requestBody), - ) - - // 处理回调数据 - if callbackData != nil { - // 构建请求头映射 - headers := make(map[string]string) - for key, values := range c.Request.Header { - if len(values) > 0 { - headers[key] = values[0] - } - } - - // 构建查询参数映射 - queryParams := make(map[string]string) - for key, values := range c.Request.URL.Query() { - if len(values) > 0 { - queryParams[key] = values[0] - } - } - - if err := h.esignCallbackService.HandleCallback(c.Request.Context(), callbackData, headers, queryParams); err != nil { - h.logger.Error("处理e签宝回调失败", zap.Error(err)) - h.response.BadRequest(c, "回调处理失败: "+err.Error()) - return - } - } - - // 返回成功响应 - c.JSON(200, map[string]interface{}{ - "code": "200", - "msg": "success", - }) + h.response.Success(c, result, "获取用户认证列表成功") +} + +// ListCertifications 获取认证列表(管理员) +// @Summary 获取认证列表 +// @Description 管理员获取认证申请列表 +// @Tags 认证管理 +// @Accept json +// @Produce json +// @Security Bearer +// @Param page query int false "页码" default(1) +// @Param page_size query int false "每页数量" default(10) +// @Param sort_by query string false "排序字段" +// @Param sort_order query string false "排序方向" Enums(asc, desc) +// @Param status query string false "认证状态" +// @Param user_id query string false "用户ID" +// @Param company_name query string false "公司名称" +// @Param legal_person_name query string false "法人姓名" +// @Param search_keyword query string false "搜索关键词" +// @Success 200 {object} responses.CertificationListResponse "获取认证列表成功" +// @Failure 401 {object} map[string]interface{} "未认证" +// @Failure 403 {object} map[string]interface{} "权限不足" +// @Failure 500 {object} map[string]interface{} "服务器内部错误" +// @Router /api/v1/certifications [get] +func (h *CertificationHandler) ListCertifications(c *gin.Context) { + userID := h.getCurrentUserID(c) + if userID == "" { + h.response.Unauthorized(c, "用户未登录") + return + } + + var query queries.ListCertificationsQuery + if err := h.validator.BindAndValidate(c, &query); err != nil { + return + } + + result, err := h.appService.ListCertifications(c.Request.Context(), &query) + if err != nil { + h.logger.Error("获取认证列表失败", zap.Error(err)) + h.response.BadRequest(c, err.Error()) + return + } + + h.response.Success(c, result, "获取认证列表成功") +} + +// GetCertificationStatistics 获取认证统计 +// @Summary 获取认证统计 +// @Description 获取认证相关的统计数据 +// @Tags 认证管理 +// @Accept json +// @Produce json +// @Security Bearer +// @Param start_date query string true "开始日期" format(date) +// @Param end_date query string true "结束日期" format(date) +// @Param period query string false "统计周期" Enums(daily, weekly, monthly, yearly) default(daily) +// @Param group_by query []string false "分组字段" +// @Param user_ids query []string false "用户ID列表" +// @Param statuses query []string false "状态列表" +// @Success 200 {object} responses.CertificationStatisticsResponse "获取认证统计成功" +// @Failure 400 {object} map[string]interface{} "请求参数错误" +// @Failure 401 {object} map[string]interface{} "未认证" +// @Failure 500 {object} map[string]interface{} "服务器内部错误" +// @Router /api/v1/certifications/statistics [get] +func (h *CertificationHandler) GetCertificationStatistics(c *gin.Context) { + userID := h.getCurrentUserID(c) + if userID == "" { + h.response.Unauthorized(c, "用户未登录") + return + } + + var query queries.GetCertificationStatisticsQuery + if err := h.validator.BindAndValidate(c, &query); err != nil { + return + } + + result, err := h.appService.GetCertificationStatistics(c.Request.Context(), &query) + if err != nil { + h.logger.Error("获取认证统计失败", zap.Error(err)) + h.response.BadRequest(c, err.Error()) + return + } + + h.response.Success(c, result, "获取认证统计成功") +} + +// ================ 回调处理 ================ + +// HandleEsignCallback 处理e签宝回调 +// @Summary 处理e签宝回调 +// @Description 处理e签宝的企业认证和合同签署回调 +// @Tags 认证管理 +// @Accept json +// @Produce json +// @Param request body commands.EsignCallbackCommand true "e签宝回调数据" +// @Success 200 {object} responses.CallbackResponse "回调处理成功" +// @Failure 400 {object} map[string]interface{} "请求参数错误" +// @Failure 500 {object} map[string]interface{} "服务器内部错误" +// @Router /api/v1/certifications/callbacks [post] +func (h *CertificationHandler) HandleEsignCallback(c *gin.Context) { + var cmd commands.EsignCallbackCommand + if err := h.validator.BindAndValidate(c, &cmd); err != nil { + return + } + + result, err := h.appService.HandleEsignCallback(c.Request.Context(), &cmd) + if err != nil { + h.logger.Error("处理e签宝回调失败", zap.Error(err), zap.String("certification_id", cmd.CertificationID)) + h.response.BadRequest(c, err.Error()) + return + } + + h.response.Success(c, result, "回调处理成功") +} + +// ================ 管理员操作 ================ + +// ForceTransitionStatus 强制状态转换(管理员) +// @Summary 强制状态转换 +// @Description 管理员强制转换认证状态 +// @Tags 认证管理 +// @Accept json +// @Produce json +// @Security Bearer +// @Param request body commands.ForceTransitionStatusCommand true "强制状态转换请求" +// @Success 200 {object} responses.CertificationResponse "状态转换成功" +// @Failure 400 {object} map[string]interface{} "请求参数错误" +// @Failure 401 {object} map[string]interface{} "未认证" +// @Failure 403 {object} map[string]interface{} "权限不足" +// @Failure 500 {object} map[string]interface{} "服务器内部错误" +// @Router /api/v1/certifications/force-transition [post] +func (h *CertificationHandler) ForceTransitionStatus(c *gin.Context) { + adminID := h.getCurrentUserID(c) + if adminID == "" { + h.response.Unauthorized(c, "用户未登录") + return + } + + var cmd commands.ForceTransitionStatusCommand + if err := h.validator.BindAndValidate(c, &cmd); err != nil { + return + } + cmd.AdminID = adminID + + result, err := h.appService.ForceTransitionStatus(c.Request.Context(), &cmd) + if err != nil { + h.logger.Error("强制状态转换失败", zap.Error(err), zap.String("certification_id", cmd.CertificationID)) + h.response.BadRequest(c, err.Error()) + return + } + + h.response.Success(c, result, "状态转换成功") +} + +// GetSystemMonitoring 获取系统监控数据 +// @Summary 获取系统监控数据 +// @Description 获取认证系统的监控数据 +// @Tags 认证管理 +// @Accept json +// @Produce json +// @Security Bearer +// @Param time_range query string false "时间范围" Enums(1h, 6h, 24h, 7d, 30d) default(24h) +// @Param metrics query []string false "监控指标" +// @Success 200 {object} responses.SystemMonitoringResponse "获取系统监控数据成功" +// @Failure 400 {object} map[string]interface{} "请求参数错误" +// @Failure 401 {object} map[string]interface{} "未认证" +// @Failure 403 {object} map[string]interface{} "权限不足" +// @Failure 500 {object} map[string]interface{} "服务器内部错误" +// @Router /api/v1/certifications/monitoring [get] +func (h *CertificationHandler) GetSystemMonitoring(c *gin.Context) { + userID := h.getCurrentUserID(c) + if userID == "" { + h.response.Unauthorized(c, "用户未登录") + return + } + + var query queries.GetSystemMonitoringQuery + if err := h.validator.BindAndValidate(c, &query); err != nil { + return + } + + result, err := h.appService.GetSystemMonitoring(c.Request.Context(), &query) + if err != nil { + h.logger.Error("获取系统监控数据失败", zap.Error(err)) + h.response.BadRequest(c, err.Error()) + return + } + + h.response.Success(c, result, "获取系统监控数据成功") +} + +// ================ 辅助方法 ================ + +// getCurrentUserID 获取当前用户ID +func (h *CertificationHandler) getCurrentUserID(c *gin.Context) string { + if userID, exists := c.Get("user_id"); exists { + if id, ok := userID.(string); ok { + return id + } + } + return "" } diff --git a/internal/infrastructure/http/routes/certification_routes.go b/internal/infrastructure/http/routes/certification_routes.go index fafc307..c8810b9 100644 --- a/internal/infrastructure/http/routes/certification_routes.go +++ b/internal/infrastructure/http/routes/certification_routes.go @@ -1,58 +1,100 @@ package routes import ( - "tyapi-server/internal/infrastructure/http/handlers" - sharedhttp "tyapi-server/internal/shared/http" - "tyapi-server/internal/shared/middleware" - "go.uber.org/zap" + + "tyapi-server/internal/infrastructure/http/handlers" + "tyapi-server/internal/shared/http" + "tyapi-server/internal/shared/middleware" ) -// CertificationRoutes 认证路由注册器 +// CertificationRoutes 认证路由 type CertificationRoutes struct { - handler *handlers.CertificationHandler - authMiddleware *middleware.JWTAuthMiddleware - logger *zap.Logger + handler *handlers.CertificationHandler + router *http.GinRouter + logger *zap.Logger + auth *middleware.JWTAuthMiddleware + optional *middleware.OptionalAuthMiddleware } -// NewCertificationRoutes 创建认证路由注册器 +// NewCertificationRoutes 创建认证路由 func NewCertificationRoutes( handler *handlers.CertificationHandler, - authMiddleware *middleware.JWTAuthMiddleware, + router *http.GinRouter, logger *zap.Logger, + auth *middleware.JWTAuthMiddleware, + optional *middleware.OptionalAuthMiddleware, ) *CertificationRoutes { return &CertificationRoutes{ - handler: handler, - authMiddleware: authMiddleware, - logger: logger, + handler: handler, + router: router, + logger: logger, + auth: auth, + optional: optional, } } -// Register 注册认证相关路由 -func (r *CertificationRoutes) Register(router *sharedhttp.GinRouter) { - // 认证相关路由组 - engine := router.GetEngine() - certificationGroup := engine.Group("/api/v1/certification") - certificationGroup.Use(r.authMiddleware.Handle()) +// Register 注册认证路由 +func (r *CertificationRoutes) Register(router *http.GinRouter) { + // 认证管理路由组 + certificationGroup := router.GetEngine().Group("/api/v1/certifications") { - // 认证状态查询 - certificationGroup.GET("/status", r.handler.GetCertificationStatus) // 获取认证状态 - certificationGroup.GET("/details", r.handler.GetCertificationDetails) // 获取认证详情 - certificationGroup.GET("/progress", r.handler.GetCertificationProgress) // 获取认证进度 + // 需要认证的路由 + authGroup := certificationGroup.Group("") + authGroup.Use(r.auth.Handle()) + { + authGroup.GET("/user", r.handler.GetUserCertifications) // 获取用户认证列表 + authGroup.GET("", r.handler.ListCertifications) // 查询认证列表(管理员) + authGroup.GET("/statistics", r.handler.GetCertificationStatistics) // 获取认证统计 - // 企业信息管理 - certificationGroup.POST("/submit-enterprise-info", r.handler.SubmitEnterpriseInfo) // 提交企业信息(自动创建认证申请) + // 1. 获取认证详情 + authGroup.GET("/:id", r.handler.GetCertification) - // 企业认证 - certificationGroup.GET("/enterprise-auth-url", r.handler.GetEnterpriseAuthURL) // 获取企业认证链接 + // 2. 提交企业信息 + authGroup.POST("/:id/enterprise-info", r.handler.SubmitEnterpriseInfo) - // 合同管理 - certificationGroup.POST("/apply-contract", r.handler.ApplyContract) // 申请合同 - certificationGroup.GET("/contract-sign-url", r.handler.GetContractSignURL) // 获取合同签署链接 + // 合同管理 + authGroup.POST("/apply-contract", r.handler.ApplyContract) // 申请合同签署 + + // 重试操作 + authGroup.POST("/retry", r.handler.RetryOperation) // 重试操作 + + // 管理员操作 + authGroup.POST("/force-transition", r.handler.ForceTransitionStatus) // 强制状态转换 + authGroup.GET("/monitoring", r.handler.GetSystemMonitoring) // 获取系统监控数据 + } + + // 回调路由(不需要认证,但需要验证签名) + callbackGroup := certificationGroup.Group("/callbacks") + { + callbackGroup.POST("", r.handler.HandleEsignCallback) // e签宝回调(统一处理企业认证和合同签署回调) + } } - callbackGroup := engine.Group("/api/v1/certification") - // e签宝回调 - callbackGroup.POST("/esign-callback", r.handler.EsignCallback) // e签宝回调 r.logger.Info("认证路由注册完成") } + +// GetRoutes 获取路由信息(用于调试) +func (r *CertificationRoutes) GetRoutes() []RouteInfo { + return []RouteInfo{ + {Method: "POST", Path: "/api/v1/certifications", Handler: "CreateCertification", Auth: true}, + {Method: "GET", Path: "/api/v1/certifications/:id", Handler: "GetCertification", Auth: true}, + {Method: "GET", Path: "/api/v1/certifications/user", Handler: "GetUserCertifications", Auth: true}, + {Method: "GET", Path: "/api/v1/certifications", Handler: "ListCertifications", Auth: true}, + {Method: "GET", Path: "/api/v1/certifications/statistics", Handler: "GetCertificationStatistics", Auth: true}, + {Method: "POST", Path: "/api/v1/certifications/:id/enterprise-info", Handler: "SubmitEnterpriseInfo", Auth: true}, + {Method: "POST", Path: "/api/v1/certifications/apply-contract", Handler: "ApplyContract", Auth: true}, + {Method: "POST", Path: "/api/v1/certifications/retry", Handler: "RetryOperation", Auth: true}, + {Method: "POST", Path: "/api/v1/certifications/force-transition", Handler: "ForceTransitionStatus", Auth: true}, + {Method: "GET", Path: "/api/v1/certifications/monitoring", Handler: "GetSystemMonitoring", Auth: true}, + {Method: "POST", Path: "/api/v1/certifications/callbacks", Handler: "HandleEsignCallback", Auth: false}, + } +} + +// RouteInfo 路由信息 +type RouteInfo struct { + Method string + Path string + Handler string + Auth bool +}