Files
in-server/app/main/api/internal/service/reportpdfservice.go

145 lines
4.2 KiB
Go
Raw Normal View History

2026-03-18 00:01:48 +08:00
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-page`, chromedp.ByQuery),
// 再等待一小段时间以便异步数据加载
chromedp.Sleep(500*time.Millisecond),
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)
}