package services import ( "context" "fmt" "io" "mime/multipart" "time" "tyapi-server/internal/domains/finance/entities" "tyapi-server/internal/domains/finance/events" "tyapi-server/internal/domains/finance/repositories" "tyapi-server/internal/domains/finance/value_objects" "tyapi-server/internal/infrastructure/external/storage" "github.com/shopspring/decimal" "go.uber.org/zap" ) // ApplyInvoiceRequest 申请开票请求 type ApplyInvoiceRequest struct { InvoiceType value_objects.InvoiceType `json:"invoice_type" binding:"required"` Amount string `json:"amount" binding:"required"` InvoiceInfo *value_objects.InvoiceInfo `json:"invoice_info" binding:"required"` } // ApproveInvoiceRequest 通过发票申请请求 type ApproveInvoiceRequest struct { AdminNotes string `json:"admin_notes"` } // RejectInvoiceRequest 拒绝发票申请请求 type RejectInvoiceRequest struct { Reason string `json:"reason" binding:"required"` } // InvoiceAggregateService 发票聚合服务接口 // 职责:协调发票申请聚合根的生命周期,调用领域服务进行业务规则验证,发布领域事件 type InvoiceAggregateService interface { // 申请开票 ApplyInvoice(ctx context.Context, userID string, req ApplyInvoiceRequest) (*entities.InvoiceApplication, error) // 通过发票申请(上传发票) ApproveInvoiceApplication(ctx context.Context, applicationID string, file multipart.File, req ApproveInvoiceRequest) error // 拒绝发票申请 RejectInvoiceApplication(ctx context.Context, applicationID string, req RejectInvoiceRequest) error } // InvoiceAggregateServiceImpl 发票聚合服务实现 type InvoiceAggregateServiceImpl struct { applicationRepo repositories.InvoiceApplicationRepository userInvoiceInfoRepo repositories.UserInvoiceInfoRepository domainService InvoiceDomainService qiniuStorageService *storage.QiNiuStorageService logger *zap.Logger eventPublisher EventPublisher } // EventPublisher 事件发布器接口 type EventPublisher interface { PublishInvoiceApplicationCreated(ctx context.Context, event *events.InvoiceApplicationCreatedEvent) error PublishInvoiceApplicationApproved(ctx context.Context, event *events.InvoiceApplicationApprovedEvent) error PublishInvoiceApplicationRejected(ctx context.Context, event *events.InvoiceApplicationRejectedEvent) error PublishInvoiceFileUploaded(ctx context.Context, event *events.InvoiceFileUploadedEvent) error } // NewInvoiceAggregateService 创建发票聚合服务 func NewInvoiceAggregateService( applicationRepo repositories.InvoiceApplicationRepository, userInvoiceInfoRepo repositories.UserInvoiceInfoRepository, domainService InvoiceDomainService, qiniuStorageService *storage.QiNiuStorageService, logger *zap.Logger, eventPublisher EventPublisher, ) InvoiceAggregateService { return &InvoiceAggregateServiceImpl{ applicationRepo: applicationRepo, userInvoiceInfoRepo: userInvoiceInfoRepo, domainService: domainService, qiniuStorageService: qiniuStorageService, logger: logger, eventPublisher: eventPublisher, } } // ApplyInvoice 申请开票 func (s *InvoiceAggregateServiceImpl) ApplyInvoice(ctx context.Context, userID string, req ApplyInvoiceRequest) (*entities.InvoiceApplication, error) { // 1. 解析金额 amount, err := decimal.NewFromString(req.Amount) if err != nil { return nil, fmt.Errorf("无效的金额格式: %w", err) } // 2. 验证发票信息 if err := s.domainService.ValidateInvoiceInfo(ctx, req.InvoiceInfo, req.InvoiceType); err != nil { return nil, fmt.Errorf("发票信息验证失败: %w", err) } // 3. 获取用户开票信息 userInvoiceInfo, err := s.userInvoiceInfoRepo.FindByUserID(ctx, userID) if err != nil { return nil, fmt.Errorf("获取用户开票信息失败: %w", err) } if userInvoiceInfo == nil { return nil, fmt.Errorf("用户开票信息不存在") } // 4. 创建发票申请聚合根 application := entities.NewInvoiceApplication(userID, req.InvoiceType, amount, userInvoiceInfo.ID) // 5. 设置开票信息快照 application.SetInvoiceInfoSnapshot(req.InvoiceInfo) // 6. 验证聚合根业务规则 if err := s.domainService.ValidateInvoiceApplication(ctx, application); err != nil { return nil, fmt.Errorf("发票申请业务规则验证失败: %w", err) } // 7. 保存聚合根 if err := s.applicationRepo.Create(ctx, application); err != nil { return nil, fmt.Errorf("保存发票申请失败: %w", err) } // 8. 发布领域事件 event := events.NewInvoiceApplicationCreatedEvent( application.ID, application.UserID, application.InvoiceType, application.Amount, application.CompanyName, application.ReceivingEmail, ) if err := s.eventPublisher.PublishInvoiceApplicationCreated(ctx, event); err != nil { // 记录错误但不影响主流程 fmt.Printf("发布发票申请创建事件失败: %v\n", err) } return application, nil } // ApproveInvoiceApplication 通过发票申请(上传发票) func (s *InvoiceAggregateServiceImpl) ApproveInvoiceApplication(ctx context.Context, applicationID string, file multipart.File, req ApproveInvoiceRequest) error { // 1. 获取发票申请 application, err := s.applicationRepo.FindByID(ctx, applicationID) if err != nil { return fmt.Errorf("获取发票申请失败: %w", err) } if application == nil { return fmt.Errorf("发票申请不存在") } // 2. 验证状态转换 if err := s.domainService.ValidateStatusTransition(application.Status, entities.ApplicationStatusCompleted); err != nil { return fmt.Errorf("状态转换验证失败: %w", err) } // 3. 处理文件上传 // 读取文件内容 fileBytes, err := io.ReadAll(file) if err != nil { s.logger.Error("读取上传文件失败", zap.Error(err)) return fmt.Errorf("读取上传文件失败: %w", err) } // 生成文件名(使用时间戳确保唯一性) fileName := fmt.Sprintf("invoice_%s_%d.pdf", applicationID, time.Now().Unix()) // 上传文件到七牛云 uploadResult, err := s.qiniuStorageService.UploadFile(ctx, fileBytes, fileName) if err != nil { s.logger.Error("上传发票文件到七牛云失败", zap.String("file_name", fileName), zap.Error(err)) return fmt.Errorf("上传发票文件到七牛云失败: %w", err) } // 从上传结果获取文件信息 fileID := uploadResult.Key fileURL := uploadResult.URL fileSize := uploadResult.Size // 4. 更新聚合根状态 application.MarkCompleted("admin_user_id") application.SetFileInfo(fileID, fileName, fileURL, fileSize) application.AdminNotes = &req.AdminNotes // 5. 保存聚合根 if err := s.applicationRepo.Update(ctx, application); err != nil { return fmt.Errorf("更新发票申请失败: %w", err) } // 6. 发布领域事件 approvedEvent := events.NewInvoiceApplicationApprovedEvent( application.ID, application.UserID, application.Amount, application.ReceivingEmail, ) if err := s.eventPublisher.PublishInvoiceApplicationApproved(ctx, approvedEvent); err != nil { s.logger.Error("发布发票申请通过事件失败", zap.String("application_id", applicationID), zap.Error(err), ) // 事件发布失败不影响主流程,只记录日志 } else { s.logger.Info("发票申请通过事件发布成功", zap.String("application_id", applicationID), ) } fileUploadedEvent := events.NewInvoiceFileUploadedEvent( application.ID, application.UserID, fileID, fileName, fileURL, application.ReceivingEmail, application.CompanyName, application.Amount, application.InvoiceType, ) if err := s.eventPublisher.PublishInvoiceFileUploaded(ctx, fileUploadedEvent); err != nil { s.logger.Error("发布发票文件上传事件失败", zap.String("application_id", applicationID), zap.Error(err), ) // 事件发布失败不影响主流程,只记录日志 } else { s.logger.Info("发票文件上传事件发布成功", zap.String("application_id", applicationID), ) } return nil } // RejectInvoiceApplication 拒绝发票申请 func (s *InvoiceAggregateServiceImpl) RejectInvoiceApplication(ctx context.Context, applicationID string, req RejectInvoiceRequest) error { // 1. 获取发票申请 application, err := s.applicationRepo.FindByID(ctx, applicationID) if err != nil { return fmt.Errorf("获取发票申请失败: %w", err) } if application == nil { return fmt.Errorf("发票申请不存在") } // 2. 验证状态转换 if err := s.domainService.ValidateStatusTransition(application.Status, entities.ApplicationStatusRejected); err != nil { return fmt.Errorf("状态转换验证失败: %w", err) } // 3. 更新聚合根状态 application.MarkRejected(req.Reason, "admin_user_id") // 4. 保存聚合根 if err := s.applicationRepo.Update(ctx, application); err != nil { return fmt.Errorf("更新发票申请失败: %w", err) } // 5. 发布领域事件 event := events.NewInvoiceApplicationRejectedEvent( application.ID, application.UserID, req.Reason, application.ReceivingEmail, ) if err := s.eventPublisher.PublishInvoiceApplicationRejected(ctx, event); err != nil { fmt.Printf("发布发票申请拒绝事件失败: %v\n", err) } return nil }