Files
in-server/app/main/api/internal/service/reportpdfservice.go
2026-03-18 01:01:59 +08:00

143 lines
4.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package service
import (
"context"
"fmt"
"os"
"path/filepath"
"time"
"github.com/chromedp/cdproto/page"
"github.com/chromedp/chromedp"
"github.com/zeromicro/go-zero/core/logx"
)
// ReportPDFService 负责根据订单信息渲染前端报告页面并生成 PDF。
type ReportPDFService struct {
baseReportURL string
outputDir string
}
func NewReportPDFService(baseReportURL, outputDir string) *ReportPDFService {
return &ReportPDFService{
baseReportURL: baseReportURL,
outputDir: outputDir,
}
}
// GenerateReportPDF 根据 orderId / orderNo 生成报告 PDF。
// orderId 与 orderNo 至少需要一个非空,否则返回错误。
func (s *ReportPDFService) GenerateReportPDF(ctx context.Context, orderId, orderNo string) ([]byte, error) {
if orderId == "" && orderNo == "" {
return nil, fmt.Errorf("orderId 和 orderNo 不能同时为空")
}
reportURL := s.buildReportURL(orderId, orderNo)
logx.WithContext(ctx).Infof("GenerateReportPDF, reportURL=%s", reportURL)
// 为单次渲染创建 chromedp 上下文,并设置超时。
// 如需更高性能,可在外层维护一个共享的 chromedp 池。
ctxt, cancel := chromedp.NewContext(ctx)
defer cancel()
ctxt, timeoutCancel := context.WithTimeout(ctxt, 40*time.Second)
defer timeoutCancel()
var pdfBuf []byte
err := chromedp.Run(ctxt,
chromedp.Navigate(reportURL),
// 等待报告主体区域渲染完成(数据加载并成功渲染后才会出现)
chromedp.WaitVisible(`.pc-report-body`, chromedp.ByQuery),
chromedp.ActionFunc(func(ctx context.Context) error {
buf, _, pdfErr := page.PrintToPDF().
WithPrintBackground(true).
WithMarginTop(0).
WithMarginBottom(0).
WithMarginLeft(0).
WithMarginRight(0).
Do(ctx)
if pdfErr != nil {
return pdfErr
}
pdfBuf = buf
return nil
}),
)
if err != nil {
return nil, fmt.Errorf("使用 chromedp 生成报告 PDF 失败: %w", err)
}
return pdfBuf, nil
}
// GetOrGenerateReportPDF 先从本地缓存读取,若不存在则生成并写入磁盘。
func (s *ReportPDFService) GetOrGenerateReportPDF(ctx context.Context, orderId, orderNo string) ([]byte, error) {
if orderId == "" && orderNo == "" {
return nil, fmt.Errorf("orderId 和 orderNo 不能同时为空")
}
// 优先使用订单ID作为文件名否则退回订单号
fileKey := orderId
if fileKey == "" {
fileKey = orderNo
}
fileName := fmt.Sprintf("%s.pdf", fileKey)
if s.outputDir == "" {
// 未配置缓存目录,则每次都生成但不落盘
return s.GenerateReportPDF(ctx, orderId, orderNo)
}
fullPath := filepath.Join(s.outputDir, fileName)
// 如果文件已存在且非空,直接读取返回
if info, err := os.Stat(fullPath); err == nil && info.Size() > 0 {
data, readErr := os.ReadFile(fullPath)
if readErr == nil {
logx.WithContext(ctx).Infof("命中报告 PDF 缓存: %s", fullPath)
return data, nil
}
}
// 缓存未命中或读取失败,重新生成
pdfBytes, genErr := s.GenerateReportPDF(ctx, orderId, orderNo)
if genErr != nil {
return nil, genErr
}
// 尝试写入缓存(失败不影响主流程)
if mkErr := os.MkdirAll(s.outputDir, 0o755); mkErr == nil {
if writeErr := os.WriteFile(fullPath, pdfBytes, 0o644); writeErr != nil {
logx.WithContext(ctx).Errorf("写入报告 PDF 缓存失败: path=%s, err=%v", fullPath, writeErr)
} else {
logx.WithContext(ctx).Infof("生成并缓存报告 PDF 成功: %s", fullPath)
}
} else {
logx.WithContext(ctx).Errorf("创建报告 PDF 缓存目录失败: dir=%s, err=%v", s.outputDir, mkErr)
}
return pdfBytes, nil
}
func (s *ReportPDFService) buildReportURL(orderId, orderNo string) string {
// /in-webview 对外的 PC 报告路由,需与前端路由保持一致,例如 /report-pc
// 这里假设为: {baseReportURL}/report-pc?order_id=xxx&out_trade_no=xxx
// baseReportURL 通过配置注入,例如 https://your-domain/in-webview
query := ""
if orderId != "" {
query += "order_id=" + orderId
}
if orderNo != "" {
if query != "" {
query += "&"
}
query += "out_trade_no=" + orderNo
}
// 标记为 PDF 渲染模式,前端可据此调用免登录接口
if query != "" {
query += "&"
}
query += "pdf=1"
return fmt.Sprintf("%s/report-pc?%s", s.baseReportURL, query)
}