From c3b0de6ac5527c9ec704e9089d25066522fa7ee3 Mon Sep 17 00:00:00 2001 From: liangzai <2440983361@qq.com> Date: Mon, 27 Jul 2026 00:03:07 +0800 Subject: [PATCH] f --- .../certification/already_authorized_test.go | 33 +++++- .../certification_application_service_impl.go | 111 ++++++++++++++---- .../certification/platform_flow.go | 11 +- .../user/services/user_aggregate_service.go | 53 +++++++-- internal/shared/fadada/corp_service.go | 4 +- internal/shared/fadada/errors.go | 89 ++++++++++++++ internal/shared/fadada/errors_test.go | 28 +++++ 7 files changed, 281 insertions(+), 48 deletions(-) create mode 100644 internal/shared/fadada/errors.go create mode 100644 internal/shared/fadada/errors_test.go diff --git a/internal/application/certification/already_authorized_test.go b/internal/application/certification/already_authorized_test.go index 27abc87..ca50873 100644 --- a/internal/application/certification/already_authorized_test.go +++ b/internal/application/certification/already_authorized_test.go @@ -2,7 +2,10 @@ package certification import ( "errors" + "fmt" "testing" + + "tyapi-server/internal/shared/fadada" ) func TestIsEnterpriseAlreadyAuthorizedErr(t *testing.T) { @@ -12,18 +15,38 @@ func TestIsEnterpriseAlreadyAuthorizedErr(t *testing.T) { want bool }{ { - name: "fadada 210002", + name: "structured 210002", + err: fadada.NewAPIError("获取法大大企业认证链接失败", fadada.CodeAuthScopeDuplicate, "授权范围重复获取", "abc"), + want: true, + }, + { + name: "structured 210013", + err: fadada.NewAPIError("获取法大大企业认证链接失败", fadada.CodeAuthScopeDuplicateCorp, "授权范围重复获取", "def"), + want: true, + }, + { + name: "legacy string 210002", err: errors.New("获取法大大企业认证链接失败: code=210002 msg=该企业已授权,无需重复操作 requestId=abc"), want: true, }, { - name: "exist authorize wording", - err: errors.New("当前应用已存在授权"), + name: "legacy string 210013", + err: errors.New("获取法大大企业认证链接失败: code=210013 msg=授权范围重复获取 requestId=xyz"), want: true, }, { - name: "other error", - err: errors.New("获取法大大企业认证链接失败: code=100011 msg=参数错误"), + name: "wrapped structured 210002", + err: fmt.Errorf("生成企业认证链接失败: %w", fadada.NewAPIError("获取法大大企业认证链接失败", fadada.CodeAuthScopeDuplicate, "授权范围重复获取", "rid")), + want: true, + }, + { + name: "msg only should not match", + err: errors.New("当前应用已存在授权"), + want: false, + }, + { + name: "other error 100011", + err: fadada.NewAPIError("获取法大大企业认证链接失败", fadada.CodeParamIllegal, "请求参数非法", "rid"), want: false, }, { diff --git a/internal/application/certification/certification_application_service_impl.go b/internal/application/certification/certification_application_service_impl.go index 33486a7..0047fb2 100644 --- a/internal/application/certification/certification_application_service_impl.go +++ b/internal/application/certification/certification_application_service_impl.go @@ -30,6 +30,7 @@ import ( "tyapi-server/internal/infrastructure/external/storage" "tyapi-server/internal/shared/database" "tyapi-server/internal/shared/esign" + "tyapi-server/internal/shared/fadada" sharedOCR "tyapi-server/internal/shared/ocr" "github.com/shopspring/decimal" @@ -171,6 +172,9 @@ func (s *CertificationApplicationServiceImpl) SubmitEnterpriseInfo( } } + // 企业名半角括号统一为全角,避免与工商登记不一致 + cmd.CompanyName = normalizeCompanyNameParentheses(cmd.CompanyName) + // 1.5 插入企业信息提交记录(包含扩展字段) record := entities.NewEnterpriseInfoSubmitRecord( cmd.UserID, @@ -557,9 +561,14 @@ func (s *CertificationApplicationServiceImpl) ApplyContract( return nil, err } ei := enterpriseInfo.EnterpriseInfo + submitRec, _ := s.enterpriseInfoSubmitRecordRepo.FindLatestByUserID(ctx, cmd.UserID) + _, transactorMobile, _ := pickTransactorFromRecord(submitRec) + if transactorMobile == "" { + transactorMobile = ei.LegalPersonPhone + } urlRes, err := provider.GetActorSignURL(ctx, &ports.GetActorSignURLRequest{ SignFlowID: flowID, - TransactorMobile: ei.LegalPersonPhone, + TransactorMobile: transactorMobile, PartyAName: ei.CompanyName, }) if err != nil { @@ -635,9 +644,14 @@ func (s *CertificationApplicationServiceImpl) RefreshContractSignURL( return nil, fmt.Errorf("获取企业信息失败: %w", err) } ei := enterpriseInfo.EnterpriseInfo + submitRec, _ := s.enterpriseInfoSubmitRecordRepo.FindLatestByUserID(ctx, userID) + _, transactorMobile, _ := pickTransactorFromRecord(submitRec) + if transactorMobile == "" { + transactorMobile = ei.LegalPersonPhone + } urlRes, err := provider.GetActorSignURL(ctx, &ports.GetActorSignURLRequest{ SignFlowID: cert.EsignFlowID, - TransactorMobile: ei.LegalPersonPhone, + TransactorMobile: transactorMobile, PartyAName: ei.CompanyName, }) if err != nil { @@ -753,9 +767,24 @@ func (s *CertificationApplicationServiceImpl) GetCertification( } if cert.Status == enums.StatusInfoSubmitted { - err = s.checkAndCompleteEnterpriseVerification(ctx, cert) - if err != nil { - return nil, err + if autoErr := s.checkAndCompleteEnterpriseVerification(ctx, cert); autoErr != nil { + // 自动推进失败时仍返回当前认证详情,避免前端整页报错无法展示步骤 + s.logger.Error("获取详情时自动完成企业认证失败,返回当前状态", + zap.String("user_id", query.UserID), + zap.String("cert_status", string(cert.Status)), + zap.Error(autoErr)) + fresh, loadErr := s.aggregateService.LoadCertificationByUserID(ctx, query.UserID) + if loadErr != nil { + return nil, fmt.Errorf("加载认证信息失败: %w", loadErr) + } + cert = fresh + response := s.convertToResponse(cert) + meta, metaErr := s.AddStatusMetadata(ctx, cert) + if metaErr == nil && meta != nil { + response.Metadata = meta + response.Metadata["auto_complete_error"] = autoErr.Error() + } + return response, nil } } if cert.Status == enums.StatusContractApplied { @@ -1407,16 +1436,11 @@ func isEnterpriseAlreadyRealnamedErr(err error) bool { return strings.Contains(msg, "企业用户已实名") || strings.Contains(msg, "已实名") } -// isEnterpriseAlreadyAuthorizedErr 法大大等平台:企业已对本应用授权,无需再获取授权链接(如 code=210002) +// isEnterpriseAlreadyAuthorizedErr 法大大:授权范围重复获取,无需再获取授权链接。 +// 官方错误码说明要求按 code 判断(210002 / 210013),勿依赖 msg 文案。 +// 文档:https://dev.fadada.com/api-doc/JOQQIJXLFL/H4F0HSKMHQ3HPIIM/5-1 func isEnterpriseAlreadyAuthorizedErr(err error) bool { - if err == nil { - return false - } - msg := err.Error() - return strings.Contains(msg, "210002") || - strings.Contains(msg, "该企业已授权") || - strings.Contains(msg, "无需重复操作") || - strings.Contains(msg, "已存在授权") + return fadada.IsAuthAlreadyGranted(err) } func firstNonEmptyStr(values ...string) string { @@ -1465,6 +1489,36 @@ func pickAuthorizedRepName(record *entities.EnterpriseInfoSubmitRecord, legalPer return legalPersonName } +// normalizeCompanyNameParentheses 将企业名中的半角括号 () 转为全角 () +func normalizeCompanyNameParentheses(name string) string { + name = strings.TrimSpace(name) + if name == "" { + return name + } + replacer := strings.NewReplacer("(", "(", ")", ")") + return replacer.Replace(name) +} + +// pickTransactorFromRecord 法大大经办人:优先授权代表,缺省回退法定代表人 +func pickTransactorFromRecord(record *entities.EnterpriseInfoSubmitRecord) (name, mobile, idNo string) { + if record == nil { + return "", "", "" + } + name = strings.TrimSpace(record.AuthorizedRepName) + mobile = strings.TrimSpace(record.AuthorizedRepPhone) + idNo = strings.TrimSpace(record.AuthorizedRepID) + if name == "" { + name = strings.TrimSpace(record.LegalPersonName) + } + if mobile == "" { + mobile = strings.TrimSpace(record.LegalPersonPhone) + } + if idNo == "" { + idNo = strings.TrimSpace(record.LegalPersonID) + } + return name, mobile, idNo +} + // pickEnterpriseString 优先用户域企业表字段,为空则用最近一次认证提交记录(避免 enterprise_infos 未同步导致合同控件无值) func pickEnterpriseString(primary string, record *entities.EnterpriseInfoSubmitRecord, fromRecord func(*entities.EnterpriseInfoSubmitRecord) string) string { if strings.TrimSpace(primary) != "" { @@ -1491,14 +1545,24 @@ func (s *CertificationApplicationServiceImpl) generateContractAndSignURL(ctx con address = record.EnterpriseAddress } } + transactorName, transactorMobile, transactorID := pickTransactorFromRecord(record) + if transactorName == "" { + transactorName = enterpriseInfo.LegalPersonName + } + if transactorMobile == "" { + transactorMobile = enterpriseInfo.LegalPersonPhone + } + if transactorID == "" { + transactorID = enterpriseInfo.LegalPersonID + } req := &ports.SignFlowCreateRequest{ Subject: s.contractSubjectPrefix(cert) + "-" + enterpriseInfo.CompanyName, FileID: cert.ContractFileID, PartyAName: enterpriseInfo.CompanyName, PartyAUSCC: enterpriseInfo.UnifiedSocialCode, - TransactorName: enterpriseInfo.LegalPersonName, - TransactorMobile: enterpriseInfo.LegalPersonPhone, - TransactorID: enterpriseInfo.LegalPersonID, + TransactorName: transactorName, + TransactorMobile: transactorMobile, + TransactorID: transactorID, TransReferenceID: cert.ID, Fill: ensureContractFill(cert, enterpriseInfo.CompanyName, enterpriseInfo.UnifiedSocialCode, address, repName), } @@ -1545,7 +1609,8 @@ func (s *CertificationApplicationServiceImpl) completeEnterpriseVerification( zap.String("user_id", userID), zap.String("record_id", record.ID)) - err = s.userAggregateService.CreateEnterpriseInfo( + // 新用户:创建企业信息;重新认证:更新已有企业信息(CreateOrUpdateEnterpriseInfo 内部分流) + err = s.userAggregateService.CreateOrUpdateEnterpriseInfo( ctx, userID, record.CompanyName, @@ -1558,9 +1623,8 @@ func (s *CertificationApplicationServiceImpl) completeEnterpriseVerification( if err != nil { s.logger.Error("保存企业信息到用户域失败", zap.Error(err)) return fmt.Errorf("保存企业信息失败: %s", err.Error()) - } else { - s.logger.Info("企业信息已保存到用户域", zap.String("user_id", userID)) } + 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 { @@ -1605,14 +1669,15 @@ func (s *CertificationApplicationServiceImpl) createSignTaskAfterEnterpriseVerif } repName := pickAuthorizedRepName(record, record.LegalPersonName) address := record.EnterpriseAddress + transactorName, transactorMobile, transactorID := pickTransactorFromRecord(record) req := &ports.SignFlowCreateRequest{ Subject: s.contractSubjectPrefix(cert) + "-" + record.CompanyName, FileID: cert.ContractFileID, PartyAName: record.CompanyName, PartyAUSCC: record.UnifiedSocialCode, - TransactorName: record.LegalPersonName, - TransactorMobile: record.LegalPersonPhone, - TransactorID: record.LegalPersonID, + TransactorName: transactorName, + TransactorMobile: transactorMobile, + TransactorID: transactorID, TransReferenceID: cert.ID, Fill: ensureContractFill(cert, record.CompanyName, record.UnifiedSocialCode, address, repName), SkipActorURL: true, diff --git a/internal/application/certification/platform_flow.go b/internal/application/certification/platform_flow.go index 7d69868..6519f11 100644 --- a/internal/application/certification/platform_flow.go +++ b/internal/application/certification/platform_flow.go @@ -88,16 +88,17 @@ func (s *CertificationApplicationServiceImpl) SelectSignPlatform( return nil, err } + transactorName, transactorMobile, transactorID := pickTransactorFromRecord(record) authReq := &ports.EnterpriseAuthRequest{ ClientCorpID: record.UnifiedSocialCode, - ClientUserID: record.LegalPersonID, + ClientUserID: transactorID, CompanyName: record.CompanyName, UnifiedSocialCode: record.UnifiedSocialCode, LegalPersonName: record.LegalPersonName, LegalPersonID: record.LegalPersonID, - TransactorName: record.LegalPersonName, - TransactorMobile: record.LegalPersonPhone, - TransactorID: record.LegalPersonID, + TransactorName: transactorName, + TransactorMobile: transactorMobile, + TransactorID: transactorID, } authRes, alreadyVerified, err := s.generateEnterpriseAuthOrDetectVerifiedWithProvider(ctx, provider, authReq) @@ -192,7 +193,7 @@ func (s *CertificationApplicationServiceImpl) generateEnterpriseAuthOrDetectVeri return authRes, false, nil } - // 法大大:企业已对本应用授权(210002)= 授权已完成,直接跳过授权页 + // 法大大:授权范围重复获取(code=210002/210013)= 已对本应用授权,跳过授权页 if isEnterpriseAlreadyAuthorizedErr(err) { s.logger.Info("第三方返回企业已授权,跳过认证链接并视为认证完成", zap.String("company_name", authReq.CompanyName), diff --git a/internal/domains/user/services/user_aggregate_service.go b/internal/domains/user/services/user_aggregate_service.go index b6a9bc2..af6e7c5 100644 --- a/internal/domains/user/services/user_aggregate_service.go +++ b/internal/domains/user/services/user_aggregate_service.go @@ -467,7 +467,9 @@ func (s *UserAggregateServiceImpl) CheckUnifiedSocialCodeExists(ctx context.Cont return exists, nil } -// CreateOrUpdateEnterpriseInfo 认证域专用:写入/覆盖企业信息 +// CreateOrUpdateEnterpriseInfo 认证域专用:按两条链路写入企业信息 +// - 新用户:尚无企业信息 → 创建 +// - 重新认证:已有企业信息 → 覆盖更新 func (s *UserAggregateServiceImpl) CreateOrUpdateEnterpriseInfo( ctx context.Context, userID, companyName, unifiedSocialCode, legalPersonName, legalPersonID, legalPersonPhone, enterpriseAddress string, @@ -476,19 +478,44 @@ func (s *UserAggregateServiceImpl) CreateOrUpdateEnterpriseInfo( if err != nil { return fmt.Errorf("用户不存在: %w", err) } - if user.EnterpriseInfo == nil { - enterpriseInfo, err := entities.NewEnterpriseInfo(userID, companyName, unifiedSocialCode, legalPersonName, legalPersonID, legalPersonPhone, enterpriseAddress) - if err != nil { - return err - } - user.EnterpriseInfo = enterpriseInfo - } else { - err := user.EnterpriseInfo.UpdateEnterpriseInfo(companyName, unifiedSocialCode, legalPersonName, legalPersonID, legalPersonPhone, enterpriseAddress) - if err != nil { - return err - } + + // 统一校验 USCC(排除本人),新用户创建与重新认证更新均适用 + exists, err := s.CheckUnifiedSocialCodeExists(ctx, unifiedSocialCode, userID) + if err != nil { + return fmt.Errorf("检查统一社会信用代码失败: %w", err) } - return s.SaveUser(ctx, user) + if exists { + return fmt.Errorf("统一社会信用代码已被其他用户使用") + } + + if user.HasEnterpriseInfo() { + s.logger.Info("重新认证链路:更新已有企业信息", + zap.String("user_id", userID), + zap.String("unified_social_code", unifiedSocialCode), + zap.String("enterprise_info_id", user.EnterpriseInfo.ID), + ) + if err := user.UpdateEnterpriseInfo(companyName, unifiedSocialCode, legalPersonName, legalPersonID, legalPersonPhone, enterpriseAddress); err != nil { + return fmt.Errorf("更新企业信息失败: %w", err) + } + if err := s.SaveUser(ctx, user); err != nil { + return fmt.Errorf("保存企业信息失败: %w", err) + } + s.logger.Info("重新认证链路:企业信息更新成功", zap.String("user_id", userID)) + return nil + } + + s.logger.Info("新用户认证链路:创建企业信息", + zap.String("user_id", userID), + zap.String("unified_social_code", unifiedSocialCode), + ) + if err := user.CreateEnterpriseInfo(companyName, unifiedSocialCode, legalPersonName, legalPersonID, legalPersonPhone, enterpriseAddress); err != nil { + return fmt.Errorf("创建企业信息失败: %w", err) + } + if err := s.SaveUser(ctx, user); err != nil { + return fmt.Errorf("保存企业信息失败: %w", err) + } + s.logger.Info("新用户认证链路:企业信息创建成功", zap.String("user_id", userID)) + return nil } // CompleteCertification 完成认证 diff --git a/internal/shared/fadada/corp_service.go b/internal/shared/fadada/corp_service.go index 3875717..bf579ea 100644 --- a/internal/shared/fadada/corp_service.go +++ b/internal/shared/fadada/corp_service.go @@ -77,7 +77,7 @@ func (c *Client) GenerateEnterpriseAuth(req *EnterpriseAuthRequest) (*Enterprise return nil, err } if res.Code != successCode { - return nil, fmt.Errorf("获取法大大企业认证链接失败: code=%s msg=%s requestId=%s", res.Code, res.Msg, res.RequestID) + return nil, NewAPIError("获取法大大企业认证链接失败", res.Code, res.Msg, res.RequestID) } var data getCorpAuthURLData @@ -128,7 +128,7 @@ func (c *Client) QueryOrgIdentity(req *QueryOrgIdentityRequest) (*QueryOrgIdenti return nil, err } if res.Code != successCode { - return nil, fmt.Errorf("查询法大大企业实名状态失败: code=%s msg=%s requestId=%s", res.Code, res.Msg, res.RequestID) + return nil, NewAPIError("查询法大大企业实名状态失败", res.Code, res.Msg, res.RequestID) } var data getIdentifiedStatusData diff --git a/internal/shared/fadada/errors.go b/internal/shared/fadada/errors.go new file mode 100644 index 0000000..4e03e82 --- /dev/null +++ b/internal/shared/fadada/errors.go @@ -0,0 +1,89 @@ +package fadada + +import ( + "errors" + "fmt" + "regexp" + "strings" +) + +// 法大大 FASC OpenAPI 错误码(以官方「错误码说明」文档 code 为准,勿依赖 msg 文案)。 +// 文档:https://dev.fadada.com/api-doc/JOQQIJXLFL/H4F0HSKMHQ3HPIIM/5-1 +const ( + CodeSuccess = "100000" // 请求成功 + + // 公共 + CodeUnknownException = "100001" // 不可预知异常 + CodeAccessTokenInvalid = "100002" // 访问凭证失效 + CodeSignInvalid = "100003" // 接口签名错误 + CodeTooFrequent = "100004" // 请求频繁 + CodeParamIllegal = "100011" // 请求参数非法 + CodeParamEmpty = "100012" // 请求参数为空 + CodeBusinessIDNotFound = "212007" // 业务场景标识不存在 + + // 授权范围重复获取:主体已对本应用授权,无须再获取授权链接 + // 个人用户帐号管理 / 企业用户帐号管理 均有说明 + CodeAuthScopeDuplicate = "210002" // 授权范围重复获取 + CodeAuthScopeDuplicateCorp = "210013" // 授权范围重复获取(无须重复获取授权申请链接) +) + +// APIError 法大大业务错误(按 code 判断,不依赖 msg) +type APIError struct { + Op string // 调用场景说明 + Code string + Msg string + RequestID string +} + +func (e *APIError) Error() string { + if e == nil { + return "" + } + op := strings.TrimSpace(e.Op) + if op == "" { + op = "法大大接口调用失败" + } + return fmt.Sprintf("%s: code=%s msg=%s requestId=%s", op, e.Code, e.Msg, e.RequestID) +} + +// NewAPIError 构造结构化业务错误 +func NewAPIError(op, code, msg, requestID string) *APIError { + return &APIError{ + Op: strings.TrimSpace(op), + Code: strings.TrimSpace(code), + Msg: strings.TrimSpace(msg), + RequestID: strings.TrimSpace(requestID), + } +} + +// IsAuthAlreadyGranted 是否表示「已授权 / 授权范围重复获取」,应跳过再次获取授权链接。 +// 官方文档:应按返回码 code 判断,msg 可能调整。 +func IsAuthAlreadyGranted(err error) bool { + if err == nil { + return false + } + var apiErr *APIError + if errors.As(err, &apiErr) && apiErr != nil { + return isAuthAlreadyGrantedCode(apiErr.Code) + } + return isAuthAlreadyGrantedCode(extractFadadaCode(err.Error())) +} + +func isAuthAlreadyGrantedCode(code string) bool { + switch strings.TrimSpace(code) { + case CodeAuthScopeDuplicate, CodeAuthScopeDuplicateCorp: + return true + default: + return false + } +} + +var fadadaCodeRe = regexp.MustCompile(`(?i)(?:^|[^\d])code\s*=\s*(\d{6})(?:[^\d]|$)`) + +func extractFadadaCode(msg string) string { + m := fadadaCodeRe.FindStringSubmatch(msg) + if len(m) < 2 { + return "" + } + return m[1] +} diff --git a/internal/shared/fadada/errors_test.go b/internal/shared/fadada/errors_test.go new file mode 100644 index 0000000..6f686b3 --- /dev/null +++ b/internal/shared/fadada/errors_test.go @@ -0,0 +1,28 @@ +package fadada + +import ( + "errors" + "fmt" + "testing" +) + +func TestIsAuthAlreadyGranted(t *testing.T) { + if !IsAuthAlreadyGranted(NewAPIError("op", CodeAuthScopeDuplicate, "授权范围重复获取", "1")) { + t.Fatal("210002 should match") + } + if !IsAuthAlreadyGranted(NewAPIError("op", CodeAuthScopeDuplicateCorp, "授权范围重复获取", "2")) { + t.Fatal("210013 should match") + } + if IsAuthAlreadyGranted(NewAPIError("op", CodeParamIllegal, "请求参数非法", "3")) { + t.Fatal("100011 should not match") + } + if !IsAuthAlreadyGranted(fmt.Errorf("wrap: %w", NewAPIError("op", CodeAuthScopeDuplicate, "x", "4"))) { + t.Fatal("wrapped 210002 should match") + } + if !IsAuthAlreadyGranted(errors.New("fail: code=210013 msg=授权范围重复获取 requestId=x")) { + t.Fatal("legacy string 210013 should match") + } + if IsAuthAlreadyGranted(errors.New("无需重复操作")) { + t.Fatal("msg-only must not match per official docs") + } +}