751 lines
		
	
	
		
			27 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			751 lines
		
	
	
		
			27 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package finance
 | ||
| 
 | ||
| import (
 | ||
| 	"context"
 | ||
| 	"fmt"
 | ||
| 	"mime/multipart"
 | ||
| 	"time"
 | ||
| 
 | ||
| 	"tyapi-server/internal/application/finance/dto"
 | ||
| 	"tyapi-server/internal/domains/finance/entities"
 | ||
| 	finance_repo "tyapi-server/internal/domains/finance/repositories"
 | ||
| 	"tyapi-server/internal/domains/finance/services"
 | ||
| 	"tyapi-server/internal/domains/finance/value_objects"
 | ||
| 	user_repo "tyapi-server/internal/domains/user/repositories"
 | ||
| 	user_service "tyapi-server/internal/domains/user/services"
 | ||
| 	"tyapi-server/internal/infrastructure/external/storage"
 | ||
| 
 | ||
| 	"github.com/shopspring/decimal"
 | ||
| 	"go.uber.org/zap"
 | ||
| )
 | ||
| 
 | ||
| // ==================== 用户端发票应用服务 ====================
 | ||
| 
 | ||
| // InvoiceApplicationService 发票应用服务接口
 | ||
| // 职责:跨域协调、数据聚合、事务管理、外部服务调用
 | ||
| type InvoiceApplicationService interface {
 | ||
| 	// ApplyInvoice 申请开票
 | ||
| 	ApplyInvoice(ctx context.Context, userID string, req ApplyInvoiceRequest) (*dto.InvoiceApplicationResponse, error)
 | ||
| 
 | ||
| 	// GetUserInvoiceInfo 获取用户发票信息
 | ||
| 	GetUserInvoiceInfo(ctx context.Context, userID string) (*dto.InvoiceInfoResponse, error)
 | ||
| 
 | ||
| 	// UpdateUserInvoiceInfo 更新用户发票信息
 | ||
| 	UpdateUserInvoiceInfo(ctx context.Context, userID string, req UpdateInvoiceInfoRequest) error
 | ||
| 
 | ||
| 	// GetUserInvoiceRecords 获取用户开票记录
 | ||
| 	GetUserInvoiceRecords(ctx context.Context, userID string, req GetInvoiceRecordsRequest) (*dto.InvoiceRecordsResponse, error)
 | ||
| 
 | ||
| 	// DownloadInvoiceFile 下载发票文件
 | ||
| 	DownloadInvoiceFile(ctx context.Context, userID string, applicationID string) (*dto.FileDownloadResponse, error)
 | ||
| 
 | ||
| 	// GetAvailableAmount 获取可开票金额
 | ||
| 	GetAvailableAmount(ctx context.Context, userID string) (*dto.AvailableAmountResponse, error)
 | ||
| }
 | ||
| 
 | ||
| // InvoiceApplicationServiceImpl 发票应用服务实现
 | ||
| type InvoiceApplicationServiceImpl struct {
 | ||
| 	// 仓储层依赖
 | ||
| 	invoiceRepo         finance_repo.InvoiceApplicationRepository
 | ||
| 	userInvoiceInfoRepo finance_repo.UserInvoiceInfoRepository
 | ||
| 	userRepo            user_repo.UserRepository
 | ||
| 	rechargeRecordRepo  finance_repo.RechargeRecordRepository
 | ||
| 	walletRepo          finance_repo.WalletRepository
 | ||
| 
 | ||
| 	// 领域服务依赖
 | ||
| 	invoiceDomainService    services.InvoiceDomainService
 | ||
| 	invoiceAggregateService services.InvoiceAggregateService
 | ||
| 	userInvoiceInfoService  services.UserInvoiceInfoService
 | ||
| 	userAggregateService    user_service.UserAggregateService
 | ||
| 
 | ||
| 	// 外部服务依赖
 | ||
| 	storageService *storage.QiNiuStorageService
 | ||
| 	logger         *zap.Logger
 | ||
| }
 | ||
| 
 | ||
| // NewInvoiceApplicationService 创建发票应用服务
 | ||
| func NewInvoiceApplicationService(
 | ||
| 	invoiceRepo finance_repo.InvoiceApplicationRepository,
 | ||
| 	userInvoiceInfoRepo finance_repo.UserInvoiceInfoRepository,
 | ||
| 	userRepo user_repo.UserRepository,
 | ||
| 	userAggregateService user_service.UserAggregateService,
 | ||
| 	rechargeRecordRepo finance_repo.RechargeRecordRepository,
 | ||
| 	walletRepo finance_repo.WalletRepository,
 | ||
| 	invoiceDomainService services.InvoiceDomainService,
 | ||
| 	invoiceAggregateService services.InvoiceAggregateService,
 | ||
| 	userInvoiceInfoService services.UserInvoiceInfoService,
 | ||
| 	storageService *storage.QiNiuStorageService,
 | ||
| 	logger *zap.Logger,
 | ||
| ) InvoiceApplicationService {
 | ||
| 	return &InvoiceApplicationServiceImpl{
 | ||
| 		invoiceRepo:             invoiceRepo,
 | ||
| 		userInvoiceInfoRepo:     userInvoiceInfoRepo,
 | ||
| 		userRepo:                userRepo,
 | ||
| 		userAggregateService:    userAggregateService,
 | ||
| 		rechargeRecordRepo:      rechargeRecordRepo,
 | ||
| 		walletRepo:              walletRepo,
 | ||
| 		invoiceDomainService:    invoiceDomainService,
 | ||
| 		invoiceAggregateService: invoiceAggregateService,
 | ||
| 		userInvoiceInfoService:  userInvoiceInfoService,
 | ||
| 		storageService:          storageService,
 | ||
| 		logger:                  logger,
 | ||
| 	}
 | ||
| }
 | ||
