Files
tyapi-server/internal/shared/pdf/pdf_generator_refactored.go
2025-12-04 14:21:58 +08:00

237 lines
7.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 (
"bytes"
"context"
"fmt"
"os"
"path/filepath"
"strings"
"tyapi-server/internal/domains/product/entities"
"github.com/jung-kurt/gofpdf/v2"
"github.com/shopspring/decimal"
"go.uber.org/zap"
)
// PDFGeneratorRefactored 重构后的PDF生成器
type PDFGeneratorRefactored struct {
logger *zap.Logger
fontManager *FontManager
textProcessor *TextProcessor
markdownProc *MarkdownProcessor
tableParser *TableParser
tableRenderer *TableRenderer
jsonProcessor *JSONProcessor
logoPath string
watermarkText string
}
// NewPDFGeneratorRefactored 创建重构后的PDF生成器
func NewPDFGeneratorRefactored(logger *zap.Logger) *PDFGeneratorRefactored {
// 设置全局logger用于资源路径查找
SetGlobalLogger(logger)
// 初始化各个模块
textProcessor := NewTextProcessor()
fontManager := NewFontManager(logger)
markdownProc := NewMarkdownProcessor(textProcessor)
tableParser := NewTableParser(logger, fontManager)
tableRenderer := NewTableRenderer(logger, fontManager, textProcessor)
jsonProcessor := NewJSONProcessor()
gen := &PDFGeneratorRefactored{
logger: logger,
fontManager: fontManager,
textProcessor: textProcessor,
markdownProc: markdownProc,
tableParser: tableParser,
tableRenderer: tableRenderer,
jsonProcessor: jsonProcessor,
watermarkText: "海南海宇大数据有限公司",
}
// 查找logo文件
gen.findLogo()
return gen
}
// findLogo 查找logo文件仅从resources/pdf加载
func (g *PDFGeneratorRefactored) findLogo() {
// 获取resources/pdf目录使用统一的资源路径查找函数
resourcesPDFDir := GetResourcesPDFDir()
logoPath := filepath.Join(resourcesPDFDir, "logo.png")
// 检查文件是否存在
if _, err := os.Stat(logoPath); err == nil {
g.logoPath = logoPath
return
}
// 只记录关键错误
g.logger.Warn("未找到logo文件", zap.String("path", logoPath))
}
// GenerateProductPDF 为产品生成PDF文档接受响应类型内部转换
func (g *PDFGeneratorRefactored) GenerateProductPDF(ctx context.Context, productID string, productName, productCode, description, content string, price float64, doc *entities.ProductDocumentation) ([]byte, error) {
// 构建临时的 Product entity仅用于PDF生成
product := &entities.Product{
ID: productID,
Name: productName,
Code: productCode,
Description: description,
Content: content,
}
// 如果有价格信息,设置价格
if price > 0 {
product.Price = decimal.NewFromFloat(price)
}
return g.generatePDF(product, doc)
}
// GenerateProductPDFFromEntity 从entity类型生成PDF推荐使用
func (g *PDFGeneratorRefactored) GenerateProductPDFFromEntity(ctx context.Context, product *entities.Product, doc *entities.ProductDocumentation) ([]byte, error) {
return g.generatePDF(product, doc)
}
// generatePDF 内部PDF生成方法
func (g *PDFGeneratorRefactored) generatePDF(product *entities.Product, doc *entities.ProductDocumentation) (result []byte, err error) {
defer func() {
if r := recover(); r != nil {
// 将panic转换为error而不是重新抛出
if e, ok := r.(error); ok {
err = fmt.Errorf("PDF生成panic: %w", e)
} else {
err = fmt.Errorf("PDF生成panic: %v", r)
}
result = nil
}
}()
// 创建PDF文档 (A4大小gofpdf v2 默认支持UTF-8)
pdf := gofpdf.New("P", "mm", "A4", "")
// 优化边距,减少空白
pdf.SetMargins(15, 25, 15)
// 加载黑体字体(用于所有内容,除了水印)
chineseFontAvailable := g.fontManager.LoadChineseFont(pdf)
// 加载水印字体(使用宋体或其他非黑体字体)
g.fontManager.LoadWatermarkFont(pdf)
// 设置文档信息
pdf.SetTitle("Product Documentation", true)
pdf.SetAuthor("TYAPI Server", true)
pdf.SetCreator("TYAPI Server", true)
// 创建页面构建器
pageBuilder := NewPageBuilder(g.logger, g.fontManager, g.textProcessor, g.markdownProc, g.tableParser, g.tableRenderer, g.jsonProcessor, g.logoPath, g.watermarkText)
// 添加第一页(产品信息)
pageBuilder.AddFirstPage(pdf, product, doc, chineseFontAvailable)
// 如果有关联的文档,添加接口文档页面
if doc != nil {
pageBuilder.AddDocumentationPages(pdf, doc, chineseFontAvailable)
}
// 生成PDF字节流
// 注意gofpdf在Output时会重新访问字体文件
// 保存当前工作目录
originalWorkDir, _ := os.Getwd()
// 关键修复gofpdf在Output时会将绝对路径 /app/resources/pdf/fonts/simhei.ttf
// 转换为相对路径 app/resources/pdf/fonts/simhei.ttf去掉开头的/
//
// 为什么开发环境可以,生产环境不行?
// 1. Windows开发环境路径格式为 C:\...,不以/开头gofpdf处理方式不同
// 2. Linux生产环境路径格式为 /app/...,以/开头gofpdf在Output时会去掉开头的/
// 如果工作目录是 /app相对路径 app/resources 无法正确解析
// 3. 解决方案:将工作目录切换到根目录(/),这样相对路径 app/resources 就能解析为 /app/resources
resourcesDir := GetResourcesPDFDir()
if resourcesDir != "" && len(resourcesDir) > 0 && resourcesDir[0] == '/' {
// 切换到根目录,这样 gofpdf 转换后的相对路径 app/resources/pdf/fonts 就能解析为 /app/resources/pdf/fonts
if err := os.Chdir("/"); err == nil {
g.logger.Info("临时切换工作目录到根目录以修复gofpdf路径问题",
zap.String("reason", "gofpdf在Linux环境下会将绝对路径去掉开头的/转换为相对路径"),
zap.String("original_work_dir", originalWorkDir),
zap.String("new_work_dir", "/"),
zap.String("resources_dir", resourcesDir),
zap.String("example", "gofpdf将 /app/resources/pdf/fonts 转换为 app/resources/pdf/fonts需要工作目录为/才能正确解析"),
)
defer func() {
// 恢复原始工作目录
if originalWorkDir != "" {
if err := os.Chdir(originalWorkDir); err == nil {
g.logger.Debug("已恢复原始工作目录", zap.String("work_dir", originalWorkDir))
}
}
}()
} else {
g.logger.Warn("无法切换到根目录", zap.Error(err))
}
}
var buf bytes.Buffer
// 在Output前验证字体文件路径
if workDir, err := os.Getwd(); err == nil {
fontPath := filepath.Join(resourcesDir, "fonts", "simhei.ttf")
// 验证字体文件是否存在(使用绝对路径)
if absFontPath, err := filepath.Abs(fontPath); err == nil {
if _, err := os.Stat(absFontPath); err == nil {
g.logger.Debug("Output前验证字体文件存在",
zap.String("font_path", absFontPath),
zap.String("work_dir", workDir),
)
} else {
g.logger.Warn("Output前字体文件不存在",
zap.String("font_path", absFontPath),
zap.Error(err),
)
}
}
g.logger.Debug("准备生成PDF",
zap.String("work_dir", workDir),
zap.String("resources_pdf_dir", resourcesDir),
)
}
err = pdf.Output(&buf)
if err != nil {
// 记录详细的错误信息
currentWorkDir := ""
if wd, e := os.Getwd(); e == nil {
currentWorkDir = wd
}
// 尝试分析错误:如果是路径问题,记录更多信息
errStr := err.Error()
if strings.Contains(errStr, "stat ") && strings.Contains(errStr, ": no such file") {
g.logger.Error("PDF Output失败字体文件路径问题",
zap.Error(err),
zap.String("current_work_dir", currentWorkDir),
zap.String("resources_pdf_dir", resourcesDir),
zap.String("error_message", errStr),
)
} else {
g.logger.Error("PDF Output失败",
zap.Error(err),
zap.String("current_work_dir", currentWorkDir),
zap.String("resources_pdf_dir", resourcesDir),
)
}
return nil, fmt.Errorf("生成PDF失败: %w", err)
}
pdfBytes := buf.Bytes()
return pdfBytes, nil
}