From f927f75637b6620555f17032519b39b5686b9f34 Mon Sep 17 00:00:00 2001 From: Mrx <18278715334@163.com> Date: Sat, 9 May 2026 21:44:50 +0800 Subject: [PATCH 01/28] f --- .../api/services/processors/ivyz/ivyzzq3B_processor.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/internal/domains/api/services/processors/ivyz/ivyzzq3B_processor.go b/internal/domains/api/services/processors/ivyz/ivyzzq3B_processor.go index c878331..ecdc5f2 100644 --- a/internal/domains/api/services/processors/ivyz/ivyzzq3B_processor.go +++ b/internal/domains/api/services/processors/ivyz/ivyzzq3B_processor.go @@ -30,15 +30,11 @@ func ProcessIVYZZQ3BRequest(ctx context.Context, params []byte, deps *processors if err != nil { return nil, errors.Join(processors.ErrSystem, err) } - encryptedImageUrl, err := deps.ZhichaService.Encrypt(paramsDto.ImageUrl) - if err != nil { - return nil, errors.Join(processors.ErrSystem, err) - } reqData := map[string]interface{}{ "idCard": encryptedIDCard, "name": encryptedName, - "imageId": encryptedImageUrl, + "imageId": paramsDto.ImageUrl, "authorized": paramsDto.Authorized, } From 07394a4ffa7ddcbc56d587d7590cf93450b8679f Mon Sep 17 00:00:00 2001 From: Mrx <18278715334@163.com> Date: Sun, 10 May 2026 13:36:47 +0800 Subject: [PATCH 02/28] f --- config.yaml | 2 +- configs/env.development.yaml | 8 +- configs/env.production.yaml | 2 +- .../certification_application_service_impl.go | 127 +- internal/application/certification/new.md | 1733 +++++++++++++++++ .../certification/entities/certification.go | 7 + .../domains/user/entities/contract_info.go | 52 + .../services/contract_aggregate_service.go | 46 + internal/shared/esign/signflow_service.go | 96 +- internal/shared/esign/template_service.go | 2 +- internal/shared/esign/types.go | 11 +- internal/shared/esign/utils.go | 6 +- 12 files changed, 2010 insertions(+), 82 deletions(-) create mode 100644 internal/application/certification/new.md diff --git a/config.yaml b/config.yaml index 03ee80e..c4ec820 100644 --- a/config.yaml +++ b/config.yaml @@ -255,7 +255,7 @@ esign: app_id: "7439073138" app_secret: "d76e27fdd169b391e09262a0959dac5c" server_url: "https://smlopenapi.esign.cn" - template_id: "9f7a3f63cc5a48b085b127ba027d234d" + template_id: "6c91bfd5b1bb48c585f5eaceeea893d4" contract: name: "天远数据API合作协议" expire_days: 7 diff --git a/configs/env.development.yaml b/configs/env.development.yaml index 14aa98e..7959ede 100644 --- a/configs/env.development.yaml +++ b/configs/env.development.yaml @@ -46,10 +46,10 @@ ocr: # 📝 e签宝服务配置 # =========================================== esign: - app_id: "7439073713" - app_secret: "c7d8cb0d701f7890601d221e9b6edfef" - server_url: "https://smlopenapi.esign.cn" - template_id: "9f7a3f63cc5a48b085b127ba027d234d" + app_id: "5112008003" + app_secret: "d487672273e7aa70c800804a1d9499b9" + server_url: "https://openapi.esign.cn" + template_id: "6c91bfd5b1bb48c585f5eaceeea893d4" contract: name: "天远数据API合作协议" expire_days: 7 diff --git a/configs/env.production.yaml b/configs/env.production.yaml index 7d337ca..d273c01 100644 --- a/configs/env.production.yaml +++ b/configs/env.production.yaml @@ -79,7 +79,7 @@ esign: app_id: "5112008003" app_secret: "d487672273e7aa70c800804a1d9499b9" server_url: "https://openapi.esign.cn" - template_id: "9f7a3f63cc5a48b085b127ba027d234d" + template_id: "6c91bfd5b1bb48c585f5eaceeea893d4" contract: name: "天远数据API合作协议" expire_days: 7 diff --git a/internal/application/certification/certification_application_service_impl.go b/internal/application/certification/certification_application_service_impl.go index 8278ae8..37aaf51 100644 --- a/internal/application/certification/certification_application_service_impl.go +++ b/internal/application/certification/certification_application_service_impl.go @@ -9,7 +9,6 @@ import ( "strings" "time" - "github.com/shopspring/decimal" "tyapi-server/internal/application/certification/dto/commands" "tyapi-server/internal/application/certification/dto/queries" "tyapi-server/internal/application/certification/dto/responses" @@ -19,11 +18,11 @@ import ( certification_value_objects "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" finance_entities "tyapi-server/internal/domains/finance/entities" finance_repositories "tyapi-server/internal/domains/finance/repositories" - "tyapi-server/internal/domains/certification/services" - subordinate_repositories "tyapi-server/internal/domains/subordinate/repositories" finance_service "tyapi-server/internal/domains/finance/services" + subordinate_repositories "tyapi-server/internal/domains/subordinate/repositories" user_entities "tyapi-server/internal/domains/user/entities" user_service "tyapi-server/internal/domains/user/services" "tyapi-server/internal/infrastructure/external/notification" @@ -32,6 +31,8 @@ import ( "tyapi-server/internal/shared/esign" sharedOCR "tyapi-server/internal/shared/ocr" + "github.com/shopspring/decimal" + "go.uber.org/zap" ) @@ -726,7 +727,7 @@ func (s *CertificationApplicationServiceImpl) HandleEsignCallback( } // 生成合同 - err = s.generateAndAddContractFile(txCtx, cert, record.CompanyName, record.LegalPersonName, record.UnifiedSocialCode, record.EnterpriseAddress, record.LegalPersonPhone, record.LegalPersonID) + err = s.generateAndAddContractFile(txCtx, cert, record.CompanyName, record.UnifiedSocialCode, record.EnterpriseAddress, pickAuthorizedRepName(record, record.LegalPersonName)) if err != nil { return err } @@ -1287,6 +1288,25 @@ func (s *CertificationApplicationServiceImpl) validateContractApplicationPrecond return nil } +// pickAuthorizedRepName 合同模板「客户授权代表」: 优先企业提交记录中的授权代表, 否则为法定代表人 +func pickAuthorizedRepName(record *entities.EnterpriseInfoSubmitRecord, legalPersonName string) string { + if record != nil && strings.TrimSpace(record.AuthorizedRepName) != "" { + return strings.TrimSpace(record.AuthorizedRepName) + } + return legalPersonName +} + +// pickEnterpriseString 优先用户域企业表字段,为空则用最近一次认证提交记录(避免 enterprise_infos 未同步导致合同控件无值) +func pickEnterpriseString(primary string, record *entities.EnterpriseInfoSubmitRecord, fromRecord func(*entities.EnterpriseInfoSubmitRecord) string) string { + if strings.TrimSpace(primary) != "" { + return strings.TrimSpace(primary) + } + if record == nil { + return "" + } + return strings.TrimSpace(fromRecord(record)) +} + // generateContractAndSignURL 生成合同和签署链接 func (s *CertificationApplicationServiceImpl) generateContractAndSignURL(ctx context.Context, cert *entities.Certification, enterpriseInfo *user_entities.EnterpriseInfo) (*certification_value_objects.ContractInfo, error) { // 发起签署流程 @@ -1361,7 +1381,7 @@ func (s *CertificationApplicationServiceImpl) completeEnterpriseVerification( } // 生成合同 - err = s.generateAndAddContractFile(ctx, cert, record.CompanyName, record.LegalPersonName, record.UnifiedSocialCode, record.EnterpriseAddress, record.LegalPersonPhone, record.LegalPersonID) + err = s.generateAndAddContractFile(ctx, cert, record.CompanyName, record.UnifiedSocialCode, record.EnterpriseAddress, pickAuthorizedRepName(record, record.LegalPersonName)) if err != nil { return err } @@ -1383,27 +1403,41 @@ func (s *CertificationApplicationServiceImpl) generateAndAddContractFile( ctx context.Context, cert *entities.Certification, companyName string, - legalPersonName string, unifiedSocialCode string, enterpriseAddress string, - legalPersonPhone string, - legalPersonID string, + authorizedRepName string, ) error { s.logger.Info("合同生成-步骤1-开始填充合同模板", zap.String("user_id", cert.UserID), zap.String("company_name", companyName)) + + // 协议编号:已有则复用,否则新生成 + if cert.ContractCode == "" { + cert.SetContractCode(user_entities.GenerateContractCode(user_entities.ContractTypeCooperation)) + } + agreementNo := cert.ContractCode + + // e签宝日期控件格式必须与模板预设一致(本模板为 yyyy年MM月dd日) + signDate := time.Now().Format("2006年01月02日") + + if strings.TrimSpace(unifiedSocialCode) == "" { + s.logger.Warn("合同模板控件 jftyshxydm:统一社会信用代码为空;若 PDF 上该处空白,请核对 enterprise_infos.unified_social_code、提交记录或 e签宝模板控件 componentKey 是否与代码键名一致", + zap.String("user_id", cert.UserID)) + } + + // 控件 key 须与 e签宝控制台该控件「控件编码/componentKey」完全一致(区分大小写) 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日"), + "jfqym": companyName, + "jfqym2": companyName, + "jfsqdb": authorizedRepName, + "jftyshxydm": unifiedSocialCode, + "jflxdz": enterpriseAddress, + // 甲方 + "xybh": agreementNo, + "qsrq1": signDate, + "qsrq3": signDate, + // 乙方 + "qsrq2": signDate, } fillTemplateResp, err := s.esignClient.FillTemplate(fileComponent) if err != nil { @@ -1412,7 +1446,8 @@ func (s *CertificationApplicationServiceImpl) generateAndAddContractFile( } s.logger.Info("合同生成-步骤1-模板填充成功", zap.String("user_id", cert.UserID), - zap.String("file_id", fillTemplateResp.FileID)) + zap.String("file_id", fillTemplateResp.FileID), + zap.String("contract_code", agreementNo)) err = cert.AddContractFileID(fillTemplateResp.FileID, fillTemplateResp.FileDownloadUrl) if err != nil { s.logger.Error("加入合同文件ID链接失败", zap.Error(err)) @@ -1432,9 +1467,26 @@ func (s *CertificationApplicationServiceImpl) updateContractFile(ctx context.Con s.logger.Error("获取企业信息失败", zap.Error(err)) return fmt.Errorf("获取企业信息失败: %w", err) } + if enterpriseInfo.EnterpriseInfo == nil { + return fmt.Errorf("用户企业信息不存在") + } + + ei := enterpriseInfo.EnterpriseInfo + submitRec, recErr := s.enterpriseInfoSubmitRecordRepo.FindLatestByUserID(ctx, cert.UserID) + if recErr != nil { + s.logger.Warn("更新合同时加载企业提交记录失败,统一社会信用代码等仅以用户域为准", + zap.String("user_id", cert.UserID), + zap.Error(recErr)) + submitRec = nil + } + authRep := pickAuthorizedRepName(submitRec, ei.LegalPersonName) + + company := pickEnterpriseString(ei.CompanyName, submitRec, func(r *entities.EnterpriseInfoSubmitRecord) string { return r.CompanyName }) + uscc := pickEnterpriseString(ei.UnifiedSocialCode, submitRec, func(r *entities.EnterpriseInfoSubmitRecord) string { return r.UnifiedSocialCode }) + addr := pickEnterpriseString(ei.EnterpriseAddress, submitRec, func(r *entities.EnterpriseInfoSubmitRecord) string { return r.EnterpriseAddress }) // 生成合同 - err = s.generateAndAddContractFile(ctx, cert, enterpriseInfo.EnterpriseInfo.CompanyName, enterpriseInfo.EnterpriseInfo.LegalPersonName, enterpriseInfo.EnterpriseInfo.UnifiedSocialCode, enterpriseInfo.EnterpriseInfo.EnterpriseAddress, enterpriseInfo.EnterpriseInfo.LegalPersonPhone, enterpriseInfo.EnterpriseInfo.LegalPersonID) + err = s.generateAndAddContractFile(ctx, cert, company, uscc, addr, authRep) if err != nil { return err } @@ -1571,16 +1623,29 @@ func (s *CertificationApplicationServiceImpl) handleContractAfterSignComplete(ct 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, - ) + // 4. 保存到合同聚合根(复用认证阶段生成的合同编号;旧数据无编号时退回自动生成) + if strings.TrimSpace(cert.ContractCode) != "" { + _, err = s.contractAggregateService.CreateContractWithCode( + ctx, + user.EnterpriseInfo.ID, + cert.UserID, + fileName, + user_entities.ContractTypeCooperation, + fileId, + qiniuURL, + strings.TrimSpace(cert.ContractCode), + ) + } else { + _, 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 diff --git a/internal/application/certification/new.md b/internal/application/certification/new.md new file mode 100644 index 0000000..0fc6ca8 --- /dev/null +++ b/internal/application/certification/new.md @@ -0,0 +1,1733 @@ +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.UnifiedSocialCode, record.EnterpriseAddress, pickAuthorizedRepName(record, record.LegalPersonName)) + 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.UnifiedSocialCode, record.EnterpriseAddress, pickAuthorizedRepName(record, record.LegalPersonName)) + 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 +} + +// pickAuthorizedRepName 合同模板「客户授权代表」: 优先企业提交记录中的授权代表, 否则为法定代表人 +func pickAuthorizedRepName(record *entities.EnterpriseInfoSubmitRecord, legalPersonName string) string { + if record != nil && strings.TrimSpace(record.AuthorizedRepName) != "" { + return strings.TrimSpace(record.AuthorizedRepName) + } + return legalPersonName +} + +// generateAndAddContractFile 生成并添加合同文件的公共方法 +func (s *CertificationApplicationServiceImpl) generateAndAddContractFile( + ctx context.Context, + cert *entities.Certification, + companyName string, + unifiedSocialCode string, + enterpriseAddress string, + authorizedRepName string, +) error { + s.logger.Info("合同生成-步骤1-开始填充合同模板", + zap.String("user_id", cert.UserID), + zap.String("company_name", companyName)) + + // 协议编号:已有则复用,否则新生成 + if cert.ContractCode == "" { + cert.SetContractCode(user_entities.GenerateContractCode(user_entities.ContractTypeCooperation)) + } + agreementNo := cert.ContractCode + + signDate := time.Now().Format("2006年01月02日") + + // 控件 key 与 e 签宝合同模板中控件名一致(新合同) + fileComponent := map[string]string{ + "jfqym": companyName, + "jfqym2": companyName, + "jfsqdb": authorizedRepName, + "jftyshxydm": unifiedSocialCode, + "jflxdz": enterpriseAddress, + // 甲方 + "xybh": agreementNo, + "qsrq1": signDate, + "qsrq3": signDate, + // 乙方 + "qsrq2": signDate, + } + 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), + zap.String("contract_code", agreementNo)) + 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) + } + + ei := enterpriseInfo.EnterpriseInfo + submitRec, _ := s.enterpriseInfoSubmitRecordRepo.FindLatestByUserID(ctx, cert.UserID) + authRep := pickAuthorizedRepName(submitRec, ei.LegalPersonName) + + // 生成合同 + err = s.generateAndAddContractFile(ctx, cert, ei.CompanyName, ei.UnifiedSocialCode, ei.EnterpriseAddress, authRep) + 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.CreateContractWithCode( + ctx, + user.EnterpriseInfo.ID, + cert.UserID, + fileName, + user_entities.ContractTypeCooperation, + fileId, + qiniuURL, + cert.ContractCode, + ) + 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 +} diff --git a/internal/domains/certification/entities/certification.go b/internal/domains/certification/entities/certification.go index 58adecd..e78b7a8 100644 --- a/internal/domains/certification/entities/certification.go +++ b/internal/domains/certification/entities/certification.go @@ -35,6 +35,8 @@ type Certification struct { 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:"合同签署链接"` + // ContractCode 合作协议编号(与电子合同模板控件 xybh 一致,签署完成后写入用户域合同) + ContractCode string `gorm:"type:varchar(255)" json:"contract_code,omitempty" comment:"合作协议编号"` // === 失败信息 === FailureReason enums.FailureReason `gorm:"type:varchar(100)" json:"failure_reason,omitempty" comment:"失败原因"` @@ -323,6 +325,11 @@ func (c *Certification) ApplyContract(EsignFlowID string, ContractSignURL string return nil } +// SetContractCode 设置合作协议编号(首次生成合同时写入,后续换文件复用) +func (c *Certification) SetContractCode(code string) { + c.ContractCode = code +} + // AddContractFileID 生成合同文件 func (c *Certification) AddContractFileID(contractFileID string, contractURL string) error { c.ContractFileID = contractFileID diff --git a/internal/domains/user/entities/contract_info.go b/internal/domains/user/entities/contract_info.go index 5bcb751..4afe8ed 100644 --- a/internal/domains/user/entities/contract_info.go +++ b/internal/domains/user/entities/contract_info.go @@ -119,6 +119,58 @@ func NewContractInfo(enterpriseInfoID, userID, contractName string, contractType return contractInfo, nil } +// NewContractInfoWithContractCode 使用指定合同编号创建合同信息(与认证阶段生成的编号一致) +func NewContractInfoWithContractCode(enterpriseInfoID, userID, contractName string, contractType ContractType, contractFileID, contractFileURL, contractCode string) (*ContractInfo, error) { + if enterpriseInfoID == "" { + return nil, fmt.Errorf("企业信息ID不能为空") + } + if userID == "" { + return nil, fmt.Errorf("用户ID不能为空") + } + if contractName == "" { + return nil, fmt.Errorf("合同名称不能为空") + } + if contractType == "" { + return nil, fmt.Errorf("合同类型不能为空") + } + if contractFileID == "" { + return nil, fmt.Errorf("合同文件ID不能为空") + } + if contractFileURL == "" { + return nil, fmt.Errorf("合同文件URL不能为空") + } + if contractCode == "" { + return nil, fmt.Errorf("合同编号不能为空") + } + if !isValidContractType(contractType) { + return nil, fmt.Errorf("无效的合同类型: %s", contractType) + } + + contractInfo := &ContractInfo{ + ID: uuid.New().String(), + EnterpriseInfoID: enterpriseInfoID, + UserID: userID, + ContractCode: contractCode, + ContractName: contractName, + ContractType: contractType, + ContractFileID: contractFileID, + ContractFileURL: contractFileURL, + domainEvents: make([]interface{}, 0), + } + + contractInfo.addDomainEvent(&ContractInfoCreatedEvent{ + ContractInfoID: contractInfo.ID, + EnterpriseInfoID: enterpriseInfoID, + UserID: userID, + ContractCode: contractCode, + ContractName: contractName, + ContractType: string(contractType), + CreatedAt: time.Now(), + }) + + return contractInfo, nil +} + // ================ 聚合根核心方法 ================ // UpdateContractInfo 更新合同信息 diff --git a/internal/domains/user/services/contract_aggregate_service.go b/internal/domains/user/services/contract_aggregate_service.go index 5868f4b..fe6a02e 100644 --- a/internal/domains/user/services/contract_aggregate_service.go +++ b/internal/domains/user/services/contract_aggregate_service.go @@ -14,6 +14,7 @@ import ( type ContractAggregateService interface { // 聚合根生命周期管理 CreateContract(ctx context.Context, enterpriseInfoID, userID, contractName string, contractType entities.ContractType, contractFileID, contractFileURL string) (*entities.ContractInfo, error) + CreateContractWithCode(ctx context.Context, enterpriseInfoID, userID, contractName string, contractType entities.ContractType, contractFileID, contractFileURL, contractCode string) (*entities.ContractInfo, error) LoadContract(ctx context.Context, contractID string) (*entities.ContractInfo, error) SaveContract(ctx context.Context, contract *entities.ContractInfo) error DeleteContract(ctx context.Context, contractID string) error @@ -94,6 +95,51 @@ func (s *ContractAggregateServiceImpl) CreateContract( return contract, nil } +// CreateContractWithCode 使用指定合同编号创建合同信息(与认证合同模板上的编号一致) +func (s *ContractAggregateServiceImpl) CreateContractWithCode( + ctx context.Context, + enterpriseInfoID, userID, contractName string, + contractType entities.ContractType, + contractFileID, contractFileURL, contractCode string, +) (*entities.ContractInfo, error) { + s.logger.Debug("创建合同信息(指定编号)", + zap.String("enterprise_info_id", enterpriseInfoID), + zap.String("user_id", userID), + zap.String("contract_name", contractName), + zap.String("contract_code", contractCode), + zap.String("contract_type", string(contractType))) + + exists, err := s.ExistsByContractFileID(ctx, contractFileID) + if err != nil { + return nil, fmt.Errorf("检查合同文件ID失败: %w", err) + } + if exists { + return nil, fmt.Errorf("合同文件ID已存在") + } + + contract, err := entities.NewContractInfoWithContractCode(enterpriseInfoID, userID, contractName, contractType, contractFileID, contractFileURL, contractCode) + if err != nil { + return nil, fmt.Errorf("创建合同信息失败: %w", err) + } + + if err := s.ValidateBusinessRules(ctx, contract); err != nil { + return nil, fmt.Errorf("业务规则验证失败: %w", err) + } + + err = s.SaveContract(ctx, contract) + if err != nil { + return nil, fmt.Errorf("保存合同信息失败: %w", err) + } + + s.logger.Info("合同信息创建成功", + zap.String("contract_id", contract.ID), + zap.String("enterprise_info_id", enterpriseInfoID), + zap.String("contract_code", contractCode), + zap.String("contract_name", contractName)) + + return contract, nil +} + // LoadContract 加载合同信息 func (s *ContractAggregateServiceImpl) LoadContract(ctx context.Context, contractID string) (*entities.ContractInfo, error) { s.logger.Debug("加载合同信息", zap.String("contract_id", contractID)) diff --git a/internal/shared/esign/signflow_service.go b/internal/shared/esign/signflow_service.go index 4327d42..591aea7 100644 --- a/internal/shared/esign/signflow_service.go +++ b/internal/shared/esign/signflow_service.go @@ -28,13 +28,13 @@ func (s *SignFlowService) UpdateConfig(config *Config) { // 创建包含多个签署人的签署流程,支持自动盖章和手动签署 func (s *SignFlowService) Create(req *CreateSignFlowRequest) (string, error) { fmt.Println("开始创建签署流程...") - fmt.Println("(将创建包含甲方自动盖章和乙方手动签署的流程)") + fmt.Println("(将创建包含甲方手动签署和乙方自动盖章的流程)") - // 构建甲方签署人信息(自动盖章) - partyASigner := s.buildPartyASigner(req.FileID) + // 构建甲方签署人信息(手动签署) + partyASigner := s.buildPartyASigner(req.FileID, req.SignerAccount, req.SignerName, req.TransactorPhone, req.TransactorName, req.TransactorIDCardNum) - // 构建乙方签署人信息(手动签署) - partyBSigner := s.buildPartyBSigner(req.FileID, req.SignerAccount, req.SignerName, req.TransactorPhone, req.TransactorName, req.TransactorIDCardNum) + // 构建乙方签署人信息(自动盖章) + partyBSigner := s.buildPartyBSigner(req.FileID) signers := []SignerInfo{partyASigner, partyBSigner} @@ -128,34 +128,11 @@ func (s *SignFlowService) GetSignURL(signFlowID, psnAccount, orgName string) (st return response.Data.Url, response.Data.ShortUrl, nil } -// buildPartyASigner 构建甲方签署人信息(自动盖章) -func (s *SignFlowService) buildPartyASigner(fileID string) SignerInfo { - return SignerInfo{ - SignConfig: &SignConfig{SignOrder: 1}, - SignerType: SignerTypeOrg, - SignFields: []SignField{ - { - CustomBizNum: "甲方签章", - FileId: fileID, - NormalSignFieldConfig: &NormalSignFieldConfig{ - AutoSign: true, - SignFieldStyle: SignFieldStyleNormal, - SignFieldPosition: &SignFieldPosition{ - PositionPage: "8", - PositionX: 200, - PositionY: 430, - }, - }, - }, - }, - } -} - -// buildPartyBSigner 构建乙方签署人信息(手动签署) -func (s *SignFlowService) buildPartyBSigner(fileID, signerAccount, signerName, transactorPhone, transactorName, transactorIDCardNum string) SignerInfo { +// buildPartyASigner 构建甲方签署人信息(手动签署) +func (s *SignFlowService) buildPartyASigner(fileID, signerAccount, signerName, transactorPhone, transactorName, transactorIDCardNum string) SignerInfo { return SignerInfo{ SignConfig: &SignConfig{ - SignOrder: 2, + SignOrder: 1, }, AuthConfig: &AuthConfig{ PsnAvailableAuthModes: []string{AuthModeMobile3}, @@ -182,19 +159,66 @@ func (s *SignFlowService) buildPartyBSigner(fileID, signerAccount, signerName, t }, SignFields: []SignField{ { - CustomBizNum: "乙方签章", + CustomBizNum: "甲方签章", FileId: fileID, NormalSignFieldConfig: &NormalSignFieldConfig{ AutoSign: false, SignFieldStyle: SignFieldStyleNormal, SignFieldPosition: &SignFieldPosition{ - PositionPage: "8", - PositionX: 450, - PositionY: 430, + PositionPage: "10", + PositionX: 165, + PositionY: 197, }, OrgSealBizTypes: "PUBLIC", }, }, + { + CustomBizNum: "甲方骑缝章", // 建议设唯一标识,便于调试 + FileId: fileID, + NormalSignFieldConfig: &NormalSignFieldConfig{ + AutoSign: false, + SignFieldStyle: SignFieldStyleSeam, // 必须为 2(Edges) + SignFieldPosition: &SignFieldPosition{ + AcrossPageMode: "ALL", // 覆盖全部页面(推荐) + PositionY: 694.0, // 您指定的 Y 坐标(float64) + }, + }, + }, + }, + } +} + +// buildPartyBSigner 构建乙方签署人信息(自动盖章) +func (s *SignFlowService) buildPartyBSigner(fileID string) SignerInfo { + return SignerInfo{ + SignConfig: &SignConfig{SignOrder: 2}, + SignerType: SignerTypeOrg, + SignFields: []SignField{ + { + CustomBizNum: "乙方签章", + FileId: fileID, + NormalSignFieldConfig: &NormalSignFieldConfig{ + AutoSign: true, + SignFieldStyle: SignFieldStyleNormal, + SignFieldPosition: &SignFieldPosition{ + PositionPage: "10", + PositionX: 403, + PositionY: 197, + }, + }, + }, + { + CustomBizNum: "乙方骑缝章", // 建议设唯一标识,便于调试 + FileId: fileID, + NormalSignFieldConfig: &NormalSignFieldConfig{ + AutoSign: true, // 骑缝章也支持自动签署 + SignFieldStyle: SignFieldStyleSeam, // 必须为 2(Edges) + SignFieldPosition: &SignFieldPosition{ + AcrossPageMode: "ALL", // 覆盖全部页面(推荐) + PositionY: 554.0, // 您指定的 Y 坐标(float64) + }, + }, + }, }, } } @@ -216,4 +240,4 @@ func (s *SignFlowService) buildSignFlowConfig() SignFlowConfig { RedirectUrl: s.config.Sign.RedirectUrl, }, } -} \ No newline at end of file +} diff --git a/internal/shared/esign/template_service.go b/internal/shared/esign/template_service.go index 5e17022..ea21d98 100644 --- a/internal/shared/esign/template_service.go +++ b/internal/shared/esign/template_service.go @@ -161,7 +161,7 @@ func CreateDefaultComponents() []Component { }, { ComponentKey: "QDRQ", - ComponentValue: time.Now().Format("2006年01月02日"), + ComponentValue: time.Now().Format("2006-01-02"), }, } } diff --git a/internal/shared/esign/types.go b/internal/shared/esign/types.go index 78b0385..c9cdc08 100644 --- a/internal/shared/esign/types.go +++ b/internal/shared/esign/types.go @@ -75,8 +75,8 @@ type SignFlowConfig struct { // RedirectConfig 重定向配置 type RedirectConfig struct { - RedirectUrl string `json:"redirectUrl"` // 重定向URL - RedirectDelayTime int64 `json:"redirectDelayTime"` //重定向时间 + RedirectUrl string `json:"redirectUrl"` // 重定向URL + RedirectDelayTime int64 `json:"redirectDelayTime"` //重定向时间 } // AuthConfig 认证配置 @@ -170,9 +170,10 @@ type NormalSignFieldConfig struct { // SignFieldPosition 签署区位置 type SignFieldPosition struct { - PositionPage string `json:"positionPage"` // 页码 - PositionX float64 `json:"positionX"` // X坐标 - PositionY float64 `json:"positionY"` // Y坐标 + PositionPage string `json:"positionPage,omitempty"` // 页码(骑缝章可与 acrossPageMode 组合) + PositionX float64 `json:"positionX,omitempty"` // X坐标 + PositionY float64 `json:"positionY,omitempty"` // Y坐标 + AcrossPageMode string `json:"acrossPageMode,omitempty"` // 骑缝章跨页:如 ALL 表示全部页面 } // ==================== 签署页面链接相关结构体 ==================== diff --git a/internal/shared/esign/utils.go b/internal/shared/esign/utils.go index 88d304a..362aec8 100644 --- a/internal/shared/esign/utils.go +++ b/internal/shared/esign/utils.go @@ -104,11 +104,11 @@ func getCurrentDate() string { } // formatDateForTemplate 格式化日期用于模板填写 -// 格式: "2006年01月02日" +// e签宝日期控件通常预设为 yyyy-MM-dd,与中文年月日格式不兼容时需用本格式。 // -// 返回: 中文格式的日期字符串 +// 返回: yyyy-MM-dd func formatDateForTemplate() string { - return time.Now().Format("2006年01月02日") + return time.Now().Format("2006-01-02") } // generateFileName 生成带时间戳的文件名 From be446de86d0456abfcbd53557b9cd44593f6a7e6 Mon Sep 17 00:00:00 2001 From: Mrx <18278715334@163.com> Date: Mon, 11 May 2026 12:13:32 +0800 Subject: [PATCH 03/28] f --- .../domains/api/services/processors/flxg/flxg0v4b_processor.go | 2 +- .../domains/api/services/processors/flxg/flxg5a3b_processor.go | 2 +- .../domains/api/services/processors/flxg/flxg7e8f_processor.go | 2 +- .../domains/api/services/processors/flxg/flxgca3d_processor.go | 2 +- .../domains/api/services/processors/flxg/flxgdea9_processor.go | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/domains/api/services/processors/flxg/flxg0v4b_processor.go b/internal/domains/api/services/processors/flxg/flxg0v4b_processor.go index e3884ec..b7280eb 100644 --- a/internal/domains/api/services/processors/flxg/flxg0v4b_processor.go +++ b/internal/domains/api/services/processors/flxg/flxg0v4b_processor.go @@ -25,7 +25,7 @@ func ProcessFLXG0V4BRequest(ctx context.Context, params []byte, deps *processors return nil, errors.Join(processors.ErrInvalidParam, err) } // 去掉司法案件案件去掉身份证号码 - if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" { + if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" { return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空")) } encryptedName, err := deps.WestDexService.Encrypt(paramsDto.Name) diff --git a/internal/domains/api/services/processors/flxg/flxg5a3b_processor.go b/internal/domains/api/services/processors/flxg/flxg5a3b_processor.go index 48dd6f8..b0b9118 100644 --- a/internal/domains/api/services/processors/flxg/flxg5a3b_processor.go +++ b/internal/domains/api/services/processors/flxg/flxg5a3b_processor.go @@ -20,7 +20,7 @@ func ProcessFLXG5A3BRequest(ctx context.Context, params []byte, deps *processors if err := deps.Validator.ValidateStruct(paramsDto); err != nil { return nil, errors.Join(processors.ErrInvalidParam, err) } - if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" { + if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" { return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空")) } encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name) diff --git a/internal/domains/api/services/processors/flxg/flxg7e8f_processor.go b/internal/domains/api/services/processors/flxg/flxg7e8f_processor.go index 7b959cb..fe849b6 100644 --- a/internal/domains/api/services/processors/flxg/flxg7e8f_processor.go +++ b/internal/domains/api/services/processors/flxg/flxg7e8f_processor.go @@ -20,7 +20,7 @@ func ProcessFLXG7E8FRequest(ctx context.Context, params []byte, deps *processors if err := deps.Validator.ValidateStruct(paramsDto); err != nil { return nil, errors.Join(processors.ErrInvalidParam, err) } - if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" { + if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" { return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空")) } diff --git a/internal/domains/api/services/processors/flxg/flxgca3d_processor.go b/internal/domains/api/services/processors/flxg/flxgca3d_processor.go index 849325b..f410fd2 100644 --- a/internal/domains/api/services/processors/flxg/flxgca3d_processor.go +++ b/internal/domains/api/services/processors/flxg/flxgca3d_processor.go @@ -20,7 +20,7 @@ func ProcessFLXGCA3DRequest(ctx context.Context, params []byte, deps *processors if err := deps.Validator.ValidateStruct(paramsDto); err != nil { return nil, errors.Join(processors.ErrInvalidParam, err) } - if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" { + if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" { return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空")) } encryptedName, err := deps.WestDexService.Encrypt(paramsDto.Name) diff --git a/internal/domains/api/services/processors/flxg/flxgdea9_processor.go b/internal/domains/api/services/processors/flxg/flxgdea9_processor.go index 5f6e7d7..a42166a 100644 --- a/internal/domains/api/services/processors/flxg/flxgdea9_processor.go +++ b/internal/domains/api/services/processors/flxg/flxgdea9_processor.go @@ -25,7 +25,7 @@ func ProcessFLXGDEA9Request(ctx context.Context, params []byte, deps *processors if err != nil { return nil, errors.Join(processors.ErrSystem, err) } - if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" { + if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" { return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空")) } encryptedIDCard, err := deps.ZhichaService.Encrypt(paramsDto.IDCard) From a55d881a3afd33bc6a8ceac82dcef5bbf5281a94 Mon Sep 17 00:00:00 2001 From: Mrx <18278715334@163.com> Date: Mon, 11 May 2026 13:06:25 +0800 Subject: [PATCH 04/28] f --- .../processors/ivyz/ivyzzq3B_processor.go | 101 +++++++++++++++--- 1 file changed, 88 insertions(+), 13 deletions(-) diff --git a/internal/domains/api/services/processors/ivyz/ivyzzq3B_processor.go b/internal/domains/api/services/processors/ivyz/ivyzzq3B_processor.go index ecdc5f2..f22a0ce 100644 --- a/internal/domains/api/services/processors/ivyz/ivyzzq3B_processor.go +++ b/internal/domains/api/services/processors/ivyz/ivyzzq3B_processor.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "errors" + "fmt" "strconv" "time" @@ -65,15 +66,21 @@ type IVYZZQ3BOut struct { } type IVYZZQ3BOutResultData struct { - // VerificationResult 审核校验结果:valid 身份审核通过;invalid 身份审核不通过(与 similarity 区间联动,见 mapVerificationResultFromSimilarity) + // VerificationCode 审核校验编码,透传上游 incorrect + VerificationCode string `json:"verification_code"` + // VerificationResult 审核校验结果:valid 身份审核通过;invalid 身份审核不通过(score >= 0.45 为 valid) VerificationResult string `json:"verification_result"` - // Similarity 照片相似度分数字符串(0–1000)。区间说明:(0,600)不同人;(600,700)不能确定是否同人;(700,1000)同人。数值为上游 score(0~1)×1000。 + // VerificationMessage 审核校验信息,透传上游 msg + VerificationMessage string `json:"verification_message"` + // Similarity 照片相似度分数字符串(0–1000)。区间说明:(0,600)不同人;(600,700)不能确定是否同人;(700,1000)同人。 Similarity string `json:"similarity"` } -// zci062UpstreamResp 智查 ZCI062 成功返回体中的分数字段(分值越大相似度越高) +// zci062UpstreamResp 智查 ZCI062 成功返回体中的字段 type zci062UpstreamResp struct { - Score interface{} `json:"score"` + Score interface{} `json:"score"` + Msg string `json:"msg"` + Incorrect interface{} `json:"incorrect"` } func mapZCI062RespToIVYZZQ3B(respBytes []byte) ([]byte, error) { @@ -82,27 +89,95 @@ func mapZCI062RespToIVYZZQ3B(respBytes []byte) ([]byte, error) { return nil, err } - score := parseScoreToFloat64(r.Score) - similarityVal := score * 1000 + score := buildScoreToFloat64(r.Score) + similarityVal := buildlarity(score) similarity := strconv.FormatFloat(similarityVal, 'f', 2, 64) - verificationResult := mapVerificationResultFromSimilarity(similarityVal) + verificationResult := buildverifres(score) out := IVYZZQ3BOut{ HandleTime: time.Now().Format("2006-01-02 15:04:05"), ResultData: IVYZZQ3BOutResultData{ - VerificationResult: verificationResult, - Similarity: similarity, + VerificationCode: buildToString(r.Incorrect), + VerificationResult: verificationResult, + VerificationMessage: r.Msg, + Similarity: similarity, }, } return json.Marshal(out) } -// mapVerificationResultFromSimilarity 与 similarity(0–1000)区间说明对齐: -// (700,1000】系统判断为同一人 → 身份审核通过 valid;其余 → invalid。 -func mapVerificationResultFromSimilarity(similarity float64) string { - if similarity >= 700 { +// buildverifres 审核判定逻辑:score >= 0.45 为 valid,否则为 invalid +func buildverifres(score float64) string { + if score >= 0.45 { return "valid" } return "invalid" } + +// buildlarity 将 score(0~1) 分段映射到 similarity(0~1000): +// 0.40 -> 600,0.45 -> 700 +func buildlarity(score float64) float64 { + if score <= 0 { + return 0 + } + if score >= 1 { + return 1000 + } + if score < 0.40 { + // [0, 0.40) -> [0, 600) + return (score / 0.40) * 600 + } + if score < 0.45 { + // [0.40, 0.45) -> [600, 700) + return 600 + ((score-0.40)/0.05)*100 + } + // [0.45, 1] -> [700, 1000] + return 700 + ((score-0.45)/0.55)*300 +} + +func buildScoreToFloat64(v interface{}) float64 { + switch t := v.(type) { + case float64: + return t + case float32: + return float64(t) + case int: + return float64(t) + case int32: + return float64(t) + case int64: + return float64(t) + case json.Number: + if f, err := t.Float64(); err == nil { + return f + } + case string: + if f, err := strconv.ParseFloat(t, 64); err == nil { + return f + } + } + return 0 +} + +func buildToString(v interface{}) string { + if v == nil { + return "" + } + switch t := v.(type) { + case string: + return t + case int: + return strconv.Itoa(t) + case int32: + return strconv.FormatInt(int64(t), 10) + case int64: + return strconv.FormatInt(t, 10) + case float64: + return strconv.FormatFloat(t, 'f', -1, 64) + case float32: + return strconv.FormatFloat(float64(t), 'f', -1, 64) + default: + return fmt.Sprintf("%v", v) + } +} From 38410a73787fce46c797157ca85d4921e5b5ca63 Mon Sep 17 00:00:00 2001 From: Mrx <18278715334@163.com> Date: Mon, 11 May 2026 13:24:59 +0800 Subject: [PATCH 05/28] f --- .../api/services/processors/ivyz/ivyzzq3B_processor.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/domains/api/services/processors/ivyz/ivyzzq3B_processor.go b/internal/domains/api/services/processors/ivyz/ivyzzq3B_processor.go index f22a0ce..bcf90f9 100644 --- a/internal/domains/api/services/processors/ivyz/ivyzzq3B_processor.go +++ b/internal/domains/api/services/processors/ivyz/ivyzzq3B_processor.go @@ -39,7 +39,7 @@ func ProcessIVYZZQ3BRequest(ctx context.Context, params []byte, deps *processors "authorized": paramsDto.Authorized, } - respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI062", reqData) + respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI1001", reqData) if err != nil { if errors.Is(err, zhicha.ErrDatasource) { return nil, errors.Join(processors.ErrDatasource, err) @@ -53,7 +53,7 @@ func ProcessIVYZZQ3BRequest(ctx context.Context, params []byte, deps *processors return nil, errors.Join(processors.ErrSystem, err) } - outBytes, err := mapZCI062RespToIVYZZQ3B(respBytes) + outBytes, err := buildToStringmapZCI1001RespToIVYZZQ3B(respBytes) if err != nil { return nil, errors.Join(processors.ErrSystem, err) } @@ -83,7 +83,7 @@ type zci062UpstreamResp struct { Incorrect interface{} `json:"incorrect"` } -func mapZCI062RespToIVYZZQ3B(respBytes []byte) ([]byte, error) { +func buildToStringmapZCI1001RespToIVYZZQ3B(respBytes []byte) ([]byte, error) { var r zci062UpstreamResp if err := json.Unmarshal(respBytes, &r); err != nil { return nil, err From 8c301de7d241fd6e78f6dd18168d6fea5246a1c1 Mon Sep 17 00:00:00 2001 From: Mrx <18278715334@163.com> Date: Mon, 11 May 2026 13:36:49 +0800 Subject: [PATCH 06/28] f --- internal/domains/api/dto/api_request_dto.go | 3 +- .../processors/ivyz/ivyzzq3B_processor.go | 37 +++++++++++-------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/internal/domains/api/dto/api_request_dto.go b/internal/domains/api/dto/api_request_dto.go index 2a4a040..041f2f5 100644 --- a/internal/domains/api/dto/api_request_dto.go +++ b/internal/domains/api/dto/api_request_dto.go @@ -249,7 +249,8 @@ type IVYZZQT3Req struct { type IVYZZQ3BReq struct { Name string `json:"name" validate:"required,min=1,validName"` IDCard string `json:"id_card" validate:"required,validIDCard"` - ImageUrl string `json:"image_url" validate:"required,url"` + PhotoData string `json:"photo_data" validate:"omitempty,validBase64Image"` + ImageUrl string `json:"image_url" validate:"omitempty,url"` Authorized string `json:"authorized" validate:"required,oneof=0 1"` } diff --git a/internal/domains/api/services/processors/ivyz/ivyzzq3B_processor.go b/internal/domains/api/services/processors/ivyz/ivyzzq3B_processor.go index bcf90f9..6a63e45 100644 --- a/internal/domains/api/services/processors/ivyz/ivyzzq3B_processor.go +++ b/internal/domains/api/services/processors/ivyz/ivyzzq3B_processor.go @@ -35,10 +35,15 @@ func ProcessIVYZZQ3BRequest(ctx context.Context, params []byte, deps *processors reqData := map[string]interface{}{ "idCard": encryptedIDCard, "name": encryptedName, - "imageId": paramsDto.ImageUrl, "authorized": paramsDto.Authorized, } + if paramsDto.ImageUrl != "" { + reqData["url"] = paramsDto.ImageUrl + } else { + reqData["image"] = paramsDto.PhotoData + } + respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI1001", reqData) if err != nil { if errors.Is(err, zhicha.ErrDatasource) { @@ -53,7 +58,7 @@ func ProcessIVYZZQ3BRequest(ctx context.Context, params []byte, deps *processors return nil, errors.Join(processors.ErrSystem, err) } - outBytes, err := buildToStringmapZCI1001RespToIVYZZQ3B(respBytes) + outBytes, err := mapZCI1001RespToIVYZZQ3B(respBytes) if err != nil { return nil, errors.Join(processors.ErrSystem, err) } @@ -76,28 +81,28 @@ type IVYZZQ3BOutResultData struct { Similarity string `json:"similarity"` } -// zci062UpstreamResp 智查 ZCI062 成功返回体中的字段 -type zci062UpstreamResp struct { +// zci1001UpstreamResp 智查 ZCI1001 成功返回体中的字段 +type zci1001UpstreamResp struct { Score interface{} `json:"score"` Msg string `json:"msg"` Incorrect interface{} `json:"incorrect"` } -func buildToStringmapZCI1001RespToIVYZZQ3B(respBytes []byte) ([]byte, error) { - var r zci062UpstreamResp +func mapZCI1001RespToIVYZZQ3B(respBytes []byte) ([]byte, error) { + var r zci1001UpstreamResp if err := json.Unmarshal(respBytes, &r); err != nil { return nil, err } - score := buildScoreToFloat64(r.Score) - similarityVal := buildlarity(score) + score := parseScoreZCI1001ToFloat64(r.Score) + similarityVal := mapScoreToZCI001Similarity(score) similarity := strconv.FormatFloat(similarityVal, 'f', 2, 64) - verificationResult := buildverifres(score) + verificationResult := buildToStringmapZCI1001RespToIVYZZQ3B(score) out := IVYZZQ3BOut{ HandleTime: time.Now().Format("2006-01-02 15:04:05"), ResultData: IVYZZQ3BOutResultData{ - VerificationCode: buildToString(r.Incorrect), + VerificationCode: valueZCI001ToString(r.Incorrect), VerificationResult: verificationResult, VerificationMessage: r.Msg, Similarity: similarity, @@ -107,17 +112,17 @@ func buildToStringmapZCI1001RespToIVYZZQ3B(respBytes []byte) ([]byte, error) { return json.Marshal(out) } -// buildverifres 审核判定逻辑:score >= 0.45 为 valid,否则为 invalid -func buildverifres(score float64) string { +// buildToStringmapZCI1001RespToIVYZZQ3B 审核判定逻辑:score >= 0.45 为 valid,否则为 invalid +func buildToStringmapZCI1001RespToIVYZZQ3B(score float64) string { if score >= 0.45 { return "valid" } return "invalid" } -// buildlarity 将 score(0~1) 分段映射到 similarity(0~1000): +// mapScoreToZCI001Similarity 将 score(0~1) 分段映射到 similarity(0~1000): // 0.40 -> 600,0.45 -> 700 -func buildlarity(score float64) float64 { +func mapScoreToZCI001Similarity(score float64) float64 { if score <= 0 { return 0 } @@ -136,7 +141,7 @@ func buildlarity(score float64) float64 { return 700 + ((score-0.45)/0.55)*300 } -func buildScoreToFloat64(v interface{}) float64 { +func parseScoreZCI1001ToFloat64(v interface{}) float64 { switch t := v.(type) { case float64: return t @@ -160,7 +165,7 @@ func buildScoreToFloat64(v interface{}) float64 { return 0 } -func buildToString(v interface{}) string { +func valueZCI001ToString(v interface{}) string { if v == nil { return "" } From 40f12ac1cf2f9533df0cd18dc25ce206ee9fcb96 Mon Sep 17 00:00:00 2001 From: Mrx <18278715334@163.com> Date: Tue, 19 May 2026 17:21:16 +0800 Subject: [PATCH 07/28] f --- .../processors/qygl/qygl4b2e_processor.go | 117 ++++++++++--- .../processors/qygl/qygl7d9a_processor.go | 155 +++++++++++++++--- .../processors/qygl/qygl8b4d_processor.go | 152 ++++++++++++++--- 3 files changed, 359 insertions(+), 65 deletions(-) diff --git a/internal/domains/api/services/processors/qygl/qygl4b2e_processor.go b/internal/domains/api/services/processors/qygl/qygl4b2e_processor.go index 1f94d0e..a5b3e69 100644 --- a/internal/domains/api/services/processors/qygl/qygl4b2e_processor.go +++ b/internal/domains/api/services/processors/qygl/qygl4b2e_processor.go @@ -8,10 +8,31 @@ import ( "tyapi-server/internal/domains/api/dto" "tyapi-server/internal/domains/api/services/processors" + "tyapi-server/internal/infrastructure/external/shujubao" ) +// QYGL4B2EItem 返回列表项 +type QYGL4B2EItem struct { + PublishTime string `json:"publish_time"` + CaseType string `json:"case_type"` + ID int64 `json:"id"` + Department string `json:"department"` + TaxpayerName string `json:"taxpayer_name"` +} + +// QYGL4B2EResponse 最终返回结构 +type QYGL4B2EResponse struct { + Total int64 `json:"total"` + Items []QYGL4B2EItem `json:"items"` +} + // ProcessQYGL4B2ERequest QYGL4B2E API处理方法 - 税收违法 -func ProcessQYGL4B2ERequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { +func ProcessQYGL4B2ERequest( + ctx context.Context, + params []byte, + deps *processors.ProcessorDependencies, +) ([]byte, error) { + var paramsDto dto.QYGL5A3CReq if err := json.Unmarshal(params, ¶msDto); err != nil { return nil, errors.Join(processors.ErrSystem, err) @@ -21,39 +42,89 @@ func ProcessQYGL4B2ERequest(ctx context.Context, params []byte, deps *processors return nil, errors.Join(processors.ErrInvalidParam, err) } - // 设置默认值 + // 默认值 pageSize := paramsDto.PageSize if pageSize == 0 { - pageSize = int64(20) + pageSize = 20 } pageNum := paramsDto.PageNum if pageNum == 0 { - pageNum = int64(1) + pageNum = 1 } - // 构建API调用参数 - apiParams := map[string]string{ - "keyword": paramsDto.EntCode, - "pageSize": strconv.FormatInt(pageSize, 10), - "pageNum": strconv.FormatInt(pageNum, 10), + // 调用外部接口 + reqParams := map[string]interface{}{ + "key": "c67673dd2e92deb2d2ec91b87bb0a81c", + "creditCode": paramsDto.EntCode, } - // 调用天眼查API - 税收违法 - response, err := deps.TianYanChaService.CallAPI(ctx, "TaxContravention", apiParams) - if err != nil { - return nil, convertTianYanChaError(err) - } - - // 检查天眼查API调用是否成功 - if !response.Success { - return nil, errors.Join(processors.ErrDatasource, errors.New(response.Message)) - } - - // 返回天眼查响应数据 - respBytes, err := json.Marshal(response.Data) + apiPath := "/communication/personal/10233" + data, err := deps.ShujubaoService.CallAPI(ctx, apiPath, reqParams) if err != nil { + if errors.Is(err, shujubao.ErrDatasource) { + return nil, errors.Join(processors.ErrDatasource, err) + } + if errors.Is(err, shujubao.ErrQueryEmpty) { + return nil, errors.Join(processors.ErrNotFound, err) + } return nil, errors.Join(processors.ErrSystem, err) } - return respBytes, nil + // 原始返回结构处理 - data 是 map[string]interface{} 类型 + dataMap, ok := data.(map[string]interface{}) + if !ok { + return nil, errors.Join(processors.ErrSystem, errors.New("data不是map类型")) + } + + // 提取 total + total := int64(0) + if v, ok := dataMap["total"]; ok { + switch val := v.(type) { + case float64: + total = int64(val) + case int: + total = int64(val) + case string: + total, _ = strconv.ParseInt(val, 10, 64) + } + } + + // 提取 items 数组 + srcItems := make([]map[string]interface{}, 0) + if v, ok := dataMap["items"]; ok { + if arr, ok := v.([]interface{}); ok { + for _, item := range arr { + if itemMap, ok := item.(map[string]interface{}); ok { + srcItems = append(srcItems, itemMap) + } + } + } + } + + // 构造返回数据 + resp := QYGL4B2EResponse{ + Total: total, + Items: make([]QYGL4B2EItem, 0, len(srcItems)), + } + + getString := func(item map[string]interface{}, key string) string { + if v, ok := item[key]; ok { + if str, ok := v.(string); ok { + return str + } + } + return "" + } + + for i, v := range srcItems { + resp.Items = append(resp.Items, QYGL4B2EItem{ + ID: int64(i + 1), + TaxpayerName: getString(v, "entityName"), + Department: getString(v, "belongDepartment"), + CaseType: getString(v, "caseType"), + PublishTime: getString(v, "illegalTime"), + }) + } + + return json.Marshal(resp) } diff --git a/internal/domains/api/services/processors/qygl/qygl7d9a_processor.go b/internal/domains/api/services/processors/qygl/qygl7d9a_processor.go index 32d6a7d..efc2379 100644 --- a/internal/domains/api/services/processors/qygl/qygl7d9a_processor.go +++ b/internal/domains/api/services/processors/qygl/qygl7d9a_processor.go @@ -8,8 +8,59 @@ import ( "tyapi-server/internal/domains/api/dto" "tyapi-server/internal/domains/api/services/processors" + "tyapi-server/internal/infrastructure/external/shujubao" ) +/* +## 返回字段说明 + +| 返回值字段 | 字段类型 | 字段说明 | 备注 | +|------------|----------|----------|------| +| total | Number | int(11) | 总数 | +| items | Array | | | +| _child | Object | | | +| taxIdNumber | String | varchar(150) | 纳税人识别号 | +| newOwnTaxBalance | String | varchar(20) | 当前新发生欠税余额 | +| ownTaxAmount | String | varchar(50) | 欠税金额 | +| publishDate | String | 日期 | 发布时间 | +| ownTaxBalance | String | varchar(20) | 欠税余额 | +| type | String | varchar(10) | 税务类型 | +| personIdNumber | String | varchar(150) | 证件号码 | +| taxCategory | String | varchar(255) | 欠税税种 | +| taxpayerType | String | varchar(10) | 纳税人类型 | +| personIdName | String | varchar(50) | 法人证件名称 | +| name | String | varchar(255) | 纳税人名称 | +| location | String | varchar(150) | 经营地点 | +| department | String | varchar(200) | 税务机关 | +| regType | String | varchar(50) | 注册类型 | +| legalpersonName | String | varchar(150) | 法人或负责人名称 | +*/ + +// QYGL7D9AItem 返回列表项 +type QYGL7D9AItem struct { + TaxIdNumber string `json:"taxIdNumber"` + NewOwnTaxBalance string `json:"newOwnTaxBalance"` + OwnTaxAmount string `json:"ownTaxAmount"` + PublishDate string `json:"publishDate"` + OwnTaxBalance string `json:"ownTaxBalance"` + Type string `json:"type"` + PersonIdNumber string `json:"personIdNumber"` + TaxCategory string `json:"taxCategory"` + TaxpayerType string `json:"taxpayerType"` + PersonIdName string `json:"personIdName"` + Name string `json:"name"` + Location string `json:"location"` + Department string `json:"department"` + RegType string `json:"regType"` + LegalpersonName string `json:"legalpersonName"` +} + +// QYGL7D9AResponse 最终返回结构 +type QYGL7D9AResponse struct { + Total int64 `json:"total"` + Items []QYGL7D9AItem `json:"items"` +} + // ProcessQYGL7D9ARequest QYGL7D9A API处理方法 - 欠税公告 func ProcessQYGL7D9ARequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { var paramsDto dto.QYGL5A3CReq @@ -21,39 +72,99 @@ func ProcessQYGL7D9ARequest(ctx context.Context, params []byte, deps *processors return nil, errors.Join(processors.ErrInvalidParam, err) } - // 设置默认值 + // 默认值 pageSize := paramsDto.PageSize if pageSize == 0 { - pageSize = int64(20) + pageSize = 20 } pageNum := paramsDto.PageNum if pageNum == 0 { - pageNum = int64(1) + pageNum = 1 } - // 构建API调用参数 - apiParams := map[string]string{ - "keyword": paramsDto.EntCode, - "pageSize": strconv.FormatInt(pageSize, 10), - "pageNum": strconv.FormatInt(pageNum, 10), + // 调用外部接口 + reqParams := map[string]interface{}{ + "key": "9ad1365cb0580863a239b0255649fb1a", + "creditCode": paramsDto.EntCode, } - // 调用天眼查API - 欠税公告 - response, err := deps.TianYanChaService.CallAPI(ctx, "OwnTax", apiParams) - if err != nil { - return nil, convertTianYanChaError(err) - } - - // 检查天眼查API调用是否成功 - if !response.Success { - return nil, errors.Join(processors.ErrDatasource, errors.New(response.Message)) - } - - // 返回天眼查响应数据 - respBytes, err := json.Marshal(response.Data) + apiPath := "/communication/personal/10235" + data, err := deps.ShujubaoService.CallAPI(ctx, apiPath, reqParams) if err != nil { + if errors.Is(err, shujubao.ErrDatasource) { + return nil, errors.Join(processors.ErrDatasource, err) + } + if errors.Is(err, shujubao.ErrQueryEmpty) { + return nil, errors.Join(processors.ErrNotFound, err) + } return nil, errors.Join(processors.ErrSystem, err) } - return respBytes, nil + // 原始返回结构处理 - data 是 map[string]interface{} 类型 + dataMap, ok := data.(map[string]interface{}) + if !ok { + return nil, errors.Join(processors.ErrSystem, errors.New("data不是map类型")) + } + + // 提取 total + totalStr := "" + if v, ok := dataMap["total"]; ok { + if str, ok := v.(string); ok { + totalStr = str + } + } + + // 提取 items 数组 + srcItems := make([]map[string]interface{}, 0) + if v, ok := dataMap["items"]; ok { + if arr, ok := v.([]interface{}); ok { + for _, item := range arr { + if itemMap, ok := item.(map[string]interface{}); ok { + srcItems = append(srcItems, itemMap) + } + } + } + } + + // 构造返回数据(缺失字段留空字符串) + resp := QYGL7D9AResponse{ + Total: 0, + Items: make([]QYGL7D9AItem, 0, len(srcItems)), + } + + getString := func(item map[string]interface{}, key string) string { + if v, ok := item[key]; ok { + if str, ok := v.(string); ok { + return str + } + } + return "" + } + + for _, v := range srcItems { + resp.Items = append(resp.Items, QYGL7D9AItem{ + TaxIdNumber: getString(v, "taxpayerCode"), + NewOwnTaxBalance: getString(v, "thisOwedAmount"), + OwnTaxAmount: getString(v, "totalOwedAmount"), + PublishDate: getString(v, "publishDate"), + OwnTaxBalance: getString(v, "beforeOwedAmount"), + Type: getString(v, "taxBureauType"), + PersonIdNumber: "", + TaxCategory: getString(v, "taxOwedType"), + TaxpayerType: "", + PersonIdName: "", + Name: getString(v, "entityName"), + Location: getString(v, "businessAddress"), + Department: getString(v, "taxBureauName"), + RegType: "", + LegalpersonName: getString(v, "legalPerson"), + }) + } + + // total 转 int64 + if totalStr != "" { + resp.Total, _ = strconv.ParseInt(totalStr, 10, 64) + } + + return json.Marshal(resp) } diff --git a/internal/domains/api/services/processors/qygl/qygl8b4d_processor.go b/internal/domains/api/services/processors/qygl/qygl8b4d_processor.go index 35e3e10..037e27f 100644 --- a/internal/domains/api/services/processors/qygl/qygl8b4d_processor.go +++ b/internal/domains/api/services/processors/qygl/qygl8b4d_processor.go @@ -8,8 +8,50 @@ import ( "tyapi-server/internal/domains/api/dto" "tyapi-server/internal/domains/api/services/processors" + "tyapi-server/internal/infrastructure/external/shujubao" ) +/* +## 返回字段说明 + +| 字段名 | 类型 | 说明 | +|--------|------|------| +| items | array | 融资历史记录列表,无记录时为空数组 | +| items[].round | string | 融资轮次 | +| items[].amount | string | 融资金额 | +| items[].date | string | 融资日期,格式:YYYY-MM-DD | +| items[].investors | array of string | 投资方列表 | +| items[].postValuation | string | 融资后估值 | +| items[].preValuation | string | 融资前估值 | +| items[].currency | string | 币种 | +| items[].intro | string | 项目简介 | +| items[].shareRatio | string | 持股比例 | +| total | integer | 符合条件的融资记录总数,无记录时为 0 | +| pageNum | integer | 当前页码 | +| pageSize | integer | 每页记录条数 | +*/ + +// QYGL8B4DItem 融资历史单项 +type QYGL8B4DItem struct { + Round string `json:"round"` + Amount string `json:"amount"` + Date string `json:"date"` + Investors []string `json:"investors"` + PostValuation string `json:"postValuation"` + PreValuation string `json:"preValuation"` + Currency string `json:"currency"` + Intro string `json:"intro"` + ShareRatio string `json:"shareRatio"` +} + +// QYGL8B4DResponse 融资历史返回结构 +type QYGL8B4DResponse struct { + Items []QYGL8B4DItem `json:"items"` + Total int64 `json:"total"` + PageNum int64 `json:"pageNum"` + PageSize int64 `json:"pageSize"` +} + // ProcessQYGL8B4DRequest QYGL8B4D API处理方法 - 融资历史 func ProcessQYGL8B4DRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { var paramsDto dto.QYGL8B4DReq @@ -21,7 +63,7 @@ func ProcessQYGL8B4DRequest(ctx context.Context, params []byte, deps *processors return nil, errors.Join(processors.ErrInvalidParam, err) } - // 设置默认值 + // 默认值 pageSize := paramsDto.PageSize if pageSize == 0 { pageSize = int64(20) @@ -31,29 +73,99 @@ func ProcessQYGL8B4DRequest(ctx context.Context, params []byte, deps *processors pageNum = int64(1) } - // 构建API调用参数 - apiParams := map[string]string{ - "keyword": paramsDto.EntCode, - "pageSize": strconv.FormatInt(pageSize, 10), - "pageNum": strconv.FormatInt(pageNum, 10), + // 调用外部接口 + reqParams := map[string]interface{}{ + "key": "8887a17748d767b0a1f417171108873f", + "creditCode": paramsDto.EntCode, } - // 调用天眼查API - 融资历史 - response, err := deps.TianYanChaService.CallAPI(ctx, "FinancingHistory", apiParams) - if err != nil { - return nil, convertTianYanChaError(err) - } - - // 检查天眼查API调用是否成功 - if !response.Success { - return nil, errors.Join(processors.ErrDatasource, errors.New(response.Message)) - } - - // 返回天眼查响应数据 - respBytes, err := json.Marshal(response.Data) + apiPath := "/communication/personal/10435" + data, err := deps.ShujubaoService.CallAPI(ctx, apiPath, reqParams) if err != nil { + if errors.Is(err, shujubao.ErrDatasource) { + return nil, errors.Join(processors.ErrDatasource, err) + } + if errors.Is(err, shujubao.ErrQueryEmpty) { + return nil, errors.Join(processors.ErrNotFound, err) + } return nil, errors.Join(processors.ErrSystem, err) } - return respBytes, nil + // 原始返回结构处理 - data 是 map[string]interface{} 类型 + dataMap, ok := data.(map[string]interface{}) + if !ok { + return nil, errors.Join(processors.ErrSystem, errors.New("data不是map类型")) + } + + // 提取 page 对象中的 totalRecords + totalRecords := "" + if page, ok := dataMap["page"].(map[string]interface{}); ok { + if v, ok := page["totalRecords"]; ok { + if str, ok := v.(string); ok { + totalRecords = str + } + } + } + + // 提取 records 数组(不是 items) + records := make([]map[string]interface{}, 0) + if v, ok := dataMap["records"]; ok { + if arr, ok := v.([]interface{}); ok { + for _, item := range arr { + if itemMap, ok := item.(map[string]interface{}); ok { + records = append(records, itemMap) + } + } + } + } + + // 构造返回数据 + resp := QYGL8B4DResponse{ + Items: make([]QYGL8B4DItem, 0, len(records)), + PageNum: pageNum, + PageSize: pageSize, + } + + for _, item := range records { + // 提取字段,使用类型断言 + getString := func(key string) string { + if v, ok := item[key]; ok { + if str, ok := v.(string); ok { + return str + } + } + return "" + } + + investAmount := getString("investAmount") + afterValuation := getString("afterInvestValuation") + + // 计算持股比例:持股比例 = 本轮投资金额 / 投后估值 × 100% + shareRatio := "" + investAmountFloat, err1 := strconv.ParseFloat(investAmount, 64) + afterValuationFloat, err2 := strconv.ParseFloat(afterValuation, 64) + if err1 == nil && err2 == nil && afterValuationFloat > 0 { + ratio := investAmountFloat / afterValuationFloat * 100 + shareRatio = strconv.FormatFloat(ratio, 'f', 2, 64) // 保留两位小数 + } + + resp.Items = append(resp.Items, QYGL8B4DItem{ + Round: getString("financingRound"), + Amount: investAmount, + Date: getString("financingDate"), + Investors: []string{getString("financingRoundInvestor")}, + PostValuation: afterValuation, + PreValuation: getString("beforeInvestValuation"), + Currency: getString("currency"), + Intro: "", + ShareRatio: shareRatio, + }) + } + + // total + if totalRecords != "" { + resp.Total, _ = strconv.ParseInt(totalRecords, 10, 64) + } + + return json.Marshal(resp) } From a2913c26ab9b99ffbdbd82f593efef1220c1a5fc Mon Sep 17 00:00:00 2001 From: Mrx <18278715334@163.com> Date: Wed, 20 May 2026 15:52:04 +0800 Subject: [PATCH 08/28] f --- .../api/services/processors/qygl/qygl4b2e_processor.go | 6 +++--- .../api/services/processors/qygl/qygl7d9a_processor.go | 6 +++--- .../api/services/processors/qygl/qygl8b4d_processor.go | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/internal/domains/api/services/processors/qygl/qygl4b2e_processor.go b/internal/domains/api/services/processors/qygl/qygl4b2e_processor.go index a5b3e69..082eb4d 100644 --- a/internal/domains/api/services/processors/qygl/qygl4b2e_processor.go +++ b/internal/domains/api/services/processors/qygl/qygl4b2e_processor.go @@ -64,9 +64,9 @@ func ProcessQYGL4B2ERequest( if errors.Is(err, shujubao.ErrDatasource) { return nil, errors.Join(processors.ErrDatasource, err) } - if errors.Is(err, shujubao.ErrQueryEmpty) { - return nil, errors.Join(processors.ErrNotFound, err) - } + // if errors.Is(err, shujubao.ErrQueryEmpty) { + // return nil, errors.Join(processors.ErrNotFound, err) + // } return nil, errors.Join(processors.ErrSystem, err) } diff --git a/internal/domains/api/services/processors/qygl/qygl7d9a_processor.go b/internal/domains/api/services/processors/qygl/qygl7d9a_processor.go index efc2379..582d059 100644 --- a/internal/domains/api/services/processors/qygl/qygl7d9a_processor.go +++ b/internal/domains/api/services/processors/qygl/qygl7d9a_processor.go @@ -94,9 +94,9 @@ func ProcessQYGL7D9ARequest(ctx context.Context, params []byte, deps *processors if errors.Is(err, shujubao.ErrDatasource) { return nil, errors.Join(processors.ErrDatasource, err) } - if errors.Is(err, shujubao.ErrQueryEmpty) { - return nil, errors.Join(processors.ErrNotFound, err) - } + // if errors.Is(err, shujubao.ErrQueryEmpty) { + // return nil, errors.Join(processors.ErrNotFound, err) + // } return nil, errors.Join(processors.ErrSystem, err) } diff --git a/internal/domains/api/services/processors/qygl/qygl8b4d_processor.go b/internal/domains/api/services/processors/qygl/qygl8b4d_processor.go index 037e27f..5b6cfe4 100644 --- a/internal/domains/api/services/processors/qygl/qygl8b4d_processor.go +++ b/internal/domains/api/services/processors/qygl/qygl8b4d_processor.go @@ -85,9 +85,9 @@ func ProcessQYGL8B4DRequest(ctx context.Context, params []byte, deps *processors if errors.Is(err, shujubao.ErrDatasource) { return nil, errors.Join(processors.ErrDatasource, err) } - if errors.Is(err, shujubao.ErrQueryEmpty) { - return nil, errors.Join(processors.ErrNotFound, err) - } + // if errors.Is(err, shujubao.ErrQueryEmpty) { + // return nil, errors.Join(processors.ErrNotFound, err) + // } return nil, errors.Join(processors.ErrSystem, err) } From 95006a245591d9515f43a00a4ad17bda0cefe536 Mon Sep 17 00:00:00 2001 From: Mrx <18278715334@163.com> Date: Wed, 20 May 2026 16:16:50 +0800 Subject: [PATCH 09/28] f --- .../api/services/processors/qygl/qygl4b2e_processor.go | 9 +++++---- .../api/services/processors/qygl/qygl7d9a_processor.go | 9 +++++---- .../api/services/processors/qygl/qygl8b4d_processor.go | 9 +++++---- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/internal/domains/api/services/processors/qygl/qygl4b2e_processor.go b/internal/domains/api/services/processors/qygl/qygl4b2e_processor.go index 082eb4d..5d4db59 100644 --- a/internal/domains/api/services/processors/qygl/qygl4b2e_processor.go +++ b/internal/domains/api/services/processors/qygl/qygl4b2e_processor.go @@ -64,10 +64,11 @@ func ProcessQYGL4B2ERequest( if errors.Is(err, shujubao.ErrDatasource) { return nil, errors.Join(processors.ErrDatasource, err) } - // if errors.Is(err, shujubao.ErrQueryEmpty) { - // return nil, errors.Join(processors.ErrNotFound, err) - // } - return nil, errors.Join(processors.ErrSystem, err) + if errors.Is(err, shujubao.ErrQueryEmpty) { + data = map[string]interface{}{} + } else { + return nil, errors.Join(processors.ErrSystem, err) + } } // 原始返回结构处理 - data 是 map[string]interface{} 类型 diff --git a/internal/domains/api/services/processors/qygl/qygl7d9a_processor.go b/internal/domains/api/services/processors/qygl/qygl7d9a_processor.go index 582d059..083352c 100644 --- a/internal/domains/api/services/processors/qygl/qygl7d9a_processor.go +++ b/internal/domains/api/services/processors/qygl/qygl7d9a_processor.go @@ -94,10 +94,11 @@ func ProcessQYGL7D9ARequest(ctx context.Context, params []byte, deps *processors if errors.Is(err, shujubao.ErrDatasource) { return nil, errors.Join(processors.ErrDatasource, err) } - // if errors.Is(err, shujubao.ErrQueryEmpty) { - // return nil, errors.Join(processors.ErrNotFound, err) - // } - return nil, errors.Join(processors.ErrSystem, err) + if errors.Is(err, shujubao.ErrQueryEmpty) { + data = map[string]interface{}{} + } else { + return nil, errors.Join(processors.ErrSystem, err) + } } // 原始返回结构处理 - data 是 map[string]interface{} 类型 diff --git a/internal/domains/api/services/processors/qygl/qygl8b4d_processor.go b/internal/domains/api/services/processors/qygl/qygl8b4d_processor.go index 5b6cfe4..f4e43b6 100644 --- a/internal/domains/api/services/processors/qygl/qygl8b4d_processor.go +++ b/internal/domains/api/services/processors/qygl/qygl8b4d_processor.go @@ -85,10 +85,11 @@ func ProcessQYGL8B4DRequest(ctx context.Context, params []byte, deps *processors if errors.Is(err, shujubao.ErrDatasource) { return nil, errors.Join(processors.ErrDatasource, err) } - // if errors.Is(err, shujubao.ErrQueryEmpty) { - // return nil, errors.Join(processors.ErrNotFound, err) - // } - return nil, errors.Join(processors.ErrSystem, err) + if errors.Is(err, shujubao.ErrQueryEmpty) { + data = map[string]interface{}{} + } else { + return nil, errors.Join(processors.ErrSystem, err) + } } // 原始返回结构处理 - data 是 map[string]interface{} 类型 From 9fb5db56e39355eacbb175d98a29d512eafdb2d5 Mon Sep 17 00:00:00 2001 From: Mrx <18278715334@163.com> Date: Wed, 20 May 2026 20:30:50 +0800 Subject: [PATCH 10/28] f+ --- .../processors/qcxg/qcxg5f3a_processor.go | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/internal/domains/api/services/processors/qcxg/qcxg5f3a_processor.go b/internal/domains/api/services/processors/qcxg/qcxg5f3a_processor.go index 7c6338a..830dab6 100644 --- a/internal/domains/api/services/processors/qcxg/qcxg5f3a_processor.go +++ b/internal/domains/api/services/processors/qcxg/qcxg5f3a_processor.go @@ -10,7 +10,7 @@ import ( "tyapi-server/internal/infrastructure/external/jiguang" ) -// ProcessQCXG5F3ARequest QCXG5F3A API处理方法 - 极光名下车辆车牌查询 +// ProcessQCXG5F3ARequest QCXG5F3A API处理方法 - 极光名下车辆车牌查询 以替换数量 func ProcessQCXG5F3ARequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { var paramsDto dto.QCXG5F3AReq if err := json.Unmarshal(params, ¶msDto); err != nil { @@ -21,21 +21,17 @@ func ProcessQCXG5F3ARequest(ctx context.Context, params []byte, deps *processors return nil, errors.Join(processors.ErrInvalidParam, err) } - null := "" // 构建请求参数 reqData := map[string]interface{}{ - "id_card": paramsDto.IDCard, - "name": paramsDto.Name, - "userType": null, - "vehicleType": null, - "encryptionType": null, - "encryptionContent": null, + "idNum": paramsDto.IDCard, + "name": paramsDto.Name, + "userType": "1", } // 调用极光API - // apiCode: vehicle-person-vehicles (用于请求头) - // apiPath: vehicle/person-vehicles (用于URL路径) - respBytes, err := deps.JiguangService.CallAPI(ctx, "vehicle-person-vehicles", "vehicle/person-vehicles", reqData) + // apiCode: vehicle-inquiry-under-name (用于请求头) + // apiPath: vehicle/inquiry-under-name (用于URL路径) + respBytes, err := deps.JiguangService.CallAPI(ctx, "vehicle-inquiry-under-name", "vehicle/inquiry-under-name", reqData) if err != nil { // 根据错误类型返回相应的错误 if errors.Is(err, jiguang.ErrNotFound) { From 1dfe688db7e96f4516f0bd47cefd32832d48a066 Mon Sep 17 00:00:00 2001 From: Mrx <18278715334@163.com> Date: Thu, 21 May 2026 10:27:05 +0800 Subject: [PATCH 11/28] f --- .../processors/qcxg/qcxg9p1c_processor.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/internal/domains/api/services/processors/qcxg/qcxg9p1c_processor.go b/internal/domains/api/services/processors/qcxg/qcxg9p1c_processor.go index 1223cde..6f615ee 100644 --- a/internal/domains/api/services/processors/qcxg/qcxg9p1c_processor.go +++ b/internal/domains/api/services/processors/qcxg/qcxg9p1c_processor.go @@ -27,18 +27,15 @@ func ProcessQCXG9P1CRequest(ctx context.Context, params []byte, deps *processors null := "" // 构建请求参数 reqData := map[string]interface{}{ - "id_card": paramsDto.IDCard, - "name": null, - "userType": null, - "vehicleType": null, - "encryptionType": null, - "encryptionContent": null, + "idNum": paramsDto.IDCard, + "name": null, + "userType": "1", } // 调用极光API - // apiCode: vehicle-person-vehicles (用于请求头) - // apiPath: vehicle/person-vehicles (用于URL路径) - respBytes, err := deps.JiguangService.CallAPI(ctx, "vehicle-person-vehicles", "vehicle/person-vehicles", reqData) + // apiCode: vehicle-inquiry-under-name (用于请求头) + // apiPath: vehicle/inquiry-under-name (用于URL路径) + respBytes, err := deps.JiguangService.CallAPI(ctx, "vehicle-inquiry-under-name", "vehicle/inquiry-under-name", reqData) if err != nil { // 根据错误类型返回相应的错误 if errors.Is(err, jiguang.ErrNotFound) { @@ -50,6 +47,9 @@ func ProcessQCXG9P1CRequest(ctx context.Context, params []byte, deps *processors } } + // 极光服务已经返回了 data 字段的 JSON,直接返回即可 + return respBytes, nil + // 使用 gjson 检查并转换 vehicleCount 字段 vehicleCountResult := gjson.GetBytes(respBytes, "vehicleCount") if vehicleCountResult.Exists() && vehicleCountResult.Type == gjson.String { From 2020bd732aac47d7aa5e789afec3faf133cb4106 Mon Sep 17 00:00:00 2001 From: Mrx <18278715334@163.com> Date: Thu, 21 May 2026 10:27:41 +0800 Subject: [PATCH 12/28] f --- .../domains/api/services/processors/qcxg/qcxg9p1c_processor.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/domains/api/services/processors/qcxg/qcxg9p1c_processor.go b/internal/domains/api/services/processors/qcxg/qcxg9p1c_processor.go index 6f615ee..3f18c40 100644 --- a/internal/domains/api/services/processors/qcxg/qcxg9p1c_processor.go +++ b/internal/domains/api/services/processors/qcxg/qcxg9p1c_processor.go @@ -48,7 +48,7 @@ func ProcessQCXG9P1CRequest(ctx context.Context, params []byte, deps *processors } // 极光服务已经返回了 data 字段的 JSON,直接返回即可 - return respBytes, nil + // return respBytes, nil // 使用 gjson 检查并转换 vehicleCount 字段 vehicleCountResult := gjson.GetBytes(respBytes, "vehicleCount") From 95c3fc03154f8fc3aec2e7c690f72b2e104fb949 Mon Sep 17 00:00:00 2001 From: Mrx <18278715334@163.com> Date: Sat, 23 May 2026 10:30:17 +0800 Subject: [PATCH 13/28] f --- .../api/services/processors/flxg/flxg0v4b_processor.go | 2 +- .../api/services/processors/flxg/flxg5a3b_processor.go | 2 +- .../api/services/processors/flxg/flxg7e8f_processor.go | 2 +- .../api/services/processors/flxg/flxgca3d_processor.go | 2 +- .../api/services/processors/flxg/flxgdea8_processor.go | 4 ++++ .../api/services/processors/flxg/flxgdea9_processor.go | 2 +- 6 files changed, 9 insertions(+), 5 deletions(-) diff --git a/internal/domains/api/services/processors/flxg/flxg0v4b_processor.go b/internal/domains/api/services/processors/flxg/flxg0v4b_processor.go index b7280eb..6c18004 100644 --- a/internal/domains/api/services/processors/flxg/flxg0v4b_processor.go +++ b/internal/domains/api/services/processors/flxg/flxg0v4b_processor.go @@ -25,7 +25,7 @@ func ProcessFLXG0V4BRequest(ctx context.Context, params []byte, deps *processors return nil, errors.Join(processors.ErrInvalidParam, err) } // 去掉司法案件案件去掉身份证号码 - if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" { + if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" { return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空")) } encryptedName, err := deps.WestDexService.Encrypt(paramsDto.Name) diff --git a/internal/domains/api/services/processors/flxg/flxg5a3b_processor.go b/internal/domains/api/services/processors/flxg/flxg5a3b_processor.go index b0b9118..a8ef54e 100644 --- a/internal/domains/api/services/processors/flxg/flxg5a3b_processor.go +++ b/internal/domains/api/services/processors/flxg/flxg5a3b_processor.go @@ -20,7 +20,7 @@ func ProcessFLXG5A3BRequest(ctx context.Context, params []byte, deps *processors if err := deps.Validator.ValidateStruct(paramsDto); err != nil { return nil, errors.Join(processors.ErrInvalidParam, err) } - if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" { + if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" { return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空")) } encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name) diff --git a/internal/domains/api/services/processors/flxg/flxg7e8f_processor.go b/internal/domains/api/services/processors/flxg/flxg7e8f_processor.go index fe849b6..8b43ac1 100644 --- a/internal/domains/api/services/processors/flxg/flxg7e8f_processor.go +++ b/internal/domains/api/services/processors/flxg/flxg7e8f_processor.go @@ -20,7 +20,7 @@ func ProcessFLXG7E8FRequest(ctx context.Context, params []byte, deps *processors if err := deps.Validator.ValidateStruct(paramsDto); err != nil { return nil, errors.Join(processors.ErrInvalidParam, err) } - if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" { + if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" { return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空")) } diff --git a/internal/domains/api/services/processors/flxg/flxgca3d_processor.go b/internal/domains/api/services/processors/flxg/flxgca3d_processor.go index f410fd2..c355218 100644 --- a/internal/domains/api/services/processors/flxg/flxgca3d_processor.go +++ b/internal/domains/api/services/processors/flxg/flxgca3d_processor.go @@ -20,7 +20,7 @@ func ProcessFLXGCA3DRequest(ctx context.Context, params []byte, deps *processors if err := deps.Validator.ValidateStruct(paramsDto); err != nil { return nil, errors.Join(processors.ErrInvalidParam, err) } - if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" { + if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" { return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空")) } encryptedName, err := deps.WestDexService.Encrypt(paramsDto.Name) diff --git a/internal/domains/api/services/processors/flxg/flxgdea8_processor.go b/internal/domains/api/services/processors/flxg/flxgdea8_processor.go index 981c4e7..e30bbb0 100644 --- a/internal/domains/api/services/processors/flxg/flxgdea8_processor.go +++ b/internal/domains/api/services/processors/flxg/flxgdea8_processor.go @@ -20,6 +20,10 @@ func ProcessFLXGDEA8Request(ctx context.Context, params []byte, deps *processors if err := deps.Validator.ValidateStruct(paramsDto); err != nil { return nil, errors.Join(processors.ErrInvalidParam, err) } + // 去掉司法案件案件去掉身份证号码 + if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" { + return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空")) + } encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name) if err != nil { diff --git a/internal/domains/api/services/processors/flxg/flxgdea9_processor.go b/internal/domains/api/services/processors/flxg/flxgdea9_processor.go index a42166a..d002a68 100644 --- a/internal/domains/api/services/processors/flxg/flxgdea9_processor.go +++ b/internal/domains/api/services/processors/flxg/flxgdea9_processor.go @@ -25,7 +25,7 @@ func ProcessFLXGDEA9Request(ctx context.Context, params []byte, deps *processors if err != nil { return nil, errors.Join(processors.ErrSystem, err) } - if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" { + if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" { return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空")) } encryptedIDCard, err := deps.ZhichaService.Encrypt(paramsDto.IDCard) From 1cb3363b5cbfa0ef7effd997743b5deaa9df1053 Mon Sep 17 00:00:00 2001 From: Mrx <18278715334@163.com> Date: Tue, 26 May 2026 11:14:24 +0800 Subject: [PATCH 14/28] f --- .../api/services/processors/flxg/flxg5a3b_processor.go | 1 + .../api/services/processors/jrzq/jrzq1e7b_processor.go | 10 +++++----- .../api/services/processors/jrzq/jrzq3p01_processor.go | 1 + .../api/services/processors/qygl/qygl6s1b_processor.go | 3 ++- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/internal/domains/api/services/processors/flxg/flxg5a3b_processor.go b/internal/domains/api/services/processors/flxg/flxg5a3b_processor.go index a8ef54e..95e289a 100644 --- a/internal/domains/api/services/processors/flxg/flxg5a3b_processor.go +++ b/internal/domains/api/services/processors/flxg/flxg5a3b_processor.go @@ -23,6 +23,7 @@ func ProcessFLXG5A3BRequest(ctx context.Context, params []byte, deps *processors if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" { return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空")) } + encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name) if err != nil { return nil, errors.Join(processors.ErrSystem, err) diff --git a/internal/domains/api/services/processors/jrzq/jrzq1e7b_processor.go b/internal/domains/api/services/processors/jrzq/jrzq1e7b_processor.go index c991b8d..9828442 100644 --- a/internal/domains/api/services/processors/jrzq/jrzq1e7b_processor.go +++ b/internal/domains/api/services/processors/jrzq/jrzq1e7b_processor.go @@ -37,9 +37,9 @@ func ProcessJRZQ1E7BRequest(ctx context.Context, params []byte, deps *processors } reqData := map[string]interface{}{ - "name": encryptedName, - "idCard": encryptedIDCard, - "phone": encryptedMobileNo, + "name": encryptedName, + "idCard": encryptedIDCard, + "phone": encryptedMobileNo, "authorized": paramsDto.Authorized, } @@ -55,9 +55,9 @@ func ProcessJRZQ1E7BRequest(ctx context.Context, params []byte, deps *processors // 将响应数据转换为 JSON 字节 respBytes, err := json.Marshal(respData) if err != nil { - return nil, errors.Join(processors.ErrSystem, err) + // return nil, errors.Join(processors.ErrSystem, err) + return json.Marshal(map[string]interface{}{}) } return respBytes, nil } - diff --git a/internal/domains/api/services/processors/jrzq/jrzq3p01_processor.go b/internal/domains/api/services/processors/jrzq/jrzq3p01_processor.go index 72dc1d0..8672201 100644 --- a/internal/domains/api/services/processors/jrzq/jrzq3p01_processor.go +++ b/internal/domains/api/services/processors/jrzq/jrzq3p01_processor.go @@ -40,6 +40,7 @@ func ProcessJRZQ3P01Request(ctx context.Context, params []byte, deps *processors respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI109", reqData) if err != nil { if errors.Is(err, zhicha.ErrDatasource) { + return nil, errors.Join(processors.ErrDatasource, err) } else { return nil, errors.Join(processors.ErrSystem, err) diff --git a/internal/domains/api/services/processors/qygl/qygl6s1b_processor.go b/internal/domains/api/services/processors/qygl/qygl6s1b_processor.go index 0f31b89..c17784d 100644 --- a/internal/domains/api/services/processors/qygl/qygl6s1b_processor.go +++ b/internal/domains/api/services/processors/qygl/qygl6s1b_processor.go @@ -37,7 +37,8 @@ func ProcessQYGL6S1BRequest(ctx context.Context, params []byte, deps *processors return nil, errors.Join(processors.ErrDatasource, err) } if errors.Is(err, shujubao.ErrQueryEmpty) { - return nil, errors.Join(processors.ErrNotFound, err) + // return nil, errors.Join(processors.ErrNotFound, e + data = map[string]interface{}{} } return nil, errors.Join(processors.ErrSystem, err) } From 9bd83ddaba4eebc5424454d681d05ca72f7f5fb9 Mon Sep 17 00:00:00 2001 From: Mrx <18278715334@163.com> Date: Tue, 26 May 2026 11:21:58 +0800 Subject: [PATCH 15/28] f --- .../domains/api/services/processors/qygl/qygl6s1b_processor.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/domains/api/services/processors/qygl/qygl6s1b_processor.go b/internal/domains/api/services/processors/qygl/qygl6s1b_processor.go index c17784d..6759ace 100644 --- a/internal/domains/api/services/processors/qygl/qygl6s1b_processor.go +++ b/internal/domains/api/services/processors/qygl/qygl6s1b_processor.go @@ -39,8 +39,9 @@ func ProcessQYGL6S1BRequest(ctx context.Context, params []byte, deps *processors if errors.Is(err, shujubao.ErrQueryEmpty) { // return nil, errors.Join(processors.ErrNotFound, e data = map[string]interface{}{} + } else { + return nil, errors.Join(processors.ErrSystem, err) } - return nil, errors.Join(processors.ErrSystem, err) } // 解析响应中的 JSON 字符串(使用 RecursiveParse) From 760c5812ee3adff6ce6ae096250dad8237881dac Mon Sep 17 00:00:00 2001 From: Mrx <18278715334@163.com> Date: Tue, 26 May 2026 11:28:25 +0800 Subject: [PATCH 16/28] f --- .../api/services/processors/qygl/qygl6s1b_processor.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/domains/api/services/processors/qygl/qygl6s1b_processor.go b/internal/domains/api/services/processors/qygl/qygl6s1b_processor.go index 6759ace..f258356 100644 --- a/internal/domains/api/services/processors/qygl/qygl6s1b_processor.go +++ b/internal/domains/api/services/processors/qygl/qygl6s1b_processor.go @@ -37,8 +37,8 @@ func ProcessQYGL6S1BRequest(ctx context.Context, params []byte, deps *processors return nil, errors.Join(processors.ErrDatasource, err) } if errors.Is(err, shujubao.ErrQueryEmpty) { - // return nil, errors.Join(processors.ErrNotFound, e - data = map[string]interface{}{} + return nil, errors.Join(processors.ErrNotFound, err) + // data = map[string]interface{}{} } else { return nil, errors.Join(processors.ErrSystem, err) } From f10c5dd626fe8c8851334f7b424cf67335438685 Mon Sep 17 00:00:00 2001 From: Mrx <18278715334@163.com> Date: Tue, 26 May 2026 16:17:09 +0800 Subject: [PATCH 17/28] f --- config.yaml | 2 + internal/config/config.go | 2 + internal/domains/api/dto/api_request_dto.go | 10 ++ .../api/services/api_request_service.go | 2 + .../api/services/form_config_service.go | 2 + .../processors/flxg/flxg0v4b_processor.go | 2 +- .../processors/flxg/flxg5a3b_processor.go | 2 +- .../processors/flxg/flxg7e8f_processor.go | 2 +- .../processors/flxg/flxgca3d_processor.go | 2 +- .../processors/flxg/flxgdea8_processor.go | 2 +- .../processors/flxg/flxgdea9_processor.go | 2 +- .../processors/flxg/flxghb4f_processor.go | 60 +++++++ .../processors/jrzq/jrzq2f8a_processor.go | 7 +- .../processors/qygl/qyglbh7y_processor.go | 55 ++++++ internal/infrastructure/external/huibo/2.md | 90 ++++++++++ .../infrastructure/external/huibo/crypto.go | 83 +++++++++ .../external/huibo/curl_helper.go | 59 ++++++ .../external/huibo/huibo_factory.go | 2 + .../external/huibo/huibo_service.go | 170 ++++++++++++++++++ .../external/huibo/status_codes.go | 52 ++++++ 20 files changed, 598 insertions(+), 10 deletions(-) create mode 100644 internal/domains/api/services/processors/flxg/flxghb4f_processor.go create mode 100644 internal/domains/api/services/processors/qygl/qyglbh7y_processor.go create mode 100644 internal/infrastructure/external/huibo/2.md create mode 100644 internal/infrastructure/external/huibo/crypto.go create mode 100644 internal/infrastructure/external/huibo/curl_helper.go create mode 100644 internal/infrastructure/external/huibo/status_codes.go diff --git a/config.yaml b/config.yaml index c4ec820..6399b64 100644 --- a/config.yaml +++ b/config.yaml @@ -654,6 +654,8 @@ huibo: aes_key: "NQYN3YO+pb/GEcCBNX0ptMb7cUlnXSPvcX7VvNofBkc=" work_order_code: "gd219219725093" product_code: "22089" + baseUrl2: "https://napi.zhixin.net:9000/api/data" + app_code2: "1508795945301708800" logging: enabled: true diff --git a/internal/config/config.go b/internal/config/config.go index 19366b0..13e432e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -682,6 +682,8 @@ type HuiboConfig struct { AESKey string `mapstructure:"aes_key"` WorkOrderCode string `mapstructure:"work_order_code"` ProductCode string `mapstructure:"product_code"` + BaseURL2 string `mapstructure:"baseUrl2"` + AppCode2 string `mapstructure:"app_code2"` Logging HuiboLoggingConfig `mapstructure:"logging"` } diff --git a/internal/domains/api/dto/api_request_dto.go b/internal/domains/api/dto/api_request_dto.go index 041f2f5..6bfa06c 100644 --- a/internal/domains/api/dto/api_request_dto.go +++ b/internal/domains/api/dto/api_request_dto.go @@ -534,6 +534,12 @@ type IVYZ4Y27Req struct { IDCard string `json:"id_card" validate:"required,validIDCard"` AuthAuthorizeFileBase64 string `json:"auth_authorize_file_base64" validate:"required,validBase64PDF"` } + +type FLXGHB4FReq struct { + Name string `json:"name" validate:"required,min=1,validName"` + IDCard string `json:"id_card" validate:"required,validIDCard"` +} + type IVYZP2Q6Req struct { Name string `json:"name" validate:"required,min=1,validName"` IDCard string `json:"id_card" validate:"required,validIDCard"` @@ -572,6 +578,10 @@ type QYGL5A3CReq struct { PageNum int64 `json:"page_num" validate:"omitempty,min=1"` } +type QYGLBH7YReq struct { + EntName string `json:"ent_name" validate:"omitempty,min=1,validEnterpriseName"` +} + type QYGL2naoReq struct { EntCode string `json:"ent_code" validate:"required,validUSCI"` PageSize int64 `json:"page_size" validate:"omitempty,min=1,max=100"` diff --git a/internal/domains/api/services/api_request_service.go b/internal/domains/api/services/api_request_service.go index a3117ff..f47e630 100644 --- a/internal/domains/api/services/api_request_service.go +++ b/internal/domains/api/services/api_request_service.go @@ -186,6 +186,7 @@ func registerAllProcessors(combService *comb.CombService) { "FLXG3A9B": flxg.ProcessFLXG3A9BRequest, "FLXGK5D2": flxg.ProcessFLXGK5D2Request, "FLXGDJG3": flxg.ProcessFLXGDJG3Request, //董监高司法综合信息核验 + "FLXGHB4F": flxg.ProcessFLXGHB4FRequest, //个人涉诉案件查询汇博 // JRZQ系列处理器 "JRZQ8203": jrzq.ProcessJRZQ8203Request, "JRZQ0A03": jrzq.ProcessJRZQ0A03Request, @@ -254,6 +255,7 @@ func registerAllProcessors(combService *comb.CombService) { "QYGLDJ12": qygl.ProcessQYGLDJ12Request, //企业年报信息核验 "QYGL8848": qygl.ProcessQYGL8848Request, //企业税收违法核查 "QYGLDJ33": qygl.ProcessQYGLDJ33Request, //企业年报信息核验 + "QYGLBH7Y": qygl.ProcessQYGLBH7YRequest, //企业涉诉案件查询汇博 // YYSY系列处理器 "YYSY35TA": yysy.ProcessYYSY35TARequest, //运营商归属地数卖 diff --git a/internal/domains/api/services/form_config_service.go b/internal/domains/api/services/form_config_service.go index 1bd2fc4..69cf966 100644 --- a/internal/domains/api/services/form_config_service.go +++ b/internal/domains/api/services/form_config_service.go @@ -281,6 +281,8 @@ func (s *FormConfigServiceImpl) getDTOStruct(ctx context.Context, apiCode string "IVYZRAX1": &dto.IVYZRAX1Req{}, //融安信用分 "IVYZRAX2": &dto.IVYZRAX1Req{}, //融御反欺诈 "IVYZ2MN7": &dto.IVYZ2MN6Req{}, //学历Bzhicha + "FLXGHB4F": &dto.FLXGHB4FReq{}, //个人涉诉案件查询汇博 + "QYGLBH7Y": &dto.QYGLBH7YReq{}, //企业涉诉案件查询汇博 } // 优先返回已配置的DTO diff --git a/internal/domains/api/services/processors/flxg/flxg0v4b_processor.go b/internal/domains/api/services/processors/flxg/flxg0v4b_processor.go index 6c18004..b4bba6d 100644 --- a/internal/domains/api/services/processors/flxg/flxg0v4b_processor.go +++ b/internal/domains/api/services/processors/flxg/flxg0v4b_processor.go @@ -25,7 +25,7 @@ func ProcessFLXG0V4BRequest(ctx context.Context, params []byte, deps *processors return nil, errors.Join(processors.ErrInvalidParam, err) } // 去掉司法案件案件去掉身份证号码 - if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" { + if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "420116198907031413" { return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空")) } encryptedName, err := deps.WestDexService.Encrypt(paramsDto.Name) diff --git a/internal/domains/api/services/processors/flxg/flxg5a3b_processor.go b/internal/domains/api/services/processors/flxg/flxg5a3b_processor.go index 95e289a..d6454a7 100644 --- a/internal/domains/api/services/processors/flxg/flxg5a3b_processor.go +++ b/internal/domains/api/services/processors/flxg/flxg5a3b_processor.go @@ -20,7 +20,7 @@ func ProcessFLXG5A3BRequest(ctx context.Context, params []byte, deps *processors if err := deps.Validator.ValidateStruct(paramsDto); err != nil { return nil, errors.Join(processors.ErrInvalidParam, err) } - if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" { + if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "420116198907031413" { return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空")) } diff --git a/internal/domains/api/services/processors/flxg/flxg7e8f_processor.go b/internal/domains/api/services/processors/flxg/flxg7e8f_processor.go index 8b43ac1..af59fa7 100644 --- a/internal/domains/api/services/processors/flxg/flxg7e8f_processor.go +++ b/internal/domains/api/services/processors/flxg/flxg7e8f_processor.go @@ -20,7 +20,7 @@ func ProcessFLXG7E8FRequest(ctx context.Context, params []byte, deps *processors if err := deps.Validator.ValidateStruct(paramsDto); err != nil { return nil, errors.Join(processors.ErrInvalidParam, err) } - if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" { + if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "420116198907031413" { return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空")) } diff --git a/internal/domains/api/services/processors/flxg/flxgca3d_processor.go b/internal/domains/api/services/processors/flxg/flxgca3d_processor.go index c355218..ab20c6c 100644 --- a/internal/domains/api/services/processors/flxg/flxgca3d_processor.go +++ b/internal/domains/api/services/processors/flxg/flxgca3d_processor.go @@ -20,7 +20,7 @@ func ProcessFLXGCA3DRequest(ctx context.Context, params []byte, deps *processors if err := deps.Validator.ValidateStruct(paramsDto); err != nil { return nil, errors.Join(processors.ErrInvalidParam, err) } - if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" { + if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "420116198907031413" { return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空")) } encryptedName, err := deps.WestDexService.Encrypt(paramsDto.Name) diff --git a/internal/domains/api/services/processors/flxg/flxgdea8_processor.go b/internal/domains/api/services/processors/flxg/flxgdea8_processor.go index e30bbb0..b2c95f4 100644 --- a/internal/domains/api/services/processors/flxg/flxgdea8_processor.go +++ b/internal/domains/api/services/processors/flxg/flxgdea8_processor.go @@ -21,7 +21,7 @@ func ProcessFLXGDEA8Request(ctx context.Context, params []byte, deps *processors return nil, errors.Join(processors.ErrInvalidParam, err) } // 去掉司法案件案件去掉身份证号码 - if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" { + if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "420116198907031413" { return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空")) } diff --git a/internal/domains/api/services/processors/flxg/flxgdea9_processor.go b/internal/domains/api/services/processors/flxg/flxgdea9_processor.go index d002a68..68da3c6 100644 --- a/internal/domains/api/services/processors/flxg/flxgdea9_processor.go +++ b/internal/domains/api/services/processors/flxg/flxgdea9_processor.go @@ -25,7 +25,7 @@ func ProcessFLXGDEA9Request(ctx context.Context, params []byte, deps *processors if err != nil { return nil, errors.Join(processors.ErrSystem, err) } - if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" { + if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "420116198907031413" { return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空")) } encryptedIDCard, err := deps.ZhichaService.Encrypt(paramsDto.IDCard) diff --git a/internal/domains/api/services/processors/flxg/flxghb4f_processor.go b/internal/domains/api/services/processors/flxg/flxghb4f_processor.go new file mode 100644 index 0000000..307d36b --- /dev/null +++ b/internal/domains/api/services/processors/flxg/flxghb4f_processor.go @@ -0,0 +1,60 @@ +package flxg + +import ( + "context" + "encoding/json" + "errors" + + "tyapi-server/internal/domains/api/dto" + "tyapi-server/internal/domains/api/services/processors" + "tyapi-server/internal/infrastructure/external/huibo" +) + +// ProcessFLXGHB4FRequest FLXGHB4F API处理方法 - 个人涉诉案件查询汇博 +func ProcessFLXGHB4FRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { + var paramsDto dto.FLXGHB4FReq + if err := json.Unmarshal(params, ¶msDto); err != nil { + return nil, errors.Join(processors.ErrSystem, err) + } + + if deps.HuiboService == nil { + return nil, errors.Join(processors.ErrSystem, errors.New("汇博服务未初始化")) + } + + // 使用 MD5 加密 name 和 idCard + encryptedName := "MD5:" + huibo.MD5Encrypt(paramsDto.Name, deps.HuiboService.GetConfig().AppKey) + encryptedIDCard := "MD5:" + huibo.MD5Encrypt(paramsDto.IDCard, deps.HuiboService.GetConfig().AppKey) + + reqdata := map[string]interface{}{ + "name": encryptedName, + "idCard": encryptedIDCard, + } + + respBytes, err := deps.HuiboService.CallAPI2(ctx, "P_004_0271", reqdata) + if err != nil { + return nil, errors.Join(processors.ErrDatasource, err) + } + + // 解析响应 + var response huibo.CallAPI2Response + if err := json.Unmarshal(respBytes, &response); err != nil { + return nil, errors.Join(processors.ErrSystem, err) + } + + // 处理状态码 + switch response.Code { + case huibo.CallAPI2StatusSuccess: + // 查询成功 + if response.Data == nil { + return []byte("{}"), nil + } + return respBytes, nil + case huibo.CallAPI2StatusNoData: + // 查询成功,无数据 - 按产品约定按调用成功计费 + return []byte("{}"), nil + default: + // 其他错误状态码 + message := huibo.GetCallAPI2StatusMessage(response.Code) + return nil, errors.Join(processors.ErrDatasource, errors.New(message)) + } +} diff --git a/internal/domains/api/services/processors/jrzq/jrzq2f8a_processor.go b/internal/domains/api/services/processors/jrzq/jrzq2f8a_processor.go index 4aed478..b4460dd 100644 --- a/internal/domains/api/services/processors/jrzq/jrzq2f8a_processor.go +++ b/internal/domains/api/services/processors/jrzq/jrzq2f8a_processor.go @@ -37,9 +37,9 @@ func ProcessJRZQ2F8ARequest(ctx context.Context, params []byte, deps *processors } reqData := map[string]interface{}{ - "name": encryptedName, - "idCard": encryptedIDCard, - "phone": encryptedMobileNo, + "name": encryptedName, + "idCard": encryptedIDCard, + "phone": encryptedMobileNo, "authorized": paramsDto.Authorized, } @@ -60,4 +60,3 @@ func ProcessJRZQ2F8ARequest(ctx context.Context, params []byte, deps *processors return respBytes, nil } - diff --git a/internal/domains/api/services/processors/qygl/qyglbh7y_processor.go b/internal/domains/api/services/processors/qygl/qyglbh7y_processor.go new file mode 100644 index 0000000..18c6d13 --- /dev/null +++ b/internal/domains/api/services/processors/qygl/qyglbh7y_processor.go @@ -0,0 +1,55 @@ +package qygl + +import ( + "context" + "encoding/json" + "errors" + + "tyapi-server/internal/domains/api/dto" + "tyapi-server/internal/domains/api/services/processors" + "tyapi-server/internal/infrastructure/external/huibo" +) + +// ProcessQYGLBH7YRequest QYGLBH7Y API处理方法 - 个人涉诉案件查询汇博 +func ProcessQYGLBH7YRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { + var paramsDto dto.QYGLBH7YReq + if err := json.Unmarshal(params, ¶msDto); err != nil { + return nil, errors.Join(processors.ErrSystem, err) + } + + if deps.HuiboService == nil { + return nil, errors.Join(processors.ErrSystem, errors.New("汇博服务未初始化")) + } + + reqdata := map[string]interface{}{ + "name": paramsDto.EntName, + } + + respBytes, err := deps.HuiboService.CallAPI2(ctx, "E_004_0261", reqdata) + if err != nil { + return nil, errors.Join(processors.ErrDatasource, err) + } + + // 解析响应 + var response huibo.CallAPI2Response + if err := json.Unmarshal(respBytes, &response); err != nil { + return nil, errors.Join(processors.ErrSystem, err) + } + + // 处理状态码 + switch response.Code { + case huibo.CallAPI2StatusSuccess: + // 查询成功 + if response.Data == nil { + return []byte("{}"), nil + } + return respBytes, nil + case huibo.CallAPI2StatusNoData: + // 查询成功,无数据 - 按产品约定按调用成功计费 + return []byte("{}"), nil + default: + // 其他错误状态码 + message := huibo.GetCallAPI2StatusMessage(response.Code) + return nil, errors.Join(processors.ErrDatasource, errors.New(message)) + } +} diff --git a/internal/infrastructure/external/huibo/2.md b/internal/infrastructure/external/huibo/2.md new file mode 100644 index 0000000..507e5fd --- /dev/null +++ b/internal/infrastructure/external/huibo/2.md @@ -0,0 +1,90 @@ +自然人公开涉诉信息查询接口文档 +接口编码:BHSC-P_004_0271 +版本:V1.0 +实施日期:2026-04-13 +适用场景:合作厂家接入中胜信用平台,用于个人涉诉案件查询(含失信、限高) +1. 通信说明 +请求方式:HTTP POST +数据格式:JSON +编码格式:UTF-8 +安全要求:请求 IP 需提前绑定 +2. 请求信息 +请求地址 +http://host:port/api/data +请求头(Header) +表格 +参数名 含义 必填 类型 备注 +AppCode 接口授权码 Y String 接口服务商提供 +pcode 产品编码 Y String 固定值:P_004_0271 +请求体(Body) +表格 +参数名 含义 必填 类型 说明 +name 姓名 Y String 支持明文 / MD5 加密 +idCard 身份证号 Y String 支持明文 / MD5 加密 +请求示例(明文) +json +{ + "idCard": "2103111*****0", + "name": "*****" +} +请求示例(加密) +json +{ + "name": "MD5:3e29aae20b7b92775*****", + "idCard": "MD5:a0c28f3a5a*****14" +} +3. 响应信息 +表格 +字段名 含义 类型 备注 +code 状态码 String 参考状态码说明 +message 描述信息 String - +orderNo 订单号 String - +pcode 产品编码 String 与请求一致 +param 请求参数 Object 原样返回 +charge 是否收费 Boolean true = 收费;false = 不收费 +time 响应时间戳 String 13 位毫秒 +data 业务数据 Object 涉诉 / 失信 / 限高数据 +响应示例 +json +{ + "code": "100", + "orderNo": "1361269246899077120", + "charge": true, + "data": { + "ss": { + "preservation": { "count": {} }, + "crc": 35****4186, + "cases_tree": {}, + "administrative": {}, + "civil": {}, + "count": {}, + "implement": {}, + "criminal": {}, + "bankrupt": {} + }, + "sxbzxr": [{}], + "xgbzxr": [] + }, + "pcode": "P_004_0271", + "param": null, + "time": "1744593480538", + "message": "查询成功" +} +data 节点说明 +ss:涉诉案件(民事 / 刑事 / 执行 / 行政 / 破产等) +sxbzxr:失信被执行人 +xgbzxr:限制高消费 +4. 状态码说明 +表格 +状态码 描述 +100 查询成功 +101 参数错误 +103 账户不存在 +104 IP 限制 +105 账号已过期 +107 服务不存在 +108 产品通道已关闭 +109 账户资金不足 +110 查询成功,无数据 +500 未知请求错误 +要不要我帮你把这份接口直接写成可上线的小程序请求代码(含加密、header、异常捕获、状态码统一处理)? \ No newline at end of file diff --git a/internal/infrastructure/external/huibo/crypto.go b/internal/infrastructure/external/huibo/crypto.go new file mode 100644 index 0000000..834fbee --- /dev/null +++ b/internal/infrastructure/external/huibo/crypto.go @@ -0,0 +1,83 @@ +package huibo + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/md5" + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "errors" + "fmt" + "io" +) + +// MD5Encrypt 使用 AppKey 进行 MD5 加密 +func MD5Encrypt(data, appKey string) string { + h := md5.New() + h.Write([]byte(data + appKey)) + return fmt.Sprintf("%x", h.Sum(nil)) +} + +// HMACSHA256Base64 使用 HMAC-SHA256 算法生成签名 +func HMACSHA256Base64(data, secret string) string { + m := hmac.New(sha256.New, []byte(secret)) + _, _ = m.Write([]byte(data)) + return base64.StdEncoding.EncodeToString(m.Sum(nil)) +} + +// EncryptAESGCMBase64 使用 AES-GCM 算法加密数据 +func EncryptAESGCMBase64(plainText, base64Key string) (string, error) { + key, err := base64.StdEncoding.DecodeString(base64Key) + if err != nil { + return "", err + } + block, err := aes.NewCipher(key) + if err != nil { + return "", err + } + gcm, err := cipher.NewGCM(block) + if err != nil { + return "", err + } + + iv := make([]byte, 12) + if _, err = io.ReadFull(rand.Reader, iv); err != nil { + return "", err + } + ciphertext := gcm.Seal(nil, iv, []byte(plainText), nil) + out := append(iv, ciphertext...) + return base64.StdEncoding.EncodeToString(out), nil +} + +// DecryptAESGCMBase64 使用 AES-GCM 算法解密数据 +func DecryptAESGCMBase64(encryptedBase64, base64Key string) (string, error) { + key, err := base64.StdEncoding.DecodeString(base64Key) + if err != nil { + return "", err + } + raw, err := base64.StdEncoding.DecodeString(encryptedBase64) + if err != nil { + return "", err + } + if len(raw) < 13 { + return "", errors.New("密文长度非法") + } + + iv := raw[:12] + ciphertext := raw[12:] + block, err := aes.NewCipher(key) + if err != nil { + return "", err + } + gcm, err := cipher.NewGCM(block) + if err != nil { + return "", err + } + plain, err := gcm.Open(nil, iv, ciphertext, nil) + if err != nil { + return "", err + } + return string(plain), nil +} diff --git a/internal/infrastructure/external/huibo/curl_helper.go b/internal/infrastructure/external/huibo/curl_helper.go new file mode 100644 index 0000000..f7ca797 --- /dev/null +++ b/internal/infrastructure/external/huibo/curl_helper.go @@ -0,0 +1,59 @@ +package huibo + +import ( + "net/http" + "strings" +) + +// generateCurlCommand 从 HTTP 请求生成 curl 命令 +func generateCurlCommand(req *http.Request) string { + var cmd strings.Builder + cmd.WriteString("curl -X ") + cmd.WriteString(req.Method) + cmd.WriteString(" '") + cmd.WriteString(req.URL.String()) + cmd.WriteString("'") + + // 添加请求头 + for key, values := range req.Header { + for _, value := range values { + cmd.WriteString(" \\\n -H '") + cmd.WriteString(key) + cmd.WriteString(": ") + cmd.WriteString(value) + cmd.WriteString("'") + } + } + + return cmd.String() +} + +// generateCurlCommandWithBody 生成包含请求体的 curl 命令 +func generateCurlCommandWithBody(method, url string, headers map[string]string, body string) string { + var cmd strings.Builder + cmd.WriteString("curl -X ") + cmd.WriteString(method) + cmd.WriteString(" '") + cmd.WriteString(url) + cmd.WriteString("'") + + // 添加请求头 + for key, value := range headers { + cmd.WriteString(" \\\n -H '") + cmd.WriteString(key) + cmd.WriteString(": ") + cmd.WriteString(value) + cmd.WriteString("'") + } + + // 添加请求体 + if body != "" { + cmd.WriteString(" \\\n -d '") + cmd.WriteString(body) + cmd.WriteString("'") + } + + cmd.WriteString(" \\\n --compressed") + + return cmd.String() +} \ No newline at end of file diff --git a/internal/infrastructure/external/huibo/huibo_factory.go b/internal/infrastructure/external/huibo/huibo_factory.go index 4a65df3..f5e17b7 100644 --- a/internal/infrastructure/external/huibo/huibo_factory.go +++ b/internal/infrastructure/external/huibo/huibo_factory.go @@ -39,6 +39,8 @@ func NewHuiboServiceWithConfig(cfg *config.Config) (*HuiboService, error) { AESKey: cfg.Huibo.AESKey, WorkOrderCode: cfg.Huibo.WorkOrderCode, ProductCode: cfg.Huibo.ProductCode, + BaseURL2: cfg.Huibo.BaseURL2, + AppCode2: cfg.Huibo.AppCode2, }, logger) return service, nil diff --git a/internal/infrastructure/external/huibo/huibo_service.go b/internal/infrastructure/external/huibo/huibo_service.go index a9773e5..f0b93e6 100644 --- a/internal/infrastructure/external/huibo/huibo_service.go +++ b/internal/infrastructure/external/huibo/huibo_service.go @@ -6,6 +6,7 @@ import ( "crypto/aes" "crypto/cipher" "crypto/hmac" + "crypto/md5" "crypto/rand" "crypto/sha256" "encoding/base64" @@ -85,6 +86,8 @@ type HuiboConfig struct { AESKey string WorkOrderCode string ProductCode string + BaseURL2 string // CallAPI2 使用的 URL + AppCode2 string // CallAPI2 使用的 AppCode } type HuiboService struct { @@ -101,10 +104,22 @@ type responseWrapper struct { } `json:"data"` } +// CallAPI2Response CallAPI2 的响应结构体 +type CallAPI2Response struct { + Code string `json:"code"` + Data map[string]interface{} `json:"data"` + Msg string `json:"msg"` +} + func NewHuiboService(config HuiboConfig, logger *external_logger.ExternalServiceLogger) *HuiboService { return &HuiboService{config: config, logger: logger} } +// GetConfig 获取汇博配置 +func (s *HuiboService) GetConfig() HuiboConfig { + return s.config +} + // CallEducationBackgroundDetailed 教育背景(详细)查询 func (s *HuiboService) CallEducationBackgroundDetailed(ctx context.Context, name, idCard, authPDFBase64 string) ([]byte, error) { requestID := s.generateRequestID() @@ -440,3 +455,158 @@ func randomDigits(n int) string { } return string(b) } + +// MD5Encrypt 使用配置的 AppKey 进行 MD5 加密 +func (s *HuiboService) MD5Encrypt(data string) string { + h := md5.New() + h.Write([]byte(data + s.config.AppKey)) + return fmt.Sprintf("%x", h.Sum(nil)) +} + +// CallAPI2 通用 HTTP 调用方法,返回原始响应 JSON +func (s *HuiboService) CallAPI2(ctx context.Context, pcode string, requestData map[string]interface{}) ([]byte, error) { + startTime := time.Now() + transactionID := "" + if v, ok := ctx.Value("transaction_id").(string); ok { + transactionID = v + } + + if s.logger != nil { + s.logger.LogRequest("", transactionID, "huibo_callapi2", s.config.BaseURL2) + } + + if strings.TrimSpace(s.config.BaseURL2) == "" { + return nil, errors.Join(ErrSystem, errors.New("汇博配置不完整:BaseURL2为空")) + } + + if strings.TrimSpace(s.config.AppCode2) == "" { + return nil, errors.Join(ErrSystem, errors.New("汇博配置不完整:AppCode2为空")) + } + + reqJSON, err := json.Marshal(requestData) + if err != nil { + return nil, errors.Join(ErrSystem, fmt.Errorf("请求参数序列化失败: %w", err)) + } + + // 构建 curl 命令的 headers + headers := map[string]string{ + "AppCode": s.config.AppCode2, + "pcode": pcode, + "Content-Type": "application/json", + "X-ORDER-CODE": s.config.XOrderCode, + } + + // 生成包含请求体的 curl 命令用于日志记录 + curlCmd := generateCurlCommandWithBody("POST", s.config.BaseURL2, headers, string(reqJSON)) + + // 创建 HTTP 请求 + req, err := http.NewRequestWithContext(ctx, http.MethodPost, s.config.BaseURL2, bytes.NewBuffer(reqJSON)) + if err != nil { + return nil, errors.Join(ErrSystem, fmt.Errorf("创建HTTP请求失败: %w", err)) + } + + // req.Header.Set(headerAuthorization, s.config.AppID+"::"+s.config.AppKey) + // req.Header.Set(headerWorkOrderCode, s.config.WorkOrderCode) + // req.Header.Set(headerOrderCode, s.config.XOrderCode) + // req.Header.Set(headerSecretIDHdr, s.config.SecretID) + // req.Header.Set(headerAESKeyHdr, s.config.AESKey) + // req.Header.Set("Content-Type", writer.FormDataContentType()) + + // 设置请求头 + req.Header.Set("AppCode", s.config.AppCode2) + req.Header.Set("pcode", pcode) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-ORDER-CODE", s.config.XOrderCode) + + client := &http.Client{Timeout: 60 * time.Second} + resp, err := client.Do(req) + if err != nil { + if s.logger != nil { + s.logger.LogErrorWithFields("汇博 CallAPI2 HTTP 请求失败", + zap.String("url", s.config.BaseURL2), + zap.String("pcode", pcode), + zap.String("curl", curlCmd), + zap.Error(err), + ) + } + return nil, errors.Join(ErrDatasource, err) + } + defer resp.Body.Close() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + if s.logger != nil { + s.logger.LogErrorWithFields("汇博 CallAPI2 读取响应体失败", + zap.String("url", s.config.BaseURL2), + zap.Int("http_status", resp.StatusCode), + zap.Error(err), + ) + } + return nil, errors.Join(ErrSystem, fmt.Errorf("读取响应体失败: %w", err)) + } + + // 解析响应以检查业务状态码 + var response CallAPI2Response + if err := json.Unmarshal(respBody, &response); err != nil { + if s.logger != nil { + s.logger.LogErrorWithFields("汇博 CallAPI2 响应解析失败", + zap.String("url", s.config.BaseURL2), + zap.String("pcode", pcode), + zap.Error(err), + ) + } + return nil, errors.Join(ErrDatasource, fmt.Errorf("响应解析失败: %w", err)) + } + + // 根据业务状态码进行处理 + switch response.Code { + case CallAPI2StatusSuccess: + // 查询成功 + if s.logger != nil { + s.logger.LogInfo( + "汇博 CallAPI2 查询成功", + zap.String("pcode", pcode), + zap.String("code", response.Code), + zap.String("transaction_id", transactionID), + ) + } + case CallAPI2StatusNoData: + // 查询成功,无数据 + if s.logger != nil { + s.logger.LogInfo( + "汇博 CallAPI2 查询成功但无数据", + zap.String("pcode", pcode), + zap.String("code", response.Code), + zap.String("transaction_id", transactionID), + ) + } + default: + // 其他错误状态码 + message := GetCallAPI2StatusMessage(response.Code) + if s.logger != nil { + s.logger.LogErrorWithFields("汇博 CallAPI2 业务状态异常", + zap.String("url", s.config.BaseURL2), + zap.String("pcode", pcode), + zap.String("code", response.Code), + zap.String("message", message), + ) + } + return nil, errors.Join(ErrDatasource, fmt.Errorf("业务状态异常(code=%s,msg=%s)", response.Code, message)) + } + + // 记录 curl 命令和响应 + if s.logger != nil { + s.logger.LogInfo( + "汇博 CallAPI2 请求响应", + zap.String("curl", curlCmd), + zap.String("response_body", string(respBody)), + zap.String("transaction_id", transactionID), + ) + } + + if s.logger != nil { + s.logger.LogResponse("", transactionID, "huibo_callapi2", http.StatusOK, time.Since(startTime)) + } + + return respBody, nil +} diff --git a/internal/infrastructure/external/huibo/status_codes.go b/internal/infrastructure/external/huibo/status_codes.go new file mode 100644 index 0000000..5a59915 --- /dev/null +++ b/internal/infrastructure/external/huibo/status_codes.go @@ -0,0 +1,52 @@ +package huibo + +// CallAPI2 状态码常量 +const ( + CallAPI2StatusSuccess = "100" // 查询成功 + CallAPI2StatusNoData = "110" // 查询成功,无数据 + CallAPI2StatusParamError = "101" // 参数错误 + CallAPI2StatusAccountError = "103" // 账户不存在 + CallAPI2StatusIPError = "104" // IP 限制 + CallAPI2StatusExpired = "105" // 账号已过期 + CallAPI2StatusServiceError = "107" // 服务不存在 + CallAPI2StatusChannelError = "108" // 产品通道已关闭 + CallAPI2StatusBalanceError = "109" // 账户资金不足 + CallAPI2StatusUnknownError = "500" // 未知请求错误 +) + +// CallAPI2 状态码对应的错误信息 +var CallAPI2StatusMessages = map[string]string{ + CallAPI2StatusSuccess: "查询成功", + CallAPI2StatusNoData: "查询成功,无数据", + CallAPI2StatusParamError: "参数错误", + CallAPI2StatusAccountError: "账户不存在", + CallAPI2StatusIPError: "IP 限制", + CallAPI2StatusExpired: "账号已过期", + CallAPI2StatusServiceError: "服务不存在", + CallAPI2StatusChannelError: "产品通道已关闭", + CallAPI2StatusBalanceError: "账户资金不足", + CallAPI2StatusUnknownError: "未知请求错误", +} + +// IsCallAPI2Success 判断 CallAPI2 状态码是否为成功(需要扣费) +func IsCallAPI2Success(code string) bool { + return code == CallAPI2StatusSuccess +} + +// IsCallAPI2NoData 判断 CallAPI2 状态码是否为无数据(需要扣费) +func IsCallAPI2NoData(code string) bool { + return code == CallAPI2StatusNoData +} + +// IsCallAPI2Billable 判断 CallAPI2 状态码是否需要扣费 +func IsCallAPI2Billable(code string) bool { + return IsCallAPI2Success(code) || IsCallAPI2NoData(code) +} + +// GetCallAPI2StatusMessage 获取 CallAPI2 状态码对应的错误信息 +func GetCallAPI2StatusMessage(code string) string { + if msg, ok := CallAPI2StatusMessages[code]; ok { + return msg + } + return "未知状态码: " + code +} From 4fe180e4c83e71593c8c26d62f3df5113a383be2 Mon Sep 17 00:00:00 2001 From: Mrx <18278715334@163.com> Date: Tue, 26 May 2026 16:21:55 +0800 Subject: [PATCH 18/28] f --- .../domains/api/services/processors/qygl/qyglbh7y_processor.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/domains/api/services/processors/qygl/qyglbh7y_processor.go b/internal/domains/api/services/processors/qygl/qyglbh7y_processor.go index 18c6d13..a87bb1e 100644 --- a/internal/domains/api/services/processors/qygl/qyglbh7y_processor.go +++ b/internal/domains/api/services/processors/qygl/qyglbh7y_processor.go @@ -22,7 +22,7 @@ func ProcessQYGLBH7YRequest(ctx context.Context, params []byte, deps *processors } reqdata := map[string]interface{}{ - "name": paramsDto.EntName, + "companyName": paramsDto.EntName, } respBytes, err := deps.HuiboService.CallAPI2(ctx, "E_004_0261", reqdata) From 28fc40525ee1177b9b72a3e91b045bb9e27b8246 Mon Sep 17 00:00:00 2001 From: Mrx <18278715334@163.com> Date: Tue, 26 May 2026 16:25:11 +0800 Subject: [PATCH 19/28] f --- .../domains/api/services/processors/qygl/qyglbh7y_processor.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/domains/api/services/processors/qygl/qyglbh7y_processor.go b/internal/domains/api/services/processors/qygl/qyglbh7y_processor.go index a87bb1e..8f4c41f 100644 --- a/internal/domains/api/services/processors/qygl/qyglbh7y_processor.go +++ b/internal/domains/api/services/processors/qygl/qyglbh7y_processor.go @@ -10,7 +10,7 @@ import ( "tyapi-server/internal/infrastructure/external/huibo" ) -// ProcessQYGLBH7YRequest QYGLBH7Y API处理方法 - 个人涉诉案件查询汇博 +// ProcessQYGLBH7YRequest QYGLBH7Y API处理方法 - 企业案件查询汇博 func ProcessQYGLBH7YRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { var paramsDto dto.QYGLBH7YReq if err := json.Unmarshal(params, ¶msDto); err != nil { From 2a3336963908f0b07792281e124c631fee5a8726 Mon Sep 17 00:00:00 2001 From: Mrx <18278715334@163.com> Date: Tue, 26 May 2026 18:09:49 +0800 Subject: [PATCH 20/28] f --- .../processors/flxg/flxghb4f_processor.go | 8 +- internal/infrastructure/external/huibo/2.md | 90 ---- .../external/huibo/crypto_test.go | 432 ++++++++++++++++++ 3 files changed, 436 insertions(+), 94 deletions(-) delete mode 100644 internal/infrastructure/external/huibo/2.md create mode 100644 internal/infrastructure/external/huibo/crypto_test.go diff --git a/internal/domains/api/services/processors/flxg/flxghb4f_processor.go b/internal/domains/api/services/processors/flxg/flxghb4f_processor.go index 307d36b..0518e00 100644 --- a/internal/domains/api/services/processors/flxg/flxghb4f_processor.go +++ b/internal/domains/api/services/processors/flxg/flxghb4f_processor.go @@ -22,12 +22,12 @@ func ProcessFLXGHB4FRequest(ctx context.Context, params []byte, deps *processors } // 使用 MD5 加密 name 和 idCard - encryptedName := "MD5:" + huibo.MD5Encrypt(paramsDto.Name, deps.HuiboService.GetConfig().AppKey) - encryptedIDCard := "MD5:" + huibo.MD5Encrypt(paramsDto.IDCard, deps.HuiboService.GetConfig().AppKey) + // encryptedName := "MD5:" + huibo.MD5Encrypt(paramsDto.Name, deps.HuiboService.GetConfig().AppKey) + // encryptedIDCard := "MD5:" + huibo.MD5Encrypt(paramsDto.IDCard, deps.HuiboService.GetConfig().AppKey) reqdata := map[string]interface{}{ - "name": encryptedName, - "idCard": encryptedIDCard, + "name": paramsDto.Name, + "idCard": paramsDto.IDCard, } respBytes, err := deps.HuiboService.CallAPI2(ctx, "P_004_0271", reqdata) diff --git a/internal/infrastructure/external/huibo/2.md b/internal/infrastructure/external/huibo/2.md deleted file mode 100644 index 507e5fd..0000000 --- a/internal/infrastructure/external/huibo/2.md +++ /dev/null @@ -1,90 +0,0 @@ -自然人公开涉诉信息查询接口文档 -接口编码:BHSC-P_004_0271 -版本:V1.0 -实施日期:2026-04-13 -适用场景:合作厂家接入中胜信用平台,用于个人涉诉案件查询(含失信、限高) -1. 通信说明 -请求方式:HTTP POST -数据格式:JSON -编码格式:UTF-8 -安全要求:请求 IP 需提前绑定 -2. 请求信息 -请求地址 -http://host:port/api/data -请求头(Header) -表格 -参数名 含义 必填 类型 备注 -AppCode 接口授权码 Y String 接口服务商提供 -pcode 产品编码 Y String 固定值:P_004_0271 -请求体(Body) -表格 -参数名 含义 必填 类型 说明 -name 姓名 Y String 支持明文 / MD5 加密 -idCard 身份证号 Y String 支持明文 / MD5 加密 -请求示例(明文) -json -{ - "idCard": "2103111*****0", - "name": "*****" -} -请求示例(加密) -json -{ - "name": "MD5:3e29aae20b7b92775*****", - "idCard": "MD5:a0c28f3a5a*****14" -} -3. 响应信息 -表格 -字段名 含义 类型 备注 -code 状态码 String 参考状态码说明 -message 描述信息 String - -orderNo 订单号 String - -pcode 产品编码 String 与请求一致 -param 请求参数 Object 原样返回 -charge 是否收费 Boolean true = 收费;false = 不收费 -time 响应时间戳 String 13 位毫秒 -data 业务数据 Object 涉诉 / 失信 / 限高数据 -响应示例 -json -{ - "code": "100", - "orderNo": "1361269246899077120", - "charge": true, - "data": { - "ss": { - "preservation": { "count": {} }, - "crc": 35****4186, - "cases_tree": {}, - "administrative": {}, - "civil": {}, - "count": {}, - "implement": {}, - "criminal": {}, - "bankrupt": {} - }, - "sxbzxr": [{}], - "xgbzxr": [] - }, - "pcode": "P_004_0271", - "param": null, - "time": "1744593480538", - "message": "查询成功" -} -data 节点说明 -ss:涉诉案件(民事 / 刑事 / 执行 / 行政 / 破产等) -sxbzxr:失信被执行人 -xgbzxr:限制高消费 -4. 状态码说明 -表格 -状态码 描述 -100 查询成功 -101 参数错误 -103 账户不存在 -104 IP 限制 -105 账号已过期 -107 服务不存在 -108 产品通道已关闭 -109 账户资金不足 -110 查询成功,无数据 -500 未知请求错误 -要不要我帮你把这份接口直接写成可上线的小程序请求代码(含加密、header、异常捕获、状态码统一处理)? \ No newline at end of file diff --git a/internal/infrastructure/external/huibo/crypto_test.go b/internal/infrastructure/external/huibo/crypto_test.go new file mode 100644 index 0000000..6658db7 --- /dev/null +++ b/internal/infrastructure/external/huibo/crypto_test.go @@ -0,0 +1,432 @@ +package huibo + +import ( + "encoding/base64" + "testing" +) + +// 测试 MD5 加密(固定密文对比验证) +func TestMD5Encrypt(t *testing.T) { + appKey := "a6c9935e967894e731c62ecfcd9b7c95" + + testCases := []struct { + name string + data string + expected string // 固定密文 + }{ + { + name: "姓名", + data: "何志勇", + expected: "64d4d5c6457026117a4911acf189e269", // 固定密文 + }, + { + name: "身份证号", + data: "452528197907133014", + expected: "7c6cc77dabb83d95948904dba5a7219d", // 固定密文 + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // 加密 + result := MD5Encrypt(tc.data, appKey) + + // 最核心:对比是否和固定密文一致 + if result != tc.expected { + t.Errorf("加密不匹配!\n明文:%s\n期望密文:%s\n实际密文:%s", + tc.data, tc.expected, result) + return + } + + // 打印成功日志 + t.Logf("✅ 校验成功\n明文:%s\n密文:%s", tc.data, result) + }) + } +} + +// 测试 HMAC-SHA256 签名 +func TestHMACSHA256Base64(t *testing.T) { + secret := "test_secret_key" + + testCases := []struct { + name string + data string + secret string + }{ + { + name: "简单字符串", + data: "hello world", + secret: secret, + }, + { + name: "JSON数据", + data: `{"name":"张三","idCard":"110101199003072345"}`, + secret: secret, + }, + { + name: "URL参数", + data: "idCard=110101199003072345&name=张三&productCode=22089", + secret: secret, + }, + { + name: "空字符串", + data: "", + secret: secret, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := HMACSHA256Base64(tc.data, tc.secret) + + // 验证结果不为空 + if result == "" { + t.Error("HMAC-SHA256签名结果为空") + } + + // 验证结果是有效的Base64 + _, err := base64.StdEncoding.DecodeString(result) + if err != nil { + t.Errorf("HMAC-SHA256结果不是有效的Base64: %v", err) + } + + // 验证相同输入产生相同输出 + result2 := HMACSHA256Base64(tc.data, tc.secret) + if result != result2 { + t.Error("相同输入产生的签名不一致") + } + + // 验证不同输入产生不同输出 + result3 := HMACSHA256Base64(tc.data+"x", tc.secret) + if result == result3 { + t.Error("不同输入产生的签名相同") + } + + t.Logf("数据: %s, 签名: %s", tc.data, result) + }) + } +} + +// 测试 AES-GCM 加密解密 +func TestEncryptDecryptAESGCMBase64(t *testing.T) { + // 生成一个有效的 Base64 编密的 AES 密钥 + key := make([]byte, 32) // AES-256 + for i := range key { + key[i] = byte(i) + } + base64Key := base64.StdEncoding.EncodeToString(key) + + testCases := []struct { + name string + data string + }{ + { + name: "简单文本", + data: "hello world", + }, + { + name: "中文文本", + data: "你好世界", + }, + { + name: "JSON数据", + data: `{"name":"张三","idCard":"110101199003072345"}`, + }, + { + name: "长文本", + data: "这是一个很长的文本,用来测试加密解密功能是否正常工作。包含各种字符:123456789!@#$%^&*()_+-=[]{}|;':\",./<>?", + }, + { + name: "空字符串", + data: "", + }, + { + name: "特殊字符", + data: "\n\t\r\x00", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // 加密 + encrypted, err := EncryptAESGCMBase64(tc.data, base64Key) + if err != nil { + t.Fatalf("加密失败: %v", err) + } + + // 验证加密结果不为空 + if encrypted == "" { + t.Error("加密结果为空") + } + + // 验证加密结果是有效的Base64 + _, err = base64.StdEncoding.DecodeString(encrypted) + if err != nil { + t.Errorf("加密结果不是有效的Base64: %v", err) + } + + // 验证加密结果与原文不同 + if encrypted == tc.data { + t.Error("加密结果与原文相同") + } + + // 解密 + decrypted, err := DecryptAESGCMBase64(encrypted, base64Key) + if err != nil { + t.Fatalf("解密失败: %v", err) + } + + // 验证解密结果与原文一致 + if decrypted != tc.data { + t.Errorf("解密结果不匹配,期望: %s, 实际: %s", tc.data, decrypted) + } + + t.Logf("原文: %s", tc.data) + t.Logf("密文: %s", encrypted) + t.Logf("解密: %s", decrypted) + }) + } +} + +// 测试错误的密钥 +func TestEncryptDecryptWithWrongKey(t *testing.T) { + // 生成两个不同的密钥 + key1 := make([]byte, 32) + for i := range key1 { + key1[i] = byte(i) + } + base64Key1 := base64.StdEncoding.EncodeToString(key1) + + key2 := make([]byte, 32) + for i := range key2 { + key2[i] = byte(i + 1) + } + base64Key2 := base64.StdEncoding.EncodeToString(key2) + + data := "sensitive data" + + // 用密钥1加密 + encrypted, err := EncryptAESGCMBase64(data, base64Key1) + if err != nil { + t.Fatalf("加密失败: %v", err) + } + + // 用密钥2解密 + decrypted, err := DecryptAESGCMBase64(encrypted, base64Key2) + if err == nil { + t.Error("用错误密钥解密应该返回错误") + } + + // 验证解密结果与原文不同(如果解密成功的话) + if decrypted == data { + t.Error("用错误密钥解密不应该得到正确结果") + } + + t.Logf("用错误密钥解密预期失败: %v", err) +} + +// 测试无效的 Base64 密钥 +func TestEncryptWithInvalidBase64Key(t *testing.T) { + data := "test data" + + invalidKeys := []string{ + "", // 空字符串 + "not_base64", // 非Base64 + "abc", // 解码后太短 + } + + for _, invalidKey := range invalidKeys { + _, err := EncryptAESGCMBase64(data, invalidKey) + if err == nil { + t.Errorf("使用无效密钥 %s 应该返回错误", invalidKey) + } + t.Logf("无效密钥 %s 预期失败: %v", invalidKey, err) + } +} + +// 测试解密无效数据 +func TestDecryptWithInvalidData(t *testing.T) { + // 生成一个有效的密钥 + key := make([]byte, 32) + for i := range key { + key[i] = byte(i) + } + base64Key := base64.StdEncoding.EncodeToString(key) + + invalidData := []string{ + "", // 空字符串 + "invalid_base64", // 非Base64 + "dGVzdA==", // 有效的Base64但不是AES-GCM数据 + "short", // 太短 + } + + for _, data := range invalidData { + _, err := DecryptAESGCMBase64(data, base64Key) + if err == nil { + t.Errorf("使用无效数据 %s 应该返回错误", data) + } + t.Logf("无效数据 %s 预期失败: %v", data, err) + } +} + +// 测试加密结果的唯一性 +func TestEncryptionUniqueness(t *testing.T) { + // 生成一个有效的密钥 + key := make([]byte, 32) + for i := range key { + key[i] = byte(i) + } + base64Key := base64.StdEncoding.EncodeToString(key) + + data := "same data" + + // 加密多次 + results := make([]string, 10) + for i := 0; i < 10; i++ { + encrypted, err := EncryptAESGCMBase64(data, base64Key) + if err != nil { + t.Fatalf("第%d次加密失败: %v", i, err) + } + results[i] = encrypted + } + + // 验证每次加密结果都不同(因为包含随机IV) + uniqueCount := 0 + for i := 0; i < len(results); i++ { + isUnique := true + for j := 0; j < len(results); j++ { + if i != j && results[i] == results[j] { + isUnique = false + break + } + } + if isUnique { + uniqueCount++ + } + } + + if uniqueCount != len(results) { + t.Errorf("加密结果应该唯一,实际上只有%d个唯一结果,期望%d个", uniqueCount, len(results)) + } + + t.Logf("生成了%d个不同的加密结果", uniqueCount) +} + +// 测试使用真实配置的加密解密 +func TestEncryptionWithRealConfig(t *testing.T) { + // 使用配置文件中的真实AES密钥 + aesKey := "NQYN3YO+pb/GEcCBNX0ptMb7cUlnXSPvcX7VvNofBkc=" + appKey := "a6c9935e967894e731c62ecfcd9b7c95" + + // 测试数据 + testData := `{"name":"张三","idCard":"110101199003072345","productCode":"22089"}` + + t.Run("MD5加密", func(t *testing.T) { + // 测试 MD5 加密 + md5Result := MD5Encrypt("张三", appKey) + t.Logf("姓名 MD5: %s", md5Result) + + md5Result2 := MD5Encrypt("110101199003072345", appKey) + t.Logf("身份证号 MD5: %s", md5Result2) + + // 验证格式 + if len(md5Result) != 32 { + t.Errorf("MD5结果长度错误,期望32位,实际%d位", len(md5Result)) + } + }) + + t.Run("HMAC-SHA256签名", func(t *testing.T) { + // 生成排序后的参数 + sortedParam := "idCard=110101199003072345&name=张三&productCode=22089" + signature := HMACSHA256Base64(sortedParam, aesKey) + t.Logf("签名参数: %s", sortedParam) + t.Logf("HMAC-SHA256签名: %s", signature) + + // 验证格式 + _, err := base64.StdEncoding.DecodeString(signature) + if err != nil { + t.Errorf("签名不是有效的Base64: %v", err) + } + }) + + t.Run("AES-GCM加密解密", func(t *testing.T) { + // 测试 AES-GCM 加密 + encrypted, err := EncryptAESGCMBase64(testData, aesKey) + if err != nil { + t.Fatalf("加密失败: %v", err) + } + t.Logf("原始数据: %s", testData) + t.Logf("加密结果: %s", encrypted) + + // 测试解密 + decrypted, err := DecryptAESGCMBase64(encrypted, aesKey) + if err != nil { + t.Fatalf("解密失败: %v", err) + } + t.Logf("解密结果: %s", decrypted) + + // 验证结果 + if decrypted != testData { + t.Errorf("解密结果不匹配") + } + }) +} + +// 基准测试 +func BenchmarkMD5Encrypt(b *testing.B) { + data := "张三" + appKey := "a6c9935e967894e731c62ecfcd9b7c95" + + b.ResetTimer() + for i := 0; i < b.N; i++ { + MD5Encrypt(data, appKey) + } +} + +func BenchmarkHMACSHA256Base64(b *testing.B) { + data := "idCard=110101199003072345&name=张三" + secret := "a6c9935e967894e731c62ecfcd9b7c95" + + b.ResetTimer() + for i := 0; i < b.N; i++ { + HMACSHA256Base64(data, secret) + } +} + +func BenchmarkEncryptAESGCMBase64(b *testing.B) { + data := `{"name":"张三","idCard":"110101199003072345","productCode":"22089"}` + + // 生成密钥 + key := make([]byte, 32) + for i := range key { + key[i] = byte(i) + } + base64Key := base64.StdEncoding.EncodeToString(key) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + EncryptAESGCMBase64(data, base64Key) + } +} + +func BenchmarkDecryptAESGCMBase64(b *testing.B) { + data := `{"name":"张三","idCard":"110101199003072345","productCode":"22089"}` + + // 生成密钥 + key := make([]byte, 32) + for i := range key { + key[i] = byte(i) + } + base64Key := base64.StdEncoding.EncodeToString(key) + + // 先加密一次 + encrypted, err := EncryptAESGCMBase64(data, base64Key) + if err != nil { + b.Fatalf("预加密失败: %v", err) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + DecryptAESGCMBase64(encrypted, base64Key) + } +} From b7fb2a73c9e1c0a0b090e36467947827a729b946 Mon Sep 17 00:00:00 2001 From: Mrx <18278715334@163.com> Date: Wed, 27 May 2026 12:11:26 +0800 Subject: [PATCH 21/28] f --- .../api/gorm_api_call_repository.go | 44 +++++++++ internal/infrastructure/external/huibo/2.md | 90 +++++++++++++++++++ .../http/handlers/api_handler.go | 10 +++ 3 files changed, 144 insertions(+) create mode 100644 internal/infrastructure/external/huibo/2.md diff --git a/internal/infrastructure/database/repositories/api/gorm_api_call_repository.go b/internal/infrastructure/database/repositories/api/gorm_api_call_repository.go index 7ec6489..a7a5496 100644 --- a/internal/infrastructure/database/repositories/api/gorm_api_call_repository.go +++ b/internal/infrastructure/database/repositories/api/gorm_api_call_repository.go @@ -149,6 +149,28 @@ func (r *GormApiCallRepository) ListByUserIdWithFiltersAndProductName(ctx contex // 应用筛选条件 if filters != nil { + // 产品ID筛选(支持多个) + if productIds, ok := filters["product_ids"].(string); ok && productIds != "" { + // 多个产品ID,逗号分隔 + productIdsList := strings.Split(productIds, ",") + // 去除空白字符 + var cleanProductIds []string + for _, id := range productIdsList { + id = strings.TrimSpace(id) + if id != "" { + cleanProductIds = append(cleanProductIds, id) + } + } + if len(cleanProductIds) > 0 { + placeholders := strings.Repeat("?,", len(cleanProductIds)) + placeholders = placeholders[:len(placeholders)-1] // 移除最后一个逗号 + whereCondition += " AND ac.product_id IN (" + placeholders + ")" + for _, id := range cleanProductIds { + whereArgs = append(whereArgs, id) + } + } + } + // 时间范围筛选 if startTime, ok := filters["start_time"].(time.Time); ok { whereCondition += " AND ac.created_at >= ?" @@ -337,6 +359,28 @@ func (r *GormApiCallRepository) ListWithFiltersAndProductName(ctx context.Contex whereArgs = append(whereArgs, userId) } + // 产品ID筛选(支持多个) + if productIds, ok := filters["product_ids"].(string); ok && productIds != "" { + // 多个产品ID,逗号分隔 + productIdsList := strings.Split(productIds, ",") + // 去除空白字符 + var cleanProductIds []string + for _, id := range productIdsList { + id = strings.TrimSpace(id) + if id != "" { + cleanProductIds = append(cleanProductIds, id) + } + } + if len(cleanProductIds) > 0 { + placeholders := strings.Repeat("?,", len(cleanProductIds)) + placeholders = placeholders[:len(placeholders)-1] // 移除最后一个逗号 + whereCondition += " AND ac.product_id IN (" + placeholders + ")" + for _, id := range cleanProductIds { + whereArgs = append(whereArgs, id) + } + } + } + // 时间范围筛选 if startTime, ok := filters["start_time"].(time.Time); ok { whereCondition += " AND ac.created_at >= ?" diff --git a/internal/infrastructure/external/huibo/2.md b/internal/infrastructure/external/huibo/2.md new file mode 100644 index 0000000..507e5fd --- /dev/null +++ b/internal/infrastructure/external/huibo/2.md @@ -0,0 +1,90 @@ +自然人公开涉诉信息查询接口文档 +接口编码:BHSC-P_004_0271 +版本:V1.0 +实施日期:2026-04-13 +适用场景:合作厂家接入中胜信用平台,用于个人涉诉案件查询(含失信、限高) +1. 通信说明 +请求方式:HTTP POST +数据格式:JSON +编码格式:UTF-8 +安全要求:请求 IP 需提前绑定 +2. 请求信息 +请求地址 +http://host:port/api/data +请求头(Header) +表格 +参数名 含义 必填 类型 备注 +AppCode 接口授权码 Y String 接口服务商提供 +pcode 产品编码 Y String 固定值:P_004_0271 +请求体(Body) +表格 +参数名 含义 必填 类型 说明 +name 姓名 Y String 支持明文 / MD5 加密 +idCard 身份证号 Y String 支持明文 / MD5 加密 +请求示例(明文) +json +{ + "idCard": "2103111*****0", + "name": "*****" +} +请求示例(加密) +json +{ + "name": "MD5:3e29aae20b7b92775*****", + "idCard": "MD5:a0c28f3a5a*****14" +} +3. 响应信息 +表格 +字段名 含义 类型 备注 +code 状态码 String 参考状态码说明 +message 描述信息 String - +orderNo 订单号 String - +pcode 产品编码 String 与请求一致 +param 请求参数 Object 原样返回 +charge 是否收费 Boolean true = 收费;false = 不收费 +time 响应时间戳 String 13 位毫秒 +data 业务数据 Object 涉诉 / 失信 / 限高数据 +响应示例 +json +{ + "code": "100", + "orderNo": "1361269246899077120", + "charge": true, + "data": { + "ss": { + "preservation": { "count": {} }, + "crc": 35****4186, + "cases_tree": {}, + "administrative": {}, + "civil": {}, + "count": {}, + "implement": {}, + "criminal": {}, + "bankrupt": {} + }, + "sxbzxr": [{}], + "xgbzxr": [] + }, + "pcode": "P_004_0271", + "param": null, + "time": "1744593480538", + "message": "查询成功" +} +data 节点说明 +ss:涉诉案件(民事 / 刑事 / 执行 / 行政 / 破产等) +sxbzxr:失信被执行人 +xgbzxr:限制高消费 +4. 状态码说明 +表格 +状态码 描述 +100 查询成功 +101 参数错误 +103 账户不存在 +104 IP 限制 +105 账号已过期 +107 服务不存在 +108 产品通道已关闭 +109 账户资金不足 +110 查询成功,无数据 +500 未知请求错误 +要不要我帮你把这份接口直接写成可上线的小程序请求代码(含加密、header、异常捕获、状态码统一处理)? \ No newline at end of file diff --git a/internal/infrastructure/http/handlers/api_handler.go b/internal/infrastructure/http/handlers/api_handler.go index 9a9bac2..565d900 100644 --- a/internal/infrastructure/http/handlers/api_handler.go +++ b/internal/infrastructure/http/handlers/api_handler.go @@ -426,6 +426,16 @@ func (h *ApiHandler) GetAdminApiCalls(c *gin.Context) { filters["user_id"] = userId } + // 用户ID列表筛选 + if userIds := c.Query("user_ids"); userIds != "" { + filters["user_ids"] = userIds + } + + // 产品ID列表筛选 + if productIds := c.Query("product_ids"); productIds != "" { + filters["product_ids"] = productIds + } + // 时间范围筛选 if startTime := c.Query("start_time"); startTime != "" { if t, err := time.Parse("2006-01-02 15:04:05", startTime); err == nil { From 5cee8ff0352016732e322c9f861d1f1d509f686b Mon Sep 17 00:00:00 2001 From: Mrx <18278715334@163.com> Date: Wed, 27 May 2026 13:00:51 +0800 Subject: [PATCH 22/28] f --- .../product/product_application_service.go | 3 + .../product_application_service_impl.go | 370 +++++++++++------- internal/container/container.go | 2 + internal/domains/api/dto/api_request_dto.go | 18 + .../api/services/api_request_service.go | 4 +- .../api/services/form_config_service.go | 3 + .../processors/qygl/qygl2ysb_processor.go | 47 +++ .../processors/qygl/qygl3ysb_processor.go | 48 +++ .../processors/qygl/qygl4yab_processor.go | 49 +++ .../services/product_management_service.go | 43 ++ .../product/gorm_product_repository.go | 32 +- .../http/handlers/product_admin_handler.go | 51 +++ .../http/routes/product_admin_routes.go | 3 + internal/shared/export/export.go | 33 +- internal/shared/services/export_service.go | 9 +- 15 files changed, 569 insertions(+), 146 deletions(-) create mode 100644 internal/domains/api/services/processors/qygl/qygl2ysb_processor.go create mode 100644 internal/domains/api/services/processors/qygl/qygl3ysb_processor.go create mode 100644 internal/domains/api/services/processors/qygl/qygl4yab_processor.go diff --git a/internal/application/product/product_application_service.go b/internal/application/product/product_application_service.go index 6b3c54d..0682407 100644 --- a/internal/application/product/product_application_service.go +++ b/internal/application/product/product_application_service.go @@ -47,4 +47,7 @@ type ProductApplicationService interface { CreateProductApiConfig(ctx context.Context, productID string, config *responses.ProductApiConfigResponse) error UpdateProductApiConfig(ctx context.Context, configID string, config *responses.ProductApiConfigResponse) error DeleteProductApiConfig(ctx context.Context, configID string) error + + // 产品字典导出 + ExportProductDictionary(ctx context.Context, format string) ([]byte, error) } diff --git a/internal/application/product/product_application_service_impl.go b/internal/application/product/product_application_service_impl.go index 25ae9ec..3d0cb56 100644 --- a/internal/application/product/product_application_service_impl.go +++ b/internal/application/product/product_application_service_impl.go @@ -16,6 +16,7 @@ import ( api_services "tyapi-server/internal/domains/api/services" "tyapi-server/internal/domains/product/entities" product_service "tyapi-server/internal/domains/product/services" + "tyapi-server/internal/shared/export" "tyapi-server/internal/shared/interfaces" ) @@ -28,6 +29,7 @@ type ProductApplicationServiceImpl struct { documentationAppService DocumentationApplicationServiceInterface formConfigService api_services.FormConfigService logger *zap.Logger + exportManager *export.ExportManager } // NewProductApplicationService 创建产品应用服务 @@ -38,6 +40,7 @@ func NewProductApplicationService( documentationAppService DocumentationApplicationServiceInterface, formConfigService api_services.FormConfigService, logger *zap.Logger, + exportManager *export.ExportManager, ) ProductApplicationService { return &ProductApplicationServiceImpl{ productManagementService: productManagementService, @@ -46,6 +49,7 @@ func NewProductApplicationService( documentationAppService: documentationAppService, formConfigService: formConfigService, logger: logger, + exportManager: exportManager, } } @@ -492,24 +496,24 @@ func (s *ProductApplicationServiceImpl) GetProductByIDForUser(ctx context.Contex // convertToProductInfoResponse 转换为产品信息响应 func (s *ProductApplicationServiceImpl) convertToProductInfoResponse(product *entities.Product) *responses.ProductInfoResponse { response := &responses.ProductInfoResponse{ - ID: product.ID, - OldID: product.OldID, - Name: product.Name, - Code: product.Code, - Description: product.Description, - Content: product.Content, - CategoryID: product.CategoryID, - SubCategoryID: product.SubCategoryID, - Price: product.Price.InexactFloat64(), - IsEnabled: product.IsEnabled, - IsPackage: product.IsPackage, - SellUIComponent: product.SellUIComponent, + ID: product.ID, + OldID: product.OldID, + Name: product.Name, + Code: product.Code, + Description: product.Description, + Content: product.Content, + CategoryID: product.CategoryID, + SubCategoryID: product.SubCategoryID, + Price: product.Price.InexactFloat64(), + IsEnabled: product.IsEnabled, + IsPackage: product.IsPackage, + SellUIComponent: product.SellUIComponent, UIComponentPrice: product.UIComponentPrice.InexactFloat64(), - SEOTitle: product.SEOTitle, - SEODescription: product.SEODescription, - SEOKeywords: product.SEOKeywords, - CreatedAt: product.CreatedAt, - UpdatedAt: product.UpdatedAt, + SEOTitle: product.SEOTitle, + SEODescription: product.SEODescription, + SEOKeywords: product.SEOKeywords, + CreatedAt: product.CreatedAt, + UpdatedAt: product.UpdatedAt, } // 添加一级分类信息 @@ -544,27 +548,27 @@ func (s *ProductApplicationServiceImpl) convertToProductInfoResponse(product *en // convertToProductAdminInfoResponse 转换为管理员产品信息响应 func (s *ProductApplicationServiceImpl) convertToProductAdminInfoResponse(product *entities.Product) *responses.ProductAdminInfoResponse { response := &responses.ProductAdminInfoResponse{ - ID: product.ID, - OldID: product.OldID, - Name: product.Name, - Code: product.Code, - Description: product.Description, - Content: product.Content, - CategoryID: product.CategoryID, - SubCategoryID: product.SubCategoryID, - Price: product.Price.InexactFloat64(), - CostPrice: product.CostPrice.InexactFloat64(), - Remark: product.Remark, - IsEnabled: product.IsEnabled, - IsVisible: product.IsVisible, // 管理员可以看到可见状态 - IsPackage: product.IsPackage, - SellUIComponent: product.SellUIComponent, + ID: product.ID, + OldID: product.OldID, + Name: product.Name, + Code: product.Code, + Description: product.Description, + Content: product.Content, + CategoryID: product.CategoryID, + SubCategoryID: product.SubCategoryID, + Price: product.Price.InexactFloat64(), + CostPrice: product.CostPrice.InexactFloat64(), + Remark: product.Remark, + IsEnabled: product.IsEnabled, + IsVisible: product.IsVisible, // 管理员可以看到可见状态 + IsPackage: product.IsPackage, + SellUIComponent: product.SellUIComponent, UIComponentPrice: product.UIComponentPrice.InexactFloat64(), - SEOTitle: product.SEOTitle, - SEODescription: product.SEODescription, - SEOKeywords: product.SEOKeywords, - CreatedAt: product.CreatedAt, - UpdatedAt: product.UpdatedAt, + SEOTitle: product.SEOTitle, + SEODescription: product.SEODescription, + SEOKeywords: product.SEOKeywords, + CreatedAt: product.CreatedAt, + UpdatedAt: product.UpdatedAt, } // 添加一级分类信息 @@ -994,103 +998,103 @@ func (s *ProductApplicationServiceImpl) mergeRequestParamsFromDTOs(ctx context.C // getDTOMap 获取API代码到DTO结构体的映射(复用form_config_service的逻辑) func (s *ProductApplicationServiceImpl) getDTOMap() map[string]interface{} { return map[string]interface{}{ - "IVYZ9363": &dto.IVYZ9363Req{}, - "IVYZ385E": &dto.IVYZ385EReq{}, - "IVYZ5733": &dto.IVYZ5733Req{}, - "FLXG3D56": &dto.FLXG3D56Req{}, - "FLXG75FE": &dto.FLXG75FEReq{}, - "FLXG0V3B": &dto.FLXG0V3BReq{}, - "FLXG0V4B": &dto.FLXG0V4BReq{}, - "FLXG54F5": &dto.FLXG54F5Req{}, - "FLXG162A": &dto.FLXG162AReq{}, - "FLXG0687": &dto.FLXG0687Req{}, - "FLXGBC21": &dto.FLXGBC21Req{}, - "FLXG970F": &dto.FLXG970FReq{}, - "FLXG5876": &dto.FLXG5876Req{}, - "FLXG9687": &dto.FLXG9687Req{}, - "FLXGC9D1": &dto.FLXGC9D1Req{}, - "FLXGCA3D": &dto.FLXGCA3DReq{}, - "FLXGDEC7": &dto.FLXGDEC7Req{}, - "JRZQ0A03": &dto.JRZQ0A03Req{}, - "JRZQ4AA8": &dto.JRZQ4AA8Req{}, - "JRZQ8203": &dto.JRZQ8203Req{}, - "JRZQDCBE": &dto.JRZQDCBEReq{}, - "QYGL2ACD": &dto.QYGL2ACDReq{}, - "QYGL6F2D": &dto.QYGL6F2DReq{}, - "QYGL45BD": &dto.QYGL45BDReq{}, - "QYGL8261": &dto.QYGL8261Req{}, - "QYGL8271": &dto.QYGL8271Req{}, - "QYGLB4C0": &dto.QYGLB4C0Req{}, - "QYGL23T7": &dto.QYGL23T7Req{}, - "QYGL5A3C": &dto.QYGL5A3CReq{}, - "QYGL8B4D": &dto.QYGL8B4DReq{}, - "QYGL9E2F": &dto.QYGL9E2FReq{}, - "QYGL7C1A": &dto.QYGL7C1AReq{}, - "QYGL3F8E": &dto.QYGL3F8EReq{}, - "YYSY4B37": &dto.YYSY4B37Req{}, - "YYSY4B21": &dto.YYSY4B21Req{}, - "YYSY6F2E": &dto.YYSY6F2EReq{}, - "YYSY09CD": &dto.YYSY09CDReq{}, - "IVYZ0B03": &dto.IVYZ0B03Req{}, + "IVYZ9363": &dto.IVYZ9363Req{}, + "IVYZ385E": &dto.IVYZ385EReq{}, + "IVYZ5733": &dto.IVYZ5733Req{}, + "FLXG3D56": &dto.FLXG3D56Req{}, + "FLXG75FE": &dto.FLXG75FEReq{}, + "FLXG0V3B": &dto.FLXG0V3BReq{}, + "FLXG0V4B": &dto.FLXG0V4BReq{}, + "FLXG54F5": &dto.FLXG54F5Req{}, + "FLXG162A": &dto.FLXG162AReq{}, + "FLXG0687": &dto.FLXG0687Req{}, + "FLXGBC21": &dto.FLXGBC21Req{}, + "FLXG970F": &dto.FLXG970FReq{}, + "FLXG5876": &dto.FLXG5876Req{}, + "FLXG9687": &dto.FLXG9687Req{}, + "FLXGC9D1": &dto.FLXGC9D1Req{}, + "FLXGCA3D": &dto.FLXGCA3DReq{}, + "FLXGDEC7": &dto.FLXGDEC7Req{}, + "JRZQ0A03": &dto.JRZQ0A03Req{}, + "JRZQ4AA8": &dto.JRZQ4AA8Req{}, + "JRZQ8203": &dto.JRZQ8203Req{}, + "JRZQDCBE": &dto.JRZQDCBEReq{}, + "QYGL2ACD": &dto.QYGL2ACDReq{}, + "QYGL6F2D": &dto.QYGL6F2DReq{}, + "QYGL45BD": &dto.QYGL45BDReq{}, + "QYGL8261": &dto.QYGL8261Req{}, + "QYGL8271": &dto.QYGL8271Req{}, + "QYGLB4C0": &dto.QYGLB4C0Req{}, + "QYGL23T7": &dto.QYGL23T7Req{}, + "QYGL5A3C": &dto.QYGL5A3CReq{}, + "QYGL8B4D": &dto.QYGL8B4DReq{}, + "QYGL9E2F": &dto.QYGL9E2FReq{}, + "QYGL7C1A": &dto.QYGL7C1AReq{}, + "QYGL3F8E": &dto.QYGL3F8EReq{}, + "YYSY4B37": &dto.YYSY4B37Req{}, + "YYSY4B21": &dto.YYSY4B21Req{}, + "YYSY6F2E": &dto.YYSY6F2EReq{}, + "YYSY09CD": &dto.YYSY09CDReq{}, + "IVYZ0B03": &dto.IVYZ0B03Req{}, "YYSYBE08": &dto.YYSYBE08Req{}, "YYSYBE08TEST": &dto.YYSYBE08Req{}, - "YYSYD50F": &dto.YYSYD50FReq{}, - "YYSYF7DB": &dto.YYSYF7DBReq{}, - "IVYZ9A2B": &dto.IVYZ9A2BReq{}, - "IVYZ7F2A": &dto.IVYZ7F2AReq{}, - "IVYZ4E8B": &dto.IVYZ4E8BReq{}, - "IVYZ1C9D": &dto.IVYZ1C9DReq{}, - "IVYZGZ08": &dto.IVYZGZ08Req{}, - "FLXG8A3F": &dto.FLXG8A3FReq{}, - "FLXG5B2E": &dto.FLXG5B2EReq{}, - "COMB298Y": &dto.COMB298YReq{}, - "COMB86PM": &dto.COMB86PMReq{}, - "QCXG7A2B": &dto.QCXG7A2BReq{}, - "COMENT01": &dto.COMENT01Req{}, - "JRZQ09J8": &dto.JRZQ09J8Req{}, - "FLXGDEA8": &dto.FLXGDEA8Req{}, - "FLXGDEA9": &dto.FLXGDEA9Req{}, - "JRZQ1D09": &dto.JRZQ1D09Req{}, - "IVYZ2A8B": &dto.IVYZ2A8BReq{}, - "IVYZ7C9D": &dto.IVYZ7C9DReq{}, - "IVYZ5E3F": &dto.IVYZ5E3FReq{}, - "YYSY4F2E": &dto.YYSY4F2EReq{}, - "YYSY8B1C": &dto.YYSY8B1CReq{}, - "YYSY6D9A": &dto.YYSY6D9AReq{}, - "YYSY3E7F": &dto.YYSY3E7FReq{}, - "FLXG5A3B": &dto.FLXG5A3BReq{}, - "FLXG9C1D": &dto.FLXG9C1DReq{}, - "FLXG2E8F": &dto.FLXG2E8FReq{}, - "JRZQ3C7B": &dto.JRZQ3C7BReq{}, - "JRZQ8A2D": &dto.JRZQ8A2DReq{}, - "JRZQ5E9F": &dto.JRZQ5E9FReq{}, - "JRZQ4B6C": &dto.JRZQ4B6CReq{}, - "JRZQ7F1A": &dto.JRZQ7F1AReq{}, - "DWBG6A2C": &dto.DWBG6A2CReq{}, - "DWBG8B4D": &dto.DWBG8B4DReq{}, - "FLXG8B4D": &dto.FLXG8B4DReq{}, - "IVYZ81NC": &dto.IVYZ81NCReq{}, - "IVYZ2MN6": &dto.IVYZ2MN6Req{}, - "IVYZ7F3A": &dto.IVYZ7F3AReq{}, - "IVYZ3P9M": &dto.IVYZ3P9MReq{}, - "IVYZ3A7F": &dto.IVYZ3A7FReq{}, - "IVYZ9D2E": &dto.IVYZ9D2EReq{}, - "DWBG7F3A": &dto.DWBG7F3AReq{}, - "YYSY8F3A": &dto.YYSY8F3AReq{}, - "QCXG9P1C": &dto.QCXG9P1CReq{}, - "JRZQ9E2A": &dto.JRZQ9E2AReq{}, - "YYSY9A1B": &dto.YYSY9A1BReq{}, - "YYSY8C2D": &dto.YYSY8C2DReq{}, - "YYSY7D3E": &dto.YYSY7D3EReq{}, - "YYSY9E4A": &dto.YYSY9E4AReq{}, - "JRZQ6F2A": &dto.JRZQ6F2AReq{}, - "JRZQ8B3C": &dto.JRZQ8B3CReq{}, - "JRZQ9D4E": &dto.JRZQ9D4EReq{}, - "FLXG7E8F": &dto.FLXG7E8FReq{}, - "QYGL5F6A": &dto.QYGL5F6AReq{}, - "IVYZ6G7H": &dto.IVYZ6G7HReq{}, - "IVYZ8I9J": &dto.IVYZ8I9JReq{}, - "JRZQ0L85": &dto.JRZQ0L85Req{}, + "YYSYD50F": &dto.YYSYD50FReq{}, + "YYSYF7DB": &dto.YYSYF7DBReq{}, + "IVYZ9A2B": &dto.IVYZ9A2BReq{}, + "IVYZ7F2A": &dto.IVYZ7F2AReq{}, + "IVYZ4E8B": &dto.IVYZ4E8BReq{}, + "IVYZ1C9D": &dto.IVYZ1C9DReq{}, + "IVYZGZ08": &dto.IVYZGZ08Req{}, + "FLXG8A3F": &dto.FLXG8A3FReq{}, + "FLXG5B2E": &dto.FLXG5B2EReq{}, + "COMB298Y": &dto.COMB298YReq{}, + "COMB86PM": &dto.COMB86PMReq{}, + "QCXG7A2B": &dto.QCXG7A2BReq{}, + "COMENT01": &dto.COMENT01Req{}, + "JRZQ09J8": &dto.JRZQ09J8Req{}, + "FLXGDEA8": &dto.FLXGDEA8Req{}, + "FLXGDEA9": &dto.FLXGDEA9Req{}, + "JRZQ1D09": &dto.JRZQ1D09Req{}, + "IVYZ2A8B": &dto.IVYZ2A8BReq{}, + "IVYZ7C9D": &dto.IVYZ7C9DReq{}, + "IVYZ5E3F": &dto.IVYZ5E3FReq{}, + "YYSY4F2E": &dto.YYSY4F2EReq{}, + "YYSY8B1C": &dto.YYSY8B1CReq{}, + "YYSY6D9A": &dto.YYSY6D9AReq{}, + "YYSY3E7F": &dto.YYSY3E7FReq{}, + "FLXG5A3B": &dto.FLXG5A3BReq{}, + "FLXG9C1D": &dto.FLXG9C1DReq{}, + "FLXG2E8F": &dto.FLXG2E8FReq{}, + "JRZQ3C7B": &dto.JRZQ3C7BReq{}, + "JRZQ8A2D": &dto.JRZQ8A2DReq{}, + "JRZQ5E9F": &dto.JRZQ5E9FReq{}, + "JRZQ4B6C": &dto.JRZQ4B6CReq{}, + "JRZQ7F1A": &dto.JRZQ7F1AReq{}, + "DWBG6A2C": &dto.DWBG6A2CReq{}, + "DWBG8B4D": &dto.DWBG8B4DReq{}, + "FLXG8B4D": &dto.FLXG8B4DReq{}, + "IVYZ81NC": &dto.IVYZ81NCReq{}, + "IVYZ2MN6": &dto.IVYZ2MN6Req{}, + "IVYZ7F3A": &dto.IVYZ7F3AReq{}, + "IVYZ3P9M": &dto.IVYZ3P9MReq{}, + "IVYZ3A7F": &dto.IVYZ3A7FReq{}, + "IVYZ9D2E": &dto.IVYZ9D2EReq{}, + "DWBG7F3A": &dto.DWBG7F3AReq{}, + "YYSY8F3A": &dto.YYSY8F3AReq{}, + "QCXG9P1C": &dto.QCXG9P1CReq{}, + "JRZQ9E2A": &dto.JRZQ9E2AReq{}, + "YYSY9A1B": &dto.YYSY9A1BReq{}, + "YYSY8C2D": &dto.YYSY8C2DReq{}, + "YYSY7D3E": &dto.YYSY7D3EReq{}, + "YYSY9E4A": &dto.YYSY9E4AReq{}, + "JRZQ6F2A": &dto.JRZQ6F2AReq{}, + "JRZQ8B3C": &dto.JRZQ8B3CReq{}, + "JRZQ9D4E": &dto.JRZQ9D4EReq{}, + "FLXG7E8F": &dto.FLXG7E8FReq{}, + "QYGL5F6A": &dto.QYGL5F6AReq{}, + "IVYZ6G7H": &dto.IVYZ6G7HReq{}, + "IVYZ8I9J": &dto.IVYZ8I9JReq{}, + "JRZQ0L85": &dto.JRZQ0L85Req{}, } } @@ -1240,3 +1244,105 @@ func (s *ProductApplicationServiceImpl) mapFieldTypeToDocType(frontendType strin return "string" } } + +// ExportProductDictionary 导出产品字典 +func (s *ProductApplicationServiceImpl) ExportProductDictionary(ctx context.Context, format string) ([]byte, error) { + // 查询所有启用且可见的产品及其分类信息 + products, err := s.productManagementService.GetAllProductsForDictionary(ctx) + if err != nil { + s.logger.Error("获取产品字典数据失败", zap.Error(err)) + return nil, err + } + + if len(products) == 0 { + return nil, fmt.Errorf("没有找到符合条件的产品数据") + } + + // 按分类分组整理数据 + categoryGroups := make(map[string][]map[string]interface{}) + categoryOrder := []string{} // 保持分类顺序 + + for _, product := range products { + // 获取分类名称 + categoryName := "未分类" + if product.Category != nil { + categoryName = product.Category.Name + // 如果有二级分类,添加到分类名称中 + if product.SubCategory != nil && product.SubCategory.Name != "" { + categoryName = categoryName + " / " + product.SubCategory.Name + } + } + + // 如果分类不存在,初始化并添加到顺序列表 + if _, exists := categoryGroups[categoryName]; !exists { + categoryGroups[categoryName] = []map[string]interface{}{} + categoryOrder = append(categoryOrder, categoryName) + } + + // 添加产品到对应分类 + productInfo := map[string]interface{}{ + "category": categoryName, + "product_code": product.Code, + "product_name": product.Name, + "description": product.Description, + } + categoryGroups[categoryName] = append(categoryGroups[categoryName], productInfo) + } + + // 准备导出数据 + headers := []string{"分类", "产品编码", "产品名称", "产品简介"} + columnWidths := []float64{25, 15, 20, 40} + + // 构建数据行 + var data [][]interface{} + for _, categoryName := range categoryOrder { + productsInCategory := categoryGroups[categoryName] + for i, product := range productsInCategory { + // 只有每个分类的第一个产品才显示分类名称 + var categoryNameForRow interface{} + if i == 0 { + categoryNameForRow = product["category"] + } else { + categoryNameForRow = "" + } + + row := []interface{}{ + categoryNameForRow, + product["product_code"], + product["product_name"], + product["description"], + } + data = append(data, row) + } + } + + // 计算需要合并的行 + mergedRegions := [][]int{} + currentRow := 1 // 从第1行开始(第0行是表头) + for _, categoryName := range categoryOrder { + productsCount := len(categoryGroups[categoryName]) + if productsCount > 1 { + // 合并相同分类的单元格:从当前行开始,合并productsCount行,第0列 + // Excel格式:[startRow, startCol, endRow, endCol] + mergedRegions = append(mergedRegions, []int{ + currentRow, // startRow + 0, // startCol (分类列) + currentRow + productsCount - 1, // endRow + 0, // endCol + }) + } + currentRow += productsCount + } + + // 创建导出配置 + config := &export.ExportConfig{ + SheetName: "产品字典", + Headers: headers, + Data: data, + ColumnWidths: columnWidths, + MergedRegions: mergedRegions, + } + + // 使用导出管理器生成文件 + return s.exportManager.Export(ctx, config, format) +} diff --git a/internal/container/container.go b/internal/container/container.go index 0d9b43d..79e9f78 100644 --- a/internal/container/container.go +++ b/internal/container/container.go @@ -1026,6 +1026,7 @@ func NewContainer() *Container { documentationAppService product.DocumentationApplicationServiceInterface, formConfigService api_services.FormConfigService, logger *zap.Logger, + exportManager *export.ExportManager, ) product.ProductApplicationService { return product.NewProductApplicationService( productManagementService, @@ -1034,6 +1035,7 @@ func NewContainer() *Container { documentationAppService, formConfigService, logger, + exportManager, ) }, fx.As(new(product.ProductApplicationService)), diff --git a/internal/domains/api/dto/api_request_dto.go b/internal/domains/api/dto/api_request_dto.go index 6bfa06c..c3fca07 100644 --- a/internal/domains/api/dto/api_request_dto.go +++ b/internal/domains/api/dto/api_request_dto.go @@ -488,6 +488,24 @@ type IVYZ2A8BReq struct { Authorized string `json:"authorized" validate:"required,oneof=0 1"` } +type QYGL4YABReq struct { + EntName string `json:"ent_name" validate:"required,min=1,validEnterpriseName"` + EntCode string `json:"ent_code" validate:"required,validUSCI"` + IDCard string `json:"id_card" validate:"required,validIDCard"` + Name string `json:"name" validate:"required,min=1,validName"` +} + +type QYGL3YSBReq struct { + Name string `json:"name" validate:"required,min=1,validName"` + EntCode string `json:"ent_code" validate:"required,validUSCI"` + EntName string `json:"ent_name" validate:"required,min=1,validEnterpriseName"` +} + +type QYGL2YSBReq struct { + EntCode string `json:"ent_code" validate:"required,validUSCI"` + Name string `json:"name" validate:"required,min=1,validName"` +} + type IVYZ7C9DReq struct { IDCard string `json:"id_card" validate:"required,validIDCard"` Name string `json:"name" validate:"required,min=1,validName"` diff --git a/internal/domains/api/services/api_request_service.go b/internal/domains/api/services/api_request_service.go index f47e630..a4f0f01 100644 --- a/internal/domains/api/services/api_request_service.go +++ b/internal/domains/api/services/api_request_service.go @@ -256,7 +256,9 @@ func registerAllProcessors(combService *comb.CombService) { "QYGL8848": qygl.ProcessQYGL8848Request, //企业税收违法核查 "QYGLDJ33": qygl.ProcessQYGLDJ33Request, //企业年报信息核验 "QYGLBH7Y": qygl.ProcessQYGLBH7YRequest, //企业涉诉案件查询汇博 - + "QYGL4YAB": qygl.ProcessQYGL4YABRequest, //企业四要素认证shumai + "QYGL3YSB": qygl.ProcessQYGL3YSBRequest, //企业三要素认证shumai + "QYGL2YSB": qygl.ProcessQYGL2YSBRequest, //企业二要素认证shumai // YYSY系列处理器 "YYSY35TA": yysy.ProcessYYSY35TARequest, //运营商归属地数卖 "YYSYD50F": yysy.ProcessYYSYD50FRequest, diff --git a/internal/domains/api/services/form_config_service.go b/internal/domains/api/services/form_config_service.go index 69cf966..5e447e3 100644 --- a/internal/domains/api/services/form_config_service.go +++ b/internal/domains/api/services/form_config_service.go @@ -283,6 +283,9 @@ func (s *FormConfigServiceImpl) getDTOStruct(ctx context.Context, apiCode string "IVYZ2MN7": &dto.IVYZ2MN6Req{}, //学历Bzhicha "FLXGHB4F": &dto.FLXGHB4FReq{}, //个人涉诉案件查询汇博 "QYGLBH7Y": &dto.QYGLBH7YReq{}, //企业涉诉案件查询汇博 + "QYGL4YAB": &dto.QYGL4YABReq{}, //企业四要素认证shumai + "QYGL3YSB": &dto.QYGL3YSBReq{}, //企业三要素认证shumai + "QYGL2YSB": &dto.QYGL2YSBReq{}, //企业二要素认证shumai } // 优先返回已配置的DTO diff --git a/internal/domains/api/services/processors/qygl/qygl2ysb_processor.go b/internal/domains/api/services/processors/qygl/qygl2ysb_processor.go new file mode 100644 index 0000000..d293abd --- /dev/null +++ b/internal/domains/api/services/processors/qygl/qygl2ysb_processor.go @@ -0,0 +1,47 @@ +package qygl + +import ( + "context" + "encoding/json" + "errors" + + "tyapi-server/internal/domains/api/dto" + "tyapi-server/internal/domains/api/services/processors" + "tyapi-server/internal/infrastructure/external/shumai" +) + +// ProcessQYGL2YSBRequest QYGL2YSB API处理方法 - 企业二要素认证 +func ProcessQYGL2YSBRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { + var paramsDto dto.QYGL2YSBReq + if err := json.Unmarshal(params, ¶msDto); err != nil { + return nil, errors.Join(processors.ErrSystem, err) + } + + if err := deps.Validator.ValidateStruct(paramsDto); err != nil { + return nil, errors.Join(processors.ErrInvalidParam, err) + } + reqFormData := map[string]interface{}{ + "legalPerson": paramsDto.Name, + "creditNo": paramsDto.EntCode, + } + apiPath := "/v4/company/two/check" // 接口路径,根据数脉文档填写(如 v4/xxx) + respBytes, err := deps.ShumaiService.CallAPIForm(ctx, apiPath, reqFormData) + if err != nil { + if errors.Is(err, shumai.ErrDatasource) { + // 数据源错误 + return nil, errors.Join(processors.ErrDatasource, err) + } else if errors.Is(err, shumai.ErrSystem) { + // 系统错误 + return nil, errors.Join(processors.ErrSystem, err) + } else if errors.Is(err, shumai.ErrNotFound) { + // 查无记录 + return nil, errors.Join(processors.ErrNotFound, err) + } else { + // 其他未知错误 + return nil, errors.Join(processors.ErrSystem, err) + } + + } + return respBytes, nil + +} diff --git a/internal/domains/api/services/processors/qygl/qygl3ysb_processor.go b/internal/domains/api/services/processors/qygl/qygl3ysb_processor.go new file mode 100644 index 0000000..b8bd31f --- /dev/null +++ b/internal/domains/api/services/processors/qygl/qygl3ysb_processor.go @@ -0,0 +1,48 @@ +package qygl + +import ( + "context" + "encoding/json" + "errors" + + "tyapi-server/internal/domains/api/dto" + "tyapi-server/internal/domains/api/services/processors" + "tyapi-server/internal/infrastructure/external/shumai" +) + +// ProcessQYGL3YSBRequest QYGL3YSB API处理方法 - 企业三要素认证 +func ProcessQYGL3YSBRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { + var paramsDto dto.QYGL3YSBReq + if err := json.Unmarshal(params, ¶msDto); err != nil { + return nil, errors.Join(processors.ErrSystem, err) + } + + if err := deps.Validator.ValidateStruct(paramsDto); err != nil { + return nil, errors.Join(processors.ErrInvalidParam, err) + } + reqFormData := map[string]interface{}{ + "legalPerson": paramsDto.Name, + "companyName": paramsDto.EntName, + "creditNo": paramsDto.EntCode, + } + apiPath := "/v4/company-three/check" // 接口路径,根据数脉文档填写(如 v4/xxx) + respBytes, err := deps.ShumaiService.CallAPIForm(ctx, apiPath, reqFormData) + if err != nil { + if errors.Is(err, shumai.ErrDatasource) { + // 数据源错误 + return nil, errors.Join(processors.ErrDatasource, err) + } else if errors.Is(err, shumai.ErrSystem) { + // 系统错误 + return nil, errors.Join(processors.ErrSystem, err) + } else if errors.Is(err, shumai.ErrNotFound) { + // 查无记录 + return nil, errors.Join(processors.ErrNotFound, err) + } else { + // 其他未知错误 + return nil, errors.Join(processors.ErrSystem, err) + } + + } + return respBytes, nil + +} diff --git a/internal/domains/api/services/processors/qygl/qygl4yab_processor.go b/internal/domains/api/services/processors/qygl/qygl4yab_processor.go new file mode 100644 index 0000000..d5ecd5d --- /dev/null +++ b/internal/domains/api/services/processors/qygl/qygl4yab_processor.go @@ -0,0 +1,49 @@ +package qygl + +import ( + "context" + "encoding/json" + "errors" + + "tyapi-server/internal/domains/api/dto" + "tyapi-server/internal/domains/api/services/processors" + "tyapi-server/internal/infrastructure/external/shumai" +) + +// ProcessQYGL4YABRequest QYGL4YAB API处理方法 - 企业四要素认证 +func ProcessQYGL4YABRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { + var paramsDto dto.QYGL4YABReq + if err := json.Unmarshal(params, ¶msDto); err != nil { + return nil, errors.Join(processors.ErrSystem, err) + } + + if err := deps.Validator.ValidateStruct(paramsDto); err != nil { + return nil, errors.Join(processors.ErrInvalidParam, err) + } + reqFormData := map[string]interface{}{ + "idCard": paramsDto.IDCard, + "legalPerson": paramsDto.Name, + "companyName": paramsDto.EntName, + "creditNo": paramsDto.EntCode, + } + apiPath := "/v4/company-four/check" // 接口路径,根据数脉文档填写(如 v4/xxx) + respBytes, err := deps.ShumaiService.CallAPIForm(ctx, apiPath, reqFormData) + if err != nil { + if errors.Is(err, shumai.ErrDatasource) { + // 数据源错误 + return nil, errors.Join(processors.ErrDatasource, err) + } else if errors.Is(err, shumai.ErrSystem) { + // 系统错误 + return nil, errors.Join(processors.ErrSystem, err) + } else if errors.Is(err, shumai.ErrNotFound) { + // 查无记录 + return nil, errors.Join(processors.ErrNotFound, err) + } else { + // 其他未知错误 + return nil, errors.Join(processors.ErrSystem, err) + } + + } + return respBytes, nil + +} diff --git a/internal/domains/product/services/product_management_service.go b/internal/domains/product/services/product_management_service.go index 9b784d7..37fccbc 100644 --- a/internal/domains/product/services/product_management_service.go +++ b/internal/domains/product/services/product_management_service.go @@ -435,3 +435,46 @@ func (s *ProductManagementService) ListProductsWithSubscriptionStatus(ctx contex return products, subscriptionStatusMap, total, nil } + +// GetAllProductsForDictionary 获取所有启用且可见的产品(用于导出产品字典) +func (s *ProductManagementService) GetAllProductsForDictionary(ctx context.Context) ([]*entities.Product, error) { + // 构建查询条件:启用且可见 + isEnabled := true + isVisible := true + + filters := map[string]interface{}{ + "is_enabled": isEnabled, + "is_visible": isVisible, + } + + options := interfaces.ListOptions{ + Page: 1, + PageSize: 1000, // 获取所有产品 + Sort: "sort", // 使用 products 表的 sort 字段,排序后再按分类分组 + Order: "asc", + } + + // 获取产品列表 + products, _, err := s.ListProducts(ctx, filters, options) + if err != nil { + s.logger.Error("获取产品字典数据失败", zap.Error(err)) + return nil, fmt.Errorf("获取产品字典数据失败: %w", err) + } + + // 预加载分类信息 + for _, product := range products { + if product.CategoryID != "" { + category, _ := s.categoryRepo.GetByID(ctx, product.CategoryID) + product.Category = &category + } + + if product.SubCategoryID != nil && *product.SubCategoryID != "" { + subCategory, err := s.subCategoryRepo.GetByID(ctx, *product.SubCategoryID) + if err == nil && subCategory != nil { + product.SubCategory = subCategory + } + } + } + + return products, nil +} diff --git a/internal/infrastructure/database/repositories/product/gorm_product_repository.go b/internal/infrastructure/database/repositories/product/gorm_product_repository.go index 685299d..c72a074 100644 --- a/internal/infrastructure/database/repositories/product/gorm_product_repository.go +++ b/internal/infrastructure/database/repositories/product/gorm_product_repository.go @@ -3,6 +3,8 @@ package repositories import ( "context" "errors" + "strings" + "tyapi-server/internal/domains/product/entities" "tyapi-server/internal/domains/product/repositories" "tyapi-server/internal/domains/product/repositories/queries" @@ -165,13 +167,33 @@ func (r *GormProductRepository) ListProducts(ctx context.Context, query *queries // 应用排序 if query.SortBy != "" { - order := query.SortBy - if query.SortOrder == "desc" { - order += " DESC" + // 检查是否是关联表字段排序 + if strings.Contains(query.SortBy, ".") { + parts := strings.Split(query.SortBy, ".") + if len(parts) == 2 { + // 关联表字段排序,需要JOIN表 + joinTable := parts[0] + sortField := parts[1] + dbQuery = dbQuery.Joins("JOIN "+joinTable+" ON products.category_id = "+joinTable+".id") + + order := joinTable + "." + sortField + if query.SortOrder == "desc" { + order += " DESC" + } else { + order += " ASC" + } + dbQuery = dbQuery.Order(order) + } } else { - order += " ASC" + // 本表字段排序 + order := query.SortBy + if query.SortOrder == "desc" { + order += " DESC" + } else { + order += " ASC" + } + dbQuery = dbQuery.Order(order) } - dbQuery = dbQuery.Order(order) } else { dbQuery = dbQuery.Order("created_at DESC") } diff --git a/internal/infrastructure/http/handlers/product_admin_handler.go b/internal/infrastructure/http/handlers/product_admin_handler.go index b49f6f5..b3d18d8 100644 --- a/internal/infrastructure/http/handlers/product_admin_handler.go +++ b/internal/infrastructure/http/handlers/product_admin_handler.go @@ -1659,3 +1659,54 @@ func (h *ProductAdminHandler) ExportAdminApiCalls(c *gin.Context) { c.Header("Content-Disposition", "attachment; filename="+filename) c.Data(200, contentType, fileData) } + +// ExportProductDictionary 导出产品字典 +// @Summary 导出产品字典 +// @Description 导出所有启用且可见的产品字典,按分类分组,左侧分类列合并单元格 +// @Tags 产品管理 +// @Accept json +// @Produce application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,text/csv +// @Security Bearer +// @Param format query string false "导出格式" Enums(excel, csv) default(excel) +// @Success 200 {file} file "导出文件" +// @Failure 400 {object} map[string]interface{} "请求参数错误" +// @Failure 401 {object} map[string]interface{} "未认证" +// @Failure 500 {object} map[string]interface{} "服务器内部错误" +// @Router /api/v1/admin/products/export-dictionary [get] +func (h *ProductAdminHandler) ExportProductDictionary(c *gin.Context) { + // 获取导出格式,默认为excel + format := c.DefaultQuery("format", "excel") + if format != "excel" && format != "csv" { + h.responseBuilder.BadRequest(c, "不支持的导出格式") + return + } + + // 调用应用服务导出数据 + fileData, err := h.productAppService.ExportProductDictionary(c.Request.Context(), format) + if err != nil { + h.logger.Error("导出产品字典失败", zap.Error(err)) + + // 根据错误信息返回具体的提示 + errMsg := err.Error() + if strings.Contains(errMsg, "没有找到符合条件的产品数据") || strings.Contains(errMsg, "没有数据") { + h.responseBuilder.NotFound(c, "没有找到符合条件的产品数据,请确保有启用且可见的产品") + } else if strings.Contains(errMsg, "参数") || strings.Contains(errMsg, "参数错误") { + h.responseBuilder.BadRequest(c, errMsg) + } else { + h.responseBuilder.BadRequest(c, "导出产品字典失败:"+errMsg) + } + return + } + + // 设置响应头 + contentType := "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + filename := "产品字典.xlsx" + if format == "csv" { + contentType = "text/csv;charset=utf-8" + filename = "产品字典.csv" + } + + c.Header("Content-Type", contentType) + c.Header("Content-Disposition", "attachment; filename="+filename) + c.Data(200, contentType, fileData) +} diff --git a/internal/infrastructure/http/routes/product_admin_routes.go b/internal/infrastructure/http/routes/product_admin_routes.go index 6277daa..c9fa881 100644 --- a/internal/infrastructure/http/routes/product_admin_routes.go +++ b/internal/infrastructure/http/routes/product_admin_routes.go @@ -60,6 +60,9 @@ func (r *ProductAdminRoutes) Register(router *sharedhttp.GinRouter) { products.GET("/:id/documentation", r.handler.GetProductDocumentation) products.POST("/:id/documentation", r.handler.CreateOrUpdateProductDocumentation) products.DELETE("/:id/documentation", r.handler.DeleteProductDocumentation) + + // 产品字典导出 + products.GET("/export-dictionary", r.handler.ExportProductDictionary) } // 分类管理 diff --git a/internal/shared/export/export.go b/internal/shared/export/export.go index c363912..0554a0a 100644 --- a/internal/shared/export/export.go +++ b/internal/shared/export/export.go @@ -11,10 +11,11 @@ import ( // ExportConfig 定义了导出所需的配置 type ExportConfig struct { - SheetName string // 工作表名称 - Headers []string // 表头 - Data [][]interface{} // 导出数据 - ColumnWidths []float64 // 列宽 + SheetName string // 工作表名称 + Headers []string // 表头 + Data [][]interface{} // 导出数据 + ColumnWidths []float64 // 列宽 + MergedRegions [][]int // 合并单元格配置 [[startRow, startCol, endRow, endCol], ...] } // ExportManager 负责管理不同格式的导出 @@ -95,6 +96,30 @@ func (m *ExportManager) generateExcel(ctx context.Context, config *ExportConfig) } } + // 合并单元格 + if len(config.MergedRegions) > 0 { + for _, region := range config.MergedRegions { + startRow := region[0] + startCol := region[1] + endRow := region[2] + endCol := region[3] + + startCell, err := excelize.CoordinatesToCellName(startCol+1, startRow+1) + if err != nil { + return nil, fmt.Errorf("生成合并区域起始单元格坐标失败: %v", err) + } + endCell, err := excelize.CoordinatesToCellName(endCol+1, endRow+1) + if err != nil { + return nil, fmt.Errorf("生成合并区域结束单元格坐标失败: %v", err) + } + + err = f.MergeCell(sheetName, startCell, endCell) + if err != nil { + return nil, fmt.Errorf("合并单元格失败: %v", err) + } + } + } + // 设置列宽 for i, width := range config.ColumnWidths { col, err := excelize.ColumnNumberToName(i + 1) diff --git a/internal/shared/services/export_service.go b/internal/shared/services/export_service.go index abcc0c3..3d3229a 100644 --- a/internal/shared/services/export_service.go +++ b/internal/shared/services/export_service.go @@ -11,10 +11,11 @@ import ( // ExportConfig 定义了导出所需的配置 type ExportConfig struct { - SheetName string // 工作表名称 - Headers []string // 表头 - Data [][]interface{} // 导出数据 - ColumnWidths []float64 // 列宽 + SheetName string // 工作表名称 + Headers []string // 表头 + Data [][]interface{} // 导出数据 + ColumnWidths []float64 // 列宽 + MergedRegions [][]int // 合并单元格配置 [[startRow, startCol, endRow, endCol], ...] } // ExportManager 负责管理不同格式的导出 From 9e136bbd6b85ca56e83ee45fb7827ecdf2e0d98c Mon Sep 17 00:00:00 2001 From: Mrx <18278715334@163.com> Date: Wed, 27 May 2026 13:10:00 +0800 Subject: [PATCH 23/28] f --- internal/domains/api/dto/api_request_dto.go | 18 +++++++++--------- .../processors/qygl/qygl2ysb_processor.go | 2 +- .../processors/qygl/qygl3ysb_processor.go | 2 +- .../processors/qygl/qygl4yab_processor.go | 2 +- .../services/product_management_service.go | 19 ++----------------- .../product/gorm_product_repository.go | 2 +- 6 files changed, 15 insertions(+), 30 deletions(-) diff --git a/internal/domains/api/dto/api_request_dto.go b/internal/domains/api/dto/api_request_dto.go index c3fca07..4fcbbbc 100644 --- a/internal/domains/api/dto/api_request_dto.go +++ b/internal/domains/api/dto/api_request_dto.go @@ -489,21 +489,21 @@ type IVYZ2A8BReq struct { } type QYGL4YABReq struct { - EntName string `json:"ent_name" validate:"required,min=1,validEnterpriseName"` - EntCode string `json:"ent_code" validate:"required,validUSCI"` - IDCard string `json:"id_card" validate:"required,validIDCard"` - Name string `json:"name" validate:"required,min=1,validName"` + EntName string `json:"ent_name" validate:"required,min=1,validEnterpriseName"` + LegalPerson string `json:"legal_person" validate:"required,min=1,validName"` + EntCode string `json:"ent_code" validate:"required,validUSCI"` + IDCard string `json:"id_card" validate:"required,validIDCard"` } type QYGL3YSBReq struct { - Name string `json:"name" validate:"required,min=1,validName"` - EntCode string `json:"ent_code" validate:"required,validUSCI"` - EntName string `json:"ent_name" validate:"required,min=1,validEnterpriseName"` + EntName string `json:"ent_name" validate:"required,min=1,validEnterpriseName"` + LegalPerson string `json:"legal_person" validate:"required,min=1,validName"` + EntCode string `json:"ent_code" validate:"required,validUSCI"` } type QYGL2YSBReq struct { - EntCode string `json:"ent_code" validate:"required,validUSCI"` - Name string `json:"name" validate:"required,min=1,validName"` + LegalPerson string `json:"legal_person" validate:"required,min=1,validName"` + EntCode string `json:"ent_code" validate:"required,validUSCI"` } type IVYZ7C9DReq struct { diff --git a/internal/domains/api/services/processors/qygl/qygl2ysb_processor.go b/internal/domains/api/services/processors/qygl/qygl2ysb_processor.go index d293abd..d4e6df7 100644 --- a/internal/domains/api/services/processors/qygl/qygl2ysb_processor.go +++ b/internal/domains/api/services/processors/qygl/qygl2ysb_processor.go @@ -21,7 +21,7 @@ func ProcessQYGL2YSBRequest(ctx context.Context, params []byte, deps *processors return nil, errors.Join(processors.ErrInvalidParam, err) } reqFormData := map[string]interface{}{ - "legalPerson": paramsDto.Name, + "legalPerson": paramsDto.LegalPerson, "creditNo": paramsDto.EntCode, } apiPath := "/v4/company/two/check" // 接口路径,根据数脉文档填写(如 v4/xxx) diff --git a/internal/domains/api/services/processors/qygl/qygl3ysb_processor.go b/internal/domains/api/services/processors/qygl/qygl3ysb_processor.go index b8bd31f..81ecf4a 100644 --- a/internal/domains/api/services/processors/qygl/qygl3ysb_processor.go +++ b/internal/domains/api/services/processors/qygl/qygl3ysb_processor.go @@ -21,7 +21,7 @@ func ProcessQYGL3YSBRequest(ctx context.Context, params []byte, deps *processors return nil, errors.Join(processors.ErrInvalidParam, err) } reqFormData := map[string]interface{}{ - "legalPerson": paramsDto.Name, + "legalPerson": paramsDto.LegalPerson, "companyName": paramsDto.EntName, "creditNo": paramsDto.EntCode, } diff --git a/internal/domains/api/services/processors/qygl/qygl4yab_processor.go b/internal/domains/api/services/processors/qygl/qygl4yab_processor.go index d5ecd5d..f905f3b 100644 --- a/internal/domains/api/services/processors/qygl/qygl4yab_processor.go +++ b/internal/domains/api/services/processors/qygl/qygl4yab_processor.go @@ -22,7 +22,7 @@ func ProcessQYGL4YABRequest(ctx context.Context, params []byte, deps *processors } reqFormData := map[string]interface{}{ "idCard": paramsDto.IDCard, - "legalPerson": paramsDto.Name, + "legalPerson": paramsDto.LegalPerson, "companyName": paramsDto.EntName, "creditNo": paramsDto.EntCode, } diff --git a/internal/domains/product/services/product_management_service.go b/internal/domains/product/services/product_management_service.go index 37fccbc..a9cbc2e 100644 --- a/internal/domains/product/services/product_management_service.go +++ b/internal/domains/product/services/product_management_service.go @@ -449,8 +449,8 @@ func (s *ProductManagementService) GetAllProductsForDictionary(ctx context.Conte options := interfaces.ListOptions{ Page: 1, - PageSize: 1000, // 获取所有产品 - Sort: "sort", // 使用 products 表的 sort 字段,排序后再按分类分组 + PageSize: 1000, // 获取所有产品 + Sort: "category.sort", // 按分类的排序字段排序 Order: "asc", } @@ -461,20 +461,5 @@ func (s *ProductManagementService) GetAllProductsForDictionary(ctx context.Conte return nil, fmt.Errorf("获取产品字典数据失败: %w", err) } - // 预加载分类信息 - for _, product := range products { - if product.CategoryID != "" { - category, _ := s.categoryRepo.GetByID(ctx, product.CategoryID) - product.Category = &category - } - - if product.SubCategoryID != nil && *product.SubCategoryID != "" { - subCategory, err := s.subCategoryRepo.GetByID(ctx, *product.SubCategoryID) - if err == nil && subCategory != nil { - product.SubCategory = subCategory - } - } - } - return products, nil } diff --git a/internal/infrastructure/database/repositories/product/gorm_product_repository.go b/internal/infrastructure/database/repositories/product/gorm_product_repository.go index c72a074..c4f824b 100644 --- a/internal/infrastructure/database/repositories/product/gorm_product_repository.go +++ b/internal/infrastructure/database/repositories/product/gorm_product_repository.go @@ -205,7 +205,7 @@ func (r *GormProductRepository) ListProducts(ctx context.Context, query *queries } // 预加载分类信息并获取数据 - if err := dbQuery.Preload("Category").Find(&productEntities).Error; err != nil { + if err := dbQuery.Preload("Category").Preload("SubCategory").Find(&productEntities).Error; err != nil { return nil, 0, err } From f2c46186495cf11cb0fe22933d2c2e0308f9dbe6 Mon Sep 17 00:00:00 2001 From: Mrx <18278715334@163.com> Date: Wed, 27 May 2026 14:12:11 +0800 Subject: [PATCH 24/28] f --- internal/domains/api/dto/api_request_dto.go | 6 +++ .../api/services/api_request_service.go | 1 + .../api/services/form_config_service.go | 9 ++++ .../processors/qygl/qygldg77_processor.go | 48 +++++++++++++++++++ 4 files changed, 64 insertions(+) create mode 100644 internal/domains/api/services/processors/qygl/qygldg77_processor.go diff --git a/internal/domains/api/dto/api_request_dto.go b/internal/domains/api/dto/api_request_dto.go index 4fcbbbc..dc71ff7 100644 --- a/internal/domains/api/dto/api_request_dto.go +++ b/internal/domains/api/dto/api_request_dto.go @@ -506,6 +506,12 @@ type QYGL2YSBReq struct { EntCode string `json:"ent_code" validate:"required,validUSCI"` } +type QYGLDG77Req struct { + EntName string `json:"ent_name" validate:"required,min=1,validEnterpriseName"` + AccountNo string `json:"account_no" validate:"required,min=1,"` + AccountBank string `json:"account_bank" validate:"required,min=1,"` +} + type IVYZ7C9DReq struct { IDCard string `json:"id_card" validate:"required,validIDCard"` Name string `json:"name" validate:"required,min=1,validName"` diff --git a/internal/domains/api/services/api_request_service.go b/internal/domains/api/services/api_request_service.go index a4f0f01..8cba7ba 100644 --- a/internal/domains/api/services/api_request_service.go +++ b/internal/domains/api/services/api_request_service.go @@ -259,6 +259,7 @@ func registerAllProcessors(combService *comb.CombService) { "QYGL4YAB": qygl.ProcessQYGL4YABRequest, //企业四要素认证shumai "QYGL3YSB": qygl.ProcessQYGL3YSBRequest, //企业三要素认证shumai "QYGL2YSB": qygl.ProcessQYGL2YSBRequest, //企业二要素认证shumai + "QYGLDG77": qygl.ProcessQYGLDG77Request, //企业对公打款认证shumai // YYSY系列处理器 "YYSY35TA": yysy.ProcessYYSY35TARequest, //运营商归属地数卖 "YYSYD50F": yysy.ProcessYYSYD50FRequest, diff --git a/internal/domains/api/services/form_config_service.go b/internal/domains/api/services/form_config_service.go index 5e447e3..b2876bc 100644 --- a/internal/domains/api/services/form_config_service.go +++ b/internal/domains/api/services/form_config_service.go @@ -286,6 +286,7 @@ func (s *FormConfigServiceImpl) getDTOStruct(ctx context.Context, apiCode string "QYGL4YAB": &dto.QYGL4YABReq{}, //企业四要素认证shumai "QYGL3YSB": &dto.QYGL3YSBReq{}, //企业三要素认证shumai "QYGL2YSB": &dto.QYGL2YSBReq{}, //企业二要素认证shumai + "QYGLDG77": &dto.QYGLDG77Req{}, //企业对公打款认证shumai } // 优先返回已配置的DTO @@ -518,6 +519,8 @@ func (s *FormConfigServiceImpl) generateFieldLabel(jsonTag string) string { "plate_color": "车牌颜色", "marital_type": "婚姻状况类型", "auth_authorize_file_base64": "PDF授权文件Base64编码(≤500KB,仅PDF)", + "accountNo": "企业账户", + "accountBank": "开户行", } if label, exists := labelMap[jsonTag]; exists { @@ -584,6 +587,8 @@ func (s *FormConfigServiceImpl) generateExampleValue(fieldType reflect.Type, jso "plate_color": "0", "marital_type": "10", "auth_authorize_file_base64": "JVBERi0xLjQKJcTl8uXr...(示例PDF的Base64编码)", + "accountNo": "6222021234567890123", + "accountBank": "中国工商银行", } if example, exists := exampleMap[jsonTag]; exists { @@ -659,6 +664,8 @@ func (s *FormConfigServiceImpl) generatePlaceholder(jsonTag string, fieldType st "plate_color": "请输入车牌颜色", "marital_type": "请选择婚姻状况类型", "auth_authorize_file_base64": "请输入PDF文件的Base64编码字符串", + "accountNo": "请输入企业账户", + "accountBank": "请输入开户行", } if placeholder, exists := placeholderMap[jsonTag]; exists { @@ -736,6 +743,8 @@ func (s *FormConfigServiceImpl) generateDescription(jsonTag string, validation s "plate_color": "车牌颜色", "marital_type": "婚姻状况类型:10-未登记(无登记记录),20-已婚,30-丧偶,40-离异", "auth_authorize_file_base64": "请输入PDF文件的Base64编码字符串", + "accountNo": "请输入企业账户", + "accountBank": "请输入开户行", } if desc, exists := descMap[jsonTag]; exists { diff --git a/internal/domains/api/services/processors/qygl/qygldg77_processor.go b/internal/domains/api/services/processors/qygl/qygldg77_processor.go new file mode 100644 index 0000000..7395bad --- /dev/null +++ b/internal/domains/api/services/processors/qygl/qygldg77_processor.go @@ -0,0 +1,48 @@ +package qygl + +import ( + "context" + "encoding/json" + "errors" + + "tyapi-server/internal/domains/api/dto" + "tyapi-server/internal/domains/api/services/processors" + "tyapi-server/internal/infrastructure/external/shumai" +) + +// ProcessQYGLDG77Request QYGLDG77 API处理方法 - 企业打款认证 +func ProcessQYGLDG77Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { + var paramsDto dto.QYGLDG77Req + if err := json.Unmarshal(params, ¶msDto); err != nil { + return nil, errors.Join(processors.ErrSystem, err) + } + + if err := deps.Validator.ValidateStruct(paramsDto); err != nil { + return nil, errors.Join(processors.ErrInvalidParam, err) + } + reqFormData := map[string]interface{}{ + "companyName": paramsDto.EntName, + "accountNo": paramsDto.AccountNo, + "accountBank": paramsDto.AccountBank, + } + apiPath := "/v2/company/dkrz/check" // 接口路径,根据数脉文档填写(如 v4/xxx) + respBytes, err := deps.ShumaiService.CallAPIForm(ctx, apiPath, reqFormData) + if err != nil { + if errors.Is(err, shumai.ErrDatasource) { + // 数据源错误 + return nil, errors.Join(processors.ErrDatasource, err) + } else if errors.Is(err, shumai.ErrSystem) { + // 系统错误 + return nil, errors.Join(processors.ErrSystem, err) + } else if errors.Is(err, shumai.ErrNotFound) { + // 查无记录 + return nil, errors.Join(processors.ErrNotFound, err) + } else { + // 其他未知错误 + return nil, errors.Join(processors.ErrSystem, err) + } + + } + return respBytes, nil + +} From b16e68f3bd6e908d047643f70005b7250c5fe7b6 Mon Sep 17 00:00:00 2001 From: Mrx <18278715334@163.com> Date: Wed, 27 May 2026 14:16:41 +0800 Subject: [PATCH 25/28] f --- .../domains/api/services/form_config_service.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/internal/domains/api/services/form_config_service.go b/internal/domains/api/services/form_config_service.go index b2876bc..1384838 100644 --- a/internal/domains/api/services/form_config_service.go +++ b/internal/domains/api/services/form_config_service.go @@ -519,8 +519,8 @@ func (s *FormConfigServiceImpl) generateFieldLabel(jsonTag string) string { "plate_color": "车牌颜色", "marital_type": "婚姻状况类型", "auth_authorize_file_base64": "PDF授权文件Base64编码(≤500KB,仅PDF)", - "accountNo": "企业账户", - "accountBank": "开户行", + "account_no": "企业账户", + "account_bank": "开户行", } if label, exists := labelMap[jsonTag]; exists { @@ -587,8 +587,8 @@ func (s *FormConfigServiceImpl) generateExampleValue(fieldType reflect.Type, jso "plate_color": "0", "marital_type": "10", "auth_authorize_file_base64": "JVBERi0xLjQKJcTl8uXr...(示例PDF的Base64编码)", - "accountNo": "6222021234567890123", - "accountBank": "中国工商银行", + "account_no": "6222021234567890123", + "account_bank": "中国工商银行", } if example, exists := exampleMap[jsonTag]; exists { @@ -664,8 +664,8 @@ func (s *FormConfigServiceImpl) generatePlaceholder(jsonTag string, fieldType st "plate_color": "请输入车牌颜色", "marital_type": "请选择婚姻状况类型", "auth_authorize_file_base64": "请输入PDF文件的Base64编码字符串", - "accountNo": "请输入企业账户", - "accountBank": "请输入开户行", + "account_no": "请输入企业账户", + "account_bank": "请输入开户行", } if placeholder, exists := placeholderMap[jsonTag]; exists { @@ -743,8 +743,8 @@ func (s *FormConfigServiceImpl) generateDescription(jsonTag string, validation s "plate_color": "车牌颜色", "marital_type": "婚姻状况类型:10-未登记(无登记记录),20-已婚,30-丧偶,40-离异", "auth_authorize_file_base64": "请输入PDF文件的Base64编码字符串", - "accountNo": "请输入企业账户", - "accountBank": "请输入开户行", + "account_no": "请输入企业账户", + "account_bank": "请输入开户行", } if desc, exists := descMap[jsonTag]; exists { From 6cde82ee699b5580a5c5afeb972b7e528b10b78a Mon Sep 17 00:00:00 2001 From: Mrx <18278715334@163.com> Date: Wed, 27 May 2026 15:18:43 +0800 Subject: [PATCH 26/28] f --- .../product_application_service_impl.go | 32 ++++++++++++++----- .../services/product_management_service.go | 4 +-- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/internal/application/product/product_application_service_impl.go b/internal/application/product/product_application_service_impl.go index 3d0cb56..f759900 100644 --- a/internal/application/product/product_application_service_impl.go +++ b/internal/application/product/product_application_service_impl.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "reflect" + "sort" "strings" "github.com/shopspring/decimal" @@ -1258,15 +1259,24 @@ func (s *ProductApplicationServiceImpl) ExportProductDictionary(ctx context.Cont return nil, fmt.Errorf("没有找到符合条件的产品数据") } - // 按分类分组整理数据 - categoryGroups := make(map[string][]map[string]interface{}) - categoryOrder := []string{} // 保持分类顺序 + // 按分类名称分组整理数据 + type categoryInfo struct { + name string + sort int + } + + categoryGroups := make(map[string][]map[string]interface{}) // 使用分类名称作为key + categorySortMap := make(map[string]int) // 分类名称到sort值的映射 + var categoryNames []string // 保持分类顺序 for _, product := range products { - // 获取分类名称 + // 获取分类信息 categoryName := "未分类" + categorySort := 999999 // 默认排序值,确保未分类的产品排在最后 + if product.Category != nil { categoryName = product.Category.Name + categorySort = product.Category.Sort // 如果有二级分类,添加到分类名称中 if product.SubCategory != nil && product.SubCategory.Name != "" { categoryName = categoryName + " / " + product.SubCategory.Name @@ -1276,7 +1286,8 @@ func (s *ProductApplicationServiceImpl) ExportProductDictionary(ctx context.Cont // 如果分类不存在,初始化并添加到顺序列表 if _, exists := categoryGroups[categoryName]; !exists { categoryGroups[categoryName] = []map[string]interface{}{} - categoryOrder = append(categoryOrder, categoryName) + categoryNames = append(categoryNames, categoryName) + categorySortMap[categoryName] = categorySort } // 添加产品到对应分类 @@ -1289,19 +1300,24 @@ func (s *ProductApplicationServiceImpl) ExportProductDictionary(ctx context.Cont categoryGroups[categoryName] = append(categoryGroups[categoryName], productInfo) } + // 按分类的sort值对分类名称进行排序 + sort.Slice(categoryNames, func(i, j int) bool { + return categorySortMap[categoryNames[i]] < categorySortMap[categoryNames[j]] + }) + // 准备导出数据 headers := []string{"分类", "产品编码", "产品名称", "产品简介"} columnWidths := []float64{25, 15, 20, 40} // 构建数据行 var data [][]interface{} - for _, categoryName := range categoryOrder { + for _, categoryName := range categoryNames { productsInCategory := categoryGroups[categoryName] for i, product := range productsInCategory { // 只有每个分类的第一个产品才显示分类名称 var categoryNameForRow interface{} if i == 0 { - categoryNameForRow = product["category"] + categoryNameForRow = categoryName } else { categoryNameForRow = "" } @@ -1319,7 +1335,7 @@ func (s *ProductApplicationServiceImpl) ExportProductDictionary(ctx context.Cont // 计算需要合并的行 mergedRegions := [][]int{} currentRow := 1 // 从第1行开始(第0行是表头) - for _, categoryName := range categoryOrder { + for _, categoryName := range categoryNames { productsCount := len(categoryGroups[categoryName]) if productsCount > 1 { // 合并相同分类的单元格:从当前行开始,合并productsCount行,第0列 diff --git a/internal/domains/product/services/product_management_service.go b/internal/domains/product/services/product_management_service.go index a9cbc2e..896e594 100644 --- a/internal/domains/product/services/product_management_service.go +++ b/internal/domains/product/services/product_management_service.go @@ -449,8 +449,8 @@ func (s *ProductManagementService) GetAllProductsForDictionary(ctx context.Conte options := interfaces.ListOptions{ Page: 1, - PageSize: 1000, // 获取所有产品 - Sort: "category.sort", // 按分类的排序字段排序 + PageSize: 1000, // 获取所有产品 + Sort: "id", // 按产品ID排序,避免JOIN问题 Order: "asc", } From b04b43cb82a5cf2b016d25a0dc550f2df817a8ea Mon Sep 17 00:00:00 2001 From: Mrx <18278715334@163.com> Date: Wed, 27 May 2026 15:30:59 +0800 Subject: [PATCH 27/28] f --- .../domains/api/services/processors/flxg/flxg0v4b_processor.go | 2 +- .../domains/api/services/processors/flxg/flxg5a3b_processor.go | 2 +- .../domains/api/services/processors/flxg/flxg7e8f_processor.go | 2 +- .../domains/api/services/processors/flxg/flxgca3d_processor.go | 2 +- .../domains/api/services/processors/flxg/flxgdea8_processor.go | 2 +- .../domains/api/services/processors/flxg/flxgdea9_processor.go | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/domains/api/services/processors/flxg/flxg0v4b_processor.go b/internal/domains/api/services/processors/flxg/flxg0v4b_processor.go index b4bba6d..9c64476 100644 --- a/internal/domains/api/services/processors/flxg/flxg0v4b_processor.go +++ b/internal/domains/api/services/processors/flxg/flxg0v4b_processor.go @@ -25,7 +25,7 @@ func ProcessFLXG0V4BRequest(ctx context.Context, params []byte, deps *processors return nil, errors.Join(processors.ErrInvalidParam, err) } // 去掉司法案件案件去掉身份证号码 - if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "420116198907031413" { + if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "420116198907031413" || paramsDto.IDCard == "13032319930128263X" { return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空")) } encryptedName, err := deps.WestDexService.Encrypt(paramsDto.Name) diff --git a/internal/domains/api/services/processors/flxg/flxg5a3b_processor.go b/internal/domains/api/services/processors/flxg/flxg5a3b_processor.go index d6454a7..49e5969 100644 --- a/internal/domains/api/services/processors/flxg/flxg5a3b_processor.go +++ b/internal/domains/api/services/processors/flxg/flxg5a3b_processor.go @@ -20,7 +20,7 @@ func ProcessFLXG5A3BRequest(ctx context.Context, params []byte, deps *processors if err := deps.Validator.ValidateStruct(paramsDto); err != nil { return nil, errors.Join(processors.ErrInvalidParam, err) } - if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "420116198907031413" { + if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "420116198907031413" || paramsDto.IDCard == "13032319930128263X" { return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空")) } diff --git a/internal/domains/api/services/processors/flxg/flxg7e8f_processor.go b/internal/domains/api/services/processors/flxg/flxg7e8f_processor.go index af59fa7..8742681 100644 --- a/internal/domains/api/services/processors/flxg/flxg7e8f_processor.go +++ b/internal/domains/api/services/processors/flxg/flxg7e8f_processor.go @@ -20,7 +20,7 @@ func ProcessFLXG7E8FRequest(ctx context.Context, params []byte, deps *processors if err := deps.Validator.ValidateStruct(paramsDto); err != nil { return nil, errors.Join(processors.ErrInvalidParam, err) } - if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "420116198907031413" { + if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "420116198907031413" || paramsDto.IDCard == "13032319930128263X" { return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空")) } diff --git a/internal/domains/api/services/processors/flxg/flxgca3d_processor.go b/internal/domains/api/services/processors/flxg/flxgca3d_processor.go index ab20c6c..1a4608f 100644 --- a/internal/domains/api/services/processors/flxg/flxgca3d_processor.go +++ b/internal/domains/api/services/processors/flxg/flxgca3d_processor.go @@ -20,7 +20,7 @@ func ProcessFLXGCA3DRequest(ctx context.Context, params []byte, deps *processors if err := deps.Validator.ValidateStruct(paramsDto); err != nil { return nil, errors.Join(processors.ErrInvalidParam, err) } - if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "420116198907031413" { + if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "420116198907031413" || paramsDto.IDCard == "13032319930128263X" { return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空")) } encryptedName, err := deps.WestDexService.Encrypt(paramsDto.Name) diff --git a/internal/domains/api/services/processors/flxg/flxgdea8_processor.go b/internal/domains/api/services/processors/flxg/flxgdea8_processor.go index b2c95f4..d3d48ff 100644 --- a/internal/domains/api/services/processors/flxg/flxgdea8_processor.go +++ b/internal/domains/api/services/processors/flxg/flxgdea8_processor.go @@ -21,7 +21,7 @@ func ProcessFLXGDEA8Request(ctx context.Context, params []byte, deps *processors return nil, errors.Join(processors.ErrInvalidParam, err) } // 去掉司法案件案件去掉身份证号码 - if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "420116198907031413" { + if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "420116198907031413" || paramsDto.IDCard == "13032319930128263X" { return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空")) } diff --git a/internal/domains/api/services/processors/flxg/flxgdea9_processor.go b/internal/domains/api/services/processors/flxg/flxgdea9_processor.go index 68da3c6..c9e5f29 100644 --- a/internal/domains/api/services/processors/flxg/flxgdea9_processor.go +++ b/internal/domains/api/services/processors/flxg/flxgdea9_processor.go @@ -25,7 +25,7 @@ func ProcessFLXGDEA9Request(ctx context.Context, params []byte, deps *processors if err != nil { return nil, errors.Join(processors.ErrSystem, err) } - if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "420116198907031413" { + if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998" || paramsDto.IDCard == "640102198708020925" || paramsDto.IDCard == "420624197310234034" || paramsDto.IDCard == "350104198501184416" || paramsDto.IDCard == "410521198606018056" || paramsDto.IDCard == "410482198504029333" || paramsDto.IDCard == "370982199012037272" || paramsDto.IDCard == "431027198810290730" || paramsDto.IDCard == "362502199510298017" || paramsDto.IDCard == "340826199008250378" || paramsDto.IDCard == "321027198304072129" || paramsDto.IDCard == "420116198907031413" || paramsDto.IDCard == "13032319930128263X" { return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空")) } encryptedIDCard, err := deps.ZhichaService.Encrypt(paramsDto.IDCard) From 43acbeb8f482be9feb36dc3bc666ba41a6234286 Mon Sep 17 00:00:00 2001 From: Mrx <18278715334@163.com> Date: Thu, 28 May 2026 10:55:28 +0800 Subject: [PATCH 28/28] f --- config.yaml | 34 +++ go.mod | 8 +- go.sum | 6 +- internal/config/config.go | 29 ++ internal/container/container.go | 5 + internal/domains/api/dto/api_request_dto.go | 5 + .../api/services/api_request_service.go | 7 +- .../api/services/form_config_service.go | 1 + .../api/services/processors/dependencies.go | 4 + .../processors/qcxg/qcxg4d2e_processor.go | 31 +-- .../processors/qcxg/qcxg5f3a_processor.go | 73 +++-- .../processors/qcxg/qcxg9p1c_processor.go | 92 ++++--- .../processors/qcxg/qcxgm4cl_processor.go | 48 ++++ .../infrastructure/external/nuoer/crypto.go | 38 +++ .../external/nuoer/crypto_test.go | 21 ++ .../external/nuoer/nuoer_errors.go | 141 ++++++++++ .../external/nuoer/nuoer_factory.go | 64 +++++ .../external/nuoer/nuoer_service.go | 253 ++++++++++++++++++ 18 files changed, 758 insertions(+), 102 deletions(-) create mode 100644 internal/domains/api/services/processors/qcxg/qcxgm4cl_processor.go create mode 100644 internal/infrastructure/external/nuoer/crypto.go create mode 100644 internal/infrastructure/external/nuoer/crypto_test.go create mode 100644 internal/infrastructure/external/nuoer/nuoer_errors.go create mode 100644 internal/infrastructure/external/nuoer/nuoer_factory.go create mode 100644 internal/infrastructure/external/nuoer/nuoer_service.go diff --git a/config.yaml b/config.yaml index 6399b64..d804bda 100644 --- a/config.yaml +++ b/config.yaml @@ -679,3 +679,37 @@ huibo: max_backups: 5 max_age: 30 compress: true + + + +# =========================================== +# 🌐 诺尔智汇配置 +# =========================================== +nuoer: + url: "https://api.enolfax.com/enol/api" + app_id: "t4qO2mR3" + app_secret: "d1515bf9ed2f2fe063b5f4f7e2c50f0ec65bfd58" + timeout: 4s + logging: + enabled: true + log_dir: "logs/external_services" + service_name: "nuoer" + use_daily: true + enable_level_separation: true + # 各级别配置 + level_configs: + info: + max_size: 100 + max_backups: 5 + max_age: 30 + compress: true + error: + max_size: 200 + max_backups: 10 + max_age: 90 + compress: true + warn: + max_size: 100 + max_backups: 5 + max_age: 30 + compress: true \ No newline at end of file diff --git a/go.mod b/go.mod index 5e480be..8dd8e10 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,8 @@ require ( github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13 github.com/alibabacloud-go/tea v1.3.13 github.com/aliyun/alibaba-cloud-sdk-go v1.63.107 + github.com/chromedp/cdproto v0.0.0-20250319231242-a755498943c8 + github.com/chromedp/chromedp v0.13.2 github.com/gin-contrib/cors v1.7.6 github.com/gin-gonic/gin v1.10.1 github.com/go-playground/locales v0.14.1 @@ -16,6 +18,7 @@ require ( github.com/google/uuid v1.6.0 github.com/hibiken/asynq v0.25.1 github.com/jung-kurt/gofpdf/v2 v2.17.3 + github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20260313013624-04e51e218220 github.com/prometheus/client_golang v1.22.0 github.com/qiniu/go-sdk/v7 v7.25.4 github.com/redis/go-redis/v9 v9.11.0 @@ -58,8 +61,6 @@ require ( github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/cenkalti/backoff/v5 v5.0.2 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/chromedp/cdproto v0.0.0-20250319231242-a755498943c8 // indirect - github.com/chromedp/chromedp v0.13.2 // indirect github.com/chromedp/sysutil v1.1.0 // indirect github.com/clbanning/mxj/v2 v2.7.0 // indirect github.com/cloudwego/base64x v0.1.5 // indirect @@ -94,15 +95,12 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/leodido/go-urn v1.4.0 // indirect - github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20260313013624-04e51e218220 // indirect github.com/mailru/easyjson v0.7.6 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect - github.com/oschwald/geoip2-golang v1.13.0 // indirect - github.com/oschwald/maxminddb-golang v1.13.0 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.6.1 // indirect diff --git a/go.sum b/go.sum index e9e4a0d..f4c970a 100644 --- a/go.sum +++ b/go.sum @@ -244,6 +244,7 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= @@ -268,10 +269,7 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A= github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU= -github.com/oschwald/geoip2-golang v1.13.0 h1:Q44/Ldc703pasJeP5V9+aFSZFmBN7DKHbNsSFzQATJI= -github.com/oschwald/geoip2-golang v1.13.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo= -github.com/oschwald/maxminddb-golang v1.13.0 h1:R8xBorY71s84yO06NgTmQvqvTvlS/bnYZrrWX1MElnU= -github.com/oschwald/maxminddb-golang v1.13.0/go.mod h1:BU0z8BfFVhi1LQaonTwwGQlsHUEu9pWNdMfmq4ztm0o= +github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= diff --git a/internal/config/config.go b/internal/config/config.go index 13e432e..e255e19 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -45,6 +45,7 @@ type Config struct { Shujubao ShujubaoConfig `mapstructure:"shujubao"` PDFGen PDFGenConfig `mapstructure:"pdfgen"` Huibo HuiboConfig `mapstructure:"huibo"` + Nuoer NuoerConfig `mapstructure:"nuoer"` } // ServerConfig HTTP服务器配置 @@ -705,6 +706,34 @@ type HuiboLevelFileConfig struct { Compress bool `mapstructure:"compress"` } +// NuoerConfig 诺尔智汇配置 +type NuoerConfig struct { + URL string `mapstructure:"url"` + AppID string `mapstructure:"app_id"` + AppSecret string `mapstructure:"app_secret"` + Timeout time.Duration `mapstructure:"timeout"` + + Logging NuoerLoggingConfig `mapstructure:"logging"` +} + +// NuoerLoggingConfig 诺尔智汇日志配置 +type NuoerLoggingConfig struct { + Enabled bool `mapstructure:"enabled"` + LogDir string `mapstructure:"log_dir"` + ServiceName string `mapstructure:"service_name"` + UseDaily bool `mapstructure:"use_daily"` + EnableLevelSeparation bool `mapstructure:"enable_level_separation"` + LevelConfigs map[string]NuoerLevelFileConfig `mapstructure:"level_configs"` +} + +// NuoerLevelFileConfig 诺尔智汇级别文件配置 +type NuoerLevelFileConfig struct { + MaxSize int `mapstructure:"max_size"` + MaxBackups int `mapstructure:"max_backups"` + MaxAge int `mapstructure:"max_age"` + Compress bool `mapstructure:"compress"` +} + // DomainConfig 域名配置 type DomainConfig struct { API string `mapstructure:"api"` // API域名 diff --git a/internal/container/container.go b/internal/container/container.go index 79e9f78..a0c99f8 100644 --- a/internal/container/container.go +++ b/internal/container/container.go @@ -45,6 +45,7 @@ import ( "tyapi-server/internal/infrastructure/external/huibo" "tyapi-server/internal/infrastructure/external/jiguang" "tyapi-server/internal/infrastructure/external/muzi" + "tyapi-server/internal/infrastructure/external/nuoer" "tyapi-server/internal/infrastructure/external/ocr" "tyapi-server/internal/infrastructure/external/shujubao" "tyapi-server/internal/infrastructure/external/shumai" @@ -405,6 +406,10 @@ func NewContainer() *Container { func(cfg *config.Config) (*shujubao.ShujubaoService, error) { return shujubao.NewShujubaoServiceWithConfig(cfg) }, + // NuoerService - 诺尔智汇服务 + func(cfg *config.Config) (*nuoer.NuoerService, error) { + return nuoer.NewNuoerServiceWithConfig(cfg) + }, func(cfg *config.Config) *yushan.YushanService { return yushan.NewYushanService( cfg.Yushan.URL, diff --git a/internal/domains/api/dto/api_request_dto.go b/internal/domains/api/dto/api_request_dto.go index dc71ff7..029b9c9 100644 --- a/internal/domains/api/dto/api_request_dto.go +++ b/internal/domains/api/dto/api_request_dto.go @@ -358,6 +358,11 @@ type QCXGGB2QReq struct { Name string `json:"name" validate:"required,min=1,validName"` CarPlateType string `json:"carplate_type" validate:"required"` } + +type QCXGM4CLReq struct { + IDCard string `json:"id_card" validate:"required,validIDCard"` +} + type QCXGJJ2AReq struct { VinCode string `json:"vin_code" validate:"required"` EngineNumber string `json:"engine_number" validate:"omitempty"` diff --git a/internal/domains/api/services/api_request_service.go b/internal/domains/api/services/api_request_service.go index 8cba7ba..e211349 100644 --- a/internal/domains/api/services/api_request_service.go +++ b/internal/domains/api/services/api_request_service.go @@ -24,6 +24,7 @@ import ( "tyapi-server/internal/infrastructure/external/huibo" "tyapi-server/internal/infrastructure/external/jiguang" "tyapi-server/internal/infrastructure/external/muzi" + "tyapi-server/internal/infrastructure/external/nuoer" "tyapi-server/internal/infrastructure/external/shujubao" "tyapi-server/internal/infrastructure/external/shumai" "tyapi-server/internal/infrastructure/external/tianyancha" @@ -68,6 +69,7 @@ func NewApiRequestService( jiguangService *jiguang.JiguangService, shumaiService *shumai.ShumaiService, huiboService *huibo.HuiboService, + nuoerService *nuoer.NuoerService, validator interfaces.RequestValidator, productManagementService *services.ProductManagementService, cfg *appconfig.Config, @@ -84,6 +86,7 @@ func NewApiRequestService( jiguangService, shumaiService, huiboService, + nuoerService, validator, productManagementService, cfg, @@ -105,6 +108,7 @@ func NewApiRequestServiceWithRepos( jiguangService *jiguang.JiguangService, shumaiService *shumai.ShumaiService, huiboService *huibo.HuiboService, + nuoerService *nuoer.NuoerService, validator interfaces.RequestValidator, productManagementService *services.ProductManagementService, cfg *appconfig.Config, @@ -132,6 +136,7 @@ func NewApiRequestServiceWithRepos( jiguangService, shumaiService, huiboService, + nuoerService, validator, combService, reportRepo, @@ -378,7 +383,7 @@ func registerAllProcessors(combService *comb.CombService) { "QCXG5U0Z": qcxg.ProcessQCXG5U0ZRequest, // 车辆静态信息查询 10479 "QCXGY7F2": qcxg.ProcessQCXGY7F2Request, // 二手车VIN估值 10443 "QCXG3M7Z": qcxg.ProcessQCXG3M7ZRequest, //人车关系核验(ETC)10093 月更 - + "QCXGM4CL": qcxg.ProcessQCXGM4CLRequest, //名下车辆诺尔 // DWBG系列处理器 - 多维报告 "DWBG6A2C": dwbg.ProcessDWBG6A2CRequest, "DWBG8B4D": dwbg.ProcessDWBG8B4DRequest, diff --git a/internal/domains/api/services/form_config_service.go b/internal/domains/api/services/form_config_service.go index 1384838..d3d458d 100644 --- a/internal/domains/api/services/form_config_service.go +++ b/internal/domains/api/services/form_config_service.go @@ -287,6 +287,7 @@ func (s *FormConfigServiceImpl) getDTOStruct(ctx context.Context, apiCode string "QYGL3YSB": &dto.QYGL3YSBReq{}, //企业三要素认证shumai "QYGL2YSB": &dto.QYGL2YSBReq{}, //企业二要素认证shumai "QYGLDG77": &dto.QYGLDG77Req{}, //企业对公打款认证shumai + "QCXGM4CL": &dto.QCXGM4CLReq{}, //名下车辆诺尔 } // 优先返回已配置的DTO diff --git a/internal/domains/api/services/processors/dependencies.go b/internal/domains/api/services/processors/dependencies.go index 1455e2e..39a8058 100644 --- a/internal/domains/api/services/processors/dependencies.go +++ b/internal/domains/api/services/processors/dependencies.go @@ -9,6 +9,7 @@ import ( "tyapi-server/internal/infrastructure/external/huibo" "tyapi-server/internal/infrastructure/external/jiguang" "tyapi-server/internal/infrastructure/external/muzi" + "tyapi-server/internal/infrastructure/external/nuoer" "tyapi-server/internal/infrastructure/external/shujubao" "tyapi-server/internal/infrastructure/external/shumai" "tyapi-server/internal/infrastructure/external/tianyancha" @@ -42,6 +43,7 @@ type ProcessorDependencies struct { JiguangService *jiguang.JiguangService ShumaiService *shumai.ShumaiService HuiboService *huibo.HuiboService + NuoerService *nuoer.NuoerService Validator interfaces.RequestValidator CombService CombServiceInterface // Changed to interface to break import cycle Options *commands.ApiCallOptions // 添加Options支持 @@ -70,6 +72,7 @@ func NewProcessorDependencies( jiguangService *jiguang.JiguangService, shumaiService *shumai.ShumaiService, huiboService *huibo.HuiboService, + nuoerService *nuoer.NuoerService, validator interfaces.RequestValidator, combService CombServiceInterface, // Changed to interface reportRepo repositories.ReportRepository, @@ -88,6 +91,7 @@ func NewProcessorDependencies( JiguangService: jiguangService, ShumaiService: shumaiService, HuiboService: huiboService, + NuoerService: nuoerService, Validator: validator, CombService: combService, Options: nil, // 初始化为nil,在调用时设置 diff --git a/internal/domains/api/services/processors/qcxg/qcxg4d2e_processor.go b/internal/domains/api/services/processors/qcxg/qcxg4d2e_processor.go index 84417d1..02c789a 100644 --- a/internal/domains/api/services/processors/qcxg/qcxg4d2e_processor.go +++ b/internal/domains/api/services/processors/qcxg/qcxg4d2e_processor.go @@ -7,10 +7,9 @@ import ( "tyapi-server/internal/domains/api/dto" "tyapi-server/internal/domains/api/services/processors" - "tyapi-server/internal/infrastructure/external/jiguang" ) -// ProcessQCXG4D2ERequest QCXG4D2E API处理方法 - 极光名下车辆数量查询 +// ProcessQCXG4D2ERequest QCXG4D2E API处理方法 - 名下车辆数量(委托诺尔 QCXGM4CL) func ProcessQCXG4D2ERequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { var paramsDto dto.QCXG4D2EReq if err := json.Unmarshal(params, ¶msDto); err != nil { @@ -21,27 +20,15 @@ func ProcessQCXG4D2ERequest(ctx context.Context, params []byte, deps *processors return nil, errors.Join(processors.ErrInvalidParam, err) } - // 构建请求参数 - reqData := map[string]interface{}{ - "idNum": paramsDto.IDCard, - "userType": paramsDto.UserType, - } - - // 调用极光API - // apiCode: vehicle-inquiry-under-name (用于请求头) - // apiPath: vehicle/inquiry-under-name (用于URL路径) - respBytes, err := deps.JiguangService.CallAPI(ctx, "vehicle-inquiry-under-name", "vehicle/inquiry-under-name", reqData) + m4clParams, err := json.Marshal(dto.QCXGM4CLReq{IDCard: paramsDto.IDCard}) if err != nil { - // 根据错误类型返回相应的错误 - if errors.Is(err, jiguang.ErrNotFound) { - return nil, errors.Join(processors.ErrNotFound, err) - } else if errors.Is(err, jiguang.ErrDatasource) { - return nil, errors.Join(processors.ErrDatasource, err) - } else { - return nil, errors.Join(processors.ErrSystem, err) - } + return nil, errors.Join(processors.ErrSystem, err) } - // 极光服务已经返回了 data 字段的 JSON,直接返回即可 - return respBytes, nil + raw, err := ProcessQCXGM4CLRequest(ctx, m4clParams, deps) + if err != nil { + return nil, err + } + + return transformQCXG5F3AResponse(raw) } diff --git a/internal/domains/api/services/processors/qcxg/qcxg5f3a_processor.go b/internal/domains/api/services/processors/qcxg/qcxg5f3a_processor.go index 830dab6..f7c1b9c 100644 --- a/internal/domains/api/services/processors/qcxg/qcxg5f3a_processor.go +++ b/internal/domains/api/services/processors/qcxg/qcxg5f3a_processor.go @@ -4,13 +4,15 @@ import ( "context" "encoding/json" "errors" + "strconv" "tyapi-server/internal/domains/api/dto" "tyapi-server/internal/domains/api/services/processors" - "tyapi-server/internal/infrastructure/external/jiguang" + + "github.com/tidwall/gjson" ) -// ProcessQCXG5F3ARequest QCXG5F3A API处理方法 - 极光名下车辆车牌查询 以替换数量 +// ProcessQCXG5F3ARequest QCXG5F3A API处理方法 - 名下车辆(委托诺尔 QCXGM4CL,响应格式兼容极光) func ProcessQCXG5F3ARequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { var paramsDto dto.QCXG5F3AReq if err := json.Unmarshal(params, ¶msDto); err != nil { @@ -21,28 +23,53 @@ func ProcessQCXG5F3ARequest(ctx context.Context, params []byte, deps *processors return nil, errors.Join(processors.ErrInvalidParam, err) } - // 构建请求参数 - reqData := map[string]interface{}{ - "idNum": paramsDto.IDCard, - "name": paramsDto.Name, - "userType": "1", - } - - // 调用极光API - // apiCode: vehicle-inquiry-under-name (用于请求头) - // apiPath: vehicle/inquiry-under-name (用于URL路径) - respBytes, err := deps.JiguangService.CallAPI(ctx, "vehicle-inquiry-under-name", "vehicle/inquiry-under-name", reqData) + m4clParams, err := json.Marshal(dto.QCXGM4CLReq{IDCard: paramsDto.IDCard}) if err != nil { - // 根据错误类型返回相应的错误 - if errors.Is(err, jiguang.ErrNotFound) { - return nil, errors.Join(processors.ErrNotFound, err) - } else if errors.Is(err, jiguang.ErrDatasource) { - return nil, errors.Join(processors.ErrDatasource, err) - } else { - return nil, errors.Join(processors.ErrSystem, err) - } + return nil, errors.Join(processors.ErrSystem, err) } - // 极光服务已经返回了 data 字段的 JSON,直接返回即可 - return respBytes, nil + raw, err := ProcessQCXGM4CLRequest(ctx, m4clParams, deps) + if err != nil { + return nil, err + } + + return transformQCXG5F3AResponse(raw) +} + +// transformQCXG5F3AResponse 将诺尔响应转为 QCXG5F3A 对外格式:去掉 busiCode/busiMsg,展开 result,vehicleCount 为字符串 +func transformQCXG5F3AResponse(raw []byte) ([]byte, error) { + base := gjson.GetBytes(raw, "result") + if !base.Exists() { + base = gjson.ParseBytes(raw) + } + + list := base.Get("list").Value() + if list == nil { + list = []interface{}{} + } + + countStr, err := formatVehicleCountAsString(base.Get("vehicleCount")) + if err != nil { + return nil, errors.Join(processors.ErrSystem, err) + } + + out := map[string]interface{}{ + "vehicleCount": countStr, + "list": list, + } + return json.Marshal(out) +} + +func formatVehicleCountAsString(v gjson.Result) (string, error) { + if !v.Exists() { + return "0", nil + } + switch v.Type { + case gjson.String: + return v.String(), nil + case gjson.Number: + return strconv.FormatInt(v.Int(), 10), nil + default: + return "", errors.New("vehicleCount 类型无效") + } } diff --git a/internal/domains/api/services/processors/qcxg/qcxg9p1c_processor.go b/internal/domains/api/services/processors/qcxg/qcxg9p1c_processor.go index 3f18c40..4b9f7ab 100644 --- a/internal/domains/api/services/processors/qcxg/qcxg9p1c_processor.go +++ b/internal/domains/api/services/processors/qcxg/qcxg9p1c_processor.go @@ -8,12 +8,11 @@ import ( "tyapi-server/internal/domains/api/dto" "tyapi-server/internal/domains/api/services/processors" - "tyapi-server/internal/infrastructure/external/jiguang" "github.com/tidwall/gjson" ) -// ProcessQCXG9P1CRequest QCXG9P1C API处理方法 兼容旧版 极光名下车牌查询数量 +// ProcessQCXG9P1CRequest QCXG9P1C API处理方法 - 名下车辆详版(委托诺尔 QCXGM4CL) func ProcessQCXG9P1CRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { var paramsDto dto.QCXG9P1CReq if err := json.Unmarshal(params, ¶msDto); err != nil { @@ -24,54 +23,53 @@ func ProcessQCXG9P1CRequest(ctx context.Context, params []byte, deps *processors return nil, errors.Join(processors.ErrInvalidParam, err) } - null := "" - // 构建请求参数 - reqData := map[string]interface{}{ - "idNum": paramsDto.IDCard, - "name": null, - "userType": "1", - } - - // 调用极光API - // apiCode: vehicle-inquiry-under-name (用于请求头) - // apiPath: vehicle/inquiry-under-name (用于URL路径) - respBytes, err := deps.JiguangService.CallAPI(ctx, "vehicle-inquiry-under-name", "vehicle/inquiry-under-name", reqData) + m4clParams, err := json.Marshal(dto.QCXGM4CLReq{IDCard: paramsDto.IDCard}) if err != nil { - // 根据错误类型返回相应的错误 - if errors.Is(err, jiguang.ErrNotFound) { - return nil, errors.Join(processors.ErrNotFound, err) - } else if errors.Is(err, jiguang.ErrDatasource) { - return nil, errors.Join(processors.ErrDatasource, err) - } else { - return nil, errors.Join(processors.ErrSystem, err) - } + return nil, errors.Join(processors.ErrSystem, err) } - // 极光服务已经返回了 data 字段的 JSON,直接返回即可 - // return respBytes, nil - - // 使用 gjson 检查并转换 vehicleCount 字段 - vehicleCountResult := gjson.GetBytes(respBytes, "vehicleCount") - if vehicleCountResult.Exists() && vehicleCountResult.Type == gjson.String { - // 如果是字符串类型,转换为整数 - vehicleCountInt, err := strconv.Atoi(vehicleCountResult.String()) - if err != nil { - return nil, errors.Join(processors.ErrSystem, err) - } - // 解析 JSON 并修改 vehicleCount 字段 - var respData map[string]interface{} - if err := json.Unmarshal(respBytes, &respData); err != nil { - return nil, errors.Join(processors.ErrSystem, err) - } - respData["vehicleCount"] = vehicleCountInt - // 重新序列化为JSON并返回 - resultBytes, err := json.Marshal(respData) - if err != nil { - return nil, errors.Join(processors.ErrSystem, err) - } - return resultBytes, nil + raw, err := ProcessQCXGM4CLRequest(ctx, m4clParams, deps) + if err != nil { + return nil, err } - // 如果 vehicleCount 不存在或不是字符串,直接返回原始响应 - return respBytes, nil + return transformQCXG9P1CResponse(raw) +} + +// transformQCXG9P1CResponse 将诺尔响应转为 QCXG9P1C 对外格式:去掉 busiCode/busiMsg,展开 result,vehicleCount 为整数 +func transformQCXG9P1CResponse(raw []byte) ([]byte, error) { + base := gjson.GetBytes(raw, "result") + if !base.Exists() { + base = gjson.ParseBytes(raw) + } + + list := base.Get("list").Value() + if list == nil { + list = []interface{}{} + } + + countInt, err := formatVehicleCountAsInt(base.Get("vehicleCount")) + if err != nil { + return nil, errors.Join(processors.ErrSystem, err) + } + + out := map[string]interface{}{ + "vehicleCount": countInt, + "list": list, + } + return json.Marshal(out) +} + +func formatVehicleCountAsInt(v gjson.Result) (int, error) { + if !v.Exists() { + return 0, nil + } + switch v.Type { + case gjson.String: + return strconv.Atoi(v.String()) + case gjson.Number: + return int(v.Int()), nil + default: + return 0, errors.New("vehicleCount 类型无效") + } } diff --git a/internal/domains/api/services/processors/qcxg/qcxgm4cl_processor.go b/internal/domains/api/services/processors/qcxg/qcxgm4cl_processor.go new file mode 100644 index 0000000..9be2ee1 --- /dev/null +++ b/internal/domains/api/services/processors/qcxg/qcxgm4cl_processor.go @@ -0,0 +1,48 @@ +package qcxg + +import ( + "context" + "encoding/json" + "errors" + + "tyapi-server/internal/domains/api/dto" + "tyapi-server/internal/domains/api/services/processors" + "tyapi-server/internal/infrastructure/external/nuoer" +) + +// ProcessQCXGM4CLRequest QCXGM4CL API处理方法 - 名下车辆诺尔 +func ProcessQCXGM4CLRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { + var paramsDto dto.QCXGM4CLReq + if err := json.Unmarshal(params, ¶msDto); err != nil { + return nil, errors.Join(processors.ErrSystem, err) + } + + if err := deps.Validator.ValidateStruct(paramsDto); err != nil { + return nil, errors.Join(processors.ErrInvalidParam, err) + } + + body := map[string]string{ + "idCard": paramsDto.IDCard, + } + + nuoerDoCheckAPIKey := "id_vehicle_query_102" + ApiPath := "/v1/doCheck" + + resp, err := deps.NuoerService.CallAPI(ctx, nuoerDoCheckAPIKey, ApiPath, body) + if err != nil { + if errors.Is(err, nuoer.ErrNotFound) { + return nil, errors.Join(processors.ErrNotFound, err) + } + if errors.Is(err, nuoer.ErrDatasource) { + return nil, errors.Join(processors.ErrDatasource, err) + } + return nil, errors.Join(processors.ErrSystem, err) + } + + respBytes, err := json.Marshal(resp.Data) + if err != nil { + return nil, errors.Join(processors.ErrSystem, err) + } + + return respBytes, nil +} diff --git a/internal/infrastructure/external/nuoer/crypto.go b/internal/infrastructure/external/nuoer/crypto.go new file mode 100644 index 0000000..7fb1398 --- /dev/null +++ b/internal/infrastructure/external/nuoer/crypto.go @@ -0,0 +1,38 @@ +package nuoer + +import ( + "crypto/md5" + "encoding/hex" + "sort" + "strings" +) + +// Sign 根据 body 业务参数与 secret 生成 MD5 签名。 +// 规则:排除空值参数,按 key 的 ASCII 升序排序,拼接「参数名+参数值」后追加 secret,再 MD5(小写十六进制)。 +func Sign(body map[string]string, secret string) string { + if len(body) == 0 { + return genMD5(secret) + } + + keys := make([]string, 0, len(body)) + for k, v := range body { + if strings.TrimSpace(v) == "" { + continue + } + keys = append(keys, k) + } + sort.Strings(keys) + + var sb strings.Builder + for _, k := range keys { + sb.WriteString(k) + sb.WriteString(body[k]) + } + sb.WriteString(secret) + return genMD5(sb.String()) +} + +func genMD5(s string) string { + sum := md5.Sum([]byte(s)) + return hex.EncodeToString(sum[:]) +} diff --git a/internal/infrastructure/external/nuoer/crypto_test.go b/internal/infrastructure/external/nuoer/crypto_test.go new file mode 100644 index 0000000..bd101c8 --- /dev/null +++ b/internal/infrastructure/external/nuoer/crypto_test.go @@ -0,0 +1,21 @@ +package nuoer + +import "testing" + +func TestSign(t *testing.T) { + body := map[string]string{ + "name": "张三", + "mobile": "13290879000", + "idCard": "330129199511153412", + } + secret := "secret" + got := Sign(body, secret) + if got == "" { + t.Fatal("sign should not be empty") + } + // 文档示例:name张三mobile13290879000idCard330129199511153412secret + want := genMD5("idCard330129199511153412mobile13290879000name张三secret") + if got != want { + t.Fatalf("sign mismatch: got %s want %s", got, want) + } +} diff --git a/internal/infrastructure/external/nuoer/nuoer_errors.go b/internal/infrastructure/external/nuoer/nuoer_errors.go new file mode 100644 index 0000000..f77ed6b --- /dev/null +++ b/internal/infrastructure/external/nuoer/nuoer_errors.go @@ -0,0 +1,141 @@ +package nuoer + +import ( + "errors" + "fmt" +) + +// 平台层 code 返回码(见文档2) +const ( + CodeSuccess = 0 // 成功 + CodeResponseError = -1 // 响应异常 +) + +// 业务层 busiCode 返回码(见文档2) +const ( + BusiCodeSuccess = 10 // 查询成功【计费】 + BusiCodeNotFound = 1000 // 数据未查得 + BusiCodeInsufficientFund = 1001 // 账户余额不足 + BusiCodeAccountNotFound = 1002 // 账户信息不存在 + BusiCodeAppIDError = 1003 // appId异常 + BusiCodeProductError = 1004 // 产品编号异常 + BusiCodeAccountError = 1005 // 账号信息异常 + BusiCodeOverdraftLimit = 1006 // 透支余额已达上限 + BusiCodeDataRequestError = 1007 // 数据请求异常 + BusiCodeServiceNotOpen = 1009 // 服务尚未开通 +) + +var ( + ErrDatasource = errors.New("数据源异常") + ErrSystem = errors.New("系统异常") + ErrNotFound = errors.New("查询为空") +) + +// platformCodeDesc 平台层 code -> 描述 +var platformCodeDesc = map[int]string{ + CodeSuccess: "成功", + CodeResponseError: "响应异常", +} + +// busiCodeDesc 业务层 busiCode -> 描述 +var busiCodeDesc = map[int]string{ + BusiCodeSuccess: "查询成功【计费】", + BusiCodeNotFound: "数据未查得", + BusiCodeInsufficientFund: "账户余额不足", + BusiCodeAccountNotFound: "账户信息不存在", + BusiCodeAppIDError: "appId异常", + BusiCodeProductError: "产品编号异常", + BusiCodeAccountError: "账号信息异常", + BusiCodeOverdraftLimit: "透支余额已达上限", + BusiCodeDataRequestError: "数据请求异常", + BusiCodeServiceNotOpen: "服务尚未开通", +} + +// GetPlatformCodeDesc 根据平台 code 获取描述 +func GetPlatformCodeDesc(code int) string { + if desc, ok := platformCodeDesc[code]; ok { + return desc + } + return "" +} + +// GetBusiCodeDesc 根据 busiCode 获取描述 +func GetBusiCodeDesc(busiCode int) string { + if desc, ok := busiCodeDesc[busiCode]; ok { + return desc + } + return "" +} + +// nuoerError 诺尔智汇平台层错误(响应 code 字段) +type nuoerError struct { + Code int + Message string +} + +func (e *nuoerError) Error() string { + return fmt.Sprintf("诺尔智汇返回错误,code: %d,msg: %s", e.Code, e.Message) +} + +// NewNuoerError 创建平台层错误 +func NewNuoerError(code int, message string) *nuoerError { + if message == "" { + if desc := GetPlatformCodeDesc(code); desc != "" { + message = desc + } else { + message = "诺尔智汇返回未知错误" + } + } + return &nuoerError{Code: code, Message: message} +} + +// nuoerBusiError 诺尔智汇业务层错误(data.busiCode 字段) +type nuoerBusiError struct { + BusiCode int + BusiMsg string +} + +func (e *nuoerBusiError) Error() string { + return fmt.Sprintf("诺尔智汇业务错误,busiCode: %d,busiMsg: %s", e.BusiCode, e.BusiMsg) +} + +// NewNuoerBusiError 创建业务层错误 +func NewNuoerBusiError(busiCode int, busiMsg string) *nuoerBusiError { + if busiMsg == "" { + if desc := GetBusiCodeDesc(busiCode); desc != "" { + busiMsg = desc + } else { + busiMsg = "诺尔智汇业务返回未知错误" + } + } + return &nuoerBusiError{BusiCode: busiCode, BusiMsg: busiMsg} +} + +// GetNotFoundErrByBusiCode 将 busiCode 映射为「查询为空」类错误(不扣费场景) +func GetNotFoundErrByBusiCode(busiCode int) error { + switch busiCode { + case BusiCodeNotFound: + return ErrNotFound + default: + return nil + } +} + +// GetErrByBusiCode 将 busiCode 映射为内部哨兵错误,供处理器 errors.Is 判断 +func GetErrByBusiCode(busiCode int) error { + if busiCode == BusiCodeSuccess { + return nil + } + if notFound := GetNotFoundErrByBusiCode(busiCode); notFound != nil { + return notFound + } + return ErrDatasource +} + +// GetErrByPlatformCode 将平台 code 映射为内部哨兵错误 +func GetErrByPlatformCode(code int) error { + if code == CodeSuccess { + return nil + } + return ErrDatasource +} diff --git a/internal/infrastructure/external/nuoer/nuoer_factory.go b/internal/infrastructure/external/nuoer/nuoer_factory.go new file mode 100644 index 0000000..27e97c4 --- /dev/null +++ b/internal/infrastructure/external/nuoer/nuoer_factory.go @@ -0,0 +1,64 @@ +package nuoer + +import ( + "time" + + "tyapi-server/internal/config" + "tyapi-server/internal/shared/external_logger" +) + +// NewNuoerServiceWithConfig 使用配置创建诺尔智汇服务 +func NewNuoerServiceWithConfig(cfg *config.Config) (*NuoerService, error) { + loggingConfig := external_logger.ExternalServiceLoggingConfig{ + Enabled: cfg.Nuoer.Logging.Enabled, + LogDir: cfg.Nuoer.Logging.LogDir, + ServiceName: "nuoer", + UseDaily: cfg.Nuoer.Logging.UseDaily, + EnableLevelSeparation: cfg.Nuoer.Logging.EnableLevelSeparation, + LevelConfigs: make(map[string]external_logger.ExternalServiceLevelFileConfig), + } + + for level, levelCfg := range cfg.Nuoer.Logging.LevelConfigs { + loggingConfig.LevelConfigs[level] = external_logger.ExternalServiceLevelFileConfig{ + MaxSize: levelCfg.MaxSize, + MaxBackups: levelCfg.MaxBackups, + MaxAge: levelCfg.MaxAge, + Compress: levelCfg.Compress, + } + } + + logger, err := external_logger.NewExternalServiceLogger(loggingConfig) + if err != nil { + return nil, err + } + + timeout := cfg.Nuoer.Timeout + if timeout <= 0 { + timeout = defaultRequestTimeout + } + + return NewNuoerService( + cfg.Nuoer.URL, + cfg.Nuoer.AppID, + cfg.Nuoer.AppSecret, + timeout, + logger, + ), nil +} + +// NewNuoerServiceWithLogging 使用自定义日志配置创建诺尔智汇服务 +func NewNuoerServiceWithLogging(url, appID, appSecret string, timeout time.Duration, loggingConfig external_logger.ExternalServiceLoggingConfig) (*NuoerService, error) { + loggingConfig.ServiceName = "nuoer" + + logger, err := external_logger.NewExternalServiceLogger(loggingConfig) + if err != nil { + return nil, err + } + + return NewNuoerService(url, appID, appSecret, timeout, logger), nil +} + +// NewNuoerServiceSimple 创建无日志的诺尔智汇服务 +func NewNuoerServiceSimple(url, appID, appSecret string, timeout time.Duration) *NuoerService { + return NewNuoerService(url, appID, appSecret, timeout, nil) +} diff --git a/internal/infrastructure/external/nuoer/nuoer_service.go b/internal/infrastructure/external/nuoer/nuoer_service.go new file mode 100644 index 0000000..83626f3 --- /dev/null +++ b/internal/infrastructure/external/nuoer/nuoer_service.go @@ -0,0 +1,253 @@ +package nuoer + +import ( + "bytes" + "context" + "crypto/md5" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strings" + "time" + + "tyapi-server/internal/shared/external_logger" +) + +const defaultRequestTimeout = 4 * time.Second + +// nuoerResponse 诺尔智汇通用响应 +type nuoerResponse struct { + Code int `json:"code"` + Msg string `json:"msg"` + SeqNo string `json:"seqNo"` + Data interface{} `json:"data"` +} + +// serviceConfig 诺尔智汇服务运行时配置 +type serviceConfig struct { + URL string + AppID string + AppSecret string + Timeout time.Duration +} + +// NuoerService 诺尔智汇服务 +type NuoerService struct { + config serviceConfig + logger *external_logger.ExternalServiceLogger +} + +// NewNuoerService 创建诺尔智汇服务实例 +func NewNuoerService(url, appID, appSecret string, timeout time.Duration, logger *external_logger.ExternalServiceLogger) *NuoerService { + if timeout <= 0 { + timeout = defaultRequestTimeout + } + return &NuoerService{ + config: serviceConfig{ + URL: url, + AppID: appID, + AppSecret: appSecret, + Timeout: timeout, + }, + logger: logger, + } +} + +func (s *NuoerService) generateRequestID() string { + timestamp := time.Now().UnixNano() + hash := md5.Sum([]byte(fmt.Sprintf("%d_%s", timestamp, s.config.AppID))) + return fmt.Sprintf("nuoer_%x", hash[:8]) +} + +func (s *NuoerService) CallAPI(ctx context.Context, apiKey, apiPath string, body map[string]string) (*nuoerResponse, error) { + requestURL := strings.TrimSuffix(s.config.URL, "/") + if apiPath != "" { + if !strings.HasPrefix(apiPath, "/") { + apiPath = "/" + apiPath + } + requestURL += apiPath + } + + requestID := s.generateRequestID() + startTime := time.Now() + + var transactionID string + if id, ok := ctx.Value("transaction_id").(string); ok { + transactionID = id + } + + // 对调用方传入的 body 全量参与加签(排除空值,按 key 升序,见 Sign) + sign := Sign(body, s.config.AppSecret) + + requestPayload := map[string]interface{}{ + "appId": s.config.AppID, + "sign": sign, + "apiKey": apiKey, + "body": body, + } + + if s.logger != nil { + s.logger.LogRequest(requestID, transactionID, apiKey, requestURL) + } + + bodyBytes, err := json.Marshal(requestPayload) + if err != nil { + err = errors.Join(ErrSystem, err) + if s.logger != nil { + s.logger.LogError(requestID, transactionID, apiKey, err, requestPayload) + } + return nil, err + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, requestURL, bytes.NewBuffer(bodyBytes)) + if err != nil { + err = errors.Join(ErrSystem, err) + if s.logger != nil { + s.logger.LogError(requestID, transactionID, apiKey, err, requestPayload) + } + return nil, err + } + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{Timeout: s.config.Timeout} + resp, err := client.Do(req) + if err != nil { + err = wrapHTTPError(err) + if s.logger != nil { + s.logger.LogError(requestID, transactionID, apiKey, err, requestPayload) + } + return nil, err + } + defer resp.Body.Close() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + err = errors.Join(ErrSystem, err) + if s.logger != nil { + s.logger.LogError(requestID, transactionID, apiKey, err, requestPayload) + } + return nil, err + } + + if s.logger != nil { + s.logger.LogResponse(requestID, transactionID, apiKey, resp.StatusCode, time.Since(startTime)) + } + + if resp.StatusCode != http.StatusOK { + err = errors.Join(ErrDatasource, fmt.Errorf("HTTP状态码 %d", resp.StatusCode)) + if s.logger != nil { + s.logger.LogError(requestID, transactionID, apiKey, err, requestPayload) + } + return nil, err + } + + var nuoerResp nuoerResponse + if err := json.Unmarshal(respBody, &nuoerResp); err != nil { + err = errors.Join(ErrSystem, fmt.Errorf("响应解析失败: %w", err)) + if s.logger != nil { + s.logger.LogError(requestID, transactionID, apiKey, err, requestPayload) + } + return nil, err + } + + if nuoerResp.Code != CodeSuccess { + nuoerErr := NewNuoerError(nuoerResp.Code, nuoerResp.Msg) + err = errors.Join(GetErrByPlatformCode(nuoerResp.Code), nuoerErr) + if s.logger != nil { + s.logger.LogError(requestID, transactionID, apiKey, nuoerErr, requestPayload) + } + return nil, err + } + + if nuoerResp.Data == nil { + err = errors.Join(ErrSystem, errors.New("响应 data 为空")) + if s.logger != nil { + s.logger.LogError(requestID, transactionID, apiKey, err, requestPayload) + } + return nil, err + } + + busiCode, busiMsg, ok := parseDataBusiInfo(nuoerResp.Data) + if !ok { + err = errors.Join(ErrSystem, errors.New("响应 data 无法解析 busiCode")) + if s.logger != nil { + s.logger.LogError(requestID, transactionID, apiKey, err, requestPayload) + } + return nil, err + } + + if busiCode != BusiCodeSuccess { + busiErr := NewNuoerBusiError(busiCode, busiMsg) + err = errors.Join(GetErrByBusiCode(busiCode), busiErr) + if s.logger != nil { + s.logger.LogError(requestID, transactionID, apiKey, busiErr, requestPayload) + } + return nil, err + } + + cleanedData, err := stripBusiMetaFromData(nuoerResp.Data) + if err != nil { + err = errors.Join(ErrSystem, fmt.Errorf("响应 data 清理失败: %w", err)) + if s.logger != nil { + s.logger.LogError(requestID, transactionID, apiKey, err, requestPayload) + } + return nil, err + } + nuoerResp.Data = cleanedData + + return &nuoerResp, nil +} + +// nuoerDataBusiMeta 业务层状态字段,仅用于解析校验,不对外返回 +type nuoerDataBusiMeta struct { + BusiCode int `json:"busiCode"` + BusiMsg string `json:"busiMsg"` +} + +// parseDataBusiInfo 从各接口不同的 data 结构中解析 busiCode、busiMsg +func parseDataBusiInfo(data interface{}) (busiCode int, busiMsg string, ok bool) { + if data == nil { + return 0, "", false + } + raw, err := json.Marshal(data) + if err != nil { + return 0, "", false + } + var meta nuoerDataBusiMeta + if err := json.Unmarshal(raw, &meta); err != nil { + return 0, "", false + } + return meta.BusiCode, meta.BusiMsg, true +} + +// stripBusiMetaFromData 去掉 data 中的 busiCode、busiMsg,仅保留业务载荷 +func stripBusiMetaFromData(data interface{}) (interface{}, error) { + raw, err := json.Marshal(data) + if err != nil { + return nil, err + } + var payload map[string]interface{} + if err := json.Unmarshal(raw, &payload); err != nil { + return nil, err + } + delete(payload, "busiCode") + delete(payload, "busiMsg") + return payload, nil +} + +func wrapHTTPError(err error) error { + if err == context.DeadlineExceeded { + return errors.Join(ErrDatasource, err) + } + if netErr, ok := err.(interface{ Timeout() bool }); ok && netErr.Timeout() { + return errors.Join(ErrDatasource, err) + } + switch err.Error() { + case "context deadline exceeded", "timeout", "Client.Timeout exceeded", "net/http: request canceled": + return errors.Join(ErrDatasource, err) + default: + return errors.Join(ErrSystem, err) + } +}