178 lines
4.7 KiB
Go
178 lines
4.7 KiB
Go
|
|
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 异步预渲染企业报告 PDF(headless 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 != ""
|
|||
|
|
}
|