package pdf import ( "context" "fmt" "math" "os" "path/filepath" "strings" "tyapi-server/internal/domains/product/entities" "github.com/jung-kurt/gofpdf/v2" qrcode "github.com/skip2/go-qrcode" "go.uber.org/zap" ) // PageBuilder 页面构建器 type PageBuilder struct { logger *zap.Logger fontManager *FontManager textProcessor *TextProcessor markdownProc *MarkdownProcessor markdownConverter *MarkdownConverter tableParser *TableParser tableRenderer *TableRenderer jsonProcessor *JSONProcessor logoPath string watermarkText string } // NewPageBuilder 创建页面构建器 func NewPageBuilder( logger *zap.Logger, fontManager *FontManager, textProcessor *TextProcessor, markdownProc *MarkdownProcessor, tableParser *TableParser, tableRenderer *TableRenderer, jsonProcessor *JSONProcessor, logoPath string, watermarkText string, ) *PageBuilder { markdownConverter := NewMarkdownConverter(textProcessor) return &PageBuilder{ logger: logger, fontManager: fontManager, textProcessor: textProcessor, markdownProc: markdownProc, markdownConverter: markdownConverter, tableParser: tableParser, tableRenderer: tableRenderer, jsonProcessor: jsonProcessor, logoPath: logoPath, watermarkText: watermarkText, } } // AddFirstPage 添加第一页(封面页 - 产品功能简述) func (pb *PageBuilder) AddFirstPage(pdf *gofpdf.Fpdf, product *entities.Product, doc *entities.ProductDocumentation, chineseFontAvailable bool) { pdf.AddPage() // 添加页眉(logo和文字) pb.addHeader(pdf, chineseFontAvailable) // 添加水印 pb.addWatermark(pdf, chineseFontAvailable) // 封面页布局 - 居中显示 pageWidth, _ := pdf.GetPageSize() // 标题区域(页面中上部) pdf.SetY(80) pdf.SetTextColor(0, 0, 0) pb.fontManager.SetFont(pdf, "B", 32) _, lineHt := pdf.GetFontSize() // 清理产品名称中的无效字符 cleanName := pb.textProcessor.CleanText(product.Name) pdf.CellFormat(0, lineHt*1.5, cleanName, "", 1, "C", false, 0, "") // 添加"接口文档"副标题 pdf.Ln(10) pb.fontManager.SetFont(pdf, "", 18) _, lineHt = pdf.GetFontSize() pdf.CellFormat(0, lineHt, "接口文档", "", 1, "C", false, 0, "") // 分隔线 pdf.Ln(20) pdf.SetLineWidth(0.5) pdf.Line(pageWidth*0.2, pdf.GetY(), pageWidth*0.8, pdf.GetY()) // 产品编码(居中) pdf.Ln(30) pdf.SetTextColor(0, 0, 0) pb.fontManager.SetFont(pdf, "", 14) _, lineHt = pdf.GetFontSize() pdf.CellFormat(0, lineHt, fmt.Sprintf("产品编码:%s", product.Code), "", 1, "C", false, 0, "") // 产品描述(居中显示,段落格式) if product.Description != "" { pdf.Ln(25) desc := pb.textProcessor.StripHTML(product.Description) desc = pb.textProcessor.CleanText(desc) pb.fontManager.SetFont(pdf, "", 14) _, lineHt = pdf.GetFontSize() // 居中对齐的MultiCell(通过计算宽度实现) descWidth := pageWidth * 0.7 descLines := pb.safeSplitText(pdf, desc, descWidth, chineseFontAvailable) currentX := (pageWidth - descWidth) / 2 for _, line := range descLines { pdf.SetX(currentX) pdf.CellFormat(descWidth, lineHt*1.5, line, "", 1, "C", false, 0, "") } } // 产品详情(如果存在) if product.Content != "" { pdf.Ln(20) content := pb.textProcessor.StripHTML(product.Content) content = pb.textProcessor.CleanText(content) pb.fontManager.SetFont(pdf, "", 12) _, lineHt = pdf.GetFontSize() contentWidth := pageWidth * 0.7 contentLines := pb.safeSplitText(pdf, content, contentWidth, chineseFontAvailable) currentX := (pageWidth - contentWidth) / 2 for _, line := range contentLines { pdf.SetX(currentX) pdf.CellFormat(contentWidth, lineHt*1.4, line, "", 1, "C", false, 0, "") } } // 价格信息(右下角,在产品详情之后) if !product.Price.IsZero() { // 获取产品详情结束后的Y坐标,稍微下移显示价格 contentEndY := pdf.GetY() pdf.SetY(contentEndY + 5) pdf.SetTextColor(0, 0, 0) pb.fontManager.SetFont(pdf, "", 14) _, priceLineHt := pdf.GetFontSize() priceText := fmt.Sprintf("价格:%s 元", product.Price.String()) textWidth := pdf.GetStringWidth(priceText) // 右对齐:从页面宽度减去文本宽度和右边距(15mm) pdf.SetX(pageWidth - textWidth - 15) pdf.CellFormat(textWidth, priceLineHt, priceText, "", 0, "R", false, 0, "") } } // AddDocumentationPages 添加接口文档页面 func (pb *PageBuilder) AddDocumentationPages(pdf *gofpdf.Fpdf, doc *entities.ProductDocumentation, chineseFontAvailable bool) { // 创建自定义的AddPage函数,确保每页都有水印 addPageWithWatermark := func() { pdf.AddPage() pb.addHeader(pdf, chineseFontAvailable) pb.addWatermark(pdf, chineseFontAvailable) // 每页都添加水印 } addPageWithWatermark() pdf.SetY(45) pb.fontManager.SetFont(pdf, "B", 18) _, lineHt := pdf.GetFontSize() pdf.CellFormat(0, lineHt, "接口文档", "", 1, "L", false, 0, "") // 请求URL pdf.Ln(8) pdf.SetTextColor(0, 0, 0) // 确保深黑色 pb.fontManager.SetFont(pdf, "B", 12) pdf.CellFormat(0, lineHt, "请求URL:", "", 1, "L", false, 0, "") // URL使用黑体字体(可能包含中文字符) // 先清理URL中的乱码 cleanURL := pb.textProcessor.CleanText(doc.RequestURL) pb.fontManager.SetFont(pdf, "", 10) // 使用黑体 pdf.SetTextColor(0, 0, 0) pdf.MultiCell(0, lineHt*1.2, cleanURL, "", "L", false) // 请求方法 pdf.Ln(5) pb.fontManager.SetFont(pdf, "B", 12) pdf.CellFormat(0, lineHt, fmt.Sprintf("请求方法:%s", doc.RequestMethod), "", 1, "L", false, 0, "") // 基本信息 if doc.BasicInfo != "" { pdf.Ln(8) pb.addSection(pdf, "基本信息", doc.BasicInfo, chineseFontAvailable) } // 请求参数 if doc.RequestParams != "" { pdf.Ln(8) // 显示标题 pdf.SetTextColor(0, 0, 0) pb.fontManager.SetFont(pdf, "B", 14) _, lineHt = pdf.GetFontSize() pdf.CellFormat(0, lineHt, "请求参数:", "", 1, "L", false, 0, "") // 使用新的数据库驱动方式处理请求参数 if err := pb.tableParser.ParseAndRenderTable(context.Background(), pdf, doc, "request_params"); err != nil { pb.logger.Warn("渲染请求参数表格失败,回退到文本显示", zap.Error(err)) // 如果表格渲染失败,显示为文本 text := pb.textProcessor.CleanText(doc.RequestParams) if strings.TrimSpace(text) != "" { pb.fontManager.SetFont(pdf, "", 10) pdf.MultiCell(0, lineHt*1.3, text, "", "L", false) } } // 生成JSON示例 if jsonExample := pb.jsonProcessor.GenerateJSONExample(doc.RequestParams, pb.tableParser); jsonExample != "" { pdf.Ln(5) pdf.SetTextColor(0, 0, 0) // 确保深黑色 pb.fontManager.SetFont(pdf, "B", 14) pdf.CellFormat(0, lineHt, "请求示例:", "", 1, "L", false, 0, "") // JSON中可能包含中文值,使用黑体字体 pb.fontManager.SetFont(pdf, "", 9) // 使用黑体显示JSON(支持中文) pdf.SetTextColor(0, 0, 0) pdf.MultiCell(0, lineHt*1.3, jsonExample, "", "L", false) } } // 响应示例 if doc.ResponseExample != "" { pdf.Ln(8) pdf.SetTextColor(0, 0, 0) // 确保深黑色 pb.fontManager.SetFont(pdf, "B", 14) _, lineHt = pdf.GetFontSize() pdf.CellFormat(0, lineHt, "响应示例:", "", 1, "L", false, 0, "") // 优先尝试提取和格式化JSON jsonContent := pb.jsonProcessor.ExtractJSON(doc.ResponseExample) if jsonContent != "" { // 格式化JSON formattedJSON, err := pb.jsonProcessor.FormatJSON(jsonContent) if err == nil { jsonContent = formattedJSON } pdf.SetTextColor(0, 0, 0) // 确保深黑色 pb.fontManager.SetFont(pdf, "", 9) // 使用等宽字体显示JSON(支持中文) pdf.MultiCell(0, lineHt*1.3, jsonContent, "", "L", false) } else { // 如果没有JSON,尝试使用表格方式处理 if err := pb.tableParser.ParseAndRenderTable(context.Background(), pdf, doc, "response_example"); err != nil { pb.logger.Warn("渲染响应示例表格失败,回退到文本显示", zap.Error(err)) // 如果表格渲染失败,显示为文本 text := pb.textProcessor.CleanText(doc.ResponseExample) if strings.TrimSpace(text) != "" { pb.fontManager.SetFont(pdf, "", 10) pdf.SetTextColor(0, 0, 0) // 确保深黑色 pdf.MultiCell(0, lineHt*1.3, text, "", "L", false) } } } } // 返回字段说明 if doc.ResponseFields != "" { pdf.Ln(8) // 显示标题 pdf.SetTextColor(0, 0, 0) pb.fontManager.SetFont(pdf, "B", 14) _, lineHt = pdf.GetFontSize() pdf.CellFormat(0, lineHt, "返回字段说明:", "", 1, "L", false, 0, "") // 使用新的数据库驱动方式处理返回字段(支持多个表格,带标题) if err := pb.tableParser.ParseAndRenderTablesWithTitles(context.Background(), pdf, doc, "response_fields"); err != nil { pb.logger.Warn("渲染返回字段表格失败,回退到文本显示", zap.Error(err), zap.String("content_preview", pb.getContentPreview(doc.ResponseFields, 200))) // 如果表格渲染失败,显示为文本 text := pb.textProcessor.CleanText(doc.ResponseFields) if strings.TrimSpace(text) != "" { pb.fontManager.SetFont(pdf, "", 10) pdf.MultiCell(0, lineHt*1.3, text, "", "L", false) } else { pb.logger.Warn("返回字段内容为空或只有空白字符") } } } else { pb.logger.Debug("返回字段内容为空,跳过渲染") } // 错误代码 if doc.ErrorCodes != "" { pdf.Ln(8) // 显示标题 pdf.SetTextColor(0, 0, 0) pb.fontManager.SetFont(pdf, "B", 14) _, lineHt = pdf.GetFontSize() pdf.CellFormat(0, lineHt, "错误代码:", "", 1, "L", false, 0, "") // 使用新的数据库驱动方式处理错误代码 if err := pb.tableParser.ParseAndRenderTable(context.Background(), pdf, doc, "error_codes"); err != nil { pb.logger.Warn("渲染错误代码表格失败,回退到文本显示", zap.Error(err)) // 如果表格渲染失败,显示为文本 text := pb.textProcessor.CleanText(doc.ErrorCodes) if strings.TrimSpace(text) != "" { pb.fontManager.SetFont(pdf, "", 10) pdf.MultiCell(0, lineHt*1.3, text, "", "L", false) } } } // 添加说明文字和二维码 pb.addAdditionalInfo(pdf, doc, chineseFontAvailable) } // AddDocumentationPagesWithoutAdditionalInfo 添加接口文档页面(不包含二维码和说明) // 用于组合包场景,在所有文档渲染完成后统一添加二维码和说明 func (pb *PageBuilder) AddDocumentationPagesWithoutAdditionalInfo(pdf *gofpdf.Fpdf, doc *entities.ProductDocumentation, chineseFontAvailable bool) { // 创建自定义的AddPage函数,确保每页都有水印 addPageWithWatermark := func() { pdf.AddPage() pb.addHeader(pdf, chineseFontAvailable) pb.addWatermark(pdf, chineseFontAvailable) // 每页都添加水印 } addPageWithWatermark() pdf.SetY(45) pb.fontManager.SetFont(pdf, "B", 18) _, lineHt := pdf.GetFontSize() pdf.CellFormat(0, lineHt, "接口文档", "", 1, "L", false, 0, "") // 请求URL pdf.Ln(8) pdf.SetTextColor(0, 0, 0) // 确保深黑色 pb.fontManager.SetFont(pdf, "B", 12) pdf.CellFormat(0, lineHt, "请求URL:", "", 1, "L", false, 0, "") // URL使用黑体字体(可能包含中文字符) // 先清理URL中的乱码 cleanURL := pb.textProcessor.CleanText(doc.RequestURL) pb.fontManager.SetFont(pdf, "", 10) // 使用黑体 pdf.SetTextColor(0, 0, 0) pdf.MultiCell(0, lineHt*1.2, cleanURL, "", "L", false) // 请求方法 pdf.Ln(5) pb.fontManager.SetFont(pdf, "B", 12) pdf.CellFormat(0, lineHt, fmt.Sprintf("请求方法:%s", doc.RequestMethod), "", 1, "L", false, 0, "") // 基本信息 if doc.BasicInfo != "" { pdf.Ln(8) pb.addSection(pdf, "基本信息", doc.BasicInfo, chineseFontAvailable) } // 请求参数 if doc.RequestParams != "" { pdf.Ln(8) // 显示标题 pdf.SetTextColor(0, 0, 0) pb.fontManager.SetFont(pdf, "B", 14) _, lineHt = pdf.GetFontSize() pdf.CellFormat(0, lineHt, "请求参数:", "", 1, "L", false, 0, "") // 使用新的数据库驱动方式处理请求参数 if err := pb.tableParser.ParseAndRenderTable(context.Background(), pdf, doc, "request_params"); err != nil { pb.logger.Warn("渲染请求参数表格失败,回退到文本显示", zap.Error(err)) // 如果表格渲染失败,显示为文本 text := pb.textProcessor.CleanText(doc.RequestParams) if strings.TrimSpace(text) != "" { pb.fontManager.SetFont(pdf, "", 10) pdf.MultiCell(0, lineHt*1.3, text, "", "L", false) } } // 生成JSON示例 if jsonExample := pb.jsonProcessor.GenerateJSONExample(doc.RequestParams, pb.tableParser); jsonExample != "" { pdf.Ln(5) pdf.SetTextColor(0, 0, 0) // 确保深黑色 pb.fontManager.SetFont(pdf, "B", 14) pdf.CellFormat(0, lineHt, "请求示例:", "", 1, "L", false, 0, "") // JSON中可能包含中文值,使用黑体字体 pb.fontManager.SetFont(pdf, "", 9) // 使用黑体显示JSON(支持中文) pdf.SetTextColor(0, 0, 0) pdf.MultiCell(0, lineHt*1.3, jsonExample, "", "L", false) } } // 响应示例 if doc.ResponseExample != "" { pdf.Ln(8) pdf.SetTextColor(0, 0, 0) // 确保深黑色 pb.fontManager.SetFont(pdf, "B", 14) _, lineHt = pdf.GetFontSize() pdf.CellFormat(0, lineHt, "响应示例:", "", 1, "L", false, 0, "") // 优先尝试提取和格式化JSON jsonContent := pb.jsonProcessor.ExtractJSON(doc.ResponseExample) if jsonContent != "" { // 格式化JSON formattedJSON, err := pb.jsonProcessor.FormatJSON(jsonContent) if err == nil { jsonContent = formattedJSON } pdf.SetTextColor(0, 0, 0) // 确保深黑色 pb.fontManager.SetFont(pdf, "", 9) // 使用等宽字体显示JSON(支持中文) pdf.MultiCell(0, lineHt*1.3, jsonContent, "", "L", false) } else { // 如果没有JSON,尝试使用表格方式处理 if err := pb.tableParser.ParseAndRenderTable(context.Background(), pdf, doc, "response_example"); err != nil { pb.logger.Warn("渲染响应示例表格失败,回退到文本显示", zap.Error(err)) // 如果表格渲染失败,显示为文本 text := pb.textProcessor.CleanText(doc.ResponseExample) if strings.TrimSpace(text) != "" { pb.fontManager.SetFont(pdf, "", 10) pdf.SetTextColor(0, 0, 0) // 确保深黑色 pdf.MultiCell(0, lineHt*1.3, text, "", "L", false) } } } } // 返回字段说明 if doc.ResponseFields != "" { pdf.Ln(8) // 显示标题 pdf.SetTextColor(0, 0, 0) pb.fontManager.SetFont(pdf, "B", 14) _, lineHt = pdf.GetFontSize() pdf.CellFormat(0, lineHt, "返回字段说明:", "", 1, "L", false, 0, "") // 使用新的数据库驱动方式处理返回字段(支持多个表格,带标题) if err := pb.tableParser.ParseAndRenderTablesWithTitles(context.Background(), pdf, doc, "response_fields"); err != nil { pb.logger.Warn("渲染返回字段表格失败,回退到文本显示", zap.Error(err), zap.String("content_preview", pb.getContentPreview(doc.ResponseFields, 200))) // 如果表格渲染失败,显示为文本 text := pb.textProcessor.CleanText(doc.ResponseFields) if strings.TrimSpace(text) != "" { pb.fontManager.SetFont(pdf, "", 10) pdf.MultiCell(0, lineHt*1.3, text, "", "L", false) } else { pb.logger.Warn("返回字段内容为空或只有空白字符") } } } else { pb.logger.Debug("返回字段内容为空,跳过渲染") } // 错误代码 if doc.ErrorCodes != "" { pdf.Ln(8) // 显示标题 pdf.SetTextColor(0, 0, 0) pb.fontManager.SetFont(pdf, "B", 14) _, lineHt = pdf.GetFontSize() pdf.CellFormat(0, lineHt, "错误代码:", "", 1, "L", false, 0, "") // 使用新的数据库驱动方式处理错误代码 if err := pb.tableParser.ParseAndRenderTable(context.Background(), pdf, doc, "error_codes"); err != nil { pb.logger.Warn("渲染错误代码表格失败,回退到文本显示", zap.Error(err)) // 如果表格渲染失败,显示为文本 text := pb.textProcessor.CleanText(doc.ErrorCodes) if strings.TrimSpace(text) != "" { pb.fontManager.SetFont(pdf, "", 10) pdf.MultiCell(0, lineHt*1.3, text, "", "L", false) } } } // 注意:这里不添加二维码和说明,由调用方统一添加 } // AddSubProductDocumentationPages 添加子产品的接口文档页面(用于组合包) func (pb *PageBuilder) AddSubProductDocumentationPages(pdf *gofpdf.Fpdf, subProduct *entities.Product, doc *entities.ProductDocumentation, chineseFontAvailable bool, isLastSubProduct bool) { // 创建自定义的AddPage函数,确保每页都有水印 addPageWithWatermark := func() { pdf.AddPage() pb.addHeader(pdf, chineseFontAvailable) pb.addWatermark(pdf, chineseFontAvailable) // 每页都添加水印 } addPageWithWatermark() pdf.SetY(45) pb.fontManager.SetFont(pdf, "B", 18) _, lineHt := pdf.GetFontSize() // 显示子产品标题 subProductTitle := fmt.Sprintf("子产品接口文档:%s", subProduct.Name) if subProduct.Code != "" { subProductTitle = fmt.Sprintf("子产品接口文档:%s (%s)", subProduct.Name, subProduct.Code) } pdf.CellFormat(0, lineHt, subProductTitle, "", 1, "L", false, 0, "") // 请求URL pdf.Ln(8) pdf.SetTextColor(0, 0, 0) // 确保深黑色 pb.fontManager.SetFont(pdf, "B", 12) pdf.CellFormat(0, lineHt, "请求URL:", "", 1, "L", false, 0, "") // URL使用黑体字体(可能包含中文字符) // 先清理URL中的乱码 cleanURL := pb.textProcessor.CleanText(doc.RequestURL) pb.fontManager.SetFont(pdf, "", 10) // 使用黑体 pdf.SetTextColor(0, 0, 0) pdf.MultiCell(0, lineHt*1.2, cleanURL, "", "L", false) // 请求方法 pdf.Ln(5) pb.fontManager.SetFont(pdf, "B", 12) pdf.CellFormat(0, lineHt, fmt.Sprintf("请求方法:%s", doc.RequestMethod), "", 1, "L", false, 0, "") // 基本信息 if doc.BasicInfo != "" { pdf.Ln(8) pb.addSection(pdf, "基本信息", doc.BasicInfo, chineseFontAvailable) } // 请求参数 if doc.RequestParams != "" { pdf.Ln(8) // 显示标题 pdf.SetTextColor(0, 0, 0) pb.fontManager.SetFont(pdf, "B", 14) _, lineHt = pdf.GetFontSize() pdf.CellFormat(0, lineHt, "请求参数:", "", 1, "L", false, 0, "") // 使用新的数据库驱动方式处理请求参数 if err := pb.tableParser.ParseAndRenderTable(context.Background(), pdf, doc, "request_params"); err != nil { pb.logger.Warn("渲染请求参数表格失败,回退到文本显示", zap.Error(err)) // 如果表格渲染失败,显示为文本 text := pb.textProcessor.CleanText(doc.RequestParams) if strings.TrimSpace(text) != "" { pb.fontManager.SetFont(pdf, "", 10) pdf.MultiCell(0, lineHt*1.3, text, "", "L", false) } } // 生成JSON示例 if jsonExample := pb.jsonProcessor.GenerateJSONExample(doc.RequestParams, pb.tableParser); jsonExample != "" { pdf.Ln(5) pdf.SetTextColor(0, 0, 0) // 确保深黑色 pb.fontManager.SetFont(pdf, "B", 14) pdf.CellFormat(0, lineHt, "请求示例:", "", 1, "L", false, 0, "") // JSON中可能包含中文值,使用黑体字体 pb.fontManager.SetFont(pdf, "", 9) // 使用黑体显示JSON(支持中文) pdf.SetTextColor(0, 0, 0) pdf.MultiCell(0, lineHt*1.3, jsonExample, "", "L", false) } } // 响应示例 if doc.ResponseExample != "" { pdf.Ln(8) pdf.SetTextColor(0, 0, 0) // 确保深黑色 pb.fontManager.SetFont(pdf, "B", 14) _, lineHt = pdf.GetFontSize() pdf.CellFormat(0, lineHt, "响应示例:", "", 1, "L", false, 0, "") // 优先尝试提取和格式化JSON jsonContent := pb.jsonProcessor.ExtractJSON(doc.ResponseExample) if jsonContent != "" { // 格式化JSON formattedJSON, err := pb.jsonProcessor.FormatJSON(jsonContent) if err == nil { jsonContent = formattedJSON } pdf.SetTextColor(0, 0, 0) // 确保深黑色 pb.fontManager.SetFont(pdf, "", 9) // 使用等宽字体显示JSON(支持中文) pdf.MultiCell(0, lineHt*1.3, jsonContent, "", "L", false) } else { // 如果没有JSON,尝试使用表格方式处理 if err := pb.tableParser.ParseAndRenderTable(context.Background(), pdf, doc, "response_example"); err != nil { pb.logger.Warn("渲染响应示例表格失败,回退到文本显示", zap.Error(err)) // 如果表格渲染失败,显示为文本 text := pb.textProcessor.CleanText(doc.ResponseExample) if strings.TrimSpace(text) != "" { pb.fontManager.SetFont(pdf, "", 10) pdf.SetTextColor(0, 0, 0) // 确保深黑色 pdf.MultiCell(0, lineHt*1.3, text, "", "L", false) } } } } // 返回字段说明 if doc.ResponseFields != "" { pdf.Ln(8) // 显示标题 pdf.SetTextColor(0, 0, 0) pb.fontManager.SetFont(pdf, "B", 14) _, lineHt = pdf.GetFontSize() pdf.CellFormat(0, lineHt, "返回字段说明:", "", 1, "L", false, 0, "") // 使用新的数据库驱动方式处理返回字段(支持多个表格,带标题) if err := pb.tableParser.ParseAndRenderTablesWithTitles(context.Background(), pdf, doc, "response_fields"); err != nil { pb.logger.Warn("渲染返回字段表格失败,回退到文本显示", zap.Error(err), zap.String("content_preview", pb.getContentPreview(doc.ResponseFields, 200))) // 如果表格渲染失败,显示为文本 text := pb.textProcessor.CleanText(doc.ResponseFields) if strings.TrimSpace(text) != "" { pb.fontManager.SetFont(pdf, "", 10) pdf.MultiCell(0, lineHt*1.3, text, "", "L", false) } else { pb.logger.Warn("返回字段内容为空或只有空白字符") } } } else { pb.logger.Debug("返回字段内容为空,跳过渲染") } // 错误代码 if doc.ErrorCodes != "" { pdf.Ln(8) // 显示标题 pdf.SetTextColor(0, 0, 0) pb.fontManager.SetFont(pdf, "B", 14) _, lineHt = pdf.GetFontSize() pdf.CellFormat(0, lineHt, "错误代码:", "", 1, "L", false, 0, "") // 使用新的数据库驱动方式处理错误代码 if err := pb.tableParser.ParseAndRenderTable(context.Background(), pdf, doc, "error_codes"); err != nil { pb.logger.Warn("渲染错误代码表格失败,回退到文本显示", zap.Error(err)) // 如果表格渲染失败,显示为文本 text := pb.textProcessor.CleanText(doc.ErrorCodes) if strings.TrimSpace(text) != "" { pb.fontManager.SetFont(pdf, "", 10) pdf.MultiCell(0, lineHt*1.3, text, "", "L", false) } } } // 注意:这里不添加二维码和说明,由调用方统一添加 } // addSection 添加章节 func (pb *PageBuilder) addSection(pdf *gofpdf.Fpdf, title, content string, chineseFontAvailable bool) { _, lineHt := pdf.GetFontSize() pb.fontManager.SetFont(pdf, "B", 14) pdf.CellFormat(0, lineHt, title+":", "", 1, "L", false, 0, "") // 第一步:预处理和转换(标准化markdown格式) content = pb.markdownConverter.PreprocessContent(content) // 第二步:将内容格式化为标准的markdown表格格式(如果还不是) content = pb.markdownProc.FormatContentAsMarkdownTable(content) // 先尝试提取JSON(如果是代码块格式) if jsonContent := pb.jsonProcessor.ExtractJSON(content); jsonContent != "" { // 格式化JSON formattedJSON, err := pb.jsonProcessor.FormatJSON(jsonContent) if err == nil { jsonContent = formattedJSON } pdf.SetTextColor(0, 0, 0) pb.fontManager.SetFont(pdf, "", 9) // 使用黑体 pdf.MultiCell(0, lineHt*1.2, jsonContent, "", "L", false) } else { // 按#号标题分割内容,每个标题下的内容单独处理 sections := pb.markdownProc.SplitByMarkdownHeaders(content) if len(sections) > 0 { // 如果有多个章节,逐个处理 for i, section := range sections { if i > 0 { pdf.Ln(5) // 章节之间的间距 } // 如果有标题,先显示标题 if section.Title != "" { titleLevel := section.Level fontSize := 14.0 - float64(titleLevel-2)*2 // ## 是14, ### 是12, #### 是10 if fontSize < 10 { fontSize = 10 } pb.fontManager.SetFont(pdf, "B", fontSize) pdf.SetTextColor(0, 0, 0) // 清理标题中的#号 cleanTitle := strings.TrimSpace(strings.TrimLeft(section.Title, "#")) pdf.CellFormat(0, lineHt*1.2, cleanTitle, "", 1, "L", false, 0, "") pdf.Ln(3) } // 处理该章节的内容(可能是表格或文本) pb.processSectionContent(pdf, section.Content, chineseFontAvailable, lineHt) } } else { // 如果没有标题分割,直接处理整个内容 pb.processSectionContent(pdf, content, chineseFontAvailable, lineHt) } } } // processRequestParams 处理请求参数:直接解析所有表格,确保表格能够正确渲染 func (pb *PageBuilder) processRequestParams(pdf *gofpdf.Fpdf, content string, chineseFontAvailable bool, lineHt float64) { // 第一步:预处理和转换(标准化markdown格式) content = pb.markdownConverter.PreprocessContent(content) // 第二步:将数据格式化为标准的markdown表格格式 processedContent := pb.markdownProc.FormatContentAsMarkdownTable(content) // 解析并显示所有表格(不按标题分组) // 将内容按表格分割,找到所有表格块 allTables := pb.tableParser.ExtractAllTables(processedContent) if len(allTables) > 0 { // 有表格,逐个渲染 for i, tableBlock := range allTables { if i > 0 { pdf.Ln(5) // 表格之间的间距 } // 渲染表格前的说明文字(包括标题) if tableBlock.BeforeText != "" { beforeText := tableBlock.BeforeText // 处理标题和文本 pb.renderTextWithTitles(pdf, beforeText, chineseFontAvailable, lineHt) pdf.Ln(3) } // 渲染表格 if len(tableBlock.TableData) > 0 && pb.tableParser.IsValidTable(tableBlock.TableData) { pb.tableRenderer.RenderTable(pdf, tableBlock.TableData) } // 渲染表格后的说明文字 if tableBlock.AfterText != "" { afterText := pb.textProcessor.StripHTML(tableBlock.AfterText) afterText = pb.textProcessor.CleanText(afterText) if strings.TrimSpace(afterText) != "" { pdf.Ln(3) pdf.SetTextColor(0, 0, 0) pb.fontManager.SetFont(pdf, "", 10) pdf.MultiCell(0, lineHt*1.3, afterText, "", "L", false) } } } } else { // 没有表格,显示为文本 text := pb.textProcessor.StripHTML(processedContent) text = pb.textProcessor.CleanText(text) if strings.TrimSpace(text) != "" { pdf.SetTextColor(0, 0, 0) pb.fontManager.SetFont(pdf, "", 10) pdf.MultiCell(0, lineHt*1.3, text, "", "L", false) } } } // processResponseExample 处理响应示例:不按markdown标题分级,直接解析所有表格,但保留标题显示 func (pb *PageBuilder) processResponseExample(pdf *gofpdf.Fpdf, content string, chineseFontAvailable bool, lineHt float64) { // 第一步:预处理和转换(标准化markdown格式) content = pb.markdownConverter.PreprocessContent(content) // 第二步:将数据格式化为标准的markdown表格格式 processedContent := pb.markdownProc.FormatContentAsMarkdownTable(content) // 尝试提取JSON内容(如果存在代码块) jsonContent := pb.jsonProcessor.ExtractJSON(processedContent) if jsonContent != "" { pdf.SetTextColor(0, 0, 0) formattedJSON, err := pb.jsonProcessor.FormatJSON(jsonContent) if err == nil { jsonContent = formattedJSON } pb.fontManager.SetFont(pdf, "", 9) // 使用黑体 pdf.MultiCell(0, lineHt*1.3, jsonContent, "", "L", false) pdf.Ln(5) } // 解析并显示所有表格(不按标题分组) // 将内容按表格分割,找到所有表格块 allTables := pb.tableParser.ExtractAllTables(processedContent) if len(allTables) > 0 { // 有表格,逐个渲染 for i, tableBlock := range allTables { if i > 0 { pdf.Ln(5) // 表格之间的间距 } // 渲染表格前的说明文字(包括标题) if tableBlock.BeforeText != "" { beforeText := tableBlock.BeforeText // 处理标题和文本 pb.renderTextWithTitles(pdf, beforeText, chineseFontAvailable, lineHt) pdf.Ln(3) } // 渲染表格 if len(tableBlock.TableData) > 0 && pb.tableParser.IsValidTable(tableBlock.TableData) { pb.tableRenderer.RenderTable(pdf, tableBlock.TableData) } // 渲染表格后的说明文字 if tableBlock.AfterText != "" { afterText := pb.textProcessor.StripHTML(tableBlock.AfterText) afterText = pb.textProcessor.CleanText(afterText) if strings.TrimSpace(afterText) != "" { pdf.Ln(3) pdf.SetTextColor(0, 0, 0) pb.fontManager.SetFont(pdf, "", 10) pdf.MultiCell(0, lineHt*1.3, afterText, "", "L", false) } } } } else { // 没有表格,显示为文本 text := pb.textProcessor.StripHTML(processedContent) text = pb.textProcessor.CleanText(text) if strings.TrimSpace(text) != "" { pdf.SetTextColor(0, 0, 0) pb.fontManager.SetFont(pdf, "", 10) pdf.MultiCell(0, lineHt*1.3, text, "", "L", false) } } } // processSectionContent 处理单个章节的内容(解析表格或显示文本) func (pb *PageBuilder) processSectionContent(pdf *gofpdf.Fpdf, content string, chineseFontAvailable bool, lineHt float64) { // 尝试解析markdown表格 tableData := pb.tableParser.ParseMarkdownTable(content) // 检查内容是否包含表格标记(|符号) hasTableMarkers := strings.Contains(content, "|") // 如果解析出了表格数据(即使验证失败),或者内容包含表格标记,都尝试渲染表格 // 放宽条件:支持只有表头的表格(单行表格) if len(tableData) >= 1 && hasTableMarkers { // 如果表格有效,或者至少有表头,都尝试渲染 if pb.tableParser.IsValidTable(tableData) { // 如果是有效的表格,先检查表格前后是否有说明文字 // 提取表格前后的文本(用于显示说明) lines := strings.Split(content, "\n") var beforeTable []string var afterTable []string inTable := false tableStartLine := -1 tableEndLine := -1 // 找到表格的起始和结束行 usePipeDelimiter := false for _, line := range lines { if strings.Contains(strings.TrimSpace(line), "|") { usePipeDelimiter = true break } } for i, line := range lines { trimmedLine := strings.TrimSpace(line) if usePipeDelimiter && strings.Contains(trimmedLine, "|") { if !inTable { tableStartLine = i inTable = true } tableEndLine = i } else if inTable && usePipeDelimiter && !strings.Contains(trimmedLine, "|") { // 表格可能结束了 if strings.HasPrefix(trimmedLine, "```") { tableEndLine = i - 1 break } } } // 提取表格前的文本 if tableStartLine > 0 { beforeTable = lines[0:tableStartLine] } // 提取表格后的文本 if tableEndLine >= 0 && tableEndLine < len(lines)-1 { afterTable = lines[tableEndLine+1:] } // 显示表格前的说明文字 if len(beforeTable) > 0 { beforeText := strings.Join(beforeTable, "\n") beforeText = pb.textProcessor.StripHTML(beforeText) beforeText = pb.textProcessor.CleanText(beforeText) if strings.TrimSpace(beforeText) != "" { pdf.SetTextColor(0, 0, 0) pb.fontManager.SetFont(pdf, "", 10) pdf.MultiCell(0, lineHt*1.3, beforeText, "", "L", false) pdf.Ln(3) } } // 渲染表格 pb.tableRenderer.RenderTable(pdf, tableData) // 显示表格后的说明文字 if len(afterTable) > 0 { afterText := strings.Join(afterTable, "\n") afterText = pb.textProcessor.StripHTML(afterText) afterText = pb.textProcessor.CleanText(afterText) if strings.TrimSpace(afterText) != "" { pdf.Ln(3) pdf.SetTextColor(0, 0, 0) pb.fontManager.SetFont(pdf, "", 10) pdf.MultiCell(0, lineHt*1.3, afterText, "", "L", false) } } } } else { // 如果不是有效表格,显示为文本(完整显示markdown内容) pdf.SetTextColor(0, 0, 0) pb.fontManager.SetFont(pdf, "", 10) text := pb.textProcessor.StripHTML(content) text = pb.textProcessor.CleanText(text) // 清理无效字符,保留中文 // 如果文本不为空,显示它 if strings.TrimSpace(text) != "" { pdf.MultiCell(0, lineHt*1.3, text, "", "L", false) } } } // renderTextWithTitles 渲染包含markdown标题的文本 func (pb *PageBuilder) renderTextWithTitles(pdf *gofpdf.Fpdf, text string, chineseFontAvailable bool, lineHt float64) { lines := strings.Split(text, "\n") for _, line := range lines { trimmedLine := strings.TrimSpace(line) // 检查是否是标题行 if strings.HasPrefix(trimmedLine, "#") { // 计算标题级别 level := 0 for _, r := range trimmedLine { if r == '#' { level++ } else { break } } // 提取标题文本(移除#号) titleText := strings.TrimSpace(trimmedLine[level:]) if titleText == "" { continue } // 根据级别设置字体大小 fontSize := 14.0 - float64(level-2)*2 if fontSize < 10 { fontSize = 10 } if fontSize > 16 { fontSize = 16 } // 渲染标题 pdf.SetTextColor(0, 0, 0) pb.fontManager.SetFont(pdf, "B", fontSize) _, titleLineHt := pdf.GetFontSize() pdf.CellFormat(0, titleLineHt*1.2, titleText, "", 1, "L", false, 0, "") pdf.Ln(2) } else if strings.TrimSpace(line) != "" { // 普通文本行(只去除HTML标签,保留markdown格式) cleanText := pb.textProcessor.StripHTML(line) cleanText = pb.textProcessor.CleanTextPreservingMarkdown(cleanText) if strings.TrimSpace(cleanText) != "" { pdf.SetTextColor(0, 0, 0) pb.fontManager.SetFont(pdf, "", 10) pdf.MultiCell(0, lineHt*1.3, cleanText, "", "L", false) } } else { // 空行,添加间距 pdf.Ln(2) } } } // addHeader 添加页眉(logo和文字) func (pb *PageBuilder) addHeader(pdf *gofpdf.Fpdf, chineseFontAvailable bool) { pdf.SetY(5) // 绘制logo(如果存在) if pb.logoPath != "" { if _, err := os.Stat(pb.logoPath); err == nil { pdf.ImageOptions(pb.logoPath, 15, 5, 15, 15, false, gofpdf.ImageOptions{}, 0, "") } else { pb.logger.Warn("logo文件不存在", zap.String("path", pb.logoPath)) } } // 绘制"天远数据"文字(使用中文字体如果可用) pdf.SetXY(33, 8) pb.fontManager.SetFont(pdf, "B", 14) pdf.CellFormat(0, 10, "天远数据", "", 0, "L", false, 0, "") // 绘制下横线(优化位置,左边距是15mm) pdf.Line(15, 22, 75, 22) } // addWatermark 添加水印(从左边开始向上倾斜45度,考虑可用区域) func (pb *PageBuilder) addWatermark(pdf *gofpdf.Fpdf, chineseFontAvailable bool) { // 如果中文字体不可用,跳过水印(避免显示乱码) if !chineseFontAvailable { return } // 保存当前图形状态 pdf.TransformBegin() defer pdf.TransformEnd() // 获取页面尺寸和边距 _, pageHeight := pdf.GetPageSize() leftMargin, topMargin, _, bottomMargin := pdf.GetMargins() // 计算实际可用区域高度 usableHeight := pageHeight - topMargin - bottomMargin // 设置水印样式(使用水印字体,非黑体) fontSize := 45.0 pb.fontManager.SetWatermarkFont(pdf, "", fontSize) // 设置灰色和透明度(加深水印,使其更明显) pdf.SetTextColor(180, 180, 180) // 深一点的灰色 pdf.SetAlpha(0.25, "Normal") // 增加透明度,让水印更明显 // 计算文字宽度 textWidth := pdf.GetStringWidth(pb.watermarkText) if textWidth == 0 { // 如果无法获取宽度(字体未注册),使用估算值(中文字符大约每个 fontSize/3 mm) textWidth = float64(len([]rune(pb.watermarkText))) * fontSize / 3.0 } // 从左边开始,计算起始位置 // 起始X:左边距 // 起始Y:考虑水印文字长度和旋转后需要的空间 startX := leftMargin startY := topMargin + textWidth*0.5 // 为旋转留出空间 // 移动到起始位置 pdf.TransformTranslate(startX, startY) // 向上倾斜45度(顺时针旋转45度,即-45度,或逆时针315度) pdf.TransformRotate(-45, 0, 0) // 检查文字是否会超出可用区域(旋转后的对角线长度) rotatedDiagonal := math.Sqrt(textWidth*textWidth + fontSize*fontSize) if rotatedDiagonal > usableHeight*0.8 { // 如果太大,缩小字体 fontSize = fontSize * usableHeight * 0.8 / rotatedDiagonal pb.fontManager.SetWatermarkFont(pdf, "", fontSize) textWidth = pdf.GetStringWidth(pb.watermarkText) if textWidth == 0 { textWidth = float64(len([]rune(pb.watermarkText))) * fontSize / 3.0 } } // 从左边开始绘制水印文字 pdf.SetXY(0, 0) pdf.CellFormat(textWidth, fontSize, pb.watermarkText, "", 0, "L", false, 0, "") // 恢复透明度和颜色 pdf.SetAlpha(1.0, "Normal") pdf.SetTextColor(0, 0, 0) // 恢复为黑色 } // getContentPreview 获取内容预览(用于日志记录) func (pb *PageBuilder) getContentPreview(content string, maxLen int) string { content = strings.TrimSpace(content) if len(content) <= maxLen { return content } return content[:maxLen] + "..." } // safeSplitText 安全地分割文本,避免在没有中文字体时调用SplitText导致panic func (pb *PageBuilder) safeSplitText(pdf *gofpdf.Fpdf, text string, width float64, chineseFontAvailable bool) []string { // 检查文本是否包含中文字符 hasChinese := false for _, r := range text { if (r >= 0x4E00 && r <= 0x9FFF) || // 中文字符范围 (r >= 0x3400 && r <= 0x4DBF) || // 扩展A (r >= 0x20000 && r <= 0x2A6DF) || // 扩展B (r >= 0x3000 && r <= 0x303F) || // CJK符号和标点 (r >= 0xFF00 && r <= 0xFFEF) { // 全角字符 hasChinese = true break } } // 如果文本包含中文但中文字体不可用,直接使用手动分割方法 // 如果中文字体可用且文本不包含中文,可以尝试使用SplitText // 即使中文字体可用,如果文本包含中文,也要小心处理(字体可能未正确加载) if chineseFontAvailable && !hasChinese { // 对于纯英文/ASCII文本,可以安全使用SplitText var lines []string func() { defer func() { if r := recover(); r != nil { pb.logger.Warn("SplitText发生panic,使用备用方法", zap.Any("error", r)) } }() lines = pdf.SplitText(text, width) }() if len(lines) > 0 { return lines } } // 使用手动分割方法(适用于中文文本或SplitText失败的情况) // 估算字符宽度:中文字符约6mm,英文字符约3mm(基于14号字体) fontSize, _ := pdf.GetFontSize() chineseCharWidth := fontSize * 0.43 // 中文字符宽度(mm) englishCharWidth := fontSize * 0.22 // 英文字符宽度(mm) var lines []string var currentLine strings.Builder currentWidth := 0.0 runes := []rune(text) for _, r := range runes { // 计算字符宽度 var charWidth float64 if (r >= 0x4E00 && r <= 0x9FFF) || // 中文字符范围 (r >= 0x3400 && r <= 0x4DBF) || // 扩展A (r >= 0x20000 && r <= 0x2A6DF) || // 扩展B (r >= 0x3000 && r <= 0x303F) || // CJK符号和标点 (r >= 0xFF00 && r <= 0xFFEF) { // 全角字符 charWidth = chineseCharWidth } else { charWidth = englishCharWidth } // 检查是否需要换行 if currentWidth+charWidth > width && currentLine.Len() > 0 { // 保存当前行 lines = append(lines, currentLine.String()) currentLine.Reset() currentWidth = 0.0 } // 处理换行符 if r == '\n' { if currentLine.Len() > 0 { lines = append(lines, currentLine.String()) currentLine.Reset() currentWidth = 0.0 } else { // 空行 lines = append(lines, "") } continue } // 添加字符到当前行 currentLine.WriteRune(r) currentWidth += charWidth } // 添加最后一行 if currentLine.Len() > 0 { lines = append(lines, currentLine.String()) } // 如果没有分割出任何行,至少返回一行 if len(lines) == 0 { lines = []string{text} } return lines } // AddAdditionalInfo 添加说明文字和二维码(公开方法) func (pb *PageBuilder) AddAdditionalInfo(pdf *gofpdf.Fpdf, doc *entities.ProductDocumentation, chineseFontAvailable bool) { pb.addAdditionalInfo(pdf, doc, chineseFontAvailable) } // addAdditionalInfo 添加说明文字和二维码(私有方法) func (pb *PageBuilder) addAdditionalInfo(pdf *gofpdf.Fpdf, doc *entities.ProductDocumentation, chineseFontAvailable bool) { // 检查是否需要换页 pageWidth, pageHeight := pdf.GetPageSize() _, _, _, bottomMargin := pdf.GetMargins() currentY := pdf.GetY() remainingHeight := pageHeight - currentY - bottomMargin // 如果剩余空间不足,添加新页 if remainingHeight < 100 { pdf.AddPage() pb.addHeader(pdf, chineseFontAvailable) pb.addWatermark(pdf, chineseFontAvailable) pdf.SetY(45) } // 添加分隔线 pdf.Ln(10) pdf.SetLineWidth(0.5) pdf.SetDrawColor(200, 200, 200) pdf.Line(15, pdf.GetY(), pageWidth-15, pdf.GetY()) pdf.SetDrawColor(0, 0, 0) // 添加说明文字标题 pdf.Ln(15) pdf.SetTextColor(0, 0, 0) pb.fontManager.SetFont(pdf, "B", 16) _, lineHt := pdf.GetFontSize() pdf.CellFormat(0, lineHt, "接入流程说明", "", 1, "L", false, 0, "") // 读取说明文本文件 explanationText := pb.readExplanationText() if explanationText != "" { pb.logger.Debug("开始渲染说明文本", zap.Int("text_length", len(explanationText)), zap.Int("line_count", len(strings.Split(explanationText, "\n"))), ) pdf.Ln(5) pb.fontManager.SetFont(pdf, "", 11) _, lineHt = pdf.GetFontSize() // 处理说明文本,按行分割并显示 lines := strings.Split(explanationText, "\n") renderedLines := 0 for i, line := range lines { // 保留原始行用于日志 originalLine := line line = strings.TrimSpace(line) // 处理空行 if line == "" { pdf.Ln(3) continue } // 清理文本(保留中文字符和标点) cleanLine := pb.textProcessor.CleanText(line) // 检查清理后的文本是否为空 if strings.TrimSpace(cleanLine) == "" { pb.logger.Warn("文本行清理后为空,跳过渲染", zap.Int("line_number", i+1), zap.String("original_line", originalLine), ) continue } // 渲染文本行 // 使用MultiCell自动换行,支持长文本 pdf.MultiCell(0, lineHt*1.4, cleanLine, "", "L", false) renderedLines++ } pb.logger.Info("说明文本渲染完成", zap.Int("total_lines", len(lines)), zap.Int("rendered_lines", renderedLines), ) } else { pb.logger.Warn("说明文本为空,跳过渲染") } // 添加二维码生成方法和使用方法说明 pb.addQRCodeSection(pdf, doc, chineseFontAvailable) } // readExplanationText 读取说明文本文件 func (pb *PageBuilder) readExplanationText() string { resourcesPDFDir := GetResourcesPDFDir() if resourcesPDFDir == "" { pb.logger.Error("无法获取resources/pdf目录路径") return "" } textFilePath := filepath.Join(resourcesPDFDir, "后勤服务.txt") // 记录尝试读取的文件路径 pb.logger.Debug("尝试读取说明文本文件", zap.String("path", textFilePath)) // 检查文件是否存在 fileInfo, err := os.Stat(textFilePath) if err != nil { if os.IsNotExist(err) { pb.logger.Warn("说明文本文件不存在", zap.String("path", textFilePath), zap.String("resources_dir", resourcesPDFDir), ) } else { pb.logger.Error("检查说明文本文件时出错", zap.String("path", textFilePath), zap.Error(err), ) } return "" } // 检查文件大小 if fileInfo.Size() == 0 { pb.logger.Warn("说明文本文件为空", zap.String("path", textFilePath)) return "" } // 尝试读取文件(使用os.ReadFile替代已废弃的ioutil.ReadFile) content, err := os.ReadFile(textFilePath) if err != nil { pb.logger.Error("读取说明文本文件失败", zap.String("path", textFilePath), zap.Error(err), ) return "" } // 转换为字符串 text := string(content) // 检查内容是否为空(去除空白字符后) trimmedText := strings.TrimSpace(text) if trimmedText == "" { pb.logger.Warn("说明文本文件内容为空(只有空白字符)", zap.String("path", textFilePath), zap.Int("file_size", len(content)), ) return "" } // 记录读取成功的信息 pb.logger.Info("成功读取说明文本文件", zap.String("path", textFilePath), zap.Int64("file_size", fileInfo.Size()), zap.Int("content_length", len(content)), zap.Int("text_length", len(text)), zap.Int("line_count", len(strings.Split(text, "\n"))), ) // 返回文本内容 return text } // addQRCodeSection 添加二维码生成方法和使用方法说明 func (pb *PageBuilder) addQRCodeSection(pdf *gofpdf.Fpdf, doc *entities.ProductDocumentation, chineseFontAvailable bool) { _, pageHeight := pdf.GetPageSize() _, _, _, bottomMargin := pdf.GetMargins() currentY := pdf.GetY() // 检查是否需要换页(为二维码预留空间) if pageHeight-currentY-bottomMargin < 120 { pdf.AddPage() pb.addHeader(pdf, chineseFontAvailable) pb.addWatermark(pdf, chineseFontAvailable) pdf.SetY(45) } // 添加二维码标题 pdf.Ln(15) pdf.SetTextColor(0, 0, 0) pb.fontManager.SetFont(pdf, "B", 16) _, lineHt := pdf.GetFontSize() pdf.CellFormat(0, lineHt, "天远api官网二维码", "", 1, "L", false, 0, "") // 先生成并添加二维码图片(确保二维码能够正常显示) pb.addQRCodeImage(pdf, "https://tianyuanapi.com/", chineseFontAvailable) // 二维码说明文字(简化版,放在二维码之后) pdf.Ln(10) pb.fontManager.SetFont(pdf, "", 11) _, lineHt = pdf.GetFontSize() qrCodeExplanation := "使用手机扫描上方二维码可直接跳转到天远API官网(https://tianyuanapi.com/),获取更多接口文档和资源。\n\n" + "二维码使用方法:\n" + "1. 使用手机相机或二维码扫描应用扫描二维码\n" + "2. 扫描后会自动跳转到天远API官网首页\n" + "3. 在官网可以查看完整的产品列表、接口文档和使用说明" // 处理说明文本,按行分割并显示 lines := strings.Split(qrCodeExplanation, "\n") for _, line := range lines { line = strings.TrimSpace(line) if line == "" { pdf.Ln(2) continue } // 普通文本行 cleanLine := pb.textProcessor.CleanText(line) if strings.TrimSpace(cleanLine) != "" { pdf.MultiCell(0, lineHt*1.3, cleanLine, "", "L", false) } } } // addQRCodeImage 生成并添加二维码图片到PDF func (pb *PageBuilder) addQRCodeImage(pdf *gofpdf.Fpdf, content string, chineseFontAvailable bool) { // 检查是否需要换页 pageWidth, pageHeight := pdf.GetPageSize() _, _, _, bottomMargin := pdf.GetMargins() currentY := pdf.GetY() // 二维码大小(40mm) qrSize := 40.0 if pageHeight-currentY-bottomMargin < qrSize+20 { pdf.AddPage() pb.addHeader(pdf, chineseFontAvailable) pb.addWatermark(pdf, chineseFontAvailable) pdf.SetY(45) } // 生成二维码 qr, err := qrcode.New(content, qrcode.Medium) if err != nil { pb.logger.Warn("生成二维码失败", zap.Error(err)) return } // 将二维码转换为PNG字节 qrBytes, err := qr.PNG(256) if err != nil { pb.logger.Warn("转换二维码为PNG失败", zap.Error(err)) return } // 创建临时文件保存二维码(使用os.CreateTemp替代已废弃的ioutil.TempFile) tmpFile, err := os.CreateTemp("", "qrcode_*.png") if err != nil { pb.logger.Warn("创建临时文件失败", zap.Error(err)) return } defer os.Remove(tmpFile.Name()) // 清理临时文件 // 写入二维码数据 if _, err := tmpFile.Write(qrBytes); err != nil { pb.logger.Warn("写入二维码数据失败", zap.Error(err)) tmpFile.Close() return } tmpFile.Close() // 添加二维码说明 pdf.Ln(10) pb.fontManager.SetFont(pdf, "", 10) _, lineHt := pdf.GetFontSize() pdf.CellFormat(0, lineHt, "官网二维码:", "", 1, "L", false, 0, "") // 计算二维码位置(居中) qrX := (pageWidth - qrSize) / 2 // 添加二维码图片 pdf.Ln(5) pdf.ImageOptions(tmpFile.Name(), qrX, pdf.GetY(), qrSize, qrSize, false, gofpdf.ImageOptions{}, 0, "") // 添加二维码下方的说明文字 pdf.SetY(pdf.GetY() + qrSize + 5) pb.fontManager.SetFont(pdf, "", 9) _, lineHt = pdf.GetFontSize() qrNote := "使用手机扫描上方二维码可访问官网获取更多详情" noteWidth := pdf.GetStringWidth(qrNote) noteX := (pageWidth - noteWidth) / 2 pdf.SetX(noteX) pdf.CellFormat(noteWidth, lineHt, qrNote, "", 1, "C", false, 0, "") }