| 
 | ||
| // ApplyInvoice 申请开票
 | ||
| func (s *InvoiceApplicationServiceImpl) ApplyInvoice(ctx context.Context, userID string, req ApplyInvoiceRequest) (*dto.InvoiceApplicationResponse, error) {
 | ||
| 	// 1. 验证用户是否存在
 | ||
| 	user, err := s.userRepo.GetByID(ctx, userID)
 | ||
| 	if err != nil {
 | ||
| 		return nil, err
 | ||
| 	}
 | ||
| 	if user.ID == "" {
 | ||
| 		return nil, fmt.Errorf("用户不存在")
 | ||
| 	}
 | ||
| 
 | ||
| 	// 2. 验证发票类型
 | ||
| 	invoiceType := value_objects.InvoiceType(req.InvoiceType)
 | ||
| 	if !invoiceType.IsValid() {
 | ||
| 		return nil, fmt.Errorf("无效的发票类型")
 | ||
| 	}
 | ||
| 
 | ||
| 	// 3. 获取用户企业认证信息
 | ||
| 	userWithEnterprise, err := s.userAggregateService.GetUserWithEnterpriseInfo(ctx, userID)
 | ||
| 	if err != nil {
 | ||
| 		return nil, fmt.Errorf("获取用户企业认证信息失败: %w", err)
 | ||
| 	}
 | ||
| 
 | ||
| 	// 4. 检查用户是否有企业认证信息
 | ||
| 	if userWithEnterprise.EnterpriseInfo == nil {
 | ||
| 		return nil, fmt.Errorf("用户未完成企业认证,无法申请开票")
 | ||
| 	}
 | ||
| 
 | ||
| 	// 5. 获取用户开票信息
 | ||
| 	userInvoiceInfo, err := s.userInvoiceInfoService.GetUserInvoiceInfoWithEnterpriseInfo(
 | ||
| 		ctx,
 | ||
| 		userID,
 | ||
| 		userWithEnterprise.EnterpriseInfo.CompanyName,
 | ||
| 		userWithEnterprise.EnterpriseInfo.UnifiedSocialCode,
 | ||
| 	)
 | ||
| 	if err != nil {
 | ||
| 		return nil, err
 | ||
| 	}
 | ||
| 
 | ||
| 	// 6. 验证开票信息完整性
 | ||
| 	invoiceInfo := value_objects.NewInvoiceInfo(
 | ||
| 		userInvoiceInfo.CompanyName,
 | ||
| 		userInvoiceInfo.TaxpayerID,
 | ||
| 		userInvoiceInfo.BankName,
 | ||
| 		userInvoiceInfo.BankAccount,
 | ||
| 		userInvoiceInfo.CompanyAddress,
 | ||
| 		userInvoiceInfo.CompanyPhone,
 | ||
| 		userInvoiceInfo.ReceivingEmail,
 | ||
| 	)
 | ||
| 
 | ||
| 	if err := s.userInvoiceInfoService.ValidateInvoiceInfo(ctx, invoiceInfo, invoiceType); err != nil {
 | ||
| 		return nil, err
 | ||
| 	}
 | ||
| 
 | ||
| 	// 7. 计算可开票金额
 | ||
| 	availableAmount, err := s.calculateAvailableAmount(ctx, userID)
 | ||
| 	if err != nil {
 | ||
| 		return nil, fmt.Errorf("计算可开票金额失败: %w", err)
 | ||
| 	}
 | ||
| 
 | ||
| 	// 8. 验证开票金额
 | ||
| 	amount, err := decimal.NewFromString(req.Amount)
 | ||
| 	if err != nil {
 | ||
| 		return nil, fmt.Errorf("无效的金额格式: %w", err)
 | ||
| 	}
 | ||
| 
 | ||
| 	if err := s.invoiceDomainService.ValidateInvoiceAmount(ctx, amount, availableAmount); err != nil {
 | ||
| 		return nil, err
 | ||
| 	}
 | ||
| 
 | ||
| 	// 9. 调用聚合服务申请开票
 | ||
| 	aggregateReq := services.ApplyInvoiceRequest{
 | ||
| 		InvoiceType: invoiceType,
 | ||
| 		Amount:      req.Amount,
 | ||
| 		InvoiceInfo: invoiceInfo,
 | ||
| 	}
 | ||
| 
 | ||
| 	application, err := s.invoiceAggregateService.ApplyInvoice(ctx, userID, aggregateReq)
 | ||
| 	if err != nil {
 | ||
| 		return nil, err
 | ||
| 	}
 | ||
| 
 | ||
| 	// 10. 构建响应DTO
 | ||
| 	return &dto.InvoiceApplicationResponse{
 | ||
| 		ID:          application.ID,
 | ||
| 		UserID:      application.UserID,
 | ||
| 		InvoiceType: application.InvoiceType,
 | ||
| 		Amount:      application.Amount,
 | ||
| 		Status:      application.Status,
 | ||
| 		InvoiceInfo: invoiceInfo,
 | ||
| 		CreatedAt:   application.CreatedAt,
 | ||
| 	}, nil
 | ||
| }
 | ||
| 
 | ||
| // GetUserInvoiceInfo 获取用户发票信息
 | ||
