diff --git a/internal/domains/product/entities/product_documentation.go b/internal/domains/product/entities/product_documentation.go index ecf7ef4..b73c743 100644 --- a/internal/domains/product/entities/product_documentation.go +++ b/internal/domains/product/entities/product_documentation.go @@ -45,7 +45,6 @@ func (pd *ProductDocumentation) IsValid() bool { return pd.DeletedAt.Time.IsZero() } - // UpdateContent 更新文档内容 func (pd *ProductDocumentation) UpdateContent(requestURL, requestMethod, basicInfo, requestParams, responseFields, responseExample, errorCodes string) { pd.RequestURL = requestURL @@ -167,7 +166,6 @@ func (pd *ProductDocumentation) UpdateDocumentation(requestURL, requestMethod, b return nil } - // GetDocumentationSummary 获取文档摘要 func (pd *ProductDocumentation) GetDocumentationSummary() map[string]interface{} { return map[string]interface{}{ diff --git a/internal/shared/pdf/database_table_renderer.go b/internal/shared/pdf/database_table_renderer.go index 9786c0b..16b1afb 100644 --- a/internal/shared/pdf/database_table_renderer.go +++ b/internal/shared/pdf/database_table_renderer.go @@ -2,7 +2,9 @@ package pdf import ( "math" + "regexp" "strings" + "unicode" "github.com/jung-kurt/gofpdf/v2" "go.uber.org/zap" @@ -227,6 +229,101 @@ func (r *DatabaseTableRenderer) calculateColumnWidths(pdf *gofpdf.Fpdf, tableDat return colWidths } +// cleanTextForPDF 清理文本,移除可能导致PDF生成问题的字符 +func (r *DatabaseTableRenderer) cleanTextForPDF(text string) string { + // 先移除HTML标签 + text = strings.TrimSpace(text) + text = strings.ReplaceAll(text, "
", " ") + text = strings.ReplaceAll(text, "
", " ") + text = strings.ReplaceAll(text, "
", " ") + + // 移除控制字符(除了换行符和制表符) + var cleaned strings.Builder + for _, r := range text { + // 保留可打印字符、空格、换行符、制表符 + if unicode.IsPrint(r) || r == '\n' || r == '\t' || r == '\r' { + cleaned.WriteRune(r) + } else if unicode.IsSpace(r) { + // 将其他空白字符转换为空格 + cleaned.WriteRune(' ') + } + } + text = cleaned.String() + + // 移除多余的空白字符 + text = regexp.MustCompile(`\s+`).ReplaceAllString(text, " ") + + // 确保文本不为空且长度合理 + if len([]rune(text)) > 10000 { + // 如果文本过长,截断 + runes := []rune(text) + text = string(runes[:10000]) + "..." + } + + return strings.TrimSpace(text) +} + +// safeSplitText 安全地调用SplitText,带错误恢复 +func (r *DatabaseTableRenderer) safeSplitText(pdf *gofpdf.Fpdf, text string, width float64) []string { + // 先清理文本 + text = r.cleanTextForPDF(text) + + // 如果文本为空或宽度无效,返回单行 + if text == "" || width <= 0 { + return []string{text} + } + + // 使用匿名函数和recover保护SplitText调用 + var lines []string + func() { + defer func() { + if rec := recover(); rec != nil { + r.logger.Warn("SplitText发生panic,使用估算值", + zap.Any("error", rec), + zap.String("text_preview", func() string { + if len(text) > 50 { + return text[:50] + "..." + } + return text + }()), + zap.Float64("width", width)) + // 如果panic发生,使用估算值 + charCount := len([]rune(text)) + estimatedLines := math.Max(1, math.Ceil(float64(charCount)/20)) + lines = make([]string, int(estimatedLines)) + if estimatedLines == 1 { + lines[0] = text + } else { + // 简单分割文本 + runes := []rune(text) + charsPerLine := int(math.Ceil(float64(len(runes)) / estimatedLines)) + for i := 0; i < int(estimatedLines); i++ { + start := i * charsPerLine + end := start + charsPerLine + if end > len(runes) { + end = len(runes) + } + if start < len(runes) { + lines[i] = string(runes[start:end]) + } else { + lines[i] = "" + } + } + } + } + }() + // 尝试调用SplitText + lines = pdf.SplitText(text, width) + }() + + // 如果lines为nil或空,返回单行 + if lines == nil || len(lines) == 0 { + return []string{text} + } + + return lines +} + // renderHeader 渲染表头 func (r *DatabaseTableRenderer) renderHeader(pdf *gofpdf.Fpdf, headers []string, colWidths []float64, startY float64) float64 { _, lineHt := pdf.GetFontSize() @@ -238,7 +335,8 @@ func (r *DatabaseTableRenderer) renderHeader(pdf *gofpdf.Fpdf, headers []string, break } colW := colWidths[i] - headerLines := pdf.SplitText(header, colW-6) // 增加边距,从4增加到6 + // 使用安全的SplitText方法 + headerLines := r.safeSplitText(pdf, header, colW-6) headerHeight := float64(len(headerLines)) * lineHt // 添加上下内边距 headerHeight += lineHt * 0.8 // 上下各0.4倍行高的内边距 @@ -263,11 +361,7 @@ func (r *DatabaseTableRenderer) renderHeader(pdf *gofpdf.Fpdf, headers []string, colW := colWidths[i] // 清理表头数据,移除任何残留的HTML标签和样式 - header = strings.TrimSpace(header) - // 移除HTML标签(使用简单的替换) - header = strings.ReplaceAll(header, "
", " ") - header = strings.ReplaceAll(header, "
", " ") - header = strings.ReplaceAll(header, "
", " ") + header = r.cleanTextForPDF(header) // 绘制表头背景 pdf.Rect(currentX, startY, colW, maxHeaderHeight, "FD") @@ -275,7 +369,8 @@ func (r *DatabaseTableRenderer) renderHeader(pdf *gofpdf.Fpdf, headers []string, // 绘制表头文本 if header != "" { // 计算文本的实际高度(减少内边距,给文本更多空间) - headerLines := pdf.SplitText(header, colW-4) + // 使用安全的SplitText方法 + headerLines := r.safeSplitText(pdf, header, colW-4) textHeight := float64(len(headerLines)) * lineHt if textHeight < lineHt { textHeight = lineHt @@ -360,31 +455,17 @@ func (r *DatabaseTableRenderer) renderRows(pdf *gofpdf.Fpdf, rows [][]string, co cell := row[j] cellWidth := colWidths[j] - 4 // 减少内边距到4mm,给文本更多空间 + // 先清理单元格文本(在计算宽度之前) + cell = r.cleanTextForPDF(cell) + // 计算文本实际宽度,判断是否需要换行 textWidth := r.getTextWidth(pdf, cell) var lines []string // 只有当文本宽度超过单元格宽度时才换行 if textWidth > cellWidth { - // 文本需要换行 - func() { - defer func() { - if rec := recover(); rec != nil { - r.logger.Warn("SplitText失败,使用估算", - zap.Any("error", rec), - zap.Int("row_index", rowIndex), - zap.Int("col_index", j)) - // 使用估算值 - charCount := len([]rune(cell)) - estimatedLines := math.Max(1, math.Ceil(float64(charCount)/20)) - lines = make([]string, int(estimatedLines)) - for i := range lines { - lines[i] = "" - } - } - }() - lines = pdf.SplitText(cell, cellWidth) - }() + // 文本需要换行,使用安全的SplitText方法 + lines = r.safeSplitText(pdf, cell, cellWidth) } else { // 文本不需要换行,单行显示 lines = []string{cell} @@ -417,11 +498,7 @@ func (r *DatabaseTableRenderer) renderRows(pdf *gofpdf.Fpdf, rows [][]string, co cell := row[j] // 清理单元格数据,移除任何残留的HTML标签和样式 - cell = strings.TrimSpace(cell) - // 移除HTML标签(使用简单的正则表达式) - cell = strings.ReplaceAll(cell, "
", " ") - cell = strings.ReplaceAll(cell, "
", " ") - cell = strings.ReplaceAll(cell, "
", " ") + cell = r.cleanTextForPDF(cell) // 绘制单元格背景 if fill { @@ -440,21 +517,8 @@ func (r *DatabaseTableRenderer) renderRows(pdf *gofpdf.Fpdf, rows [][]string, co var textLines []string // 只有当文本宽度超过单元格宽度时才换行 if textWidth > cellWidth { - // 文本需要换行 - func() { - defer func() { - if rec := recover(); rec != nil { - // 如果SplitText失败,使用估算 - charCount := len([]rune(cell)) - estimatedLines := math.Max(1, math.Ceil(float64(charCount)/20)) - textLines = make([]string, int(estimatedLines)) - for i := range textLines { - textLines[i] = "" - } - } - }() - textLines = pdf.SplitText(cell, cellWidth) - }() + // 文本需要换行,使用安全的SplitText方法 + textLines = r.safeSplitText(pdf, cell, cellWidth) } else { // 文本不需要换行,单行显示 textLines = []string{cell}