diff --git a/pkg/router/analysis.go b/pkg/router/analysis.go index 74876b1a..8fcf0e45 100644 --- a/pkg/router/analysis.go +++ b/pkg/router/analysis.go @@ -49,6 +49,7 @@ func AnalysisRouter(r *gin.RouterGroup) { competitiveReport.POST("count-by-work-uuids", serviceCast.CountCompetitiveReportByWorkUuids) // 根据作品UUID统计竞品报告数量 competitiveReport.POST("export-list", serviceCast.ListCompetitiveReportExport) // 竞品报告列表导出 competitiveReport.POST("export-single-list", serviceCast.ListCompetitiveReportSingleExport) // 竞品报告单个列表导出 + competitiveReport.POST("import-pdf-batch", serviceCast.ImportPdfBatch) // 批量导入PDF(下载、重命名、上传) } // 员工任务相关路由(需要App登录验证 diff --git a/pkg/service/cast/report.go b/pkg/service/cast/report.go index e978401a..b64b5af2 100644 --- a/pkg/service/cast/report.go +++ b/pkg/service/cast/report.go @@ -1354,6 +1354,140 @@ func generateReportFileName(title, artistName string) string { return fmt.Sprintf("%s%s老师的竞品报告%d", today, artistName, timestamp) } +// ImportPdfBatch 批量导入 PDF(下载、重命名、上传) +func ImportPdfBatch(ctx *gin.Context) { + // 获取上传的Excel文件 + excelFile, err := ctx.FormFile("file") + if err != nil { + service.Error(ctx, err) + return + } + + loginInfo := login.GetUserInfoFromC(ctx) + lockKey := fmt.Sprintf("import_pdf_batch:%d", loginInfo.ID) + replay := cache.RedisClient.SetNX(lockKey, time.Now().Format("20060102150405"), 5*time.Minute) + if !replay.Val() { + service.Error(ctx, errors.New("有导入任务正在进行,请稍后再试")) + return + } + defer cache.RedisClient.Del(lockKey) + + tempDir := "./runtime/pdf_import" + _, err = utils.CheckDirPath(tempDir, true) + if err != nil { + service.Error(ctx, err) + return + } + + // 生成文件名并保存文件 + fileName := fmt.Sprintf("%d_pdf_import.xlsx", time.Now().UnixMicro()) + excelPath := filepath.Join(tempDir, fileName) + if err = ctx.SaveUploadedFile(excelFile, excelPath); err != nil { + service.Error(ctx, err) + return + } + + // 打开Excel文件 + excelData, err := excelize.OpenFile(excelPath) + if err != nil { + service.Error(ctx, err) + return + } + defer excelData.Close() + + // 解析Excel中的数据 + rows, err := excelData.GetRows("Sheet1") + if err != nil { + service.Error(ctx, err) + return + } + + // 生成结果文件URL + urlHost := config.AppConfig.System.FieeHost + urlResult := fmt.Sprintf("%s/api/fiee/static/pdf_import/%s", urlHost, fileName) + + // 确保临时目录存在 + _, err = utils.CheckDirPath("./runtime/pdf_import/", true) + if err != nil { + service.Error(ctx, err) + return + } + + // 记录处理结果 + successCount := 0 + failCount := 0 + + for line, row := range rows { + // 跳过表头 + if line == 0 { + continue + } + // 跳过空行 + if len(row) == 0 { + continue + } + + // A列:PDF URL + pdfUrl := utils.CleanString(row[0]) + // B列:新文件名 + newFileName := utils.CleanString(row[1]) + + // 验证必填字段 + if pdfUrl == "" { + excelData.SetCellValue("Sheet1", fmt.Sprintf("D%d", line+1), "PDF URL不能为空") + failCount++ + continue + } + if newFileName == "" { + excelData.SetCellValue("Sheet1", fmt.Sprintf("D%d", line+1), "新文件名不能为空") + failCount++ + continue + } + + // 下载 PDF + fullPath, err := utils.SaveUrlFileDisk(pdfUrl, "runtime/pdf_import/", newFileName+".pdf") + if err != nil { + zap.L().Error("下载PDF失败", zap.String("pdfUrl", pdfUrl), zap.Error(err)) + excelData.SetCellValue("Sheet1", fmt.Sprintf("D%d", line+1), fmt.Sprintf("下载PDF失败: %s", err.Error())) + failCount++ + continue + } + + // 上传到 OSS + uploadUrl, uploadErr := upload.PutBos(fullPath, upload.PdfType, true) + if uploadErr != nil { + zap.L().Error("上传PDF失败", zap.Error(uploadErr)) + excelData.SetCellValue("Sheet1", fmt.Sprintf("D%d", line+1), fmt.Sprintf("上传PDF失败: %s", uploadErr.Error())) + failCount++ + // 清理临时文件 + if _, err := os.Stat(fullPath); err == nil { + os.Remove(fullPath) + } + continue + } + + // 写入新URL到C列 + excelData.SetCellValue("Sheet1", fmt.Sprintf("C%d", line+1), uploadUrl) + successCount++ + zap.L().Info("PDF处理成功", zap.String("pdfUrl", pdfUrl), zap.String("newUrl", uploadUrl)) + } + + // 保存结果文件 + resultPath := fmt.Sprintf("./runtime/pdf_import/%s", fileName) + if err = excelData.SaveAs(resultPath); err != nil { + service.Error(ctx, err) + return + } + + // 返回结果 + service.Success(ctx, map[string]interface{}{ + "successCount": successCount, + "failCount": failCount, + "resultUrl": urlResult, + }) + return +} + // truncateCompetitorReportData 截断竞品报告数据中超长的字段 // 字段长度要求参考 AI 生成竞品报告的限制 func truncateCompetitorReportData(data utils.CompetitorReportData) utils.CompetitorReportData {