| func (s *InvoiceApplicationServiceImpl) GetUserInvoiceInfo(ctx context.Context, userID string) (*dto.InvoiceInfoResponse, error) {
 | ||
| 	// 1. 获取用户企业认证信息
 | ||
| 	user, err := s.userAggregateService.GetUserWithEnterpriseInfo(ctx, userID)
 | ||
| 	if err != nil {
 | ||
| 		return nil, fmt.Errorf("获取用户企业认证信息失败: %w", err)
 | ||
| 	}
 | ||
| 
 | ||
| 	// 2. 获取企业认证信息
 | ||
| 	var companyName, taxpayerID string
 | ||
| 	var companyNameReadOnly, taxpayerIDReadOnly bool
 | ||
| 
 | ||
| 	if user.EnterpriseInfo != nil {
 | ||
| 		companyName = user.EnterpriseInfo.CompanyName
 | ||
| 		taxpayerID = user.EnterpriseInfo.UnifiedSocialCode
 | ||
| 		companyNameReadOnly = true
 | ||
| 		taxpayerIDReadOnly = true
 | ||
| 	}
 | ||
| 
 | ||
| 	// 3. 获取用户开票信息(包含企业认证信息)
 | ||
| 	userInvoiceInfo, err := s.userInvoiceInfoService.GetUserInvoiceInfoWithEnterpriseInfo(ctx, userID, companyName, taxpayerID)
 | ||
| 	if err != nil {
 | ||
| 		return nil, err
 | ||
| 	}
 | ||
| 
 | ||
| 	// 4. 构建响应DTO
 | ||
| 	return &dto.InvoiceInfoResponse{
 | ||
| 		CompanyName:    userInvoiceInfo.CompanyName,
 | ||
| 		TaxpayerID:     userInvoiceInfo.TaxpayerID,
 | ||
| 		BankName:       userInvoiceInfo.BankName,
 | ||
| 		BankAccount:    userInvoiceInfo.BankAccount,
 | ||
| 		CompanyAddress: userInvoiceInfo.CompanyAddress,
 | ||
| 		CompanyPhone:   userInvoiceInfo.CompanyPhone,
 | ||
| 		ReceivingEmail: userInvoiceInfo.ReceivingEmail,
 | ||
| 		IsComplete:     userInvoiceInfo.IsComplete(),
 | ||
| 		MissingFields:  userInvoiceInfo.GetMissingFields(),
 | ||
| 		// 字段权限标识
 | ||
| 		CompanyNameReadOnly: companyNameReadOnly, // 公司名称只读(从企业认证信息获取)
 | ||
| 		TaxpayerIDReadOnly:  taxpayerIDReadOnly,  // 纳税人识别号只读(从企业认证信息获取)
 | ||
| 	}, nil
 | ||
| }
 | ||
| 
 | ||
| // UpdateUserInvoiceInfo 更新用户发票信息
 | ||
| func (s *InvoiceApplicationServiceImpl) UpdateUserInvoiceInfo(ctx context.Context, userID string, req UpdateInvoiceInfoRequest) error {
 | ||
| 	// 1. 获取用户企业认证信息
 | ||
| 	user, err := s.userAggregateService.GetUserWithEnterpriseInfo(ctx, userID)
 | ||
| 	if err != nil {
 | ||
| 		return fmt.Errorf("获取用户企业认证信息失败: %w", err)
 | ||
| 	}
 | ||
| 
 | ||
| 	// 2. 检查用户是否有企业认证信息
 | ||
| 	if user.EnterpriseInfo == nil {
 | ||
| 		return fmt.Errorf("用户未完成企业认证,无法创建开票信息")
 | ||
| 	}
 | ||
| 
 | ||
| 	// 3. 创建开票信息对象,公司名称和纳税人识别号从企业认证信息中获取
 | ||
| 	invoiceInfo := value_objects.NewInvoiceInfo(
 | ||
| 		"", // 公司名称将由服务层从企业认证信息中获取
 | ||
| 		"", // 纳税人识别号将由服务层从企业认证信息中获取
 | ||
| 		req.BankName,
 | ||
| 		req.BankAccount,
 | ||
| 		req.CompanyAddress,
 | ||
| 		req.CompanyPhone,
 | ||
| 		req.ReceivingEmail,
 | ||
| 	)
 | ||
| 
 | ||
| 	// 4. 使用包含企业认证信息的方法
 | ||
| 	_, err = s.userInvoiceInfoService.CreateOrUpdateUserInvoiceInfoWithEnterpriseInfo(
 | ||
| 		ctx,
 | ||
| 		userID,
 | ||
| 		invoiceInfo,
 | ||
| 		user.EnterpriseInfo.CompanyName,
 | ||
| 		user.EnterpriseInfo.UnifiedSocialCode,
 | ||
| 	)
 | ||
| 	return err
 | ||
| }
 | ||
| 
 | ||
| // GetUserInvoiceRecords 获取用户开票记录
 | ||
