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 | |||
|  | } |