This commit is contained in:
2026-03-21 19:10:50 +08:00
parent 3775101081
commit 2fcf55deee
20 changed files with 60704 additions and 436 deletions

View File

@@ -6,6 +6,7 @@ import (
"html/template"
"net/http"
"net/url"
"time"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
@@ -27,6 +28,7 @@ type QYGLReportHandler struct {
reportRepo api_repositories.ReportRepository
pdfCacheManager *pdf.PDFCacheManager
qyglPDFPregen *pdf.QYGLReportPDFPregen
}
// NewQYGLReportHandler 创建企业报告页面处理器
@@ -35,12 +37,14 @@ func NewQYGLReportHandler(
logger *zap.Logger,
reportRepo api_repositories.ReportRepository,
pdfCacheManager *pdf.PDFCacheManager,
qyglPDFPregen *pdf.QYGLReportPDFPregen,
) *QYGLReportHandler {
return &QYGLReportHandler{
apiRequestService: apiRequestService,
logger: logger,
reportRepo: reportRepo,
pdfCacheManager: pdfCacheManager,
qyglPDFPregen: qyglPDFPregen,
}
}
@@ -141,7 +145,49 @@ func (h *QYGLReportHandler) GetQYGLReportPageByID(c *gin.Context) {
})
}
// GetQYGLReportPDFByID 通过编号导出企业全景报告 PDF基于 headless Chrome 渲染 HTML
// GetQYGLReportPDFStatusByID 查询企业报告 PDF 预生成状态(供前端轮询
// GET /reports/qygl/:id/pdf/status
func (h *QYGLReportHandler) GetQYGLReportPDFStatusByID(c *gin.Context) {
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{"status": "none", "message": "报告编号不能为空"})
return
}
if h.pdfCacheManager != nil {
if b, hit, _, err := h.pdfCacheManager.GetByReportID(id); hit && err == nil && len(b) > 0 {
c.JSON(http.StatusOK, gin.H{"status": string(pdf.QYGLReportPDFStatusReady), "message": ""})
return
}
}
if h.qyglPDFPregen == nil || !h.qyglPDFPregen.Enabled() {
c.JSON(http.StatusOK, gin.H{"status": string(pdf.QYGLReportPDFStatusNone), "message": "未启用预生成,将在下载时现场生成"})
return
}
st, msg := h.qyglPDFPregen.Status(id)
c.JSON(http.StatusOK, gin.H{"status": string(st), "message": userFacingPDFStatusMessage(st, msg)})
}
func userFacingPDFStatusMessage(st pdf.QYGLReportPDFStatus, raw string) string {
switch st {
case pdf.QYGLReportPDFStatusPending:
return "排队生成中"
case pdf.QYGLReportPDFStatusGenerating:
return "正在生成 PDF"
case pdf.QYGLReportPDFStatusFailed:
if raw != "" {
return raw
}
return "预生成失败,下载时将重新生成"
case pdf.QYGLReportPDFStatusReady:
return ""
case pdf.QYGLReportPDFStatusNone:
return "尚未开始预生成"
default:
return ""
}
}
// GetQYGLReportPDFByID 通过编号导出企业全景报告 PDF优先读缓存可短时等待预生成否则 headless 现场生成并写入缓存
// GET /reports/qygl/:id/pdf
func (h *QYGLReportHandler) GetQYGLReportPDFByID(c *gin.Context) {
id := c.Param("id")
@@ -150,7 +196,6 @@ func (h *QYGLReportHandler) GetQYGLReportPDFByID(c *gin.Context) {
return
}
// 可选:从数据库查一次,用于生成更友好的文件名
var fileName = "企业全景报告.pdf"
if h.reportRepo != nil {
if entity, err := h.reportRepo.FindByReportID(c.Request.Context(), id); err == nil && entity != nil {
@@ -160,46 +205,66 @@ func (h *QYGLReportHandler) GetQYGLReportPDFByID(c *gin.Context) {
}
}
// 临时关闭企业全景报告 PDF 的读取缓存,强制每次都重新生成
// 如果后续需要恢复缓存,可在此重新启用 pdfCacheManager.GetByReportID 的逻辑。
// 根据当前请求推断访问协议(支持通过反向代理的 X-Forwarded-Proto
scheme := "http"
if c.Request.TLS != nil {
scheme = "https"
} else if forwardedProto := c.Request.Header.Get("X-Forwarded-Proto"); forwardedProto != "" {
scheme = forwardedProto
var pdfBytes []byte
if h.pdfCacheManager != nil {
if b, hit, _, err := h.pdfCacheManager.GetByReportID(id); hit && err == nil && len(b) > 0 {
pdfBytes = b
}
}
// 构建用于 headless 浏览器访问的完整报告页面 URL
reportURL := fmt.Sprintf("%s://%s/reports/qygl/%s", scheme, c.Request.Host, id)
h.logger.Info("开始生成企业全景报告 PDFheadless Chrome",
zap.String("report_id", id),
zap.String("url", reportURL),
)
pdfGen := pdf.NewHTMLPDFGenerator(h.logger)
pdfBytes, err := pdfGen.GenerateFromURL(c.Request.Context(), reportURL)
if err != nil {
h.logger.Error("生成企业全景报告 PDF 失败", zap.String("report_id", id), zap.Error(err))
c.String(http.StatusInternalServerError, "生成企业报告 PDF 失败,请稍后重试")
return
// 缓存未命中时:若正在预生成,短时等待(与前端轮询互补)
if len(pdfBytes) == 0 && h.qyglPDFPregen != nil && h.qyglPDFPregen.Enabled() && h.pdfCacheManager != nil {
deadline := time.Now().Add(90 * time.Second)
for time.Now().Before(deadline) {
st, _ := h.qyglPDFPregen.Status(id)
if st == pdf.QYGLReportPDFStatusFailed {
break
}
if b, hit, _, err := h.pdfCacheManager.GetByReportID(id); hit && err == nil && len(b) > 0 {
pdfBytes = b
break
}
time.Sleep(400 * time.Millisecond)
}
}
if len(pdfBytes) == 0 {
h.logger.Error("生成的企业全景报告 PDF 为空", zap.String("report_id", id))
c.String(http.StatusInternalServerError, "生成的企业报告 PDF 为空,请稍后重试")
return
}
scheme := "http"
if c.Request.TLS != nil {
scheme = "https"
} else if forwardedProto := c.Request.Header.Get("X-Forwarded-Proto"); forwardedProto != "" {
scheme = forwardedProto
}
reportURL := fmt.Sprintf("%s://%s/reports/qygl/%s", scheme, c.Request.Host, id)
// 临时关闭企业全景报告 PDF 的写入缓存,只返回最新生成的 PDF。
// 若后续需要重新启用缓存,可恢复对 pdfCacheManager.SetByReportID 的调用。
h.logger.Info("现场生成企业全景报告 PDFheadless Chrome",
zap.String("report_id", id),
zap.String("url", reportURL),
)
pdfGen := pdf.NewHTMLPDFGenerator(h.logger)
var err error
pdfBytes, err = pdfGen.GenerateFromURL(c.Request.Context(), reportURL)
if err != nil {
h.logger.Error("生成企业全景报告 PDF 失败", zap.String("report_id", id), zap.Error(err))
c.String(http.StatusInternalServerError, "生成企业报告 PDF 失败,请稍后重试")
return
}
if len(pdfBytes) == 0 {
h.logger.Error("生成的企业全景报告 PDF 为空", zap.String("report_id", id))
c.String(http.StatusInternalServerError, "生成的企业报告 PDF 为空,请稍后重试")
return
}
if h.pdfCacheManager != nil {
_ = h.pdfCacheManager.SetByReportID(id, pdfBytes)
}
if h.qyglPDFPregen != nil {
h.qyglPDFPregen.MarkReadyAfterUpload(id)
}
}
encodedFileName := url.QueryEscape(fileName)
c.Header("Content-Type", "application/pdf")
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", encodedFileName))
c.Data(http.StatusOK, "application/pdf", pdfBytes)
}

View File

@@ -29,7 +29,9 @@ func (r *QYGLReportRoutes) Register(router *sharedhttp.GinRouter) {
// 企业全景报告页面(通过编号查看)
engine.GET("/reports/qygl/:id", r.handler.GetQYGLReportPageByID)
// 企业全景报告 PDF 预生成状态(通过编号,供前端轮询)
engine.GET("/reports/qygl/:id/pdf/status", r.handler.GetQYGLReportPDFStatusByID)
// 企业全景报告 PDF 导出(通过编号)
engine.GET("/reports/qygl/:id/pdf", r.handler.GetQYGLReportPDFByID)
}