| func (s *InvoiceApplicationServiceImpl) GetUserInvoiceRecords(ctx context.Context, userID string, req GetInvoiceRecordsRequest) (*dto.InvoiceRecordsResponse, error) {
 | ||
| 	// 1. 验证用户是否存在
 | ||
| 	user, err := s.userRepo.GetByID(ctx, userID)
 | ||
| 	if err != nil {
 | ||
| 		return nil, err
 | ||
| 	}
 | ||
| 	if user.ID == "" {
 | ||
| 		return nil, fmt.Errorf("用户不存在")
 | ||
| 	}
 | ||
| 
 | ||
| 	// 2. 获取发票申请记录
 | ||
| 	var status entities.ApplicationStatus
 | ||
| 	if req.Status != "" {
 | ||
| 		status = entities.ApplicationStatus(req.Status)
 | ||
| 	}
 | ||
| 
 | ||
| 	// 3. 解析时间范围
 | ||
| 	var startTime, endTime *time.Time
 | ||
| 	if req.StartTime != "" {
 | ||
| 		if t, err := time.Parse("2006-01-02 15:04:05", req.StartTime); err == nil {
 | ||
| 			startTime = &t
 | ||
| 		}
 | ||
| 	}
 | ||
| 	if req.EndTime != "" {
 | ||
| 		if t, err := time.Parse("2006-01-02 15:04:05", req.EndTime); err == nil {
 | ||
| 			endTime = &t
 | ||
| 		}
 | ||
| 	}
 | ||
| 
 | ||
| 	// 4. 获取发票申请记录(需要更新仓储层方法以支持时间筛选)
 | ||
| 	applications, total, err := s.invoiceRepo.FindByUserIDAndStatusWithTimeRange(ctx, userID, status, startTime, endTime, req.Page, req.PageSize)
 | ||
| 	if err != nil {
 | ||
| 		return nil, err
 | ||
| 	}
 | ||
| 
 | ||
| 	// 5. 构建响应DTO
 | ||
| 	records := make([]*dto.InvoiceRecordResponse, len(applications))
 | ||
| 	for i, app := range applications {
 | ||
| 		// 使用快照信息(申请时的开票信息)
 | ||
| 		records[i] = &dto.InvoiceRecordResponse{
 | ||
| 			ID:             app.ID,
 | ||
| 			UserID:         app.UserID,
 | ||
| 			InvoiceType:    app.InvoiceType,
 | ||
| 			Amount:         app.Amount,
 | ||
| 			Status:         app.Status,
 | ||
| 			CompanyName:    app.CompanyName,    // 使用快照的公司名称
 | ||
| 			TaxpayerID:     app.TaxpayerID,     // 使用快照的纳税人识别号
 | ||
| 			BankName:       app.BankName,       // 使用快照的银行名称
 | ||
| 			BankAccount:    app.BankAccount,    // 使用快照的银行账号
 | ||
| 			CompanyAddress: app.CompanyAddress, // 使用快照的企业地址
 | ||
| 			CompanyPhone:   app.CompanyPhone,   // 使用快照的企业电话
 | ||
| 			ReceivingEmail: app.ReceivingEmail, // 使用快照的接收邮箱
 | ||
| 			FileName:       app.FileName,
 | ||
| 			FileSize:       app.FileSize,
 | ||
| 			FileURL:        app.FileURL,
 | ||
| 			ProcessedAt:    app.ProcessedAt,
 | ||
| 			CreatedAt:      app.CreatedAt,
 | ||
| 			RejectReason:   app.RejectReason,
 | ||
| 		}
 | ||
| 	}
 | ||
| 
 | ||
| 	return &dto.InvoiceRecordsResponse{
 | ||
| 		Records:    records,
 | ||
| 		Total:      total,
 | ||
| 		Page:       req.Page,
 | ||
| 		PageSize:   req.PageSize,
 | ||
| 		TotalPages: (int(total) + req.PageSize - 1) / req.PageSize,
 | ||
| 	}, nil
 | ||
| }
 | ||
| 
 | ||
| // DownloadInvoiceFile 下载发票文件
 | ||
| func (s *InvoiceApplicationServiceImpl) DownloadInvoiceFile(ctx context.Context, userID string, applicationID string) (*dto.FileDownloadResponse, error) {
 | ||
| 	// 1. 查找申请记录
 | ||
| 	application, err := s.invoiceRepo.FindByID(ctx, applicationID)
 | ||
| 	if err != nil {
 | ||
| 		return nil, err
 | ||
| 	}
 | ||
| 	if application == nil {
 | ||
| 		return nil, fmt.Errorf("申请记录不存在")
 | ||
| 	}
 | ||
| 
 | ||
| 	// 2. 验证权限(只能下载自己的发票)
 | ||
| 	if application.UserID != userID {
 | ||
| 		return nil, fmt.Errorf("无权访问此发票")
 | ||
| 	}
 | ||
| 
 | ||
| 	// 3. 验证状态(只能下载已完成的发票)
 | ||
| 	if application.Status != entities.ApplicationStatusCompleted {
 | ||
| 		return nil, fmt.Errorf("发票尚未通过审核")
 | ||
| 	}
 | ||
| 
 | ||
| 	// 4. 验证文件信息
 | ||
| 	if application.FileURL == nil || *application.FileURL == "" {
 | ||
| 		return nil, fmt.Errorf("发票文件不存在")
 | ||
| 	}
 | ||
| 
 | ||
| 	// 5. 从七牛云下载文件内容
 | ||
| 	fileContent, err := s.storageService.DownloadFile(ctx, *application.FileURL)
 | ||
| 	if err != nil {
 | ||
| 		return nil, fmt.Errorf("下载文件失败: %w", err)
 | ||
| 	}
 | ||
| 
 | ||
| 	// 6. 构建响应DTO
 | ||
| 	return &dto.FileDownloadResponse{
 | ||
| 		FileID:      *application.FileID,
 | ||
| 		FileName:    *application.FileName,
 | ||
| 		FileSize:    *application.FileSize,
 | ||
| 		FileURL:     *application.FileURL,
 | ||
| 		FileContent: fileContent,
 | ||
| 	}, nil
 | ||
| }
 | ||
| 
 | ||
| // GetAvailableAmount 获取可开票金额
 | ||
