diff --git a/pkg/service/cast/report.go b/pkg/service/cast/report.go index ad900ac..6e1babd 100644 --- a/pkg/service/cast/report.go +++ b/pkg/service/cast/report.go @@ -4,18 +4,23 @@ import ( "context" "errors" "fmt" + "fonchain-fiee/api/accountFiee" "fonchain-fiee/api/bundle" "fonchain-fiee/api/cast" + "fonchain-fiee/cmd/config" "fonchain-fiee/pkg/cache" "fonchain-fiee/pkg/e" modelCast "fonchain-fiee/pkg/model/cast" + "fonchain-fiee/pkg/model/login" "fonchain-fiee/pkg/service" "fonchain-fiee/pkg/utils" + "path/filepath" "strconv" "time" "dubbo.apache.org/dubbo-go/v3/common/constant" "github.com/gin-gonic/gin" + "github.com/xuri/excelize/v2" "go.uber.org/zap" ) @@ -48,19 +53,211 @@ func CreateCompetitiveReport(ctx *gin.Context) { // ImportCompetitiveReportBatch 批量导入竞品报告 func ImportCompetitiveReportBatch(ctx *gin.Context) { - var req *cast.ImportCompetitiveReportBatchReq - var err error - if err = ctx.ShouldBind(&req); err != nil { - service.Error(ctx, err) - return - } - newCtx := NewCtxWithUserInfo(ctx) - resp, err := service.CastProvider.ImportCompetitiveReportBatch(newCtx, req) + // 获取上传的Excel文件 + excelFile, err := ctx.FormFile("file") if err != nil { service.Error(ctx, err) return } - service.Success(ctx, resp) + + loginInfo := login.GetUserInfoFromC(ctx) + lockKey := fmt.Sprintf("import_competitive_report_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/report" + _, err = utils.CheckDirPath(tempDir, true) + if err != nil { + service.Error(ctx, err) + return + } + + // 生成文件名并保存文件 + fileName := fmt.Sprintf("%d_competitive_report.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 + } + + // 初始化请求结构 + req := cast.ImportCompetitiveReportBatchReq{ + Reports: make([]*cast.CreateCompetitiveReportReq, 0), + } + + // 生成结果文件URL + urlHost := config.AppConfig.System.FieeHost + urlResult := fmt.Sprintf("%s/api/fiee/static/report/%s", urlHost, fileName) + + // 记录每行数据对应的Excel行号(用于后续匹配失败记录) + reportRowMap := make(map[*cast.CreateCompetitiveReportReq]int) + + for line, row := range rows { + // 跳过表头 + if line == 0 { + continue + } + // 跳过空行 + if len(row) == 0 { + continue + } + + // 创建报告请求对象 + temp := &cast.CreateCompetitiveReportReq{ + Source: 2, // 来源:2 导入 + } + // 记录Excel行号(line+1是因为Excel行号从1开始,且跳过表头) + excelRowNum := line + 1 + reportRowMap[temp] = excelRowNum + + // 解析艺人编号(B列,row[1]) + var artistNum string + if len(row) > 1 && utils.CleanString(row[1]) != "" { + artistNum = utils.CleanString(row[1]) + artistSubNum := utils.CleanString(row[1]) + if artistSubNum == "" { + temp.Remark = "艺人编号不能为空" + req.Reports = append(req.Reports, temp) + continue + } + var subInfoResp *accountFiee.UserInfoResponse + subInfoResp, err := service.AccountFieeProvider.SubNumGetInfo(context.Background(), &accountFiee.SubNumGetInfoRequest{ + SubNum: artistSubNum, + Domain: "app", + }) + + if err != nil { + temp.Remark = fmt.Sprintf("自媒体用户查询失败:%s", err.Error()) + zap.L().Error("AccountFieeProvider.SubNumGetInfo", zap.Error(err)) + req.Reports = append(req.Reports, temp) + continue + } + + if subInfoResp == nil || subInfoResp.Id == 0 { + temp.Remark = "自媒体用户不存在" + zap.L().Error("AccountFieeProvider.SubNumGetInfo user not found", zap.String("subNum", artistSubNum)) + req.Reports = append(req.Reports, temp) + continue + } + + // 设置艺人信息 + temp.SubNum = artistSubNum + temp.ArtistID = fmt.Sprint(subInfoResp.Id) + temp.ArtistName = subInfoResp.Name + temp.ArtistPhone = subInfoResp.TelNum + } + + // 解析标题(C列,row[2]) + if len(row) > 2 { + temp.Title = utils.CleanString(row[2]) + } + + // 解析报告内容(D列,row[3]) + if len(row) > 3 { + temp.ReportContent = row[3] + } + + // 解析图片URL(E列,row[4]) + if len(row) > 4 && utils.CleanString(row[4]) != "" { + 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 = "艺人编号不能为空" + req.Reports = append(req.Reports, temp) + continue + } + + if temp.PdfUrl == "" { + temp.Remark = "PDF URL不能为空" + req.Reports = append(req.Reports, temp) + continue + } + + req.Reports = append(req.Reports, temp) + } + + // 检查是否有数据 + if len(req.Reports) == 0 { + service.Error(ctx, errors.New(e.ErrNoData)) + return + } + + // 调用批量导入接口 + newCtx := NewCtxWithUserInfo(ctx) + resp, err := service.CastProvider.ImportCompetitiveReportBatch(newCtx, &req) + if err != nil { + service.Error(ctx, err) + return + } + + // 如果有失败的数据,生成结果文件 + if resp.FailCount > 0 { + hasValueRows := make(map[int]bool, resp.FailCount) + // 遍历响应结果,标记失败的行 + // resp.Reports的顺序应该与req.Reports的顺序一致 + for i, v := range resp.Reports { + if !v.Success { + // 根据索引找到对应的请求对象 + if i < len(req.Reports) { + reqReport := req.Reports[i] + // 通过请求对象找到对应的Excel行号 + if excelRowNum, ok := reportRowMap[reqReport]; ok { + // 将错误信息写入最后一列(G列,可根据实际模板调整) + excelData.SetCellValue("Sheet1", fmt.Sprintf("G%d", excelRowNum), v.Remark) + hasValueRows[excelRowNum] = true + } + } + } + } + + // 删除成功的行(从后往前删除,避免行号变化) + for i := len(rows) - 1; i >= 1; i-- { + if !hasValueRows[i+1] { + if err = excelData.RemoveRow("Sheet1", i+1); err != nil { + continue + } + } + } + // 保存结果文件 + resultPath := fmt.Sprintf("./runtime/report/%s", fileName) + if err = excelData.SaveAs(resultPath); err != nil { + service.Error(ctx, err) + return + } + } + + // 返回结果 + service.Success(ctx, map[string]interface{}{ + "successCount": resp.SuccessCount, + "failCount": resp.FailCount, + "resultUrl": urlResult, + }) return }