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