| func (s *InvoiceApplicationServiceImpl) GetAvailableAmount(ctx context.Context, userID string) (*dto.AvailableAmountResponse, error) {
 | ||
| 	// 1. 验证用户是否存在
 | ||
| 	user, err := s.userRepo.GetByID(ctx, userID)
 | ||
| 	if err != nil {
 | ||
| 		return nil, err
 | ||
| 	}
 | ||
| 	if user.ID == "" {
 | ||
| 		return nil, fmt.Errorf("用户不存在")
 | ||
| 	}
 | ||
| 
 | ||
| 	// 2. 计算可开票金额
 | ||
| 	availableAmount, err := s.calculateAvailableAmount(ctx, userID)
 | ||
| 	if err != nil {
 | ||
| 		return nil, err
 | ||
| 	}
 | ||
| 
 | ||
| 	// 3. 获取真实充值金额(支付宝充值+对公转账)和总赠送金额
 | ||
| 	realRecharged, totalGifted, totalInvoiced, err := s.getAmountSummary(ctx, userID)
 | ||
| 	if err != nil {
 | ||
| 		return nil, err
 | ||
| 	}
 | ||
| 
 | ||
| 	// 4. 获取待处理申请金额
 | ||
| 	pendingAmount, err := s.getPendingApplicationsAmount(ctx, userID)
 | ||
| 	if err != nil {
 | ||
| 		return nil, err
 | ||
| 	}
 | ||
| 
 | ||
| 	// 5. 构建响应DTO
 | ||
| 	return &dto.AvailableAmountResponse{
 | ||
| 		AvailableAmount:     availableAmount,
 | ||
| 		TotalRecharged:      realRecharged, // 使用真实充值金额(支付宝充值+对公转账)
 | ||
| 		TotalGifted:         totalGifted,
 | ||
| 		TotalInvoiced:       totalInvoiced,
 | ||
| 		PendingApplications: pendingAmount,
 | ||
| 	}, nil
 | ||
| }
 | ||
| 
 | ||
| // calculateAvailableAmount 计算可开票金额(私有方法)
 | ||
| func (s *InvoiceApplicationServiceImpl) calculateAvailableAmount(ctx context.Context, userID string) (decimal.Decimal, error) {
 | ||
| 	// 1. 获取真实充值金额(支付宝充值+对公转账)和总赠送金额
 | ||
| 	realRecharged, totalGifted, totalInvoiced, err := s.getAmountSummary(ctx, userID)
 | ||
| 	if err != nil {
 | ||
| 		return decimal.Zero, err
 | ||
| 	}
 | ||
| 
 | ||
| 	// 2. 获取待处理中的申请金额
 | ||
| 	pendingAmount, err := s.getPendingApplicationsAmount(ctx, userID)
 | ||
| 	if err != nil {
 | ||
| 		return decimal.Zero, err
 | ||
| 	}
 | ||
| 	fmt.Println("realRecharged", realRecharged)
 | ||
| 	fmt.Println("totalGifted", totalGifted)
 | ||
| 	fmt.Println("totalInvoiced", totalInvoiced)
 | ||
| 	fmt.Println("pendingAmount", pendingAmount)
 | ||
| 	// 3. 计算可开票金额:真实充值金额 - 已开票 - 待处理申请
 | ||
| 	// 可开票金额 = 真实充值金额(支付宝充值+对公转账) - 已开票金额 - 待处理申请金额
 | ||
| 	availableAmount := realRecharged.Sub(totalInvoiced).Sub(pendingAmount)
 | ||
| 	fmt.Println("availableAmount", availableAmount)
 | ||
| 	// 确保可开票金额不为负数
 | ||
| 	if availableAmount.LessThan(decimal.Zero) {
 | ||
| 		availableAmount = decimal.Zero
 | ||
| 	}
 | ||
| 
 | ||
| 	return availableAmount, nil
 | ||
| }
 | ||
| 
 | ||
| // getAmountSummary 获取金额汇总(私有方法)
 | ||
| func (s *InvoiceApplicationServiceImpl) getAmountSummary(ctx context.Context, userID string) (decimal.Decimal, decimal.Decimal, decimal.Decimal, error) {
 | ||
| 	// 1. 获取用户所有成功的充值记录
 | ||
| 	rechargeRecords, err := s.rechargeRecordRepo.GetByUserID(ctx, userID)
 | ||
| 	if err != nil {
 | ||
| 		return decimal.Zero, decimal.Zero, decimal.Zero, fmt.Errorf("获取充值记录失败: %w", err)
 | ||
| 	}
 | ||
| 
 | ||
| 	// 2. 计算真实充值金额(支付宝充值 + 对公转账)和总赠送金额
 | ||
| 	var realRecharged decimal.Decimal // 真实充值金额:支付宝充值 + 对公转账
 | ||
| 	var totalGifted decimal.Decimal   // 总赠送金额
 | ||
| 	for _, record := range rechargeRecords {
 | ||
| 		if record.IsSuccess() {
 | ||
| 			if record.RechargeType == entities.RechargeTypeGift {
 | ||
| 				// 赠送金额不计入可开票金额
 | ||
| 				totalGifted = totalGifted.Add(record.Amount)
 | ||
| 			} else if record.RechargeType == entities.RechargeTypeAlipay || record.RechargeType == entities.RechargeTypeTransfer {
 | ||
| 				// 只有支付宝充值和对公转账计入可开票金额
 | ||
| 				realRecharged = realRecharged.Add(record.Amount)
 | ||
| 			}
 | ||
| 		}
 | ||
| 	}
 | ||
| 
 | ||
| 	// 3. 获取用户所有发票申请记录(包括待处理、已完成、已拒绝)
 | ||
| 	applications, _, err := s.invoiceRepo.FindByUserID(ctx, userID, 1, 1000) // 获取所有记录
 | ||
| 	if err != nil {
 | ||
| 		return decimal.Zero, decimal.Zero, decimal.Zero, fmt.Errorf("获取发票申请记录失败: %w", err)
 | ||
| 	}
 | ||
| 
 | ||
| 	var totalInvoiced decimal.Decimal
 | ||
| 	for _, application := range applications {
 | ||
| 		// 计算已完成的发票申请金额
 | ||
| 		if application.IsCompleted() {
 | ||
| 			totalInvoiced = totalInvoiced.Add(application.Amount)
 | ||
| 		}
 | ||
| 		// 注意:待处理中的申请金额不计算在已开票金额中,但会在可开票金额计算时被扣除
 | ||
| 	}
 | ||
| 
 | ||
| 	return realRecharged, totalGifted, totalInvoiced, nil
 | ||
| }
 | ||
