package certification import ( "context" "encoding/json" "fmt" "io" "net/http" "strings" "time" "hyapi-server/internal/application/certification/dto/commands" "hyapi-server/internal/application/certification/dto/queries" "hyapi-server/internal/application/certification/dto/responses" "hyapi-server/internal/config" api_service "hyapi-server/internal/domains/api/services" "hyapi-server/internal/domains/certification/entities" certification_value_objects "hyapi-server/internal/domains/certification/entities/value_objects" "hyapi-server/internal/domains/certification/enums" "hyapi-server/internal/domains/certification/repositories" "hyapi-server/internal/domains/certification/services" finance_service "hyapi-server/internal/domains/finance/services" user_entities "hyapi-server/internal/domains/user/entities" user_service "hyapi-server/internal/domains/user/services" "hyapi-server/internal/infrastructure/external/notification" "hyapi-server/internal/infrastructure/external/storage" "hyapi-server/internal/shared/database" "hyapi-server/internal/shared/esign" sharedOCR "hyapi-server/internal/shared/ocr" "go.uber.org/zap" ) // CertificationApplicationServiceImpl 认证应用服务实现 // 负责用例协调,DTO转换,是应用层的核心组件 type CertificationApplicationServiceImpl struct { // 领域服务依赖 aggregateService services.CertificationAggregateService userAggregateService user_service.UserAggregateService smsCodeService *user_service.SMSCodeService esignClient *esign.Client esignConfig *esign.Config qiniuStorageService *storage.QiNiuStorageService contractAggregateService user_service.ContractAggregateService walletAggregateService finance_service.WalletAggregateService apiUserAggregateService api_service.ApiUserAggregateService enterpriseInfoSubmitRecordService *services.EnterpriseInfoSubmitRecordService ocrService sharedOCR.OCRService // 仓储依赖 queryRepository repositories.CertificationQueryRepository enterpriseInfoSubmitRecordRepo repositories.EnterpriseInfoSubmitRecordRepository txManager *database.TransactionManager wechatWorkService *notification.WeChatWorkService logger *zap.Logger config *config.Config } // NewCertificationApplicationService 创建认证应用服务 func NewCertificationApplicationService( aggregateService services.CertificationAggregateService, userAggregateService user_service.UserAggregateService, queryRepository repositories.CertificationQueryRepository, enterpriseInfoSubmitRecordRepo repositories.EnterpriseInfoSubmitRecordRepository, smsCodeService *user_service.SMSCodeService, esignClient *esign.Client, esignConfig *esign.Config, qiniuStorageService *storage.QiNiuStorageService, contractAggregateService user_service.ContractAggregateService, walletAggregateService finance_service.WalletAggregateService, apiUserAggregateService api_service.ApiUserAggregateService, enterpriseInfoSubmitRecordService *services.EnterpriseInfoSubmitRecordService, ocrService sharedOCR.OCRService, txManager *database.TransactionManager, logger *zap.Logger, cfg *config.Config, ) CertificationApplicationService { var wechatSvc *notification.WeChatWorkService if cfg != nil && cfg.WechatWork.WebhookURL != "" { wechatSvc = notification.NewWeChatWorkService(cfg.WechatWork.WebhookURL, cfg.WechatWork.Secret, logger) } return &CertificationApplicationServiceImpl{ aggregateService: aggregateService, userAggregateService: userAggregateService, queryRepository: queryRepository, enterpriseInfoSubmitRecordRepo: enterpriseInfoSubmitRecordRepo, smsCodeService: smsCodeService, esignClient: esignClient, esignConfig: esignConfig, qiniuStorageService: qiniuStorageService, contractAggregateService: contractAggregateService, walletAggregateService: walletAggregateService, apiUserAggregateService: apiUserAggregateService, enterpriseInfoSubmitRecordService: enterpriseInfoSubmitRecordService, ocrService: ocrService, txManager: txManager, wechatWorkService: wechatSvc, logger: logger, config: cfg, } } // ================ 用户操作用例 ================ // SubmitEnterpriseInfo 提交企业信息 func (s *CertificationApplicationServiceImpl) SubmitEnterpriseInfo( ctx context.Context, cmd *commands.SubmitEnterpriseInfoCommand, ) (*responses.CertificationResponse, error) { s.logger.Info("开始提交企业信息", zap.String("user_id", cmd.UserID), zap.String("company_name", cmd.CompanyName), zap.String("unified_social_code", cmd.UnifiedSocialCode)) // 0. 若该用户已有待审核(认证状态仍在待审核),则不允许重复提交 latestRecord, err := s.enterpriseInfoSubmitRecordRepo.FindLatestByUserID(ctx, cmd.UserID) if err == nil && latestRecord != nil { s.logger.Info("步骤0-检测到历史提交记录", zap.String("user_id", cmd.UserID), zap.String("latest_record_id", latestRecord.ID)) cert, loadErr := s.aggregateService.LoadCertificationByUserID(ctx, cmd.UserID) if loadErr == nil && cert != nil && cert.Status == enums.StatusInfoPendingReview { s.logger.Warn("步骤0-存在待审核记录,拒绝重复提交", zap.String("user_id", cmd.UserID), zap.String("cert_status", string(cert.Status))) return nil, fmt.Errorf("您已有待审核的提交,请等待管理员审核后再操作") } } // 0.5 已通过人工审核或已进入后续流程:幂等返回当前认证数据(不调 e签宝、不新建提交记录) existsCertEarly, err := s.aggregateService.ExistsByUserID(ctx, cmd.UserID) if err != nil { return nil, fmt.Errorf("检查认证记录失败: %w", err) } if existsCertEarly { certEarly, loadErr := s.aggregateService.LoadCertificationByUserID(ctx, cmd.UserID) if loadErr != nil { return nil, fmt.Errorf("加载认证信息失败: %w", loadErr) } switch certEarly.Status { case enums.StatusInfoSubmitted, enums.StatusEnterpriseVerified, enums.StatusContractApplied, enums.StatusContractSigned, enums.StatusCompleted, enums.StatusContractRejected, enums.StatusContractExpired: meta, metaErr := s.AddStatusMetadata(ctx, certEarly) if metaErr != nil { return nil, metaErr } resp := s.convertToResponse(certEarly) if meta != nil { resp.Metadata = meta } else { resp.Metadata = map[string]interface{}{} } resp.Metadata["next_action"] = enums.GetUserActionHint(certEarly.Status) s.logger.Info("企业信息提交幂等返回", zap.String("user_id", cmd.UserID), zap.String("status", string(certEarly.Status))) return resp, nil } } // 1.5 插入企业信息提交记录(包含扩展字段) record := entities.NewEnterpriseInfoSubmitRecord( cmd.UserID, cmd.CompanyName, cmd.UnifiedSocialCode, cmd.LegalPersonName, cmd.LegalPersonID, cmd.LegalPersonPhone, cmd.EnterpriseAddress, ) // 扩展字段赋值 record.BusinessLicenseImageURL = cmd.BusinessLicenseImageURL if len(cmd.OfficePlaceImageURLs) > 0 { if data, mErr := json.Marshal(cmd.OfficePlaceImageURLs); mErr == nil { record.OfficePlaceImageURLs = string(data) } else { s.logger.Warn("序列化办公场地图片URL失败", zap.Error(mErr)) } } record.APIUsage = cmd.APIUsage if len(cmd.ScenarioAttachmentURLs) > 0 { if data, mErr := json.Marshal(cmd.ScenarioAttachmentURLs); mErr == nil { record.ScenarioAttachmentURLs = string(data) } else { s.logger.Warn("序列化场景附件图片URL失败", zap.Error(mErr)) } } // 授权代表信息落库 record.AuthorizedRepName = cmd.AuthorizedRepName record.AuthorizedRepID = cmd.AuthorizedRepID record.AuthorizedRepPhone = cmd.AuthorizedRepPhone if len(cmd.AuthorizedRepIDImageURLs) > 0 { if data, mErr := json.Marshal(cmd.AuthorizedRepIDImageURLs); mErr == nil { record.AuthorizedRepIDImageURLs = string(data) } else { s.logger.Warn("序列化授权代表身份证图片URL失败", zap.Error(mErr)) } } // 验证验证码 // 特殊验证码"768005"直接跳过验证环节 if cmd.VerificationCode != "768005" { s.logger.Info("步骤1-开始验证短信验证码", zap.String("user_id", cmd.UserID)) if err := s.smsCodeService.VerifyCode(ctx, cmd.LegalPersonPhone, cmd.VerificationCode, user_entities.SMSSceneCertification); err != nil { s.logger.Warn("步骤1-短信验证码校验失败", zap.String("user_id", cmd.UserID), zap.Error(err)) record.MarkAsFailed(err.Error()) saveErr := s.enterpriseInfoSubmitRecordService.Save(ctx, record) if saveErr != nil { return nil, fmt.Errorf("保存企业信息提交记录失败: %s", saveErr.Error()) } return nil, fmt.Errorf("验证码错误或已过期") } s.logger.Info("步骤1-短信验证码校验通过", zap.String("user_id", cmd.UserID)) } else { s.logger.Info("步骤1-命中特殊验证码,跳过校验", zap.String("user_id", cmd.UserID)) } s.logger.Info("开始处理企业信息提交", zap.String("user_id", cmd.UserID)) // 1. 检查企业信息是否重复(统一社会信用代码:已认证或已提交待审核的都不能重复) // 1.1 已写入用户域 enterprise_infos 的(已完成认证) exists, err := s.userAggregateService.CheckUnifiedSocialCodeExists(ctx, cmd.UnifiedSocialCode, cmd.UserID) if err != nil { s.logger.Error("步骤2.1-检查用户域统一社会信用代码失败", zap.String("user_id", cmd.UserID), zap.String("unified_social_code", cmd.UnifiedSocialCode), zap.Error(err)) record.MarkAsFailed(err.Error()) saveErr := s.enterpriseInfoSubmitRecordService.Save(ctx, record) if saveErr != nil { return nil, fmt.Errorf("保存企业信息提交记录失败: %s", saveErr.Error()) } return nil, fmt.Errorf("检查企业信息失败: %s", err.Error()) } if exists { s.logger.Warn("步骤2.1-统一社会信用代码已被占用(用户域)", zap.String("user_id", cmd.UserID), zap.String("unified_social_code", cmd.UnifiedSocialCode)) record.MarkAsFailed("该企业信息已被其他用户使用,请确认企业信息是否正确") saveErr := s.enterpriseInfoSubmitRecordService.Save(ctx, record) if saveErr != nil { return nil, fmt.Errorf("保存企业信息提交记录失败: %s", saveErr.Error()) } return nil, fmt.Errorf("该企业信息已被其他用户使用,请确认企业信息是否正确") } // 1.2 已提交/已通过验证的提交记录(尚未完成认证但已占用的信用代码) existsInSubmit, err := s.enterpriseInfoSubmitRecordRepo.ExistsByUnifiedSocialCodeExcludeUser(ctx, cmd.UnifiedSocialCode, cmd.UserID) if err != nil { s.logger.Error("步骤2.2-检查提交记录统一社会信用代码失败", zap.String("user_id", cmd.UserID), zap.String("unified_social_code", cmd.UnifiedSocialCode), zap.Error(err)) record.MarkAsFailed(err.Error()) saveErr := s.enterpriseInfoSubmitRecordService.Save(ctx, record) if saveErr != nil { return nil, fmt.Errorf("保存企业信息提交记录失败: %s", saveErr.Error()) } return nil, fmt.Errorf("检查企业信息失败: %s", err.Error()) } if existsInSubmit { s.logger.Warn("步骤2.2-统一社会信用代码已被占用(提交记录)", zap.String("user_id", cmd.UserID), zap.String("unified_social_code", cmd.UnifiedSocialCode)) record.MarkAsFailed("该企业信息已被其他用户使用,请确认企业信息是否正确") saveErr := s.enterpriseInfoSubmitRecordService.Save(ctx, record) if saveErr != nil { return nil, fmt.Errorf("保存企业信息提交记录失败: %s", saveErr.Error()) } return nil, fmt.Errorf("该企业信息已被其他用户使用,请确认企业信息是否正确") } enterpriseInfo := &certification_value_objects.EnterpriseInfo{ CompanyName: cmd.CompanyName, UnifiedSocialCode: cmd.UnifiedSocialCode, LegalPersonName: cmd.LegalPersonName, LegalPersonID: cmd.LegalPersonID, LegalPersonPhone: cmd.LegalPersonPhone, EnterpriseAddress: cmd.EnterpriseAddress, } err = enterpriseInfo.Validate() if err != nil { s.logger.Error("企业信息验证失败", zap.Error(err)) record.MarkAsFailed(err.Error()) saveErr := s.enterpriseInfoSubmitRecordService.Save(ctx, record) if saveErr != nil { return nil, fmt.Errorf("保存企业信息提交记录失败: %s", saveErr.Error()) } return nil, fmt.Errorf("企业信息验证失败: %s", err.Error()) } s.logger.Info("步骤3-企业信息基础校验通过", zap.String("user_id", cmd.UserID), zap.String("company_name", enterpriseInfo.CompanyName)) err = s.enterpriseInfoSubmitRecordService.ValidateWithWestdex(ctx, enterpriseInfo) if err != nil { s.logger.Error("企业信息验证失败", zap.Error(err)) record.MarkAsFailed(err.Error()) saveErr := s.enterpriseInfoSubmitRecordService.Save(ctx, record) if saveErr != nil { return nil, fmt.Errorf("保存企业信息提交记录失败: %s", saveErr.Error()) } return nil, fmt.Errorf("企业信息验证失败, %s", err.Error()) } s.logger.Info("步骤4-企业信息三方校验通过", zap.String("user_id", cmd.UserID), zap.String("company_name", enterpriseInfo.CompanyName)) record.MarkAsVerified() var response *responses.CertificationResponse err = s.txManager.ExecuteInTx(ctx, func(txCtx context.Context) error { s.logger.Info("步骤5-开始事务处理认证提交流程", zap.String("user_id", cmd.UserID)) // 2. 检查用户认证是否存在 existsCert, err := s.aggregateService.ExistsByUserID(txCtx, cmd.UserID) if err != nil { return fmt.Errorf("检查用户认证是否存在失败: %s", err.Error()) } if !existsCert { // 创建 s.logger.Info("步骤5.1-认证记录不存在,开始创建", zap.String("user_id", cmd.UserID)) _, err := s.aggregateService.CreateCertification(txCtx, cmd.UserID) if err != nil { return fmt.Errorf("创建认证信息失败: %s", err.Error()) } s.logger.Info("步骤5.1-认证记录创建成功", zap.String("user_id", cmd.UserID)) } // 3. 加载认证聚合根 cert, err := s.aggregateService.LoadCertificationByUserID(txCtx, cmd.UserID) if err != nil { return fmt.Errorf("加载认证信息失败: %s", err.Error()) } // 4. 提交企业信息:进入人工审核(三真/企业信息审核);e签宝链接仅在管理员审核通过后生成(见 AdminApproveSubmitRecord) if err := cert.SubmitEnterpriseInfoForReview(enterpriseInfo); err != nil { return fmt.Errorf("提交企业信息失败: %s", err.Error()) } if err := s.aggregateService.SaveCertification(txCtx, cert); err != nil { return fmt.Errorf("保存认证信息失败: %s", err.Error()) } // 5. 提交记录与认证状态在同一事务内保存 if saveErr := s.enterpriseInfoSubmitRecordService.Save(txCtx, record); saveErr != nil { return fmt.Errorf("保存企业信息提交记录失败: %s", saveErr.Error()) } s.logger.Info("步骤5.3-企业信息提交记录保存成功", zap.String("user_id", cmd.UserID), zap.String("record_id", record.ID)) var enterpriseInfoMeta map[string]interface{} if raw, mErr := json.Marshal(enterpriseInfo); mErr == nil { _ = json.Unmarshal(raw, &enterpriseInfoMeta) } if enterpriseInfoMeta == nil { enterpriseInfoMeta = map[string]interface{}{} } enterpriseInfoMeta["submit_at"] = record.SubmitAt.Format(time.RFC3339) respMeta := map[string]interface{}{ "enterprise_info": enterpriseInfoMeta, "polling": map[string]interface{}{ "enabled": false, "endpoint": "/api/v1/certifications/confirm-auth", "interval_seconds": 3, }, "next_action": "请等待管理员审核企业信息", "target_view": "manual_review", } // 6. 转换为响应 DTO response = s.convertToResponse(cert) response.Metadata = respMeta return nil }) if err != nil { return nil, err } // 提醒管理员处理待审核申请(配置企业微信 Webhook 时生效) if s.wechatWorkService != nil { contactPhone := cmd.LegalPersonPhone if strings.TrimSpace(cmd.AuthorizedRepPhone) != "" { contactPhone = fmt.Sprintf("法人 %s;授权代表 %s", cmd.LegalPersonPhone, cmd.AuthorizedRepPhone) } else { contactPhone = fmt.Sprintf("%s(法人)", cmd.LegalPersonPhone) } _ = s.wechatWorkService.SendCertificationNotification(ctx, "pending_manual_review", map[string]interface{}{ "company_name": cmd.CompanyName, "legal_person_name": cmd.LegalPersonName, "authorized_rep_name": cmd.AuthorizedRepName, "contact_phone": contactPhone, "api_usage": cmd.APIUsage, "submit_at": record.SubmitAt.Format("2006-01-02 15:04:05"), }) } s.logger.Info("企业信息提交成功", zap.String("user_id", cmd.UserID)) return response, nil } // 审核状态检查(步骤二) // 规则:企业信息提交成功后进入待审核;审核通过后才允许进行企业认证确认(ConfirmAuth)。 func (s *CertificationApplicationServiceImpl) checkAuditStatus(ctx context.Context, cert *entities.Certification) error { switch cert.Status { case enums.StatusInfoSubmitted, enums.StatusEnterpriseVerified, enums.StatusContractApplied, enums.StatusContractSigned, enums.StatusCompleted: return nil case enums.StatusInfoPendingReview: return fmt.Errorf("企业信息已提交,正在审核中") case enums.StatusInfoRejected: return fmt.Errorf("企业信息审核未通过") default: return fmt.Errorf("认证状态不正确,当前状态: %s", enums.GetStatusName(cert.Status)) } } // ConfirmAuth 确认认证状态 func (s *CertificationApplicationServiceImpl) ConfirmAuth( ctx context.Context, cmd *queries.ConfirmAuthCommand, ) (*responses.ConfirmAuthResponse, error) { s.logger.Info("开始确认状态", zap.String("user_id", cmd.UserID)) cert, err := s.aggregateService.LoadCertificationByUserID(ctx, cmd.UserID) if err != nil { return nil, fmt.Errorf("加载认证信息失败: %s", err.Error()) } // 步骤二:审核状态检查(审核通过后才能进入企业认证确认) s.logger.Info("确认状态-步骤1-开始审核状态检查", zap.String("user_id", cmd.UserID)) if err := s.checkAuditStatus(ctx, cert); err != nil { return nil, err } s.logger.Info("确认状态-步骤1-审核状态检查通过", zap.String("user_id", cmd.UserID), zap.String("cert_status", string(cert.Status))) record, err := s.enterpriseInfoSubmitRecordRepo.FindLatestByUserID(ctx, cert.UserID) if err != nil { return nil, fmt.Errorf("查找企业信息失败: %w", err) } s.logger.Info("确认状态-步骤2-获取最近提交记录成功", zap.String("user_id", cmd.UserID), zap.String("record_id", record.ID)) s.logger.Info("确认状态-步骤3-开始查询三方实名状态", zap.String("user_id", cmd.UserID), zap.String("company_name", record.CompanyName)) identity, err := s.esignClient.QueryOrgIdentityInfo(&esign.QueryOrgIdentityRequest{ OrgName: record.CompanyName, }) if err != nil { s.logger.Error("查询企业认证信息失败", zap.Error(err)) return nil, fmt.Errorf("查询企业认证信息失败: %w", err) } reason := "" if identity != nil && identity.Data.RealnameStatus == 1 { s.logger.Info("确认状态-步骤3-三方实名状态已完成,准备事务内推进认证", zap.String("user_id", cmd.UserID)) err = s.txManager.ExecuteInTx(ctx, func(txCtx context.Context) error { err = s.completeEnterpriseVerification(txCtx, cert, cert.UserID, record.CompanyName, record.LegalPersonName) if err != nil { return err } reason = "企业认证成功" return nil }) if err != nil { return nil, fmt.Errorf("完成企业认证失败: %w", err) } s.logger.Info("确认状态-步骤4-认证状态推进完成", zap.String("user_id", cmd.UserID), zap.String("cert_status", string(cert.Status))) } else { reason = "企业未完成" s.logger.Info("确认状态-步骤3-三方实名状态未完成", zap.String("user_id", cmd.UserID)) } return &responses.ConfirmAuthResponse{ Status: cert.Status, Reason: reason, }, nil } // ConfirmSign 确认签署状态 func (s *CertificationApplicationServiceImpl) ConfirmSign( ctx context.Context, cmd *queries.ConfirmSignCommand, ) (*responses.ConfirmSignResponse, error) { cert, err := s.aggregateService.LoadCertificationByUserID(ctx, cmd.UserID) if err != nil { return nil, fmt.Errorf("加载认证信息失败: %s", err.Error()) } reason, err := s.checkAndUpdateSignStatus(ctx, cert) if err != nil { return nil, fmt.Errorf("确认签署状态失败: %w", err) } return &responses.ConfirmSignResponse{ Status: cert.Status, Reason: reason, }, nil } // ApplyContract 申请合同签署 func (s *CertificationApplicationServiceImpl) ApplyContract( ctx context.Context, cmd *commands.ApplyContractCommand, ) (*responses.ContractSignUrlResponse, error) { s.logger.Info("开始申请合同签署", zap.String("user_id", cmd.UserID)) // 1. 验证命令完整性 if err := s.validateApplyContractCommand(cmd); err != nil { return nil, fmt.Errorf("命令验证失败: %s", err.Error()) } // 2. 加载认证聚合根 cert, err := s.aggregateService.LoadCertificationByUserID(ctx, cmd.UserID) if err != nil { return nil, fmt.Errorf("加载认证信息失败: %s", err.Error()) } // 3. 验证业务前置条件 if err := s.validateContractApplicationPreconditions(cert, cmd.UserID); err != nil { return nil, fmt.Errorf("业务前置条件验证失败: %s", err.Error()) } // 5. 生成合同和签署链接 enterpriseInfo, err := s.userAggregateService.GetUserWithEnterpriseInfo(ctx, cmd.UserID) if err != nil { s.logger.Error("获取企业信息失败", zap.Error(err)) return nil, fmt.Errorf("获取企业信息失败: %w", err) } contractInfo, err := s.generateContractAndSignURL(ctx, cert, enterpriseInfo.EnterpriseInfo) if err != nil { s.logger.Error("生成合同失败", zap.Error(err)) return nil, fmt.Errorf("生成合同失败: %s", err.Error()) } err = cert.ApplyContract(contractInfo.EsignFlowID, contractInfo.ContractSignURL) if err != nil { s.logger.Error("合同申请状态转换失败", zap.Error(err)) return nil, fmt.Errorf("合同申请失败: %s", err.Error()) } // 7. 保存认证信息 err = s.aggregateService.SaveCertification(ctx, cert) if err != nil { s.logger.Error("保存认证信息失败", zap.Error(err)) return nil, fmt.Errorf("保存认证信息失败: %s", err.Error()) } // 8. 构建响应 response := responses.NewContractSignUrlResponse( cert.ID, contractInfo.ContractSignURL, contractInfo.ContractURL, "请在规定时间内完成合同签署", "合同申请成功", ) s.logger.Info("合同申请成功", zap.String("user_id", cmd.UserID)) return response, nil } // ================ 查询用例 ================ // GetCertification 获取认证详情 func (s *CertificationApplicationServiceImpl) GetCertification( ctx context.Context, query *queries.GetCertificationQuery, ) (*responses.CertificationResponse, error) { s.logger.Debug("获取认证详情", zap.String("user_id", query.UserID)) // 1. 检查用户认证是否存在 exists, err := s.aggregateService.ExistsByUserID(ctx, query.UserID) if err != nil { s.logger.Error("获取认证信息失败", zap.Error(err)) return nil, fmt.Errorf("获取认证信息失败: %w", err) } var cert *entities.Certification if !exists { // 创建新的认证记录 cert, err = s.aggregateService.CreateCertification(ctx, query.UserID) if err != nil { s.logger.Error("创建认证信息失败", zap.Error(err)) return nil, fmt.Errorf("创建认证信息失败: %w", err) } } else { // 加载现有认证记录 cert, err = s.aggregateService.LoadCertificationByUserID(ctx, query.UserID) if err != nil { s.logger.Error("加载认证信息失败", zap.Error(err)) return nil, fmt.Errorf("加载认证信息失败: %w", err) } } // 2. 检查是否需要更新合同文件 if cert.IsContractFileNeedUpdate() { err = s.updateContractFile(ctx, cert) if err != nil { return nil, err } } if cert.Status == enums.StatusInfoSubmitted { err = s.checkAndCompleteEnterpriseVerification(ctx, cert) if err != nil { return nil, err } } if cert.Status == enums.StatusContractApplied { _, err = s.checkAndUpdateSignStatus(ctx, cert) if err != nil { return nil, err } } // 2. 转换为响应DTO response := s.convertToResponse(cert) // 3. 添加状态相关的元数据 meta, err := s.AddStatusMetadata(ctx, cert) if err != nil { return nil, err } if meta != nil { response.Metadata = meta } s.logger.Info("获取认证详情成功", zap.String("user_id", query.UserID)) 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 } // ================ e签宝回调处理 ================ // HandleEsignCallback 处理e签宝回调 func (s *CertificationApplicationServiceImpl) HandleEsignCallback( ctx context.Context, cmd *commands.EsignCallbackCommand, ) error { // if err := esign.VerifySignature(cmd.Data, cmd.Headers, cmd.QueryParams, s.esignConfig.AppSecret); err != nil { // return fmt.Errorf("e签宝回调验签失败: %w", err) // } // 4. 根据回调类型处理业务逻辑 switch cmd.Data.Action { case "AUTH_PASS": // 只处理企业认证通过 if cmd.Data.AuthType == "ORG" { err := s.txManager.ExecuteInTx(ctx, func(txCtx context.Context) error { // 1. 根据AuthFlowId加载认证信息 cert, err := s.aggregateService.LoadCertificationByAuthFlowId(txCtx, cmd.Data.AuthFlowId) if err != nil { return fmt.Errorf("加载认证信息失败: %s", err.Error()) } if cmd.Data.Organization == nil || cmd.Data.Organization.OrgName == "" { return fmt.Errorf("组织信息为空") } if cert.Status != enums.StatusInfoSubmitted { return fmt.Errorf("认证状态不正确") } // 2. 完成企业认证 err = cert.CompleteEnterpriseVerification() if err != nil { return fmt.Errorf("完成企业认证失败: %s", err.Error()) } record, err := s.enterpriseInfoSubmitRecordRepo.FindLatestByUserID(txCtx, cert.UserID) if err != nil { s.logger.Error("查找已认证企业信息失败", zap.Error(err)) return fmt.Errorf("查找已认证企业信息失败: %w", err) } // 5. 写入用户域 err = s.userAggregateService.CreateOrUpdateEnterpriseInfo( txCtx, record.UserID, record.CompanyName, record.UnifiedSocialCode, record.LegalPersonName, record.LegalPersonID, record.LegalPersonPhone, record.EnterpriseAddress, ) if err != nil { s.logger.Error("同步企业信息到用户域失败", zap.Error(err)) return fmt.Errorf("同步企业信息到用户域失败: %w", err) } // 生成合同 err = s.generateAndAddContractFile(txCtx, cert, record.CompanyName, record.LegalPersonName, record.UnifiedSocialCode, record.EnterpriseAddress, record.LegalPersonPhone, record.LegalPersonID) if err != nil { return err } // 3. 保存认证信息 err = s.aggregateService.SaveCertification(txCtx, cert) if err != nil { return fmt.Errorf("保存认证信息失败: %s", err.Error()) } s.logger.Info("完成企业认证", zap.String("certification_id", cert.ID)) return nil }) if err != nil { s.logger.Error("完成企业认证失败", zap.Error(err)) return fmt.Errorf("完成企业认证失败: %w", err) } } return nil default: s.logger.Info("忽略未知的回调动作", zap.String("action", cmd.Data.Action)) return nil } } // ================ 管理员后台操作用例 ================ // AdminCompleteCertificationWithoutContract 管理员代用户完成认证(暂不关联合同) func (s *CertificationApplicationServiceImpl) AdminCompleteCertificationWithoutContract( ctx context.Context, cmd *commands.AdminCompleteCertificationCommand, ) (*responses.CertificationResponse, error) { s.logger.Info("管理员代用户完成认证(不关联合同)", zap.String("admin_id", cmd.AdminID), zap.String("user_id", cmd.UserID), ) // 1. 基础参数及企业信息校验 enterpriseInfo := &certification_value_objects.EnterpriseInfo{ CompanyName: cmd.CompanyName, UnifiedSocialCode: cmd.UnifiedSocialCode, LegalPersonName: cmd.LegalPersonName, LegalPersonID: cmd.LegalPersonID, LegalPersonPhone: cmd.LegalPersonPhone, EnterpriseAddress: cmd.EnterpriseAddress, } if err := enterpriseInfo.Validate(); err != nil { return nil, fmt.Errorf("企业信息验证失败: %s", err.Error()) } // 检查统一社会信用代码唯一性(排除当前用户) exists, err := s.userAggregateService.CheckUnifiedSocialCodeExists(ctx, cmd.UnifiedSocialCode, cmd.UserID) if err != nil { return nil, fmt.Errorf("检查企业信息失败: %s", err.Error()) } if exists { return nil, fmt.Errorf("统一社会信用代码已被其他用户使用") } var cert *entities.Certification // 2. 事务内:创建/加载认证、写入企业信息、直接完成认证、创建钱包和API用户 err = s.txManager.ExecuteInTx(ctx, func(txCtx context.Context) error { // 2.1 检查并创建认证记录 existsCert, err := s.aggregateService.ExistsByUserID(txCtx, cmd.UserID) if err != nil { return fmt.Errorf("检查用户认证是否存在失败: %s", err.Error()) } if !existsCert { cert, err = s.aggregateService.CreateCertification(txCtx, cmd.UserID) if err != nil { return fmt.Errorf("创建认证信息失败: %s", err.Error()) } } else { cert, err = s.aggregateService.LoadCertificationByUserID(txCtx, cmd.UserID) if err != nil { return fmt.Errorf("加载认证信息失败: %s", err.Error()) } } // 2.2 写入/覆盖用户域企业信息 if err := s.userAggregateService.CreateOrUpdateEnterpriseInfo( txCtx, cmd.UserID, cmd.CompanyName, cmd.UnifiedSocialCode, cmd.LegalPersonName, cmd.LegalPersonID, cmd.LegalPersonPhone, cmd.EnterpriseAddress, ); err != nil { return fmt.Errorf("保存企业信息失败: %s", err.Error()) } // 2.3 直接将认证状态设置为完成(管理员操作,暂不校验合同信息) if err := cert.TransitionTo( enums.StatusCompleted, enums.ActorTypeAdmin, cmd.AdminID, fmt.Sprintf("管理员代用户完成认证:%s", cmd.Reason), ); err != nil { return fmt.Errorf("更新认证状态失败: %s", err.Error()) } // 2.4 基础激活:创建钱包、API用户并在用户域标记完成认证 if err := s.completeUserActivationWithoutContract(txCtx, cert); err != nil { return err } // 2.5 保存认证信息 if err := s.aggregateService.SaveCertification(txCtx, cert); err != nil { return fmt.Errorf("保存认证信息失败: %s", err.Error()) } return nil }) if err != nil { return nil, err } response := s.convertToResponse(cert) s.logger.Info("管理员代用户完成认证成功(不关联合同)", zap.String("admin_id", cmd.AdminID), zap.String("user_id", cmd.UserID), zap.String("certification_id", cert.ID), ) return response, nil } // AdminListSubmitRecords 管理端分页查询企业信息提交记录 func (s *CertificationApplicationServiceImpl) AdminListSubmitRecords( ctx context.Context, query *queries.AdminListSubmitRecordsQuery, ) (*responses.AdminSubmitRecordsListResponse, error) { if query.PageSize <= 0 { query.PageSize = 10 } if query.Page <= 0 { query.Page = 1 } filter := repositories.ListSubmitRecordsFilter{ Page: query.Page, PageSize: query.PageSize, CertificationStatus: query.CertificationStatus, CompanyName: query.CompanyName, LegalPersonPhone: query.LegalPersonPhone, LegalPersonName: query.LegalPersonName, } result, err := s.enterpriseInfoSubmitRecordRepo.List(ctx, filter) if err != nil { return nil, fmt.Errorf("查询提交记录失败: %w", err) } items := make([]*responses.AdminSubmitRecordItem, 0, len(result.Records)) for _, r := range result.Records { certStatus := "" if cert, err := s.aggregateService.LoadCertificationByUserID(ctx, r.UserID); err == nil && cert != nil { certStatus = string(cert.Status) } items = append(items, &responses.AdminSubmitRecordItem{ ID: r.ID, UserID: r.UserID, CompanyName: r.CompanyName, UnifiedSocialCode: r.UnifiedSocialCode, LegalPersonName: r.LegalPersonName, SubmitAt: r.SubmitAt, Status: r.Status, CertificationStatus: certStatus, }) } totalPages := int((result.Total + int64(query.PageSize) - 1) / int64(query.PageSize)) if totalPages == 0 { totalPages = 1 } return &responses.AdminSubmitRecordsListResponse{ Items: items, Total: result.Total, Page: query.Page, PageSize: query.PageSize, TotalPages: totalPages, }, nil } // AdminGetSubmitRecordByID 管理端获取单条提交记录详情 func (s *CertificationApplicationServiceImpl) AdminGetSubmitRecordByID(ctx context.Context, recordID string) (*responses.AdminSubmitRecordDetail, error) { record, err := s.enterpriseInfoSubmitRecordRepo.FindByID(ctx, recordID) if err != nil { return nil, fmt.Errorf("获取提交记录失败: %w", err) } certStatus := "" if cert, loadErr := s.aggregateService.LoadCertificationByUserID(ctx, record.UserID); loadErr == nil && cert != nil { certStatus = string(cert.Status) } return &responses.AdminSubmitRecordDetail{ ID: record.ID, UserID: record.UserID, CompanyName: record.CompanyName, UnifiedSocialCode: record.UnifiedSocialCode, LegalPersonName: record.LegalPersonName, LegalPersonID: record.LegalPersonID, LegalPersonPhone: record.LegalPersonPhone, EnterpriseAddress: record.EnterpriseAddress, AuthorizedRepName: record.AuthorizedRepName, AuthorizedRepID: record.AuthorizedRepID, AuthorizedRepPhone: record.AuthorizedRepPhone, AuthorizedRepIDImageURLs: record.AuthorizedRepIDImageURLs, BusinessLicenseImageURL: record.BusinessLicenseImageURL, OfficePlaceImageURLs: record.OfficePlaceImageURLs, APIUsage: record.APIUsage, ScenarioAttachmentURLs: record.ScenarioAttachmentURLs, Status: record.Status, SubmitAt: record.SubmitAt, VerifiedAt: record.VerifiedAt, FailedAt: record.FailedAt, FailureReason: record.FailureReason, CertificationStatus: certStatus, CreatedAt: record.CreatedAt, UpdatedAt: record.UpdatedAt, }, nil } // AdminApproveSubmitRecord 管理端审核通过 func (s *CertificationApplicationServiceImpl) AdminApproveSubmitRecord(ctx context.Context, recordID, adminID, remark string) error { record, err := s.enterpriseInfoSubmitRecordRepo.FindByID(ctx, recordID) if err != nil { return fmt.Errorf("获取提交记录失败: %w", err) } if record.Status != "verified" { return fmt.Errorf("该条提交记录未通过前置校验或已失败,无法审核通过") } cert, err := s.aggregateService.LoadCertificationByUserID(ctx, record.UserID) if err != nil { return fmt.Errorf("加载认证信息失败: %w", err) } // 幂等:认证已进入「已提交企业信息」或更后续状态,说明已通过审核,无需重复操作 switch cert.Status { case enums.StatusInfoSubmitted, enums.StatusEnterpriseVerified, enums.StatusContractApplied, enums.StatusContractSigned, enums.StatusCompleted, enums.StatusContractRejected, enums.StatusContractExpired: return nil } if cert.Status != enums.StatusInfoPendingReview { return fmt.Errorf("认证状态不是待审核,当前: %s", enums.GetStatusName(cert.Status)) } enterpriseInfo := &certification_value_objects.EnterpriseInfo{ CompanyName: record.CompanyName, UnifiedSocialCode: record.UnifiedSocialCode, LegalPersonName: record.LegalPersonName, LegalPersonID: record.LegalPersonID, LegalPersonPhone: record.LegalPersonPhone, EnterpriseAddress: record.EnterpriseAddress, } authReq := &esign.EnterpriseAuthRequest{ CompanyName: enterpriseInfo.CompanyName, UnifiedSocialCode: enterpriseInfo.UnifiedSocialCode, LegalPersonName: enterpriseInfo.LegalPersonName, LegalPersonID: enterpriseInfo.LegalPersonID, TransactorName: enterpriseInfo.LegalPersonName, TransactorMobile: enterpriseInfo.LegalPersonPhone, TransactorID: enterpriseInfo.LegalPersonID, } authURL, alreadyVerified, err := s.generateEnterpriseAuthOrDetectVerified(ctx, authReq) if err != nil { return fmt.Errorf("生成企业认证链接失败: %w", err) } if alreadyVerified { if err := cert.ApproveEnterpriseInfoReview("", "", adminID); err != nil { return fmt.Errorf("更新认证状态失败: %w", err) } if err := s.completeEnterpriseVerification(ctx, cert, cert.UserID, record.CompanyName, record.LegalPersonName); err != nil { return err } record.MarkManualApproved(adminID, remark) if err := s.enterpriseInfoSubmitRecordService.Save(ctx, record); err != nil { return fmt.Errorf("保存企业信息提交记录失败: %w", err) } s.logger.Info("管理员审核通过企业信息", zap.String("record_id", recordID), zap.String("admin_id", adminID)) return nil } if err := cert.ApproveEnterpriseInfoReview(authURL.AuthShortURL, authURL.AuthFlowID, adminID); err != nil { return fmt.Errorf("更新认证状态失败: %w", err) } if err := s.aggregateService.SaveCertification(ctx, cert); err != nil { return fmt.Errorf("保存认证信息失败: %w", err) } record.MarkManualApproved(adminID, remark) if err := s.enterpriseInfoSubmitRecordService.Save(ctx, record); err != nil { return fmt.Errorf("保存企业信息提交记录失败: %w", err) } s.logger.Info("管理员审核通过企业信息", zap.String("record_id", recordID), zap.String("admin_id", adminID)) return nil } // AdminRejectSubmitRecord 管理端审核拒绝 func (s *CertificationApplicationServiceImpl) AdminRejectSubmitRecord(ctx context.Context, recordID, adminID, remark string) error { if remark == "" { return fmt.Errorf("拒绝时必须填写审核备注") } record, err := s.enterpriseInfoSubmitRecordRepo.FindByID(ctx, recordID) if err != nil { return fmt.Errorf("获取提交记录失败: %w", err) } if record.Status != "verified" { return fmt.Errorf("该条提交记录未通过前置校验或已失败,无法从后台拒绝(请查看历史失败原因)") } cert, err := s.aggregateService.LoadCertificationByUserID(ctx, record.UserID) if err != nil { return fmt.Errorf("加载认证信息失败: %w", err) } // 幂等:认证已处于拒绝或后续状态,无需重复拒绝 switch cert.Status { case enums.StatusInfoRejected, enums.StatusEnterpriseVerified, enums.StatusContractApplied, enums.StatusContractSigned, enums.StatusCompleted, enums.StatusContractRejected, enums.StatusContractExpired: return nil } if cert.Status != enums.StatusInfoPendingReview { return fmt.Errorf("认证状态不是待审核,当前: %s", enums.GetStatusName(cert.Status)) } if err := cert.RejectEnterpriseInfoReview(adminID, remark); err != nil { return fmt.Errorf("更新认证状态失败: %w", err) } if err := s.aggregateService.SaveCertification(ctx, cert); err != nil { return fmt.Errorf("保存认证信息失败: %w", err) } record.MarkManualRejected(adminID, remark) if err := s.enterpriseInfoSubmitRecordService.Save(ctx, record); err != nil { return fmt.Errorf("保存企业信息提交记录失败: %w", err) } s.logger.Info("管理员审核拒绝企业信息", zap.String("record_id", recordID), zap.String("admin_id", adminID)) return nil } // AdminTransitionCertificationStatus 管理端按用户变更认证状态(以状态机为准) func (s *CertificationApplicationServiceImpl) AdminTransitionCertificationStatus(ctx context.Context, cmd *commands.AdminTransitionCertificationStatusCommand) error { cert, err := s.aggregateService.LoadCertificationByUserID(ctx, cmd.UserID) if err != nil { return fmt.Errorf("加载认证信息失败: %w", err) } record, err := s.enterpriseInfoSubmitRecordRepo.FindLatestByUserID(ctx, cmd.UserID) if err != nil { return fmt.Errorf("查找企业信息提交记录失败: %w", err) } if record == nil { return fmt.Errorf("未找到该用户的企业信息提交记录") } switch cmd.TargetStatus { case string(enums.StatusInfoSubmitted): // 审核通过:与 AdminApproveSubmitRecord 一致,推状态并生成企业认证链接 switch cert.Status { case enums.StatusInfoSubmitted, enums.StatusEnterpriseVerified, enums.StatusContractApplied, enums.StatusContractSigned, enums.StatusCompleted, enums.StatusContractRejected, enums.StatusContractExpired: return nil } if cert.Status != enums.StatusInfoPendingReview { return fmt.Errorf("认证状态不是待审核,当前: %s", enums.GetStatusName(cert.Status)) } enterpriseInfo := &certification_value_objects.EnterpriseInfo{ CompanyName: record.CompanyName, UnifiedSocialCode: record.UnifiedSocialCode, LegalPersonName: record.LegalPersonName, LegalPersonID: record.LegalPersonID, LegalPersonPhone: record.LegalPersonPhone, EnterpriseAddress: record.EnterpriseAddress, } authReq := &esign.EnterpriseAuthRequest{ CompanyName: enterpriseInfo.CompanyName, UnifiedSocialCode: enterpriseInfo.UnifiedSocialCode, LegalPersonName: enterpriseInfo.LegalPersonName, LegalPersonID: enterpriseInfo.LegalPersonID, TransactorName: enterpriseInfo.LegalPersonName, TransactorMobile: enterpriseInfo.LegalPersonPhone, TransactorID: enterpriseInfo.LegalPersonID, } authURL, alreadyVerified, err := s.generateEnterpriseAuthOrDetectVerified(ctx, authReq) if err != nil { return fmt.Errorf("生成企业认证链接失败: %w", err) } if alreadyVerified { if err := cert.ApproveEnterpriseInfoReview("", "", cmd.AdminID); err != nil { return fmt.Errorf("更新认证状态失败: %w", err) } if err := s.completeEnterpriseVerification(ctx, cert, cert.UserID, record.CompanyName, record.LegalPersonName); err != nil { return err } record.MarkManualApproved(cmd.AdminID, cmd.Remark) if err := s.enterpriseInfoSubmitRecordService.Save(ctx, record); err != nil { return fmt.Errorf("保存企业信息提交记录失败: %w", err) } s.logger.Info("管理端变更认证状态为通过", zap.String("user_id", cmd.UserID), zap.String("admin_id", cmd.AdminID)) return nil } if err := cert.ApproveEnterpriseInfoReview(authURL.AuthShortURL, authURL.AuthFlowID, cmd.AdminID); err != nil { return fmt.Errorf("更新认证状态失败: %w", err) } if err := s.aggregateService.SaveCertification(ctx, cert); err != nil { return fmt.Errorf("保存认证信息失败: %w", err) } record.MarkManualApproved(cmd.AdminID, cmd.Remark) if err := s.enterpriseInfoSubmitRecordService.Save(ctx, record); err != nil { return fmt.Errorf("保存企业信息提交记录失败: %w", err) } s.logger.Info("管理端变更认证状态为通过", zap.String("user_id", cmd.UserID), zap.String("admin_id", cmd.AdminID)) return nil case string(enums.StatusInfoRejected): // 审核拒绝 if cert.Status == enums.StatusInfoRejected || cert.Status == enums.StatusEnterpriseVerified || cert.Status == enums.StatusContractApplied || cert.Status == enums.StatusContractSigned || cert.Status == enums.StatusCompleted { return nil } if cert.Status != enums.StatusInfoPendingReview { return fmt.Errorf("认证状态不是待审核,当前: %s", enums.GetStatusName(cert.Status)) } if err := cert.RejectEnterpriseInfoReview(cmd.AdminID, cmd.Remark); err != nil { return fmt.Errorf("更新认证状态失败: %w", err) } if err := s.aggregateService.SaveCertification(ctx, cert); err != nil { return fmt.Errorf("保存认证信息失败: %w", err) } record.MarkManualRejected(cmd.AdminID, cmd.Remark) if err := s.enterpriseInfoSubmitRecordService.Save(ctx, record); err != nil { return fmt.Errorf("保存企业信息提交记录失败: %w", err) } s.logger.Info("管理端变更认证状态为拒绝", zap.String("user_id", cmd.UserID), zap.String("admin_id", cmd.AdminID)) return nil default: return fmt.Errorf("不支持的目标状态: %s", cmd.TargetStatus) } } // ================ 辅助方法 ================ // 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, CompletedAt: cert.CompletedAt, 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 } func (s *CertificationApplicationServiceImpl) generateEnterpriseAuthOrDetectVerified( ctx context.Context, req *esign.EnterpriseAuthRequest, ) (*esign.EnterpriseAuthResult, bool, error) { s.logger.Info("企业认证链接生成-步骤1-开始调用三方创建认证链接", zap.String("company_name", req.CompanyName), zap.String("unified_social_code", req.UnifiedSocialCode)) authURL, err := s.esignClient.GenerateEnterpriseAuth(req) if err == nil { s.logger.Info("企业认证链接生成-步骤1-创建成功", zap.String("company_name", req.CompanyName), zap.String("auth_flow_id", authURL.AuthFlowID)) return authURL, false, nil } if !isEnterpriseAlreadyRealnamedErr(err) { s.logger.Error("企业认证链接生成-步骤1-创建失败且非已实名场景", zap.String("company_name", req.CompanyName), zap.Error(err)) return nil, false, err } s.logger.Warn("企业已实名,跳过生成认证链接并转为自动确认", zap.String("company_name", req.CompanyName), zap.String("unified_social_code", req.UnifiedSocialCode), zap.Error(err)) identity, identityErr := s.esignClient.QueryOrgIdentityInfo(&esign.QueryOrgIdentityRequest{ OrgIDCardNum: req.UnifiedSocialCode, OrgIDCardType: esign.OrgIDCardTypeUSCC, }) if identityErr != nil { s.logger.Warn("企业认证链接生成-步骤2-按信用代码查询实名状态失败,回退按企业名查询", zap.String("company_name", req.CompanyName), zap.Error(identityErr)) identity, identityErr = s.esignClient.QueryOrgIdentityInfo(&esign.QueryOrgIdentityRequest{ OrgName: req.CompanyName, }) } if identityErr != nil { return nil, false, fmt.Errorf("企业用户已实名,但查询实名状态失败: %w", identityErr) } s.logger.Info("企业认证链接生成-步骤2-实名状态查询成功", zap.String("company_name", req.CompanyName), zap.Int32("realname_status", identity.Data.RealnameStatus)) if identity == nil || identity.Data.RealnameStatus != 1 { return nil, false, err } s.logger.Info("企业认证链接生成-步骤3-确认企业已实名,返回自动确认标记", zap.String("company_name", req.CompanyName)) return nil, true, nil } func isEnterpriseAlreadyRealnamedErr(err error) bool { if err == nil { return false } msg := err.Error() return strings.Contains(msg, "企业用户已实名") || strings.Contains(msg, "已实名") } // validateApplyContractCommand 验证申请合同命令 func (s *CertificationApplicationServiceImpl) validateApplyContractCommand(cmd *commands.ApplyContractCommand) error { if cmd.UserID == "" { return fmt.Errorf("用户ID不能为空") } return nil } // validateContractApplicationPreconditions 验证合同申请前置条件 func (s *CertificationApplicationServiceImpl) validateContractApplicationPreconditions(cert *entities.Certification, userID string) error { if cert.UserID != userID { return fmt.Errorf("用户无权限操作此认证申请") } if cert.Status != enums.StatusEnterpriseVerified { return fmt.Errorf("必须先完成企业认证才能申请合同") } return nil } // generateContractAndSignURL 生成合同和签署链接 func (s *CertificationApplicationServiceImpl) generateContractAndSignURL(ctx context.Context, cert *entities.Certification, enterpriseInfo *user_entities.EnterpriseInfo) (*certification_value_objects.ContractInfo, error) { // 发起签署流程 signFlowID, err := s.esignClient.CreateSignFlow(&esign.CreateSignFlowRequest{ FileID: cert.ContractFileID, SignerAccount: enterpriseInfo.UnifiedSocialCode, SignerName: enterpriseInfo.CompanyName, TransactorPhone: enterpriseInfo.LegalPersonPhone, TransactorName: enterpriseInfo.LegalPersonName, TransactorIDCardNum: enterpriseInfo.LegalPersonID, }) if err != nil { return nil, fmt.Errorf("生成合同失败: %s", err.Error()) } _, shortUrl, err := s.esignClient.GetSignURL(signFlowID, enterpriseInfo.LegalPersonPhone, enterpriseInfo.CompanyName) if err != nil { return nil, fmt.Errorf("获取签署链接失败: %s", err.Error()) } return &certification_value_objects.ContractInfo{ ContractFileID: cert.ContractFileID, EsignFlowID: signFlowID, ContractSignURL: shortUrl, }, nil } // ================ 重构后的公共方法 ================ // completeEnterpriseVerification 完成企业认证的公共方法 func (s *CertificationApplicationServiceImpl) completeEnterpriseVerification( ctx context.Context, cert *entities.Certification, userID string, companyName string, legalPersonName string, ) error { s.logger.Info("完成企业认证-步骤1-开始状态流转", zap.String("user_id", userID), zap.String("company_name", companyName)) // 完成企业认证 err := cert.CompleteEnterpriseVerification() if err != nil { s.logger.Error("完成企业认证失败", zap.Error(err)) return fmt.Errorf("完成企业认证失败: %w", err) } // 保存企业信息到用户域 record, err := s.enterpriseInfoSubmitRecordRepo.FindLatestByUserID(ctx, userID) if err != nil { s.logger.Error("查找企业信息失败", zap.Error(err)) return fmt.Errorf("查找企业信息失败: %w", err) } s.logger.Info("完成企业认证-步骤2-获取提交记录成功", zap.String("user_id", userID), zap.String("record_id", record.ID)) err = s.userAggregateService.CreateEnterpriseInfo( ctx, userID, record.CompanyName, record.UnifiedSocialCode, record.LegalPersonName, record.LegalPersonID, record.LegalPersonPhone, record.EnterpriseAddress, ) if err != nil { s.logger.Error("保存企业信息到用户域失败", zap.Error(err)) return fmt.Errorf("保存企业信息失败: %s", err.Error()) } else { s.logger.Info("企业信息已保存到用户域", zap.String("user_id", userID)) } // 生成合同 err = s.generateAndAddContractFile(ctx, cert, record.CompanyName, record.LegalPersonName, record.UnifiedSocialCode, record.EnterpriseAddress, record.LegalPersonPhone, record.LegalPersonID) if err != nil { return err } s.logger.Info("完成企业认证-步骤3-合同文件生成并写入认证成功", zap.String("user_id", userID)) // 保存认证信息 err = s.aggregateService.SaveCertification(ctx, cert) if err != nil { s.logger.Error("保存认证信息失败", zap.Error(err)) return fmt.Errorf("保存认证信息失败: %w", err) } s.logger.Info("完成企业认证-步骤4-认证信息保存成功", zap.String("user_id", userID)) return nil } // generateAndAddContractFile 生成并添加合同文件的公共方法 func (s *CertificationApplicationServiceImpl) generateAndAddContractFile( ctx context.Context, cert *entities.Certification, companyName string, legalPersonName string, unifiedSocialCode string, enterpriseAddress string, legalPersonPhone string, legalPersonID string, ) error { s.logger.Info("合同生成-步骤1-开始填充合同模板", zap.String("user_id", cert.UserID), zap.String("company_name", companyName)) fileComponent := map[string]string{ "YFCompanyName": companyName, "YFCompanyName2": companyName, "YFLegalPersonName": legalPersonName, "YFLegalPersonName2": legalPersonName, "YFUnifiedSocialCode": unifiedSocialCode, "YFEnterpriseAddress": enterpriseAddress, "YFContactPerson": legalPersonName, "YFMobile": legalPersonPhone, "SignDate": time.Now().Format("2006年01月02日"), "SignDate2": time.Now().Format("2006年01月02日"), "SignDate3": time.Now().Format("2006年01月02日"), } fillTemplateResp, err := s.esignClient.FillTemplate(fileComponent) if err != nil { s.logger.Error("生成合同失败", zap.Error(err)) return fmt.Errorf("生成合同失败: %s", err.Error()) } s.logger.Info("合同生成-步骤1-模板填充成功", zap.String("user_id", cert.UserID), zap.String("file_id", fillTemplateResp.FileID)) err = cert.AddContractFileID(fillTemplateResp.FileID, fillTemplateResp.FileDownloadUrl) if err != nil { s.logger.Error("加入合同文件ID链接失败", zap.Error(err)) return fmt.Errorf("加入合同文件ID链接失败: %s", err.Error()) } s.logger.Info("合同生成-步骤2-合同文件写入认证实体成功", zap.String("user_id", cert.UserID), zap.String("file_id", fillTemplateResp.FileID)) return nil } // updateContractFile 更新合同文件的公共方法 func (s *CertificationApplicationServiceImpl) updateContractFile(ctx context.Context, cert *entities.Certification) error { // 获取企业信息 enterpriseInfo, err := s.userAggregateService.GetUserWithEnterpriseInfo(ctx, cert.UserID) if err != nil { s.logger.Error("获取企业信息失败", zap.Error(err)) return fmt.Errorf("获取企业信息失败: %w", err) } // 生成合同 err = s.generateAndAddContractFile(ctx, cert, enterpriseInfo.EnterpriseInfo.CompanyName, enterpriseInfo.EnterpriseInfo.LegalPersonName, enterpriseInfo.EnterpriseInfo.UnifiedSocialCode, enterpriseInfo.EnterpriseInfo.EnterpriseAddress, enterpriseInfo.EnterpriseInfo.LegalPersonPhone, enterpriseInfo.EnterpriseInfo.LegalPersonID) if err != nil { return err } // 更新认证信息 err = s.aggregateService.SaveCertification(ctx, cert) if err != nil { s.logger.Error("保存认证信息失败", zap.Error(err)) return fmt.Errorf("保存认证信息失败: %w", err) } return nil } // checkAndCompleteEnterpriseVerification 检查并完成企业认证的公共方法 func (s *CertificationApplicationServiceImpl) checkAndCompleteEnterpriseVerification(ctx context.Context, cert *entities.Certification) error { record, err := s.enterpriseInfoSubmitRecordRepo.FindLatestByUserID(ctx, cert.UserID) if err != nil { return fmt.Errorf("查找企业信息失败: %w", err) } identity, err := s.esignClient.QueryOrgIdentityInfo(&esign.QueryOrgIdentityRequest{ OrgName: record.CompanyName, }) if err != nil { s.logger.Error("查询企业认证信息失败", zap.Error(err)) } if identity != nil && identity.Data.RealnameStatus == 1 { err = s.txManager.ExecuteInTx(ctx, func(txCtx context.Context) error { return s.completeEnterpriseVerification(txCtx, cert, cert.UserID, record.CompanyName, record.LegalPersonName) }) if err != nil { return fmt.Errorf("完成企业认证失败: %w", err) } } return nil } // checkAndUpdateSignStatus 检查并更新签署状态的公共方法 func (s *CertificationApplicationServiceImpl) checkAndUpdateSignStatus(ctx context.Context, cert *entities.Certification) (string, error) { var reason string err := s.txManager.ExecuteInTx(ctx, func(txCtx context.Context) error { if cert.Status != enums.StatusContractApplied { return fmt.Errorf("认证状态不正确") } detail, err := s.esignClient.QuerySignFlowDetail(cert.EsignFlowID) if err != nil { return fmt.Errorf("查询签署流程详情失败: %s", err.Error()) } if detail.Data.SignFlowStatus == 2 { err = cert.SignSuccess() if err != nil { return fmt.Errorf("合同签署成功失败: %s", err.Error()) } err = cert.CompleteCertification() if err != nil { return fmt.Errorf("完成认证失败: %s", err.Error()) } // 同步合同信息到用户域 err = s.handleContractAfterSignComplete(txCtx, cert) if err != nil { s.logger.Error("同步合同信息到用户域失败", zap.Error(err)) return fmt.Errorf("同步合同信息失败: %s", err.Error()) } reason = "合同签署成功" } else if detail.Data.SignFlowStatus == 7 { err = cert.ContractRejection(detail.Data.SignFlowDescription) if err != nil { return fmt.Errorf("合同签署失败: %s", err.Error()) } reason = "合同签署拒签" } else if detail.Data.SignFlowStatus == 5 { err = cert.ContractExpiration() if err != nil { return fmt.Errorf("合同签署过期失败: %s", err.Error()) } reason = "合同签署过期" } else { reason = "合同签署中" } err = s.aggregateService.SaveCertification(ctx, cert) if err != nil { return fmt.Errorf("保存认证信息失败: %s", err.Error()) } return nil }) if err != nil { return "", err } return reason, nil } // handleContractAfterSignComplete 处理签署完成后的合同 func (s *CertificationApplicationServiceImpl) handleContractAfterSignComplete(ctx context.Context, cert *entities.Certification) error { // 获取用户的企业信息 user, err := s.userAggregateService.GetUserWithEnterpriseInfo(ctx, cert.UserID) if err != nil { return fmt.Errorf("加载用户信息失败: %w", err) } if user.EnterpriseInfo == nil { return fmt.Errorf("用户企业信息不存在") } // 1. 获取所有已签署合同文件信息 downloadSignedFileResponse, err := s.esignClient.DownloadSignedFile(cert.EsignFlowID) if err != nil { return fmt.Errorf("下载已签署文件失败: %s", err.Error()) } files := downloadSignedFileResponse.Data.Files if len(files) == 0 { return fmt.Errorf("未获取到已签署合同文件") } for _, file := range files { fileUrl := file.DownloadUrl fileName := file.FileName fileId := file.FileId s.logger.Info("下载已签署文件准备", zap.String("file_url", fileUrl), zap.String("file_name", fileName)) // 2. 下载文件内容 fileBytes, err := s.downloadFileContent(ctx, fileUrl) if err != nil { s.logger.Error("下载合同文件内容失败", zap.String("file_name", fileName), zap.Error(err)) continue } // 3. 上传到七牛云 uploadResult, err := s.qiniuStorageService.UploadFile(ctx, fileBytes, fileName) if err != nil { s.logger.Error("上传合同文件到七牛云失败", zap.String("file_name", fileName), zap.Error(err)) continue } qiniuURL := uploadResult.URL s.logger.Info("合同文件已上传七牛云", zap.String("file_name", fileName), zap.String("qiniu_url", qiniuURL)) // 4. 保存到合同聚合根 _, err = s.contractAggregateService.CreateContract( ctx, user.EnterpriseInfo.ID, cert.UserID, fileName, user_entities.ContractTypeCooperation, fileId, qiniuURL, ) if err != nil { s.logger.Error("保存合同信息到聚合根失败", zap.String("file_name", fileName), zap.Error(err)) continue } s.logger.Info("合同信息已保存到聚合根", zap.String("file_name", fileName), zap.String("qiniu_url", qiniuURL)) } // 合同签署完成后的基础激活流程 return s.completeUserActivationWithoutContract(ctx, cert) } // downloadFileContent 通过URL下载文件内容 func (s *CertificationApplicationServiceImpl) downloadFileContent(ctx context.Context, fileUrl string) ([]byte, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, fileUrl, nil) if err != nil { return nil, err } resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("下载失败,状态码: %d", resp.StatusCode) } return io.ReadAll(resp.Body) } // 添加状态相关的元数据 func (s *CertificationApplicationServiceImpl) AddStatusMetadata(ctx context.Context, cert *entities.Certification) (map[string]interface{}, error) { metadata := make(map[string]interface{}) metadata = cert.GetDataByStatus() switch cert.Status { case enums.StatusPending, enums.StatusInfoPendingReview, enums.StatusInfoRejected, enums.StatusInfoSubmitted, enums.StatusEnterpriseVerified: record, err := s.enterpriseInfoSubmitRecordRepo.FindLatestByUserID(ctx, cert.UserID) if err == nil && record != nil { enterpriseInfo := map[string]interface{}{ "company_name": record.CompanyName, "legal_person_name": record.LegalPersonName, "unified_social_code": record.UnifiedSocialCode, "enterprise_address": record.EnterpriseAddress, "legal_person_phone": record.LegalPersonPhone, "legal_person_id": record.LegalPersonID, "submit_at": record.SubmitAt.Format(time.RFC3339), } metadata["enterprise_info"] = enterpriseInfo } case enums.StatusCompleted: // 获取最终合同信息 contracts, err := s.contractAggregateService.FindByUserID(ctx, cert.UserID) if err == nil && len(contracts) > 0 { metadata["contract_url"] = contracts[0].ContractFileURL } } return metadata, nil } // completeUserActivationWithoutContract 创建钱包、API用户并在用户域标记完成认证(不依赖合同信息) func (s *CertificationApplicationServiceImpl) completeUserActivationWithoutContract(ctx context.Context, cert *entities.Certification) error { // 创建钱包 if _, err := s.walletAggregateService.CreateWallet(ctx, cert.UserID); err != nil { s.logger.Error("创建钱包失败", zap.String("user_id", cert.UserID), zap.Error(err)) } // 创建API用户 if err := s.apiUserAggregateService.CreateApiUser(ctx, cert.UserID); err != nil { s.logger.Error("创建API用户失败", zap.String("user_id", cert.UserID), zap.Error(err)) } // 标记用户域完成认证 if err := s.userAggregateService.CompleteCertification(ctx, cert.UserID); err != nil { s.logger.Error("用户域完成认证失败", zap.String("user_id", cert.UserID), zap.Error(err)) return err } // 企业认证成功企业微信通知(仅展示企业名称和联系手机) if s.wechatWorkService != nil { user, err := s.userAggregateService.GetUserWithEnterpriseInfo(ctx, cert.UserID) if err == nil { companyName := "未知企业" phone := "" if user.EnterpriseInfo != nil { companyName = user.EnterpriseInfo.CompanyName if user.EnterpriseInfo.LegalPersonPhone != "" { phone = user.EnterpriseInfo.LegalPersonPhone } } if user.Phone != "" && phone == "" { phone = user.Phone } content := fmt.Sprintf( "### 【海宇数据】企业认证成功\n"+ "> 企业名称:%s\n"+ "> 联系手机:%s\n"+ "> 完成时间:%s\n"+ "\n该企业已完成认证,请相关同事同步更新内部系统。", companyName, phone, time.Now().Format("2006-01-02 15:04:05"), ) _ = s.wechatWorkService.SendMarkdownMessage(ctx, content) } } return nil } // RecognizeBusinessLicense OCR识别营业执照 func (s *CertificationApplicationServiceImpl) RecognizeBusinessLicense( ctx context.Context, imageBytes []byte, ) (*responses.BusinessLicenseResult, error) { s.logger.Info("开始OCR识别营业执照", zap.Int("image_size", len(imageBytes))) // 调用OCR服务识别营业执照 result, err := s.ocrService.RecognizeBusinessLicense(ctx, imageBytes) if err != nil { s.logger.Error("OCR识别营业执照失败", zap.Error(err)) return nil, fmt.Errorf("营业执照识别失败: %w", err) } // 验证识别结果 if err := s.ocrService.ValidateBusinessLicense(result); err != nil { s.logger.Error("营业执照识别结果验证失败", zap.Error(err)) return nil, fmt.Errorf("营业执照识别结果不完整: %w", err) } s.logger.Info("营业执照OCR识别成功", zap.String("company_name", result.CompanyName), zap.String("unified_social_code", result.UnifiedSocialCode), zap.String("legal_person_name", result.LegalPersonName), zap.Float64("confidence", result.Confidence), ) return result, nil }