feat:优化竞品报告pdf生成效果
This commit is contained in:
parent
9dfb751aaf
commit
e1e453322a
2
.gitignore
vendored
2
.gitignore
vendored
@ -31,3 +31,5 @@
|
|||||||
/cmd/logs/*.log
|
/cmd/logs/*.log
|
||||||
/cmd/runtime/log/*.log
|
/cmd/runtime/log/*.log
|
||||||
/build/*
|
/build/*
|
||||||
|
CLAUDE.md
|
||||||
|
.claude/settings.local.json
|
||||||
|
|||||||
100
pkg/utils/pdf.go
100
pkg/utils/pdf.go
@ -253,66 +253,99 @@ func GenerateCompetitorReportPDF(templatePath, outputPath string, data Competito
|
|||||||
|
|
||||||
pdf.SetPage(1)
|
pdf.SetPage(1)
|
||||||
|
|
||||||
// 概述 - 使用MultiCell自动换行,一行最多35个字
|
// 概述 - 使用逐行写入,一行最多35个字
|
||||||
pdf.SetXY(200, 104)
|
pdf.SetXY(200, 104)
|
||||||
summaryRect := gopdf.Rect{W: 350.0, H: lineHeight}
|
summaryLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Summary), 35)
|
||||||
pdf.MultiCell(&summaryRect, data.HighlightAnalysis.Summary)
|
for i, line := range summaryLines {
|
||||||
|
pdf.SetXY(200, 104+float64(i)*lineHeight)
|
||||||
|
pdf.Cell(nil, line)
|
||||||
|
}
|
||||||
|
|
||||||
// 标题亮点 - 一行最多9个字
|
// 标题亮点 - 一行最多9个字
|
||||||
pdf.SetXY(200, 184)
|
pdf.SetXY(200, 184)
|
||||||
themeRect := gopdf.Rect{W: 120.0, H: lineHeight}
|
themeLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Theme), 9)
|
||||||
pdf.MultiCell(&themeRect, data.HighlightAnalysis.Points.Theme)
|
for i, line := range themeLines {
|
||||||
|
pdf.SetXY(200, 184+float64(i)*lineHeight)
|
||||||
|
pdf.Cell(nil, line)
|
||||||
|
}
|
||||||
|
|
||||||
// 题材亮点 - 一行最多9个字
|
// 题材亮点 - 一行最多9个字
|
||||||
pdf.SetXY(330, 184)
|
pdf.SetXY(330, 184)
|
||||||
narrativeRect := gopdf.Rect{W: 120.0, H: lineHeight}
|
narrativeLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Narrative), 9)
|
||||||
pdf.MultiCell(&narrativeRect, data.HighlightAnalysis.Points.Narrative)
|
for i, line := range narrativeLines {
|
||||||
|
pdf.SetXY(330, 184+float64(i)*lineHeight)
|
||||||
|
pdf.Cell(nil, line)
|
||||||
|
}
|
||||||
|
|
||||||
// 内容亮点 - 一行最多9个字
|
// 内容亮点 - 一行最多9个字
|
||||||
pdf.SetXY(460, 184)
|
pdf.SetXY(460, 184)
|
||||||
contentRect := gopdf.Rect{W: 120.0, H: lineHeight}
|
contentLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Content), 9)
|
||||||
pdf.MultiCell(&contentRect, data.HighlightAnalysis.Points.Content)
|
for i, line := range contentLines {
|
||||||
|
pdf.SetXY(460, 184+float64(i)*lineHeight)
|
||||||
|
pdf.Cell(nil, line)
|
||||||
|
}
|
||||||
|
|
||||||
// 文案亮点 - 一行最多9个字
|
// 文案亮点 - 一行最多9个字
|
||||||
pdf.SetXY(200, 323)
|
pdf.SetXY(200, 323)
|
||||||
copywritingRect := gopdf.Rect{W: 120.0, H: lineHeight}
|
copywritingLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Copywriting), 9)
|
||||||
pdf.MultiCell(©writingRect, data.HighlightAnalysis.Points.Copywriting)
|
for i, line := range copywritingLines {
|
||||||
|
pdf.SetXY(200, 323+float64(i)*lineHeight)
|
||||||
|
pdf.Cell(nil, line)
|
||||||
|
}
|
||||||
|
|
||||||
// 数据亮点 - 一行最多9个字
|
// 数据亮点 - 一行最多9个字
|
||||||
pdf.SetXY(330, 323)
|
pdf.SetXY(330, 323)
|
||||||
dataRect := gopdf.Rect{W: 120.0, H: lineHeight}
|
dataLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Data), 9)
|
||||||
pdf.MultiCell(&dataRect, data.HighlightAnalysis.Points.Data)
|
for i, line := range dataLines {
|
||||||
|
pdf.SetXY(330, 323+float64(i)*lineHeight)
|
||||||
|
pdf.Cell(nil, line)
|
||||||
|
}
|
||||||
|
|
||||||
// 配乐亮点(仅视频) - 一行最多9个字
|
// 配乐亮点(仅视频) - 一行最多9个字
|
||||||
if data.HighlightAnalysis.Points.Music != "" {
|
if data.HighlightAnalysis.Points.Music != "" {
|
||||||
pdf.SetXY(460, 323)
|
pdf.SetXY(460, 323)
|
||||||
musicRect := gopdf.Rect{W: 120.0, H: lineHeight}
|
musicLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Music), 9)
|
||||||
pdf.MultiCell(&musicRect, data.HighlightAnalysis.Points.Music)
|
for i, line := range musicLines {
|
||||||
|
pdf.SetXY(460, 323+float64(i)*lineHeight)
|
||||||
|
pdf.Cell(nil, line)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 浏览量 - 一行最多35个字
|
// 浏览量 - 一行最多35个字
|
||||||
pdf.SetXY(200, 474)
|
pdf.SetXY(200, 474)
|
||||||
viewsRect := gopdf.Rect{W: 350.0, H: lineHeight}
|
viewsLines := splitTextByRune(cleanTextForPDF(data.DataPerformance.Views), 35)
|
||||||
pdf.MultiCell(&viewsRect, data.DataPerformance.Views)
|
for i, line := range viewsLines {
|
||||||
|
pdf.SetXY(200, 474+float64(i)*lineHeight)
|
||||||
|
pdf.Cell(nil, line)
|
||||||
|
}
|
||||||
|
|
||||||
// 完播率 - 一行最多35个字
|
// 完播率 - 一行最多35个字
|
||||||
// 始终显示在固定位置,有内容时填充内容
|
// 始终显示在固定位置,有内容时填充内容
|
||||||
pdf.SetXY(200, 539)
|
pdf.SetXY(200, 539)
|
||||||
if data.DataPerformance.Completion != "" {
|
if data.DataPerformance.Completion != "" {
|
||||||
completionRect := gopdf.Rect{W: 350.0, H: lineHeight}
|
completionLines := splitTextByRune(cleanTextForPDF(data.DataPerformance.Completion), 35)
|
||||||
pdf.MultiCell(&completionRect, data.DataPerformance.Completion)
|
for i, line := range completionLines {
|
||||||
|
pdf.SetXY(200, 539+float64(i)*lineHeight)
|
||||||
|
pdf.Cell(nil, line)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 点赞/分享/评论 - 一行最多35个字
|
// 点赞/分享/评论 - 一行最多35个字
|
||||||
// 始终固定在完播率下面的位置
|
// 始终固定在完播率下面的位置
|
||||||
pdf.SetXY(200, 600)
|
pdf.SetXY(200, 600)
|
||||||
engagementRect := gopdf.Rect{W: 350.0, H: lineHeight}
|
engagementLines := splitTextByRune(cleanTextForPDF(data.DataPerformance.Engagement), 35)
|
||||||
pdf.MultiCell(&engagementRect, data.DataPerformance.Engagement)
|
for i, line := range engagementLines {
|
||||||
|
pdf.SetXY(200, 600+float64(i)*lineHeight)
|
||||||
|
pdf.Cell(nil, line)
|
||||||
|
}
|
||||||
|
|
||||||
// 整体总结及可优化建议 - 一行最多35个字
|
// 整体总结及可优化建议 - 一行最多35个字
|
||||||
pdf.SetXY(200, 676)
|
pdf.SetXY(200, 676)
|
||||||
summaryRect = gopdf.Rect{W: 350.0, H: lineHeight}
|
overallSummaryLines := splitTextByRune(cleanTextForPDF(data.OverallSummary), 35)
|
||||||
pdf.MultiCell(&summaryRect, data.OverallSummary)
|
for i, line := range overallSummaryLines {
|
||||||
|
pdf.SetXY(200, 676+float64(i)*lineHeight)
|
||||||
|
pdf.Cell(nil, line)
|
||||||
|
}
|
||||||
|
|
||||||
// 生成新的 PDF
|
// 生成新的 PDF
|
||||||
if err = pdf.WritePdf(outputPath); err != nil {
|
if err = pdf.WritePdf(outputPath); err != nil {
|
||||||
@ -322,6 +355,27 @@ func GenerateCompetitorReportPDF(templatePath, outputPath string, data Competito
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// splitTextByRune 将文本按指定字符数拆分成多行
|
||||||
|
// 按每行最大字符数拆分,中文、英文都按 1 个 rune 计
|
||||||
|
func splitTextByRune(text string, maxRunesPerLine int) []string {
|
||||||
|
if text == "" {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
runes := []rune(text)
|
||||||
|
if len(runes) <= maxRunesPerLine {
|
||||||
|
return []string{text}
|
||||||
|
}
|
||||||
|
var lines []string
|
||||||
|
for i := 0; i < len(runes); i += maxRunesPerLine {
|
||||||
|
end := i + maxRunesPerLine
|
||||||
|
if end > len(runes) {
|
||||||
|
end = len(runes)
|
||||||
|
}
|
||||||
|
lines = append(lines, string(runes[i:end]))
|
||||||
|
}
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
// wrapText 将文本按指定宽度换行(按字符数计算)
|
// wrapText 将文本按指定宽度换行(按字符数计算)
|
||||||
func wrapText(text string, maxLen int) []string {
|
func wrapText(text string, maxLen int) []string {
|
||||||
if text == "" {
|
if text == "" {
|
||||||
|
|||||||
123
pkg/utils/pdf_competitor_test.go
Normal file
123
pkg/utils/pdf_competitor_test.go
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// getProjectRoot 获取项目根目录
|
||||||
|
func getProjectRoot() string {
|
||||||
|
// 假设测试从项目根目录运行
|
||||||
|
dir, _ := os.Getwd()
|
||||||
|
// 向上查找 go.mod 确定项目根目录
|
||||||
|
for {
|
||||||
|
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
parent := filepath.Dir(dir)
|
||||||
|
if parent == dir {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
dir = parent
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGenerateCompetitorReportPDF 测试生成竞品报告PDF
|
||||||
|
func TestGenerateCompetitorReportPDF(t *testing.T) {
|
||||||
|
// 获取项目根目录
|
||||||
|
root := getProjectRoot()
|
||||||
|
fmt.Printf("项目根目录: %s\n", root)
|
||||||
|
|
||||||
|
// 准备测试数据
|
||||||
|
data := CompetitorReportData{
|
||||||
|
HighlightAnalysis: HighlightAnalysisData{
|
||||||
|
Summary: "本视频通过展示产品使用的真实场景,突出用户产品优势和痛点,内容详实且具有吸引力。",
|
||||||
|
Points: PointsData{
|
||||||
|
Theme: "标题简洁有力,突出核心卖点'省时省力',引发用户好奇心",
|
||||||
|
Narrative: "采用情景剧形式展示产品使用场景,剧情贴近生活,易引发共鸣",
|
||||||
|
Content: "通过前后对比展示产品效果,直观呈现产品价值",
|
||||||
|
Copywriting: "文案简洁明了,突出用户痛点解决方案,语气亲切自然",
|
||||||
|
Data: "点赞量10万+,评论5000+,分享2万+,数据表现优异",
|
||||||
|
Music: "背景音乐节奏轻快,与视频内容匹配度高,增强观看体验",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
DataPerformance: DataPerformanceData{
|
||||||
|
Views: "播放量突破500万,推荐流量占比60%,自然流量表现优秀",
|
||||||
|
Completion: "完播率45%,高于同类视频平均值(30%),前3秒吸引力强",
|
||||||
|
Engagement: "点赞率2%,评论率0.1%,分享率0.4%,互动数据表现优秀",
|
||||||
|
},
|
||||||
|
OverallSummary: "整体来看,该竞品视频在内容策划、表现形式和互动数据方面都表现优秀。优势在于:1)内容真实可信,通过实际使用场景展示产品效果;2)剧情设计合理,前3秒抓住用户注意力;3)文案简洁有力,直击用户痛点。建议优化方向:1)可以增加更多用户评价内容,增强可信度;2)适当增加福利引导,提高转化率;3)结尾可以增加引导关注话术,提升粉丝沉淀。",
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模板路径
|
||||||
|
templatePath := filepath.Join(root, "data", "竞品报告pdf模板.pdf")
|
||||||
|
// 输出路径
|
||||||
|
outputPath := filepath.Join(root, "data", "output", "竞品报告测试_multicell.pdf")
|
||||||
|
|
||||||
|
// 确保输出目录存在
|
||||||
|
outputDir := filepath.Dir(outputPath)
|
||||||
|
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||||
|
t.Errorf("创建输出目录失败: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用函数生成PDF
|
||||||
|
err := GenerateCompetitorReportPDF(templatePath, outputPath, data)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("生成竞品报告PDF失败: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("PDF生成成功: %s\n", outputPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGenerateCompetitorReportPDFImageOnly 测试仅图片的竞品报告PDF(无配乐和完播率)
|
||||||
|
func TestGenerateCompetitorReportPDFImageOnly(t *testing.T) {
|
||||||
|
// 获取项目根目录
|
||||||
|
root := getProjectRoot()
|
||||||
|
|
||||||
|
// 准备测试数据(仅图片,没有视频的配乐和完播率)
|
||||||
|
data := CompetitorReportData{
|
||||||
|
HighlightAnalysis: HighlightAnalysisData{
|
||||||
|
Summary: "该图文内容通过精美的视觉设计和精准的标签定位,成功吸引目标用户关注。",
|
||||||
|
Points: PointsData{
|
||||||
|
Theme: "标题设置悬念,引发用户点击欲望",
|
||||||
|
Narrative: "采用九宫格形式展示产品特点,视觉冲击力强",
|
||||||
|
Content: "内容排版清晰,重点突出,便于用户快速获取信息",
|
||||||
|
Copywriting: "文案简洁,配合表情符号增加趣味性",
|
||||||
|
Data: "收藏量5万+,评论1000+,分享8000+",
|
||||||
|
Music: "", // 图片无配乐
|
||||||
|
},
|
||||||
|
},
|
||||||
|
DataPerformance: DataPerformanceData{
|
||||||
|
Views: "曝光量100万+,点击率3%,表现良好",
|
||||||
|
Completion: "", // 图文无完播率
|
||||||
|
Engagement: "收藏率5%,评论率0.1%,分享率0.8%",
|
||||||
|
},
|
||||||
|
OverallSummary: "该图文内容整体表现优秀,特别是在视觉设计和内容排版方面。亮点:1)九宫格形式统一,视觉效果好;2)标签设置精准,触达目标用户;3)发布时间合理,获得更多曝光。优化建议:1)可以增加更多用户案例展示;2)适当加入互动话题,提高评论量。",
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模板路径
|
||||||
|
templatePath := filepath.Join(root, "data", "竞品报告pdf模板.pdf")
|
||||||
|
// 输出路径
|
||||||
|
outputPath := filepath.Join(root, "data", "output", "竞品报告测试_图文11.pdf")
|
||||||
|
|
||||||
|
// 确保输出目录存在
|
||||||
|
outputDir := filepath.Dir(outputPath)
|
||||||
|
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||||
|
t.Errorf("创建输出目录失败: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用函数生成PDF
|
||||||
|
err := GenerateCompetitorReportPDF(templatePath, outputPath, data)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("生成竞品报告PDF失败: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("PDF生成成功: %s\n", outputPath)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user