This commit is contained in:
2025-07-11 21:05:58 +08:00
parent 5b4392894f
commit e3d64e7485
74 changed files with 14379 additions and 697 deletions

View File

@@ -0,0 +1,404 @@
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
}

View File

@@ -0,0 +1,287 @@
package services
import (
"context"
"fmt"
"time"
"tyapi-server/internal/domains/certification/entities"
"tyapi-server/internal/domains/certification/enums"
"tyapi-server/internal/domains/certification/repositories"
"go.uber.org/zap"
)
// StateTransition 状态转换规则
type StateTransition struct {
From enums.CertificationStatus
To enums.CertificationStatus
Action string
AllowUser bool // 是否允许用户操作
AllowAdmin bool // 是否允许管理员操作
RequiresValidation bool // 是否需要额外验证
}
// CertificationStateMachine 认证状态机
type CertificationStateMachine struct {
transitions map[enums.CertificationStatus][]StateTransition
certRepo repositories.CertificationRepository
logger *zap.Logger
}
// NewCertificationStateMachine 创建认证状态机
func NewCertificationStateMachine(
certRepo repositories.CertificationRepository,
logger *zap.Logger,
) *CertificationStateMachine {
sm := &CertificationStateMachine{
transitions: make(map[enums.CertificationStatus][]StateTransition),
certRepo: certRepo,
logger: logger,
}
// 初始化状态转换规则
sm.initializeTransitions()
return sm
}
// initializeTransitions 初始化状态转换规则
func (sm *CertificationStateMachine) initializeTransitions() {
transitions := []StateTransition{
// 正常流程转换
{enums.StatusPending, enums.StatusInfoSubmitted, "submit_info", true, false, true},
{enums.StatusInfoSubmitted, enums.StatusFaceVerified, "face_verify", true, false, true},
{enums.StatusFaceVerified, enums.StatusContractApplied, "apply_contract", true, false, false},
{enums.StatusContractApplied, enums.StatusContractPending, "system_process", false, false, false},
{enums.StatusContractPending, enums.StatusContractApproved, "admin_approve", false, true, true},
{enums.StatusContractApproved, enums.StatusContractSigned, "user_sign", true, false, true},
{enums.StatusContractSigned, enums.StatusCompleted, "system_complete", false, false, false},
// 失败和重试转换
{enums.StatusInfoSubmitted, enums.StatusFaceFailed, "face_fail", false, false, false},
{enums.StatusFaceFailed, enums.StatusFaceVerified, "retry_face", true, false, true},
{enums.StatusContractPending, enums.StatusRejected, "admin_reject", false, true, true},
{enums.StatusRejected, enums.StatusInfoSubmitted, "restart_process", true, false, false},
{enums.StatusContractApproved, enums.StatusSignFailed, "sign_fail", false, false, false},
{enums.StatusSignFailed, enums.StatusContractSigned, "retry_sign", true, false, true},
}
// 构建状态转换映射
for _, transition := range transitions {
sm.transitions[transition.From] = append(sm.transitions[transition.From], transition)
}
}
// CanTransition 检查是否可以转换到指定状态
func (sm *CertificationStateMachine) CanTransition(
from enums.CertificationStatus,
to enums.CertificationStatus,
isUser bool,
isAdmin bool,
) (bool, string) {
validTransitions, exists := sm.transitions[from]
if !exists {
return false, "当前状态不支持任何转换"
}
for _, transition := range validTransitions {
if transition.To == to {
if isUser && !transition.AllowUser {
return false, "用户不允许执行此操作"
}
if isAdmin && !transition.AllowAdmin {
return false, "管理员不允许执行此操作"
}
if !isUser && !isAdmin && (transition.AllowUser || transition.AllowAdmin) {
return false, "此操作需要用户或管理员权限"
}
return true, ""
}
}
return false, "不支持的状态转换"
}
// TransitionTo 执行状态转换
func (sm *CertificationStateMachine) TransitionTo(
ctx context.Context,
certificationID string,
targetStatus enums.CertificationStatus,
isUser bool,
isAdmin bool,
metadata map[string]interface{},
) error {
// 获取当前认证记录
cert, err := sm.certRepo.GetByID(ctx, certificationID)
if err != nil {
return fmt.Errorf("获取认证记录失败: %w", err)
}
// 检查是否可以转换
canTransition, reason := sm.CanTransition(cert.Status, targetStatus, isUser, isAdmin)
if !canTransition {
return fmt.Errorf("状态转换失败: %s", reason)
}
// 执行状态转换前的验证
if err := sm.validateTransition(ctx, cert, targetStatus, metadata); err != nil {
return fmt.Errorf("状态转换验证失败: %w", err)
}
// 更新状态和时间戳
oldStatus := cert.Status
cert.Status = targetStatus
sm.updateTimestamp(cert, targetStatus)
// 更新其他字段
sm.updateCertificationFields(cert, targetStatus, metadata)
// 保存到数据库
if err := sm.certRepo.Update(ctx, cert); err != nil {
return fmt.Errorf("保存状态转换失败: %w", err)
}
sm.logger.Info("认证状态转换成功",
zap.String("certification_id", certificationID),
zap.String("from_status", string(oldStatus)),
zap.String("to_status", string(targetStatus)),
zap.Bool("is_user", isUser),
zap.Bool("is_admin", isAdmin),
)
return nil
}
// updateTimestamp 更新对应的时间戳字段
func (sm *CertificationStateMachine) updateTimestamp(cert *entities.Certification, status enums.CertificationStatus) {
now := time.Now()
switch status {
case enums.StatusInfoSubmitted:
cert.InfoSubmittedAt = &now
case enums.StatusFaceVerified:
cert.FaceVerifiedAt = &now
case enums.StatusContractApplied:
cert.ContractAppliedAt = &now
case enums.StatusContractApproved:
cert.ContractApprovedAt = &now
case enums.StatusContractSigned:
cert.ContractSignedAt = &now
case enums.StatusCompleted:
cert.CompletedAt = &now
}
}
// updateCertificationFields 根据状态更新认证记录的其他字段
func (sm *CertificationStateMachine) updateCertificationFields(
cert *entities.Certification,
status enums.CertificationStatus,
metadata map[string]interface{},
) {
switch status {
case enums.StatusContractApproved:
if adminID, ok := metadata["admin_id"].(string); ok {
cert.AdminID = &adminID
}
if approvalNotes, ok := metadata["approval_notes"].(string); ok {
cert.ApprovalNotes = approvalNotes
}
if signingURL, ok := metadata["signing_url"].(string); ok {
cert.SigningURL = signingURL
}
case enums.StatusRejected:
if adminID, ok := metadata["admin_id"].(string); ok {
cert.AdminID = &adminID
}
if rejectReason, ok := metadata["reject_reason"].(string); ok {
cert.RejectReason = rejectReason
}
case enums.StatusContractSigned:
if contractURL, ok := metadata["contract_url"].(string); ok {
cert.ContractURL = contractURL
}
}
}
// validateTransition 验证状态转换的有效性
func (sm *CertificationStateMachine) validateTransition(
ctx context.Context,
cert *entities.Certification,
targetStatus enums.CertificationStatus,
metadata map[string]interface{},
) error {
switch targetStatus {
case enums.StatusInfoSubmitted:
// 验证企业信息是否完整
if cert.EnterpriseID == nil {
return fmt.Errorf("企业信息未提交")
}
case enums.StatusFaceVerified:
// 验证人脸识别是否成功
// 这里可以添加人脸识别结果的验证逻辑
case enums.StatusContractApproved:
// 验证管理员审核信息
if metadata["signing_url"] == nil || metadata["signing_url"].(string) == "" {
return fmt.Errorf("缺少合同签署链接")
}
case enums.StatusRejected:
// 验证拒绝原因
if metadata["reject_reason"] == nil || metadata["reject_reason"].(string) == "" {
return fmt.Errorf("缺少拒绝原因")
}
case enums.StatusContractSigned:
// 验证合同签署信息
if cert.SigningURL == "" {
return fmt.Errorf("缺少合同签署链接")
}
}
return nil
}
// GetValidNextStatuses 获取当前状态可以转换到的下一个状态列表
func (sm *CertificationStateMachine) GetValidNextStatuses(
currentStatus enums.CertificationStatus,
isUser bool,
isAdmin bool,
) []enums.CertificationStatus {
var validStatuses []enums.CertificationStatus
transitions, exists := sm.transitions[currentStatus]
if !exists {
return validStatuses
}
for _, transition := range transitions {
if (isUser && transition.AllowUser) || (isAdmin && transition.AllowAdmin) {
validStatuses = append(validStatuses, transition.To)
}
}
return validStatuses
}
// GetTransitionAction 获取状态转换对应的操作名称
func (sm *CertificationStateMachine) GetTransitionAction(
from enums.CertificationStatus,
to enums.CertificationStatus,
) string {
transitions, exists := sm.transitions[from]
if !exists {
return ""
}
for _, transition := range transitions {
if transition.To == to {
return transition.Action
}
}
return ""
}