f
This commit is contained in:
@@ -2,8 +2,10 @@ package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
@@ -14,6 +16,7 @@ import (
|
||||
api_services "tyapi-server/internal/domains/api/services"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/domains/api/services/processors/qygl"
|
||||
"tyapi-server/internal/shared/pdf"
|
||||
)
|
||||
|
||||
// QYGLReportHandler 企业全景报告页面渲染处理器
|
||||
@@ -135,3 +138,59 @@ func (h *QYGLReportHandler) GetQYGLReportPageByID(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// GetQYGLReportPDFByID 通过编号导出企业全景报告 PDF(基于 headless Chrome 渲染 HTML)
|
||||
// GET /reports/qygl/:id/pdf
|
||||
func (h *QYGLReportHandler) GetQYGLReportPDFByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
c.String(http.StatusBadRequest, "报告编号不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
// 可选:从数据库查一次,用于生成更友好的文件名
|
||||
var fileName = "企业全景报告.pdf"
|
||||
if h.reportRepo != nil {
|
||||
if entity, err := h.reportRepo.FindByReportID(c.Request.Context(), id); err == nil && entity != nil {
|
||||
if entity.EntName != "" {
|
||||
fileName = fmt.Sprintf("%s_企业全景报告.pdf", entity.EntName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 根据当前请求推断访问协议(支持通过反向代理的 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
|
||||
}
|
||||
|
||||
// 构建用于 headless 浏览器访问的完整报告页面 URL
|
||||
reportURL := fmt.Sprintf("%s://%s/reports/qygl/%s", scheme, c.Request.Host, id)
|
||||
|
||||
h.logger.Info("开始生成企业全景报告 PDF(headless 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.logger.Error("生成的企业全景报告 PDF 为空", zap.String("report_id", id))
|
||||
c.String(http.StatusInternalServerError, "生成的企业报告 PDF 为空,请稍后重试")
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -28,5 +28,8 @@ func (r *QYGLReportRoutes) Register(router *sharedhttp.GinRouter) {
|
||||
|
||||
// 企业全景报告页面(通过编号查看)
|
||||
engine.GET("/reports/qygl/:id", r.handler.GetQYGLReportPageByID)
|
||||
|
||||
// 企业全景报告 PDF 导出(通过编号)
|
||||
engine.GET("/reports/qygl/:id/pdf", r.handler.GetQYGLReportPDFByID)
|
||||
}
|
||||
|
||||
|
||||
82
internal/shared/pdf/html_pdf_generator.go
Normal file
82
internal/shared/pdf/html_pdf_generator.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package pdf
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/chromedp/cdproto/page"
|
||||
"github.com/chromedp/chromedp"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// HTMLPDFGenerator 使用 headless Chrome 将 HTML 页面渲染为 PDF
|
||||
type HTMLPDFGenerator struct {
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewHTMLPDFGenerator 创建 HTMLPDFGenerator
|
||||
func NewHTMLPDFGenerator(logger *zap.Logger) *HTMLPDFGenerator {
|
||||
if logger == nil {
|
||||
logger = zap.NewNop()
|
||||
}
|
||||
return &HTMLPDFGenerator{
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateFromURL 使用 headless Chrome 打开指定 URL,并导出为 PDF 字节流
|
||||
// 这里固定使用 A4 纵向纸张,开启背景打印
|
||||
func (g *HTMLPDFGenerator) GenerateFromURL(ctx context.Context, url string) ([]byte, error) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
// 整个生成过程增加超时时间,避免长时间卡死
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 创建 Chrome 上下文(使用系统默认的 headless Chrome/Chromium)
|
||||
chromeCtx, cancelChrome := chromedp.NewContext(timeoutCtx)
|
||||
defer cancelChrome()
|
||||
|
||||
var pdfBuf []byte
|
||||
|
||||
tasks := chromedp.Tasks{
|
||||
chromedp.Navigate(url),
|
||||
// 等待页面主体和报告容器就绪,确保数据渲染完成
|
||||
chromedp.WaitReady("body", chromedp.ByQuery),
|
||||
chromedp.WaitVisible(".page", chromedp.ByQuery),
|
||||
chromedp.ActionFunc(func(ctx context.Context) error {
|
||||
g.logger.Info("开始通过 headless Chrome 生成企业报告 PDF", zap.String("url", url))
|
||||
var err error
|
||||
pdfBuf, err = page.PrintToPDF().
|
||||
WithPrintBackground(true).
|
||||
WithPaperWidth(8.27). // A4 宽度(英寸 -> 约 210mm)
|
||||
WithPaperHeight(11.69). // A4 高度(英寸 -> 约 297mm)
|
||||
WithMarginTop(0.4).
|
||||
WithMarginBottom(0.4).
|
||||
WithMarginLeft(0.4).
|
||||
WithMarginRight(0.4).
|
||||
Do(ctx)
|
||||
return err
|
||||
}),
|
||||
}
|
||||
|
||||
if err := chromedp.Run(chromeCtx, tasks); err != nil {
|
||||
g.logger.Error("使用 headless Chrome 生成 HTML 报告 PDF 失败", zap.String("url", url), zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(pdfBuf) == 0 {
|
||||
return nil, fmt.Errorf("生成的 PDF 内容为空")
|
||||
}
|
||||
|
||||
g.logger.Info("通过 headless Chrome 生成企业报告 PDF 成功",
|
||||
zap.String("url", url),
|
||||
zap.Int("pdf_size", len(pdfBuf)),
|
||||
)
|
||||
|
||||
return pdfBuf, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user