diff --git a/pkg/service/cast/report.go b/pkg/service/cast/report.go index 1415983b..a4331f62 100644 --- a/pkg/service/cast/report.go +++ b/pkg/service/cast/report.go @@ -138,6 +138,9 @@ func CreateCompetitiveReportCore(ctx *gin.Context, req *cast.CreateCompetitiveRe competitorReportData.ImageURL = req.ImageUrl } + // 截断超长字段(按AI生成的字段长度要求) + competitorReportData = truncateCompetitorReportData(competitorReportData) + zap.L().Info("解析成功", zap.Any("competitorReportData", competitorReportData)) today := time.Now().Format("20060102") @@ -411,6 +414,9 @@ func ImportCompetitiveReportBatch(ctx *gin.Context) { competitorReportData.HighlightAnalysis = highlightAnalysis competitorReportData.DataPerformance = dataPerformance + // 截断超长字段(按AI生成的字段长度要求) + competitorReportData = truncateCompetitorReportData(competitorReportData) + // 验证必填字段 if artistNum == "" { temp.Remark = "艺人编号不能为空" @@ -1309,3 +1315,38 @@ func checkAndReuploadImageForReport(imageUrl string) (string, error) { return compressUrl, nil } + +// truncateCompetitorReportData 截断竞品报告数据中超长的字段 +// 字段长度要求参考 AI 生成竞品报告的限制 +func truncateCompetitorReportData(data utils.CompetitorReportData) utils.CompetitorReportData { + // 字段长度限制 + const ( + MaxSummary = 78 // 概述 + MaxPointField = 60 // 标题/题材/内容/文案/数据/配乐亮点 + MaxViews = 60 // 浏览量 + MaxCompletion = 60 // 完播率 + MaxEngagement = 60 // 点赞/分享/评论 + MaxOverallSummary = 300 // 整体总结及可优化建议 + ) + + // 截断亮点分析摘要 + data.HighlightAnalysis.Summary = utils.TruncateTextByRune(data.HighlightAnalysis.Summary, MaxSummary) + + // 截断各亮点字段 + data.HighlightAnalysis.Points.Theme = utils.TruncateTextByRune(data.HighlightAnalysis.Points.Theme, MaxPointField) + data.HighlightAnalysis.Points.Narrative = utils.TruncateTextByRune(data.HighlightAnalysis.Points.Narrative, MaxPointField) + data.HighlightAnalysis.Points.Content = utils.TruncateTextByRune(data.HighlightAnalysis.Points.Content, MaxPointField) + data.HighlightAnalysis.Points.Copywriting = utils.TruncateTextByRune(data.HighlightAnalysis.Points.Copywriting, MaxPointField) + data.HighlightAnalysis.Points.Data = utils.TruncateTextByRune(data.HighlightAnalysis.Points.Data, MaxPointField) + data.HighlightAnalysis.Points.Music = utils.TruncateTextByRune(data.HighlightAnalysis.Points.Music, MaxPointField) + + // 截断数据表现字段 + data.DataPerformance.Views = utils.TruncateTextByRune(data.DataPerformance.Views, MaxViews) + data.DataPerformance.Completion = utils.TruncateTextByRune(data.DataPerformance.Completion, MaxCompletion) + data.DataPerformance.Engagement = utils.TruncateTextByRune(data.DataPerformance.Engagement, MaxEngagement) + + // 截断整体总结 + data.OverallSummary = utils.TruncateTextByRune(data.OverallSummary, MaxOverallSummary) + + return data +} diff --git a/pkg/utils/pdf.go b/pkg/utils/pdf.go index a1ce80d7..4b824ea4 100644 --- a/pkg/utils/pdf.go +++ b/pkg/utils/pdf.go @@ -508,6 +508,30 @@ func splitTextByRune(text string, maxWidth float64) []string { return lines } +// TruncateTextByRune 按字符权重截断文本 +// 中文字符算1.0,英文字母/数字/英文符号算0.5 +// 返回截断后的文本,确保总权重不超过maxWidth +func TruncateTextByRune(text string, maxWidth float64) string { + if text == "" { + return "" + } + + runes := []rune(text) + var result []rune + currentWidth := 0.0 + + for _, r := range runes { + charWidth := getCharWidth(r) + if currentWidth+charWidth > maxWidth { + break + } + result = append(result, r) + currentWidth += charWidth + } + + return string(result) +} + // ConvertCompetitorReportToText 将竞品报告数据转换为文本格式 // 参数: // - data: 竞品报告数据 diff --git a/pkg/utils/pdf_competitor_test.go b/pkg/utils/pdf_competitor_test.go index 5b8414fb..9dae6a7b 100644 --- a/pkg/utils/pdf_competitor_test.go +++ b/pkg/utils/pdf_competitor_test.go @@ -221,3 +221,84 @@ func TestGenerateCompetitorReportPDFMixedContent(t *testing.T) { fmt.Printf("PDF生成成功(中英混合测试): %s\n", outputPath) } + +// TestTruncateTextByRune 测试按字符权重截断文本 +func TestTruncateTextByRune(t *testing.T) { + tests := []struct { + name string + text string + maxWidth float64 + want string + }{ + { + name: "空文本", + text: "", + maxWidth: 10, + want: "", + }, + { + name: "纯中文截断", + text: "这是一段很长的中文内容需要被截断", + maxWidth: 10, + want: "这是一段很长的中文内", // 10个中文字=10,正好 + }, + { + name: "纯英文截断(按0.5计算)", + text: "abcdefghijklmnop", + maxWidth: 5, + want: "abcdefghij", // 10个字母=5.0,正好 + }, + { + name: "数字截断(按0.5计算)", + text: "1234567890", + maxWidth: 3, + want: "123456", // 6个数字=3.0,正好 + }, + { + name: "中英混合截断", + text: "hello你好world世界", + maxWidth: 6, + // h(0.5)+e(0.5)+l(0.5)+l(0.5)+o(0.5)=2.5 + // +你(1)+好(1)=4.5 + // +w(0.5)+o(0.5)+r(0.5)+l(0.5)=2,再加就超过6了 + // 所以应该是 hello你好wor + want: "hello你好wor", + }, + { + name: "符号截断(按0.5计算)", + text: "hello,world!", + maxWidth: 5, + // h(0.5)+e(0.5)+l(0.5)+l(0.5)+o(0.5)+,(0.5)=3 + // +w(0.5)+o(0.5)+r(0.5)+l(0.5)+d(0.5)+!(0.5)=6,超过5 + // 所以保留 hello,worl + want: "hello,worl", + }, + { + name: "截断到正好边界", + text: "中文字符", + maxWidth: 4, + want: "中文字符", // 4个中文字=4.0,正好 + }, + { + name: "宽度为0", + text: "hello", + maxWidth: 0, + want: "", + }, + { + name: "宽度小于单个字符", + text: "hello", + maxWidth: 0.3, + want: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := TruncateTextByRune(tt.text, tt.maxWidth) + if got != tt.want { + t.Errorf("TruncateTextByRune(%q, %v) = %q, want %q", tt.text, tt.maxWidth, got, tt.want) + } + }) + } +}