diff --git a/data/竞品报告导入模板.xlsx b/data/竞品报告导入模板.xlsx index 14a632f..74ebedb 100644 Binary files a/data/竞品报告导入模板.xlsx and b/data/竞品报告导入模板.xlsx differ diff --git a/pkg/service/cast/report.go b/pkg/service/cast/report.go index bd1f46e..9aafc41 100644 --- a/pkg/service/cast/report.go +++ b/pkg/service/cast/report.go @@ -311,11 +311,6 @@ func ImportCompetitiveReportBatch(ctx *gin.Context) { temp.ImageUrl = utils.CleanString(row[4]) } - // 解析PDF URL(F列,row[5]),可选 - if len(row) > 5 && utils.CleanString(row[5]) != "" { - temp.PdfUrl = utils.CleanString(row[5]) - } - // 验证必填字段 if artistNum == "" { temp.Remark = "艺人编号不能为空" @@ -323,12 +318,84 @@ func ImportCompetitiveReportBatch(ctx *gin.Context) { continue } - if temp.PdfUrl == "" { - temp.Remark = "PDF URL不能为空" + // 验证报告内容和图片不能同时为空 + if temp.ReportContent == "" && temp.ImageUrl == "" { + temp.Remark = "报告内容和图片不能同时为空" req.Reports = append(req.Reports, temp) continue } + // 如果已经有错误信息,跳过PDF生成 + if temp.Remark != "" { + req.Reports = append(req.Reports, temp) + continue + } + + // 检查图片URL是否包含阿里云,如果包含则下载并重新上传到OSS + if temp.ImageUrl != "" { + newImageUrl, err := checkAndReuploadImageForReport(temp.ImageUrl) + if err != nil { + temp.Remark = fmt.Sprintf("图片处理失败: %v", err) + zap.L().Error("图片重新上传失败", zap.String("imageUrl", temp.ImageUrl), zap.Error(err)) + req.Reports = append(req.Reports, temp) + continue + } + temp.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 + + // 确保目录存在 + _, 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 + } + + // 上传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 + + // 清理临时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) } @@ -358,8 +425,8 @@ func ImportCompetitiveReportBatch(ctx *gin.Context) { reqReport := req.Reports[i] // 通过请求对象找到对应的Excel行号 if excelRowNum, ok := reportRowMap[reqReport]; ok { - // 将错误信息写入最后一列(G列,可根据实际模板调整) - excelData.SetCellValue("Sheet1", fmt.Sprintf("G%d", excelRowNum), v.Remark) + // 将错误信息写入最后一列(F列) + excelData.SetCellValue("Sheet1", fmt.Sprintf("F%d", excelRowNum), v.Remark) hasValueRows[excelRowNum] = true } } diff --git a/pkg/utils/pdf.go b/pkg/utils/pdf.go index ff65f56..0339b17 100644 --- a/pkg/utils/pdf.go +++ b/pkg/utils/pdf.go @@ -48,6 +48,9 @@ func loadChineseFont(pdf *gofpdf.Fpdf, fontPath string) error { // GeneratePDF 生成PDF文件 func GeneratePDF(text, imageURL, outputPath, fontPath string) error { + if text == "" { + return errors.New("文本不能为空") + } // 创建PDF实例,P=纵向,mm=毫米单位,A4=页面大小 pdf := gofpdf.New("P", "mm", "A4", "") @@ -80,83 +83,86 @@ func GeneratePDF(text, imageURL, outputPath, fontPath string) error { lineHeight := 7.0 pdf.MultiCell(textWidth, lineHeight, cleanedText, "", "L", false) - // 添加一些间距 - pdf.Ln(5) + // 如果提供了图片URL,则添加图片 + if imageURL != "" { + // 添加一些间距 + pdf.Ln(5) - // 解析URL获取文件扩展名 - u, err := url.Parse(imageURL) - if err != nil { - return fmt.Errorf("图片链接解析错误: %v", err) + // 解析URL获取文件扩展名 + u, err := url.Parse(imageURL) + if err != nil { + return fmt.Errorf("图片链接解析错误: %v", err) + } + fileExt := filepath.Ext(u.Path) + // 如果没有扩展名,默认使用.jpg + if fileExt == "" { + fileExt = ".jpg" + } + + // 下载图片 + resp, err := http.Get(imageURL) + if err != nil { + return fmt.Errorf("下载图片失败: %v", err) + } + defer resp.Body.Close() + + // 读取图片数据 + imageData, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("读取图片数据失败: %v", err) + } + + // 将图片数据保存到临时文件(gofpdf需要文件路径) + 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() + + // A4纵向页面宽度210mm,减去左右边距40mm,可用宽度170mm + // 图片宽度设为可用宽度的70% + imageWidth := textWidth * 0.7 + // 计算居中位置:页面宽度210mm,图片居中 + imageX := (210.0 - imageWidth) / 2 + currentY := pdf.GetY() + + // 注册图片并获取原始尺寸,用于计算缩放后的高度 + imgInfo := pdf.RegisterImageOptions(tmpFile.Name(), gofpdf.ImageOptions{}) + if imgInfo == nil { + return fmt.Errorf("注册图片失败") + } + + // 计算缩放后的图片高度(按比例缩放) + // 原始宽度:原始高度 = 缩放后宽度:缩放后高度 + originalWidth, originalHeight := imgInfo.Extent() + imageHeight := (imageWidth / originalWidth) * originalHeight + + // A4页面高度297mm,底部边距10mm,计算可用的最大Y坐标 + pageHeight := 297.0 + bottomMargin := 10.0 + maxY := pageHeight - bottomMargin + + // 检查当前页面剩余空间是否足够放下图片 + // 如果图片底部会超出页面可用区域,则添加新页面 + if currentY+imageHeight > maxY { + pdf.AddPage() + // 新页面从顶部边距开始 + currentY = 10.0 + } + + // 添加图片 + // ImageOptions参数:图片路径、x坐标、y坐标、宽度、高度、是否流式布局、选项、链接 + // 高度设为0表示按比例自动计算 + pdf.ImageOptions(tmpFile.Name(), imageX, currentY, imageWidth, 0, false, gofpdf.ImageOptions{}, 0, "") } - fileExt := filepath.Ext(u.Path) - // 如果没有扩展名,默认使用.jpg - if fileExt == "" { - fileExt = ".jpg" - } - - // 下载图片 - resp, err := http.Get(imageURL) - if err != nil { - return fmt.Errorf("下载图片失败: %v", err) - } - defer resp.Body.Close() - - // 读取图片数据 - imageData, err := io.ReadAll(resp.Body) - if err != nil { - return fmt.Errorf("读取图片数据失败: %v", err) - } - - // 将图片数据保存到临时文件(gofpdf需要文件路径) - 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() - - // A4纵向页面宽度210mm,减去左右边距40mm,可用宽度170mm - // 图片宽度设为可用宽度的70% - imageWidth := textWidth * 0.7 - // 计算居中位置:页面宽度210mm,图片居中 - imageX := (210.0 - imageWidth) / 2 - currentY := pdf.GetY() - - // 注册图片并获取原始尺寸,用于计算缩放后的高度 - imgInfo := pdf.RegisterImageOptions(tmpFile.Name(), gofpdf.ImageOptions{}) - if imgInfo == nil { - return fmt.Errorf("注册图片失败") - } - - // 计算缩放后的图片高度(按比例缩放) - // 原始宽度:原始高度 = 缩放后宽度:缩放后高度 - originalWidth, originalHeight := imgInfo.Extent() - imageHeight := (imageWidth / originalWidth) * originalHeight - - // A4页面高度297mm,底部边距10mm,计算可用的最大Y坐标 - pageHeight := 297.0 - bottomMargin := 10.0 - maxY := pageHeight - bottomMargin - - // 检查当前页面剩余空间是否足够放下图片 - // 如果图片底部会超出页面可用区域,则添加新页面 - if currentY+imageHeight > maxY { - pdf.AddPage() - // 新页面从顶部边距开始 - currentY = 10.0 - } - - // 添加图片 - // ImageOptions参数:图片路径、x坐标、y坐标、宽度、高度、是否流式布局、选项、链接 - // 高度设为0表示按比例自动计算 - pdf.ImageOptions(tmpFile.Name(), imageX, currentY, imageWidth, 0, false, gofpdf.ImageOptions{}, 0, "") // 生成并保存PDF文件 err = pdf.OutputFileAndClose(outputPath)