| 
 | ||
| // getPendingApplicationsAmount 获取待处理申请的总金额(私有方法)
 | ||
| func (s *InvoiceApplicationServiceImpl) getPendingApplicationsAmount(ctx context.Context, userID string) (decimal.Decimal, error) {
 | ||
| 	// 获取用户所有发票申请记录
 | ||
| 	applications, _, err := s.invoiceRepo.FindByUserID(ctx, userID, 1, 1000)
 | ||
| 	if err != nil {
 | ||
| 		return decimal.Zero, fmt.Errorf("获取发票申请记录失败: %w", err)
 | ||
| 	}
 | ||
| 
 | ||
| 	var pendingAmount decimal.Decimal
 | ||
| 	for _, application := range applications {
 | ||
| 		// 只计算待处理状态的申请金额
 | ||
| 		if application.Status == entities.ApplicationStatusPending {
 | ||
| 			pendingAmount = pendingAmount.Add(application.Amount)
 | ||
| 		}
 | ||
| 	}
 | ||
| 
 | ||
| 	return pendingAmount, nil
 | ||
| }
 | ||
| 
 | ||
| // ==================== 管理员端发票应用服务 ====================
 | ||
| 
 | ||
| // AdminInvoiceApplicationService 管理员发票应用服务接口
 | ||
| type AdminInvoiceApplicationService interface {
 | ||
| 	// GetPendingApplications 获取发票申请列表(支持筛选)
 | ||
| 	GetPendingApplications(ctx context.Context, req GetPendingApplicationsRequest) (*dto.PendingApplicationsResponse, error)
 | ||
| 
 | ||
| 	// ApproveInvoiceApplication 通过发票申请
 | ||
| 	ApproveInvoiceApplication(ctx context.Context, applicationID string, file multipart.File, req ApproveInvoiceRequest) error
 | ||
| 
 | ||
| 	// RejectInvoiceApplication 拒绝发票申请
 | ||
| 	RejectInvoiceApplication(ctx context.Context, applicationID string, req RejectInvoiceRequest) error
 | ||
| 
 | ||
| 	// DownloadInvoiceFile 下载发票文件(管理员)
 | ||
| 	DownloadInvoiceFile(ctx context.Context, applicationID string) (*dto.FileDownloadResponse, error)
 | ||
| }
 | ||
| 
 | ||
| // AdminInvoiceApplicationServiceImpl 管理员发票应用服务实现
 | ||
| type AdminInvoiceApplicationServiceImpl struct {
 | ||
| 	invoiceRepo             finance_repo.InvoiceApplicationRepository
 | ||
| 	userInvoiceInfoRepo     finance_repo.UserInvoiceInfoRepository
 | ||
| 	userRepo                user_repo.UserRepository
 | ||
| 	invoiceAggregateService services.InvoiceAggregateService
 | ||
| 	storageService          *storage.QiNiuStorageService
 | ||
| 	logger                  *zap.Logger
 | ||
| }
 | ||
| 
 | ||
| // NewAdminInvoiceApplicationService 创建管理员发票应用服务
 | ||
| func NewAdminInvoiceApplicationService(
 | ||
| 	invoiceRepo finance_repo.InvoiceApplicationRepository,
 | ||
| 	userInvoiceInfoRepo finance_repo.UserInvoiceInfoRepository,
 | ||
| 	userRepo user_repo.UserRepository,
 | ||
| 	invoiceAggregateService services.InvoiceAggregateService,
 | ||
| 	storageService *storage.QiNiuStorageService,
 | ||
| 	logger *zap.Logger,
 | ||
| ) AdminInvoiceApplicationService {
 | ||
| 	return &AdminInvoiceApplicationServiceImpl{
 | ||
| 		invoiceRepo:             invoiceRepo,
 | ||
| 		userInvoiceInfoRepo:     userInvoiceInfoRepo,
 | ||
| 		userRepo:                userRepo,
 | ||
| 		invoiceAggregateService: invoiceAggregateService,
 | ||
| 		storageService:          storageService,
 | ||
| 		logger:                  logger,
 | ||
| 	}
 | ||
| }
 | ||
| 
 | ||
| // GetPendingApplications 获取发票申请列表(支持筛选)
 | ||
