From b2b570003eab57b3e58d9bbec7c253c250cc964c Mon Sep 17 00:00:00 2001 From: cjy Date: Sat, 28 Feb 2026 09:16:23 +0800 Subject: [PATCH 01/38] =?UTF-8?q?feat:=20=E4=BF=AE=E6=94=B9=E7=94=9F?= =?UTF-8?q?=E6=88=90=E7=AB=9E=E5=93=81=E6=8A=A5=E5=91=8A=E7=9A=84=E6=8F=90?= =?UTF-8?q?=E7=A4=BA=E8=AF=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/service/ai/video_vl.go | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/pkg/service/ai/video_vl.go b/pkg/service/ai/video_vl.go index 1ef21af0..b159719b 100644 --- a/pkg/service/ai/video_vl.go +++ b/pkg/service/ai/video_vl.go @@ -143,8 +143,44 @@ func AICompetitorReport(ctx *gin.Context) { if needText { textChan = make(chan textResult, 1) go func() { + // 根据是否有视频来判断作品类型 + isVideo := len(req.Videos) > 0 + + // 根据作品类型设置差异化内容 + var extraPoint, extraData string + if isVideo { + extraPoint = "6. 配乐亮点:" + extraData = "2. 完播率表现:\n3. 点赞/分享/评论表现:" + } else { + extraData = "2. 点赞/分享/评论表现:" + } + // 构建文本生成提示词:理解内容 + 用户要求 - textPrompt := fmt.Sprintf("基于以下视频和图片的内容描述:\n%s\n\n请根据以下要求生成竞品报告:注意不要输出markdown格式来进行排版,请直接输出纯文本。只需要回复竞品报告的内容,其他无关的内容不要输出,输出的内容第一行不要标题,直接输出竞品报告的正文即可\n我的要求是:\n%s", vlContent, req.TextPrompt) + textPrompt := fmt.Sprintf(`基于以下视频和图片的内容描述: +%s + +请根据以下要求生成竞品报告。注意不要输出markdown格式来进行排版,请直接输出纯文本。只需要回复竞品报告的内容,其他无关的内容不要输出,输出的内容第一行不要标题,直接输出竞品报告的正文即可 + +我的要求是: +%s + +请严格按照以下模板输出: + +一、亮点表现分析 +[100字以内的概述] + +1. 标题亮点: +2. 题材亮点: +3. 内容亮点: +4. 文案亮点: +5. 数据亮点: +%s + +二、数据表现分析 +1. 浏览量表现: +%s + +三、整体总结及可优化建议`, vlContent, req.TextPrompt, extraPoint, extraData) chatReq, err := buildChatRequest(textPrompt, nil) if err != nil { From d2ee1c86b820000c714658330eacb9a99e75c4eb Mon Sep 17 00:00:00 2001 From: cjy Date: Sat, 28 Feb 2026 09:19:03 +0800 Subject: [PATCH 02/38] =?UTF-8?q?feat:=20=E4=BF=AE=E6=94=B9=E7=94=9F?= =?UTF-8?q?=E6=88=90=E7=AB=9E=E5=93=81=E6=8A=A5=E5=91=8A=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E7=9A=84=E6=8F=90=E7=A4=BA=E8=AF=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/service/ai/video_vl.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/service/ai/video_vl.go b/pkg/service/ai/video_vl.go index b159719b..3150b8ec 100644 --- a/pkg/service/ai/video_vl.go +++ b/pkg/service/ai/video_vl.go @@ -208,7 +208,7 @@ func AICompetitorReport(ctx *gin.Context) { imageChan = make(chan imageResult, 1) go func() { // 先请求聊天获取图片提示词 - imagePromptText := fmt.Sprintf("基于以下视频和图片的内容描述:\n%s\n\n请根据以下要求生成竞品报告图片的提示词:\n%s", vlContent, req.ImagePrompt) + imagePromptText := fmt.Sprintf("基于以下视频和图片的内容描述:\n%s\n\n请根据以下要求生成竞品报告图片的提示词:\n%s\n\n重要提示:生成的图片内容中不要包含任何文字,仅仅是根据内容生成一张配图即可", vlContent, req.ImagePrompt) chatReq, err := buildChatRequest(imagePromptText, nil) if err != nil { From cb1345a55ddcbbad401d1b63915b72cc387b1c3d Mon Sep 17 00:00:00 2001 From: cjy Date: Sat, 28 Feb 2026 14:03:29 +0800 Subject: [PATCH 03/38] =?UTF-8?q?feat:=E4=BF=AE=E6=94=B9=E7=94=9F=E6=88=90?= =?UTF-8?q?=E7=AB=9E=E5=93=81=E6=8A=A5=E5=91=8A=E7=9A=84prompts=EF=BC=8C?= =?UTF-8?q?=E8=AE=A9=E5=85=B6=E7=94=9F=E6=88=90json=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=E7=9A=84=E5=9B=9E=E5=A4=8D=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/service/ai/video_vl.go | 78 +++++++++++++++++++++++++++----------- 1 file changed, 55 insertions(+), 23 deletions(-) diff --git a/pkg/service/ai/video_vl.go b/pkg/service/ai/video_vl.go index 3150b8ec..9e4f4bca 100644 --- a/pkg/service/ai/video_vl.go +++ b/pkg/service/ai/video_vl.go @@ -146,41 +146,73 @@ func AICompetitorReport(ctx *gin.Context) { // 根据是否有视频来判断作品类型 isVideo := len(req.Videos) > 0 - // 根据作品类型设置差异化内容 - var extraPoint, extraData string + // 构建文本生成提示词:理解内容 + 用户要求(JSON格式) + var textPrompt string if isVideo { - extraPoint = "6. 配乐亮点:" - extraData = "2. 完播率表现:\n3. 点赞/分享/评论表现:" - } else { - extraData = "2. 点赞/分享/评论表现:" - } - - // 构建文本生成提示词:理解内容 + 用户要求 - textPrompt := fmt.Sprintf(`基于以下视频和图片的内容描述: + textPrompt = fmt.Sprintf(`基于以下视频和图片的内容描述: %s -请根据以下要求生成竞品报告。注意不要输出markdown格式来进行排版,请直接输出纯文本。只需要回复竞品报告的内容,其他无关的内容不要输出,输出的内容第一行不要标题,直接输出竞品报告的正文即可 +请根据以下要求生成竞品报告,输出严格的JSON格式,不要输出markdown格式,不要输出其他无关内容。 我的要求是: %s -请严格按照以下模板输出: +请严格按照以下JSON模板输出(注意不要修改key,只填写value): +{ + "highlight_analysis": { + "summary": "[78字以内的概述]", + "points": { + "theme": "[标题亮点,最多60字]", + "narrative": "[题材亮点,最多60字]", + "content": "[内容亮点,最多60字]", + "copywriting": "[文案亮点,最多60字]", + "data": "[数据亮点,最多60字]", + "music": "[配乐亮点,仅视频,最多60字]" + } + }, + "data_performance_analysis": { + "views": "[浏览量表现,最多60字]", + "completion_rate": "[完播率表现,仅视频,最多60字]", + "engagement": "[点赞/分享/评论表现,最多60字]" + }, + "overall_summary_and_optimization": "[整体总结及可优化建议,最多300字]" +} -一、亮点表现分析 -[100字以内的概述] - -1. 标题亮点: -2. 题材亮点: -3. 内容亮点: -4. 文案亮点: -5. 数据亮点: +重要约束: +1. 严禁输出 JSON 以外的任何字符(包括换行说明、注释、Markdown) +2. 若无法满足字数限制,请主动压缩内容,而不是省略字段`, vlContent, req.TextPrompt) + } else { + textPrompt = fmt.Sprintf(`基于以下视频和图片的内容描述: %s -二、数据表现分析 -1. 浏览量表现: +请根据以下要求生成竞品报告,输出严格的JSON格式,不要输出markdown格式,不要输出其他无关内容。 + +我的要求是: %s -三、整体总结及可优化建议`, vlContent, req.TextPrompt, extraPoint, extraData) +请严格按照以下JSON模板输出(注意不要修改key,只填写value): +{ + "highlight_analysis": { + "summary": "[78字以内的概述]", + "points": { + "theme": "[标题亮点,最多60字]", + "narrative": "[题材亮点,最多60字]", + "content": "[内容亮点,最多60字]", + "copywriting": "[文案亮点,最多60字]", + "data": "[数据亮点,最多60字]" + } + }, + "data_performance_analysis": { + "views": "[浏览量表现,最多60字]", + "engagement": "[点赞/分享/评论表现,最多60字]" + }, + "overall_summary_and_optimization": "[整体总结及可优化建议,最多300字]" +} + +重要约束: +1. 严禁输出 JSON 以外的任何字符(包括换行说明、注释、Markdown) +2. 若无法满足字数限制,请主动压缩内容,而不是省略字段`, vlContent, req.TextPrompt) + } chatReq, err := buildChatRequest(textPrompt, nil) if err != nil { From ce9edf513c934fc9148cbfc5eb909c116fa84e82 Mon Sep 17 00:00:00 2001 From: cjy Date: Sat, 28 Feb 2026 14:28:36 +0800 Subject: [PATCH 04/38] =?UTF-8?q?feat:=20ai=20=E7=94=9F=E6=88=90=E7=AB=9E?= =?UTF-8?q?=E5=93=81=E6=8A=A5=E5=91=8A=E5=A2=9E=E5=8A=A0=E4=B8=80=E4=B8=AA?= =?UTF-8?q?=E5=AD=97=E6=AE=B5json=EF=BC=8C=E7=94=A8=E6=9D=A5=E5=90=8E?= =?UTF-8?q?=E7=BB=AD=E7=94=9F=E6=88=90pdf=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/service/ai/video_vl.go | 99 +++++++++++++++++++++++++++++++++++++- 1 file changed, 98 insertions(+), 1 deletion(-) diff --git a/pkg/service/ai/video_vl.go b/pkg/service/ai/video_vl.go index 9e4f4bca..11140182 100644 --- a/pkg/service/ai/video_vl.go +++ b/pkg/service/ai/video_vl.go @@ -1,6 +1,7 @@ package ai import ( + "encoding/json" "errors" "fmt" "fonchain-fiee/pkg/common/qwen" @@ -74,6 +75,93 @@ type CompetitorReportRequest struct { type CompetitorReportResponse struct { ImageURL string `json:"image_url,omitempty"` // 生成的图片URL(1024*1024),非必须返回 Text string `json:"text,omitempty"` // 竞品报告文本内容,非必须返回 + JSON string `json:"json,omitempty"` // 竞品报告JSON格式,非必须返回 +} + +// CompetitorReportJSON AI返回的JSON结构 +type CompetitorReportJSON struct { + HighlightAnalysis HighlightAnalysis `json:"highlight_analysis"` + DataPerformance DataPerformance `json:"data_performance_analysis"` + OverallSummary string `json:"overall_summary_and_optimization"` +} + +type HighlightAnalysis struct { + Summary string `json:"summary"` + Points Points `json:"points"` +} + +type Points struct { + Theme string `json:"theme"` + Narrative string `json:"narrative"` + Content string `json:"content"` + Copywriting string `json:"copywriting"` + Data string `json:"data"` + Music string `json:"music,omitempty"` +} + +type DataPerformance struct { + Views string `json:"views"` + Completion string `json:"completion_rate,omitempty"` + Engagement string `json:"engagement"` +} + +// convertJSONToText 将 JSON 转换为纯文本格式 +func convertJSONToText(data CompetitorReportJSON, isVideo bool) string { + var sb strings.Builder + + // 一、亮点表现分析 + sb.WriteString("一、亮点表现分析\n") + sb.WriteString(data.HighlightAnalysis.Summary) + sb.WriteString("\n\n") + + sb.WriteString("1. 标题亮点:") + sb.WriteString(data.HighlightAnalysis.Points.Theme) + sb.WriteString("\n") + + sb.WriteString("2. 题材亮点:") + sb.WriteString(data.HighlightAnalysis.Points.Narrative) + sb.WriteString("\n") + + sb.WriteString("3. 内容亮点:") + sb.WriteString(data.HighlightAnalysis.Points.Content) + sb.WriteString("\n") + + sb.WriteString("4. 文案亮点:") + sb.WriteString(data.HighlightAnalysis.Points.Copywriting) + sb.WriteString("\n") + + sb.WriteString("5. 数据亮点:") + sb.WriteString(data.HighlightAnalysis.Points.Data) + sb.WriteString("\n") + + if isVideo && data.HighlightAnalysis.Points.Music != "" { + sb.WriteString("6. 配乐亮点:") + sb.WriteString(data.HighlightAnalysis.Points.Music) + sb.WriteString("\n") + } + + // 二、数据表现分析 + sb.WriteString("\n二、数据表现分析\n") + sb.WriteString("1. 浏览量表现:") + sb.WriteString(data.DataPerformance.Views) + sb.WriteString("\n") + + if isVideo && data.DataPerformance.Completion != "" { + sb.WriteString("2. 完播率表现:") + sb.WriteString(data.DataPerformance.Completion) + sb.WriteString("\n") + sb.WriteString("3. 点赞/分享/评论表现:") + } else { + sb.WriteString("2. 点赞/分享/评论表现:") + } + sb.WriteString(data.DataPerformance.Engagement) + sb.WriteString("\n") + + // 三、整体总结及可优化建议 + sb.WriteString("\n三、整体总结及可优化建议\n") + sb.WriteString(data.OverallSummary) + + return sb.String() } // AICompetitorReport 生成竞品报告接口 @@ -335,7 +423,16 @@ func AICompetitorReport(ctx *gin.Context) { // 返回结果(只返回实际生成的内容) result := CompetitorReportResponse{} if needText { - result.Text = textRes.text + result.JSON = textRes.text // JSON 字段直接返回 AI 生成的 JSON + + // 将 JSON 解析并转换为纯文本 + var jsonData CompetitorReportJSON + if err := json.Unmarshal([]byte(textRes.text), &jsonData); err != nil { + // 如果解析失败,回退使用原始文本 + result.Text = textRes.text + } else { + result.Text = convertJSONToText(jsonData, len(req.Videos) > 0) + } } if needImage { result.ImageURL = imageRes.imageURL From 016f2c2459d83bef0562b2753222c8470cc3cf44 Mon Sep 17 00:00:00 2001 From: cjy Date: Sat, 28 Feb 2026 14:58:40 +0800 Subject: [PATCH 05/38] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=B8=80=E4=BA=9B?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E6=89=93=E5=8D=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/service/ai/video_vl.go | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/pkg/service/ai/video_vl.go b/pkg/service/ai/video_vl.go index 11140182..b9b12ca0 100644 --- a/pkg/service/ai/video_vl.go +++ b/pkg/service/ai/video_vl.go @@ -319,7 +319,13 @@ func AICompetitorReport(ctx *gin.Context) { return } - textChan <- textResult{text: chatResp.Choices[0].Message.Content} + // 打印 AI 返回的原始内容(用于调试) + aiText := chatResp.Choices[0].Message.Content + fmt.Println("========== AI 返回的原始内容 ==========") + fmt.Println(aiText) + fmt.Println("=========================================") + + textChan <- textResult{text: aiText} }() } @@ -426,11 +432,21 @@ func AICompetitorReport(ctx *gin.Context) { result.JSON = textRes.text // JSON 字段直接返回 AI 生成的 JSON // 将 JSON 解析并转换为纯文本 + fmt.Println("========== 开始解析 JSON ==========") + fmt.Println("原始内容是否以 { 开头:", strings.HasPrefix(strings.TrimSpace(textRes.text), "{")) + fmt.Println("原始内容前100字符:", strings.TrimSpace(textRes.text)[:min(100, len(strings.TrimSpace(textRes.text)))]) + var jsonData CompetitorReportJSON if err := json.Unmarshal([]byte(textRes.text), &jsonData); err != nil { // 如果解析失败,回退使用原始文本 + fmt.Println("========== JSON 解析失败 ==========") + fmt.Println("解析错误:", err) + fmt.Println("===================================") result.Text = textRes.text } else { + fmt.Println("========== JSON 解析成功 ==========") + fmt.Println("Summary:", jsonData.HighlightAnalysis.Summary) + fmt.Println("==================================") result.Text = convertJSONToText(jsonData, len(req.Videos) > 0) } } From 75ba332397d8aac2c381fe14447b61b839a2ab9f Mon Sep 17 00:00:00 2001 From: cjy Date: Sat, 28 Feb 2026 15:25:58 +0800 Subject: [PATCH 06/38] =?UTF-8?q?fix=EF=BC=9A=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E6=8F=90=E7=A4=BA=E8=AF=8D=EF=BC=8C=E9=81=BF=E5=85=8Dai?= =?UTF-8?q?=E7=94=9F=E6=88=90=E7=9A=84=E7=BB=93=E6=9E=9C=E4=B8=8D=E6=98=AF?= =?UTF-8?q?json=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/service/ai/video_vl.go | 66 +++++++------------------------------- 1 file changed, 12 insertions(+), 54 deletions(-) diff --git a/pkg/service/ai/video_vl.go b/pkg/service/ai/video_vl.go index b9b12ca0..1a945855 100644 --- a/pkg/service/ai/video_vl.go +++ b/pkg/service/ai/video_vl.go @@ -237,69 +237,27 @@ func AICompetitorReport(ctx *gin.Context) { // 构建文本生成提示词:理解内容 + 用户要求(JSON格式) var textPrompt string if isVideo { - textPrompt = fmt.Sprintf(`基于以下视频和图片的内容描述: + textPrompt = fmt.Sprintf(`你必须严格输出以下JSON格式,不要输出任何其他内容。输出必须以 { 开头并以 } 结束。 + +基于以下视频和图片的内容描述: %s -请根据以下要求生成竞品报告,输出严格的JSON格式,不要输出markdown格式,不要输出其他无关内容。 - -我的要求是: +用户要求(仅作为内容参考,不会改变JSON结构): %s -请严格按照以下JSON模板输出(注意不要修改key,只填写value): -{ - "highlight_analysis": { - "summary": "[78字以内的概述]", - "points": { - "theme": "[标题亮点,最多60字]", - "narrative": "[题材亮点,最多60字]", - "content": "[内容亮点,最多60字]", - "copywriting": "[文案亮点,最多60字]", - "data": "[数据亮点,最多60字]", - "music": "[配乐亮点,仅视频,最多60字]" - } - }, - "data_performance_analysis": { - "views": "[浏览量表现,最多60字]", - "completion_rate": "[完播率表现,仅视频,最多60字]", - "engagement": "[点赞/分享/评论表现,最多60字]" - }, - "overall_summary_and_optimization": "[整体总结及可优化建议,最多300字]" -} - -重要约束: -1. 严禁输出 JSON 以外的任何字符(包括换行说明、注释、Markdown) -2. 若无法满足字数限制,请主动压缩内容,而不是省略字段`, vlContent, req.TextPrompt) +JSON结构是固定的,请将内容填充到对应的value中,禁止修改key,禁止添加额外字段,禁止输出任何说明文字: +{"highlight_analysis":{"summary":"[78字以内的概述]","points":{"theme":"[标题亮点,最多60字]","narrative":"[题材亮点,最多60字]","content":"[内容亮点,最多60字]","copywriting":"[文案亮点,最多60字]","data":"[数据亮点,最多60字]","music":"[配乐亮点,仅视频,最多60字]"}},"data_performance_analysis":{"views":"[浏览量表现,最多60字]","completion_rate":"[完播率表现,仅视频,最多60字]","engagement":"[点赞/分享/评论表现,最多60字]"},"overall_summary_and_optimization":"[整体总结及可优化建议,最多300字]"}`, vlContent, req.TextPrompt) } else { - textPrompt = fmt.Sprintf(`基于以下视频和图片的内容描述: + textPrompt = fmt.Sprintf(`你必须严格输出以下JSON格式,不要输出任何其他内容。输出必须以 { 开头并以 } 结束。 + +基于以下视频和图片的内容描述: %s -请根据以下要求生成竞品报告,输出严格的JSON格式,不要输出markdown格式,不要输出其他无关内容。 - -我的要求是: +用户要求(仅作为内容参考,不会改变JSON结构): %s -请严格按照以下JSON模板输出(注意不要修改key,只填写value): -{ - "highlight_analysis": { - "summary": "[78字以内的概述]", - "points": { - "theme": "[标题亮点,最多60字]", - "narrative": "[题材亮点,最多60字]", - "content": "[内容亮点,最多60字]", - "copywriting": "[文案亮点,最多60字]", - "data": "[数据亮点,最多60字]" - } - }, - "data_performance_analysis": { - "views": "[浏览量表现,最多60字]", - "engagement": "[点赞/分享/评论表现,最多60字]" - }, - "overall_summary_and_optimization": "[整体总结及可优化建议,最多300字]" -} - -重要约束: -1. 严禁输出 JSON 以外的任何字符(包括换行说明、注释、Markdown) -2. 若无法满足字数限制,请主动压缩内容,而不是省略字段`, vlContent, req.TextPrompt) +JSON结构是固定的,请将内容填充到对应的value中,禁止修改key,禁止添加额外字段,禁止输出任何说明文字: +{"highlight_analysis":{"summary":"[78字以内的概述]","points":{"theme":"[标题亮点,最多60字]","narrative":"[题材亮点,最多60字]","content":"[内容亮点,最多60字]","copywriting":"[文案亮点,最多60字]","data":"[数据亮点,最多60字]"}},"data_performance_analysis":{"views":"[浏览量表现,最多60字]","engagement":"[点赞/分享/评论表现,最多60字]"},"overall_summary_and_optimization":"[整体总结及可优化建议,最多300字]"}`, vlContent, req.TextPrompt) } chatReq, err := buildChatRequest(textPrompt, nil) From 264114fce8a59d4686ec29a5842a4507f9dd3169 Mon Sep 17 00:00:00 2001 From: cjy Date: Mon, 2 Mar 2026 09:20:55 +0800 Subject: [PATCH 07/38] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E7=AB=9E?= =?UTF-8?q?=E5=93=81=E6=8A=A5=E5=91=8Apdf=E6=A8=A1=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/竞品报告pdf模板.pdf | Bin 0 -> 68612 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 data/竞品报告pdf模板.pdf diff --git a/data/竞品报告pdf模板.pdf b/data/竞品报告pdf模板.pdf new file mode 100644 index 0000000000000000000000000000000000000000..c31daaceae4622e78bff9b3fce42a8eff283360f GIT binary patch literal 68612 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 Date: Mon, 2 Mar 2026 11:08:31 +0800 Subject: [PATCH 08/38] =?UTF-8?q?=E6=9A=82=E6=97=B6=E7=9A=84=E6=96=B9?= =?UTF-8?q?=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/utils/pdf.go | 177 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) diff --git a/pkg/utils/pdf.go b/pkg/utils/pdf.go index 0339b170..045cb7ff 100644 --- a/pkg/utils/pdf.go +++ b/pkg/utils/pdf.go @@ -11,6 +11,8 @@ import ( "unicode" "github.com/phpdave11/gofpdf" + "github.com/signintech/gopdf" + "go.uber.org/zap" ) // cleanTextForPDF 清理文本,移除PDF不支持的字符(如emoji) @@ -172,3 +174,178 @@ func GeneratePDF(text, imageURL, outputPath, fontPath string) error { return nil } + +// CompetitorReportData 竞品报告数据 +type CompetitorReportData struct { + HighlightAnalysis HighlightAnalysisData `json:"highlight_analysis"` + DataPerformance DataPerformanceData `json:"data_performance_analysis"` + OverallSummary string `json:"overall_summary_and_optimization"` +} + +type HighlightAnalysisData struct { + Summary string `json:"summary"` + Points PointsData `json:"points"` +} + +type PointsData struct { + Theme string `json:"theme"` + Narrative string `json:"narrative"` + Content string `json:"content"` + Copywriting string `json:"copywriting"` + Data string `json:"data"` + Music string `json:"music,omitempty"` +} + +type DataPerformanceData struct { + Views string `json:"views"` + Completion string `json:"completion_rate,omitempty"` + Engagement string `json:"engagement"` +} + +// GenerateCompetitorReportPDF 生成竞品报告PDF +// 参数: +// - templatePath: 模板文件路径 +// - outputPath: 输出PDF路径 +// - data: 竞品报告数据 +// +// 返回: 错误信息 +func GenerateCompetitorReportPDF(templatePath, outputPath string, data CompetitorReportData) error { + fmt.Println("================================templatePath:", templatePath) + fmt.Println("================================outputPath:", outputPath) + + pdf := gopdf.GoPdf{} + pdf.Start(gopdf.Config{PageSize: *gopdf.PageSizeA4}) + + // 导入模板文件中的页面 + err := pdf.ImportPagesFromSource(templatePath, "/MediaBox") + if err != nil { + return fmt.Errorf("无法导入页面: %v", err) + } + + // 获取模板文件的总页数 + totalPages := pdf.GetNumberOfPages() + fmt.Printf("模板文件的总页数: %d\n", totalPages) + + // 根据模板路径推断字体路径(假设字体文件和模板在同一目录或data目录下) + dir := filepath.Dir(templatePath) + fontPath := filepath.Join(dir, "simfang.ttf") + if _, err := os.Stat(fontPath); err != nil { + // 尝试使用项目根目录下的data目录 + fontPath = filepath.Join("data", "simfang.ttf") + } + fmt.Printf("字体文件路径: %s\n", fontPath) + + // 加载中文字体 + ttfErr := pdf.AddTTFFont("simfang", fontPath) + if ttfErr != nil { + zap.L().Error("加载字体失败", zap.String("fontPath", fontPath), zap.Error(ttfErr)) + return fmt.Errorf("加载中文字体失败: %v", ttfErr) + } + + // 设置字体和字号 + err = pdf.SetFont("simfang", "", 10) + if err != nil { + return fmt.Errorf("设置字体失败: %v", err) + } + + // 行高15pt + lineHeight := 15.0 + + pdf.SetPage(1) + + // 概述 - 使用MultiCell自动换行,一行最多35个字 + pdf.SetXY(200, 104) + summaryRect := gopdf.Rect{W: 350.0, H: lineHeight} + pdf.MultiCell(&summaryRect, data.HighlightAnalysis.Summary) + + // 标题亮点 - 一行最多9个字 + pdf.SetXY(200, 184) + themeRect := gopdf.Rect{W: 120.0, H: lineHeight} + pdf.MultiCell(&themeRect, data.HighlightAnalysis.Points.Theme) + + // 题材亮点 - 一行最多9个字 + pdf.SetXY(330, 184) + narrativeRect := gopdf.Rect{W: 120.0, H: lineHeight} + pdf.MultiCell(&narrativeRect, data.HighlightAnalysis.Points.Narrative) + + // 内容亮点 - 一行最多9个字 + pdf.SetXY(460, 184) + contentRect := gopdf.Rect{W: 120.0, H: lineHeight} + pdf.MultiCell(&contentRect, data.HighlightAnalysis.Points.Content) + + // 文案亮点 - 一行最多9个字 + pdf.SetXY(200, 323) + copywritingRect := gopdf.Rect{W: 120.0, H: lineHeight} + pdf.MultiCell(©writingRect, data.HighlightAnalysis.Points.Copywriting) + + // 数据亮点 - 一行最多9个字 + pdf.SetXY(330, 323) + dataRect := gopdf.Rect{W: 120.0, H: lineHeight} + pdf.MultiCell(&dataRect, data.HighlightAnalysis.Points.Data) + + // 配乐亮点(仅视频) - 一行最多9个字 + if data.HighlightAnalysis.Points.Music != "" { + pdf.SetXY(460, 323) + musicRect := gopdf.Rect{W: 120.0, H: lineHeight} + pdf.MultiCell(&musicRect, data.HighlightAnalysis.Points.Music) + } + + // 浏览量 - 一行最多35个字 + pdf.SetXY(200, 474) + viewsRect := gopdf.Rect{W: 350.0, H: lineHeight} + pdf.MultiCell(&viewsRect, data.DataPerformance.Views) + + // 完播率 - 一行最多35个字 + // 始终显示在固定位置,有内容时填充内容 + pdf.SetXY(200, 539) + if data.DataPerformance.Completion != "" { + completionRect := gopdf.Rect{W: 350.0, H: lineHeight} + pdf.MultiCell(&completionRect, data.DataPerformance.Completion) + } + + // 点赞/分享/评论 - 一行最多35个字 + // 始终固定在完播率下面的位置 + pdf.SetXY(200, 600) + engagementRect := gopdf.Rect{W: 350.0, H: lineHeight} + pdf.MultiCell(&engagementRect, data.DataPerformance.Engagement) + + // 整体总结及可优化建议 - 一行最多35个字 + pdf.SetXY(200, 676) + summaryRect = gopdf.Rect{W: 350.0, H: lineHeight} + pdf.MultiCell(&summaryRect, data.OverallSummary) + + // 生成新的 PDF + if err = pdf.WritePdf(outputPath); err != nil { + return fmt.Errorf("error writing final PDF: %v", err) + } + + return nil +} + +// wrapText 将文本按指定宽度换行(按字符数计算) +func wrapText(text string, maxLen int) []string { + if text == "" { + return []string{} + } + + var lines []string + runes := []rune(text) + currentLine := "" + + for _, r := range runes { + // 如果当前行字符数达到最大限度,换行 + if len(currentLine) >= maxLen { + lines = append(lines, currentLine) + currentLine = string(r) + } else { + currentLine += string(r) + } + } + + // 添加最后一行 + if len(currentLine) > 0 { + lines = append(lines, currentLine) + } + + return lines +} From e1e453322ab888230c90744d77a859db1615cf4e Mon Sep 17 00:00:00 2001 From: cjy Date: Mon, 2 Mar 2026 13:07:33 +0800 Subject: [PATCH 09/38] =?UTF-8?q?feat=EF=BC=9A=E4=BC=98=E5=8C=96=E7=AB=9E?= =?UTF-8?q?=E5=93=81=E6=8A=A5=E5=91=8Apdf=E7=94=9F=E6=88=90=E6=95=88?= =?UTF-8?q?=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + pkg/utils/pdf.go | 100 +++++++++++++++++++------ pkg/utils/pdf_competitor_test.go | 123 +++++++++++++++++++++++++++++++ 3 files changed, 202 insertions(+), 23 deletions(-) create mode 100644 pkg/utils/pdf_competitor_test.go diff --git a/.gitignore b/.gitignore index 4ec01dac..55306709 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,5 @@ /cmd/logs/*.log /cmd/runtime/log/*.log /build/* +CLAUDE.md +.claude/settings.local.json diff --git a/pkg/utils/pdf.go b/pkg/utils/pdf.go index 045cb7ff..8d6bce1d 100644 --- a/pkg/utils/pdf.go +++ b/pkg/utils/pdf.go @@ -253,66 +253,99 @@ func GenerateCompetitorReportPDF(templatePath, outputPath string, data Competito pdf.SetPage(1) - // 概述 - 使用MultiCell自动换行,一行最多35个字 + // 概述 - 使用逐行写入,一行最多35个字 pdf.SetXY(200, 104) - summaryRect := gopdf.Rect{W: 350.0, H: lineHeight} - pdf.MultiCell(&summaryRect, data.HighlightAnalysis.Summary) + summaryLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Summary), 35) + for i, line := range summaryLines { + pdf.SetXY(200, 104+float64(i)*lineHeight) + pdf.Cell(nil, line) + } // 标题亮点 - 一行最多9个字 pdf.SetXY(200, 184) - themeRect := gopdf.Rect{W: 120.0, H: lineHeight} - pdf.MultiCell(&themeRect, data.HighlightAnalysis.Points.Theme) + themeLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Theme), 9) + for i, line := range themeLines { + pdf.SetXY(200, 184+float64(i)*lineHeight) + pdf.Cell(nil, line) + } // 题材亮点 - 一行最多9个字 pdf.SetXY(330, 184) - narrativeRect := gopdf.Rect{W: 120.0, H: lineHeight} - pdf.MultiCell(&narrativeRect, data.HighlightAnalysis.Points.Narrative) + narrativeLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Narrative), 9) + for i, line := range narrativeLines { + pdf.SetXY(330, 184+float64(i)*lineHeight) + pdf.Cell(nil, line) + } // 内容亮点 - 一行最多9个字 pdf.SetXY(460, 184) - contentRect := gopdf.Rect{W: 120.0, H: lineHeight} - pdf.MultiCell(&contentRect, data.HighlightAnalysis.Points.Content) + contentLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Content), 9) + for i, line := range contentLines { + pdf.SetXY(460, 184+float64(i)*lineHeight) + pdf.Cell(nil, line) + } // 文案亮点 - 一行最多9个字 pdf.SetXY(200, 323) - copywritingRect := gopdf.Rect{W: 120.0, H: lineHeight} - pdf.MultiCell(©writingRect, data.HighlightAnalysis.Points.Copywriting) + copywritingLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Copywriting), 9) + for i, line := range copywritingLines { + pdf.SetXY(200, 323+float64(i)*lineHeight) + pdf.Cell(nil, line) + } // 数据亮点 - 一行最多9个字 pdf.SetXY(330, 323) - dataRect := gopdf.Rect{W: 120.0, H: lineHeight} - pdf.MultiCell(&dataRect, data.HighlightAnalysis.Points.Data) + dataLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Data), 9) + for i, line := range dataLines { + pdf.SetXY(330, 323+float64(i)*lineHeight) + pdf.Cell(nil, line) + } // 配乐亮点(仅视频) - 一行最多9个字 if data.HighlightAnalysis.Points.Music != "" { pdf.SetXY(460, 323) - musicRect := gopdf.Rect{W: 120.0, H: lineHeight} - pdf.MultiCell(&musicRect, data.HighlightAnalysis.Points.Music) + musicLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Music), 9) + for i, line := range musicLines { + pdf.SetXY(460, 323+float64(i)*lineHeight) + pdf.Cell(nil, line) + } } // 浏览量 - 一行最多35个字 pdf.SetXY(200, 474) - viewsRect := gopdf.Rect{W: 350.0, H: lineHeight} - pdf.MultiCell(&viewsRect, data.DataPerformance.Views) + viewsLines := splitTextByRune(cleanTextForPDF(data.DataPerformance.Views), 35) + for i, line := range viewsLines { + pdf.SetXY(200, 474+float64(i)*lineHeight) + pdf.Cell(nil, line) + } // 完播率 - 一行最多35个字 // 始终显示在固定位置,有内容时填充内容 pdf.SetXY(200, 539) if data.DataPerformance.Completion != "" { - completionRect := gopdf.Rect{W: 350.0, H: lineHeight} - pdf.MultiCell(&completionRect, data.DataPerformance.Completion) + completionLines := splitTextByRune(cleanTextForPDF(data.DataPerformance.Completion), 35) + for i, line := range completionLines { + pdf.SetXY(200, 539+float64(i)*lineHeight) + pdf.Cell(nil, line) + } } // 点赞/分享/评论 - 一行最多35个字 // 始终固定在完播率下面的位置 pdf.SetXY(200, 600) - engagementRect := gopdf.Rect{W: 350.0, H: lineHeight} - pdf.MultiCell(&engagementRect, data.DataPerformance.Engagement) + engagementLines := splitTextByRune(cleanTextForPDF(data.DataPerformance.Engagement), 35) + for i, line := range engagementLines { + pdf.SetXY(200, 600+float64(i)*lineHeight) + pdf.Cell(nil, line) + } // 整体总结及可优化建议 - 一行最多35个字 pdf.SetXY(200, 676) - summaryRect = gopdf.Rect{W: 350.0, H: lineHeight} - pdf.MultiCell(&summaryRect, data.OverallSummary) + overallSummaryLines := splitTextByRune(cleanTextForPDF(data.OverallSummary), 35) + for i, line := range overallSummaryLines { + pdf.SetXY(200, 676+float64(i)*lineHeight) + pdf.Cell(nil, line) + } // 生成新的 PDF if err = pdf.WritePdf(outputPath); err != nil { @@ -322,6 +355,27 @@ func GenerateCompetitorReportPDF(templatePath, outputPath string, data Competito 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 将文本按指定宽度换行(按字符数计算) func wrapText(text string, maxLen int) []string { if text == "" { diff --git a/pkg/utils/pdf_competitor_test.go b/pkg/utils/pdf_competitor_test.go new file mode 100644 index 00000000..e507dfb5 --- /dev/null +++ b/pkg/utils/pdf_competitor_test.go @@ -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) +} From 667139a6107b0bce497c7a5fc0446c27e6346b89 Mon Sep 17 00:00:00 2001 From: cjy Date: Mon, 2 Mar 2026 13:09:31 +0800 Subject: [PATCH 10/38] go mod tidy --- go.mod | 1 - go.sum | 2 -- 2 files changed, 3 deletions(-) diff --git a/go.mod b/go.mod index efe5b770..3beeb778 100644 --- a/go.mod +++ b/go.mod @@ -106,7 +106,6 @@ require ( github.com/BurntSushi/toml v1.2.1 github.com/PuerkitoBio/goquery v1.8.1 github.com/disintegration/imaging v1.6.2 - github.com/duke-git/lancet/v2 v2.3.8 github.com/envoyproxy/protoc-gen-validate v0.1.0 github.com/fonchain/utils/security v0.0.0-00010101000000-000000000000 github.com/fonchain/utils/voice v0.0.0-00010101000000-000000000000 diff --git a/go.sum b/go.sum index 58aae838..9fa53579 100644 --- a/go.sum +++ b/go.sum @@ -258,8 +258,6 @@ github.com/dubbogo/net v0.0.4/go.mod h1:1CGOnM7X3he+qgGNqjeADuE5vKZQx/eMSeUkpU3u github.com/dubbogo/triple v1.0.9/go.mod h1:1t9me4j4CTvNDcsMZy6/OGarbRyAUSY0tFXGXHCp7Iw= github.com/dubbogo/triple v1.1.8 h1:yE+J3W1NTZCEPa1FoX+VWZH6UF1c0+A2MGfERlU2zbI= github.com/dubbogo/triple v1.1.8/go.mod h1:9pgEahtmsY/avYJp3dzUQE8CMMVe1NtGBmUhfICKLJk= -github.com/duke-git/lancet/v2 v2.3.8 h1:dlkqn6Nj2LRWFuObNxttkMHxrFeaV6T26JR8jbEVbPg= -github.com/duke-git/lancet/v2 v2.3.8/go.mod h1:zGa2R4xswg6EG9I6WnyubDbFO/+A/RROxIbXcwryTsc= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= From f751b37e66f53c305a784a2ed53cacd6eb4c9113 Mon Sep 17 00:00:00 2001 From: cjy Date: Mon, 2 Mar 2026 13:32:29 +0800 Subject: [PATCH 11/38] =?UTF-8?q?feat:=20=E7=AB=9E=E5=93=81=E6=8A=A5?= =?UTF-8?q?=E5=91=8A=E6=94=AF=E6=8C=81=E7=94=9F=E6=88=90=E5=B8=A6=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E7=9A=84pdf=EF=BC=8C=E5=B9=B6=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/utils/pdf.go | 278 +++++++++++++++++++++---------- pkg/utils/pdf_competitor_test.go | 52 ++++++ 2 files changed, 238 insertions(+), 92 deletions(-) diff --git a/pkg/utils/pdf.go b/pkg/utils/pdf.go index 8d6bce1d..47618d12 100644 --- a/pkg/utils/pdf.go +++ b/pkg/utils/pdf.go @@ -3,6 +3,7 @@ package utils import ( "errors" "fmt" + "image" "io" "net/http" "net/url" @@ -180,6 +181,7 @@ type CompetitorReportData struct { HighlightAnalysis HighlightAnalysisData `json:"highlight_analysis"` DataPerformance DataPerformanceData `json:"data_performance_analysis"` OverallSummary string `json:"overall_summary_and_optimization"` + ImageURL string `json:"image_url"` // 图片URL,如果有图片则生成单独一页PDF } type HighlightAnalysisData struct { @@ -204,7 +206,7 @@ type DataPerformanceData struct { // GenerateCompetitorReportPDF 生成竞品报告PDF // 参数: -// - templatePath: 模板文件路径 +// - templatePath: 模板文件路径(保留参数以兼容现有调用,传空则不使用模板) // - outputPath: 输出PDF路径 // - data: 竞品报告数据 // @@ -216,21 +218,27 @@ func GenerateCompetitorReportPDF(templatePath, outputPath string, data Competito pdf := gopdf.GoPdf{} pdf.Start(gopdf.Config{PageSize: *gopdf.PageSizeA4}) - // 导入模板文件中的页面 - err := pdf.ImportPagesFromSource(templatePath, "/MediaBox") - if err != nil { - return fmt.Errorf("无法导入页面: %v", err) + // 如果有模板路径,则导入模板 + if templatePath != "" { + err := pdf.ImportPagesFromSource(templatePath, "/MediaBox") + if err != nil { + return fmt.Errorf("无法导入页面: %v", err) + } } - // 获取模板文件的总页数 + // 获取模板文件的总页数(如果有模板) totalPages := pdf.GetNumberOfPages() fmt.Printf("模板文件的总页数: %d\n", totalPages) - // 根据模板路径推断字体路径(假设字体文件和模板在同一目录或data目录下) - dir := filepath.Dir(templatePath) - fontPath := filepath.Join(dir, "simfang.ttf") - if _, err := os.Stat(fontPath); err != nil { - // 尝试使用项目根目录下的data目录 + // 确定字体路径 + var fontPath string + if templatePath != "" { + dir := filepath.Dir(templatePath) + fontPath = filepath.Join(dir, "simfang.ttf") + if _, err := os.Stat(fontPath); err != nil { + fontPath = filepath.Join("data", "simfang.ttf") + } + } else { fontPath = filepath.Join("data", "simfang.ttf") } fmt.Printf("字体文件路径: %s\n", fontPath) @@ -243,7 +251,7 @@ func GenerateCompetitorReportPDF(templatePath, outputPath string, data Competito } // 设置字体和字号 - err = pdf.SetFont("simfang", "", 10) + err := pdf.SetFont("simfang", "", 10) if err != nil { return fmt.Errorf("设置字体失败: %v", err) } @@ -251,104 +259,190 @@ func GenerateCompetitorReportPDF(templatePath, outputPath string, data Competito // 行高15pt lineHeight := 15.0 - pdf.SetPage(1) + // 如果有内容要写入,确保在第一页 + if totalPages > 0 || (data.HighlightAnalysis.Summary != "" || data.OverallSummary != "") { + pdf.SetPage(1) - // 概述 - 使用逐行写入,一行最多35个字 - pdf.SetXY(200, 104) - summaryLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Summary), 35) - for i, line := range summaryLines { - pdf.SetXY(200, 104+float64(i)*lineHeight) - pdf.Cell(nil, line) - } + // 概述 - 使用逐行写入,一行最多35个字 + pdf.SetXY(200, 104) + summaryLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Summary), 35) + for i, line := range summaryLines { + pdf.SetXY(200, 104+float64(i)*lineHeight) + pdf.Cell(nil, line) + } - // 标题亮点 - 一行最多9个字 - pdf.SetXY(200, 184) - themeLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Theme), 9) - for i, line := range themeLines { - pdf.SetXY(200, 184+float64(i)*lineHeight) - pdf.Cell(nil, line) - } + // 标题亮点 - 一行最多9个字 + pdf.SetXY(200, 184) + themeLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Theme), 9) + for i, line := range themeLines { + pdf.SetXY(200, 184+float64(i)*lineHeight) + pdf.Cell(nil, line) + } - // 题材亮点 - 一行最多9个字 - pdf.SetXY(330, 184) - narrativeLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Narrative), 9) - for i, line := range narrativeLines { - pdf.SetXY(330, 184+float64(i)*lineHeight) - pdf.Cell(nil, line) - } + // 题材亮点 - 一行最多9个字 + pdf.SetXY(330, 184) + narrativeLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Narrative), 9) + for i, line := range narrativeLines { + pdf.SetXY(330, 184+float64(i)*lineHeight) + pdf.Cell(nil, line) + } - // 内容亮点 - 一行最多9个字 - pdf.SetXY(460, 184) - contentLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Content), 9) - for i, line := range contentLines { - pdf.SetXY(460, 184+float64(i)*lineHeight) - pdf.Cell(nil, line) - } + // 内容亮点 - 一行最多9个字 + pdf.SetXY(460, 184) + contentLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Content), 9) + for i, line := range contentLines { + pdf.SetXY(460, 184+float64(i)*lineHeight) + pdf.Cell(nil, line) + } - // 文案亮点 - 一行最多9个字 - pdf.SetXY(200, 323) - copywritingLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Copywriting), 9) - for i, line := range copywritingLines { - pdf.SetXY(200, 323+float64(i)*lineHeight) - pdf.Cell(nil, line) - } + // 文案亮点 - 一行最多9个字 + pdf.SetXY(200, 323) + copywritingLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Copywriting), 9) + for i, line := range copywritingLines { + pdf.SetXY(200, 323+float64(i)*lineHeight) + pdf.Cell(nil, line) + } - // 数据亮点 - 一行最多9个字 - pdf.SetXY(330, 323) - dataLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Data), 9) - for i, line := range dataLines { - pdf.SetXY(330, 323+float64(i)*lineHeight) - pdf.Cell(nil, line) - } + // 数据亮点 - 一行最多9个字 + pdf.SetXY(330, 323) + dataLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Data), 9) + for i, line := range dataLines { + pdf.SetXY(330, 323+float64(i)*lineHeight) + pdf.Cell(nil, line) + } - // 配乐亮点(仅视频) - 一行最多9个字 - if data.HighlightAnalysis.Points.Music != "" { - pdf.SetXY(460, 323) - musicLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Music), 9) - for i, line := range musicLines { - pdf.SetXY(460, 323+float64(i)*lineHeight) + // 配乐亮点(仅视频) - 一行最多9个字 + if data.HighlightAnalysis.Points.Music != "" { + pdf.SetXY(460, 323) + musicLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Music), 9) + for i, line := range musicLines { + pdf.SetXY(460, 323+float64(i)*lineHeight) + pdf.Cell(nil, line) + } + } + + // 浏览量 - 一行最多35个字 + pdf.SetXY(200, 474) + viewsLines := splitTextByRune(cleanTextForPDF(data.DataPerformance.Views), 35) + for i, line := range viewsLines { + pdf.SetXY(200, 474+float64(i)*lineHeight) + pdf.Cell(nil, line) + } + + // 完播率 - 一行最多35个字 + pdf.SetXY(200, 539) + if data.DataPerformance.Completion != "" { + completionLines := splitTextByRune(cleanTextForPDF(data.DataPerformance.Completion), 35) + for i, line := range completionLines { + pdf.SetXY(200, 539+float64(i)*lineHeight) + pdf.Cell(nil, line) + } + } + + // 点赞/分享/评论 - 一行最多35个字 + pdf.SetXY(200, 600) + engagementLines := splitTextByRune(cleanTextForPDF(data.DataPerformance.Engagement), 35) + for i, line := range engagementLines { + pdf.SetXY(200, 600+float64(i)*lineHeight) + pdf.Cell(nil, line) + } + + // 整体总结及可优化建议 - 一行最多35个字 + pdf.SetXY(200, 676) + overallSummaryLines := splitTextByRune(cleanTextForPDF(data.OverallSummary), 35) + for i, line := range overallSummaryLines { + pdf.SetXY(200, 676+float64(i)*lineHeight) pdf.Cell(nil, line) } } - // 浏览量 - 一行最多35个字 - pdf.SetXY(200, 474) - viewsLines := splitTextByRune(cleanTextForPDF(data.DataPerformance.Views), 35) - for i, line := range viewsLines { - pdf.SetXY(200, 474+float64(i)*lineHeight) - pdf.Cell(nil, line) - } + // 如果有图片URL,添加新页面并居中显示图片 + if data.ImageURL != "" { + // 添加新页面 + pdf.AddPage() - // 完播率 - 一行最多35个字 - // 始终显示在固定位置,有内容时填充内容 - pdf.SetXY(200, 539) - if data.DataPerformance.Completion != "" { - completionLines := splitTextByRune(cleanTextForPDF(data.DataPerformance.Completion), 35) - for i, line := range completionLines { - pdf.SetXY(200, 539+float64(i)*lineHeight) - pdf.Cell(nil, line) + // 下载图片 + resp, err := http.Get(data.ImageURL) + if err != nil { + return fmt.Errorf("下载图片失败: %v", err) } - } + defer resp.Body.Close() - // 点赞/分享/评论 - 一行最多35个字 - // 始终固定在完播率下面的位置 - pdf.SetXY(200, 600) - engagementLines := splitTextByRune(cleanTextForPDF(data.DataPerformance.Engagement), 35) - for i, line := range engagementLines { - pdf.SetXY(200, 600+float64(i)*lineHeight) - pdf.Cell(nil, line) - } + imageData, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("读取图片数据失败: %v", err) + } - // 整体总结及可优化建议 - 一行最多35个字 - pdf.SetXY(200, 676) - overallSummaryLines := splitTextByRune(cleanTextForPDF(data.OverallSummary), 35) - for i, line := range overallSummaryLines { - pdf.SetXY(200, 676+float64(i)*lineHeight) - pdf.Cell(nil, line) + // 解析URL获取文件扩展名 + u, err := url.Parse(data.ImageURL) + if err != nil { + return fmt.Errorf("图片链接解析错误: %v", err) + } + fileExt := filepath.Ext(u.Path) + if fileExt == "" { + fileExt = ".jpg" + } + + // 保存到临时文件 + tmpFile, err := os.CreateTemp("", "pdf_image_*"+fileExt) + if err != nil { + return fmt.Errorf("创建临时文件失败: %v", err) + } + defer os.Remove(tmpFile.Name()) + defer tmpFile.Close() + + _, err = tmpFile.Write(imageData) + if err != nil { + return fmt.Errorf("写入临时文件失败: %v", err) + } + tmpFile.Close() + + // 使用 image 包获取图片原始尺寸 + imgFile, err := os.Open(tmpFile.Name()) + if err != nil { + return fmt.Errorf("打开图片文件失败: %v", err) + } + defer imgFile.Close() + + config, format, err := image.DecodeConfig(imgFile) + if err != nil { + return fmt.Errorf("获取图片尺寸失败: %v", err) + } + _ = format // 忽略格式 + + // A4页面宽度595pt(210mm),高度842pt(297mm) + pageWidth := 595.0 + pageHeight := 842.0 + margin := 20.0 + + // 计算可用宽度 + availableWidth := pageWidth - 2*margin + + // 计算缩放后的图片尺寸(保持宽高比,宽度为可用宽度的80%) + imageWidth := availableWidth * 0.8 + originalWidth := float64(config.Width) + originalHeight := float64(config.Height) + imageHeight := (imageWidth / originalWidth) * originalHeight + + // 计算居中位置 + imageX := (pageWidth - imageWidth) / 2 + imageY := (pageHeight - imageHeight) / 2 + + // 使用 ImageHolderByBytes 添加图片 + imgH1, err := gopdf.ImageHolderByBytes(imageData) + if err != nil { + return fmt.Errorf("创建图片Holder失败: %v", err) + } + + // 绘制图片 + err = pdf.ImageByHolder(imgH1, imageX, imageY, &gopdf.Rect{W: imageWidth, H: imageHeight}) + if err != nil { + return fmt.Errorf("绘制图片失败: %v", err) + } } // 生成新的 PDF - if err = pdf.WritePdf(outputPath); err != nil { + if err := pdf.WritePdf(outputPath); err != nil { return fmt.Errorf("error writing final PDF: %v", err) } diff --git a/pkg/utils/pdf_competitor_test.go b/pkg/utils/pdf_competitor_test.go index e507dfb5..71521c44 100644 --- a/pkg/utils/pdf_competitor_test.go +++ b/pkg/utils/pdf_competitor_test.go @@ -121,3 +121,55 @@ func TestGenerateCompetitorReportPDFImageOnly(t *testing.T) { fmt.Printf("PDF生成成功: %s\n", outputPath) } + +// TestGenerateCompetitorReportPDFWithImage 测试带图片的竞品报告PDF +// 注意:此测试需要网络连接来下载图片,如果网络不可用会被跳过 +func TestGenerateCompetitorReportPDFWithImage(t *testing.T) { + // 获取项目根目录 + root := getProjectRoot() + + // 准备测试数据(带图片) + // 使用一个已知可用的测试图片URL + 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: "整体来看,该竞品视频在内容策划、表现形式和互动数据方面都表现优秀。", + ImageURL: "https://cdn-test.szjixun.cn/fonchain-main/test/image/12345/artwork/0.png", // 测试用图片URL + } + + // 模板路径 + templatePath := filepath.Join(root, "data", "竞品报告pdf模板.pdf") + // 输出路径 + outputPath := filepath.Join(root, "data", "output", "竞品报告测试_带图片.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.Logf("图片下载测试跳过(网络问题): %v", err) + t.Skip("网络不可用,跳过图片测试") + return + } + + fmt.Printf("PDF生成成功: %s\n", outputPath) +} From 6ef35e0852818e80ad84687ec05d881fb7b871dc Mon Sep 17 00:00:00 2001 From: cjy Date: Mon, 2 Mar 2026 13:53:03 +0800 Subject: [PATCH 12/38] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=AB=9E?= =?UTF-8?q?=E5=93=81=E6=8A=A5=E5=91=8A=E8=87=AA=E5=8A=A8=E7=94=9F=E6=88=90?= =?UTF-8?q?=E6=A8=A1=E6=9D=BF=E7=B1=BB=E5=9E=8B=E7=9A=84pdf=EF=BC=8C?= =?UTF-8?q?=E5=90=8C=E6=97=B6=E5=92=8C=E4=BB=BB=E5=8A=A1=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=8F=B0=E9=82=A3=E8=BE=B9=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/service/cast/report.go | 76 +++++++++++++++++++++++++++--- pkg/service/taskbench/taskBench.go | 3 +- 2 files changed, 71 insertions(+), 8 deletions(-) diff --git a/pkg/service/cast/report.go b/pkg/service/cast/report.go index 8388a8ab..1f9c1e2e 100644 --- a/pkg/service/cast/report.go +++ b/pkg/service/cast/report.go @@ -2,6 +2,7 @@ package cast import ( "context" + "encoding/json" "errors" "fmt" "fonchain-fiee/api/accountFiee" @@ -32,15 +33,23 @@ import ( "go.uber.org/zap" ) +// CreateCompetitiveReportReqEx 扩展的竞品报告请求(包含AI生成的JSON数据) +type CreateCompetitiveReportReqEx struct { + *cast.CreateCompetitiveReportReq // 嵌入原有请求 + ReportData string `json:"reportData"` // AI生成的竞品报告JSON数据 +} + // CreateCompetitiveReport 创建竞品报告 func CreateCompetitiveReport(ctx *gin.Context) { - var req *cast.CreateCompetitiveReportReq + var reqEx CreateCompetitiveReportReqEx var err error - if err = ctx.ShouldBind(&req); err != nil { + if err = ctx.ShouldBindJSON(&reqEx); err != nil { service.Error(ctx, err) return } - resp, err := CreateCompetitiveReportCore(ctx, req) + // 转换为原有类型 + req := reqEx.CreateCompetitiveReportReq + resp, err := CreateCompetitiveReportCore(ctx, req, reqEx.ReportData) if err != nil { service.Error(ctx, err) return @@ -49,7 +58,7 @@ func CreateCompetitiveReport(ctx *gin.Context) { return } -func CreateCompetitiveReportCore(ctx *gin.Context, req *cast.CreateCompetitiveReportReq) (*cast.CreateCompetitiveReportResp, error) { +func CreateCompetitiveReportCore(ctx *gin.Context, req *cast.CreateCompetitiveReportReq, reportData string) (*cast.CreateCompetitiveReportResp, error) { loginInfo := login.GetUserInfoFromC(ctx) lockKey := fmt.Sprintf("lock_create_competitive_report_%d", loginInfo.ID) reply := cache.RedisClient.SetNX(lockKey, time.Now().Format("2006-01-02 15:04:05"), time.Second*5) @@ -103,8 +112,9 @@ func CreateCompetitiveReportCore(ctx *gin.Context, req *cast.CreateCompetitiveRe } req.BundleOrderUuid = resp1.OrderUUID - if req.ReportContent == "" && req.ImageUrl == "" { - return nil, errors.New("报告内容和图片不能同时为空") + // 验证:报告内容、AI生成的JSON数据和图片不能同时为空 + if req.ReportContent == "" && reportData == "" && req.ImageUrl == "" { + return nil, errors.New("报告内容、AI数据和图片不能同时为空") } if req.ImageUrl != "" { @@ -116,7 +126,59 @@ func CreateCompetitiveReportCore(ctx *gin.Context, req *cast.CreateCompetitiveRe req.ImageUrl = newImageUrl } - if req.ReportContent != "" { + // 判断使用哪种方式生成PDF + if reportData != "" { + // 使用 GenerateCompetitorReportPDF 生成PDF + // 解析 JSON 数据 + var competitorReportData utils.CompetitorReportData + if err := json.Unmarshal([]byte(reportData), &competitorReportData); err != nil { + zap.L().Error("解析竞品报告数据失败", zap.String("reportData", reportData), zap.Error(err)) + return nil, errors.New("竞品报告数据格式错误") + } + + // 如果有图片URL,设置到reportData中 + if req.ImageUrl != "" { + competitorReportData.ImageURL = req.ImageUrl + } + + today := time.Now().Format("20060102") + timestamp := time.Now().UnixMicro() + pdfFileName := fmt.Sprintf("%s%s老师的竞品报告%d.pdf", today, req.ArtistName, timestamp) + pdfFilePath := "./runtime/report_pdf/" + pdfFileName + + _, err = utils.CheckDirPath("./runtime/report_pdf/", true) + if err != nil { + return nil, fmt.Errorf("创建PDF目录失败: %v", err) + } + + // 模板路径 + templatePath := "./data/竞品报告pdf模板.pdf" + + // 调用 GenerateCompetitorReportPDF + err = utils.GenerateCompetitorReportPDF(templatePath, pdfFilePath, competitorReportData) + if err != nil { + zap.L().Error("生成PDF失败", zap.Error(err)) + return nil, errors.New("生成PDF失败") + } + + defer func() { + if _, err := os.Stat(pdfFilePath); err == nil { + if err := os.Remove(pdfFilePath); err != nil { + zap.L().Warn("删除临时PDF文件失败", zap.String("path", pdfFilePath), zap.Error(err)) + } else { + zap.L().Info("删除临时PDF文件成功", zap.String("path", pdfFilePath)) + } + } + }() + + pdfUrl, uploadErr := upload.PutBos(pdfFilePath, upload.PdfType, true) + if uploadErr != nil { + zap.L().Error("上传PDF失败: %v", zap.Error(uploadErr)) + return nil, errors.New("上传PDF失败") + } + req.PdfUrl = pdfUrl + } else if req.ReportContent != "" { + // 使用原有的 GeneratePDF 生成PDF today := time.Now().Format("20060102") timestamp := time.Now().UnixMicro() pdfFileName := fmt.Sprintf("%s%s老师的竞品报告%d.pdf", today, req.ArtistName, timestamp) diff --git a/pkg/service/taskbench/taskBench.go b/pkg/service/taskbench/taskBench.go index de41db92..527dfbe3 100644 --- a/pkg/service/taskbench/taskBench.go +++ b/pkg/service/taskbench/taskBench.go @@ -384,6 +384,7 @@ type CreateWorkAnalysisWithTaskUUIDReq struct { type CreateCompetitiveReportWithTaskUUIDReq struct { *cast.CreateCompetitiveReportReq AssignRecordsUUID string `json:"assignRecordsUUID"` + ReportData string `json:"reportData"` // AI生成的竞品报告JSON数据 } func UpdateWorkImageWithTaskUUID(ctx *gin.Context) { @@ -575,7 +576,7 @@ func CreateCompetitiveReportWithTaskUUID(ctx *gin.Context) { service.Error(ctx, errors.New("任务已中止")) return } - resp, err := castService.CreateCompetitiveReportCore(ctx, req.CreateCompetitiveReportReq) + resp, err := castService.CreateCompetitiveReportCore(ctx, req.CreateCompetitiveReportReq, req.ReportData) if err != nil { service.Error(ctx, err) return From ad4c74639f7306485695dd034a8f7c007b2b4ecf Mon Sep 17 00:00:00 2001 From: cjy Date: Mon, 2 Mar 2026 14:19:28 +0800 Subject: [PATCH 13/38] =?UTF-8?q?fix:=E5=A2=9E=E5=8A=A0=E4=B8=80=E7=82=B9?= =?UTF-8?q?=E6=97=A5=E5=BF=97=EF=BC=8C=E7=94=A8=E4=BA=8E=E6=8E=92=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/service/cast/report.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/service/cast/report.go b/pkg/service/cast/report.go index 1f9c1e2e..9add2518 100644 --- a/pkg/service/cast/report.go +++ b/pkg/service/cast/report.go @@ -131,10 +131,14 @@ func CreateCompetitiveReportCore(ctx *gin.Context, req *cast.CreateCompetitiveRe // 使用 GenerateCompetitorReportPDF 生成PDF // 解析 JSON 数据 var competitorReportData utils.CompetitorReportData + zap.L().Info("reportData内容", zap.String("reportData", reportData)) if err := json.Unmarshal([]byte(reportData), &competitorReportData); err != nil { zap.L().Error("解析竞品报告数据失败", zap.String("reportData", reportData), zap.Error(err)) - return nil, errors.New("竞品报告数据格式错误") + // 尝试打印更详细的错误信息 + zap.L().Error("详细错误", zap.Error(err)) + return nil, errors.New("竞品报告数据格式错误: " + err.Error()) } + zap.L().Info("解析成功", zap.Any("competitorReportData", competitorReportData)) // 如果有图片URL,设置到reportData中 if req.ImageUrl != "" { From 1b7e105164422fbc6f6f00d79e7d6170a6c91a24 Mon Sep 17 00:00:00 2001 From: cjy Date: Mon, 2 Mar 2026 14:27:43 +0800 Subject: [PATCH 14/38] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E7=AB=9E=E5=93=81?= =?UTF-8?q?=E6=8A=A5=E5=91=8Ajson=E5=8F=8D=E5=BA=8F=E5=88=97=E5=8C=96?= =?UTF-8?q?=E6=9C=89=E7=AC=A6=E5=8F=B7=E5=AF=BC=E8=87=B4=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/service/cast/report.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pkg/service/cast/report.go b/pkg/service/cast/report.go index 9add2518..9530ae3f 100644 --- a/pkg/service/cast/report.go +++ b/pkg/service/cast/report.go @@ -132,11 +132,16 @@ func CreateCompetitiveReportCore(ctx *gin.Context, req *cast.CreateCompetitiveRe // 解析 JSON 数据 var competitorReportData utils.CompetitorReportData zap.L().Info("reportData内容", zap.String("reportData", reportData)) - if err := json.Unmarshal([]byte(reportData), &competitorReportData); err != nil { - zap.L().Error("解析竞品报告数据失败", zap.String("reportData", reportData), zap.Error(err)) + + // 处理双重编码问题:替换中文引号为英文引号 + processedReportData := strings.ReplaceAll(reportData, "”", "\"") + processedReportData = strings.ReplaceAll(processedReportData, "“", "\"") + + if err := json.Unmarshal([]byte(processedReportData), &competitorReportData); err != nil { + zap.L().Error("解析竞品报告数据失败", zap.String("reportData", processedReportData), zap.Error(err)) // 尝试打印更详细的错误信息 zap.L().Error("详细错误", zap.Error(err)) - return nil, errors.New("竞品报告数据格式错误: " + err.Error()) + return nil, errors.New("竞品报告数据格式错误") } zap.L().Info("解析成功", zap.Any("competitorReportData", competitorReportData)) From 62ee062bc5beb7bfbb6692e12d6ff28dbfa303eb Mon Sep 17 00:00:00 2001 From: cjy Date: Mon, 2 Mar 2026 15:30:13 +0800 Subject: [PATCH 15/38] =?UTF-8?q?fix:=20=E4=BF=AE=E6=94=B9ai=E8=BF=94?= =?UTF-8?q?=E5=9B=9E=E7=9A=84=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/service/ai/video_vl.go | 104 ++++++++++++++++++++++++++++++++++--- 1 file changed, 96 insertions(+), 8 deletions(-) diff --git a/pkg/service/ai/video_vl.go b/pkg/service/ai/video_vl.go index 1a945855..8fa0eeef 100644 --- a/pkg/service/ai/video_vl.go +++ b/pkg/service/ai/video_vl.go @@ -71,11 +71,38 @@ type CompetitorReportRequest struct { Model string `json:"model"` // 可选的模型名称,默认使用 qwen3-vl-plus } +// CompetitorReportData 竞品报告数据(用于返回给前端) +type CompetitorReportData struct { + HighlightAnalysis HighlightAnalysisData `json:"highlight_analysis"` + DataPerformance DataPerformanceData `json:"data_performance_analysis"` + OverallSummary string `json:"overall_summary_and_optimization"` +} + +type HighlightAnalysisData struct { + Summary string `json:"summary"` + Points PointsData `json:"points"` +} + +type PointsData struct { + Theme string `json:"theme"` + Narrative string `json:"narrative"` + Content string `json:"content"` + Copywriting string `json:"copywriting"` + Data string `json:"data"` + Music string `json:"music,omitempty"` +} + +type DataPerformanceData struct { + Views string `json:"views"` + Completion string `json:"completion_rate,omitempty"` + Engagement string `json:"engagement"` +} + // CompetitorReportResponse 竞品报告响应数据 type CompetitorReportResponse struct { - ImageURL string `json:"image_url,omitempty"` // 生成的图片URL(1024*1024),非必须返回 - Text string `json:"text,omitempty"` // 竞品报告文本内容,非必须返回 - JSON string `json:"json,omitempty"` // 竞品报告JSON格式,非必须返回 + ImageURL string `json:"image_url,omitempty"` // 生成的图片URL(1024*1024),非必须返回 + Text string `json:"text,omitempty"` // 竞品报告文本内容,非必须返回 + JsonData *CompetitorReportData `json:"json_data,omitempty"` // 竞品报告JSON数据 } // CompetitorReportJSON AI返回的JSON结构 @@ -164,6 +191,65 @@ func convertJSONToText(data CompetitorReportJSON, isVideo bool) string { return sb.String() } +// convertJSONToTextFromData 将 JSON 转换为纯文本格式(使用新的 CompetitorReportData 结构) +func convertJSONToTextFromData(data CompetitorReportData, isVideo bool) string { + var sb strings.Builder + + // 一、亮点表现分析 + sb.WriteString("一、亮点表现分析\n") + sb.WriteString(data.HighlightAnalysis.Summary) + sb.WriteString("\n\n") + + sb.WriteString("1. 标题亮点:") + sb.WriteString(data.HighlightAnalysis.Points.Theme) + sb.WriteString("\n") + + sb.WriteString("2. 题材亮点:") + sb.WriteString(data.HighlightAnalysis.Points.Narrative) + sb.WriteString("\n") + + sb.WriteString("3. 内容亮点:") + sb.WriteString(data.HighlightAnalysis.Points.Content) + sb.WriteString("\n") + + sb.WriteString("4. 文案亮点:") + sb.WriteString(data.HighlightAnalysis.Points.Copywriting) + sb.WriteString("\n") + + sb.WriteString("5. 数据亮点:") + sb.WriteString(data.HighlightAnalysis.Points.Data) + sb.WriteString("\n") + + if isVideo && data.HighlightAnalysis.Points.Music != "" { + sb.WriteString("6. 配乐亮点:") + sb.WriteString(data.HighlightAnalysis.Points.Music) + sb.WriteString("\n") + } + + // 二、数据表现分析 + sb.WriteString("\n二、数据表现分析\n") + sb.WriteString("1. 浏览量表现:") + sb.WriteString(data.DataPerformance.Views) + sb.WriteString("\n") + + if isVideo && data.DataPerformance.Completion != "" { + sb.WriteString("2. 完播率表现:") + sb.WriteString(data.DataPerformance.Completion) + sb.WriteString("\n") + sb.WriteString("3. 点赞/分享/评论表现:") + } else { + sb.WriteString("2. 点赞/分享/评论表现:") + } + sb.WriteString(data.DataPerformance.Engagement) + sb.WriteString("\n") + + // 三、整体总结及可优化建议 + sb.WriteString("\n三、整体总结及可优化建议\n") + sb.WriteString(data.OverallSummary) + + return sb.String() +} + // AICompetitorReport 生成竞品报告接口 func AICompetitorReport(ctx *gin.Context) { var req CompetitorReportRequest @@ -387,14 +473,12 @@ JSON结构是固定的,请将内容填充到对应的value中,禁止修改ke // 返回结果(只返回实际生成的内容) result := CompetitorReportResponse{} if needText { - result.JSON = textRes.text // JSON 字段直接返回 AI 生成的 JSON - - // 将 JSON 解析并转换为纯文本 + // 将 JSON 解析为结构化数据 fmt.Println("========== 开始解析 JSON ==========") fmt.Println("原始内容是否以 { 开头:", strings.HasPrefix(strings.TrimSpace(textRes.text), "{")) fmt.Println("原始内容前100字符:", strings.TrimSpace(textRes.text)[:min(100, len(strings.TrimSpace(textRes.text)))]) - var jsonData CompetitorReportJSON + var jsonData CompetitorReportData if err := json.Unmarshal([]byte(textRes.text), &jsonData); err != nil { // 如果解析失败,回退使用原始文本 fmt.Println("========== JSON 解析失败 ==========") @@ -405,7 +489,11 @@ JSON结构是固定的,请将内容填充到对应的value中,禁止修改ke fmt.Println("========== JSON 解析成功 ==========") fmt.Println("Summary:", jsonData.HighlightAnalysis.Summary) fmt.Println("==================================") - result.Text = convertJSONToText(jsonData, len(req.Videos) > 0) + + // 赋值结构体到 JsonData 中 + result.JsonData = &jsonData + + result.Text = convertJSONToTextFromData(jsonData, len(req.Videos) > 0) } } if needImage { From c5748d673bc50bb1fc9131c0a79326624a143aa8 Mon Sep 17 00:00:00 2001 From: cjy Date: Mon, 2 Mar 2026 15:35:06 +0800 Subject: [PATCH 16/38] =?UTF-8?q?fix=EF=BC=9A=20=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E7=94=9F=E6=88=90=E7=AB=9E=E5=93=81=E6=8A=A5=E5=91=8A=E7=9A=84?= =?UTF-8?q?=E8=AF=B7=E6=B1=82=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/service/cast/report.go | 33 ++++++++++++------------------ pkg/service/taskbench/taskBench.go | 4 ++-- 2 files changed, 15 insertions(+), 22 deletions(-) diff --git a/pkg/service/cast/report.go b/pkg/service/cast/report.go index 9530ae3f..bc47ef58 100644 --- a/pkg/service/cast/report.go +++ b/pkg/service/cast/report.go @@ -2,7 +2,6 @@ package cast import ( "context" - "encoding/json" "errors" "fmt" "fonchain-fiee/api/accountFiee" @@ -35,8 +34,8 @@ import ( // CreateCompetitiveReportReqEx 扩展的竞品报告请求(包含AI生成的JSON数据) type CreateCompetitiveReportReqEx struct { - *cast.CreateCompetitiveReportReq // 嵌入原有请求 - ReportData string `json:"reportData"` // AI生成的竞品报告JSON数据 + *cast.CreateCompetitiveReportReq // 嵌入原有请求 + ReportData utils.CompetitorReportData `json:"reportData"` // AI生成的竞品报告数据 } // CreateCompetitiveReport 创建竞品报告 @@ -58,7 +57,7 @@ func CreateCompetitiveReport(ctx *gin.Context) { return } -func CreateCompetitiveReportCore(ctx *gin.Context, req *cast.CreateCompetitiveReportReq, reportData string) (*cast.CreateCompetitiveReportResp, error) { +func CreateCompetitiveReportCore(ctx *gin.Context, req *cast.CreateCompetitiveReportReq, reportData utils.CompetitorReportData) (*cast.CreateCompetitiveReportResp, error) { loginInfo := login.GetUserInfoFromC(ctx) lockKey := fmt.Sprintf("lock_create_competitive_report_%d", loginInfo.ID) reply := cache.RedisClient.SetNX(lockKey, time.Now().Format("2006-01-02 15:04:05"), time.Second*5) @@ -113,7 +112,9 @@ func CreateCompetitiveReportCore(ctx *gin.Context, req *cast.CreateCompetitiveRe req.BundleOrderUuid = resp1.OrderUUID // 验证:报告内容、AI生成的JSON数据和图片不能同时为空 - if req.ReportContent == "" && reportData == "" && req.ImageUrl == "" { + // 检查 reportData 是否有数据 + hasReportData := reportData.OverallSummary != "" || reportData.HighlightAnalysis.Summary != "" + if req.ReportContent == "" && !hasReportData && req.ImageUrl == "" { return nil, errors.New("报告内容、AI数据和图片不能同时为空") } @@ -127,29 +128,21 @@ func CreateCompetitiveReportCore(ctx *gin.Context, req *cast.CreateCompetitiveRe } // 判断使用哪种方式生成PDF - if reportData != "" { + // 检查 reportData 是否有数据(通过判断 OverallSummary 是否为空) + if reportData.OverallSummary != "" || reportData.HighlightAnalysis.Summary != "" { // 使用 GenerateCompetitorReportPDF 生成PDF - // 解析 JSON 数据 - var competitorReportData utils.CompetitorReportData - zap.L().Info("reportData内容", zap.String("reportData", reportData)) + zap.L().Info("reportData内容", zap.Any("reportData", reportData)) - // 处理双重编码问题:替换中文引号为英文引号 - processedReportData := strings.ReplaceAll(reportData, "”", "\"") - processedReportData = strings.ReplaceAll(processedReportData, "“", "\"") - - if err := json.Unmarshal([]byte(processedReportData), &competitorReportData); err != nil { - zap.L().Error("解析竞品报告数据失败", zap.String("reportData", processedReportData), zap.Error(err)) - // 尝试打印更详细的错误信息 - zap.L().Error("详细错误", zap.Error(err)) - return nil, errors.New("竞品报告数据格式错误") - } - zap.L().Info("解析成功", zap.Any("competitorReportData", competitorReportData)) + // 直接使用传入的结构体数据 + competitorReportData := reportData // 如果有图片URL,设置到reportData中 if req.ImageUrl != "" { competitorReportData.ImageURL = req.ImageUrl } + zap.L().Info("解析成功", zap.Any("competitorReportData", competitorReportData)) + today := time.Now().Format("20060102") timestamp := time.Now().UnixMicro() pdfFileName := fmt.Sprintf("%s%s老师的竞品报告%d.pdf", today, req.ArtistName, timestamp) diff --git a/pkg/service/taskbench/taskBench.go b/pkg/service/taskbench/taskBench.go index 527dfbe3..0f10771e 100644 --- a/pkg/service/taskbench/taskBench.go +++ b/pkg/service/taskbench/taskBench.go @@ -383,8 +383,8 @@ type CreateWorkAnalysisWithTaskUUIDReq struct { type CreateCompetitiveReportWithTaskUUIDReq struct { *cast.CreateCompetitiveReportReq - AssignRecordsUUID string `json:"assignRecordsUUID"` - ReportData string `json:"reportData"` // AI生成的竞品报告JSON数据 + AssignRecordsUUID string `json:"assignRecordsUUID"` + ReportData utils.CompetitorReportData `json:"reportData"` // AI生成的竞品报告数据 } func UpdateWorkImageWithTaskUUID(ctx *gin.Context) { From 6242962b43708b548be2ea5f73a9f3a3d866f2a2 Mon Sep 17 00:00:00 2001 From: cjy Date: Mon, 2 Mar 2026 15:42:52 +0800 Subject: [PATCH 17/38] =?UTF-8?q?feat:=E6=9B=B4=E6=96=B0=E7=AB=9E=E5=93=81?= =?UTF-8?q?=E6=8A=A5=E5=91=8A=E5=AF=BC=E5=85=A5=E6=A8=A1=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/竞品报告导入模板_new.xlsx | Bin 0 -> 11742 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 data/竞品报告导入模板_new.xlsx diff --git a/data/竞品报告导入模板_new.xlsx b/data/竞品报告导入模板_new.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..fbfa865d51f98b3aa85d82593f28dc04cab15e00 GIT binary patch literal 11742 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 Rs6ePdWPl_8e2Qni{{vH)bw>aI literal 0 HcmV?d00001 From 9c4c4b1c66829344eff8fd94aceab979d1c05563 Mon Sep 17 00:00:00 2001 From: cjy Date: Mon, 2 Mar 2026 16:25:10 +0800 Subject: [PATCH 18/38] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E6=96=B0?= =?UTF-8?q?=E6=A8=A1=E6=9D=BF=E4=B8=8B=E7=9A=84=E7=AB=9E=E5=93=81=E6=8A=A5?= =?UTF-8?q?=E5=91=8A=E6=89=B9=E9=87=8F=E5=AF=BC=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/竞品报告导入模板.xlsx | Bin 11324 -> 11785 bytes data/竞品报告导入模板_new.xlsx | Bin 11742 -> 0 bytes pkg/service/cast/report.go | 239 ++++++++++++++++++++++++--------- 3 files changed, 178 insertions(+), 61 deletions(-) delete mode 100644 data/竞品报告导入模板_new.xlsx diff --git a/data/竞品报告导入模板.xlsx b/data/竞品报告导入模板.xlsx index 0f5505ddf41568339e9648c524779d043d940df2..272f60f7e75e1c596697305a54a840d9d394907d 100644 GIT binary patch delta 3245 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 SmSex`1dfgz%h5&t_v>E-Wbh~e diff --git a/data/竞品报告导入模板_new.xlsx b/data/竞品报告导入模板_new.xlsx deleted file mode 100644 index fbfa865d51f98b3aa85d82593f28dc04cab15e00..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11742 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 Rs6ePdWPl_8e2Qni{{vH)bw>aI diff --git a/pkg/service/cast/report.go b/pkg/service/cast/report.go index bc47ef58..e9ce8357 100644 --- a/pkg/service/cast/report.go +++ b/pkg/service/cast/report.go @@ -372,16 +372,86 @@ func ImportCompetitiveReportBatch(ctx *gin.Context) { temp.Title = nowDate + temp.ArtistName + "老师竞品报告" } } - // 解析报告内容(D列,row[3]) - if len(row) > 3 { - temp.ReportContent = row[3] + + // 构建竞品报告数据(新模板格式) + // D列(row[3]):亮点表现分析 - 对应Summary字段 + // E列(row[4]):标题亮点 + // F列(row[5]):题材亮点 + // G列(row[6]):内容亮点 + // H列(row[7]):文案亮点 + // I列(row[8]):数据亮点 + // J列(row[9]):配乐亮点 + // K列(row[10]):浏览量 + // L列(row[11]):完播率 + // M列(row[12]):点赞/分享/评论 + // N列(row[13]):整体总结及可优化建议 + // O列(row[14]):图片 + + var competitorReportData utils.CompetitorReportData + + // 解析亮点表现分析 + highlightAnalysis := utils.HighlightAnalysisData{ + Points: utils.PointsData{}, } - // 解析图片URL(E列,row[4]) - if len(row) > 4 && utils.CleanString(row[4]) != "" { - temp.ImageUrl = utils.CleanString(row[4]) + // 亮点表现分析摘要(D列,row[3]) + if len(row) > 3 { + highlightAnalysis.Summary = utils.CleanString(row[3]) } + // 标题亮点(E列,row[4]) + if len(row) > 4 { + highlightAnalysis.Points.Theme = utils.CleanString(row[4]) + } + // 题材亮点(F列,row[5]) + if len(row) > 5 { + highlightAnalysis.Points.Narrative = utils.CleanString(row[5]) + } + // 内容亮点(G列,row[6]) + if len(row) > 6 { + highlightAnalysis.Points.Content = utils.CleanString(row[6]) + } + // 文案亮点(H列,row[7]) + if len(row) > 7 { + highlightAnalysis.Points.Copywriting = utils.CleanString(row[7]) + } + // 数据亮点(I列,row[8]) + if len(row) > 8 { + highlightAnalysis.Points.Data = utils.CleanString(row[8]) + } + // 配乐亮点(J列,row[9]) + if len(row) > 9 { + highlightAnalysis.Points.Music = utils.CleanString(row[9]) + } + + // 解析数据表现 + dataPerformance := utils.DataPerformanceData{} + // 浏览量(K列,row[10]) + if len(row) > 10 { + dataPerformance.Views = utils.CleanString(row[10]) + } + // 完播率(L列,row[11]) + if len(row) > 11 { + dataPerformance.Completion = utils.CleanString(row[11]) + } + // 点赞/分享/评论(M列,row[12]) + if len(row) > 12 { + dataPerformance.Engagement = utils.CleanString(row[12]) + } + + // 整体总结及可优化建议(N列,row[13]) + if len(row) > 13 { + competitorReportData.OverallSummary = utils.CleanString(row[13]) + } + + // 图片URL(O列,row[14]) + if len(row) > 14 && utils.CleanString(row[14]) != "" { + competitorReportData.ImageURL = utils.CleanString(row[14]) + } + + competitorReportData.HighlightAnalysis = highlightAnalysis + competitorReportData.DataPerformance = dataPerformance + // 验证必填字段 if artistNum == "" { temp.Remark = "艺人编号不能为空" @@ -389,82 +459,129 @@ func ImportCompetitiveReportBatch(ctx *gin.Context) { continue } - // 验证报告内容和图片不能同时为空 - if temp.ReportContent == "" && temp.ImageUrl == "" { - temp.Remark = "报告内容和图片不能同时为空" + // 验证亮点表现分析(D列:Summary) + if highlightAnalysis.Summary == "" { + temp.Remark = "亮点表现分析摘要不能为空" req.Reports = append(req.Reports, temp) continue } - // 如果已经有错误信息,跳过PDF生成 - if temp.Remark != "" { + // 验证标题亮点(E列) + if highlightAnalysis.Points.Theme == "" { + temp.Remark = "标题亮点不能为空" req.Reports = append(req.Reports, temp) continue } - // 检查图片URL是否包含阿里云,如果包含则下载并重新上传到OSS - if temp.ImageUrl != "" { - newImageUrl, err := checkAndReuploadImageForReport(temp.ImageUrl) + // 验证题材亮点(F列) + if highlightAnalysis.Points.Narrative == "" { + temp.Remark = "题材亮点不能为空" + req.Reports = append(req.Reports, temp) + continue + } + + // 验证内容亮点(G列) + if highlightAnalysis.Points.Content == "" { + temp.Remark = "内容亮点不能为空" + req.Reports = append(req.Reports, temp) + continue + } + + // 验证文案亮点(H列) + if highlightAnalysis.Points.Copywriting == "" { + temp.Remark = "文案亮点不能为空" + req.Reports = append(req.Reports, temp) + continue + } + + // 验证数据亮点(I列) + if highlightAnalysis.Points.Data == "" { + temp.Remark = "数据亮点不能为空" + req.Reports = append(req.Reports, temp) + continue + } + + // 验证浏览量(K列) + if dataPerformance.Views == "" { + temp.Remark = "浏览量不能为空" + req.Reports = append(req.Reports, temp) + continue + } + + // 验证点赞/分享/评论(M列) + if dataPerformance.Engagement == "" { + temp.Remark = "点赞/分享/评论不能为空" + req.Reports = append(req.Reports, temp) + continue + } + + // 验证整体总结及可优化建议(N列) + if competitorReportData.OverallSummary == "" { + temp.Remark = "整体总结及可优化建议不能为空" + req.Reports = append(req.Reports, temp) + continue + } + + // 处理图片URL + if competitorReportData.ImageURL != "" { + newImageUrl, err := checkAndReuploadImageForReport(competitorReportData.ImageURL) if err != nil { temp.Remark = fmt.Sprintf("图片处理失败: %v", err) - zap.L().Error("图片重新上传失败", zap.String("imageUrl", temp.ImageUrl), zap.Error(err)) + zap.L().Error("图片重新上传失败", zap.String("imageUrl", competitorReportData.ImageURL), zap.Error(err)) req.Reports = append(req.Reports, temp) continue } - temp.ImageUrl = newImageUrl + competitorReportData.ImageURL = newImageUrl } - // 如果提供了报告内容,则生成PDF并上传 - if temp.ReportContent != "" { - // 生成临时PDF文件路径 - today := time.Now().Format("20060102") - timestamp := time.Now().UnixMicro() - pdfFileName := fmt.Sprintf("%s%s老师的竞品报告%d.pdf", today, temp.ArtistName, timestamp) - pdfFilePath := "./runtime/report_pdf/" + pdfFileName + // 生成PDF并上传 + // 生成临时PDF文件路径 + today := time.Now().Format("20060102") + timestamp := time.Now().UnixMicro() + pdfFileName := fmt.Sprintf("%s%s老师的竞品报告%d.pdf", today, temp.ArtistName, timestamp) + pdfFilePath := "./runtime/report_pdf/" + pdfFileName - // 确保目录存在 - _, err = utils.CheckDirPath("./runtime/report_pdf/", true) - if err != nil { - temp.Remark = fmt.Sprintf("创建PDF目录失败: %v", err) - req.Reports = append(req.Reports, temp) - continue - } + // 确保目录存在 + _, err = utils.CheckDirPath("./runtime/report_pdf/", true) + if err != nil { + temp.Remark = fmt.Sprintf("创建PDF目录失败: %v", err) + req.Reports = append(req.Reports, temp) + continue + } - // 生成PDF文件 - fontPath := "./data/simfang.ttf" - err = utils.GeneratePDF(temp.ReportContent, temp.ImageUrl, pdfFilePath, fontPath) - if err != nil { - zap.L().Error("生成PDF失败", zap.Error(err)) - temp.Remark = "生成PDF失败" - req.Reports = append(req.Reports, temp) - continue - } + // 模板路径 + templatePath := "./data/竞品报告pdf模板.pdf" - // 上传PDF到OSS - pdfUrl, uploadErr := upload.PutBos(pdfFilePath, upload.PdfType, true) - if uploadErr != nil { - zap.L().Error("上传PDF失败", zap.Error(uploadErr)) - temp.Remark = "上传PDF失败" - req.Reports = append(req.Reports, temp) - // 清理临时PDF文件 - if _, err := os.Stat(pdfFilePath); err == nil { - os.Remove(pdfFilePath) - } - continue - } - - // 将上传后的PDF链接设置到请求中 - temp.PdfUrl = pdfUrl + // 使用新的 GenerateCompetitorReportPDF 生成PDF + err = utils.GenerateCompetitorReportPDF(templatePath, pdfFilePath, competitorReportData) + if err != nil { + zap.L().Error("生成PDF失败", zap.Error(err)) + temp.Remark = "生成PDF失败" + req.Reports = append(req.Reports, temp) + continue + } + // 上传PDF到OSS + pdfUrl, uploadErr := upload.PutBos(pdfFilePath, upload.PdfType, true) + if uploadErr != nil { + zap.L().Error("上传PDF失败", zap.Error(uploadErr)) + temp.Remark = "上传PDF失败" + req.Reports = append(req.Reports, temp) // 清理临时PDF文件 if _, err := os.Stat(pdfFilePath); err == nil { - if err := os.Remove(pdfFilePath); err != nil { - zap.L().Warn("删除临时PDF文件失败", zap.String("path", pdfFilePath), zap.Error(err)) - } + os.Remove(pdfFilePath) + } + continue + } + + // 将上传后的PDF链接设置到请求中 + temp.PdfUrl = pdfUrl + + // 清理临时PDF文件 + if _, err := os.Stat(pdfFilePath); err == nil { + if err := os.Remove(pdfFilePath); err != nil { + zap.L().Warn("删除临时PDF文件失败", zap.String("path", pdfFilePath), zap.Error(err)) } - } else { - // 如果没有报告内容,则将图片URL设置为PDF URL - temp.PdfUrl = temp.ImageUrl } req.Reports = append(req.Reports, temp) @@ -497,7 +614,7 @@ func ImportCompetitiveReportBatch(ctx *gin.Context) { // 通过请求对象找到对应的Excel行号 if excelRowNum, ok := reportRowMap[reqReport]; ok { // 将错误信息写入最后一列(F列) - excelData.SetCellValue("Sheet1", fmt.Sprintf("F%d", excelRowNum), v.Remark) + excelData.SetCellValue("Sheet1", fmt.Sprintf("P%d", excelRowNum), v.Remark) hasValueRows[excelRowNum] = true } } From 8022eca1f85fd9b9afe74964fbd2513033a69f6d Mon Sep 17 00:00:00 2001 From: cjy Date: Mon, 2 Mar 2026 16:47:55 +0800 Subject: [PATCH 19/38] =?UTF-8?q?fix:=20=E7=AB=9E=E5=93=81=E6=8A=A5?= =?UTF-8?q?=E5=91=8A=E6=89=B9=E9=87=8F=E5=AF=BC=E5=85=A5=E7=9A=84=E6=97=B6?= =?UTF-8?q?=E5=80=99=E6=B7=BB=E5=8A=A0=E5=88=B0=E6=AD=A3=E6=96=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/service/cast/report.go | 5 +++ pkg/utils/pdf.go | 65 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/pkg/service/cast/report.go b/pkg/service/cast/report.go index e9ce8357..5f42ef29 100644 --- a/pkg/service/cast/report.go +++ b/pkg/service/cast/report.go @@ -577,6 +577,11 @@ func ImportCompetitiveReportBatch(ctx *gin.Context) { // 将上传后的PDF链接设置到请求中 temp.PdfUrl = pdfUrl + // 生成竞品报告正文 + // 判断是否为视频:如果有图片URL则为图片,否则根据配乐亮点和完播率是否有值来判断 + isVideo := competitorReportData.HighlightAnalysis.Points.Music != "" || competitorReportData.DataPerformance.Completion != "" + temp.ReportContent = utils.ConvertCompetitorReportToText(competitorReportData, isVideo) + // 清理临时PDF文件 if _, err := os.Stat(pdfFilePath); err == nil { if err := os.Remove(pdfFilePath); err != nil { diff --git a/pkg/utils/pdf.go b/pkg/utils/pdf.go index 47618d12..5afb9675 100644 --- a/pkg/utils/pdf.go +++ b/pkg/utils/pdf.go @@ -9,6 +9,7 @@ import ( "net/url" "os" "path/filepath" + "strings" "unicode" "github.com/phpdave11/gofpdf" @@ -497,3 +498,67 @@ func wrapText(text string, maxLen int) []string { return lines } + +// ConvertCompetitorReportToText 将竞品报告数据转换为文本格式 +// 参数: +// - data: 竞品报告数据 +// - isVideo: 是否为视频(视频需要包含完播率和配乐亮点) +// +// 返回: 转换后的文本内容 +func ConvertCompetitorReportToText(data CompetitorReportData, isVideo bool) string { + var sb strings.Builder + + // 一、亮点表现分析 + sb.WriteString("一、亮点表现分析\n") + sb.WriteString(data.HighlightAnalysis.Summary) + sb.WriteString("\n\n") + + sb.WriteString("1. 标题亮点:") + sb.WriteString(data.HighlightAnalysis.Points.Theme) + sb.WriteString("\n") + + sb.WriteString("2. 题材亮点:") + sb.WriteString(data.HighlightAnalysis.Points.Narrative) + sb.WriteString("\n") + + sb.WriteString("3. 内容亮点:") + sb.WriteString(data.HighlightAnalysis.Points.Content) + sb.WriteString("\n") + + sb.WriteString("4. 文案亮点:") + sb.WriteString(data.HighlightAnalysis.Points.Copywriting) + sb.WriteString("\n") + + sb.WriteString("5. 数据亮点:") + sb.WriteString(data.HighlightAnalysis.Points.Data) + sb.WriteString("\n") + + if isVideo && data.HighlightAnalysis.Points.Music != "" { + sb.WriteString("6. 配乐亮点:") + sb.WriteString(data.HighlightAnalysis.Points.Music) + sb.WriteString("\n") + } + + // 二、数据表现分析 + sb.WriteString("\n二、数据表现分析\n") + sb.WriteString("1. 浏览量表现:") + sb.WriteString(data.DataPerformance.Views) + sb.WriteString("\n") + + if isVideo && data.DataPerformance.Completion != "" { + sb.WriteString("2. 完播率表现:") + sb.WriteString(data.DataPerformance.Completion) + sb.WriteString("\n") + sb.WriteString("3. 点赞/分享/评论表现:") + } else { + sb.WriteString("2. 点赞/分享/评论表现:") + } + sb.WriteString(data.DataPerformance.Engagement) + sb.WriteString("\n") + + // 三、整体总结及可优化建议 + sb.WriteString("\n三、整体总结及可优化建议\n") + sb.WriteString(data.OverallSummary) + + return sb.String() +} From b6fd35b2ecc08eca7c183ce5d3fac94fde746357 Mon Sep 17 00:00:00 2001 From: cjy Date: Mon, 2 Mar 2026 16:54:35 +0800 Subject: [PATCH 20/38] =?UTF-8?q?fix:=20=E4=BF=AE=E6=94=B9json=E5=BA=8F?= =?UTF-8?q?=E5=88=97=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/service/cast/report.go | 4 ++-- pkg/service/taskbench/taskBench.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/service/cast/report.go b/pkg/service/cast/report.go index 5f42ef29..c3147d37 100644 --- a/pkg/service/cast/report.go +++ b/pkg/service/cast/report.go @@ -34,8 +34,8 @@ import ( // CreateCompetitiveReportReqEx 扩展的竞品报告请求(包含AI生成的JSON数据) type CreateCompetitiveReportReqEx struct { - *cast.CreateCompetitiveReportReq // 嵌入原有请求 - ReportData utils.CompetitorReportData `json:"reportData"` // AI生成的竞品报告数据 + *cast.CreateCompetitiveReportReq // 嵌入原有请求 + ReportData utils.CompetitorReportData `json:"reportData,json_data"` // AI生成的竞品报告数据(支持 reportData 和 json_data 两种字段名) } // CreateCompetitiveReport 创建竞品报告 diff --git a/pkg/service/taskbench/taskBench.go b/pkg/service/taskbench/taskBench.go index 0f10771e..d9a08f38 100644 --- a/pkg/service/taskbench/taskBench.go +++ b/pkg/service/taskbench/taskBench.go @@ -383,8 +383,8 @@ type CreateWorkAnalysisWithTaskUUIDReq struct { type CreateCompetitiveReportWithTaskUUIDReq struct { *cast.CreateCompetitiveReportReq - AssignRecordsUUID string `json:"assignRecordsUUID"` - ReportData utils.CompetitorReportData `json:"reportData"` // AI生成的竞品报告数据 + AssignRecordsUUID string `json:"assignRecordsUUID"` + ReportData utils.CompetitorReportData `json:"reportData,json_data"` // AI生成的竞品报告数据 } func UpdateWorkImageWithTaskUUID(ctx *gin.Context) { From ece9a964f9ac314e4a50e9e21dad8fcf96eeb4b7 Mon Sep 17 00:00:00 2001 From: cjy Date: Mon, 2 Mar 2026 17:07:17 +0800 Subject: [PATCH 21/38] =?UTF-8?q?feat:=20ai=E7=94=9F=E6=88=90=E4=B8=80?= =?UTF-8?q?=E4=B8=AA=E6=95=B0=E6=8D=AE=E5=88=86=E6=9E=90=E6=AD=A3=E6=96=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/service/cast/analysis.go | 93 ++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/pkg/service/cast/analysis.go b/pkg/service/cast/analysis.go index f9eb76e0..ae13a631 100644 --- a/pkg/service/cast/analysis.go +++ b/pkg/service/cast/analysis.go @@ -10,9 +10,11 @@ import ( "fonchain-fiee/api/bundle" "fonchain-fiee/api/cast" "fonchain-fiee/pkg/cache" + "fonchain-fiee/pkg/common/qwen" "fonchain-fiee/pkg/e" modelCast "fonchain-fiee/pkg/model/cast" "fonchain-fiee/pkg/model/login" + modelQwen "fonchain-fiee/pkg/model/qwen" "fonchain-fiee/pkg/service" "fonchain-fiee/pkg/service/bundle/common" "fonchain-fiee/pkg/utils" @@ -607,10 +609,101 @@ func ArtistMetricsSeries(ctx *gin.Context) { respMap["videoCount"] = videoCount respMap["imageCount"] = imageCount + // 调用 AI 分析数据 + analysis, err := generateArtistMetricsAnalysis(resp) + if err != nil { + zap.L().Error("生成艺人指标分析失败", zap.Error(err)) + // AI 分析失败不影响主业务,返回空字符串 + respMap["analysis"] = "" + } else { + respMap["analysis"] = analysis + } + service.Success(ctx, respMap) return } +// generateArtistMetricsAnalysis 调用 AI 分析艺人指标数据 +func generateArtistMetricsAnalysis(resp *cast.ArtistMetricsSeriesResp) (string, error) { + if resp == nil { + return "", errors.New("数据为空") + } + + // 构建分析用的数据摘要 + var dataSummary strings.Builder + dataSummary.WriteString("艺人各平台数据表现如下:\n") + + // 粉丝数 + if resp.FansSeries != nil { + dataSummary.WriteString(fmt.Sprintf("粉丝数总数: %d (周期类型: %d, 开始日期: %d, 结束日期: %d)\n", + resp.FansSeries.FansCount, resp.FansSeries.PeriodType, resp.FansSeries.StartDate, resp.FansSeries.EndDate)) + } + + // 播放量 + if resp.ViewsSeries != nil { + dataSummary.WriteString(fmt.Sprintf("播放量总数: %d (周期类型: %d, 开始日期: %d, 结束日期: %d)\n", + resp.ViewsSeries.ViewsCount, resp.ViewsSeries.PeriodType, resp.ViewsSeries.StartDate, resp.ViewsSeries.EndDate)) + } + + // 点赞数 + if resp.LikesSeries != nil { + dataSummary.WriteString(fmt.Sprintf("点赞数总数: %d (周期类型: %d, 开始日期: %d, 结束日期: %d)\n", + resp.LikesSeries.LikesCount, resp.LikesSeries.PeriodType, resp.LikesSeries.StartDate, resp.LikesSeries.EndDate)) + } + + // 评论数 + if resp.CommentsSeries != nil { + dataSummary.WriteString(fmt.Sprintf("评论数总数: %d (周期类型: %d, 开始日期: %d, 结束日期: %d)\n", + resp.CommentsSeries.CommentsCount, resp.CommentsSeries.PeriodType, resp.CommentsSeries.StartDate, resp.CommentsSeries.EndDate)) + } + + // 分享数 + if resp.SharesSeries != nil { + dataSummary.WriteString(fmt.Sprintf("分享数总数: %d (周期类型: %d, 开始日期: %d, 结束日期: %d)\n", + resp.SharesSeries.SharesCount, resp.SharesSeries.PeriodType, resp.SharesSeries.StartDate, resp.SharesSeries.EndDate)) + } + + // 最佳发布时间 + if resp.BestPostTime != nil { + dataSummary.WriteString(fmt.Sprintf("最佳发布时间: %s\n", resp.BestPostTime.DetailJSON)) + } + + // 最活跃日期 + if resp.MostActiveDay != nil { + dataSummary.WriteString(fmt.Sprintf("最活跃日期: %s\n", resp.MostActiveDay.DetailJSON)) + } + + // 构建 prompt + prompt := fmt.Sprintf(`根据以下艺人各平台运营数据分析运营的各平台数据表现,结合相关数据简要表述优点,字数在200字内:\n%s`, dataSummary.String()) + + // 调用 AI + req := modelQwen.ChatRequest{ + Model: "qwen-plus", + Messages: []modelQwen.Message{ + { + Role: "user", + Content: []modelQwen.Content{ + { + Type: "text", + Text: prompt, + }, + }, + }, + }, + } + + respAI, err := qwen.Chat(req) + if err != nil { + return "", err + } + + if respAI == nil || len(respAI.Choices) == 0 { + return "", errors.New("AI 返回结果为空") + } + + return respAI.Choices[0].Message.Content, nil +} + // ArtistMetricsDailyWindow 艺人指标日窗口 func ArtistMetricsDailyWindow(ctx *gin.Context) { var req *cast.ArtistMetricsDailyWindowReq From 7cc03c02fa4be6f2dba0687739876928f6c19ed8 Mon Sep 17 00:00:00 2001 From: cjy Date: Tue, 3 Mar 2026 09:17:50 +0800 Subject: [PATCH 22/38] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96=E6=8F=90?= =?UTF-8?q?=E7=A4=BA=E8=AF=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/service/ai/video_vl.go | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/pkg/service/ai/video_vl.go b/pkg/service/ai/video_vl.go index 8fa0eeef..b8441d1d 100644 --- a/pkg/service/ai/video_vl.go +++ b/pkg/service/ai/video_vl.go @@ -100,21 +100,21 @@ type DataPerformanceData struct { // CompetitorReportResponse 竞品报告响应数据 type CompetitorReportResponse struct { - ImageURL string `json:"image_url,omitempty"` // 生成的图片URL(1024*1024),非必须返回 - Text string `json:"text,omitempty"` // 竞品报告文本内容,非必须返回 - JsonData *CompetitorReportData `json:"json_data,omitempty"` // 竞品报告JSON数据 + ImageURL string `json:"image_url,omitempty"` // 生成的图片URL(1024*1024),非必须返回 + Text string `json:"text,omitempty"` // 竞品报告文本内容,非必须返回 + JsonData *CompetitorReportData `json:"json_data,omitempty"` // 竞品报告JSON数据 } // CompetitorReportJSON AI返回的JSON结构 type CompetitorReportJSON struct { HighlightAnalysis HighlightAnalysis `json:"highlight_analysis"` - DataPerformance DataPerformance `json:"data_performance_analysis"` - OverallSummary string `json:"overall_summary_and_optimization"` + DataPerformance DataPerformance `json:"data_performance_analysis"` + OverallSummary string `json:"overall_summary_and_optimization"` } type HighlightAnalysis struct { - Summary string `json:"summary"` - Points Points `json:"points"` + Summary string `json:"summary"` + Points Points `json:"points"` } type Points struct { @@ -127,9 +127,9 @@ type Points struct { } type DataPerformance struct { - Views string `json:"views"` - Completion string `json:"completion_rate,omitempty"` - Engagement string `json:"engagement"` + Views string `json:"views"` + Completion string `json:"completion_rate,omitempty"` + Engagement string `json:"engagement"` } // convertJSONToText 将 JSON 转换为纯文本格式 @@ -321,9 +321,14 @@ func AICompetitorReport(ctx *gin.Context) { isVideo := len(req.Videos) > 0 // 构建文本生成提示词:理解内容 + 用户要求(JSON格式) + // 重要:必须明确要求使用英文标点符号,确保返回的JSON符合规范 + // 重要:必须基于内容给出分析性回复,即使没有提供具体数据 var textPrompt string if isVideo { textPrompt = fmt.Sprintf(`你必须严格输出以下JSON格式,不要输出任何其他内容。输出必须以 { 开头并以 } 结束。 +重要提示: +1. 所有字符串值必须使用英文标点符号,包括英文逗号, 英文句号. 英文冒号: 英文引号" 等。禁止使用中文标点符号。 +2. 即使没有提供具体数据,也要基于视频和图片内容给出分析性回复。禁止回复"未提供数据"、"暂无数据"等类似内容,而应该根据内容分析数据表现(如根据时长分析完播率潜力、根据内容质量分析互动潜力等)。 基于以下视频和图片的内容描述: %s @@ -334,7 +339,10 @@ func AICompetitorReport(ctx *gin.Context) { JSON结构是固定的,请将内容填充到对应的value中,禁止修改key,禁止添加额外字段,禁止输出任何说明文字: {"highlight_analysis":{"summary":"[78字以内的概述]","points":{"theme":"[标题亮点,最多60字]","narrative":"[题材亮点,最多60字]","content":"[内容亮点,最多60字]","copywriting":"[文案亮点,最多60字]","data":"[数据亮点,最多60字]","music":"[配乐亮点,仅视频,最多60字]"}},"data_performance_analysis":{"views":"[浏览量表现,最多60字]","completion_rate":"[完播率表现,仅视频,最多60字]","engagement":"[点赞/分享/评论表现,最多60字]"},"overall_summary_and_optimization":"[整体总结及可优化建议,最多300字]"}`, vlContent, req.TextPrompt) } else { - textPrompt = fmt.Sprintf(`你必须严格输出以下JSON格式,不要输出任何其他内容。输出必须以 { 开头并以 } 结束。 + textPrompt = fmt.Sprintf(`你必须严格输出以下JSON格式,不要输出任何其他内容。输出开头并以 }必须以 { 结束。 +重要提示: +1. 所有字符串值必须使用英文标点符号,包括英文逗号, 英文句号. 英文冒号: 英文引号" 等。禁止使用中文标点符号。 +2. 即使没有提供具体数据,也要基于视频和图片内容给出分析性回复。禁止回复"未提供数据"、"暂无数据"等类似内容,而应该根据内容分析数据表现(如根据内容质量分析互动潜力等)。 基于以下视频和图片的内容描述: %s From 51f82425c566e036dfef1a33ae201de0cce51755 Mon Sep 17 00:00:00 2001 From: cjy Date: Tue, 3 Mar 2026 10:09:32 +0800 Subject: [PATCH 23/38] =?UTF-8?q?fix=EF=BC=9A=20=E5=9B=BA=E5=AE=9A?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E6=A8=A1=E6=9D=BF=E7=94=9F=E6=88=90=E6=8A=A5?= =?UTF-8?q?=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/service/cast/report.go | 80 +++++++++----------------------------- 1 file changed, 19 insertions(+), 61 deletions(-) diff --git a/pkg/service/cast/report.go b/pkg/service/cast/report.go index c3147d37..fdea3c89 100644 --- a/pkg/service/cast/report.go +++ b/pkg/service/cast/report.go @@ -34,8 +34,8 @@ import ( // CreateCompetitiveReportReqEx 扩展的竞品报告请求(包含AI生成的JSON数据) type CreateCompetitiveReportReqEx struct { - *cast.CreateCompetitiveReportReq // 嵌入原有请求 - ReportData utils.CompetitorReportData `json:"reportData,json_data"` // AI生成的竞品报告数据(支持 reportData 和 json_data 两种字段名) + *cast.CreateCompetitiveReportReq // 嵌入原有请求 + ReportData utils.CompetitorReportData `json:"reportData,json_data"` // AI生成的竞品报告数据(支持 reportData 和 json_data 两种字段名) } // CreateCompetitiveReport 创建竞品报告 @@ -111,11 +111,10 @@ func CreateCompetitiveReportCore(ctx *gin.Context, req *cast.CreateCompetitiveRe } req.BundleOrderUuid = resp1.OrderUUID - // 验证:报告内容、AI生成的JSON数据和图片不能同时为空 - // 检查 reportData 是否有数据 + // 验证:必须传入 json_data(使用模板方式生成PDF) hasReportData := reportData.OverallSummary != "" || reportData.HighlightAnalysis.Summary != "" - if req.ReportContent == "" && !hasReportData && req.ImageUrl == "" { - return nil, errors.New("报告内容、AI数据和图片不能同时为空") + if !hasReportData { + return nil, errors.New("请传入json_data参数") } if req.ImageUrl != "" { @@ -127,21 +126,18 @@ func CreateCompetitiveReportCore(ctx *gin.Context, req *cast.CreateCompetitiveRe req.ImageUrl = newImageUrl } - // 判断使用哪种方式生成PDF - // 检查 reportData 是否有数据(通过判断 OverallSummary 是否为空) - if reportData.OverallSummary != "" || reportData.HighlightAnalysis.Summary != "" { - // 使用 GenerateCompetitorReportPDF 生成PDF - zap.L().Info("reportData内容", zap.Any("reportData", reportData)) + // 使用模板方式生成PDF + zap.L().Info("reportData内容", zap.Any("reportData", reportData)) - // 直接使用传入的结构体数据 - competitorReportData := reportData + // 直接使用传入的结构体数据 + competitorReportData := reportData - // 如果有图片URL,设置到reportData中 - if req.ImageUrl != "" { - competitorReportData.ImageURL = req.ImageUrl - } + // 如果有图片URL,设置到reportData中 + if req.ImageUrl != "" { + competitorReportData.ImageURL = req.ImageUrl + } - zap.L().Info("解析成功", zap.Any("competitorReportData", competitorReportData)) + zap.L().Info("解析成功", zap.Any("competitorReportData", competitorReportData)) today := time.Now().Format("20060102") timestamp := time.Now().UnixMicro() @@ -173,50 +169,12 @@ func CreateCompetitiveReportCore(ctx *gin.Context, req *cast.CreateCompetitiveRe } }() - pdfUrl, uploadErr := upload.PutBos(pdfFilePath, upload.PdfType, true) - if uploadErr != nil { - zap.L().Error("上传PDF失败: %v", zap.Error(uploadErr)) - return nil, errors.New("上传PDF失败") - } - req.PdfUrl = pdfUrl - } else if req.ReportContent != "" { - // 使用原有的 GeneratePDF 生成PDF - today := time.Now().Format("20060102") - timestamp := time.Now().UnixMicro() - pdfFileName := fmt.Sprintf("%s%s老师的竞品报告%d.pdf", today, req.ArtistName, timestamp) - pdfFilePath := "./runtime/report_pdf/" + pdfFileName - - _, err = utils.CheckDirPath("./runtime/report_pdf/", true) - if err != nil { - return nil, fmt.Errorf("创建PDF目录失败: %v", err) - } - - fontPath := "./data/simfang.ttf" - err = utils.GeneratePDF(req.ReportContent, req.ImageUrl, pdfFilePath, fontPath) - if err != nil { - zap.L().Error("生成PDF失败", zap.Error(err)) - return nil, errors.New("生成PDF失败") - } - - defer func() { - if _, err := os.Stat(pdfFilePath); err == nil { - if err := os.Remove(pdfFilePath); err != nil { - zap.L().Warn("删除临时PDF文件失败", zap.String("path", pdfFilePath), zap.Error(err)) - } else { - zap.L().Info("删除临时PDF文件成功", zap.String("path", pdfFilePath)) - } - } - }() - - pdfUrl, uploadErr := upload.PutBos(pdfFilePath, upload.PdfType, true) - if uploadErr != nil { - zap.L().Error("上传PDF失败: %v", zap.Error(uploadErr)) - return nil, errors.New("上传PDF失败") - } - req.PdfUrl = pdfUrl - } else { - req.PdfUrl = req.ImageUrl + pdfUrl, uploadErr := upload.PutBos(pdfFilePath, upload.PdfType, true) + if uploadErr != nil { + zap.L().Error("上传PDF失败: %v", zap.Error(uploadErr)) + return nil, errors.New("上传PDF失败") } + req.PdfUrl = pdfUrl resp, err := service.CastProvider.CreateCompetitiveReport(newCtx, req) if err != nil { From a69815bea27cdf3564d4318cff1379797e5ac3df Mon Sep 17 00:00:00 2001 From: cjy Date: Tue, 3 Mar 2026 10:20:58 +0800 Subject: [PATCH 24/38] =?UTF-8?q?fix=EF=BC=9A=E4=BF=AE=E6=94=B9json=20tag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/service/cast/report.go | 2 +- pkg/service/taskbench/taskBench.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/service/cast/report.go b/pkg/service/cast/report.go index fdea3c89..064d0fb7 100644 --- a/pkg/service/cast/report.go +++ b/pkg/service/cast/report.go @@ -35,7 +35,7 @@ import ( // CreateCompetitiveReportReqEx 扩展的竞品报告请求(包含AI生成的JSON数据) type CreateCompetitiveReportReqEx struct { *cast.CreateCompetitiveReportReq // 嵌入原有请求 - ReportData utils.CompetitorReportData `json:"reportData,json_data"` // AI生成的竞品报告数据(支持 reportData 和 json_data 两种字段名) + ReportData utils.CompetitorReportData `json:"json_data"` // AI生成的竞品报告数据(支持 reportData 和 json_data 两种字段名) } // CreateCompetitiveReport 创建竞品报告 diff --git a/pkg/service/taskbench/taskBench.go b/pkg/service/taskbench/taskBench.go index d9a08f38..dded7164 100644 --- a/pkg/service/taskbench/taskBench.go +++ b/pkg/service/taskbench/taskBench.go @@ -384,7 +384,7 @@ type CreateWorkAnalysisWithTaskUUIDReq struct { type CreateCompetitiveReportWithTaskUUIDReq struct { *cast.CreateCompetitiveReportReq AssignRecordsUUID string `json:"assignRecordsUUID"` - ReportData utils.CompetitorReportData `json:"reportData,json_data"` // AI生成的竞品报告数据 + ReportData utils.CompetitorReportData `json:"json_data"` // AI生成的竞品报告数据 } func UpdateWorkImageWithTaskUUID(ctx *gin.Context) { From d9f8e69a11b6a3f2b958fba024e3075beeec290c Mon Sep 17 00:00:00 2001 From: cjy Date: Tue, 3 Mar 2026 10:29:10 +0800 Subject: [PATCH 25/38] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96=E6=8F=90?= =?UTF-8?q?=E7=A4=BA=E8=AF=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/service/cast/analysis.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/service/cast/analysis.go b/pkg/service/cast/analysis.go index ae13a631..984a1deb 100644 --- a/pkg/service/cast/analysis.go +++ b/pkg/service/cast/analysis.go @@ -673,8 +673,11 @@ func generateArtistMetricsAnalysis(resp *cast.ArtistMetricsSeriesResp) (string, dataSummary.WriteString(fmt.Sprintf("最活跃日期: %s\n", resp.MostActiveDay.DetailJSON)) } + // 平台枚举说明 + platformDesc := "平台枚举说明:1=TIKTOK, 2=YouTube, 3=Instagram, 4=Dailymotion, 5=BULESKY" + // 构建 prompt - prompt := fmt.Sprintf(`根据以下艺人各平台运营数据分析运营的各平台数据表现,结合相关数据简要表述优点,字数在200字内:\n%s`, dataSummary.String()) + prompt := fmt.Sprintf(`%s\n根据以下艺人各平台运营数据分析运营的各平台数据表现,结合相关数据简要表述优点,字数在200字内。注意:回复时请使用平台名称(如TIKTOK、YOUTUBE、INS、Dailymotion、BULESKY)而非数字:\n%s`, platformDesc, dataSummary.String()) // 调用 AI req := modelQwen.ChatRequest{ From bbc18f56afdff468101533e2e8ee1d72fdc74c61 Mon Sep 17 00:00:00 2001 From: cjy Date: Tue, 3 Mar 2026 10:33:34 +0800 Subject: [PATCH 26/38] =?UTF-8?q?fix=EF=BC=9A=E8=B0=83=E6=95=B4=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E6=8F=90=E7=A4=BA=E8=AF=AD=EF=BC=8C=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=E5=8C=96=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/service/cast/report.go | 51 +++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/pkg/service/cast/report.go b/pkg/service/cast/report.go index 064d0fb7..1415983b 100644 --- a/pkg/service/cast/report.go +++ b/pkg/service/cast/report.go @@ -114,7 +114,7 @@ func CreateCompetitiveReportCore(ctx *gin.Context, req *cast.CreateCompetitiveRe // 验证:必须传入 json_data(使用模板方式生成PDF) hasReportData := reportData.OverallSummary != "" || reportData.HighlightAnalysis.Summary != "" if !hasReportData { - return nil, errors.New("请传入json_data参数") + return nil, errors.New("参数错误") } if req.ImageUrl != "" { @@ -128,6 +128,7 @@ func CreateCompetitiveReportCore(ctx *gin.Context, req *cast.CreateCompetitiveRe // 使用模板方式生成PDF zap.L().Info("reportData内容", zap.Any("reportData", reportData)) + fmt.Println(reportData) // 直接使用传入的结构体数据 competitorReportData := reportData @@ -139,35 +140,35 @@ func CreateCompetitiveReportCore(ctx *gin.Context, req *cast.CreateCompetitiveRe zap.L().Info("解析成功", zap.Any("competitorReportData", competitorReportData)) - today := time.Now().Format("20060102") - timestamp := time.Now().UnixMicro() - pdfFileName := fmt.Sprintf("%s%s老师的竞品报告%d.pdf", today, req.ArtistName, timestamp) - pdfFilePath := "./runtime/report_pdf/" + pdfFileName + today := time.Now().Format("20060102") + timestamp := time.Now().UnixMicro() + pdfFileName := fmt.Sprintf("%s%s老师的竞品报告%d.pdf", today, req.ArtistName, timestamp) + pdfFilePath := "./runtime/report_pdf/" + pdfFileName - _, err = utils.CheckDirPath("./runtime/report_pdf/", true) - if err != nil { - return nil, fmt.Errorf("创建PDF目录失败: %v", err) - } + _, err = utils.CheckDirPath("./runtime/report_pdf/", true) + if err != nil { + return nil, fmt.Errorf("创建PDF目录失败: %v", err) + } - // 模板路径 - templatePath := "./data/竞品报告pdf模板.pdf" + // 模板路径 + templatePath := "./data/竞品报告pdf模板.pdf" - // 调用 GenerateCompetitorReportPDF - err = utils.GenerateCompetitorReportPDF(templatePath, pdfFilePath, competitorReportData) - if err != nil { - zap.L().Error("生成PDF失败", zap.Error(err)) - return nil, errors.New("生成PDF失败") - } + // 调用 GenerateCompetitorReportPDF + err = utils.GenerateCompetitorReportPDF(templatePath, pdfFilePath, competitorReportData) + if err != nil { + zap.L().Error("生成PDF失败", zap.Error(err)) + return nil, errors.New("生成PDF失败") + } - defer func() { - if _, err := os.Stat(pdfFilePath); err == nil { - if err := os.Remove(pdfFilePath); err != nil { - zap.L().Warn("删除临时PDF文件失败", zap.String("path", pdfFilePath), zap.Error(err)) - } else { - zap.L().Info("删除临时PDF文件成功", zap.String("path", pdfFilePath)) - } + defer func() { + if _, err := os.Stat(pdfFilePath); err == nil { + if err := os.Remove(pdfFilePath); err != nil { + zap.L().Warn("删除临时PDF文件失败", zap.String("path", pdfFilePath), zap.Error(err)) + } else { + zap.L().Info("删除临时PDF文件成功", zap.String("path", pdfFilePath)) } - }() + } + }() pdfUrl, uploadErr := upload.PutBos(pdfFilePath, upload.PdfType, true) if uploadErr != nil { From 08a70e839960e925f328fbbeddb1c28f943e3196 Mon Sep 17 00:00:00 2001 From: cjy Date: Tue, 3 Mar 2026 10:56:46 +0800 Subject: [PATCH 27/38] =?UTF-8?q?=E5=B9=B6=E5=8F=91=E6=9F=A5=E8=AF=A2?= =?UTF-8?q?=EF=BC=8C=E4=BC=98=E5=8C=96=E6=8E=A5=E5=8F=A3=E9=80=9F=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/service/cast/analysis.go | 60 ++++++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 20 deletions(-) diff --git a/pkg/service/cast/analysis.go b/pkg/service/cast/analysis.go index 984a1deb..7378f0ce 100644 --- a/pkg/service/cast/analysis.go +++ b/pkg/service/cast/analysis.go @@ -563,44 +563,64 @@ func ArtistMetricsSeries(ctx *gin.Context) { subNum = infoResp.SubNum } - subInfoResp, err1 := service.AccountFieeProvider.SubNumGetInfo(context.Background(), &accountFiee.SubNumGetInfoRequest{ - SubNum: subNum, - Domain: "app", - }) + var subInfoResp *accountFiee.UserInfoResponse + var subInfoErr error + var dataListResp *cast.ArtistDataListResp + var metricsResp *cast.ArtistMetricsSeriesResp + var dataListErr, metricsErr error - if err1 != nil { - err1 = errors.New("自媒体用户查询失败") - service.Error(ctx, err1) + wg := sync.WaitGroup{} + wg.Add(3) + + // 并行调用 SubNumGetInfo、ArtistDataList、ArtistMetricsSeries + go func() { + defer wg.Done() + subInfoResp, subInfoErr = service.AccountFieeProvider.SubNumGetInfo(context.Background(), &accountFiee.SubNumGetInfoRequest{ + SubNum: subNum, + Domain: "app", + }) + }() + + go func() { + defer wg.Done() + dataListResp, dataListErr = service.CastProvider.ArtistDataList(context.Background(), &cast.ArtistDataListReq{ + SubNum: subNum, + Page: 1, + PageSize: 1, + }) + }() + + go func() { + defer wg.Done() + metricsResp, metricsErr = service.CastProvider.ArtistMetricsSeries(context.Background(), req) + }() + + wg.Wait() + + if subInfoErr != nil { + service.Error(ctx, errors.New("自媒体用户查询失败")) return } if subInfoResp == nil || subInfoResp.Id == 0 { - err1 = errors.New("自媒体用户不存在") - service.Error(ctx, err1) + service.Error(ctx, errors.New("自媒体用户不存在")) return } req.ArtistUUID = fmt.Sprint(subInfoResp.Id) - newCtx := NewCtxWithUserInfo(ctx) var accountConsumptionNumber int32 var videoCount int64 var imageCount int64 - dataListResp, err := service.CastProvider.ArtistDataList(newCtx, &cast.ArtistDataListReq{ - SubNum: subNum, - Page: 1, - PageSize: 1, - }) - if err == nil && dataListResp != nil && len(dataListResp.Data) > 0 && dataListResp.Data[0] != nil { + if dataListErr == nil && dataListResp != nil && len(dataListResp.Data) > 0 && dataListResp.Data[0] != nil { accountConsumptionNumber = dataListResp.Data[0].AccountConsumptionNumber videoCount = dataListResp.Data[0].VideoCount imageCount = dataListResp.Data[0].ImageCount } - resp, err := service.CastProvider.ArtistMetricsSeries(newCtx, req) - if err != nil { - err = errors.New("查询失败") - service.Error(ctx, err) + if metricsErr != nil { + service.Error(ctx, errors.New("查询失败")) return } + resp := metricsResp raw, _ := json.Marshal(resp) respMap := make(map[string]interface{}) From 76c5a9f6f3e9a06b85feefac208f9e745817f5b3 Mon Sep 17 00:00:00 2001 From: cjy Date: Tue, 3 Mar 2026 11:31:04 +0800 Subject: [PATCH 28/38] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96pdf=E6=8E=92?= =?UTF-8?q?=E7=89=88=E3=80=82=E8=8B=B1=E6=96=87=E7=AC=A6=E5=8F=B7=E5=92=8C?= =?UTF-8?q?=E6=95=B0=E5=AD=97=E5=8F=AA=E7=AE=97=E5=8D=8A=E4=B8=AA=E5=AD=97?= =?UTF-8?q?=E7=AC=A6=EF=BC=8C=E5=B9=B6=E6=B7=BB=E5=8A=A0=E5=8D=95=E5=85=83?= =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/utils/pdf.go | 77 ++++++++++++++++++-------------- pkg/utils/pdf_competitor_test.go | 52 ++++++++++++++++++++- 2 files changed, 93 insertions(+), 36 deletions(-) diff --git a/pkg/utils/pdf.go b/pkg/utils/pdf.go index 5afb9675..a1ce80d7 100644 --- a/pkg/utils/pdf.go +++ b/pkg/utils/pdf.go @@ -266,7 +266,7 @@ func GenerateCompetitorReportPDF(templatePath, outputPath string, data Competito // 概述 - 使用逐行写入,一行最多35个字 pdf.SetXY(200, 104) - summaryLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Summary), 35) + summaryLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Summary), 35.0) for i, line := range summaryLines { pdf.SetXY(200, 104+float64(i)*lineHeight) pdf.Cell(nil, line) @@ -274,7 +274,7 @@ func GenerateCompetitorReportPDF(templatePath, outputPath string, data Competito // 标题亮点 - 一行最多9个字 pdf.SetXY(200, 184) - themeLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Theme), 9) + themeLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Theme), 9.0) for i, line := range themeLines { pdf.SetXY(200, 184+float64(i)*lineHeight) pdf.Cell(nil, line) @@ -282,7 +282,7 @@ func GenerateCompetitorReportPDF(templatePath, outputPath string, data Competito // 题材亮点 - 一行最多9个字 pdf.SetXY(330, 184) - narrativeLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Narrative), 9) + narrativeLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Narrative), 9.0) for i, line := range narrativeLines { pdf.SetXY(330, 184+float64(i)*lineHeight) pdf.Cell(nil, line) @@ -290,7 +290,7 @@ func GenerateCompetitorReportPDF(templatePath, outputPath string, data Competito // 内容亮点 - 一行最多9个字 pdf.SetXY(460, 184) - contentLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Content), 9) + contentLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Content), 9.0) for i, line := range contentLines { pdf.SetXY(460, 184+float64(i)*lineHeight) pdf.Cell(nil, line) @@ -298,7 +298,7 @@ func GenerateCompetitorReportPDF(templatePath, outputPath string, data Competito // 文案亮点 - 一行最多9个字 pdf.SetXY(200, 323) - copywritingLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Copywriting), 9) + copywritingLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Copywriting), 9.0) for i, line := range copywritingLines { pdf.SetXY(200, 323+float64(i)*lineHeight) pdf.Cell(nil, line) @@ -306,7 +306,7 @@ func GenerateCompetitorReportPDF(templatePath, outputPath string, data Competito // 数据亮点 - 一行最多9个字 pdf.SetXY(330, 323) - dataLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Data), 9) + dataLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Data), 9.0) for i, line := range dataLines { pdf.SetXY(330, 323+float64(i)*lineHeight) pdf.Cell(nil, line) @@ -315,7 +315,7 @@ func GenerateCompetitorReportPDF(templatePath, outputPath string, data Competito // 配乐亮点(仅视频) - 一行最多9个字 if data.HighlightAnalysis.Points.Music != "" { pdf.SetXY(460, 323) - musicLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Music), 9) + musicLines := splitTextByRune(cleanTextForPDF(data.HighlightAnalysis.Points.Music), 9.0) for i, line := range musicLines { pdf.SetXY(460, 323+float64(i)*lineHeight) pdf.Cell(nil, line) @@ -324,7 +324,7 @@ func GenerateCompetitorReportPDF(templatePath, outputPath string, data Competito // 浏览量 - 一行最多35个字 pdf.SetXY(200, 474) - viewsLines := splitTextByRune(cleanTextForPDF(data.DataPerformance.Views), 35) + viewsLines := splitTextByRune(cleanTextForPDF(data.DataPerformance.Views), 35.0) for i, line := range viewsLines { pdf.SetXY(200, 474+float64(i)*lineHeight) pdf.Cell(nil, line) @@ -333,7 +333,7 @@ func GenerateCompetitorReportPDF(templatePath, outputPath string, data Competito // 完播率 - 一行最多35个字 pdf.SetXY(200, 539) if data.DataPerformance.Completion != "" { - completionLines := splitTextByRune(cleanTextForPDF(data.DataPerformance.Completion), 35) + completionLines := splitTextByRune(cleanTextForPDF(data.DataPerformance.Completion), 35.0) for i, line := range completionLines { pdf.SetXY(200, 539+float64(i)*lineHeight) pdf.Cell(nil, line) @@ -342,7 +342,7 @@ func GenerateCompetitorReportPDF(templatePath, outputPath string, data Competito // 点赞/分享/评论 - 一行最多35个字 pdf.SetXY(200, 600) - engagementLines := splitTextByRune(cleanTextForPDF(data.DataPerformance.Engagement), 35) + engagementLines := splitTextByRune(cleanTextForPDF(data.DataPerformance.Engagement), 35.0) for i, line := range engagementLines { pdf.SetXY(200, 600+float64(i)*lineHeight) pdf.Cell(nil, line) @@ -350,7 +350,7 @@ func GenerateCompetitorReportPDF(templatePath, outputPath string, data Competito // 整体总结及可优化建议 - 一行最多35个字 pdf.SetXY(200, 676) - overallSummaryLines := splitTextByRune(cleanTextForPDF(data.OverallSummary), 35) + overallSummaryLines := splitTextByRune(cleanTextForPDF(data.OverallSummary), 35.0) for i, line := range overallSummaryLines { pdf.SetXY(200, 676+float64(i)*lineHeight) pdf.Cell(nil, line) @@ -450,49 +450,58 @@ func GenerateCompetitorReportPDF(templatePath, outputPath string, data Competito return nil } -// splitTextByRune 将文本按指定字符数拆分成多行 -// 按每行最大字符数拆分,中文、英文都按 1 个 rune 计 -func splitTextByRune(text string, maxRunesPerLine int) []string { - if text == "" { - return []string{} +// getCharWidth 获取字符的宽度权重 +// 英文字母、数字、英文符号返回 0.5,其他字符返回 1.0 +func getCharWidth(r rune) float64 { + // 英文字母 (A-Z, a-z) + if (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') { + return 0.5 } - runes := []rune(text) - if len(runes) <= maxRunesPerLine { - return []string{text} + // 数字 (0-9) + if r >= '0' && r <= '9' { + return 0.5 } - 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])) + // 英文符号 + if (r >= 0x21 && r <= 0x2F) || // ! " # $ % & ' ( ) * + , - . / + (r >= 0x3A && r <= 0x40) || // : ; < = > ? @ + (r >= 0x5B && r <= 0x60) || // [ \ ] ^ _ ` + (r >= 0x7B && r <= 0x7E) { // { | } ~ + return 0.5 } - return lines + return 1.0 } -// wrapText 将文本按指定宽度换行(按字符数计算) -func wrapText(text string, maxLen int) []string { +// splitTextByRune 将文本按指定字符宽度拆分成多行 +// 按每行最大宽度拆分,英文字母/数字/英文符号按0.5计算,其他按1计算 +func splitTextByRune(text string, maxWidth float64) []string { if text == "" { return []string{} } + runes := []rune(text) var lines []string - runes := []rune(text) currentLine := "" + currentWidth := 0.0 for _, r := range runes { - // 如果当前行字符数达到最大限度,换行 - if len(currentLine) >= maxLen { - lines = append(lines, currentLine) + charWidth := getCharWidth(r) + + // 检查加上这个字符是否会超过最大宽度 + if currentWidth+charWidth > maxWidth { + // 超过最大宽度,保存当前行并开始新行 + if currentLine != "" { + lines = append(lines, currentLine) + } currentLine = string(r) + currentWidth = charWidth } else { currentLine += string(r) + currentWidth += charWidth } } // 添加最后一行 - if len(currentLine) > 0 { + if currentLine != "" { lines = append(lines, currentLine) } diff --git a/pkg/utils/pdf_competitor_test.go b/pkg/utils/pdf_competitor_test.go index 71521c44..5b8414fb 100644 --- a/pkg/utils/pdf_competitor_test.go +++ b/pkg/utils/pdf_competitor_test.go @@ -26,7 +26,7 @@ func getProjectRoot() string { } // TestGenerateCompetitorReportPDF 测试生成竞品报告PDF -func TestGenerateCompetitorReportPDF(t *testing.T) { +func TestGenerateCompetitorReportPDF1(t *testing.T) { // 获取项目根目录 root := getProjectRoot() fmt.Printf("项目根目录: %s\n", root) @@ -124,7 +124,7 @@ func TestGenerateCompetitorReportPDFImageOnly(t *testing.T) { // TestGenerateCompetitorReportPDFWithImage 测试带图片的竞品报告PDF // 注意:此测试需要网络连接来下载图片,如果网络不可用会被跳过 -func TestGenerateCompetitorReportPDFWithImage(t *testing.T) { +func TestGenerateCompetitorReportPDFWithImage1(t *testing.T) { // 获取项目根目录 root := getProjectRoot() @@ -173,3 +173,51 @@ func TestGenerateCompetitorReportPDFWithImage(t *testing.T) { fmt.Printf("PDF生成成功: %s\n", outputPath) } + +// TestGenerateCompetitorReportPDFMixedContent 测试中英混合、数字、符号的复杂情况 +func TestGenerateCompetitorReportPDFMixedContent(t *testing.T) { + root := getProjectRoot() + + // 准备包含中英混合、数字、符号的复杂测试数据 + data := CompetitorReportData{ + HighlightAnalysis: HighlightAnalysisData{ + Summary: "这是一个关于2024年短视频创作的爆款分析报告!视频总播放量达到1.2亿次,点赞率8.5%(远超行业平均3.2%)。内容包括:①生活技巧类占比40% ②知识科普类30% ③娱乐搞笑类30%。推荐算法推荐流量占比85%,搜索流量10%,其他渠道5%。", + Points: PointsData{ + Theme: "标题:【逆袭】3个月从0到100万粉丝!我是如何做到的?必看!🔥", + Narrative: "采用问题-解决叙事结构:前3秒抛出痛点你还在为XX发愁吗,然后展示解决方案ABC", + Content: "内容分为3个板块:①前置干货预告15秒 ②核心内容展示30秒 ③互动引导15秒", + Copywriting: "文案使用了大量emoji表情🔥💯👍,增加年轻化气息!结尾一句评论区见强化互动", + Data: "点赞率8.5%,高于同类平均3.2%,转发率2.1%,评论数2.3万条,评论区活跃度TOP10%", + Music: "使用热门BGM孤勇者前奏3秒,配合画面节奏卡点!音量为Original-3dB", + }, + }, + DataPerformance: DataPerformanceData{ + Views: "发布24小时内播放量突破500万,48小时达到1200万,72小时稳定在1500万。推荐流量占比85%,搜索流量占比10%,其他渠道5%。数据表现:PV=15000000,UV=8500000。", + Completion: "平均完播率45.2%,高于行业均值28%,3秒留存率72%,5秒完播率58%,10秒完播率35%,属于高质量流量。平均观看时长18.5秒,视频总时长42秒。", + Engagement: "点赞数128000,点赞率0.85%,评论数23000,其中神评论占比15%,高赞评论TOP3:①学到了 ②太棒了 ③收藏了,分享数56000。评论区@相关账号12个,引发二次传播。", + }, + OverallSummary: "该视频成功因素:①标题使用数字热门词激发点击逆袭必看 ②内容结构清晰,每15秒一个高潮点 ③BGM选择契合内容情绪孤勇者 ④评论区运营到位。建议优化:1)可增加合集功能,将同类内容串联 2)提升粉丝粘性到10%以上 3)适当增加直播预告。整体来看,这是一条典型的爆款体质视频,值得借鉴学习!数据支撑:ROI=3.5,CPM=25元,CPC=0.8元。", + ImageURL: "", + } + + // 模板路径 + templatePath := filepath.Join(root, "data", "竞品报告pdf模板.pdf") + // 输出路径 + outputPath := filepath.Join(root, "data", "output", "竞品报告测试_中英混合.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) +} From ce9d75ef46695da5e49f4cd8b6a9eaecb5954041 Mon Sep 17 00:00:00 2001 From: cjy Date: Tue, 3 Mar 2026 13:53:08 +0800 Subject: [PATCH 29/38] =?UTF-8?q?fix:=20=E4=BF=AE=E6=94=B9=E7=AB=9E?= =?UTF-8?q?=E5=93=81=E6=8A=A5=E5=91=8A=E6=A8=A1=E6=9D=BF=E9=A2=9C=E8=89=B2?= =?UTF-8?q?=E7=9A=84=E5=91=BD=E5=90=8D=E7=A9=BA=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/竞品报告pdf模板.pdf | Bin 68612 -> 43714 bytes data/竞品报告pdf模板_old.pdf | Bin 0 -> 68612 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 data/竞品报告pdf模板_old.pdf diff --git a/data/竞品报告pdf模板.pdf b/data/竞品报告pdf模板.pdf index c31daaceae4622e78bff9b3fce42a8eff283360f..42eece439a589ae3a9ac62816660d62e5c1fc935 100644 GIT binary patch literal 43714 zcmce;1z41Aw>CV(P|`|+z!*pg%+Q?@QX(lJ&5$z;(#?`#<*ai{LPG-RoM{igTT7-7~Uk-caO*^9U2M*6+SPA%a6;P_%_T zk%R=4R~_MMZe{Lj4&?`))PnMAn%f{;paMUC(eZLZKzZeXmyu{2sFV~D!qEzQA>SVY z|BuusDw4b+8PEE5(2(Ofp0)H>^C1w*xU*sC}0T};xiW%6c8{M zGUtN}2@6{bB18qT|4RLFYOIBRn@!uz!u8BJchuE@P0k+vWlCN(grkkCEfmhj2g6l5FeM^=wc(D@iteL3NnAlRU!KPTAWTq3x}qW9jW+ zDQd+gEd^Xq;Bpq(9{a2_Ex zK%}#kwV2io#a{w}-z3>=U0t2TczHcMJa|0#dC<-_yl_!bQC=7yFCQN_u!7sg%hA=` zliShd3b5aqB6fHFy3Emq=S(LaOEikt?hY@U2gdt*qZ`0+uwDa#2#Vd<+yd?9DtY(J`vJ!MXSD$j{L)(jSR)1msGYSJ zHb$keG0zR-2mb27gv114VrPL5{FLDRFDn6W0hgZT0^>3GPtcm|H!T+OC%$?NL|Lm9RC;T2xR3#{d87A1lkh^l)->MxKSDl_f6% ziNJ2@0=NhcC}L$PW{q}6nY#kX;N*m~vpfSSULaD~q$GeBU9q?zbr$HdNY|eakapOR z6GNIi+DNi_a$6y+&E1f$Y*N;C2n2Ng?W@^mfpd@2XP-Qp%SxR~s-LZj0e=28DAG>q z=Yun0ynk=-59U653dk{ltkR*K)C zD35k?1h5y%t7>QE0yTmPo>dX!-}v~mwESH~urL3DV>$Xh&CU3_yj>b_Qw=+UdW!M9CTL<^&b~xn3P% zWoQ0hE)Y4>LPQiRU(?(f5dXV6Y9U0xI%Tnu4dRlC~?h zEQj_FfLH)&d2gbC$}11p3gPLB-RsUR3wwkmupV2jwSY1L zyyR?$`Y+o3w$g74{H#P+OaD>0u-5o(XvMP<0_9ai0*c*0SfZ_f3jeEK@xl4vf7C0_ zm#YT#1;!uGXSCZnOrRpr@L$BmU0}`+@W>nyWN+Y5m~m;h{d{U7Y`d7Yn!@#cR#o{TiSg6 z;`-vg6I}TR^+e}k+=5_~jl4ZaZk>;>m%x#`uTSMYeJkHRf)mo|c8@Uej4QoGFUWi! zKK&}&^3BKZX=Jb4q*2C)>eoaNmzzgzc*$jR-?`T;%A6`kRCXG@hM={RNNad<13+@HAX5Q%1g&@N9u5B^QP$FZo$@+U;C4-&0TT@}h)n}n4Gu>Sl?uH!1-7&V0%$|Amv)+jd zCPvuFY!0Ot7c2XQJae;qLS+5<>(%YU`wmkW0-EA2AQi;OLYV!8j@iJnTOmY{>*z5r_$weoC3FR1~TU z55&-PF?lo~2SA_ZgMScqSL6Dn+nUd&>9R-aU-}V4P)KY`LZpnhr}72&mFkOJVUSah z9^n%>^<(CxCvr5w&H4N+mL%VDnMijB%7$zCcG5kK)1J#4g@KeC>OxU_8WQ% zHC-MQH`ThjURpq(yvtIK4f*`~T#4PJ`+gf2kD|IgcqDZS=4gMNz$r7yW|V%N&a%ii z89bOG$(qC51_Q{1l#-;yq%+9Y{9g09(S07IiQcm3$bUC+x(h zn9OOPx)~pk&3{ho{EEEh10fx*d$tR;EZ+;t*A<(-sY%{X9aM0s)VO^kZ6#>*N>^`5 zc={v0a!$=T@@Bq3Z5EffvJklZNNWqL@{m^MIj4I*u>@Cry_=~mbqjnV<{P;#N*b-_k5J71TstS}Ty7Wnd%@-%hooR|^@lproytR|md;X1C{Mk!R z;JK-M9RJ+Uy`s9I)^B9u8}#2%uPJ?4{BYyln{Y;DR)gzT2Gf+R9zL!c{s{WsIqrC| zW?b{~C&jxxVn(I97oH*-hFB$)qC+|R?Qd+I+%DhB+$+fAnUl zq44$V24Peaw%kzwvW1T&2_3LEbxkARhJ?t3tNvOnvJC%A< zmty4&hXe;@{1b>7T?R38<@5a_8WAIbr(tdE&gRY>tl)pLE(^>2OMgoU_VaEdmgsTGKgZt#j;C+yzAo+V44!d2f|`} zP(ixHkGhw^vE91F&%Un|Uz0~}%z=148a)eTByF9)nsGM{=%opglN1-rV+MoNMpmr| zAf**dRt4}m)ul>;r|%SyBn**8M3OQ)&a+u~+ZH7kgu;DY-Y02fpX*Ad7r6VO6xX4T zQLrTgMmgUwAmh6gNfF0n?*GQ;hxSG1dgd{$@1PwTyX zSC6(nE>}Ook=S4QK8B`wZz^%xWQy85IO%Vgr@Xj*5;HZkz4P70^l1N}tZ^JY$q>qW zw9=Ot#jN$<`qn6@sfSkDXFGwb`t1O-l*?>SvhNQ%X`c^|jGag8!4St&pVR7(krnm6 zvzg&j^8x$&0|XE!$FZ^oE<9uT7W{jVX-%J%&pKXB`z(HY3*lpZKD^(bz@fu<`sOd> zx9?BnZ|^EC5J#}R-X3q*O%ZuL_ym1?@pzBVP}-AGj@Ix}l^dDEEy3>7IJ81HwMo_vR&GzWklF#OV(w^&a1d z;38vl8qwW*D`1{#O3*FyT7`*~KuGNC6ngw|14btkuaUBFuA5imSsgPbZIZfB)J5N% zzL+HtN}Nxn6=&z95P10c&3A*MD~b(nOrx)y7#_h@osaQP@|X;v-a%51lo9uBsz`D) zUaQE$E}y&g4xA=OyYl+m^7X*P<41Yd46;S#@ckF!pX?`OzIA`#K(n|SK-=GW()jB5 zdqrF4>8Eo?&Nh2ABLUlxe16YA>h2G9|0EY&f0baBGcElaQ8 z{P8DM`;elbTa zfC<5Wu|h3=eklAGr_BJ|QUIuY+pw z!=QXWwfS4!GottRmHbe?zjPFY^8MBk5CI4}8`Wq5BKUsm2rTCNOGn`GUpfjxfz5ud z=NE?Z|7Mea>HB+@1MtwB^73-#E(j|q|4%)Q|626t=tK6VHq_c2>4KdM$wFZ~z)S_0 zra*abX{uwn|4#<`H}~bWH2-_iAE*7Zarjq}fDo;l04qiS+}g?2%^dm9LeHq`uP)@w zg}-h0Z_>17e+AY*>Gm&T{$$Gk;XHpF;m@)qJOOZGhF|pMt`jTGkpK;NdMhQ z&f@a7L4KVCYg8=+!29jo5og_s9WWhowzK@dX?+&GzvWEi9_h;JtZ9Em#+c$5^{hNAnXr$FYD)%$R|G(Jo7a;tza(^bE+|RZWE(jCh z5fO$06B-zgAOLm30;0k^KynGd_yl=G;8+L#V*{xOj9Sii0dn^zO!NN&Aa!#f#ITJ$ z7I%sz1Yt8ulT@j^gCXEcgVlnSAJ2^f9YP0wQfdI$dXhK zaQy(wm4EpWG1UZ7YzPFOi;mut)Nq;BTDtLYb@2Ekn89z~&HrNU*Q-?LWzbJwn_E$t(6Be|zszsvo>@v5D`VU)ys-QEfklhP)-3avr!zvu zBOR7HuRE5E$Xn9~v@MzvPQ`}f3n#-LQ_W{c&+C~9er}D)TwJ|d-}piVQc z1sGk>Oykohb@*8rJOdnrqL9aC2;PC~R#9schI}y0ugh`jo!5IkWQrm~4Rh#2XCw{C zk3t7^MMiq(i{|{Ha`$)q@r&>Xz@Z{M{QNMO z2vk^502t&8Kn37}0z5*(|G$ObU!3575`F>zzW|`=*LckHrSVho$mg;oA)8X6yoJr3 z<$m~=p_3pk+{PeW<=jfi*-J+18&TZSIa0gTQ4Cw#yZc@Owb>kU&*ah;251k|q$7!H znaw|6HAh`I8c%-vNV)BT+udK)TWSyDEBG zcjZ~LuQR?y&2P$P(EB`TT)6SkudTTFo6**;mANXLm(AMl?3*YQYJtS)cIiG5NqFl$ zj+)0YzV^blmx;NpyPQam=iDnydqNl=W)bgd@bpVWsqN-E>C=5rd0ykoNd4^MgRydwhH+Lb4Y3P8uGzJg=9y*Vex+<$>#Do8S7J(uaKshzoKNl{CO9BQr;r2uRL? z-d}JWb+~_3rjZAku@H3oBdHAaz#ZH20<-&)%^C*`MK>y@nMpUJ$@_{6phUtG`cJ?5 zOxu__o0)iW)w?%areB%<2x(nve?-(h_@%-unWa+tWq_Q-lNbLUjDHq>IXhPufPBcK zQBG(_?7UCpU#5FNd2_e3M98ZfsAAz7Tcv=xAnK1PAol(lb{^=4a&!Th8y~=BoPpY7 ziM{;=fGHNx{t-o$5q37Vz{C$I-hU(B*<29Ht8Zro+)+9c_G?-v%ny*#KLc*n_~DCO z5p>L)?9-dC;D#=Q|Rg*NZ|9#dW)fA{qG z!cI}WElGB`@AvhSi%TcPn^}wX8>gqi$Lp#pBZGO~hs20v-q+$*qe^kdmyYO;Rq9SA z`G%sF4@5=8eA*uI>@Y4(iWIc1560I`EF5HQlNO|R1){>=f&yq(dNbB~n6H#P4)if@ z$Ql-?EjnjDfs2FWqmv=4`akNe*g>quV-H>$efmeaWZr|=08nMf?U{!Q8Q4xKvVb1c~coosilP!O;JHr$7 zW_e#2=9bS%c(!a3cM#{4`)gu&q)GSQYVSXRzj4Il@5GHjM#lP=n47f^o7hX^%nqD<+nQoi-lES<71ZatXtD;LA8a9xv98 zN04973N0__wRi2Jo($;~M~`+KnyB_&VKmHgSbg3; zPweLLW_KFoX(7vPv-=KSA=n;-aWR3>;HE^my_j0J-X#Gz)jmAn?T|5_u*@v{B|)8d z&!HWtraHwsXx6G96wBkdKA|havH|g3Wv>os$MF{&bdXww^Np0O)bYi5paw0*G_yud z@-^0nYLJ**mB;!BQim3S{KxYIPci2oPi^V0j0ck4@L<@WrpUA2oJfSH(By%ku ze4|clfK#|1Us=He-oN23jgN^RJ65l)Owq$1lj#SIkEZR=8jM^Vw62a)l^(ep1`crJ zl;#WT?F+0HGP80i-AW`ihB*zsMCs6h@}7@Hk1w}--Ii5Hi1opvHoqL?w=+ZteVD&^ zaNBQR?6!Mmu{Ne4D5-VLX7??mc|pQM(x|erm)UuP6fabF@t6W+%^&=h$ez~fiYBPQ zqZWFOE;fIit$FQ+6z<;JAxQ^SmgFyMRhk3oBq3g@Ja`lBfJ-ppT9##>dD|E+*HZ-Y zSO&19lsVY#!ROvLkZFav{S~J^Y}GchpOYX)8xie(JMEb3!tZ}<`4fR|UC+vEXSl<7 zx&HHLBV*4FWXz;QJQD7U8|g<$IcD#EsuwCYNla>Q&LqT!qVyHc-0cBXW~T^!+ub^) z!UbHz0#3a%wCiex#CK0YZ-P2i0m0y+v)--O>oGg;#mth4nT z!a)*QDX~k=^=br9lAE@J3{AP>bq0?Si8v?r?gd4D9c@q6&V^RBhXr<+yOgJ5(2;!6 z`X|*pnG#{GM?vZ09bB8$LJm&D&VB@es38@18@?OYSa5u)SMIt?SDP~ZOpPE_p-v)? zjgeDiA16M`cX7dMI>DHG>d+TatF+!M&?@;bH5!}~NB7-3-Cs>Vuf@Ij(Lk$Oyl?4W zZr>0$9&uik{7tjHcJlSCcMYOX9?LLAs}OjTe(~>>y37(?Ogeji6z|BsaKLxi(yH8} z*oWr%sAx2v&zDZUcKlEh2}`kwbtZ#g@+$uAkh--?p26>e$2x!XkV#10zes9uexv(Z z{iCsZzf?!GQ8Dy*;)F+KUCFl{V#X(EVxXz*Ph&+o87PY5EzO%X%pk_c0}<~MdXbC9 z`Q;9G1gG@3wAT|zx`yKNxZx;}7YvxtBGAni_9R*!5i$Erj6J(Pj%xQ%e3+p0R-Ep8 zd9R^`ki@$^@?kH=s!>FHBr98o9u~WAiQHUe?=!#WK9Zh(bwua*Vf{)mK09VWoc9xw z$#{Z`zS^jqXhXlm=kY44us`t7-(8s4jV!T*^y5LU`e*&UuZiCS@yb@kKll$=lWG{{ zqpi4(gYqoR2soDI>_!lmCY2gBP`)e%hZp&kr1Wqja!9#*%gQY_-9oFLKSBGG#6eo) zrzr~aTNyH%LvTE6H-jpZX9<3K=i*9JEEw=lC&rZ~mkXzvz82>K$be+tgsEZlrOM9c zPJAQWvi_D2$vj@@^D*`PYw|1`HjR#lpN2bac7N1L8s!!>jp;^B={RNYISWr`Quj8O zguR$RkzU!buHZG&RBm3FW_NGl(c!aG1+fGMJ8r!lJO-i;$&8S6lwk{3A9j7h3QCM3 zUhfGQXLgYz5F(tORO}$;EZ3-Br^YwCCZ9Z(xXSAXIqJ)dlgH#^0(kQ8^cG5!x>Z=bdUIKPaT7~q;}lW1HL)q!0~OXM{rc}1jn zb%pflGpSgk#4k#g%42Lr)jYj!9FVEF2j65dPMD4G?se7mZEIgMS#De=@WnkbMpyd7 zLr~t!l0|~jNHE=-Tw-G*D?a?a+lAVU6H5K%jrlF~yG57HbWpab`DNNd$-KRxGAHin zDA9Efzy88JI~v@V@8l;3HqNlcIl(SmtsEjXJo9!?67uznj(^*Py?b06snZgCRDOb) zvOO(-bB-^+$3;5fF(y5HPmGgXP{Z~$eVV1_BW^Q~QH%yn zV(H>#Rge%1Nz+dJ|Racn;Yg#$U#zeLz?cv+>r6<@# zatAodp|wnGqPds%x+B(90QO&HciF&qx$jMZk)pA*S&?IXR!+AieQr-&%PY@_5!SJ= zDCaU)ZdHW}TAyEq_xMj7&}sD9UpA6gYPL4R8&{`oVtyBJ(v|r3=-Nxgv#va)rM77a=8~=wombRdIvJz6nlLZG;!i(`Uxd()6Vi^Ww z^T>5DsVz=w8i;!2M*SpuaIUyR5BpJ3LzlkkhHEI9tugXvKvW_}KUYl@f;cj5!LOwi3@!BKmDH~OK+ za{p2b_*`caNnG65!Beab038^^m+SYJswSshG6{O#?sqL2=SQ%Ac}|;aW=pB8E+aq0 z$|TF-?ZU!W8Qee(Uo-)8vV3Hi+OS0zi_A%Q)uo1HaCL9zs7HOw*gc4t0rG3c6X~*{ z6RAm?w@aXSwa5Cjy@R`W29QB$1gI*RRtH7OL?0JOZOeG>nNfL`)u`TX%c-mO!52w% ze_dpP4UVFal4|`Btk4?8Th?;d9T(!rA46SJXS!XYo6_>-DDv^*VXctEgN~lMSe~xO zb&tlxp|YPf`GtzchKxUFCGEgT_imZ(I_JDvJp9IyB4nUf8Qv(2H-pD}KNr%vX4Fcu z*2r>oFa-j%gc9aJYe8V*DMH|w%?*}l^Wu0)B8A}DAE9jClcF06Z znPw3O)-23MUjcmzWStWs$z3iE%Z8^CnylBDT_-FJ27PykyA+oI#JsRCg%RFfiCLUf zN06IBgGidb3=TAWYV<9nu)Q&`N~D%|?*%03m6AY9?s~39=lYF)kf6+g?4@cGS;oEL z&=too!t|WuKc4mK0S@lJx&z+MRN(>x;k5=(3U&mM$0VLi8Y z)Q4(=Z}E#V@O>pXH`#syI$ym`0u=gW&8*hB+x^q{e^b zAla|4czVa4kB=nYGndUn!-hPNZPOPOUH9V?Ob_oiVb1S^P1%N$f*DZzJo2nPZ88YjRA)%tD-M)a#y`I zODZz{(<7G52AagOYzJ~~YLML2JV8ZwZc=H$t-gR;7lvOy^#a0SVzAcWSp4YR50Uq4 zTx-?(1iuJDl|JvA9eYYO&^`c33Q2hdix}L+hZu06>3eclpZ1PVhzxH0V^WqTcXDDENsTxql+X72B3`Amc!y}r@Xu(W) zw(R8j*hnGOMp>_M`MUXTOm>aKdm65H`ny^_Z3v=I`n8f_FQag8eOQxM6Q1*-VYk@vwGiK!4#yg75k0(?dq+qJF)JNb#4PU(juD*oOo)a8 zvtNCOpQ$SVu+{tU(a&4{xOYSswk|G<83icyn;nE+_N;Zl>Gnl3ZAp>jR+Es@mwnVa zJ@1aT=52@yI*wcMcdx%1Lr`bd8ch8DG2jq|MuYn-(TgYr`&dL zwVbS4SePGk8W}ZZkCZy4({K!%UGxM0!FLm6O+9YKwKJ{w$0oZ^eixmw{qzQK9vCR! zpCPBDCsa_(o7$Z^A8bZu61OLeJjGj-9)nGstitJuI}Y&fJQb92?O^UY^kVhnYQ+6; zKXbGaqCX@@P|d-+HL=xrk7$G6%ugw`qr@E43Utg-?;=nU9(9so;BOD{$~J$Oqv>G< zSxgYsa}rU0;*q4Wn22F|sRyeRlhT1>1)RMGW*f*CQ4f6_;y%-+?^=T5+evuXv(l?Z z3%&-DA8pk@OWa)j3ao$;!s}SLk?YX~Kj#%p#d;v(u_h zOzf}1M?5CyfAVg|;@Z0t8;1Qs=O8S~2DB<8#$h@2sT<{nX&;2ovzR z$;#;QTCoS^73OG_^?8}N;Caj9uy)>FaJH-FJCoa{b?%p{6Mv|Osbn0#9;yGb){es4 zNN!l0^*KaMVapP&G&VrnCgbU$oswaykn^}=-)osXeaQgmvI|$~K!uWb1DEBE)Hfxo z!J1z1V`m({NS57}2CBTC+|x%*J@YsT+@F(cF2z6DDE`XS$waekI~a#xqR3Rs9gVF< zLTl1EJ3@Kq8w#xF5-?BNyT77^NOBEER9E3~OpTb42vEXjo|B;*_Z>7xmgGGAE}`p& z8`YZ>t4|3AgBdGp`-4aVt^HLq%%5NdEsH~eABdkCACgobGVY#@Jh(Hs1A(sP0k&(o z6;YIMk(9njaBytt*6td;JDMj4bE@M$5L(5Pj*0=sA5BBBdb;B}Y*81)D?4A`V~MlQ zSX50~6>;o`(d!u6f*7_m5NP#>eszyaU0N@y7VIaf7LH<^d}|iBy5zTuaet(BoR%XdC(7ZWqQoyeK(T}((71^UePS_&92nz&sZ)e35}S}1WTQDB`e zU)WuZysgds;+=5i1C1T@CrNjtnU;vM>x3dM1ZcI9ze>mOu_RDB@T!I5=qEcfepsI9l~h|fs%DH_+p9b7)bnzoA6R_X zX^r19Y=vWUYwJ9G4}mI zjd^NEp`FyVwY>MaW`PB8P)%lvkg)Yy+#>XccBP5tvIE{TqUu8+g+Kz)DbsNaw9F(M zZ`Z2}EIii7U;Qx^N!s$~WS5wN-<_kq>?AP*zX(=aHMBP~Dt|hbuyRS-NYME?C_pGC z>X=%4TdkAEH&=7uYB+P!vBj24ogyYCSfjHjzht|tZ4%%fIkF9v*C^Usc8F`qDaJ0z z%PXRadQW-W(eg-4efSvf%~@GRM8}*5Fi;^f_2yT+E*Z`6R^6{SWe&%NhjG7*=ULR0 z6EUG>s2Fd&vin>yk1OETQf>zS>Qy`-f;VuE9v!arfz#~y?`A=g{rI7+?M^M0BIlyv zN$b0jxs3#43Ep!5Ts(lG6B{|+Uc%uTHQ3THim~fl#E6|s_Xj-EaZvZ^700c z>ROzz+$JIc?7RZMyZ=IFwDSj~_ST0E)37nJJ-L91mTNU@wwPq_vKl9^-^*?;DEK1n zSAbk~?+C2`d=4MzC#7~);r0{JZcP>)RV2JG zPGrCCL!!^wE6W{U3W@5xq$Gb0aOAtZjY|g;jcce+0j`hSi;wBtl^9~&5_yVo&9I|A z+UR(39PmlS6nBI4q6A2_gn~CV~MUCa1cmQti2Ay_bv1Y}x^87XOz@x+( zN$U^mIc=Ezz)xuBTIhzuUZ7*a5dFKh(vKqWNVz zNeEfZJ6at4tKcj4yZ7iZ2_$~*j@SxK!&HCcG{chMsE5c6JN(X~S~?qmB#kqdY=_Fs z2O)&`J=ZKn-f28f63yz4UFXx`LxmfXHPfL*7$ysNdWZbjhWz4K8$oAd?~w^+mpV;a zcpiD|axs1nFVGemQ~v3WwDPPOy;HVgpTtPqBg(-)p?(#IuIuGHZUfD>G{(8aBcyJdYdDd;{Tka@=}k2LIcybzaCHf-HRzUi zH9CHAI0k4_K^icx*8*VQC|Bsc5A*xlWt+Dk-=x~?iQAghJMfn^WZOSS-yjNL^nJ9= z!l4cHp9G)yL)16M0n=IzGDUr2q*)?K#NHbcYa=q5U^yz$eOny%4(?L#W=5N@UsNDy z9XjS`q*J^qL08QM_#`?FeDYss-~j{4c{thF@wd28#=0H{pv2ng$+fq)DkZu6?_5PrL>Mj_#RcHGti#zrp z0H?a1>_~i%>snUf;dZDP#-mO(paFPt%UY;!T|DD5h)x=9VW3Thu8eMNa

-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 literal 68612 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 Date: Tue, 3 Mar 2026 14:04:41 +0800 Subject: [PATCH 30/38] =?UTF-8?q?fix:=20=E5=BD=93=E5=AF=BC=E5=85=A5?= =?UTF-8?q?=E7=AB=9E=E5=93=81=E6=8A=A5=E5=91=8A=E5=AD=97=E6=95=B0=E8=BF=87?= =?UTF-8?q?=E9=95=BF=EF=BC=8C=E4=BC=9A=E8=87=AA=E5=8A=A8=E6=88=AA=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/service/cast/report.go | 41 ++++++++++++++++ pkg/utils/pdf.go | 24 ++++++++++ pkg/utils/pdf_competitor_test.go | 81 ++++++++++++++++++++++++++++++++ 3 files changed, 146 insertions(+) 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) + } + }) + } +} From e7a037c640767a6fdfb1afdca979cd4203408346 Mon Sep 17 00:00:00 2001 From: cjy Date: Tue, 3 Mar 2026 14:08:56 +0800 Subject: [PATCH 31/38] =?UTF-8?q?fix=EF=BC=9A=E6=89=B9=E9=87=8F=E5=AF=BC?= =?UTF-8?q?=E5=85=A5=E7=9A=84=E6=97=B6=E5=80=99=EF=BC=8C=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E4=BC=9A=E5=AD=98=E5=85=A5=E6=95=B0=E6=8D=AE=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/service/cast/report.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/service/cast/report.go b/pkg/service/cast/report.go index a4331f62..f610b2da 100644 --- a/pkg/service/cast/report.go +++ b/pkg/service/cast/report.go @@ -497,6 +497,7 @@ func ImportCompetitiveReportBatch(ctx *gin.Context) { continue } competitorReportData.ImageURL = newImageUrl + temp.ImageUrl = newImageUrl } // 生成PDF并上传 From 758a59dd2a22b8ac404f797a3775e3b41ff60083 Mon Sep 17 00:00:00 2001 From: cjy Date: Tue, 3 Mar 2026 14:24:07 +0800 Subject: [PATCH 32/38] =?UTF-8?q?fix:=20=E4=BF=AE=E6=94=B9=E6=8F=90?= =?UTF-8?q?=E7=A4=BA=E8=AF=8D=EF=BC=8C=E7=BA=A6=E6=9D=9Fai=E7=9A=84?= =?UTF-8?q?=E5=9B=9E=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/service/cast/analysis.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/service/cast/analysis.go b/pkg/service/cast/analysis.go index 7378f0ce..16a0225f 100644 --- a/pkg/service/cast/analysis.go +++ b/pkg/service/cast/analysis.go @@ -697,7 +697,7 @@ func generateArtistMetricsAnalysis(resp *cast.ArtistMetricsSeriesResp) (string, platformDesc := "平台枚举说明:1=TIKTOK, 2=YouTube, 3=Instagram, 4=Dailymotion, 5=BULESKY" // 构建 prompt - prompt := fmt.Sprintf(`%s\n根据以下艺人各平台运营数据分析运营的各平台数据表现,结合相关数据简要表述优点,字数在200字内。注意:回复时请使用平台名称(如TIKTOK、YOUTUBE、INS、Dailymotion、BULESKY)而非数字:\n%s`, platformDesc, dataSummary.String()) + prompt := fmt.Sprintf(`%s\n根据以下艺人各平台运营数据分析运营的各平台数据表现,结合相关数据简要表述优点,字数在200字内(但是回复的时候不要返回具体的字数。注意:回复时请使用平台名称,如TIKTOK、INS而非数字:\n%s`, platformDesc, dataSummary.String()) // 调用 AI req := modelQwen.ChatRequest{ From 6af7e788d3e2c205699588dec414d9298724cc80 Mon Sep 17 00:00:00 2001 From: cjy Date: Tue, 3 Mar 2026 14:30:27 +0800 Subject: [PATCH 33/38] =?UTF-8?q?fix:=20=E4=BF=AE=E6=94=B9=E6=8F=90?= =?UTF-8?q?=E7=A4=BA=E8=AF=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/service/cast/analysis.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pkg/service/cast/analysis.go b/pkg/service/cast/analysis.go index 16a0225f..c269a172 100644 --- a/pkg/service/cast/analysis.go +++ b/pkg/service/cast/analysis.go @@ -693,11 +693,8 @@ func generateArtistMetricsAnalysis(resp *cast.ArtistMetricsSeriesResp) (string, dataSummary.WriteString(fmt.Sprintf("最活跃日期: %s\n", resp.MostActiveDay.DetailJSON)) } - // 平台枚举说明 - platformDesc := "平台枚举说明:1=TIKTOK, 2=YouTube, 3=Instagram, 4=Dailymotion, 5=BULESKY" - // 构建 prompt - prompt := fmt.Sprintf(`%s\n根据以下艺人各平台运营数据分析运营的各平台数据表现,结合相关数据简要表述优点,字数在200字内(但是回复的时候不要返回具体的字数。注意:回复时请使用平台名称,如TIKTOK、INS而非数字:\n%s`, platformDesc, dataSummary.String()) + prompt := fmt.Sprintf(`根据以下艺人各平台运营数据分析各平台数据表现,结合相关数据简要表述优点,字数在200字内(但是回复的时候不要返回具体的字数。注意:回复时请使用平台名称,如TIKTOK、INS等而非数字。重要:不要逐一列举所有平台名称,只需提及有亮点的平台即可:\n%s`, dataSummary.String()) // 调用 AI req := modelQwen.ChatRequest{ From 6f5c7f219c4cfd2635953fb21eb4b696b96adb3e Mon Sep 17 00:00:00 2001 From: cjy Date: Tue, 3 Mar 2026 15:25:47 +0800 Subject: [PATCH 34/38] =?UTF-8?q?fix:=20=E4=BF=AE=E6=94=B9=E5=AD=97?= =?UTF-8?q?=E6=95=B0=E9=99=90=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/service/ai/video_vl.go | 2 +- pkg/service/cast/report.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/service/ai/video_vl.go b/pkg/service/ai/video_vl.go index b8441d1d..00c2629f 100644 --- a/pkg/service/ai/video_vl.go +++ b/pkg/service/ai/video_vl.go @@ -351,7 +351,7 @@ JSON结构是固定的,请将内容填充到对应的value中,禁止修改ke %s JSON结构是固定的,请将内容填充到对应的value中,禁止修改key,禁止添加额外字段,禁止输出任何说明文字: -{"highlight_analysis":{"summary":"[78字以内的概述]","points":{"theme":"[标题亮点,最多60字]","narrative":"[题材亮点,最多60字]","content":"[内容亮点,最多60字]","copywriting":"[文案亮点,最多60字]","data":"[数据亮点,最多60字]"}},"data_performance_analysis":{"views":"[浏览量表现,最多60字]","engagement":"[点赞/分享/评论表现,最多60字]"},"overall_summary_and_optimization":"[整体总结及可优化建议,最多300字]"}`, vlContent, req.TextPrompt) +{"highlight_analysis":{"summary":"[100字以内的概述]","points":{"theme":"[标题亮点,最多60字]","narrative":"[题材亮点,最多60字]","content":"[内容亮点,最多60字]","copywriting":"[文案亮点,最多60字]","data":"[数据亮点,最多60字]"}},"data_performance_analysis":{"views":"[浏览量表现,最多60字]","engagement":"[点赞/分享/评论表现,最多60字]"},"overall_summary_and_optimization":"[整体总结及可优化建议,最多300字]"}`, vlContent, req.TextPrompt) } chatReq, err := buildChatRequest(textPrompt, nil) diff --git a/pkg/service/cast/report.go b/pkg/service/cast/report.go index f610b2da..241cbabc 100644 --- a/pkg/service/cast/report.go +++ b/pkg/service/cast/report.go @@ -1322,7 +1322,7 @@ func checkAndReuploadImageForReport(imageUrl string) (string, error) { func truncateCompetitorReportData(data utils.CompetitorReportData) utils.CompetitorReportData { // 字段长度限制 const ( - MaxSummary = 78 // 概述 + MaxSummary = 100 // 概述 MaxPointField = 60 // 标题/题材/内容/文案/数据/配乐亮点 MaxViews = 60 // 浏览量 MaxCompletion = 60 // 完播率 From e90d298f988039a0d51f6c7836ebd29dd3c44452 Mon Sep 17 00:00:00 2001 From: cjy Date: Tue, 3 Mar 2026 15:37:10 +0800 Subject: [PATCH 35/38] =?UTF-8?q?fix:=20=E4=BF=AE=E6=94=B9=E7=94=9F?= =?UTF-8?q?=E6=88=90=E7=AB=9E=E5=93=81=E6=8A=A5=E5=91=8A=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E5=90=8D=E7=9A=84=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/service/cast/report.go | 41 +++++++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/pkg/service/cast/report.go b/pkg/service/cast/report.go index 241cbabc..491b7405 100644 --- a/pkg/service/cast/report.go +++ b/pkg/service/cast/report.go @@ -143,9 +143,8 @@ func CreateCompetitiveReportCore(ctx *gin.Context, req *cast.CreateCompetitiveRe zap.L().Info("解析成功", zap.Any("competitorReportData", competitorReportData)) - today := time.Now().Format("20060102") - timestamp := time.Now().UnixMicro() - pdfFileName := fmt.Sprintf("%s%s老师的竞品报告%d.pdf", today, req.ArtistName, timestamp) + // 生成PDF文件名,使用报告标题命名 + pdfFileName := generateReportFileName(req.Title, req.ArtistName) + ".pdf" pdfFilePath := "./runtime/report_pdf/" + pdfFileName _, err = utils.CheckDirPath("./runtime/report_pdf/", true) @@ -501,10 +500,8 @@ func ImportCompetitiveReportBatch(ctx *gin.Context) { } // 生成PDF并上传 - // 生成临时PDF文件路径 - today := time.Now().Format("20060102") - timestamp := time.Now().UnixMicro() - pdfFileName := fmt.Sprintf("%s%s老师的竞品报告%d.pdf", today, temp.ArtistName, timestamp) + // 生成临时PDF文件路径,使用报告标题命名 + pdfFileName := generateReportFileName(temp.Title, temp.ArtistName) + ".pdf" pdfFilePath := "./runtime/report_pdf/" + pdfFileName // 确保目录存在 @@ -1317,6 +1314,36 @@ func checkAndReuploadImageForReport(imageUrl string) (string, error) { return compressUrl, nil } +// generateReportFileName 生成竞品报告PDF文件名 +// 如果有标题则使用标题,否则使用默认格式(日期+老师+时间戳) +func generateReportFileName(title, artistName string) string { + // 如果有标题,使用标题命名 + if title != "" { + // 替换标题中的特殊字符为合法字符 + fileName := strings.NewReplacer( + "/", "", + "\\", "", + ":", "", + "*", "", + "?", "", + "\"", "", + "<", "", + ">", "", + "|", "", + " ", "_", + ).Replace(title) + // 限制文件名长度,避免过长 + if len(fileName) > 100 { + fileName = fileName[:100] + } + return fileName + } + // 没有标题时使用默认格式 + today := time.Now().Format("20060102") + timestamp := time.Now().UnixMicro() + return fmt.Sprintf("%s%s老师的竞品报告%d", today, artistName, timestamp) +} + // truncateCompetitorReportData 截断竞品报告数据中超长的字段 // 字段长度要求参考 AI 生成竞品报告的限制 func truncateCompetitorReportData(data utils.CompetitorReportData) utils.CompetitorReportData { From 0515d2ab03a8cd107752e5cbd7c98edc7343b194 Mon Sep 17 00:00:00 2001 From: cjy Date: Tue, 3 Mar 2026 15:44:06 +0800 Subject: [PATCH 36/38] =?UTF-8?q?fix:=20=E6=96=87=E4=BB=B6=E5=90=8D?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=97=B6=E9=97=B4=E6=88=B3=EF=BC=8C=E9=81=BF?= =?UTF-8?q?=E5=85=8D=E6=96=87=E4=BB=B6=E8=A2=AB=E8=A6=86=E7=9B=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/service/cast/report.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/pkg/service/cast/report.go b/pkg/service/cast/report.go index 491b7405..b1ff97b6 100644 --- a/pkg/service/cast/report.go +++ b/pkg/service/cast/report.go @@ -1315,9 +1315,11 @@ func checkAndReuploadImageForReport(imageUrl string) (string, error) { } // generateReportFileName 生成竞品报告PDF文件名 -// 如果有标题则使用标题,否则使用默认格式(日期+老师+时间戳) +// 始终使用标题+时间戳格式,避免文件名冲突导致OSS覆盖 func generateReportFileName(title, artistName string) string { - // 如果有标题,使用标题命名 + timestamp := time.Now().UnixMicro() + + // 如果有标题,使用标题+时间戳 if title != "" { // 替换标题中的特殊字符为合法字符 fileName := strings.NewReplacer( @@ -1332,15 +1334,15 @@ func generateReportFileName(title, artistName string) string { "|", "", " ", "_", ).Replace(title) - // 限制文件名长度,避免过长 - if len(fileName) > 100 { - fileName = fileName[:100] + // 限制文件名长度,避免过长(预留时间戳的空间) + if len(fileName) > 80 { + fileName = fileName[:80] } - return fileName + return fmt.Sprintf("%s_%d", fileName, timestamp) } + // 没有标题时使用默认格式 today := time.Now().Format("20060102") - timestamp := time.Now().UnixMicro() return fmt.Sprintf("%s%s老师的竞品报告%d", today, artistName, timestamp) } From 02e00f9d4904ff70446f93cd71ae14f256fde9ab Mon Sep 17 00:00:00 2001 From: cjy Date: Tue, 3 Mar 2026 15:53:24 +0800 Subject: [PATCH 37/38] =?UTF-8?q?fix:=E5=A2=9E=E5=8A=A0=E6=A0=87=E9=A2=98?= =?UTF-8?q?=E9=95=BF=E5=BA=A6=E9=99=90=E5=88=B6=E8=A6=81=E6=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/service/cast/report.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/service/cast/report.go b/pkg/service/cast/report.go index b1ff97b6..99ae78ad 100644 --- a/pkg/service/cast/report.go +++ b/pkg/service/cast/report.go @@ -416,6 +416,13 @@ func ImportCompetitiveReportBatch(ctx *gin.Context) { // 截断超长字段(按AI生成的字段长度要求) competitorReportData = truncateCompetitorReportData(competitorReportData) + // 验证标题长度(数据库字段为varchar(50)) + if len(temp.Title) > 50 { + temp.Remark = "标题长度超出限制" + req.Reports = append(req.Reports, temp) + continue + } + // 验证必填字段 if artistNum == "" { temp.Remark = "艺人编号不能为空" From 919f0280bc7f8ad197bc53e62f217fd420d86d80 Mon Sep 17 00:00:00 2001 From: cjy Date: Wed, 4 Mar 2026 09:15:30 +0800 Subject: [PATCH 38/38] =?UTF-8?q?fix=EF=BC=9A=E4=BD=BF=E7=94=A8utf8?= =?UTF-8?q?=E8=AE=A1=E7=AE=97=E9=95=BF=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/service/cast/report.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/service/cast/report.go b/pkg/service/cast/report.go index 99ae78ad..e978401a 100644 --- a/pkg/service/cast/report.go +++ b/pkg/service/cast/report.go @@ -19,6 +19,7 @@ import ( "fonchain-fiee/pkg/utils/stime" "net/url" "os" + "unicode/utf8" "path/filepath" "strconv" "strings" @@ -416,8 +417,8 @@ func ImportCompetitiveReportBatch(ctx *gin.Context) { // 截断超长字段(按AI生成的字段长度要求) competitorReportData = truncateCompetitorReportData(competitorReportData) - // 验证标题长度(数据库字段为varchar(50)) - if len(temp.Title) > 50 { + // 验证标题长度(数据库字段为varchar(50),按字符数计算) + if utf8.RuneCountInString(temp.Title) > 50 { temp.Remark = "标题长度超出限制" req.Reports = append(req.Reports, temp) continue