diff --git a/Dockerfile b/Dockerfile index 56a5568..32d75a3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -50,8 +50,12 @@ WORKDIR /app COPY --from=builder /app/tyapi-server . # 复制配置文件 -COPY --chown=tyapi:tyapi config.yaml . -COPY --chown=tyapi:tyapi configs/ ./configs/ +COPY config.yaml . +COPY configs/ ./configs/ + +# 复制字体文件(PDF生成需要)- 创建目录结构并复制字体文件 +RUN mkdir -p ./internal/shared/pdf/fonts +COPY --from=builder /app/internal/shared/pdf/fonts/ ./internal/shared/pdf/fonts/ # 暴露端口 diff --git a/docs/Ubuntu服务器PDF字体配置指南.md b/docs/Ubuntu服务器PDF字体配置指南.md new file mode 100644 index 0000000..99e959f --- /dev/null +++ b/docs/Ubuntu服务器PDF字体配置指南.md @@ -0,0 +1,242 @@ +# Ubuntu服务器PDF字体配置指南 + +## 概述 + +本文档说明如何在Ubuntu 24.04 LTS服务器上配置PDF生成功能所需的中文字体。 + +## 字体文件位置 + +确保字体文件存在于以下任一位置: + +### 推荐路径(按优先级) + +1. **工作目录相对路径**(最常用) + ``` + {工作目录}/internal/shared/pdf/fonts/ + ``` + 例如:如果工作目录是 `/www/tyapi-server`,则字体应在: + ``` + /www/tyapi-server/internal/shared/pdf/fonts/ + ``` + +2. **可执行文件相对路径** + ``` + {可执行文件所在目录}/internal/shared/pdf/fonts/ + ``` + +3. **环境变量指定路径** + ```bash + export PDF_FONT_DIR=/path/to/fonts + ``` + +4. **硬编码路径**(后备方案) + - `/www/tyapi-server/internal/shared/pdf/fonts` ✅(已配置) + - `/app/internal/shared/pdf/fonts`(Docker) + - `/usr/local/tyapi-server/internal/shared/pdf/fonts` + - `/opt/tyapi-server/internal/shared/pdf/fonts` + - `/home/ubuntu/tyapi-server/internal/shared/pdf/fonts` + - `/root/tyapi-server/internal/shared/pdf/fonts` + - `/var/www/tyapi-server/internal/shared/pdf/fonts` + +## 部署步骤 + +### 方法1:直接复制字体文件(推荐) + +```bash +# 1. 创建字体目录 +sudo mkdir -p /www/tyapi-server/internal/shared/pdf/fonts + +# 2. 复制字体文件(从本地或Git仓库) +# 需要以下字体文件: +# - simhei.ttf (黑体,必需) +# - simkai.ttf (楷体,可选) +# - simfang.ttf (仿宋,可选) +# - YunFengFeiYunTi-2.ttf (水印字体,可选) + +# 3. 设置权限 +sudo chmod -R 644 /www/tyapi-server/internal/shared/pdf/fonts/*.ttf +sudo chmod -R 644 /www/tyapi-server/internal/shared/pdf/fonts/*.ttc + +# 4. 确保运行用户有读取权限 +sudo chown -R $(whoami):$(whoami) /www/tyapi-server/internal/shared/pdf/fonts +``` + +### 方法2:使用环境变量 + +```bash +# 设置字体目录环境变量 +export PDF_FONT_DIR=/www/tyapi-server/internal/shared/pdf/fonts + +# 或在 systemd 服务文件中添加 +# Environment="PDF_FONT_DIR=/www/tyapi-server/internal/shared/pdf/fonts" +``` + +### 方法3:使用符号链接 + +如果字体文件在其他位置,可以创建符号链接: + +```bash +sudo mkdir -p /www/tyapi-server/internal/shared/pdf/fonts +sudo ln -s /path/to/actual/fonts/*.ttf /www/tyapi-server/internal/shared/pdf/fonts/ +``` + +## 验证字体文件 + +### 1. 检查文件是否存在 + +```bash +ls -lh /www/tyapi-server/internal/shared/pdf/fonts/ +``` + +应该看到: +``` +-rw-r--r-- 1 user user 9.5M Dec 3 18:00 simhei.ttf +-rw-r--r-- 1 user user 8.2M Dec 3 18:00 simkai.ttf +-rw-r--r-- 1 user user 7.8M Dec 3 18:00 simfang.ttf +``` + +### 2. 检查文件权限 + +```bash +stat /www/tyapi-server/internal/shared/pdf/fonts/simhei.ttf +``` + +确保有读取权限(至少 `-r--r--r--`)。 + +### 3. 检查文件类型 + +```bash +file /www/tyapi-server/internal/shared/pdf/fonts/simhei.ttf +``` + +应该显示:`TrueType font data` + +## 验证PDF生成功能 + +### 1. 查看日志 + +启动服务后,查看日志中是否有以下信息: + +```json +{"level":"INFO","msg":"找到字体文件","count":3,"paths":["/www/tyapi-server/internal/shared/pdf/fonts/simhei.ttf",...]} +{"level":"INFO","msg":"成功加载中文字体","font_path":"/www/tyapi-server/internal/shared/pdf/fonts/simhei.ttf"} +``` + +### 2. 测试PDF生成 + +调用PDF下载接口,检查: +- PDF文件能正常生成 +- 中文文字正常显示(不是乱码或空白) +- 没有字体相关的错误日志 + +### 3. 调试信息 + +如果字体未找到,查看日志中的调试信息: + +```json +{"level":"DEBUG","msg":"查找字体文件","total_paths":20,"paths":[...]} +{"level":"DEBUG","msg":"字体文件不存在","font_path":"...","error":"..."} +``` + +## 常见问题 + +### 问题1:字体文件找不到 + +**症状**:日志显示 `"未找到中文字体文件"` + +**解决方案**: +1. 确认字体文件路径是否正确 +2. 检查文件权限:`chmod 644 *.ttf` +3. 检查文件所有者:`chown user:user *.ttf` +4. 查看日志中的 `"查找字体文件"` 调试信息,确认尝试的路径 + +### 问题2:字体文件无权限读取 + +**症状**:日志显示 `"字体文件无读取权限"` + +**解决方案**: +```bash +sudo chmod 644 /www/tyapi-server/internal/shared/pdf/fonts/*.ttf +sudo chown -R $(whoami):$(whoami) /www/tyapi-server/internal/shared/pdf/fonts +``` + +### 问题3:中文显示为乱码 + +**症状**:PDF中中文显示为乱码或空白 + +**解决方案**: +1. 确认字体文件已成功加载(查看日志) +2. 确认字体文件是有效的TTF格式 +3. 检查字体文件是否损坏:`file *.ttf` + +### 问题4:Docker容器中找不到字体 + +**症状**:在Docker容器中运行时找不到字体 + +**解决方案**: +1. 确保Dockerfile中已复制字体文件: + ```dockerfile + COPY --from=builder /app/internal/shared/pdf/fonts/ ./internal/shared/pdf/fonts/ + ``` +2. 或使用volume挂载: + ```yaml + volumes: + - /www/tyapi-server/internal/shared/pdf/fonts:/app/internal/shared/pdf/fonts:ro + ``` + +## Systemd服务配置示例 + +如果使用systemd管理服务,可以在服务文件中设置环境变量: + +```ini +[Unit] +Description=TYAPI Server +After=network.target + +[Service] +Type=simple +User=ubuntu +WorkingDirectory=/www/tyapi-server +ExecStart=/www/tyapi-server/tyapi-server -env=production +Environment="PDF_FONT_DIR=/www/tyapi-server/internal/shared/pdf/fonts" +Restart=always +RestartSec=5 + +[Install] +WantedBy=multi-user.target +``` + +## 字体文件获取 + +如果本地没有字体文件,可以从以下来源获取: + +1. **Windows系统字体**(如果服务器是Windows迁移过来的) + - `C:\Windows\Fonts\simhei.ttf` → 复制到服务器 + +2. **Linux系统字体包** + ```bash + # Ubuntu/Debian + sudo apt-get install fonts-wqy-zenhei fonts-wqy-microhei + # 然后从系统字体目录复制或创建符号链接 + ``` + +3. **从项目仓库** + - 确保字体文件已提交到Git仓库 + - 使用 `git pull` 拉取最新代码 + +## 注意事项 + +1. **字体文件大小**:每个TTF文件约8-10MB,确保有足够磁盘空间 +2. **文件权限**:确保运行服务的用户有读取权限 +3. **路径一致性**:确保字体路径与代码中的查找路径一致 +4. **日志级别**:生产环境建议将字体查找日志设为DEBUG级别,避免日志过多 + +## 技术支持 + +如果遇到问题,请提供以下信息: +1. 服务器操作系统版本:`lsb_release -a` +2. 字体文件位置和权限:`ls -lh /www/tyapi-server/internal/shared/pdf/fonts/` +3. 工作目录:`pwd`(服务运行时) +4. 可执行文件位置:`which tyapi-server` 或 `readlink -f $(which tyapi-server)` +5. 相关日志:包含 `"查找字体文件"` 和 `"字体文件"` 的日志条目 + diff --git a/internal/shared/pdf/font_manager.go b/internal/shared/pdf/font_manager.go index b86af92..660dc98 100644 --- a/internal/shared/pdf/font_manager.go +++ b/internal/shared/pdf/font_manager.go @@ -25,12 +25,19 @@ func NewFontManager(logger *zap.Logger) *FontManager { _, filename, _, _ := runtime.Caller(0) baseDir := filepath.Dir(filename) - return &FontManager{ + fm := &FontManager{ logger: logger, chineseFontName: "ChineseFont", watermarkFontName: "WatermarkFont", baseDir: baseDir, } + + // 记录基础目录(用于调试) + logger.Debug("字体管理器初始化", + zap.String("base_dir", baseDir), + zap.String("caller_file", filename)) + + return fm } // LoadChineseFont 加载中文字体到PDF @@ -41,20 +48,27 @@ func (fm *FontManager) LoadChineseFont(pdf *gofpdf.Fpdf) bool { fontPaths := fm.getChineseFontPaths() if len(fontPaths) == 0 { - fm.logger.Warn("未找到中文字体文件,PDF中的中文可能显示为空白") + fm.logger.Warn("未找到中文字体文件,PDF中的中文可能显示为空白", + zap.String("base_dir", fm.baseDir), + zap.String("hint", "请确保字体文件存在于 internal/shared/pdf/fonts/ 目录下,或设置 PDF_FONT_DIR 环境变量")) return false } // 尝试加载字体 for _, fontPath := range fontPaths { + fm.logger.Debug("尝试加载字体", zap.String("font_path", fontPath)) if fm.tryAddFont(pdf, fontPath, fm.chineseFontName) { fm.chineseFontLoaded = true fm.logger.Info("成功加载中文字体", zap.String("font_path", fontPath)) return true + } else { + fm.logger.Debug("字体加载失败", zap.String("font_path", fontPath)) } } - fm.logger.Warn("所有中文字体文件都无法加载,PDF中的中文可能显示为空白") + fm.logger.Warn("所有中文字体文件都无法加载,PDF中的中文可能显示为空白", + zap.Int("tried_paths", len(fontPaths)), + zap.Strings("font_paths", fontPaths)) return false } @@ -140,23 +154,133 @@ func (fm *FontManager) getWatermarkFontPaths() []string { return fm.buildFontPaths(fontNames) } -// buildFontPaths 构建字体文件路径列表 +// buildFontPaths 构建字体文件路径列表(支持多种路径查找方式) func (fm *FontManager) buildFontPaths(fontNames []string) []string { var fontPaths []string - // 使用 pdf/fonts/ 目录 + // 方式1: 使用 pdf/fonts/ 目录(相对于当前文件) for _, fontName := range fontNames { fontPaths = append(fontPaths, filepath.Join(fm.baseDir, "fonts", fontName)) } - // 过滤出实际存在的字体文件 + // 方式2: 尝试相对于工作目录的路径(适用于编译后的二进制文件) + // 这是最常用的方式,适用于直接运行二进制文件的情况 + if workDir, err := os.Getwd(); err == nil { + fm.logger.Debug("工作目录", zap.String("work_dir", workDir)) + for _, fontName := range fontNames { + // 尝试多个可能的相对路径 + paths := []string{ + filepath.Join(workDir, "internal", "shared", "pdf", "fonts", fontName), + filepath.Join(workDir, "fonts", fontName), + filepath.Join(workDir, fontName), + // 如果工作目录是项目根目录的父目录 + filepath.Join(workDir, "tyapi-server", "internal", "shared", "pdf", "fonts", fontName), + } + fontPaths = append(fontPaths, paths...) + } + } else { + fm.logger.Warn("无法获取工作目录", zap.Error(err)) + } + + // 方式3: 尝试使用可执行文件所在目录 + // 适用于systemd服务或直接运行二进制文件的情况 + if execPath, err := os.Executable(); err == nil { + execDir := filepath.Dir(execPath) + // 解析符号链接,获取真实路径(Linux上很重要) + if realPath, err := filepath.EvalSymlinks(execPath); err == nil { + execDir = filepath.Dir(realPath) + } + fm.logger.Debug("可执行文件目录", + zap.String("exec_dir", execDir), + zap.String("exec_path", execPath)) + for _, fontName := range fontNames { + paths := []string{ + filepath.Join(execDir, "internal", "shared", "pdf", "fonts", fontName), + filepath.Join(execDir, "fonts", fontName), + filepath.Join(execDir, fontName), + // 如果可执行文件在bin目录,尝试上一级目录 + filepath.Join(filepath.Dir(execDir), "internal", "shared", "pdf", "fonts", fontName), + } + fontPaths = append(fontPaths, paths...) + } + } else { + fm.logger.Warn("无法获取可执行文件路径", zap.Error(err)) + } + + // 方式4: 尝试使用环境变量指定的路径(如果设置了) + if fontDir := os.Getenv("PDF_FONT_DIR"); fontDir != "" { + fm.logger.Info("使用环境变量指定的字体目录", zap.String("font_dir", fontDir)) + for _, fontName := range fontNames { + fontPaths = append(fontPaths, filepath.Join(fontDir, fontName)) + } + } + + // 方式5: 尝试常见的服务器部署路径(硬编码路径,作为后备方案) + // 支持多种Linux服务器部署场景 + commonServerPaths := []string{ + "/www/tyapi-server/internal/shared/pdf/fonts", // 用户提供的路径 + "/app/internal/shared/pdf/fonts", // Docker常见路径 + "/usr/local/tyapi-server/internal/shared/pdf/fonts", // 标准安装路径 + "/opt/tyapi-server/internal/shared/pdf/fonts", // 可选安装路径 + "/home/ubuntu/tyapi-server/internal/shared/pdf/fonts", // Ubuntu用户目录 + "/root/tyapi-server/internal/shared/pdf/fonts", // root用户目录 + "/var/www/tyapi-server/internal/shared/pdf/fonts", // Web服务器常见路径 + } + for _, basePath := range commonServerPaths { + for _, fontName := range fontNames { + fontPaths = append(fontPaths, filepath.Join(basePath, fontName)) + } + } + + // 记录所有尝试的路径(用于调试) + fm.logger.Debug("查找字体文件", + zap.Strings("font_names", fontNames), + zap.Int("total_paths", len(fontPaths)), + zap.Strings("paths", fontPaths)) + + // 过滤出实际存在的字体文件(并检查权限) var existingFonts []string for _, fontPath := range fontPaths { - if _, err := os.Stat(fontPath); err == nil { + // 检查文件是否存在 + fileInfo, err := os.Stat(fontPath) + if err == nil { + // 检查是否为常规文件(不是目录) + if !fileInfo.Mode().IsRegular() { + fm.logger.Debug("路径不是常规文件", + zap.String("font_path", fontPath), + zap.String("mode", fileInfo.Mode().String())) + continue + } + // 检查是否有读取权限(Linux上很重要) + if fileInfo.Mode().Perm()&0444 == 0 { + fm.logger.Warn("字体文件无读取权限", + zap.String("font_path", fontPath), + zap.String("mode", fileInfo.Mode().String())) + // 仍然尝试添加,因为可能通过其他方式有权限 + } existingFonts = append(existingFonts, fontPath) + fm.logger.Debug("找到字体文件", + zap.String("font_path", fontPath), + zap.Int64("size", fileInfo.Size())) + } else { + // 只在Debug级别记录,避免日志过多 + fm.logger.Debug("字体文件不存在", + zap.String("font_path", fontPath), + zap.Error(err)) } } + if len(existingFonts) == 0 { + fm.logger.Warn("未找到任何字体文件", + zap.Strings("font_names", fontNames), + zap.String("base_dir", fm.baseDir), + zap.Int("total_paths_checked", len(fontPaths))) + } else { + fm.logger.Info("找到字体文件", + zap.Int("count", len(existingFonts)), + zap.Strings("paths", existingFonts)) + } + return existingFonts }