| func (s *AdminInvoiceApplicationServiceImpl) GetPendingApplications(ctx context.Context, req GetPendingApplicationsRequest) (*dto.PendingApplicationsResponse, error) {
 | ||
| 	// 1. 解析状态筛选
 | ||
| 	var status entities.ApplicationStatus
 | ||
| 	if req.Status != "" {
 | ||
| 		status = entities.ApplicationStatus(req.Status)
 | ||
| 	}
 | ||
| 
 | ||
| 	// 2. 解析时间范围
 | ||
| 	var startTime, endTime *time.Time
 | ||
| 	if req.StartTime != "" {
 | ||
| 		if t, err := time.Parse("2006-01-02 15:04:05", req.StartTime); err == nil {
 | ||
| 			startTime = &t
 | ||
| 		}
 | ||
| 	}
 | ||
| 	if req.EndTime != "" {
 | ||
| 		if t, err := time.Parse("2006-01-02 15:04:05", req.EndTime); err == nil {
 | ||
| 			endTime = &t
 | ||
| 		}
 | ||
| 	}
 | ||
| 
 | ||
| 	// 3. 获取发票申请记录(支持筛选)
 | ||
| 	var applications []*entities.InvoiceApplication
 | ||
| 	var total int64
 | ||
| 	var err error
 | ||
| 
 | ||
| 	if status != "" {
 | ||
| 		// 按状态筛选
 | ||
| 		applications, total, err = s.invoiceRepo.FindByStatusWithTimeRange(ctx, status, startTime, endTime, req.Page, req.PageSize)
 | ||
| 	} else {
 | ||
| 		// 获取所有记录(按时间筛选)
 | ||
| 		applications, total, err = s.invoiceRepo.FindAllWithTimeRange(ctx, startTime, endTime, req.Page, req.PageSize)
 | ||
| 	}
 | ||
| 
 | ||
| 	if err != nil {
 | ||
| 		return nil, err
 | ||
| 	}
 | ||
| 
 | ||
| 	// 4. 构建响应DTO
 | ||
| 	pendingApplications := make([]*dto.PendingApplicationResponse, len(applications))
 | ||
| 	for i, app := range applications {
 | ||
| 		// 使用快照信息
 | ||
| 		pendingApplications[i] = &dto.PendingApplicationResponse{
 | ||
| 			ID:             app.ID,
 | ||
| 			UserID:         app.UserID,
 | ||
| 			InvoiceType:    app.InvoiceType,
 | ||
| 			Amount:         app.Amount,
 | ||
| 			Status:         app.Status,
 | ||
| 			CompanyName:    app.CompanyName,    // 使用快照的公司名称
 | ||
| 			TaxpayerID:     app.TaxpayerID,     // 使用快照的纳税人识别号
 | ||
| 			BankName:       app.BankName,       // 使用快照的银行名称
 | ||
| 			BankAccount:    app.BankAccount,    // 使用快照的银行账号
 | ||
| 			CompanyAddress: app.CompanyAddress, // 使用快照的企业地址
 | ||
| 			CompanyPhone:   app.CompanyPhone,   // 使用快照的企业电话
 | ||
| 			ReceivingEmail: app.ReceivingEmail, // 使用快照的接收邮箱
 | ||
| 			FileName:       app.FileName,
 | ||
| 			FileSize:       app.FileSize,
 | ||
| 			FileURL:        app.FileURL,
 | ||
| 			ProcessedAt:    app.ProcessedAt,
 | ||
| 			CreatedAt:      app.CreatedAt,
 | ||
| 			RejectReason:   app.RejectReason,
 | ||
| 		}
 | ||
| 	}
 | ||
| 
 | ||
| 	return &dto.PendingApplicationsResponse{
 | ||
| 		Applications: pendingApplications,
 | ||
| 		Total:        total,
 | ||
| 		Page:         req.Page,
 | ||
| 		PageSize:     req.PageSize,
 | ||
| 		TotalPages:   (int(total) + req.PageSize - 1) / req.PageSize,
 | ||
| 	}, nil
 | ||
| }
 | ||
| 
 | ||
| // ApproveInvoiceApplication 通过发票申请
 | ||
| func (s *AdminInvoiceApplicationServiceImpl) ApproveInvoiceApplication(ctx context.Context, applicationID string, file multipart.File, req ApproveInvoiceRequest) error {
 | ||
| 	// 1. 验证申请是否存在
 | ||
| 	application, err := s.invoiceRepo.FindByID(ctx, applicationID)
 | ||
| 	if err != nil {
 | ||
| 		return err
 | ||
| 	}
 | ||
| 	if application == nil {
 | ||
| 		return fmt.Errorf("发票申请不存在")
 | ||
| 	}
 | ||
| 
 | ||
| 	// 2. 验证申请状态
 | ||
| 	if application.Status != entities.ApplicationStatusPending {
 | ||
| 		return fmt.Errorf("发票申请状态不允许处理")
 | ||
| 	}
 | ||
| 
 | ||
| 	// 3. 调用聚合服务处理申请
 | ||
| 	aggregateReq := services.ApproveInvoiceRequest{
 | ||
| 		AdminNotes: req.AdminNotes,
 | ||
| 	}
 | ||
| 
 | ||
| 	return s.invoiceAggregateService.ApproveInvoiceApplication(ctx, applicationID, file, aggregateReq)
 | ||
| }
 | ||
| 
 | ||
| // RejectInvoiceApplication 拒绝发票申请
 | ||
