Files
tyapi-server/internal/shared/pdf/qygl_report_pdf_pregen.go
2026-03-21 19:10:50 +08:00

178 lines
4.7 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 pdf
import (
"context"
"net/url"
"strings"
"sync"
"time"
"go.uber.org/zap"
)
// QYGLReportPDFStatus 企业报告 PDF 预生成状态(供前端轮询)
type QYGLReportPDFStatus string
const (
QYGLReportPDFStatusNone QYGLReportPDFStatus = "none"
QYGLReportPDFStatusPending QYGLReportPDFStatus = "pending"
QYGLReportPDFStatusGenerating QYGLReportPDFStatus = "generating"
QYGLReportPDFStatusReady QYGLReportPDFStatus = "ready"
QYGLReportPDFStatusFailed QYGLReportPDFStatus = "failed"
)
// QYGLReportPDFPregen 异步预渲染企业报告 PDFheadless Chrome 访问公网可访问的报告 HTML
type QYGLReportPDFPregen struct {
logger *zap.Logger
cache *PDFCacheManager
baseURL string // 已 trim无尾斜杠空则禁用
mu sync.RWMutex
states map[string]*qyglPDFState
}
type qyglPDFState struct {
Status QYGLReportPDFStatus
Message string
UpdatedAt time.Time
}
// NewQYGLReportPDFPregen baseURL 须为外部可访问基址(如 https://api.example.com为空时不预生成
func NewQYGLReportPDFPregen(logger *zap.Logger, cache *PDFCacheManager, baseURL string) *QYGLReportPDFPregen {
if logger == nil {
logger = zap.NewNop()
}
u := strings.TrimSpace(baseURL)
u = strings.TrimRight(u, "/")
return &QYGLReportPDFPregen{
logger: logger,
cache: cache,
baseURL: u,
states: make(map[string]*qyglPDFState),
}
}
// ScheduleQYGLReportPDF 实现 processors.QYGLReportPDFScheduler
func (p *QYGLReportPDFPregen) ScheduleQYGLReportPDF(ctx context.Context, reportID string) {
p.schedule(ctx, reportID)
}
func (p *QYGLReportPDFPregen) schedule(_ context.Context, reportID string) {
if p == nil || p.baseURL == "" || reportID == "" || p.cache == nil {
return
}
if b, hit, _, err := p.cache.GetByReportID(reportID); hit && err == nil && len(b) > 0 {
p.setState(reportID, QYGLReportPDFStatusReady, "")
return
}
p.mu.Lock()
if st, ok := p.states[reportID]; ok {
if st.Status == QYGLReportPDFStatusGenerating || st.Status == QYGLReportPDFStatusPending {
p.mu.Unlock()
return
}
if st.Status == QYGLReportPDFStatusReady {
p.mu.Unlock()
return
}
}
p.states[reportID] = &qyglPDFState{
Status: QYGLReportPDFStatusPending,
Message: "",
UpdatedAt: time.Now(),
}
p.mu.Unlock()
go p.runGeneration(reportID)
}
func (p *QYGLReportPDFPregen) runGeneration(reportID string) {
p.setState(reportID, QYGLReportPDFStatusGenerating, "")
fullURL := p.baseURL + "/reports/qygl/" + url.PathEscape(reportID)
gen := NewHTMLPDFGenerator(p.logger)
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
defer cancel()
pdfBytes, err := gen.GenerateFromURL(ctx, fullURL)
if err != nil {
p.logger.Error("企业报告 PDF 预生成失败",
zap.String("report_id", reportID),
zap.String("url", fullURL),
zap.Error(err),
)
p.setState(reportID, QYGLReportPDFStatusFailed, "生成失败:"+err.Error())
return
}
if len(pdfBytes) == 0 {
p.setState(reportID, QYGLReportPDFStatusFailed, "生成的 PDF 为空")
return
}
if err := p.cache.SetByReportID(reportID, pdfBytes); err != nil {
p.logger.Error("企业报告 PDF 写入缓存失败",
zap.String("report_id", reportID),
zap.Error(err),
)
p.setState(reportID, QYGLReportPDFStatusFailed, "写入缓存失败")
return
}
p.setState(reportID, QYGLReportPDFStatusReady, "")
p.logger.Info("企业报告 PDF 预生成完成",
zap.String("report_id", reportID),
zap.Int("size", len(pdfBytes)),
)
}
func (p *QYGLReportPDFPregen) setState(reportID string, st QYGLReportPDFStatus, msg string) {
if p == nil {
return
}
p.mu.Lock()
defer p.mu.Unlock()
if p.states == nil {
p.states = make(map[string]*qyglPDFState)
}
p.states[reportID] = &qyglPDFState{
Status: st,
Message: msg,
UpdatedAt: time.Now(),
}
}
// Status 返回当前状态;若磁盘缓存已命中则始终为 ready
func (p *QYGLReportPDFPregen) Status(reportID string) (QYGLReportPDFStatus, string) {
if p == nil || reportID == "" {
return QYGLReportPDFStatusNone, ""
}
if p.cache != nil {
if b, hit, _, err := p.cache.GetByReportID(reportID); hit && err == nil && len(b) > 0 {
return QYGLReportPDFStatusReady, ""
}
}
p.mu.RLock()
defer p.mu.RUnlock()
st := p.states[reportID]
if st == nil {
if p.baseURL == "" {
return QYGLReportPDFStatusNone, "未启用预生成"
}
return QYGLReportPDFStatusNone, ""
}
return st.Status, st.Message
}
// MarkReadyAfterUpload 同步生成成功后与预生成状态对齐(可选)
func (p *QYGLReportPDFPregen) MarkReadyAfterUpload(reportID string) {
if p == nil || reportID == "" {
return
}
p.setState(reportID, QYGLReportPDFStatusReady, "")
}
// Enabled 是否配置了预生成基址
func (p *QYGLReportPDFPregen) Enabled() bool {
return p != nil && p.baseURL != ""
}