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