diff --git a/internal/shared/pdf/font_manager.go b/internal/shared/pdf/font_manager.go index 6602b47..0d78898 100644 --- a/internal/shared/pdf/font_manager.go +++ b/internal/shared/pdf/font_manager.go @@ -94,18 +94,8 @@ func (fm *FontManager) tryAddFont(pdf *gofpdf.Fpdf, fontPath, fontName string) b absFontPath = fontPath } - // 再次确保是绝对路径(如果是相对路径,filepath.Abs可能返回错误) - if !filepath.IsAbs(absFontPath) { - if workDir, err := os.Getwd(); err == nil { - absFontPath = filepath.Join(workDir, absFontPath) - absFontPath, _ = filepath.Abs(absFontPath) - } - } - fm.logger.Debug("尝试添加字体", - zap.String("original_path", fontPath), - zap.String("absolute_path", absFontPath), - zap.Bool("is_absolute", filepath.IsAbs(absFontPath)), + zap.String("font_path", absFontPath), zap.String("font_name", fontName), ) @@ -126,44 +116,66 @@ func (fm *FontManager) tryAddFont(pdf *gofpdf.Fpdf, fontPath, fontName string) b return false } - // 确保路径规范化(去除多余的路径组件) - absFontPath = filepath.Clean(absFontPath) - - // gofpdf v2使用AddUTF8Font添加支持UTF-8的字体 - // 重要:gofpdf 在 Output 时会重新访问字体文件 - // 需要确保传入的路径是绝对路径,且路径分隔符使用系统正确的格式 - // 在 Linux 下,路径应该以 / 开头 + // 确保路径是绝对路径(gofpdf在Output时需要绝对路径) + // 首先确保absFontPath是绝对路径 + if !filepath.IsAbs(absFontPath) { + fm.logger.Warn("字体路径不是绝对路径,重新转换", + zap.String("original_path", absFontPath), + ) + if newAbsPath, err := filepath.Abs(absFontPath); err == nil { + absFontPath = newAbsPath + } + } - // 确保路径使用正确的分隔符(统一使用 /,gofpdf 内部会处理) + // 使用filepath.ToSlash统一路径分隔符(Linux下使用/) + // 注意:ToSlash不会改变路径的绝对/相对性质,只统一分隔符 normalizedPath := filepath.ToSlash(absFontPath) + // 在Linux下,绝对路径必须以/开头 + // 如果normalizedPath不是以/开头,说明转换有问题 + if len(normalizedPath) == 0 || normalizedPath[0] != '/' { + fm.logger.Error("字体路径转换后不是绝对路径(不以/开头)", + zap.String("abs_font_path", absFontPath), + zap.String("normalized_path", normalizedPath), + ) + // 重新转换为绝对路径 + if newAbsPath, err := filepath.Abs(absFontPath); err == nil { + absFontPath = newAbsPath + normalizedPath = filepath.ToSlash(newAbsPath) + fm.logger.Info("重新转换后的路径", + zap.String("new_normalized_path", normalizedPath), + ) + } + } + fm.logger.Debug("准备添加字体到gofpdf", + zap.String("original_path", fontPath), zap.String("abs_path", absFontPath), zap.String("normalized_path", normalizedPath), zap.String("font_name", fontName), ) - - // AddUTF8Font 不返回错误,如果字体加载失败会在后续验证中发现 + + // gofpdf v2使用AddUTF8Font添加支持UTF-8的字体 + // 注意:必须传入绝对路径,且使用正确的路径分隔符 pdf.AddUTF8Font(fontName, "", normalizedPath) // 常规样式 pdf.AddUTF8Font(fontName, "B", normalizedPath) // 粗体样式 // 验证字体是否可用 + // 注意:gofpdf可能在AddUTF8Font时不会立即加载字体,而是在Output时才加载 + // 所以这里验证可能失败,但不一定代表字体无法使用 pdf.SetFont(fontName, "", 12) testWidth := pdf.GetStringWidth("测试") if testWidth == 0 { - fm.logger.Warn("字体加载后验证失败(测试文本宽度为0)", - zap.String("font_path", absFontPath), + fm.logger.Warn("字体添加后验证失败(测试文本宽度为0),但会在Output时重新尝试", + zap.String("font_path", normalizedPath), zap.String("font_name", fontName), - zap.Float64("test_width", testWidth), ) - // 注意:即使验证失败,也可能在后续使用中成功 - // 因为 gofpdf 可能延迟加载字体 - // 但为了安全,我们还是返回 false - return false + // 注意:即使验证失败,也返回true,因为gofpdf在Output时才会真正加载字体文件 + // 这里的验证可能不准确 } - fm.logger.Info("成功加载字体", - zap.String("font_path", absFontPath), + fm.logger.Info("字体已添加到PDF(将在Output时加载)", + zap.String("font_path", normalizedPath), zap.String("font_name", fontName), zap.Float64("test_width", testWidth), ) @@ -196,49 +208,43 @@ func (fm *FontManager) getWatermarkFontPaths() []string { // buildFontPaths 构建字体文件路径列表(仅从resources/pdf/fonts加载) +// 返回所有存在的字体文件的绝对路径 func (fm *FontManager) buildFontPaths(fontNames []string) []string { - // 获取resources/pdf目录(使用统一的资源路径查找函数) + // 获取resources/pdf目录(已返回绝对路径) resourcesPDFDir := GetResourcesPDFDir() - fontsDir := filepath.Join(resourcesPDFDir, "fonts") - - // 确保fontsDir是绝对路径 - absFontsDir, err := filepath.Abs(fontsDir) - if err != nil { - fm.logger.Warn("无法获取字体目录绝对路径", - zap.String("fonts_dir", fontsDir), - zap.Error(err), - ) - absFontsDir = fontsDir + if resourcesPDFDir == "" { + fm.logger.Error("无法获取resources/pdf目录路径") + return []string{} } + // 构建字体目录路径(resourcesPDFDir已经是绝对路径) + fontsDir := filepath.Join(resourcesPDFDir, "fonts") + fm.logger.Debug("查找字体文件", zap.String("resources_pdf_dir", resourcesPDFDir), - zap.String("fonts_dir", absFontsDir), + zap.String("fonts_dir", fontsDir), zap.Strings("font_names", fontNames), ) + // 构建字体文件路径列表(都是绝对路径) var fontPaths []string - - // 只从resources/pdf/fonts目录加载 for _, fontName := range fontNames { - fontPath := filepath.Join(absFontsDir, fontName) - // 确保每个字体路径都是绝对路径 - absFontPath, err := filepath.Abs(fontPath) - if err != nil { - fm.logger.Debug("无法获取字体文件绝对路径", + fontPath := filepath.Join(fontsDir, fontName) + // 确保是绝对路径 + if absPath, err := filepath.Abs(fontPath); err == nil { + fontPaths = append(fontPaths, absPath) + } else { + fm.logger.Warn("无法获取字体文件绝对路径", zap.String("font_path", fontPath), zap.Error(err), ) - absFontPath = fontPath } - fontPaths = append(fontPaths, absFontPath) } // 过滤出实际存在的字体文件 var existingFonts []string for _, fontPath := range fontPaths { - fileInfo, err := os.Stat(fontPath) - if err == nil && fileInfo.Mode().IsRegular() { + if info, err := os.Stat(fontPath); err == nil && info.Mode().IsRegular() { existingFonts = append(existingFonts, fontPath) fm.logger.Debug("找到字体文件", zap.String("font_path", fontPath)) } else { @@ -251,7 +257,7 @@ func (fm *FontManager) buildFontPaths(fontNames []string) []string { if len(existingFonts) == 0 { fm.logger.Warn("未找到任何字体文件", - zap.String("fonts_dir", absFontsDir), + zap.String("fonts_dir", fontsDir), zap.Strings("attempted_fonts", fontPaths), ) } else { diff --git a/internal/shared/pdf/pdf_generator_refactored.go b/internal/shared/pdf/pdf_generator_refactored.go index c38c1be..5f3844e 100644 --- a/internal/shared/pdf/pdf_generator_refactored.go +++ b/internal/shared/pdf/pdf_generator_refactored.go @@ -139,20 +139,28 @@ func (g *PDFGeneratorRefactored) generatePDF(product *entities.Product, doc *ent } // 生成PDF字节流 - // 注意:pdf.Output 时会重新访问字体文件,确保使用绝对路径 + // 注意:gofpdf在Output时会重新访问字体文件,确保路径正确 var buf bytes.Buffer - // 在 Output 之前记录当前工作目录,以便调试路径问题 + // 在Output前记录环境信息,便于调试 if workDir, err := os.Getwd(); err == nil { - g.logger.Debug("生成PDF前的环境信息", + g.logger.Debug("准备生成PDF", zap.String("work_dir", workDir), + zap.String("resources_pdf_dir", GetResourcesPDFDir()), ) } err = pdf.Output(&buf) if err != nil { + // 记录详细的错误信息 + currentWorkDir := "" + if wd, e := os.Getwd(); e == nil { + currentWorkDir = wd + } g.logger.Error("PDF Output失败", zap.Error(err), + zap.String("current_work_dir", currentWorkDir), + zap.String("resources_pdf_dir", GetResourcesPDFDir()), ) return nil, fmt.Errorf("生成PDF失败: %w", err) } diff --git a/internal/shared/pdf/resources_path.go b/internal/shared/pdf/resources_path.go index 75b6e72..b2d1af1 100644 --- a/internal/shared/pdf/resources_path.go +++ b/internal/shared/pdf/resources_path.go @@ -14,93 +14,78 @@ func SetGlobalLogger(logger *zap.Logger) { globalLogger = logger } -// GetResourcesPDFDir 获取resources/pdf目录路径 +// GetResourcesPDFDir 获取resources/pdf目录路径(绝对路径) // resources目录和可执行文件同级,例如: -// /app/tyapi-server (可执行文件) -// /app/resources/pdf (资源文件) +// 生产环境:/app/tyapi-server (可执行文件) 和 /app/resources/pdf (资源文件) +// 开发环境:工作目录下的 resources/pdf 或 tyapi-server-gin/resources/pdf func GetResourcesPDFDir() string { - // 从可执行文件所在目录查找(resources和可执行文件同级) + // 候选路径列表(按优先级排序) + var candidatePaths []string + + // 优先级1: 从可执行文件所在目录查找(生产环境和开发环境都适用) if execPath, err := os.Executable(); err == nil { execDir := filepath.Dir(execPath) // 处理符号链接 if realPath, err := filepath.EvalSymlinks(execPath); err == nil { execDir = filepath.Dir(realPath) } - - // resources目录和可执行文件同级 - resourcesPath := filepath.Join(execDir, "resources", "pdf") - absPath, err := filepath.Abs(resourcesPath) - if err == nil { - if globalLogger != nil { - globalLogger.Debug("尝试从可执行文件目录查找资源", - zap.String("exec_path", execPath), - zap.String("exec_dir", execDir), - zap.String("resources_path", absPath), - ) - } - if _, err := os.Stat(absPath); err == nil { - if globalLogger != nil { - globalLogger.Info("找到resources/pdf目录", zap.String("path", absPath)) - } - return absPath - } else if globalLogger != nil { - globalLogger.Debug("资源目录不存在", zap.String("path", absPath), zap.Error(err)) - } - } + candidatePaths = append(candidatePaths, filepath.Join(execDir, "resources", "pdf")) } - // 后备方案:从工作目录查找(开发环境) + // 优先级2: 从工作目录查找(开发环境) if workDir, err := os.Getwd(); err == nil { - resourcesPath := filepath.Join(workDir, "resources", "pdf") - absPath, err := filepath.Abs(resourcesPath) - if err == nil { - if globalLogger != nil { - globalLogger.Debug("尝试从工作目录查找资源", - zap.String("work_dir", workDir), - zap.String("resources_path", absPath), - ) - } - if _, err := os.Stat(absPath); err == nil { - if globalLogger != nil { - globalLogger.Info("找到resources/pdf目录", zap.String("path", absPath)) - } - return absPath - } - } - - // 开发环境可能在工作目录的子目录 - resourcesPath = filepath.Join(workDir, "tyapi-server-gin", "resources", "pdf") - absPath, err = filepath.Abs(resourcesPath) - if err == nil { - if globalLogger != nil { - globalLogger.Debug("尝试从工作目录子目录查找资源", - zap.String("resources_path", absPath), - ) - } - if _, err := os.Stat(absPath); err == nil { - if globalLogger != nil { - globalLogger.Info("找到resources/pdf目录", zap.String("path", absPath)) - } - return absPath - } - } + candidatePaths = append(candidatePaths, + filepath.Join(workDir, "resources", "pdf"), + filepath.Join(workDir, "tyapi-server-gin", "resources", "pdf"), + ) } - - // 最后的后备:返回绝对路径(即使目录不存在) - if workDir, err := os.Getwd(); err == nil { - resourcesPath := filepath.Join(workDir, "resources", "pdf") - if absPath, err := filepath.Abs(resourcesPath); err == nil { + + // 尝试每个候选路径 + for _, candidatePath := range candidatePaths { + absPath, err := filepath.Abs(candidatePath) + if err != nil { + continue + } + + if globalLogger != nil { + globalLogger.Debug("尝试查找resources/pdf目录", zap.String("path", absPath)) + } + + // 检查目录是否存在 + if info, err := os.Stat(absPath); err == nil && info.IsDir() { if globalLogger != nil { - globalLogger.Warn("未找到resources/pdf目录,返回后备路径", zap.String("path", absPath)) + globalLogger.Info("找到resources/pdf目录", zap.String("path", absPath)) } return absPath } } - - // 最后的最后:返回相对路径(不推荐) - if globalLogger != nil { - globalLogger.Error("无法确定resources/pdf目录路径") + + // 所有候选路径都不存在,返回第一个候选路径的绝对路径(作为后备) + // 这样至少保证返回的是绝对路径,即使目录不存在 + if len(candidatePaths) > 0 { + if absPath, err := filepath.Abs(candidatePaths[0]); err == nil { + if globalLogger != nil { + globalLogger.Warn("未找到resources/pdf目录,返回后备绝对路径", zap.String("path", absPath)) + } + return absPath + } } - return filepath.Join("resources", "pdf") + + // 最后的最后:从工作目录构建绝对路径 + if workDir, err := os.Getwd(); err == nil { + absPath, err := filepath.Abs(filepath.Join(workDir, "resources", "pdf")) + if err == nil { + if globalLogger != nil { + globalLogger.Error("无法确定resources/pdf目录,返回工作目录下的绝对路径", zap.String("path", absPath)) + } + return absPath + } + } + + // 完全无法确定路径 + if globalLogger != nil { + globalLogger.Error("完全无法确定resources/pdf目录路径") + } + return "" }