Files
tyapi-server/internal/shared/pdf/pdf_generator_refactored.go
2025-12-04 16:17:27 +08:00

249 lines
8.0 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
}
}()
// 保存当前工作目录(用于后续恢复)
originalWorkDir, _ := os.Getwd()
// 关键修复gofpdf在AddUTF8Font和Output时都会处理字体路径
// 如果路径是绝对路径 /app/resources/pdf/fonts/simhei.ttfgofpdf会去掉开头的/
// 变成相对路径 app/resources/pdf/fonts/simhei.ttf
// 解决方案在AddUTF8Font之前就切换工作目录到根目录/
resourcesDir := GetResourcesPDFDir()
workDirChanged := false
if resourcesDir != "" && len(resourcesDir) > 0 && resourcesDir[0] == '/' {
// 切换到根目录,这样 gofpdf 转换后的相对路径 app/resources 就能解析为 /app/resources
if err := os.Chdir("/"); err == nil {
workDirChanged = true
g.logger.Info("切换工作目录到根目录在AddUTF8Font之前以修复gofpdf路径问题",
zap.String("reason", "gofpdf在AddUTF8Font时就会处理路径需要在加载字体前切换工作目录"),
zap.String("original_work_dir", originalWorkDir),
zap.String("new_work_dir", "/"),
zap.String("resources_dir", resourcesDir),
)
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))
}
}
// 创建PDF文档 (A4大小gofpdf v2 默认支持UTF-8)
pdf := gofpdf.New("P", "mm", "A4", "")
// 优化边距,减少空白
pdf.SetMargins(15, 25, 15)
// 加载黑体字体(用于所有内容,除了水印)
// 注意:此时工作目录应该是根目录(/这样gofpdf处理路径时就能正确解析
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字节流
// 注意工作目录已经在AddUTF8Font之前切换到了根目录/
// 这样gofpdf在Output时使用相对路径 app/resources/pdf/fonts 就能正确解析
var buf bytes.Buffer
// 在Output前验证字体文件路径此时工作目录应该是根目录/
if workDir, err := os.Getwd(); err == nil {
// 验证绝对路径
fontAbsPath := filepath.Join(resourcesDir, "fonts", "simhei.ttf")
if _, err := os.Stat(fontAbsPath); err == nil {
g.logger.Debug("Output前验证字体文件存在绝对路径",
zap.String("font_abs_path", fontAbsPath),
zap.String("work_dir", workDir),
)
}
// 验证相对路径gofpdf可能使用的路径
fontRelPath := "app/resources/pdf/fonts/simhei.ttf"
if relAbsPath, err := filepath.Abs(fontRelPath); err == nil {
if _, err := os.Stat(relAbsPath); err == nil {
g.logger.Debug("Output前验证字体文件存在相对路径解析",
zap.String("font_rel_path", fontRelPath),
zap.String("resolved_abs_path", relAbsPath),
zap.String("work_dir", workDir),
)
} else {
g.logger.Warn("Output前相对路径解析的字体文件不存在",
zap.String("font_rel_path", fontRelPath),
zap.String("resolved_abs_path", relAbsPath),
zap.String("work_dir", workDir),
zap.Error(err),
)
}
}
g.logger.Debug("准备生成PDF",
zap.String("work_dir", workDir),
zap.String("resources_pdf_dir", resourcesDir),
zap.Bool("work_dir_changed", workDirChanged),
)
}
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
}