Files
tyapi-server/internal/shared/pdf/pdf_generator.go

2198 lines
65 KiB
Go
Raw Normal View History

2025-12-03 12:03:42 +08:00
package pdf
import (
"context"
"encoding/json"
"fmt"
"html"
"math"
"os"
"path/filepath"
"regexp"
"runtime"
"strings"
"github.com/jung-kurt/gofpdf/v2"
"github.com/shopspring/decimal"
"go.uber.org/zap"
"tyapi-server/internal/domains/product/entities"
)
// PDFGenerator PDF生成器
type PDFGenerator struct {
logger *zap.Logger
chineseFont string
logoPath string
watermarkText string
}
// NewPDFGenerator 创建PDF生成器
func NewPDFGenerator(logger *zap.Logger) *PDFGenerator {
gen := &PDFGenerator{
logger: logger,
watermarkText: "海南海宇大数据有限公司",
}
// 尝试注册中文字体
chineseFont := gen.registerChineseFont()
gen.chineseFont = chineseFont
// 查找logo文件
gen.findLogo()
return gen
}
// registerChineseFont 注册中文字体
// gofpdf v2 默认支持 UTF-8但需要添加支持中文的字体文件
func (g *PDFGenerator) registerChineseFont() string {
// 返回字体名称标识实际在generatePDF中注册
return "ChineseFont"
}
// getChineseFontPaths 获取中文字体路径(支持跨平台)
func (g *PDFGenerator) getChineseFontPaths() []string {
var fontPaths []string
// Windows系统
if runtime.GOOS == "windows" {
fontPaths = []string{
`C:\Windows\Fonts\simhei.ttf`, // 黑体(优先,常用)
`C:\Windows\Fonts\simsun.ttf`, // 宋体如果存在单独的TTF文件
`C:\Windows\Fonts\msyh.ttf`, // 微软雅黑如果存在单独的TTF文件
`C:\Windows\Fonts\simkai.ttf`, // 楷体
}
} else if runtime.GOOS == "linux" {
// Linux系统常见字体路径
fontPaths = []string{
"/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",
"/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc",
"/usr/share/fonts/truetype/arphic/uming.ttc",
"/usr/share/fonts/truetype/arphic/ukai.ttc",
"/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf",
}
} else if runtime.GOOS == "darwin" {
// macOS系统字体路径
fontPaths = []string{
"/System/Library/Fonts/PingFang.ttc",
"/System/Library/Fonts/STHeiti Light.ttc",
"/System/Library/Fonts/STSong.ttc",
"/Library/Fonts/Microsoft/SimHei.ttf",
}
}
// 过滤出实际存在的字体文件
var existingFonts []string
for _, fontPath := range fontPaths {
if _, err := os.Stat(fontPath); err == nil {
existingFonts = append(existingFonts, fontPath)
}
}
return existingFonts
}
// findLogo 查找logo文件
func (g *PDFGenerator) findLogo() {
// 获取当前文件所在目录
_, filename, _, _ := runtime.Caller(0)
baseDir := filepath.Dir(filename)
logoPaths := []string{
filepath.Join(baseDir, "天远数据.png"), // 相对当前文件
"天远数据.png", // 当前目录
filepath.Join(baseDir, "..", "天远数据.png"), // 上一级目录
filepath.Join(baseDir, "..", "..", "天远数据.png"), // 上两级目录
}
for _, logoPath := range logoPaths {
if _, err := os.Stat(logoPath); err == nil {
absPath, err := filepath.Abs(logoPath)
if err == nil {
g.logoPath = absPath
g.logger.Info("找到logo文件", zap.String("logo_path", absPath))
return
}
}
}
g.logger.Warn("未找到logo文件", zap.Strings("尝试的路径", logoPaths))
}
// GenerateProductPDF 为产品生成PDF文档接受响应类型内部转换
func (g *PDFGenerator) 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 *PDFGenerator) GenerateProductPDFFromEntity(ctx context.Context, product *entities.Product, doc *entities.ProductDocumentation) ([]byte, error) {
return g.generatePDF(product, doc)
}
// generatePDF 内部PDF生成方法
// 现在使用重构后的模块化组件
func (g *PDFGenerator) generatePDF(product *entities.Product, doc *entities.ProductDocumentation) (result []byte, err error) {
defer func() {
if r := recover(); r != nil {
g.logger.Error("PDF生成过程中发生panic",
zap.String("product_id", product.ID),
zap.String("product_name", product.Name),
zap.Any("panic_value", r),
)
// 将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
}
}()
g.logger.Info("开始生成PDF使用重构后的模块化组件",
zap.String("product_id", product.ID),
zap.String("product_name", product.Name),
zap.Bool("has_doc", doc != nil),
)
// 使用重构后的生成器
refactoredGen := NewPDFGeneratorRefactored(g.logger)
return refactoredGen.GenerateProductPDFFromEntity(context.Background(), product, doc)
}
// addFirstPage 添加第一页(封面页 - 产品功能简述)
func (g *PDFGenerator) addFirstPage(pdf *gofpdf.Fpdf, product *entities.Product, doc *entities.ProductDocumentation, chineseFontAvailable bool) {
pdf.AddPage()
// 添加页眉logo和文字
g.addHeader(pdf, chineseFontAvailable)
// 添加水印
g.addWatermark(pdf, chineseFontAvailable)
// 封面页布局 - 居中显示
pageWidth, pageHeight := pdf.GetPageSize()
// 标题区域(页面中上部)
pdf.SetY(80)
pdf.SetTextColor(0, 0, 0)
if chineseFontAvailable {
pdf.SetFont("ChineseFont", "B", 32)
} else {
pdf.SetFont("Arial", "B", 32)
}
_, lineHt := pdf.GetFontSize()
// 清理产品名称中的无效字符
cleanName := g.cleanText(product.Name)
pdf.CellFormat(0, lineHt*1.5, cleanName, "", 1, "C", false, 0, "")
// 添加"接口文档"副标题
pdf.Ln(10)
if chineseFontAvailable {
pdf.SetFont("ChineseFont", "", 18)
} else {
pdf.SetFont("Arial", "", 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)
if chineseFontAvailable {
pdf.SetFont("ChineseFont", "", 14)
} else {
pdf.SetFont("Arial", "", 14)
}
_, lineHt = pdf.GetFontSize()
pdf.CellFormat(0, lineHt, fmt.Sprintf("产品编码:%s", product.Code), "", 1, "C", false, 0, "")
// 产品描述(居中显示,段落格式)
if product.Description != "" {
pdf.Ln(25)
desc := g.stripHTML(product.Description)
desc = g.cleanText(desc)
if chineseFontAvailable {
pdf.SetFont("ChineseFont", "", 14)
} else {
pdf.SetFont("Arial", "", 14)
}
_, lineHt = pdf.GetFontSize()
// 居中对齐的MultiCell通过计算宽度实现
descWidth := pageWidth * 0.7
descLines := pdf.SplitText(desc, descWidth)
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 := g.stripHTML(product.Content)
content = g.cleanText(content)
if chineseFontAvailable {
pdf.SetFont("ChineseFont", "", 12)
} else {
pdf.SetFont("Arial", "", 12)
}
_, lineHt = pdf.GetFontSize()
contentWidth := pageWidth * 0.7
contentLines := pdf.SplitText(content, contentWidth)
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() {
pdf.SetY(pageHeight - 60)
pdf.SetTextColor(0, 0, 0)
if chineseFontAvailable {
pdf.SetFont("ChineseFont", "", 12)
} else {
pdf.SetFont("Arial", "", 12)
}
_, lineHt = pdf.GetFontSize()
pdf.CellFormat(0, lineHt, fmt.Sprintf("价格:%s 元", product.Price.String()), "", 1, "C", false, 0, "")
}
}
// addDocumentationPages 添加接口文档页面
func (g *PDFGenerator) addDocumentationPages(pdf *gofpdf.Fpdf, doc *entities.ProductDocumentation, chineseFontAvailable bool) {
// 创建自定义的AddPage函数确保每页都有水印
addPageWithWatermark := func() {
pdf.AddPage()
g.addHeader(pdf, chineseFontAvailable)
g.addWatermark(pdf, chineseFontAvailable) // 每页都添加水印
}
addPageWithWatermark()
pdf.SetY(45)
if chineseFontAvailable {
pdf.SetFont("ChineseFont", "B", 18)
} else {
pdf.SetFont("Arial", "B", 18)
}
_, lineHt := pdf.GetFontSize()
pdf.CellFormat(0, lineHt, "接口文档", "", 1, "L", false, 0, "")
// 请求URL
pdf.Ln(8)
pdf.SetTextColor(0, 0, 0) // 确保深黑色
if chineseFontAvailable {
pdf.SetFont("ChineseFont", "B", 12)
} else {
pdf.SetFont("Arial", "B", 12)
}
pdf.CellFormat(0, lineHt, "请求URL", "", 1, "L", false, 0, "")
// URL也需要使用中文字体可能包含中文字符但用Courier字体保持等宽效果
// 先清理URL中的乱码
cleanURL := g.cleanText(doc.RequestURL)
if chineseFontAvailable {
pdf.SetFont("ChineseFont", "", 10) // 使用中文字体
} else {
pdf.SetFont("Courier", "", 10)
}
pdf.SetTextColor(0, 0, 0)
pdf.MultiCell(0, lineHt*1.2, cleanURL, "", "L", false)
// 请求方法
pdf.Ln(5)
if chineseFontAvailable {
pdf.SetFont("ChineseFont", "B", 12)
} else {
pdf.SetFont("Arial", "B", 12)
}
pdf.CellFormat(0, lineHt, fmt.Sprintf("请求方法:%s", doc.RequestMethod), "", 1, "L", false, 0, "")
// 基本信息
if doc.BasicInfo != "" {
pdf.Ln(8)
g.addSection(pdf, "基本信息", doc.BasicInfo, chineseFontAvailable)
}
// 请求参数
if doc.RequestParams != "" {
pdf.Ln(8)
// 显示标题
pdf.SetTextColor(0, 0, 0)
if chineseFontAvailable {
pdf.SetFont("ChineseFont", "B", 14)
} else {
pdf.SetFont("Arial", "B", 14)
}
_, lineHt = pdf.GetFontSize()
pdf.CellFormat(0, lineHt, "请求参数:", "", 1, "L", false, 0, "")
// 处理请求参数:直接解析所有表格,确保表格能够正确渲染
g.processRequestParams(pdf, doc.RequestParams, chineseFontAvailable, lineHt)
// 生成JSON示例
if jsonExample := g.generateJSONExample(doc.RequestParams); jsonExample != "" {
pdf.Ln(5)
pdf.SetTextColor(0, 0, 0) // 确保深黑色
if chineseFontAvailable {
pdf.SetFont("ChineseFont", "B", 14)
} else {
pdf.SetFont("Arial", "B", 14)
}
pdf.CellFormat(0, lineHt, "请求示例:", "", 1, "L", false, 0, "")
// JSON中可能包含中文值必须使用中文字体
if chineseFontAvailable {
pdf.SetFont("ChineseFont", "", 9) // 使用中文字体显示JSON支持中文
} else {
pdf.SetFont("Courier", "", 9)
}
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) // 确保深黑色
if chineseFontAvailable {
pdf.SetFont("ChineseFont", "B", 14)
} else {
pdf.SetFont("Arial", "B", 14)
}
_, lineHt = pdf.GetFontSize()
pdf.CellFormat(0, lineHt, "响应示例:", "", 1, "L", false, 0, "")
// 处理响应示例不按markdown标题分级直接解析所有表格
// 确保所有数据字段都显示在表格中
g.processResponseExample(pdf, doc.ResponseExample, chineseFontAvailable, lineHt)
}
// 返回字段
if doc.ResponseFields != "" {
pdf.Ln(8)
// 先将数据格式化为标准的markdown表格格式
formattedFields := g.formatContentAsMarkdownTable(doc.ResponseFields)
g.addSection(pdf, "返回字段", formattedFields, chineseFontAvailable)
}
// 错误代码
if doc.ErrorCodes != "" {
pdf.Ln(8)
g.addSection(pdf, "错误代码", doc.ErrorCodes, chineseFontAvailable)
}
}
// addSection 添加章节
func (g *PDFGenerator) addSection(pdf *gofpdf.Fpdf, title, content string, chineseFontAvailable bool) {
_, lineHt := pdf.GetFontSize()
if chineseFontAvailable {
pdf.SetFont("ChineseFont", "B", 14)
} else {
pdf.SetFont("Arial", "B", 14)
}
pdf.CellFormat(0, lineHt, title+"", "", 1, "L", false, 0, "")
// 先将内容格式化为标准的markdown表格格式如果还不是
content = g.formatContentAsMarkdownTable(content)
// 先尝试提取JSON如果是代码块格式
if jsonContent := g.extractJSON(content); jsonContent != "" {
// 格式化JSON
formattedJSON, err := g.formatJSON(jsonContent)
if err == nil {
jsonContent = formattedJSON
}
pdf.SetTextColor(0, 0, 0)
if chineseFontAvailable {
pdf.SetFont("ChineseFont", "", 9)
} else {
pdf.SetFont("Courier", "", 9)
}
pdf.MultiCell(0, lineHt*1.2, jsonContent, "", "L", false)
} else {
// 按#号标题分割内容,每个标题下的内容单独处理
sections := g.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
}
if chineseFontAvailable {
pdf.SetFont("ChineseFont", "B", fontSize)
} else {
pdf.SetFont("Arial", "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)
}
// 处理该章节的内容(可能是表格或文本)
g.processSectionContent(pdf, section.Content, chineseFontAvailable, lineHt)
}
} else {
// 如果没有标题分割,直接处理整个内容
g.processSectionContent(pdf, content, chineseFontAvailable, lineHt)
}
}
}
// MarkdownSection 已在 markdown_processor.go 中定义
// splitByMarkdownHeaders 按markdown标题分割内容
func (g *PDFGenerator) splitByMarkdownHeaders(content string) []MarkdownSection {
lines := strings.Split(content, "\n")
var sections []MarkdownSection
var currentSection MarkdownSection
var currentContent []string
// 标题正则:匹配 #, ##, ###, #### 等
headerRegex := regexp.MustCompile(`^(#{1,6})\s+(.+)$`)
for _, line := range lines {
trimmedLine := strings.TrimSpace(line)
// 检查是否是标题行
if matches := headerRegex.FindStringSubmatch(trimmedLine); matches != nil {
// 如果之前有内容,先保存之前的章节
if currentSection.Title != "" || len(currentContent) > 0 {
if currentSection.Title != "" {
currentSection.Content = strings.Join(currentContent, "\n")
sections = append(sections, currentSection)
}
}
// 开始新章节
level := len(matches[1]) // #号的数量
currentSection = MarkdownSection{
Title: trimmedLine,
Level: level,
Content: "",
}
currentContent = []string{}
} else {
// 普通内容行,添加到当前章节
currentContent = append(currentContent, line)
}
}
// 保存最后一个章节
if currentSection.Title != "" || len(currentContent) > 0 {
if currentSection.Title != "" {
currentSection.Content = strings.Join(currentContent, "\n")
sections = append(sections, currentSection)
} else if len(currentContent) > 0 {
// 如果没有标题,但开头有内容,作为第一个章节
sections = append(sections, MarkdownSection{
Title: "",
Level: 0,
Content: strings.Join(currentContent, "\n"),
})
}
}
return sections
}
// formatContentAsMarkdownTable 将数据库中的数据格式化为标准的markdown表格格式
// 注意数据库中的数据通常不是JSON格式除了代码块中的示例主要是文本或markdown格式
func (g *PDFGenerator) formatContentAsMarkdownTable(content string) string {
if strings.TrimSpace(content) == "" {
return content
}
// 如果内容已经是markdown表格格式包含|符号),直接返回
if strings.Contains(content, "|") {
// 检查是否已经是有效的markdown表格
lines := strings.Split(content, "\n")
hasTableFormat := false
for _, line := range lines {
trimmed := strings.TrimSpace(line)
// 跳过代码块中的内容
if strings.HasPrefix(trimmed, "```") {
continue
}
if strings.Contains(trimmed, "|") && !strings.HasPrefix(trimmed, "#") {
hasTableFormat = true
break
}
}
if hasTableFormat {
return content
}
}
// 提取代码块(保留代码块不变)
codeBlocks := g.extractCodeBlocks(content)
// 移除代码块,只处理非代码块部分
contentWithoutCodeBlocks := g.removeCodeBlocks(content)
// 如果移除代码块后内容为空,说明只有代码块,直接返回原始内容
if strings.TrimSpace(contentWithoutCodeBlocks) == "" {
return content
}
// 尝试解析非代码块部分为JSON数组仅当内容看起来像JSON时
trimmedContent := strings.TrimSpace(contentWithoutCodeBlocks)
// 检查是否看起来像JSON以[或{开头)
if strings.HasPrefix(trimmedContent, "[") || strings.HasPrefix(trimmedContent, "{") {
// 尝试解析为JSON数组
var requestParams []map[string]interface{}
if err := json.Unmarshal([]byte(trimmedContent), &requestParams); err == nil && len(requestParams) > 0 {
// 成功解析为JSON数组转换为markdown表格
tableContent := g.jsonArrayToMarkdownTable(requestParams)
// 如果有代码块,在表格后添加代码块
if len(codeBlocks) > 0 {
return tableContent + "\n\n" + strings.Join(codeBlocks, "\n\n")
}
return tableContent
}
// 尝试解析为单个JSON对象
var singleObj map[string]interface{}
if err := json.Unmarshal([]byte(trimmedContent), &singleObj); err == nil {
// 检查是否是包含数组字段的对象
if params, ok := singleObj["params"].([]interface{}); ok {
// 转换为map数组
paramMaps := make([]map[string]interface{}, 0, len(params))
for _, p := range params {
if pm, ok := p.(map[string]interface{}); ok {
paramMaps = append(paramMaps, pm)
}
}
if len(paramMaps) > 0 {
tableContent := g.jsonArrayToMarkdownTable(paramMaps)
// 如果有代码块,在表格后添加代码块
if len(codeBlocks) > 0 {
return tableContent + "\n\n" + strings.Join(codeBlocks, "\n\n")
}
return tableContent
}
}
if fields, ok := singleObj["fields"].([]interface{}); ok {
// 转换为map数组
fieldMaps := make([]map[string]interface{}, 0, len(fields))
for _, f := range fields {
if fm, ok := f.(map[string]interface{}); ok {
fieldMaps = append(fieldMaps, fm)
}
}
if len(fieldMaps) > 0 {
tableContent := g.jsonArrayToMarkdownTable(fieldMaps)
// 如果有代码块,在表格后添加代码块
if len(codeBlocks) > 0 {
return tableContent + "\n\n" + strings.Join(codeBlocks, "\n\n")
}
return tableContent
}
}
}
}
// 如果无法解析为JSON返回原始内容保留代码块
return content
}
// extractCodeBlocks 提取内容中的所有代码块
func (g *PDFGenerator) extractCodeBlocks(content string) []string {
var codeBlocks []string
lines := strings.Split(content, "\n")
inCodeBlock := false
var currentBlock []string
for _, line := range lines {
trimmed := strings.TrimSpace(line)
// 检查是否是代码块开始
if strings.HasPrefix(trimmed, "```") {
if inCodeBlock {
// 代码块结束
currentBlock = append(currentBlock, line)
codeBlocks = append(codeBlocks, strings.Join(currentBlock, "\n"))
currentBlock = []string{}
inCodeBlock = false
} else {
// 代码块开始
inCodeBlock = true
currentBlock = []string{line}
}
} else if inCodeBlock {
// 在代码块中
currentBlock = append(currentBlock, line)
}
}
// 如果代码块没有正确关闭,也添加进去
if inCodeBlock && len(currentBlock) > 0 {
codeBlocks = append(codeBlocks, strings.Join(currentBlock, "\n"))
}
return codeBlocks
}
// removeCodeBlocks 移除内容中的所有代码块
func (g *PDFGenerator) removeCodeBlocks(content string) string {
lines := strings.Split(content, "\n")
var result []string
inCodeBlock := false
for _, line := range lines {
trimmed := strings.TrimSpace(line)
// 检查是否是代码块开始或结束
if strings.HasPrefix(trimmed, "```") {
inCodeBlock = !inCodeBlock
continue // 跳过代码块的标记行
}
// 如果不在代码块中,保留这一行
if !inCodeBlock {
result = append(result, line)
}
}
return strings.Join(result, "\n")
}
// jsonArrayToMarkdownTable 将JSON数组转换为标准的markdown表格
func (g *PDFGenerator) jsonArrayToMarkdownTable(data []map[string]interface{}) string {
if len(data) == 0 {
return ""
}
var result strings.Builder
// 收集所有可能的列名(保持原始顺序)
// 使用map记录是否已添加使用slice保持顺序
columnSet := make(map[string]bool)
columns := make([]string, 0)
// 遍历所有数据行,按第一次出现的顺序收集列名
for _, row := range data {
for key := range row {
if !columnSet[key] {
columns = append(columns, key)
columnSet[key] = true
}
}
}
if len(columns) == 0 {
return ""
}
// 构建表头(直接使用原始列名,不做映射)
result.WriteString("|")
for _, col := range columns {
result.WriteString(" ")
result.WriteString(col) // 直接使用原始列名
result.WriteString(" |")
}
result.WriteString("\n")
// 构建分隔行
result.WriteString("|")
for range columns {
result.WriteString(" --- |")
}
result.WriteString("\n")
// 构建数据行
for _, row := range data {
result.WriteString("|")
for _, col := range columns {
result.WriteString(" ")
value := g.formatCellValue(row[col])
result.WriteString(value)
result.WriteString(" |")
}
result.WriteString("\n")
}
return result.String()
}
// formatColumnName 格式化列名(直接返回原始列名,不做映射)
// 保持数据库原始数据的列名,不进行转换
func (g *PDFGenerator) formatColumnName(name string) string {
// 直接返回原始列名,保持数据库数据的原始格式
return name
}
// formatCellValue 格式化单元格值
func (g *PDFGenerator) formatCellValue(value interface{}) string {
if value == nil {
return ""
}
switch v := value.(type) {
case string:
// 清理字符串,移除换行符和多余空格
v = strings.ReplaceAll(v, "\n", " ")
v = strings.ReplaceAll(v, "\r", " ")
v = strings.TrimSpace(v)
// 转义markdown特殊字符
v = strings.ReplaceAll(v, "|", "\\|")
return v
case bool:
if v {
return "是"
}
return "否"
case float64:
// 如果是整数,不显示小数点
if v == float64(int64(v)) {
return fmt.Sprintf("%.0f", v)
}
return fmt.Sprintf("%g", v)
case int, int8, int16, int32, int64:
return fmt.Sprintf("%d", v)
case uint, uint8, uint16, uint32, uint64:
return fmt.Sprintf("%d", v)
default:
// 对于其他类型,转换为字符串
str := fmt.Sprintf("%v", v)
str = strings.ReplaceAll(str, "\n", " ")
str = strings.ReplaceAll(str, "\r", " ")
str = strings.ReplaceAll(str, "|", "\\|")
return strings.TrimSpace(str)
}
}
// processRequestParams 处理请求参数:直接解析所有表格,确保表格能够正确渲染
func (g *PDFGenerator) processRequestParams(pdf *gofpdf.Fpdf, content string, chineseFontAvailable bool, lineHt float64) {
// 先将数据格式化为标准的markdown表格格式
processedContent := g.formatContentAsMarkdownTable(content)
// 解析并显示所有表格(不按标题分组)
// 将内容按表格分割,找到所有表格块
allTables := g.extractAllTables(processedContent)
if len(allTables) > 0 {
// 有表格,逐个渲染
for i, tableBlock := range allTables {
if i > 0 {
pdf.Ln(5) // 表格之间的间距
}
// 渲染表格前的说明文字(包括标题)
if tableBlock.BeforeText != "" {
beforeText := tableBlock.BeforeText
// 处理标题和文本
g.renderTextWithTitles(pdf, beforeText, chineseFontAvailable, lineHt)
pdf.Ln(3)
}
// 渲染表格
if len(tableBlock.TableData) > 0 && g.isValidTable(tableBlock.TableData) {
g.addTable(pdf, tableBlock.TableData, chineseFontAvailable)
}
// 渲染表格后的说明文字
if tableBlock.AfterText != "" {
afterText := g.stripHTML(tableBlock.AfterText)
afterText = g.cleanText(afterText)
if strings.TrimSpace(afterText) != "" {
pdf.Ln(3)
pdf.SetTextColor(0, 0, 0)
if chineseFontAvailable {
pdf.SetFont("ChineseFont", "", 10)
} else {
pdf.SetFont("Arial", "", 10)
}
pdf.MultiCell(0, lineHt*1.3, afterText, "", "L", false)
}
}
}
} else {
// 没有表格,显示为文本
text := g.stripHTML(processedContent)
text = g.cleanText(text)
if strings.TrimSpace(text) != "" {
pdf.SetTextColor(0, 0, 0)
if chineseFontAvailable {
pdf.SetFont("ChineseFont", "", 10)
} else {
pdf.SetFont("Arial", "", 10)
}
pdf.MultiCell(0, lineHt*1.3, text, "", "L", false)
}
}
}
// processResponseExample 处理响应示例不按markdown标题分级直接解析所有表格但保留标题显示
func (g *PDFGenerator) processResponseExample(pdf *gofpdf.Fpdf, content string, chineseFontAvailable bool, lineHt float64) {
// 先将数据格式化为标准的markdown表格格式
processedContent := g.formatContentAsMarkdownTable(content)
// 尝试提取JSON内容如果存在代码块
jsonContent := g.extractJSON(processedContent)
if jsonContent != "" {
pdf.SetTextColor(0, 0, 0)
formattedJSON, err := g.formatJSON(jsonContent)
if err == nil {
jsonContent = formattedJSON
}
if chineseFontAvailable {
pdf.SetFont("ChineseFont", "", 9)
} else {
pdf.SetFont("Courier", "", 9)
}
pdf.MultiCell(0, lineHt*1.3, jsonContent, "", "L", false)
pdf.Ln(5)
}
// 解析并显示所有表格(不按标题分组)
// 将内容按表格分割,找到所有表格块
allTables := g.extractAllTables(processedContent)
if len(allTables) > 0 {
// 有表格,逐个渲染
for i, tableBlock := range allTables {
if i > 0 {
pdf.Ln(5) // 表格之间的间距
}
// 渲染表格前的说明文字(包括标题)
if tableBlock.BeforeText != "" {
beforeText := tableBlock.BeforeText
// 处理标题和文本
g.renderTextWithTitles(pdf, beforeText, chineseFontAvailable, lineHt)
pdf.Ln(3)
}
// 渲染表格
if len(tableBlock.TableData) > 0 && g.isValidTable(tableBlock.TableData) {
g.addTable(pdf, tableBlock.TableData, chineseFontAvailable)
}
// 渲染表格后的说明文字
if tableBlock.AfterText != "" {
afterText := g.stripHTML(tableBlock.AfterText)
afterText = g.cleanText(afterText)
if strings.TrimSpace(afterText) != "" {
pdf.Ln(3)
pdf.SetTextColor(0, 0, 0)
if chineseFontAvailable {
pdf.SetFont("ChineseFont", "", 10)
} else {
pdf.SetFont("Arial", "", 10)
}
pdf.MultiCell(0, lineHt*1.3, afterText, "", "L", false)
}
}
}
} else {
// 没有表格,显示为文本
text := g.stripHTML(processedContent)
text = g.cleanText(text)
if strings.TrimSpace(text) != "" {
pdf.SetTextColor(0, 0, 0)
if chineseFontAvailable {
pdf.SetFont("ChineseFont", "", 10)
} else {
pdf.SetFont("Arial", "", 10)
}
pdf.MultiCell(0, lineHt*1.3, text, "", "L", false)
}
}
}
// TableBlock 已在 table_parser.go 中定义
// extractAllTables 从内容中提取所有表格块保留标题作为BeforeText的一部分
func (g *PDFGenerator) extractAllTables(content string) []TableBlock {
var blocks []TableBlock
lines := strings.Split(content, "\n")
var currentTableLines []string
var beforeTableLines []string
inTable := false
lastTableEnd := -1
for i, line := range lines {
trimmedLine := strings.TrimSpace(line)
// 保留标题行不跳过标题会作为BeforeText的一部分
// 检查是否是表格行(包含|符号,且不是分隔行)
isSeparator := false
if strings.Contains(trimmedLine, "|") && strings.Contains(trimmedLine, "-") {
// 检查是否是分隔行(只包含|、-、:、空格)
isSeparator = true
for _, r := range trimmedLine {
if r != '|' && r != '-' && r != ':' && r != ' ' {
isSeparator = false
break
}
}
}
isTableLine := strings.Contains(trimmedLine, "|") && !isSeparator
if isTableLine {
if !inTable {
// 开始新表格,保存之前的文本(包括标题)
beforeTableLines = []string{}
if lastTableEnd >= 0 {
beforeTableLines = lines[lastTableEnd+1 : i]
} else {
beforeTableLines = lines[0:i]
}
inTable = true
currentTableLines = []string{}
}
currentTableLines = append(currentTableLines, line)
} else {
if inTable {
// 表格可能结束了(遇到空行或非表格内容)
// 检查是否是连续的空行(可能是表格真的结束了)
if trimmedLine == "" {
// 空行,继续收集(可能是表格内的空行)
currentTableLines = append(currentTableLines, line)
} else {
// 非空行,表格结束
// 解析并保存表格
tableContent := strings.Join(currentTableLines, "\n")
tableData := g.parseMarkdownTable(tableContent)
if len(tableData) > 0 && g.isValidTable(tableData) {
block := TableBlock{
BeforeText: strings.Join(beforeTableLines, "\n"),
TableData: tableData,
AfterText: "",
}
blocks = append(blocks, block)
lastTableEnd = i - 1
}
currentTableLines = []string{}
beforeTableLines = []string{}
inTable = false
}
} else {
// 不在表格中这些行包括标题会被收集到下一个表格的BeforeText中
// 不需要特殊处理,它们会在开始新表格时被收集
}
}
}
// 处理最后一个表格(如果还在表格中)
if inTable && len(currentTableLines) > 0 {
tableContent := strings.Join(currentTableLines, "\n")
tableData := g.parseMarkdownTable(tableContent)
if len(tableData) > 0 && g.isValidTable(tableData) {
block := TableBlock{
BeforeText: strings.Join(beforeTableLines, "\n"),
TableData: tableData,
AfterText: "",
}
blocks = append(blocks, block)
}
}
return blocks
}
// renderTextWithTitles 渲染包含markdown标题的文本
func (g *PDFGenerator) 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)
if chineseFontAvailable {
pdf.SetFont("ChineseFont", "B", fontSize)
} else {
pdf.SetFont("Arial", "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 := g.stripHTML(line)
cleanText = g.cleanTextPreservingMarkdown(cleanText)
if strings.TrimSpace(cleanText) != "" {
pdf.SetTextColor(0, 0, 0)
if chineseFontAvailable {
pdf.SetFont("ChineseFont", "", 10)
} else {
pdf.SetFont("Arial", "", 10)
}
pdf.MultiCell(0, lineHt*1.3, cleanText, "", "L", false)
}
} else {
// 空行,添加间距
pdf.Ln(2)
}
}
}
// processSectionContent 处理单个章节的内容(解析表格或显示文本)
func (g *PDFGenerator) processSectionContent(pdf *gofpdf.Fpdf, content string, chineseFontAvailable bool, lineHt float64) {
// 尝试解析markdown表格
tableData := g.parseMarkdownTable(content)
// 记录解析结果用于调试
contentPreview := content
if len(contentPreview) > 100 {
contentPreview = contentPreview[:100] + "..."
}
g.logger.Info("解析表格结果",
zap.Int("table_rows", len(tableData)),
zap.Bool("is_valid", g.isValidTable(tableData)),
zap.String("content_preview", contentPreview))
// 检查内容是否包含表格标记(|符号)
hasTableMarkers := strings.Contains(content, "|")
// 如果解析出了表格数据(即使验证失败),或者内容包含表格标记,都尝试渲染表格
// 放宽条件:支持只有表头的表格(单行表格)
if len(tableData) >= 1 && hasTableMarkers {
// 如果表格数据不够完整,但包含表格标记,尝试强制解析
if !g.isValidTable(tableData) && hasTableMarkers && len(tableData) < 2 {
g.logger.Warn("表格验证失败但包含表格标记,尝试重新解析", zap.Int("rows", len(tableData)))
// 可以在这里添加更宽松的解析逻辑
}
// 如果表格有效,或者至少有表头,都尝试渲染
if g.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 = g.stripHTML(beforeText)
beforeText = g.cleanText(beforeText)
if strings.TrimSpace(beforeText) != "" {
pdf.SetTextColor(0, 0, 0)
if chineseFontAvailable {
pdf.SetFont("ChineseFont", "", 10)
} else {
pdf.SetFont("Arial", "", 10)
}
pdf.MultiCell(0, lineHt*1.3, beforeText, "", "L", false)
pdf.Ln(3)
}
}
// 渲染表格
g.addTable(pdf, tableData, chineseFontAvailable)
// 显示表格后的说明文字
if len(afterTable) > 0 {
afterText := strings.Join(afterTable, "\n")
afterText = g.stripHTML(afterText)
afterText = g.cleanText(afterText)
if strings.TrimSpace(afterText) != "" {
pdf.Ln(3)
pdf.SetTextColor(0, 0, 0)
if chineseFontAvailable {
pdf.SetFont("ChineseFont", "", 10)
} else {
pdf.SetFont("Arial", "", 10)
}
pdf.MultiCell(0, lineHt*1.3, afterText, "", "L", false)
}
}
}
} else {
// 如果不是有效表格显示为文本完整显示markdown内容
pdf.SetTextColor(0, 0, 0)
if chineseFontAvailable {
pdf.SetFont("ChineseFont", "", 10)
} else {
pdf.SetFont("Arial", "", 10)
}
text := g.stripHTML(content)
text = g.cleanText(text) // 清理无效字符,保留中文
// 如果文本不为空,显示它
if strings.TrimSpace(text) != "" {
pdf.MultiCell(0, lineHt*1.3, text, "", "L", false)
}
}
}
// isValidTable 验证表格是否有效
func (g *PDFGenerator) isValidTable(tableData [][]string) bool {
// 至少需要表头(放宽条件,支持只有表头的情况)
if len(tableData) < 1 {
return false
}
// 表头必须至少1列支持单列表格
header := tableData[0]
if len(header) < 1 {
return false
}
// 检查表头是否包含有效内容(不是全部为空)
hasValidHeader := false
for _, cell := range header {
if strings.TrimSpace(cell) != "" {
hasValidHeader = true
break
}
}
if !hasValidHeader {
return false
}
// 如果只有表头,也认为是有效表格
if len(tableData) == 1 {
return true
}
// 检查数据行是否包含有效内容
hasValidData := false
validRowCount := 0
for i := 1; i < len(tableData); i++ {
row := tableData[i]
// 检查这一行是否包含有效内容
rowHasContent := false
for _, cell := range row {
if strings.TrimSpace(cell) != "" {
rowHasContent = true
break
}
}
if rowHasContent {
hasValidData = true
validRowCount++
}
}
// 如果有数据行,至少需要一行有效数据
if len(tableData) > 1 && !hasValidData {
return false
}
// 如果有效行数过多超过100行可能是解析错误不认为是有效表格
if validRowCount > 100 {
g.logger.Warn("表格行数过多,可能是解析错误", zap.Int("row_count", validRowCount))
return false
}
return true
}
// addTable 添加表格
func (g *PDFGenerator) addTable(pdf *gofpdf.Fpdf, tableData [][]string, chineseFontAvailable bool) {
if len(tableData) == 0 {
return
}
// 再次验证表格有效性,避免渲染无效表格
if !g.isValidTable(tableData) {
g.logger.Warn("尝试渲染无效表格,跳过", zap.Int("rows", len(tableData)))
return
}
// 支持只有表头的表格(单行表格)
if len(tableData) == 1 {
g.logger.Info("渲染单行表格(只有表头)", zap.Int("cols", len(tableData[0])))
}
_, lineHt := pdf.GetFontSize()
pdf.SetFont("Arial", "", 9)
// 计算列宽(简单平均分配)
pageWidth, _ := pdf.GetPageSize()
pageWidth = pageWidth - 40 // 减去左右边距
numCols := len(tableData[0])
colWidth := pageWidth / float64(numCols)
// 限制列宽,避免过窄
if colWidth < 30 {
colWidth = 30
}
// 绘制表头
header := tableData[0]
pdf.SetFillColor(74, 144, 226) // 蓝色背景
pdf.SetTextColor(255, 255, 255) // 白色文字
if chineseFontAvailable {
pdf.SetFont("ChineseFont", "B", 9)
} else {
pdf.SetFont("Arial", "B", 9)
}
// 清理表头文本只清理无效字符保留markdown格式
for i, cell := range header {
header[i] = g.cleanTextPreservingMarkdown(cell)
}
for _, cell := range header {
pdf.CellFormat(colWidth, lineHt*1.5, cell, "1", 0, "C", true, 0, "")
}
pdf.Ln(-1)
// 绘制数据行
pdf.SetFillColor(245, 245, 220) // 米色背景
pdf.SetTextColor(0, 0, 0) // 深黑色文字,确保清晰
if chineseFontAvailable {
pdf.SetFont("ChineseFont", "", 9) // 增大字体
} else {
pdf.SetFont("Arial", "", 9)
}
_, lineHt = pdf.GetFontSize()
for i := 1; i < len(tableData); i++ {
row := tableData[i]
fill := (i % 2) == 0 // 交替填充
// 计算这一行的起始Y坐标
startY := pdf.GetY()
// 设置字体以计算文本宽度和高度
if chineseFontAvailable {
pdf.SetFont("ChineseFont", "", 9)
} else {
pdf.SetFont("Arial", "", 9)
}
_, cellLineHt := pdf.GetFontSize()
// 先遍历一次,计算每列需要的最大高度
maxCellHeight := cellLineHt * 1.5 // 最小高度
cellWidth := colWidth - 4 // 减去左右边距
for j, cell := range row {
if j >= numCols {
break
}
// 清理单元格文本只清理无效字符保留markdown格式
cleanCell := g.cleanTextPreservingMarkdown(cell)
// 使用SplitText准确计算需要的行数
var lines []string
if chineseFontAvailable {
// 对于中文字体使用SplitText
lines = pdf.SplitText(cleanCell, cellWidth)
} else {
// 对于Arial字体如果包含中文可能失败使用估算
charCount := len([]rune(cleanCell))
if charCount == 0 {
lines = []string{""}
} else {
// 中文字符宽度大约是英文字符的2倍
estimatedWidth := 0.0
for _, r := range cleanCell {
if r >= 0x4E00 && r <= 0x9FFF {
estimatedWidth += 6.0 // 中文字符宽度
} else {
estimatedWidth += 3.0 // 英文字符宽度
}
}
estimatedLines := math.Ceil(estimatedWidth / cellWidth)
if estimatedLines < 1 {
estimatedLines = 1
}
lines = make([]string, int(estimatedLines))
// 简单分割文本
charsPerLine := int(math.Ceil(float64(charCount) / estimatedLines))
for k := 0; k < int(estimatedLines); k++ {
start := k * charsPerLine
end := start + charsPerLine
if end > charCount {
end = charCount
}
if start < charCount {
runes := []rune(cleanCell)
if start < len(runes) {
if end > len(runes) {
end = len(runes)
}
lines[k] = string(runes[start:end])
}
}
}
}
}
// 计算单元格高度
numLines := float64(len(lines))
if numLines == 0 {
numLines = 1
}
cellHeight := numLines * cellLineHt * 1.5
if cellHeight < cellLineHt*1.5 {
cellHeight = cellLineHt * 1.5
}
if cellHeight > maxCellHeight {
maxCellHeight = cellHeight
}
}
// 绘制这一行的所有单元格左边距是15mm
currentX := 15.0
for j, cell := range row {
if j >= numCols {
break
}
// 绘制单元格边框和背景
if fill {
pdf.SetFillColor(250, 250, 235) // 稍深的米色
} else {
pdf.SetFillColor(255, 255, 255)
}
pdf.Rect(currentX, startY, colWidth, maxCellHeight, "FD")
// 绘制文本使用MultiCell支持换行
pdf.SetTextColor(0, 0, 0) // 确保深黑色
// 只清理无效字符保留markdown格式
cleanCell := g.cleanTextPreservingMarkdown(cell)
// 设置到单元格内,留出边距(每个单元格都从同一行开始)
pdf.SetXY(currentX+2, startY+2)
// 使用MultiCell自动换行左对齐
if chineseFontAvailable {
pdf.SetFont("ChineseFont", "", 9)
} else {
pdf.SetFont("Arial", "", 9)
}
// 使用MultiCell会自动换行使用统一的行高
// 限制高度,避免超出单元格
pdf.MultiCell(colWidth-4, cellLineHt*1.5, cleanCell, "", "L", false)
// MultiCell后Y坐标已经改变必须重置以便下一列从同一行开始
// 这是关键确保所有列都从同一个startY开始
pdf.SetXY(currentX+colWidth, startY)
// 移动到下一列
currentX += colWidth
}
// 移动到下一行的起始位置(使用计算好的最大高度)
pdf.SetXY(15.0, startY+maxCellHeight)
}
}
// calculateCellHeight 计算单元格高度(考虑换行)
func (g *PDFGenerator) calculateCellHeight(pdf *gofpdf.Fpdf, text string, width, lineHeight float64) float64 {
// 移除中文字符避免Arial字体处理时panic
// 只保留ASCII字符和常见符号
safeText := g.removeNonASCII(text)
if safeText == "" {
// 如果全部是中文,使用一个估算值
// 中文字符通常比英文字符宽按每行30个字符估算
charCount := len([]rune(text))
estimatedLines := (charCount / 30) + 1
if estimatedLines < 1 {
estimatedLines = 1
}
return float64(estimatedLines) * lineHeight
}
// 安全地调用SplitText
defer func() {
if r := recover(); r != nil {
g.logger.Warn("SplitText失败使用估算高度", zap.Any("error", r))
}
}()
lines := pdf.SplitText(safeText, width)
if len(lines) == 0 {
return lineHeight
}
return float64(len(lines)) * lineHeight
}
// removeNonASCII 移除非ASCII字符保留ASCII字符和常见符号
func (g *PDFGenerator) removeNonASCII(text string) string {
var result strings.Builder
for _, r := range text {
// 保留ASCII字符0-127
if r < 128 {
result.WriteRune(r)
} else {
// 中文字符替换为空格或跳过
result.WriteRune(' ')
}
}
return result.String()
}
// parseMarkdownTable 解析Markdown表格支持|分隔和空格分隔)
func (g *PDFGenerator) parseMarkdownTable(text string) [][]string {
lines := strings.Split(text, "\n")
var table [][]string
var header []string
inTable := false
usePipeDelimiter := false
// 先检查是否使用 | 分隔符
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
if strings.Contains(line, "|") {
usePipeDelimiter = true
break
}
}
// 记录解析开始
g.logger.Info("开始解析markdown表格",
zap.Int("total_lines", len(lines)),
zap.Bool("use_pipe_delimiter", usePipeDelimiter))
nonTableLineCount := 0 // 连续非表格行计数(不包括空行)
maxNonTableLines := 10 // 最多允许10个连续非表格行增加容忍度
for _, line := range lines {
line = strings.TrimSpace(line)
// 检查是否是明确的结束标记
if strings.HasPrefix(line, "```") || strings.HasPrefix(line, "##") || strings.HasPrefix(line, "###") || strings.HasPrefix(line, "####") {
// 如果遇到代码块或新的标题,停止解析
if inTable {
break
}
continue
}
if line == "" {
// 空行不影响非表格行计数,继续
continue
}
var cells []string
if usePipeDelimiter {
// 使用 | 分隔符的表格
if !strings.Contains(line, "|") {
// 如果已经在表格中,遇到非表格行
if inTable {
nonTableLineCount++
// 如果连续非表格行过多,可能表格已结束
if nonTableLineCount > maxNonTableLines {
// 但先检查后面是否还有表格行
hasMoreTableRows := false
for j := len(lines) - 1; j > 0 && j > len(lines)-20; j-- {
if strings.Contains(strings.TrimSpace(lines[j]), "|") {
hasMoreTableRows = true
break
}
}
if !hasMoreTableRows {
break
}
// 如果后面还有表格行,继续解析
nonTableLineCount = 0
}
continue
}
// 如果还没开始表格,跳过非表格行
continue
}
// 重置非表格行计数(遇到表格行了)
nonTableLineCount = 0
// 跳过分隔行markdown表格的分隔行如 |---|---| 或 |----------|----------|
// 检查是否是分隔行:只包含 |、-、:、空格,且至少包含一个-
trimmedLineForCheck := strings.TrimSpace(line)
isSeparator := false
if strings.Contains(trimmedLineForCheck, "-") {
isSeparator = true
for _, r := range trimmedLineForCheck {
if r != '|' && r != '-' && r != ':' && r != ' ' {
isSeparator = false
break
}
}
}
if isSeparator {
// 这是分隔行,跳过(不管是否已有表头)
// 但如果还没有表头,这可能表示表头在下一行
if !inTable {
// 跳过分隔行,等待真正的表头
continue
}
// 如果已经有表头,这可能是格式错误,但继续解析(不停止)
continue
}
cells = strings.Split(line, "|")
// 清理首尾空元素
if len(cells) > 0 && cells[0] == "" {
cells = cells[1:]
}
if len(cells) > 0 && cells[len(cells)-1] == "" {
cells = cells[:len(cells)-1]
}
// 验证单元格数量:如果已经有表头,数据行的列数应该与表头一致(允许少量差异)
// 但不要因为列数不一致就停止,而是调整列数以匹配表头
if inTable && len(header) > 0 && len(cells) > 0 {
// 如果列数不一致,调整以匹配表头
if len(cells) < len(header) {
// 如果数据行列数少于表头,补齐空单元格
for len(cells) < len(header) {
cells = append(cells, "")
}
} else if len(cells) > len(header) {
// 如果数据行列数多于表头,截断(但记录警告)
if len(cells)-len(header) > 3 {
g.logger.Warn("表格列数差异较大,截断多余列",
zap.Int("header_cols", len(header)),
zap.Int("row_cols", len(cells)))
}
cells = cells[:len(header)]
}
}
} else {
// 使用空格/制表符分隔的表格
// 先尝试识别是否是表头行(中文表头,如"字段名类型说明"
if strings.ContainsAny(line, "字段类型说明") && !strings.Contains(line, " ") {
// 可能是连在一起的中文表头,需要手动分割
if strings.Contains(line, "字段名") {
cells = []string{"字段名", "类型", "说明"}
} else if strings.Contains(line, "字段") {
cells = []string{"字段", "类型", "说明"}
} else {
// 尝试智能分割
fields := strings.Fields(line)
cells = fields
}
} else {
// 尝试按多个连续空格或制表符分割
fields := strings.Fields(line)
if len(fields) >= 2 {
// 至少有两列,尝试智能分割
// 识别:字段名、类型、说明
fieldName := ""
fieldType := ""
description := ""
typeKeywords := []string{"object", "Object", "string", "String", "int", "Int", "bool", "Bool", "array", "Array", "number", "Number"}
// 第一个字段通常是字段名
fieldName = fields[0]
// 查找类型字段
for i := 1; i < len(fields); i++ {
isType := false
for _, kw := range typeKeywords {
if fields[i] == kw || strings.EqualFold(fields[i], kw) {
fieldType = fields[i]
// 剩余字段作为说明
if i+1 < len(fields) {
description = strings.Join(fields[i+1:], " ")
}
isType = true
break
}
}
if isType {
break
}
// 如果第二个字段看起来像类型(较短且是已知关键词的一部分)
if i == 1 && len(fields[i]) <= 10 {
fieldType = fields[i]
if i+1 < len(fields) {
description = strings.Join(fields[i+1:], " ")
}
break
}
}
// 如果没找到类型,假设第二个字段是类型
if fieldType == "" && len(fields) >= 2 {
fieldType = fields[1]
if len(fields) > 2 {
description = strings.Join(fields[2:], " ")
}
}
// 构建单元格数组
cells = []string{fieldName}
if fieldType != "" {
cells = append(cells, fieldType)
}
if description != "" {
cells = append(cells, description)
} else if len(fields) > 2 {
// 如果说明为空但还有更多字段,合并所有剩余字段
cells = append(cells, strings.Join(fields[2:], " "))
}
} else if len(fields) == 1 {
// 单列,可能是标题行或分隔
continue
} else {
continue
}
}
}
// 清理每个单元格保留markdown格式只清理HTML和无效字符
cleanedCells := make([]string, 0, len(cells))
for _, cell := range cells {
cell = strings.TrimSpace(cell)
// 先清理HTML标签但保留markdown格式如**粗体**、*斜体*等)
cell = g.stripHTML(cell)
// 清理无效字符但保留markdown语法字符*、_、`、[]、()等)
cell = g.cleanTextPreservingMarkdown(cell)
cleanedCells = append(cleanedCells, cell)
}
// 检查这一行是否包含有效内容(至少有一个非空单元格)
hasValidContent := false
for _, cell := range cleanedCells {
if strings.TrimSpace(cell) != "" {
hasValidContent = true
break
}
}
// 如果这一行完全没有有效内容,跳过(但不停止解析)
if !hasValidContent {
// 空行不应该停止解析,继续查找下一行
continue
}
// 确保cleanedCells至少有1列即使只有1列也可能是有效数据
if len(cleanedCells) == 0 {
continue
}
// 支持单列表格和多列表格至少1列
if len(cleanedCells) >= 1 {
if !inTable {
// 第一行作为表头
header = cleanedCells
// 如果表头只有1列保持单列如果2列补齐为3列字段名、类型、说明
if len(header) == 2 {
header = append(header, "说明")
}
table = append(table, header)
inTable = true
g.logger.Debug("添加表头",
zap.Int("cols", len(header)),
zap.Strings("header", header))
} else {
// 数据行,确保列数与表头一致
row := make([]string, len(header))
for i := range row {
if i < len(cleanedCells) {
row[i] = cleanedCells[i]
} else {
row[i] = ""
}
}
table = append(table, row)
// 记录第一列内容用于调试
firstCellPreview := ""
if len(row) > 0 {
firstCellPreview = row[0]
if len(firstCellPreview) > 20 {
firstCellPreview = firstCellPreview[:20] + "..."
}
}
g.logger.Debug("添加数据行",
zap.Int("row_num", len(table)-1),
zap.Int("cols", len(row)),
zap.String("first_cell", firstCellPreview))
}
} else if inTable && len(cleanedCells) > 0 {
// 如果已经在表格中,但这一行列数不够,可能是说明行,合并到上一行
if len(table) > 0 {
lastRow := table[len(table)-1]
if len(lastRow) > 0 {
// 将内容追加到最后一列的说明中
lastRow[len(lastRow)-1] += " " + strings.Join(cleanedCells, " ")
table[len(table)-1] = lastRow
}
}
}
// 注意:不再因为不符合表格格式就停止解析,继续查找可能的表格行
}
// 记录解析结果
g.logger.Info("表格解析完成",
zap.Int("table_rows", len(table)),
zap.Bool("has_header", len(table) > 0),
zap.Int("data_rows", len(table)-1))
// 放宽验证条件:至少需要表头(允许只有表头的情况,或者表头+数据行)
if len(table) < 1 {
g.logger.Warn("表格数据不足,至少需要表头", zap.Int("rows", len(table)))
return nil
}
// 如果只有表头没有数据行,也认为是有效表格(可能是单行表格)
if len(table) == 1 {
g.logger.Info("表格只有表头,没有数据行", zap.Int("header_cols", len(table[0])))
// 仍然返回,让渲染函数处理
}
// 记录表头信息
if len(table) > 0 {
g.logger.Info("表格表头",
zap.Int("header_cols", len(table[0])),
zap.Strings("header", table[0]))
}
return table
}
// cleanText 清理文本中的无效字符和乱码
func (g *PDFGenerator) cleanText(text string) string {
// 先解码HTML实体
text = html.UnescapeString(text)
// 移除或替换无效的UTF-8字符
var result strings.Builder
for _, r := range text {
// 保留:中文字符、英文字母、数字、常见标点符号、空格、换行符等
if (r >= 0x4E00 && r <= 0x9FFF) || // 中文字符范围
(r >= 0x3400 && r <= 0x4DBF) || // 扩展A
(r >= 0x20000 && r <= 0x2A6DF) || // 扩展B
(r >= 'A' && r <= 'Z') || // 大写字母
(r >= 'a' && r <= 'z') || // 小写字母
(r >= '0' && r <= '9') || // 数字
(r >= 0x0020 && r <= 0x007E) || // ASCII可打印字符
(r == '\n' || r == '\r' || r == '\t') || // 换行和制表符
(r >= 0x3000 && r <= 0x303F) || // CJK符号和标点
(r >= 0xFF00 && r <= 0xFFEF) { // 全角字符
result.WriteRune(r)
} else if r > 0x007F && r < 0x00A0 {
// 无效的控制字符,替换为空格
result.WriteRune(' ')
}
// 其他字符(如乱码)直接跳过
}
return result.String()
}
// cleanTextPreservingMarkdown 清理文本但保留markdown语法字符
func (g *PDFGenerator) cleanTextPreservingMarkdown(text string) string {
// 先解码HTML实体
text = html.UnescapeString(text)
// 移除或替换无效的UTF-8字符但保留markdown语法字符
var result strings.Builder
for _, r := range text {
// 保留:中文字符、英文字母、数字、常见标点符号、空格、换行符等
// 特别保留markdown语法字符* _ ` [ ] ( ) # - | : !
if (r >= 0x4E00 && r <= 0x9FFF) || // 中文字符范围
(r >= 0x3400 && r <= 0x4DBF) || // 扩展A
(r >= 0x20000 && r <= 0x2A6DF) || // 扩展B
(r >= 'A' && r <= 'Z') || // 大写字母
(r >= 'a' && r <= 'z') || // 小写字母
(r >= '0' && r <= '9') || // 数字
(r >= 0x0020 && r <= 0x007E) || // ASCII可打印字符包括markdown语法字符
(r == '\n' || r == '\r' || r == '\t') || // 换行和制表符
(r >= 0x3000 && r <= 0x303F) || // CJK符号和标点
(r >= 0xFF00 && r <= 0xFFEF) { // 全角字符
result.WriteRune(r)
} else if r > 0x007F && r < 0x00A0 {
// 无效的控制字符,替换为空格
result.WriteRune(' ')
}
// 其他字符(如乱码)直接跳过
}
return result.String()
}
// removeMarkdownSyntax 移除markdown语法保留纯文本
func (g *PDFGenerator) removeMarkdownSyntax(text string) string {
// 移除粗体标记 **text** 或 __text__
text = regexp.MustCompile(`\*\*([^*]+)\*\*`).ReplaceAllString(text, "$1")
text = regexp.MustCompile(`__([^_]+)__`).ReplaceAllString(text, "$1")
// 移除斜体标记 *text* 或 _text_
text = regexp.MustCompile(`\*([^*]+)\*`).ReplaceAllString(text, "$1")
text = regexp.MustCompile(`_([^_]+)_`).ReplaceAllString(text, "$1")
// 移除代码标记 `code`
text = regexp.MustCompile("`([^`]+)`").ReplaceAllString(text, "$1")
// 移除链接标记 [text](url) -> text
text = regexp.MustCompile(`\[([^\]]+)\]\([^\)]+\)`).ReplaceAllString(text, "$1")
// 移除图片标记 ![alt](url) -> alt
text = regexp.MustCompile(`!\[([^\]]*)\]\([^\)]+\)`).ReplaceAllString(text, "$1")
// 移除标题标记 # text -> text
text = regexp.MustCompile(`^#{1,6}\s+(.+)$`).ReplaceAllString(text, "$1")
return text
}
// stripHTML 去除HTML标签
func (g *PDFGenerator) stripHTML(text string) string {
// 解码HTML实体
text = html.UnescapeString(text)
// 移除HTML标签
re := regexp.MustCompile(`<[^>]+>`)
text = re.ReplaceAllString(text, "")
// 处理换行
text = strings.ReplaceAll(text, "<br>", "\n")
text = strings.ReplaceAll(text, "<br/>", "\n")
text = strings.ReplaceAll(text, "</p>", "\n")
text = strings.ReplaceAll(text, "</div>", "\n")
// 清理多余空白
text = strings.TrimSpace(text)
lines := strings.Split(text, "\n")
var cleanedLines []string
for _, line := range lines {
line = strings.TrimSpace(line)
if line != "" {
cleanedLines = append(cleanedLines, line)
}
}
return strings.Join(cleanedLines, "\n")
}
// formatJSON 格式化JSON字符串以便更好地显示
func (g *PDFGenerator) formatJSON(jsonStr string) (string, error) {
var jsonObj interface{}
if err := json.Unmarshal([]byte(jsonStr), &jsonObj); err != nil {
return jsonStr, err // 如果解析失败,返回原始字符串
}
// 重新格式化JSON使用缩进
formatted, err := json.MarshalIndent(jsonObj, "", " ")
if err != nil {
return jsonStr, err
}
return string(formatted), nil
}
// extractJSON 从文本中提取JSON
func (g *PDFGenerator) extractJSON(text string) string {
// 查找 ```json 代码块
re := regexp.MustCompile("(?s)```json\\s*\n(.*?)\n```")
matches := re.FindStringSubmatch(text)
if len(matches) > 1 {
return strings.TrimSpace(matches[1])
}
// 查找普通代码块
re = regexp.MustCompile("(?s)```\\s*\n(.*?)\n```")
matches = re.FindStringSubmatch(text)
if len(matches) > 1 {
content := strings.TrimSpace(matches[1])
// 检查是否是JSON
if strings.HasPrefix(content, "{") || strings.HasPrefix(content, "[") {
return content
}
}
return ""
}
// generateJSONExample 从请求参数表格生成JSON示例
func (g *PDFGenerator) generateJSONExample(requestParams string) string {
tableData := g.parseMarkdownTable(requestParams)
if len(tableData) < 2 {
return ""
}
// 查找字段名列和类型列
var fieldCol, typeCol int = -1, -1
header := tableData[0]
for i, h := range header {
hLower := strings.ToLower(h)
if strings.Contains(hLower, "字段") || strings.Contains(hLower, "参数") || strings.Contains(hLower, "field") {
fieldCol = i
}
if strings.Contains(hLower, "类型") || strings.Contains(hLower, "type") {
typeCol = i
}
}
if fieldCol == -1 {
return ""
}
// 生成JSON结构
jsonMap := make(map[string]interface{})
for i := 1; i < len(tableData); i++ {
row := tableData[i]
if fieldCol >= len(row) {
continue
}
fieldName := strings.TrimSpace(row[fieldCol])
if fieldName == "" {
continue
}
// 跳过表头行
if strings.Contains(strings.ToLower(fieldName), "字段") || strings.Contains(strings.ToLower(fieldName), "参数") {
continue
}
// 获取类型
fieldType := "string"
if typeCol >= 0 && typeCol < len(row) {
fieldType = strings.ToLower(strings.TrimSpace(row[typeCol]))
}
// 设置示例值
var value interface{}
if strings.Contains(fieldType, "int") || strings.Contains(fieldType, "number") {
value = 0
} else if strings.Contains(fieldType, "bool") {
value = true
} else if strings.Contains(fieldType, "array") || strings.Contains(fieldType, "list") {
value = []interface{}{}
} else if strings.Contains(fieldType, "object") || strings.Contains(fieldType, "dict") {
value = map[string]interface{}{}
} else {
// 根据字段名设置合理的示例值
fieldLower := strings.ToLower(fieldName)
if strings.Contains(fieldLower, "name") || strings.Contains(fieldName, "姓名") {
value = "张三"
} else if strings.Contains(fieldLower, "id_card") || strings.Contains(fieldLower, "idcard") || strings.Contains(fieldName, "身份证") {
value = "110101199001011234"
} else if strings.Contains(fieldLower, "phone") || strings.Contains(fieldLower, "mobile") || strings.Contains(fieldName, "手机") {
value = "13800138000"
} else if strings.Contains(fieldLower, "card") || strings.Contains(fieldName, "银行卡") {
value = "6222021234567890123"
} else {
value = "string"
}
}
// 处理嵌套字段(如 baseInfo.phone
if strings.Contains(fieldName, ".") {
parts := strings.Split(fieldName, ".")
current := jsonMap
for j := 0; j < len(parts)-1; j++ {
if _, ok := current[parts[j]].(map[string]interface{}); !ok {
current[parts[j]] = make(map[string]interface{})
}
current = current[parts[j]].(map[string]interface{})
}
current[parts[len(parts)-1]] = value
} else {
jsonMap[fieldName] = value
}
}
// 使用encoding/json正确格式化JSON
jsonBytes, err := json.MarshalIndent(jsonMap, "", " ")
if err != nil {
// 如果JSON序列化失败返回简单的字符串表示
return fmt.Sprintf("%v", jsonMap)
}
return string(jsonBytes)
}
// addHeader 添加页眉logo和文字
func (g *PDFGenerator) addHeader(pdf *gofpdf.Fpdf, chineseFontAvailable bool) {
pdf.SetY(5)
// 绘制logo如果存在
if g.logoPath != "" {
if _, err := os.Stat(g.logoPath); err == nil {
// gofpdf的ImageOptions方法调整位置和大小左边距是15mm
pdf.ImageOptions(g.logoPath, 15, 5, 15, 15, false, gofpdf.ImageOptions{}, 0, "")
g.logger.Info("已添加logo", zap.String("path", g.logoPath))
} else {
g.logger.Warn("logo文件不存在", zap.String("path", g.logoPath), zap.Error(err))
}
} else {
g.logger.Warn("logo路径为空")
}
// 绘制"天远数据"文字(使用中文字体如果可用)
pdf.SetXY(33, 8)
if chineseFontAvailable {
pdf.SetFont("ChineseFont", "B", 14)
} else {
pdf.SetFont("Arial", "B", 14)
}
pdf.CellFormat(0, 10, "天远数据", "", 0, "L", false, 0, "")
// 绘制下横线优化位置左边距是15mm
pdf.Line(15, 22, 75, 22)
}
// addWatermark 添加水印从左边开始向上倾斜45度考虑可用区域
func (g *PDFGenerator) 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
pdf.SetFont("ChineseFont", "", fontSize)
// 设置灰色和透明度(加深水印,使其更明显)
pdf.SetTextColor(180, 180, 180) // 深一点的灰色
pdf.SetAlpha(0.25, "Normal") // 增加透明度,让水印更明显
// 计算文字宽度
textWidth := pdf.GetStringWidth(g.watermarkText)
if textWidth == 0 {
// 如果无法获取宽度(字体未注册),使用估算值(中文字符大约每个 fontSize/3 mm
textWidth = float64(len([]rune(g.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
pdf.SetFont("ChineseFont", "", fontSize)
textWidth = pdf.GetStringWidth(g.watermarkText)
if textWidth == 0 {
textWidth = float64(len([]rune(g.watermarkText))) * fontSize / 3.0
}
}
// 从左边开始绘制水印文字
pdf.SetXY(0, 0)
pdf.CellFormat(textWidth, fontSize, g.watermarkText, "", 0, "L", false, 0, "")
// 恢复透明度和颜色
pdf.SetAlpha(1.0, "Normal")
pdf.SetTextColor(0, 0, 0) // 恢复为黑色
}