This commit is contained in:
2026-07-21 15:53:29 +08:00
parent 024b6614ba
commit c943f575d5
40 changed files with 5362 additions and 275 deletions

View File

@@ -17,6 +17,7 @@ import (
"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/ports"
"hyapi-server/internal/domains/certification/repositories"
"hyapi-server/internal/domains/certification/services"
finance_service "hyapi-server/internal/domains/finance/services"
@@ -40,6 +41,7 @@ type CertificationApplicationServiceImpl struct {
smsCodeService *user_service.SMSCodeService
esignClient *esign.Client
esignConfig *esign.Config
platformRegistry ports.SignPlatformRegistry
qiniuStorageService *storage.QiNiuStorageService
contractAggregateService user_service.ContractAggregateService
walletAggregateService finance_service.WalletAggregateService
@@ -65,6 +67,7 @@ func NewCertificationApplicationService(
smsCodeService *user_service.SMSCodeService,
esignClient *esign.Client,
esignConfig *esign.Config,
platformRegistry ports.SignPlatformRegistry,
qiniuStorageService *storage.QiNiuStorageService,
contractAggregateService user_service.ContractAggregateService,
walletAggregateService finance_service.WalletAggregateService,
@@ -87,6 +90,7 @@ func NewCertificationApplicationService(
smsCodeService: smsCodeService,
esignClient: esignClient,
esignConfig: esignConfig,
platformRegistry: platformRegistry,
qiniuStorageService: qiniuStorageService,
contractAggregateService: contractAggregateService,
walletAggregateService: walletAggregateService,
@@ -443,16 +447,22 @@ func (s *CertificationApplicationServiceImpl) ConfirmAuth(
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,
zap.String("company_name", record.CompanyName),
zap.String("sign_platform", string(cert.ResolvedSignPlatform())))
provider, err := s.resolveProvider(cert)
if err != nil {
return nil, err
}
verified, err := provider.QueryOrgVerified(ctx, &ports.OrgIdentityQuery{
OrgName: record.CompanyName,
OrgIdentNo: record.UnifiedSocialCode,
})
if err != nil {
s.logger.Error("查询企业认证信息失败", zap.Error(err))
return nil, fmt.Errorf("查询企业认证信息失败: %w", err)
}
reason := ""
if identity != nil && identity.Data.RealnameStatus == 1 {
if verified {
s.logger.Info("确认状态-步骤3-三方实名状态已完成,准备事务内推进认证",
zap.String("user_id", cmd.UserID))
err = s.txManager.ExecuteInTx(ctx, func(txCtx context.Context) error {
@@ -501,7 +511,7 @@ func (s *CertificationApplicationServiceImpl) ConfirmSign(
}, nil
}
// ApplyContract 申请合同签署
// ApplyContract 立即签署:若认证完成时已创建任务则只取 iframe 长链并推进状态;否则创建任务
func (s *CertificationApplicationServiceImpl) ApplyContract(
ctx context.Context,
cmd *commands.ApplyContractCommand,
@@ -509,59 +519,179 @@ func (s *CertificationApplicationServiceImpl) ApplyContract(
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())
var embedURL, shortURL string
flowID := strings.TrimSpace(cert.EsignFlowID)
if flowID != "" {
// 任务已在企业认证完成后创建(乙方通常已免签),此处只刷新甲方 iframe 长链
provider, err := s.resolveProvider(cert)
if err != nil {
return nil, err
}
ei := enterpriseInfo.EnterpriseInfo
urlRes, err := provider.GetActorSignURL(ctx, &ports.GetActorSignURLRequest{
SignFlowID: flowID,
TransactorMobile: ei.LegalPersonPhone,
PartyAName: ei.CompanyName,
})
if err != nil {
return nil, fmt.Errorf("获取签署链接失败: %s", err.Error())
}
embedURL = firstNonEmptyStr(urlRes.EmbedURL, urlRes.ShortURL)
shortURL = strings.TrimSpace(urlRes.ShortURL)
} else {
contractInfo, err := s.generateContractAndSignURL(ctx, cert, enterpriseInfo.EnterpriseInfo)
if err != nil {
s.logger.Error("生成合同失败", zap.Error(err))
return nil, fmt.Errorf("生成合同失败: %s", err.Error())
}
flowID = contractInfo.EsignFlowID
embedURL = contractInfo.ContractSignURL
shortURL = contractInfo.ContractSignShortURL
}
err = cert.ApplyContract(contractInfo.EsignFlowID, contractInfo.ContractSignURL)
if embedURL == "" {
return nil, fmt.Errorf("签署链接为空")
}
storeURL := firstNonEmptyStr(shortURL, cert.ContractSignURL, embedURL)
err = cert.ApplyContract(flowID, storeURL)
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,
embedURL,
shortURL,
cert.ContractURL,
"请在规定时间内完成合同签署",
"合同申请成功",
)
s.logger.Info("合同申请成功", zap.String("user_id", cmd.UserID))
s.logger.Info("合同申请成功", zap.String("user_id", cmd.UserID), zap.String("esign_flow_id", flowID))
return response, nil
}
// RefreshContractSignURL 进入签署页时刷新 iframe 长链
func (s *CertificationApplicationServiceImpl) RefreshContractSignURL(
ctx context.Context,
userID string,
) (*responses.ContractSignUrlResponse, error) {
if strings.TrimSpace(userID) == "" {
return nil, fmt.Errorf("用户ID不能为空")
}
cert, err := s.aggregateService.LoadCertificationByUserID(ctx, userID)
if err != nil {
return nil, fmt.Errorf("加载认证信息失败: %s", err.Error())
}
if cert.Status != enums.StatusContractApplied && cert.Status != enums.StatusEnterpriseVerified {
return nil, fmt.Errorf("当前状态不允许获取签署链接")
}
if strings.TrimSpace(cert.EsignFlowID) == "" {
return nil, fmt.Errorf("签署任务尚未创建")
}
provider, err := s.resolveProvider(cert)
if err != nil {
return nil, err
}
enterpriseInfo, err := s.userAggregateService.GetUserWithEnterpriseInfo(ctx, userID)
if err != nil {
return nil, fmt.Errorf("获取企业信息失败: %w", err)
}
ei := enterpriseInfo.EnterpriseInfo
urlRes, err := provider.GetActorSignURL(ctx, &ports.GetActorSignURLRequest{
SignFlowID: cert.EsignFlowID,
TransactorMobile: ei.LegalPersonPhone,
PartyAName: ei.CompanyName,
})
if err != nil {
return nil, fmt.Errorf("刷新签署链接失败: %s", err.Error())
}
embedURL := firstNonEmptyStr(urlRes.EmbedURL, urlRes.ShortURL)
if embedURL == "" {
return nil, fmt.Errorf("签署链接为空")
}
// 短链可回写库(稳定);长链不落库(易过期且可能超长)
if short := strings.TrimSpace(urlRes.ShortURL); short != "" && short != cert.ContractSignURL {
cert.ContractSignURL = short
_ = s.aggregateService.SaveCertification(ctx, cert)
}
return responses.NewContractSignUrlResponse(
cert.ID,
embedURL,
urlRes.ShortURL,
cert.ContractURL,
"请在规定时间内完成合同签署",
"签署链接已刷新",
), nil
}
// RefreshContractPreviewURL 合同预览页:调用法大大 get-preview-url非 PDF 直链)
func (s *CertificationApplicationServiceImpl) RefreshContractPreviewURL(
ctx context.Context,
userID string,
) (*responses.ContractPreviewUrlResponse, error) {
if strings.TrimSpace(userID) == "" {
return nil, fmt.Errorf("用户ID不能为空")
}
cert, err := s.aggregateService.LoadCertificationByUserID(ctx, userID)
if err != nil {
return nil, fmt.Errorf("加载认证信息失败: %s", err.Error())
}
if cert.Status != enums.StatusEnterpriseVerified && cert.Status != enums.StatusContractApplied {
return nil, fmt.Errorf("当前状态不允许预览合同")
}
if strings.TrimSpace(cert.EsignFlowID) == "" {
return nil, fmt.Errorf("签署任务尚未创建,无法预览")
}
provider, err := s.resolveProvider(cert)
if err != nil {
return nil, err
}
preview, err := provider.GetSignTaskPreviewURL(ctx, cert.EsignFlowID)
if err != nil {
return nil, fmt.Errorf("获取预览链接失败: %s", err.Error())
}
mode := "iframe"
if cert.ResolvedSignPlatform() == enums.SignPlatformEsign {
mode = "pdf"
}
return &responses.ContractPreviewUrlResponse{
CertificationID: cert.ID,
PreviewURL: preview.PreviewURL,
PreviewMode: mode,
SignFlowID: cert.EsignFlowID,
Message: "预览链接已刷新",
}, nil
}
// ================ 查询用例 ================
// GetCertification 获取认证详情
@@ -603,10 +733,36 @@ func (s *CertificationApplicationServiceImpl) GetCertification(
}
}
// 脏数据修复:曾因预创建签署软失败推进到 enterprise_verified 但无任务,回退到待选平台
if cert.Status == enums.StatusEnterpriseVerified && strings.TrimSpace(cert.EsignFlowID) == "" {
if err := cert.ResetAfterSignTaskCreateFailed(); err != nil {
s.logger.Warn("回退未创建签署任务的脏状态失败", zap.String("user_id", query.UserID), zap.Error(err))
} else if saveErr := s.aggregateService.SaveCertification(ctx, cert); saveErr != nil {
s.logger.Warn("保存回退状态失败", zap.String("user_id", query.UserID), zap.Error(saveErr))
} else {
s.logger.Info("已回退未创建签署任务的脏状态到待选平台", zap.String("user_id", query.UserID))
}
}
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 {
@@ -873,8 +1029,10 @@ func (s *CertificationApplicationServiceImpl) AdminListSubmitRecords(
items := make([]*responses.AdminSubmitRecordItem, 0, len(result.Records))
for _, r := range result.Records {
certStatus := ""
signPlatform := ""
if cert, err := s.aggregateService.LoadCertificationByUserID(ctx, r.UserID); err == nil && cert != nil {
certStatus = string(cert.Status)
signPlatform = string(cert.SignPlatform)
}
items = append(items, &responses.AdminSubmitRecordItem{
ID: r.ID,
@@ -884,6 +1042,8 @@ func (s *CertificationApplicationServiceImpl) AdminListSubmitRecords(
LegalPersonName: r.LegalPersonName,
SubmitAt: r.SubmitAt,
Status: r.Status,
ManualReviewStatus: r.ManualReviewStatus,
SignPlatform: signPlatform,
CertificationStatus: certStatus,
})
}
@@ -907,8 +1067,10 @@ func (s *CertificationApplicationServiceImpl) AdminGetSubmitRecordByID(ctx conte
return nil, fmt.Errorf("获取提交记录失败: %w", err)
}
certStatus := ""
signPlatform := ""
if cert, loadErr := s.aggregateService.LoadCertificationByUserID(ctx, record.UserID); loadErr == nil && cert != nil {
certStatus = string(cert.Status)
signPlatform = string(cert.SignPlatform)
}
return &responses.AdminSubmitRecordDetail{
ID: record.ID,
@@ -932,6 +1094,9 @@ func (s *CertificationApplicationServiceImpl) AdminGetSubmitRecordByID(ctx conte
VerifiedAt: record.VerifiedAt,
FailedAt: record.FailedAt,
FailureReason: record.FailureReason,
ManualReviewStatus: record.ManualReviewStatus,
ManualReviewRemark: record.ManualReviewRemark,
SignPlatform: signPlatform,
CertificationStatus: certStatus,
CreatedAt: record.CreatedAt,
UpdatedAt: record.UpdatedAt,
@@ -967,52 +1132,19 @@ func (s *CertificationApplicationServiceImpl) AdminApproveSubmitRecord(ctx conte
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))
// 幂等:已审核通过、等待用户选平台
if record.ManualReviewStatus == "approved" {
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)
}
// 审核通过后不立即生成认证链接由用户选择签署平台e签宝/法大大)后再生成
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))
s.logger.Info("管理员审核通过企业信息,等待用户选择签署平台",
zap.String("record_id", recordID),
zap.String("admin_id", adminID),
zap.String("user_id", record.UserID))
return nil
}
@@ -1076,7 +1208,7 @@ func (s *CertificationApplicationServiceImpl) AdminTransitionCertificationStatus
}
switch cmd.TargetStatus {
case string(enums.StatusInfoSubmitted):
// 审核通过:与 AdminApproveSubmitRecord 一致,推状态并生成企业认证链接
// 审核通过:与 AdminApproveSubmitRecord 一致,仅标记审核通过,等待用户选择签署平台
switch cert.Status {
case enums.StatusInfoSubmitted, enums.StatusEnterpriseVerified, enums.StatusContractApplied,
enums.StatusContractSigned, enums.StatusCompleted, enums.StatusContractRejected, enums.StatusContractExpired:
@@ -1085,45 +1217,13 @@ func (s *CertificationApplicationServiceImpl) AdminTransitionCertificationStatus
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))
s.logger.Info("管理端审核通过,等待用户选择签署平台",
zap.String("user_id", cmd.UserID),
zap.String("admin_id", cmd.AdminID))
return nil
case string(enums.StatusInfoRejected):
// 审核拒绝
@@ -1160,6 +1260,7 @@ func (s *CertificationApplicationServiceImpl) convertToResponse(cert *entities.C
UserID: cert.UserID,
Status: cert.Status,
StatusName: enums.GetStatusName(cert.Status),
SignPlatform: string(cert.SignPlatform),
Progress: cert.GetProgress(),
CreatedAt: cert.CreatedAt,
UpdatedAt: cert.UpdatedAt,
@@ -1198,58 +1299,6 @@ func (s *CertificationApplicationServiceImpl) convertToResponse(cert *entities.C
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
@@ -1258,6 +1307,18 @@ func isEnterpriseAlreadyRealnamedErr(err error) bool {
return strings.Contains(msg, "企业用户已实名") || strings.Contains(msg, "已实名")
}
// isEnterpriseAlreadyAuthorizedErr 法大大等平台:企业已对本应用授权,无需再获取授权链接(如 code=210002
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, "已存在授权")
}
// validateApplyContractCommand 验证申请合同命令
func (s *CertificationApplicationServiceImpl) validateApplyContractCommand(cmd *commands.ApplyContractCommand) error {
if cmd.UserID == "" {
@@ -1279,27 +1340,41 @@ func (s *CertificationApplicationServiceImpl) validateContractApplicationPrecond
// 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,
})
provider, err := s.resolveProvider(cert)
if err != nil {
return nil, fmt.Errorf("生成合同失败: %s", err.Error())
return nil, err
}
_, shortUrl, err := s.esignClient.GetSignURL(signFlowID, enterpriseInfo.LegalPersonPhone, enterpriseInfo.CompanyName)
record, _ := s.enterpriseInfoSubmitRecordRepo.FindLatestByUserID(ctx, cert.UserID)
repName := enterpriseInfo.LegalPersonName
address := enterpriseInfo.EnterpriseAddress
if record != nil {
repName = pickAuthorizedRepName(record, enterpriseInfo.LegalPersonName)
if address == "" {
address = record.EnterpriseAddress
}
}
req := &ports.SignFlowCreateRequest{
Subject: "海宇数据-合作协议-" + enterpriseInfo.CompanyName,
FileID: cert.ContractFileID,
PartyAName: enterpriseInfo.CompanyName,
PartyAUSCC: enterpriseInfo.UnifiedSocialCode,
TransactorName: enterpriseInfo.LegalPersonName,
TransactorMobile: enterpriseInfo.LegalPersonPhone,
TransactorID: enterpriseInfo.LegalPersonID,
TransReferenceID: cert.ID,
Fill: ensureContractFill(cert, enterpriseInfo.CompanyName, enterpriseInfo.UnifiedSocialCode, address, repName),
}
signRes, err := provider.CreateSignFlow(ctx, req)
if err != nil {
return nil, fmt.Errorf("获取签署链接失败: %s", err.Error())
return nil, fmt.Errorf("生成签署流程失败: %s", err.Error())
}
// iframe 必须用长链 SignURL法大大 EmbedURL短链不可嵌入
embedURL := firstNonEmptyStr(signRes.SignURL, signRes.ShortURL)
return &certification_value_objects.ContractInfo{
ContractFileID: cert.ContractFileID,
EsignFlowID: signFlowID,
ContractSignURL: shortUrl,
ContractFileID: cert.ContractFileID,
EsignFlowID: signRes.SignFlowID,
ContractSignURL: embedURL,
ContractSignShortURL: strings.TrimSpace(signRes.ShortURL),
}, nil
}
@@ -1333,7 +1408,7 @@ func (s *CertificationApplicationServiceImpl) completeEnterpriseVerification(
zap.String("user_id", userID),
zap.String("record_id", record.ID))
err = s.userAggregateService.CreateEnterpriseInfo(
err = s.userAggregateService.CreateOrUpdateEnterpriseInfo(
ctx,
userID,
record.CompanyName,
@@ -1346,17 +1421,26 @@ 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 {
return err
}
s.logger.Info("完成企业认证-步骤3-合同文件生成并写入认证成功", zap.String("user_id", userID))
// 立即创建并提交签署任务:乙方免验证签先盖章;甲方稍后在「立即签署」取 iframe 长链
// 创建失败必须整体失败,不得推进到 enterprise_verified / 签署合同步骤
if err := s.createSignTaskAfterEnterpriseVerified(ctx, cert, record); err != nil {
s.logger.Error("企业认证后预创建签署任务失败", zap.Error(err), zap.String("user_id", userID))
return fmt.Errorf("创建签署任务失败: %w", err)
}
s.logger.Info("完成企业认证-步骤3b-签署任务已创建(乙方免签)",
zap.String("user_id", userID),
zap.String("esign_flow_id", cert.EsignFlowID))
// 保存认证信息
err = s.aggregateService.SaveCertification(ctx, cert)
if err != nil {
@@ -1368,6 +1452,46 @@ func (s *CertificationApplicationServiceImpl) completeEnterpriseVerification(
return nil
}
// createSignTaskAfterEnterpriseVerified 认证完成后创建签署任务并启动(乙方免签),不推进到 contract_applied
func (s *CertificationApplicationServiceImpl) createSignTaskAfterEnterpriseVerified(
ctx context.Context,
cert *entities.Certification,
record *entities.EnterpriseInfoSubmitRecord,
) error {
if cert == nil || record == nil {
return fmt.Errorf("认证或企业信息为空")
}
if strings.TrimSpace(cert.EsignFlowID) != "" {
return nil
}
provider, err := s.resolveProvider(cert)
if err != nil {
return err
}
repName := pickAuthorizedRepName(record, record.LegalPersonName)
address := record.EnterpriseAddress
req := &ports.SignFlowCreateRequest{
Subject: "海宇数据-合作协议-" + record.CompanyName,
FileID: cert.ContractFileID,
PartyAName: record.CompanyName,
PartyAUSCC: record.UnifiedSocialCode,
TransactorName: record.LegalPersonName,
TransactorMobile: record.LegalPersonPhone,
TransactorID: record.LegalPersonID,
TransReferenceID: cert.ID,
Fill: ensureContractFill(cert, record.CompanyName, record.UnifiedSocialCode, address, repName),
SkipActorURL: true, // 预创建不取甲方链接,避免消耗单次 EmbedURL
}
signRes, err := provider.CreateSignFlow(ctx, req)
if err != nil {
return err
}
storeURL := firstNonEmptyStr(signRes.ShortURL, signRes.SignURL)
cert.BindSignTask(signRes.SignFlowID, storeURL)
// 预览链接约 2 小时/单次,进入预览页时再调 get-preview-url此处不落库 PDF 直链
return nil
}
// pickAuthorizedRepName 合同模板「客户授权代表」: 优先企业提交记录中的授权代表, 否则为法定代表人
func pickAuthorizedRepName(record *entities.EnterpriseInfoSubmitRecord, legalPersonName string) string {
if record != nil && strings.TrimSpace(record.AuthorizedRepName) != "" {
@@ -1396,22 +1520,18 @@ func (s *CertificationApplicationServiceImpl) generateAndAddContractFile(
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,
provider, err := s.resolveProvider(cert)
if err != nil {
return err
}
fillTemplateResp, err := s.esignClient.FillTemplate(fileComponent)
fillTemplateResp, err := provider.GenerateContractFile(ctx, &ports.ContractGenerateRequest{
AgreementNo: agreementNo,
CompanyName: companyName,
UnifiedSocialCode: unifiedSocialCode,
EnterpriseAddress: enterpriseAddress,
AuthorizedRepName: authorizedRepName,
SignDate: signDate,
})
if err != nil {
s.logger.Error("生成合同失败", zap.Error(err))
return fmt.Errorf("生成合同失败: %s", err.Error())
@@ -1419,8 +1539,9 @@ func (s *CertificationApplicationServiceImpl) generateAndAddContractFile(
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)
zap.String("contract_code", agreementNo),
zap.String("sign_platform", string(cert.ResolvedSignPlatform())))
err = cert.AddContractFileID(fillTemplateResp.FileID, fillTemplateResp.FileDownloadURL)
if err != nil {
s.logger.Error("加入合同文件ID链接失败", zap.Error(err))
return fmt.Errorf("加入合同文件ID链接失败: %s", err.Error())
@@ -1466,13 +1587,19 @@ func (s *CertificationApplicationServiceImpl) checkAndCompleteEnterpriseVerifica
if err != nil {
return fmt.Errorf("查找企业信息失败: %w", err)
}
identity, err := s.esignClient.QueryOrgIdentityInfo(&esign.QueryOrgIdentityRequest{
OrgName: record.CompanyName,
provider, err := s.resolveProvider(cert)
if err != nil {
return err
}
verified, err := provider.QueryOrgVerified(ctx, &ports.OrgIdentityQuery{
OrgName: record.CompanyName,
OrgIdentNo: record.UnifiedSocialCode,
})
if err != nil {
s.logger.Error("查询企业认证信息失败", zap.Error(err))
return nil
}
if identity != nil && identity.Data.RealnameStatus == 1 {
if verified {
err = s.txManager.ExecuteInTx(ctx, func(txCtx context.Context) error {
return s.completeEnterpriseVerification(txCtx, cert, cert.UserID, record.CompanyName, record.LegalPersonName)
})
@@ -1490,11 +1617,15 @@ func (s *CertificationApplicationServiceImpl) checkAndUpdateSignStatus(ctx conte
if cert.Status != enums.StatusContractApplied {
return fmt.Errorf("认证状态不正确")
}
detail, err := s.esignClient.QuerySignFlowDetail(cert.EsignFlowID)
provider, err := s.resolveProvider(cert)
if err != nil {
return err
}
detail, err := provider.QuerySignStatus(txCtx, cert.EsignFlowID)
if err != nil {
return fmt.Errorf("查询签署流程详情失败: %s", err.Error())
}
if detail.Data.SignFlowStatus == 2 {
if detail.Completed {
err = cert.SignSuccess()
if err != nil {
return fmt.Errorf("合同签署成功失败: %s", err.Error())
@@ -1503,21 +1634,19 @@ func (s *CertificationApplicationServiceImpl) checkAndUpdateSignStatus(ctx conte
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)
} else if detail.Rejected {
err = cert.ContractRejection(detail.Message)
if err != nil {
return fmt.Errorf("合同签署失败: %s", err.Error())
}
reason = "合同签署拒签"
} else if detail.Data.SignFlowStatus == 5 {
} else if detail.Expired {
err = cert.ContractExpiration()
if err != nil {
return fmt.Errorf("合同签署过期失败: %s", err.Error())
@@ -1549,40 +1678,42 @@ func (s *CertificationApplicationServiceImpl) handleContractAfterSignComplete(ct
return fmt.Errorf("用户企业信息不存在")
}
// 1. 获取所有已签署合同文件信息
downloadSignedFileResponse, err := s.esignClient.DownloadSignedFile(cert.EsignFlowID)
provider, err := s.resolveProvider(cert)
if err != nil {
return err
}
files, err := provider.DownloadSignedFiles(ctx, 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
for i, file := range files {
fileUrl := file.DownloadURL
fileName := fmt.Sprintf("合作协议-%s-%d.pdf", cert.ContractCode, i+1)
fileId := firstNonEmptyStr(file.DownloadID, cert.EsignFlowID)
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. 保存到合同聚合根(复用认证阶段的合同编号)
if i == 0 {
cert.ContractURL = qiniuURL
}
_, err = s.contractAggregateService.CreateContractWithCode(
ctx,
user.EnterpriseInfo.ID,
@@ -1597,7 +1728,6 @@ func (s *CertificationApplicationServiceImpl) handleContractAfterSignComplete(ct
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))
}
@@ -1648,10 +1778,22 @@ func (s *CertificationApplicationServiceImpl) AddStatusMetadata(ctx context.Cont
metadata["contract_url"] = contracts[0].ContractFileURL
}
}
metadata = s.appendPlatformSelectMetadata(ctx, cert, metadata)
if need, _ := metadata["need_select_platform"].(bool); need {
metadata["next_action"] = "请选择签署平台以继续企业认证"
}
return metadata, nil
}
func firstNonEmptyStr(values ...string) string {
for _, v := range values {
if strings.TrimSpace(v) != "" {
return v
}
}
return ""
}
// completeUserActivationWithoutContract 创建钱包、API用户并在用户域标记完成认证不依赖合同信息
func (s *CertificationApplicationServiceImpl) completeUserActivationWithoutContract(ctx context.Context, cert *entities.Certification) error {
// 创建钱包