package service import ( "context" "crypto/rand" "encoding/hex" "fmt" "license-manager/internal/models" "license-manager/internal/repository" pkgcontext "license-manager/pkg/context" "license-manager/pkg/i18n" "strings" "time" "gorm.io/gorm" ) // AdminInvoiceService 管理端发票服务接口 type AdminInvoiceService interface { // ========== 管理端发票操作 ========== RejectInvoice(ctx context.Context, invoiceID string, adminID string, req *models.InvoiceRejectRequest) (*models.Invoice, error) IssueInvoice(ctx context.Context, invoiceID string, adminID string, req *models.InvoiceIssueRequest) (*models.Invoice, error) GetAdminInvoices(ctx context.Context, req *models.InvoiceListRequest) (*models.InvoiceListResponse, error) GetAdminInvoiceSummary(ctx context.Context) (*models.InvoiceSummaryResponse, error) GetInvoiceDetail(ctx context.Context, invoiceID string, isAdmin bool, userID string) (*models.InvoiceDetailResponse, error) GenerateDownloadToken(ctx context.Context, invoiceID string) (string, error) // ========== 公共发票操作 ========== DownloadByToken(ctx context.Context, token string) (string, error) } type adminInvoiceService struct { repo repository.AdminInvoiceRepository orderRepo repository.CuOrderRepository userRepo repository.UserRepository cuUserRepo repository.CuUserRepository db *gorm.DB } func NewAdminInvoiceService(repo repository.AdminInvoiceRepository, orderRepo repository.CuOrderRepository, userRepo repository.UserRepository, cuUserRepo repository.CuUserRepository, db *gorm.DB) AdminInvoiceService { return &adminInvoiceService{ repo: repo, orderRepo: orderRepo, userRepo: userRepo, cuUserRepo: cuUserRepo, db: db, } } // ========== 管理端发票操作实现 ========== func (s *adminInvoiceService) RejectInvoice(ctx context.Context, invoiceID string, adminID string, req *models.InvoiceRejectRequest) (*models.Invoice, error) { lang := pkgcontext.GetLanguageFromContext(ctx) invoice, err := s.repo.GetByID(invoiceID) if err != nil { return nil, i18n.NewI18nError("700001", lang) } // 验证状态 if invoice.Status != models.InvoiceStatusPending { return nil, i18n.NewI18nError("900001", lang) } now := time.Now() invoice.Status = models.InvoiceStatusRejected invoice.RejectReason = &req.RejectReason invoice.Suggestion = req.Suggestion invoice.RejectedAt = &now invoice.RejectedBy = &adminID invoice.UpdatedAt = now err = s.repo.Update(invoice) if err != nil { return nil, err } return invoice, nil } func (s *adminInvoiceService) IssueInvoice(ctx context.Context, invoiceID string, adminID string, req *models.InvoiceIssueRequest) (*models.Invoice, error) { lang := pkgcontext.GetLanguageFromContext(ctx) invoice, err := s.repo.GetByID(invoiceID) if err != nil { return nil, i18n.NewI18nError("700001", lang) } prevStatus := invoice.Status issuedAt, err := time.Parse(time.RFC3339, req.IssuedAt) if err != nil { return nil, i18n.NewI18nError("900001", lang) } now := time.Now() invoice.Status = models.InvoiceStatusIssued invoice.InvoiceFileURL = &req.InvoiceFileURL invoice.IssuedAt = &issuedAt invoice.UploadedAt = &now invoice.UploadedBy = &adminID invoice.UpdatedAt = now // Clear reject info when issuing from rejected status. if prevStatus == models.InvoiceStatusRejected { invoice.RejectReason = nil invoice.Suggestion = nil invoice.RejectedAt = nil invoice.RejectedBy = nil } err = s.repo.Update(invoice) if err != nil { return nil, err } return invoice, nil } func (s *adminInvoiceService) GetAdminInvoices(ctx context.Context, req *models.InvoiceListRequest) (*models.InvoiceListResponse, error) { lang := pkgcontext.GetLanguageFromContext(ctx) // 解析日期筛选 var createdAtStart, createdAtEnd *time.Time if req.ApplyDate != "" { start, end, err := s.parseDateRange(req.ApplyDate) if err != nil { return nil, i18n.NewI18nError("900001", lang) } createdAtStart = start createdAtEnd = end } invoices, total, err := s.repo.GetAdminList(ctx, req, createdAtStart, createdAtEnd) if err != nil { return nil, i18n.NewI18nError("900004", lang, err.Error()) } // 转换为响应结构并填充多语言显示字段和用户手机号 invoiceResponses := make([]*models.InvoiceResponse, len(invoices)) for i, invoice := range invoices { invoiceResponses[i] = invoice.ToResponse() s.fillDisplayFields(invoiceResponses[i], lang) // 填充申请人手机号 cuUser, err := s.cuUserRepo.GetByID(invoice.CuUserID) if err == nil { invoiceResponses[i].CuUserPhone = cuUser.Phone } } return &models.InvoiceListResponse{ Invoices: invoiceResponses, TotalCount: total, Page: req.Page, PageSize: req.PageSize, }, nil } func (s *adminInvoiceService) GetAdminInvoiceSummary(ctx context.Context) (*models.InvoiceSummaryResponse, error) { return s.repo.GetAdminInvoiceSummary(ctx) } func (s *adminInvoiceService) GetInvoiceDetail(ctx context.Context, invoiceID string, isAdmin bool, userID string) (*models.InvoiceDetailResponse, error) { lang := pkgcontext.GetLanguageFromContext(ctx) invoice, err := s.repo.GetByID(invoiceID) if err != nil { return nil, i18n.NewI18nError("700001", lang) } // 权限验证 if !isAdmin && invoice.CuUserID != userID { return nil, i18n.NewI18nError("700001", lang) } response := &models.InvoiceDetailResponse{ InvoiceResponse: invoice.ToResponse(), } // 填充多语言显示字段 s.fillDetailDisplayFields(response, lang) // 获取关联信息 if isAdmin { // 管理端需要更多关联信息 order, err := s.orderRepo.GetByID(invoice.OrderID) if err == nil { response.OrderPackageName = order.PackageName } cuUser, err := s.cuUserRepo.GetByID(invoice.CuUserID) if err == nil && cuUser.RealName != nil { response.ApplicantName = *cuUser.RealName response.ApplicantPhone = cuUser.Phone } if invoice.UploadedBy != nil { user, err := s.userRepo.GetUserByID(ctx, *invoice.UploadedBy) if err == nil { response.UploaderName = user.FullName } } if invoice.RejectedBy != nil { user, err := s.userRepo.GetUserByID(ctx, *invoice.RejectedBy) if err == nil { response.RejecterName = user.FullName } } } return response, nil } func (s *adminInvoiceService) GenerateDownloadToken(ctx context.Context, invoiceID string) (string, error) { lang := pkgcontext.GetLanguageFromContext(ctx) invoice, err := s.repo.GetByID(invoiceID) if err != nil { return "", i18n.NewI18nError("700001", lang) } if invoice.Status != models.InvoiceStatusIssued { return "", i18n.NewI18nError("700005", lang) } if invoice.InvoiceFileURL == nil || *invoice.InvoiceFileURL == "" { return "", i18n.NewI18nError("700005", lang) } // 生成随机token tokenBytes := make([]byte, 32) _, err = rand.Read(tokenBytes) if err != nil { return "", err } token := hex.EncodeToString(tokenBytes) // 更新token err = s.repo.UpdateDownloadToken(invoiceID, &token) if err != nil { return "", err } return token, nil } // ========== 公共发票操作实现 ========== func (s *adminInvoiceService) DownloadByToken(ctx context.Context, token string) (string, error) { lang := pkgcontext.GetLanguageFromContext(ctx) invoice, err := s.repo.GetByDownloadToken(token) if err != nil { return "", i18n.NewI18nError("700001", lang) } if invoice.Status != models.InvoiceStatusIssued { return "", i18n.NewI18nError("700005", lang) } if invoice.InvoiceFileURL == nil || *invoice.InvoiceFileURL == "" { return "", i18n.NewI18nError("700005", lang) } return *invoice.InvoiceFileURL, nil } // ========== 私有辅助方法 ========== // fillDisplayFields 填充响应结构的多语言显示字段 func (s *adminInvoiceService) fillDisplayFields(response *models.InvoiceResponse, lang string) { response.StatusDisplay = i18n.GetEnumMessage("invoice_status", response.Status, lang) response.InvoiceTypeDisplay = i18n.GetEnumMessage("invoice_type", response.InvoiceType, lang) } // fillDetailDisplayFields 填充详情响应结构的多语言显示字段 func (s *adminInvoiceService) fillDetailDisplayFields(response *models.InvoiceDetailResponse, lang string) { response.StatusDisplay = i18n.GetEnumMessage("invoice_status", response.Status, lang) response.InvoiceTypeDisplay = i18n.GetEnumMessage("invoice_type", response.InvoiceType, lang) } // parseDateRange 解析日期范围 func (s *adminInvoiceService) parseDateRange(dateStr string) (*time.Time, *time.Time, error) { parts := strings.Split(dateStr, ",") if len(parts) == 1 { start, _, err := s.parseFlexibleTime(strings.TrimSpace(dateStr)) if err != nil { return nil, nil, err } end := start.AddDate(0, 0, 1) return &start, &end, nil } if len(parts) != 2 { return nil, nil, fmt.Errorf("invalid date range") } startText := strings.TrimSpace(parts[0]) endText := strings.TrimSpace(parts[1]) if startText == "" || endText == "" { return nil, nil, fmt.Errorf("invalid date range") } start, _, err := s.parseFlexibleTime(startText) if err != nil { return nil, nil, err } end, endDateOnly, err := s.parseFlexibleTime(endText) if err != nil { return nil, nil, err } if endDateOnly { end = end.AddDate(0, 0, 1) } if end.Before(start) { start, end = end, start } return &start, &end, nil } func (s *adminInvoiceService) parseFlexibleTime(value string) (time.Time, bool, error) { if t, err := time.Parse(time.RFC3339Nano, value); err == nil { return t, false, nil } if t, err := time.Parse(time.RFC3339, value); err == nil { return t, false, nil } t, err := time.Parse("2006-01-02", value) if err != nil { return time.Time{}, false, err } return t, true, nil }