| func (s *AdminInvoiceApplicationServiceImpl) RejectInvoiceApplication(ctx context.Context, applicationID string, req RejectInvoiceRequest) error {
 | ||
| 	// 1. 验证申请是否存在
 | ||
| 	application, err := s.invoiceRepo.FindByID(ctx, applicationID)
 | ||
| 	if err != nil {
 | ||
| 		return err
 | ||
| 	}
 | ||
| 	if application == nil {
 | ||
| 		return fmt.Errorf("发票申请不存在")
 | ||
| 	}
 | ||
| 
 | ||
| 	// 2. 验证申请状态
 | ||
| 	if application.Status != entities.ApplicationStatusPending {
 | ||
| 		return fmt.Errorf("发票申请状态不允许处理")
 | ||
| 	}
 | ||
| 
 | ||
| 	// 3. 调用聚合服务处理申请
 | ||
| 	aggregateReq := services.RejectInvoiceRequest{
 | ||
| 		Reason: req.Reason,
 | ||
| 	}
 | ||
| 
 | ||
| 	return s.invoiceAggregateService.RejectInvoiceApplication(ctx, applicationID, aggregateReq)
 | ||
| }
 | ||
| 
 | ||
| // ==================== 请求和响应DTO ====================
 | ||
| 
 | ||
| type ApplyInvoiceRequest struct {
 | ||
| 	InvoiceType string `json:"invoice_type" binding:"required"` // 发票类型:general/special
 | ||
| 	Amount      string `json:"amount" binding:"required"`       // 开票金额
 | ||
| }
 | ||
| 
 | ||
| type UpdateInvoiceInfoRequest struct {
 | ||
| 	CompanyName    string `json:"company_name"`                             // 公司名称(从企业认证信息获取,用户不可修改)
 | ||
| 	TaxpayerID     string `json:"taxpayer_id"`                              // 纳税人识别号(从企业认证信息获取,用户不可修改)
 | ||
| 	BankName       string `json:"bank_name"`                                // 银行名称
 | ||
| 	CompanyAddress string `json:"company_address"`                          // 公司地址
 | ||
| 	BankAccount    string `json:"bank_account"`                             // 银行账户
 | ||
| 	CompanyPhone   string `json:"company_phone"`                            // 企业注册电话
 | ||
| 	ReceivingEmail string `json:"receiving_email" binding:"required,email"` // 发票接收邮箱
 | ||
| }
 | ||
| 
 | ||
| type GetInvoiceRecordsRequest struct {
 | ||
| 	Page      int    `json:"page"`       // 页码
 | ||
| 	PageSize  int    `json:"page_size"`  // 每页数量
 | ||
| 	Status    string `json:"status"`     // 状态筛选
 | ||
| 	StartTime string `json:"start_time"` // 开始时间 (格式: 2006-01-02 15:04:05)
 | ||
| 	EndTime   string `json:"end_time"`   // 结束时间 (格式: 2006-01-02 15:04:05)
 | ||
| }
 | ||
| 
 | ||
| type GetPendingApplicationsRequest struct {
 | ||
| 	Page      int    `json:"page"`       // 页码
 | ||
| 	PageSize  int    `json:"page_size"`  // 每页数量
 | ||
| 	Status    string `json:"status"`     // 状态筛选:pending/completed/rejected
 | ||
| 	StartTime string `json:"start_time"` // 开始时间 (格式: 2006-01-02 15:04:05)
 | ||
| 	EndTime   string `json:"end_time"`   // 结束时间 (格式: 2006-01-02 15:04:05)
 | ||
| }
 | ||
| 
 | ||
| type ApproveInvoiceRequest struct {
 | ||
| 	AdminNotes string `json:"admin_notes"` // 管理员备注
 | ||
| }
 | ||
| 
 | ||
| type RejectInvoiceRequest struct {
 | ||
| 	Reason string `json:"reason" binding:"required"` // 拒绝原因
 | ||
| }
 | ||
| 
 | ||
| // DownloadInvoiceFile 下载发票文件(管理员)
 | ||
| func (s *AdminInvoiceApplicationServiceImpl) DownloadInvoiceFile(ctx context.Context, applicationID string) (*dto.FileDownloadResponse, error) {
 | ||
| 	// 1. 查找申请记录
 | ||
| 	application, err := s.invoiceRepo.FindByID(ctx, applicationID)
 | ||
| 	if err != nil {
 | ||
| 		return nil, err
 | ||
| 	}
 | ||
| 	if application == nil {
 | ||
| 		return nil, fmt.Errorf("申请记录不存在")
 | ||
| 	}
 | ||
| 
 | ||
| 	// 2. 验证状态(只能下载已完成的发票)
 | ||
| 	if application.Status != entities.ApplicationStatusCompleted {
 | ||
| 		return nil, fmt.Errorf("发票尚未通过审核")
 | ||
| 	}
 | ||
| 
 | ||
| 	// 3. 验证文件信息
 | ||
| 	if application.FileURL == nil || *application.FileURL == "" {
 | ||
| 		return nil, fmt.Errorf("发票文件不存在")
 | ||
| 	}
 | ||
| 
 | ||
| 	// 4. 从七牛云下载文件内容
 | ||
| 	fileContent, err := s.storageService.DownloadFile(ctx, *application.FileURL)
 | ||
| 	if err != nil {
 | ||
| 		return nil, fmt.Errorf("下载文件失败: %w", err)
 | ||
| 	}
 | ||
| 
 | ||
| 	// 5. 构建响应DTO
 | ||
| 	return &dto.FileDownloadResponse{
 | ||
| 		FileID:      *application.FileID,
 | ||
| 		FileName:    *application.FileName,
 | ||
| 		FileSize:    *application.FileSize,
 | ||
| 		FileURL:     *application.FileURL,
 | ||
| 		FileContent: fileContent,
 | ||
| 	}, nil
 | ||
| }
 |