2025-12-03 12:03:42 +08:00
|
|
|
|
package pdf
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"bytes"
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"os"
|
|
|
|
|
|
"path/filepath"
|
2025-12-04 14:21:58 +08:00
|
|
|
|
"strings"
|
2025-12-03 12:03:42 +08:00
|
|
|
|
|
|
|
|
|
|
"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 {
|
2025-12-04 13:20:03 +08:00
|
|
|
|
// 设置全局logger(用于资源路径查找)
|
|
|
|
|
|
SetGlobalLogger(logger)
|
|
|
|
|
|
|
2025-12-03 12:03:42 +08:00
|
|
|
|
// 初始化各个模块
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-04 12:56:39 +08:00
|
|
|
|
// findLogo 查找logo文件(仅从resources/pdf加载)
|
2025-12-03 12:03:42 +08:00
|
|
|
|
func (g *PDFGeneratorRefactored) findLogo() {
|
2025-12-04 12:56:39 +08:00
|
|
|
|
// 获取resources/pdf目录(使用统一的资源路径查找函数)
|
|
|
|
|
|
resourcesPDFDir := GetResourcesPDFDir()
|
|
|
|
|
|
logoPath := filepath.Join(resourcesPDFDir, "logo.png")
|
|
|
|
|
|
|
|
|
|
|
|
// 检查文件是否存在
|
|
|
|
|
|
if _, err := os.Stat(logoPath); err == nil {
|
|
|
|
|
|
g.logoPath = logoPath
|
|
|
|
|
|
return
|
2025-12-04 10:35:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 只记录关键错误
|
2025-12-04 12:56:39 +08:00
|
|
|
|
g.logger.Warn("未找到logo文件", zap.String("path", logoPath))
|
2025-12-03 12:03:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
|
}
|
|
|
|
|
|
}()
|
|
|
|
|
|
|
2025-12-04 14:26:56 +08:00
|
|
|
|
// 保存当前工作目录(用于后续恢复)
|
|
|
|
|
|
originalWorkDir, _ := os.Getwd()
|
2025-12-04 16:17:04 +08:00
|
|
|
|
|
2025-12-04 14:26:56 +08:00
|
|
|
|
// 关键修复:gofpdf在AddUTF8Font和Output时都会处理字体路径
|
|
|
|
|
|
// 如果路径是绝对路径 /app/resources/pdf/fonts/simhei.ttf,gofpdf会去掉开头的/
|
|
|
|
|
|
// 变成相对路径 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))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-03 12:03:42 +08:00
|
|
|
|
// 创建PDF文档 (A4大小,gofpdf v2 默认支持UTF-8)
|
|
|
|
|
|
pdf := gofpdf.New("P", "mm", "A4", "")
|
|
|
|
|
|
// 优化边距,减少空白
|
|
|
|
|
|
pdf.SetMargins(15, 25, 15)
|
|
|
|
|
|
|
|
|
|
|
|
// 加载黑体字体(用于所有内容,除了水印)
|
2025-12-04 14:26:56 +08:00
|
|
|
|
// 注意:此时工作目录应该是根目录(/),这样gofpdf处理路径时就能正确解析
|
2025-12-03 12:03:42 +08:00
|
|
|
|
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字节流
|
2025-12-04 14:26:56 +08:00
|
|
|
|
// 注意:工作目录已经在AddUTF8Font之前切换到了根目录(/)
|
|
|
|
|
|
// 这样gofpdf在Output时使用相对路径 app/resources/pdf/fonts 就能正确解析
|
2025-12-04 14:21:58 +08:00
|
|
|
|
|
2025-12-03 12:03:42 +08:00
|
|
|
|
var buf bytes.Buffer
|
2025-12-04 16:17:27 +08:00
|
|
|
|
|
2025-12-04 14:26:56 +08:00
|
|
|
|
// 在Output前验证字体文件路径(此时工作目录应该是根目录/)
|
2025-12-04 13:28:03 +08:00
|
|
|
|
if workDir, err := os.Getwd(); err == nil {
|
2025-12-04 14:26:56 +08:00
|
|
|
|
// 验证绝对路径
|
|
|
|
|
|
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),
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
2025-12-04 16:17:27 +08:00
|
|
|
|
|
2025-12-04 14:26:56 +08:00
|
|
|
|
// 验证相对路径(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),
|
2025-12-04 14:21:58 +08:00
|
|
|
|
zap.String("work_dir", workDir),
|
|
|
|
|
|
)
|
|
|
|
|
|
} else {
|
2025-12-04 14:26:56 +08:00
|
|
|
|
g.logger.Warn("Output前相对路径解析的字体文件不存在",
|
|
|
|
|
|
zap.String("font_rel_path", fontRelPath),
|
|
|
|
|
|
zap.String("resolved_abs_path", relAbsPath),
|
|
|
|
|
|
zap.String("work_dir", workDir),
|
2025-12-04 14:21:58 +08:00
|
|
|
|
zap.Error(err),
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-04 16:17:27 +08:00
|
|
|
|
|
2025-12-04 13:42:32 +08:00
|
|
|
|
g.logger.Debug("准备生成PDF",
|
2025-12-04 13:28:03 +08:00
|
|
|
|
zap.String("work_dir", workDir),
|
2025-12-04 14:21:58 +08:00
|
|
|
|
zap.String("resources_pdf_dir", resourcesDir),
|
2025-12-04 14:26:56 +08:00
|
|
|
|
zap.Bool("work_dir_changed", workDirChanged),
|
2025-12-04 13:28:03 +08:00
|
|
|
|
)
|
|
|
|
|
|
}
|
2025-12-04 16:17:27 +08:00
|
|
|
|
|
2025-12-03 12:03:42 +08:00
|
|
|
|
err = pdf.Output(&buf)
|
|
|
|
|
|
if err != nil {
|
2025-12-04 13:42:32 +08:00
|
|
|
|
// 记录详细的错误信息
|
|
|
|
|
|
currentWorkDir := ""
|
|
|
|
|
|
if wd, e := os.Getwd(); e == nil {
|
|
|
|
|
|
currentWorkDir = wd
|
|
|
|
|
|
}
|
2025-12-04 16:17:27 +08:00
|
|
|
|
|
2025-12-04 14:21:58 +08:00
|
|
|
|
// 尝试分析错误:如果是路径问题,记录更多信息
|
|
|
|
|
|
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),
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
2025-12-03 12:03:42 +08:00
|
|
|
|
return nil, fmt.Errorf("生成PDF失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pdfBytes := buf.Bytes()
|
|
|
|
|
|
|
|
|
|
|
|
return pdfBytes, nil
|
|
|
|
|
|
}
|