Files
tyapi-server/internal/domains/certification/services/certification_service.go
2025-07-11 21:05:58 +08:00

405 lines
14 KiB
Go

package services
import (
"context"
"fmt"
"time"
"github.com/google/uuid"
"go.uber.org/zap"
"tyapi-server/internal/domains/certification/dto"
"tyapi-server/internal/domains/certification/entities"
"tyapi-server/internal/domains/certification/enums"
"tyapi-server/internal/domains/certification/repositories"
"tyapi-server/internal/shared/ocr"
"tyapi-server/internal/shared/storage"
)
// CertificationService 认证服务
type CertificationService struct {
certRepo repositories.CertificationRepository
enterpriseRepo repositories.EnterpriseRepository
licenseRepo repositories.LicenseUploadRecordRepository
faceVerifyRepo repositories.FaceVerifyRecordRepository
contractRepo repositories.ContractRecordRepository
stateMachine *CertificationStateMachine
storageService storage.StorageService
ocrService ocr.OCRService
logger *zap.Logger
}
// NewCertificationService 创建认证服务
func NewCertificationService(
certRepo repositories.CertificationRepository,
enterpriseRepo repositories.EnterpriseRepository,
licenseRepo repositories.LicenseUploadRecordRepository,
faceVerifyRepo repositories.FaceVerifyRecordRepository,
contractRepo repositories.ContractRecordRepository,
stateMachine *CertificationStateMachine,
storageService storage.StorageService,
ocrService ocr.OCRService,
logger *zap.Logger,
) *CertificationService {
return &CertificationService{
certRepo: certRepo,
enterpriseRepo: enterpriseRepo,
licenseRepo: licenseRepo,
faceVerifyRepo: faceVerifyRepo,
contractRepo: contractRepo,
stateMachine: stateMachine,
storageService: storageService,
ocrService: ocrService,
logger: logger,
}
}
// CreateCertification 创建认证申请
func (s *CertificationService) CreateCertification(ctx context.Context, userID string) (*dto.CertificationCreateResponse, error) {
s.logger.Info("创建认证申请", zap.String("user_id", userID))
// 检查用户是否已有认证申请
existingCert, err := s.certRepo.GetByUserID(ctx, userID)
if err == nil && existingCert != nil {
// 如果已存在且不是最终状态,返回现有申请
if !enums.IsFinalStatus(existingCert.Status) {
return &dto.CertificationCreateResponse{
ID: existingCert.ID,
UserID: existingCert.UserID,
Status: existingCert.Status,
}, nil
}
}
// 创建新的认证申请
certification := &entities.Certification{
ID: uuid.New().String(),
UserID: userID,
Status: enums.StatusPending,
}
if err := s.certRepo.Create(ctx, certification); err != nil {
return nil, fmt.Errorf("创建认证申请失败: %w", err)
}
s.logger.Info("认证申请创建成功",
zap.String("certification_id", certification.ID),
zap.String("user_id", userID),
)
return &dto.CertificationCreateResponse{
ID: certification.ID,
UserID: certification.UserID,
Status: certification.Status,
}, nil
}
// UploadLicense 上传营业执照并进行OCR识别
func (s *CertificationService) UploadLicense(ctx context.Context, userID string, fileBytes []byte, fileName string) (*dto.UploadLicenseResponse, error) {
s.logger.Info("上传营业执照",
zap.String("user_id", userID),
zap.String("file_name", fileName),
zap.Int("file_size", len(fileBytes)),
)
// 1. 上传文件到存储服务
uploadResult, err := s.storageService.UploadFile(ctx, fileBytes, fileName)
if err != nil {
s.logger.Error("文件上传失败", zap.Error(err))
return nil, fmt.Errorf("文件上传失败: %w", err)
}
// 2. 创建上传记录
uploadRecord := &entities.LicenseUploadRecord{
ID: uuid.New().String(),
UserID: userID,
OriginalFileName: fileName,
FileSize: int64(len(fileBytes)),
FileType: uploadResult.MimeType,
FileURL: uploadResult.URL,
QiNiuKey: uploadResult.Key,
OCRProcessed: false,
OCRSuccess: false,
}
if err := s.licenseRepo.Create(ctx, uploadRecord); err != nil {
s.logger.Error("创建上传记录失败", zap.Error(err))
return nil, fmt.Errorf("创建上传记录失败: %w", err)
}
// 3. 尝试OCR识别
var enterpriseInfo *dto.OCREnterpriseInfo
var ocrError string
ocrResult, err := s.ocrService.RecognizeBusinessLicense(ctx, uploadResult.URL)
if err != nil {
s.logger.Warn("OCR识别失败", zap.Error(err))
ocrError = err.Error()
uploadRecord.OCRProcessed = true
uploadRecord.OCRSuccess = false
uploadRecord.OCRErrorMessage = ocrError
} else {
s.logger.Info("OCR识别成功",
zap.String("company_name", ocrResult.CompanyName),
zap.Float64("confidence", ocrResult.Confidence),
)
enterpriseInfo = ocrResult
uploadRecord.OCRProcessed = true
uploadRecord.OCRSuccess = true
uploadRecord.OCRConfidence = ocrResult.Confidence
// 存储OCR原始数据
if rawData, err := s.serializeOCRResult(ocrResult); err == nil {
uploadRecord.OCRRawData = rawData
}
}
// 更新上传记录
if err := s.licenseRepo.Update(ctx, uploadRecord); err != nil {
s.logger.Warn("更新上传记录失败", zap.Error(err))
}
return &dto.UploadLicenseResponse{
UploadRecordID: uploadRecord.ID,
FileURL: uploadResult.URL,
OCRProcessed: uploadRecord.OCRProcessed,
OCRSuccess: uploadRecord.OCRSuccess,
EnterpriseInfo: enterpriseInfo,
OCRErrorMessage: ocrError,
}, nil
}
// SubmitEnterpriseInfo 提交企业信息
func (s *CertificationService) SubmitEnterpriseInfo(ctx context.Context, certificationID string, req *dto.SubmitEnterpriseInfoRequest) (*dto.SubmitEnterpriseInfoResponse, error) {
s.logger.Info("提交企业信息",
zap.String("certification_id", certificationID),
zap.String("company_name", req.CompanyName),
)
// 1. 获取认证记录
cert, err := s.certRepo.GetByID(ctx, certificationID)
if err != nil {
return nil, fmt.Errorf("获取认证记录失败: %w", err)
}
// 2. 检查状态是否允许提交企业信息
if !cert.CanTransitionTo(enums.StatusInfoSubmitted) {
return nil, fmt.Errorf("当前状态不允许提交企业信息,当前状态: %s", enums.GetStatusName(cert.Status))
}
// 3. 检查统一社会信用代码是否已存在
exists, err := s.enterpriseRepo.ExistsByUnifiedSocialCode(ctx, req.UnifiedSocialCode)
if err != nil {
return nil, fmt.Errorf("检查企业信息失败: %w", err)
}
if exists {
return nil, fmt.Errorf("该统一社会信用代码已被使用")
}
// 4. 创建企业信息
enterprise := &entities.Enterprise{
ID: uuid.New().String(),
CertificationID: certificationID,
CompanyName: req.CompanyName,
UnifiedSocialCode: req.UnifiedSocialCode,
LegalPersonName: req.LegalPersonName,
LegalPersonID: req.LegalPersonID,
LicenseUploadRecordID: req.LicenseUploadRecordID,
IsOCRVerified: false,
IsFaceVerified: false,
}
if err := s.enterpriseRepo.Create(ctx, enterprise); err != nil {
return nil, fmt.Errorf("创建企业信息失败: %w", err)
}
// 5. 更新认证记录状态
cert.EnterpriseID = &enterprise.ID
if err := s.stateMachine.TransitionTo(ctx, certificationID, enums.StatusInfoSubmitted, true, false, nil); err != nil {
return nil, fmt.Errorf("更新认证状态失败: %w", err)
}
s.logger.Info("企业信息提交成功",
zap.String("certification_id", certificationID),
zap.String("enterprise_id", enterprise.ID),
)
return &dto.SubmitEnterpriseInfoResponse{
ID: certificationID,
Status: enums.StatusInfoSubmitted,
Enterprise: &dto.EnterpriseInfoResponse{
ID: enterprise.ID,
CertificationID: enterprise.CertificationID,
CompanyName: enterprise.CompanyName,
UnifiedSocialCode: enterprise.UnifiedSocialCode,
LegalPersonName: enterprise.LegalPersonName,
LegalPersonID: enterprise.LegalPersonID,
LicenseUploadRecordID: enterprise.LicenseUploadRecordID,
IsOCRVerified: enterprise.IsOCRVerified,
IsFaceVerified: enterprise.IsFaceVerified,
CreatedAt: enterprise.CreatedAt,
UpdatedAt: enterprise.UpdatedAt,
},
}, nil
}
// GetCertificationStatus 获取认证状态
func (s *CertificationService) GetCertificationStatus(ctx context.Context, userID string) (*dto.CertificationStatusResponse, error) {
s.logger.Info("获取认证状态", zap.String("user_id", userID))
// 获取用户的认证记录
cert, err := s.certRepo.GetByUserID(ctx, userID)
if err != nil {
return nil, fmt.Errorf("获取认证记录失败: %w", err)
}
// 获取企业信息
var enterprise *dto.EnterpriseInfoResponse
if cert.EnterpriseID != nil {
ent, err := s.enterpriseRepo.GetByID(ctx, *cert.EnterpriseID)
if err == nil {
enterprise = &dto.EnterpriseInfoResponse{
ID: ent.ID,
CertificationID: ent.CertificationID,
CompanyName: ent.CompanyName,
UnifiedSocialCode: ent.UnifiedSocialCode,
LegalPersonName: ent.LegalPersonName,
LegalPersonID: ent.LegalPersonID,
LicenseUploadRecordID: ent.LicenseUploadRecordID,
IsOCRVerified: ent.IsOCRVerified,
IsFaceVerified: ent.IsFaceVerified,
CreatedAt: ent.CreatedAt,
UpdatedAt: ent.UpdatedAt,
}
}
}
return &dto.CertificationStatusResponse{
ID: cert.ID,
UserID: cert.UserID,
Status: cert.Status,
StatusName: enums.GetStatusName(cert.Status),
Progress: cert.GetProgressPercentage(),
IsUserActionRequired: cert.IsUserActionRequired(),
IsAdminActionRequired: cert.IsAdminActionRequired(),
InfoSubmittedAt: cert.InfoSubmittedAt,
FaceVerifiedAt: cert.FaceVerifiedAt,
ContractAppliedAt: cert.ContractAppliedAt,
ContractApprovedAt: cert.ContractApprovedAt,
ContractSignedAt: cert.ContractSignedAt,
CompletedAt: cert.CompletedAt,
Enterprise: enterprise,
ContractURL: cert.ContractURL,
SigningURL: cert.SigningURL,
RejectReason: cert.RejectReason,
CreatedAt: cert.CreatedAt,
UpdatedAt: cert.UpdatedAt,
}, nil
}
// InitiateFaceVerify 初始化人脸识别
func (s *CertificationService) InitiateFaceVerify(ctx context.Context, certificationID string, req *dto.FaceVerifyRequest) (*dto.FaceVerifyResponse, error) {
s.logger.Info("初始化人脸识别",
zap.String("certification_id", certificationID),
zap.String("real_name", req.RealName),
)
// 1. 获取认证记录
cert, err := s.certRepo.GetByID(ctx, certificationID)
if err != nil {
return nil, fmt.Errorf("获取认证记录失败: %w", err)
}
// 2. 检查状态
if cert.Status != enums.StatusInfoSubmitted && cert.Status != enums.StatusFaceFailed {
return nil, fmt.Errorf("当前状态不允许进行人脸识别,当前状态: %s", enums.GetStatusName(cert.Status))
}
// 3. 创建人脸识别记录
verifyRecord := &entities.FaceVerifyRecord{
ID: uuid.New().String(),
CertificationID: certificationID,
UserID: cert.UserID,
CertifyID: fmt.Sprintf("cert_%s_%d", certificationID, time.Now().Unix()),
RealName: req.RealName,
IDCardNumber: req.IDCardNumber,
ReturnURL: req.ReturnURL,
Status: "PROCESSING",
ExpiresAt: time.Now().Add(24 * time.Hour), // 24小时过期
}
// TODO: 实际调用阿里云人脸识别API
// 这里是模拟实现
verifyRecord.VerifyURL = fmt.Sprintf("https://face-verify.aliyun.com/verify?certifyId=%s", verifyRecord.CertifyID)
if err := s.faceVerifyRepo.Create(ctx, verifyRecord); err != nil {
return nil, fmt.Errorf("创建人脸识别记录失败: %w", err)
}
s.logger.Info("人脸识别初始化成功",
zap.String("certification_id", certificationID),
zap.String("certify_id", verifyRecord.CertifyID),
)
return &dto.FaceVerifyResponse{
CertifyID: verifyRecord.CertifyID,
VerifyURL: verifyRecord.VerifyURL,
ExpiresAt: verifyRecord.ExpiresAt,
}, nil
}
// ApplyContract 申请电子合同
func (s *CertificationService) ApplyContract(ctx context.Context, certificationID string) (*dto.ApplyContractResponse, error) {
s.logger.Info("申请电子合同", zap.String("certification_id", certificationID))
// 1. 获取认证记录
cert, err := s.certRepo.GetByID(ctx, certificationID)
if err != nil {
return nil, fmt.Errorf("获取认证记录失败: %w", err)
}
// 2. 检查状态
if !cert.CanTransitionTo(enums.StatusContractApplied) {
return nil, fmt.Errorf("当前状态不允许申请合同,当前状态: %s", enums.GetStatusName(cert.Status))
}
// 3. 转换状态
if err := s.stateMachine.TransitionTo(ctx, certificationID, enums.StatusContractApplied, true, false, nil); err != nil {
return nil, fmt.Errorf("更新认证状态失败: %w", err)
}
// 4. 自动转换到待审核状态
if err := s.stateMachine.TransitionTo(ctx, certificationID, enums.StatusContractPending, false, false, nil); err != nil {
s.logger.Warn("自动转换到待审核状态失败", zap.Error(err))
}
// 5. 创建合同记录
contractRecord := &entities.ContractRecord{
ID: uuid.New().String(),
CertificationID: certificationID,
UserID: cert.UserID,
ContractType: "ENTERPRISE_CERTIFICATION",
Status: "PENDING",
}
if err := s.contractRepo.Create(ctx, contractRecord); err != nil {
s.logger.Warn("创建合同记录失败", zap.Error(err))
}
// TODO: 发送通知给管理员
s.logger.Info("合同申请成功", zap.String("certification_id", certificationID))
return &dto.ApplyContractResponse{
ID: certificationID,
Status: enums.StatusContractPending,
ContractAppliedAt: time.Now(),
}, nil
}
// serializeOCRResult 序列化OCR结果
func (s *CertificationService) serializeOCRResult(result *dto.OCREnterpriseInfo) (string, error) {
// 简单的JSON序列化
return fmt.Sprintf(`{"company_name":"%s","unified_social_code":"%s","legal_person_name":"%s","legal_person_id":"%s","confidence":%f}`,
result.CompanyName, result.UnifiedSocialCode, result.LegalPersonName, result.LegalPersonID, result.Confidence), nil
}