278 lines
9.0 KiB
Go
278 lines
9.0 KiB
Go
|
|
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
|
||
|
|
}
|