first commit
This commit is contained in:
144
app/main/api/internal/service/reportpdfservice.go
Normal file
144
app/main/api/internal/service/reportpdfservice.go
Normal file
@@ -0,0 +1,144 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user