Files
tyapi-server/internal/application/certification/certification_application_service_impl.go
2026-03-18 16:05:01 +08:00

1450 lines
54 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package certification
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"tyapi-server/internal/application/certification/dto/commands"
"tyapi-server/internal/application/certification/dto/queries"
"tyapi-server/internal/application/certification/dto/responses"
"tyapi-server/internal/config"
api_service "tyapi-server/internal/domains/api/services"
"tyapi-server/internal/domains/certification/entities"
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_service "tyapi-server/internal/domains/finance/services"
user_entities "tyapi-server/internal/domains/user/entities"
user_service "tyapi-server/internal/domains/user/services"
"tyapi-server/internal/infrastructure/external/notification"
"tyapi-server/internal/infrastructure/external/storage"
"tyapi-server/internal/shared/database"
"tyapi-server/internal/shared/esign"
sharedOCR "tyapi-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))
// 0. 若该用户已有待审核的提交记录,则不允许重复提交
latestRecord, err := s.enterpriseInfoSubmitRecordRepo.FindLatestByUserID(ctx, cmd.UserID)
if err == nil && latestRecord != nil && latestRecord.ManualReviewStatus == "pending" {
return nil, fmt.Errorf("您已有待审核的提交,请等待管理员审核后再操作")
}
// 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" {
if err := s.smsCodeService.VerifyCode(ctx, cmd.LegalPersonPhone, cmd.VerificationCode, user_entities.SMSSceneCertification); err != nil {
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("开始处理企业信息提交",
zap.String("user_id", cmd.UserID))
// 1. 检查企业信息是否重复(统一社会信用代码:已认证或已提交待审核的都不能重复)
// 1.1 已写入用户域 enterprise_infos 的(已完成认证)
exists, err := s.userAggregateService.CheckUnifiedSocialCodeExists(ctx, cmd.UnifiedSocialCode, cmd.UserID)
if err != nil {
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 {
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 {
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 {
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())
}
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())
}
record.MarkAsVerified()
var response *responses.CertificationResponse
err = s.txManager.ExecuteInTx(ctx, func(txCtx context.Context) error {
// 2. 检查用户认证是否存在
existsCert, err := s.aggregateService.ExistsByUserID(txCtx, cmd.UserID)
if err != nil {
return fmt.Errorf("检查用户认证是否存在失败: %s", err.Error())
}
if !existsCert {
// 创建
_, err := s.aggregateService.CreateCertification(txCtx, cmd.UserID)
if err != nil {
return fmt.Errorf("创建认证信息失败: %s", err.Error())
}
}
// 3. 加载认证聚合根
cert, err := s.aggregateService.LoadCertificationByUserID(txCtx, cmd.UserID)
if err != nil {
return fmt.Errorf("加载认证信息失败: %s", err.Error())
}
// 已是「已提交企业信息」:说明已通过人工审核,直接返回当前认证数据,前端可刷新到企业认证步骤
if cert.Status == enums.StatusInfoSubmitted {
response = s.convertToResponse(cert)
response.Metadata = map[string]interface{}{
"next_action": "您已通过审核,请完成企业认证步骤",
}
return nil
}
// 4. 提交企业信息进入人工审核(不调用 e签宝不生成认证链接
err = cert.SubmitEnterpriseInfoForReview(enterpriseInfo)
if err != nil {
return fmt.Errorf("提交企业信息失败: %s", err.Error())
}
err = s.aggregateService.SaveCertification(txCtx, cert)
if err != nil {
return fmt.Errorf("保存认证信息失败: %s", err.Error())
}
// 5. 提交记录与认证状态在同一事务内保存,避免出现「有记录但认证未变待审核」的不一致
if saveErr := s.enterpriseInfoSubmitRecordService.Save(txCtx, record); saveErr != nil {
return fmt.Errorf("保存企业信息提交记录失败: %s", saveErr.Error())
}
respMeta := map[string]interface{}{
"enterprise_info": enterpriseInfo,
"next_action": "请等待管理员审核企业信息",
}
// 6. 转换为响应 DTO
response = s.convertToResponse(cert)
if respMeta != nil {
response.Metadata = respMeta
}
return nil
})
if err != nil {
return nil, err
}
s.logger.Info("企业信息提交成功", zap.String("user_id", cmd.UserID))
return response, nil
}
// 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())
}
// 企业认证
if cert.Status != enums.StatusInfoSubmitted {
return nil, fmt.Errorf("认证状态不正确,当前状态: %s", enums.GetStatusName(cert.Status))
}
record, err := s.enterpriseInfoSubmitRecordRepo.FindLatestByUserID(ctx, cert.UserID)
if err != nil {
return nil, fmt.Errorf("查找企业信息失败: %w", err)
}
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 {
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)
}
} else {
reason = "企业未完成"
}
return &responses.ConfirmAuthResponse{
Status: cert.Status,
Reason: reason,
}, nil
}
// ConfirmSign 确认签署状态
func (s *CertificationApplicationServiceImpl) ConfirmSign(
ctx context.Context,
cmd *queries.ConfirmSignCommand,
) (*responses.ConfirmSignResponse, error) {
cert, err := s.aggregateService.LoadCertificationByUserID(ctx, cmd.UserID)
if err != nil {
return nil, fmt.Errorf("加载认证信息失败: %s", err.Error())
}
reason, err := s.checkAndUpdateSignStatus(ctx, cert)
if err != nil {
return nil, fmt.Errorf("确认签署状态失败: %w", err)
}
return &responses.ConfirmSignResponse{
Status: cert.Status,
Reason: reason,
}, nil
}
// ApplyContract 申请合同签署
func (s *CertificationApplicationServiceImpl) ApplyContract(
ctx context.Context,
cmd *commands.ApplyContractCommand,
) (*responses.ContractSignUrlResponse, error) {
s.logger.Info("开始申请合同签署",
zap.String("user_id", cmd.UserID))
// 1. 验证命令完整性
if err := s.validateApplyContractCommand(cmd); err != nil {
return nil, fmt.Errorf("命令验证失败: %s", err.Error())
}
// 2. 加载认证聚合根
cert, err := s.aggregateService.LoadCertificationByUserID(ctx, cmd.UserID)
if err != nil {
return nil, fmt.Errorf("加载认证信息失败: %s", err.Error())
}
// 3. 验证业务前置条件
if err := s.validateContractApplicationPreconditions(cert, cmd.UserID); err != nil {
return nil, fmt.Errorf("业务前置条件验证失败: %s", err.Error())
}
// 5. 生成合同和签署链接
enterpriseInfo, err := s.userAggregateService.GetUserWithEnterpriseInfo(ctx, cmd.UserID)
if err != nil {
s.logger.Error("获取企业信息失败", zap.Error(err))
return nil, fmt.Errorf("获取企业信息失败: %w", err)
}
contractInfo, err := s.generateContractAndSignURL(ctx, cert, enterpriseInfo.EnterpriseInfo)
if err != nil {
s.logger.Error("生成合同失败", zap.Error(err))
return nil, fmt.Errorf("生成合同失败: %s", err.Error())
}
err = cert.ApplyContract(contractInfo.EsignFlowID, contractInfo.ContractSignURL)
if err != nil {
s.logger.Error("合同申请状态转换失败", zap.Error(err))
return nil, fmt.Errorf("合同申请失败: %s", err.Error())
}
// 7. 保存认证信息
err = s.aggregateService.SaveCertification(ctx, cert)
if err != nil {
s.logger.Error("保存认证信息失败", zap.Error(err))
return nil, fmt.Errorf("保存认证信息失败: %s", err.Error())
}
// 8. 构建响应
response := responses.NewContractSignUrlResponse(
cert.ID,
contractInfo.ContractSignURL,
contractInfo.ContractURL,
"请在规定时间内完成合同签署",
"合同申请成功",
)
s.logger.Info("合同申请成功", zap.String("user_id", cmd.UserID))
return response, nil
}
// ================ 查询用例 ================
// GetCertification 获取认证详情
func (s *CertificationApplicationServiceImpl) GetCertification(
ctx context.Context,
query *queries.GetCertificationQuery,
) (*responses.CertificationResponse, error) {
s.logger.Debug("获取认证详情", zap.String("user_id", query.UserID))
// 1. 检查用户认证是否存在
exists, err := s.aggregateService.ExistsByUserID(ctx, query.UserID)
if err != nil {
s.logger.Error("获取认证信息失败", zap.Error(err))
return nil, fmt.Errorf("获取认证信息失败: %w", err)
}
var cert *entities.Certification
if !exists {
// 创建新的认证记录
cert, err = s.aggregateService.CreateCertification(ctx, query.UserID)
if err != nil {
s.logger.Error("创建认证信息失败", zap.Error(err))
return nil, fmt.Errorf("创建认证信息失败: %w", err)
}
} else {
// 加载现有认证记录
cert, err = s.aggregateService.LoadCertificationByUserID(ctx, query.UserID)
if err != nil {
s.logger.Error("加载认证信息失败", zap.Error(err))
return nil, fmt.Errorf("加载认证信息失败: %w", err)
}
}
// 2. 检查是否需要更新合同文件
if cert.IsContractFileNeedUpdate() {
err = s.updateContractFile(ctx, cert)
if err != nil {
return nil, err
}
}
if cert.Status == enums.StatusInfoSubmitted {
err = s.checkAndCompleteEnterpriseVerification(ctx, cert)
if err != nil {
return nil, err
}
}
if cert.Status == enums.StatusContractApplied {
_, err = s.checkAndUpdateSignStatus(ctx, cert)
if err != nil {
return nil, err
}
}
// 2. 转换为响应DTO
response := s.convertToResponse(cert)
// 3. 添加状态相关的元数据
meta, err := s.AddStatusMetadata(ctx, cert)
if err != nil {
return nil, err
}
if meta != nil {
response.Metadata = meta
}
s.logger.Info("获取认证详情成功", zap.String("user_id", query.UserID))
return response, nil
}
// ListCertifications 获取认证列表(管理员)
func (s *CertificationApplicationServiceImpl) ListCertifications(
ctx context.Context,
query *queries.ListCertificationsQuery,
) (*responses.CertificationListResponse, error) {
s.logger.Debug("获取认证列表(管理员)")
// 1. 转换为领域查询对象
domainQuery := query.ToDomainQuery()
// 2. 执行查询
certs, total, err := s.queryRepository.List(ctx, domainQuery)
if err != nil {
s.logger.Error("查询认证列表失败", zap.Error(err))
return nil, fmt.Errorf("查询认证列表失败: %w", err)
}
// 3. 转换为响应DTO
items := make([]*responses.CertificationResponse, len(certs))
for i, cert := range certs {
items[i] = s.convertToResponse(cert)
}
// 4. 构建列表响应
response := responses.NewCertificationListResponse(items, total, query.Page, query.PageSize)
return response, nil
}
// ================ e签宝回调处理 ================
// HandleEsignCallback 处理e签宝回调
func (s *CertificationApplicationServiceImpl) HandleEsignCallback(
ctx context.Context,
cmd *commands.EsignCallbackCommand,
) error {
// if err := esign.VerifySignature(cmd.Data, cmd.Headers, cmd.QueryParams, s.esignConfig.AppSecret); err != nil {
// return fmt.Errorf("e签宝回调验签失败: %w", err)
// }
// 4. 根据回调类型处理业务逻辑
switch cmd.Data.Action {
case "AUTH_PASS":
// 只处理企业认证通过
if cmd.Data.AuthType == "ORG" {
err := s.txManager.ExecuteInTx(ctx, func(txCtx context.Context) error {
// 1. 根据AuthFlowId加载认证信息
cert, err := s.aggregateService.LoadCertificationByAuthFlowId(txCtx, cmd.Data.AuthFlowId)
if err != nil {
return fmt.Errorf("加载认证信息失败: %s", err.Error())
}
if cmd.Data.Organization == nil || cmd.Data.Organization.OrgName == "" {
return fmt.Errorf("组织信息为空")
}
if cert.Status != enums.StatusInfoSubmitted {
return fmt.Errorf("认证状态不正确")
}
// 2. 完成企业认证
err = cert.CompleteEnterpriseVerification()
if err != nil {
return fmt.Errorf("完成企业认证失败: %s", err.Error())
}
record, err := s.enterpriseInfoSubmitRecordRepo.FindLatestByUserID(txCtx, cert.UserID)
if err != nil {
s.logger.Error("查找已认证企业信息失败", zap.Error(err))
return fmt.Errorf("查找已认证企业信息失败: %w", err)
}
// 5. 写入用户域
err = s.userAggregateService.CreateOrUpdateEnterpriseInfo(
txCtx,
record.UserID,
record.CompanyName,
record.UnifiedSocialCode,
record.LegalPersonName,
record.LegalPersonID,
record.LegalPersonPhone,
record.EnterpriseAddress,
)
if err != nil {
s.logger.Error("同步企业信息到用户域失败", zap.Error(err))
return fmt.Errorf("同步企业信息到用户域失败: %w", err)
}
// 生成合同
err = s.generateAndAddContractFile(txCtx, cert, record.CompanyName, record.LegalPersonName, record.UnifiedSocialCode, record.EnterpriseAddress, record.LegalPersonPhone, record.LegalPersonID)
if err != nil {
return err
}
// 3. 保存认证信息
err = s.aggregateService.SaveCertification(txCtx, cert)
if err != nil {
return fmt.Errorf("保存认证信息失败: %s", err.Error())
}
s.logger.Info("完成企业认证", zap.String("certification_id", cert.ID))
return nil
})
if err != nil {
s.logger.Error("完成企业认证失败", zap.Error(err))
return fmt.Errorf("完成企业认证失败: %w", err)
}
}
return nil
default:
s.logger.Info("忽略未知的回调动作", zap.String("action", cmd.Data.Action))
return nil
}
}
// ================ 管理员后台操作用例 ================
// AdminCompleteCertificationWithoutContract 管理员代用户完成认证(暂不关联合同)
func (s *CertificationApplicationServiceImpl) AdminCompleteCertificationWithoutContract(
ctx context.Context,
cmd *commands.AdminCompleteCertificationCommand,
) (*responses.CertificationResponse, error) {
s.logger.Info("管理员代用户完成认证(不关联合同)",
zap.String("admin_id", cmd.AdminID),
zap.String("user_id", cmd.UserID),
)
// 1. 基础参数及企业信息校验
enterpriseInfo := &certification_value_objects.EnterpriseInfo{
CompanyName: cmd.CompanyName,
UnifiedSocialCode: cmd.UnifiedSocialCode,
LegalPersonName: cmd.LegalPersonName,
LegalPersonID: cmd.LegalPersonID,
LegalPersonPhone: cmd.LegalPersonPhone,
EnterpriseAddress: cmd.EnterpriseAddress,
}
if err := enterpriseInfo.Validate(); err != nil {
return nil, fmt.Errorf("企业信息验证失败: %s", err.Error())
}
// 检查统一社会信用代码唯一性(排除当前用户)
exists, err := s.userAggregateService.CheckUnifiedSocialCodeExists(ctx, cmd.UnifiedSocialCode, cmd.UserID)
if err != nil {
return nil, fmt.Errorf("检查企业信息失败: %s", err.Error())
}
if exists {
return nil, fmt.Errorf("统一社会信用代码已被其他用户使用")
}
var cert *entities.Certification
// 2. 事务内:创建/加载认证、写入企业信息、直接完成认证、创建钱包和API用户
err = s.txManager.ExecuteInTx(ctx, func(txCtx context.Context) error {
// 2.1 检查并创建认证记录
existsCert, err := s.aggregateService.ExistsByUserID(txCtx, cmd.UserID)
if err != nil {
return fmt.Errorf("检查用户认证是否存在失败: %s", err.Error())
}
if !existsCert {
cert, err = s.aggregateService.CreateCertification(txCtx, cmd.UserID)
if err != nil {
return fmt.Errorf("创建认证信息失败: %s", err.Error())
}
} else {
cert, err = s.aggregateService.LoadCertificationByUserID(txCtx, cmd.UserID)
if err != nil {
return fmt.Errorf("加载认证信息失败: %s", err.Error())
}
}
// 2.2 写入/覆盖用户域企业信息
if err := s.userAggregateService.CreateOrUpdateEnterpriseInfo(
txCtx,
cmd.UserID,
cmd.CompanyName,
cmd.UnifiedSocialCode,
cmd.LegalPersonName,
cmd.LegalPersonID,
cmd.LegalPersonPhone,
cmd.EnterpriseAddress,
); err != nil {
return fmt.Errorf("保存企业信息失败: %s", err.Error())
}
// 2.3 直接将认证状态设置为完成(管理员操作,暂不校验合同信息)
if err := cert.TransitionTo(
enums.StatusCompleted,
enums.ActorTypeAdmin,
cmd.AdminID,
fmt.Sprintf("管理员代用户完成认证:%s", cmd.Reason),
); err != nil {
return fmt.Errorf("更新认证状态失败: %s", err.Error())
}
// 2.4 基础激活创建钱包、API用户并在用户域标记完成认证
if err := s.completeUserActivationWithoutContract(txCtx, cert); err != nil {
return err
}
// 2.5 保存认证信息
if err := s.aggregateService.SaveCertification(txCtx, cert); err != nil {
return fmt.Errorf("保存认证信息失败: %s", err.Error())
}
return nil
})
if err != nil {
return nil, err
}
response := s.convertToResponse(cert)
s.logger.Info("管理员代用户完成认证成功(不关联合同)",
zap.String("admin_id", cmd.AdminID),
zap.String("user_id", cmd.UserID),
zap.String("certification_id", cert.ID),
)
return response, nil
}
// AdminListSubmitRecords 管理端分页查询企业信息提交记录
func (s *CertificationApplicationServiceImpl) AdminListSubmitRecords(
ctx context.Context,
query *queries.AdminListSubmitRecordsQuery,
) (*responses.AdminSubmitRecordsListResponse, error) {
if query.PageSize <= 0 {
query.PageSize = 10
}
if query.Page <= 0 {
query.Page = 1
}
filter := repositories.ListSubmitRecordsFilter{
Page: query.Page,
PageSize: query.PageSize,
ManualReviewStatus: query.ManualReviewStatus,
}
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,
ManualReviewStatus: r.ManualReviewStatus,
ManualReviewedAt: r.ManualReviewedAt,
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,
ManualReviewStatus: record.ManualReviewStatus,
ManualReviewRemark: record.ManualReviewRemark,
ManualReviewedAt: record.ManualReviewedAt,
ManualReviewerID: record.ManualReviewerID,
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.ManualReviewStatus != "pending" {
return fmt.Errorf("该记录已审核,当前状态: %s", record.ManualReviewStatus)
}
cert, err := s.aggregateService.LoadCertificationByUserID(ctx, record.UserID)
if err != nil {
return fmt.Errorf("加载认证信息失败: %w", err)
}
// 兼容线上脏数据:认证已进入「已提交企业信息」或更后续状态,但提交记录仍是 pending
// 此时无需再走「待审核->通过」的状态机,只需要把提交记录补齐为 approved避免管理端无法操作。
// 说明:后续状态(如已企业认证/合同状态/完成)都意味着企业信息审核已经通过。
switch cert.Status {
case enums.StatusInfoSubmitted,
enums.StatusEnterpriseVerified,
enums.StatusContractApplied,
enums.StatusContractSigned,
enums.StatusCompleted,
enums.StatusContractRejected,
enums.StatusContractExpired:
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),
zap.String("user_id", record.UserID),
zap.String("certification_status", string(cert.Status)),
)
return nil
}
// 兼容线上脏数据:提交记录已落库但当时事务失败导致认证仍为「待认证」,先同步为待审核再执行通过
if cert.Status != enums.StatusInfoPendingReview {
if err := s.syncCertToPendingReviewIfRecordPending(ctx, cert, record); err != nil {
return err
}
cert, err = s.aggregateService.LoadCertificationByUserID(ctx, record.UserID)
if err != nil {
return fmt.Errorf("加载认证信息失败: %w", err)
}
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,
}
authURL, err := s.esignClient.GenerateEnterpriseAuth(&esign.EnterpriseAuthRequest{
CompanyName: enterpriseInfo.CompanyName,
UnifiedSocialCode: enterpriseInfo.UnifiedSocialCode,
LegalPersonName: enterpriseInfo.LegalPersonName,
LegalPersonID: enterpriseInfo.LegalPersonID,
TransactorName: enterpriseInfo.LegalPersonName,
TransactorMobile: enterpriseInfo.LegalPersonPhone,
TransactorID: enterpriseInfo.LegalPersonID,
})
if 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)
}
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)
}
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.ManualReviewStatus != "pending" {
return fmt.Errorf("该记录已审核,当前状态: %s", record.ManualReviewStatus)
}
cert, err := s.aggregateService.LoadCertificationByUserID(ctx, record.UserID)
if err != nil {
return fmt.Errorf("加载认证信息失败: %w", err)
}
// 兼容线上脏数据:提交记录已落库但当时事务失败导致认证仍为「待认证」,先同步为待审核再执行拒绝
if cert.Status != enums.StatusInfoPendingReview {
if err := s.syncCertToPendingReviewIfRecordPending(ctx, cert, record); err != nil {
return err
}
cert, err = s.aggregateService.LoadCertificationByUserID(ctx, record.UserID)
if err != nil {
return fmt.Errorf("加载认证信息失败: %w", err)
}
if cert.Status != enums.StatusInfoPendingReview {
return fmt.Errorf("认证状态不是待审核,当前: %s", enums.GetStatusName(cert.Status))
}
}
record.MarkManualRejected(adminID, remark)
if err := s.enterpriseInfoSubmitRecordService.Save(ctx, record); err != nil {
return fmt.Errorf("保存提交记录失败: %w", err)
}
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)
}
s.logger.Info("管理员审核拒绝企业信息", zap.String("record_id", recordID), zap.String("admin_id", adminID))
return nil
}
// ================ 辅助方法 ================
// syncCertToPendingReviewIfRecordPending 兼容历史脏数据:当认证为「待认证」或「已拒绝」且存在待审核提交记录时,
// 用该记录的企业信息把认证同步为「待审核」,便于管理员直接审核通过/拒绝。
func (s *CertificationApplicationServiceImpl) syncCertToPendingReviewIfRecordPending(ctx context.Context, cert *entities.Certification, record *entities.EnterpriseInfoSubmitRecord) error {
if record.ManualReviewStatus != "pending" {
return nil
}
if cert.Status != enums.StatusPending && cert.Status != enums.StatusInfoRejected {
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,
}
if err := cert.SubmitEnterpriseInfoForReview(enterpriseInfo); err != nil {
return fmt.Errorf("同步认证为待审核失败: %w", err)
}
if err := s.aggregateService.SaveCertification(ctx, cert); err != nil {
return fmt.Errorf("保存认证信息失败: %w", err)
}
s.logger.Info("已同步认证为待审核(兼容历史脏数据)", zap.String("user_id", cert.UserID), zap.String("record_id", record.ID))
return nil
}
// convertToResponse 转换实体为响应DTO
func (s *CertificationApplicationServiceImpl) convertToResponse(cert *entities.Certification) *responses.CertificationResponse {
response := &responses.CertificationResponse{
ID: cert.ID,
UserID: cert.UserID,
Status: cert.Status,
StatusName: enums.GetStatusName(cert.Status),
Progress: cert.GetProgress(),
CreatedAt: cert.CreatedAt,
UpdatedAt: cert.UpdatedAt,
InfoSubmittedAt: cert.InfoSubmittedAt,
EnterpriseVerifiedAt: cert.EnterpriseVerifiedAt,
ContractAppliedAt: cert.ContractAppliedAt,
ContractSignedAt: cert.ContractSignedAt,
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
}
// 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 {
// 完成企业认证
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)
}
err = s.userAggregateService.CreateEnterpriseInfo(
ctx,
userID,
record.CompanyName,
record.UnifiedSocialCode,
record.LegalPersonName,
record.LegalPersonID,
record.LegalPersonPhone,
record.EnterpriseAddress,
)
if err != nil {
s.logger.Error("保存企业信息到用户域失败", zap.Error(err))
return fmt.Errorf("保存企业信息失败: %s", err.Error())
} else {
s.logger.Info("企业信息已保存到用户域", zap.String("user_id", userID))
}
// 生成合同
err = s.generateAndAddContractFile(ctx, cert, record.CompanyName, record.LegalPersonName, record.UnifiedSocialCode, record.EnterpriseAddress, record.LegalPersonPhone, record.LegalPersonID)
if err != nil {
return err
}
// 保存认证信息
err = s.aggregateService.SaveCertification(ctx, cert)
if err != nil {
s.logger.Error("保存认证信息失败", zap.Error(err))
return fmt.Errorf("保存认证信息失败: %w", err)
}
return nil
}
// generateAndAddContractFile 生成并添加合同文件的公共方法
func (s *CertificationApplicationServiceImpl) generateAndAddContractFile(
ctx context.Context,
cert *entities.Certification,
companyName string,
legalPersonName string,
unifiedSocialCode string,
enterpriseAddress string,
legalPersonPhone string,
legalPersonID string,
) error {
fileComponent := map[string]string{
"YFCompanyName": companyName,
"YFCompanyName2": companyName,
"YFLegalPersonName": legalPersonName,
"YFLegalPersonName2": legalPersonName,
"YFUnifiedSocialCode": unifiedSocialCode,
"YFEnterpriseAddress": enterpriseAddress,
"YFContactPerson": legalPersonName,
"YFMobile": legalPersonPhone,
"SignDate": time.Now().Format("2006年01月02日"),
"SignDate2": time.Now().Format("2006年01月02日"),
"SignDate3": time.Now().Format("2006年01月02日"),
}
fillTemplateResp, err := s.esignClient.FillTemplate(fileComponent)
if err != nil {
s.logger.Error("生成合同失败", zap.Error(err))
return fmt.Errorf("生成合同失败: %s", err.Error())
}
err = cert.AddContractFileID(fillTemplateResp.FileID, fillTemplateResp.FileDownloadUrl)
if err != nil {
s.logger.Error("加入合同文件ID链接失败", zap.Error(err))
return fmt.Errorf("加入合同文件ID链接失败: %s", err.Error())
}
return nil
}
// updateContractFile 更新合同文件的公共方法
func (s *CertificationApplicationServiceImpl) updateContractFile(ctx context.Context, cert *entities.Certification) error {
// 获取企业信息
enterpriseInfo, err := s.userAggregateService.GetUserWithEnterpriseInfo(ctx, cert.UserID)
if err != nil {
s.logger.Error("获取企业信息失败", zap.Error(err))
return fmt.Errorf("获取企业信息失败: %w", err)
}
// 生成合同
err = s.generateAndAddContractFile(ctx, cert, enterpriseInfo.EnterpriseInfo.CompanyName, enterpriseInfo.EnterpriseInfo.LegalPersonName, enterpriseInfo.EnterpriseInfo.UnifiedSocialCode, enterpriseInfo.EnterpriseInfo.EnterpriseAddress, enterpriseInfo.EnterpriseInfo.LegalPersonPhone, enterpriseInfo.EnterpriseInfo.LegalPersonID)
if err != nil {
return err
}
// 更新认证信息
err = s.aggregateService.SaveCertification(ctx, cert)
if err != nil {
s.logger.Error("保存认证信息失败", zap.Error(err))
return fmt.Errorf("保存认证信息失败: %w", err)
}
return nil
}
// checkAndCompleteEnterpriseVerification 检查并完成企业认证的公共方法
func (s *CertificationApplicationServiceImpl) checkAndCompleteEnterpriseVerification(ctx context.Context, cert *entities.Certification) error {
record, err := s.enterpriseInfoSubmitRecordRepo.FindLatestByUserID(ctx, cert.UserID)
if err != nil {
return fmt.Errorf("查找企业信息失败: %w", err)
}
identity, err := s.esignClient.QueryOrgIdentityInfo(&esign.QueryOrgIdentityRequest{
OrgName: record.CompanyName,
})
if err != nil {
s.logger.Error("查询企业认证信息失败", zap.Error(err))
}
if identity != nil && identity.Data.RealnameStatus == 1 {
err = s.txManager.ExecuteInTx(ctx, func(txCtx context.Context) error {
return s.completeEnterpriseVerification(txCtx, cert, cert.UserID, record.CompanyName, record.LegalPersonName)
})
if err != nil {
return fmt.Errorf("完成企业认证失败: %w", err)
}
}
return nil
}
// checkAndUpdateSignStatus 检查并更新签署状态的公共方法
func (s *CertificationApplicationServiceImpl) checkAndUpdateSignStatus(ctx context.Context, cert *entities.Certification) (string, error) {
var reason string
err := s.txManager.ExecuteInTx(ctx, func(txCtx context.Context) error {
if cert.Status != enums.StatusContractApplied {
return fmt.Errorf("认证状态不正确")
}
detail, err := s.esignClient.QuerySignFlowDetail(cert.EsignFlowID)
if err != nil {
return fmt.Errorf("查询签署流程详情失败: %s", err.Error())
}
if detail.Data.SignFlowStatus == 2 {
err = cert.SignSuccess()
if err != nil {
return fmt.Errorf("合同签署成功失败: %s", err.Error())
}
err = cert.CompleteCertification()
if err != nil {
return fmt.Errorf("完成认证失败: %s", err.Error())
}
// 同步合同信息到用户域
err = s.handleContractAfterSignComplete(txCtx, cert)
if err != nil {
s.logger.Error("同步合同信息到用户域失败", zap.Error(err))
return fmt.Errorf("同步合同信息失败: %s", err.Error())
}
reason = "合同签署成功"
} else if detail.Data.SignFlowStatus == 7 {
err = cert.ContractRejection(detail.Data.SignFlowDescription)
if err != nil {
return fmt.Errorf("合同签署失败: %s", err.Error())
}
reason = "合同签署拒签"
} else if detail.Data.SignFlowStatus == 5 {
err = cert.ContractExpiration()
if err != nil {
return fmt.Errorf("合同签署过期失败: %s", err.Error())
}
reason = "合同签署过期"
} else {
reason = "合同签署中"
}
err = s.aggregateService.SaveCertification(ctx, cert)
if err != nil {
return fmt.Errorf("保存认证信息失败: %s", err.Error())
}
return nil
})
if err != nil {
return "", err
}
return reason, nil
}
// handleContractAfterSignComplete 处理签署完成后的合同
func (s *CertificationApplicationServiceImpl) handleContractAfterSignComplete(ctx context.Context, cert *entities.Certification) error {
// 获取用户的企业信息
user, err := s.userAggregateService.GetUserWithEnterpriseInfo(ctx, cert.UserID)
if err != nil {
return fmt.Errorf("加载用户信息失败: %w", err)
}
if user.EnterpriseInfo == nil {
return fmt.Errorf("用户企业信息不存在")
}
// 1. 获取所有已签署合同文件信息
downloadSignedFileResponse, err := s.esignClient.DownloadSignedFile(cert.EsignFlowID)
if err != nil {
return fmt.Errorf("下载已签署文件失败: %s", err.Error())
}
files := downloadSignedFileResponse.Data.Files
if len(files) == 0 {
return fmt.Errorf("未获取到已签署合同文件")
}
for _, file := range files {
fileUrl := file.DownloadUrl
fileName := file.FileName
fileId := file.FileId
s.logger.Info("下载已签署文件准备", zap.String("file_url", fileUrl), zap.String("file_name", fileName))
// 2. 下载文件内容
fileBytes, err := s.downloadFileContent(ctx, fileUrl)
if err != nil {
s.logger.Error("下载合同文件内容失败", zap.String("file_name", fileName), zap.Error(err))
continue
}
// 3. 上传到七牛云
uploadResult, err := s.qiniuStorageService.UploadFile(ctx, fileBytes, fileName)
if err != nil {
s.logger.Error("上传合同文件到七牛云失败", zap.String("file_name", fileName), zap.Error(err))
continue
}
qiniuURL := uploadResult.URL
s.logger.Info("合同文件已上传七牛云", zap.String("file_name", fileName), zap.String("qiniu_url", qiniuURL))
// 4. 保存到合同聚合根
_, err = s.contractAggregateService.CreateContract(
ctx,
user.EnterpriseInfo.ID,
cert.UserID,
fileName,
user_entities.ContractTypeCooperation,
fileId,
qiniuURL,
)
if err != nil {
s.logger.Error("保存合同信息到聚合根失败", zap.String("file_name", fileName), zap.Error(err))
continue
}
s.logger.Info("合同信息已保存到聚合根", zap.String("file_name", fileName), zap.String("qiniu_url", qiniuURL))
}
// 合同签署完成后的基础激活流程
return s.completeUserActivationWithoutContract(ctx, cert)
}
// downloadFileContent 通过URL下载文件内容
func (s *CertificationApplicationServiceImpl) downloadFileContent(ctx context.Context, fileUrl string) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fileUrl, nil)
if err != nil {
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("下载失败,状态码: %d", resp.StatusCode)
}
return io.ReadAll(resp.Body)
}
// 添加状态相关的元数据
func (s *CertificationApplicationServiceImpl) AddStatusMetadata(ctx context.Context, cert *entities.Certification) (map[string]interface{}, error) {
metadata := make(map[string]interface{})
metadata = cert.GetDataByStatus()
switch cert.Status {
case enums.StatusPending, enums.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,
}
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(
"### 【天远API】企业认证成功\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
}