171 lines
5.3 KiB
Go
171 lines
5.3 KiB
Go
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, 90*time.Second)
|
||
defer timeoutCancel()
|
||
|
||
var pdfBuf []byte
|
||
err := chromedp.Run(ctxt,
|
||
chromedp.Navigate(reportURL),
|
||
// 等待报告主体区域渲染完成(数据加载并成功渲染后才会出现)
|
||
chromedp.WaitVisible(`.pc-report-body`, chromedp.ByQuery),
|
||
// 额外等待:确保至少渲染出一个模块块
|
||
chromedp.WaitVisible(`.pc-product-block`, chromedp.ByQuery),
|
||
// 某些模块可能按视口/IntersectionObserver 懒加载,打印前滚动到页底触发渲染
|
||
chromedp.ActionFunc(func(ctx context.Context) error {
|
||
// 逐步滚动到页面底部,避免一次性跳转导致部分观察器不触发
|
||
var height int64
|
||
if err := chromedp.Evaluate(`Math.max(document.body.scrollHeight, document.documentElement.scrollHeight)`, &height).Do(ctx); err != nil {
|
||
return err
|
||
}
|
||
step := int64(900)
|
||
if height < step {
|
||
step = height
|
||
}
|
||
for y := int64(0); y <= height; y += step {
|
||
js := fmt.Sprintf(`window.scrollTo(0, %d);`, y)
|
||
if err := chromedp.Evaluate(js, nil).Do(ctx); err != nil {
|
||
return err
|
||
}
|
||
time.Sleep(120 * time.Millisecond)
|
||
}
|
||
// 回到顶部,确保页眉与首屏布局稳定
|
||
if err := chromedp.Evaluate(`window.scrollTo(0, 0);`, nil).Do(ctx); err != nil {
|
||
return err
|
||
}
|
||
time.Sleep(500 * time.Millisecond)
|
||
return nil
|
||
}),
|
||
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)
|
||
}
|