package imports import ( "bytes" "context" "encoding/json" "fmt" "fonchain-fiee/api/accountFiee" "fonchain-fiee/api/bundle" apiCast "fonchain-fiee/api/cast" "fonchain-fiee/api/files" "fonchain-fiee/pkg/config" "fonchain-fiee/pkg/model" modelCast "fonchain-fiee/pkg/model/cast" "fonchain-fiee/pkg/service" "fonchain-fiee/pkg/service/cast" "fonchain-fiee/pkg/service/upload" "io" "log" "net/http" "os" "path/filepath" "strconv" "strings" "time" "github.com/gin-gonic/gin" "github.com/mholt/archiver" "github.com/xuri/excelize/v2" ) func ImportPublish(c *gin.Context) { // 1. 上传画家短视频详情文件 excelFile, err := c.FormFile("excel") if err != nil { c.JSON(400, gin.H{"error": "缺少 Excel 文件 excel"}) return } zipFile, err := c.FormFile("zip") if err != nil { c.JSON(400, gin.H{"error": "缺少 ZIP 文件"}) return } // 2. 保存临时文件 tempDir := "tmp" os.MkdirAll(tempDir, 0755) excelPath := filepath.Join(tempDir, "artists.xlsx") zipPath := filepath.Join(tempDir, "archive.zip") if err = c.SaveUploadedFile(excelFile, excelPath); err != nil { c.JSON(500, gin.H{"error": "保存 Excel 失败"}) return } if err = c.SaveUploadedFile(zipFile, zipPath); err != nil { c.JSON(500, gin.H{"error": "保存 ZIP 文件失败"}) return } // 3. 解压 ZIP unzipPath := filepath.Join(tempDir, "unzipped") if _, err = os.Stat(unzipPath); err == nil { // 路径已存在,删除 if removeErr := os.RemoveAll(unzipPath); removeErr != nil { c.JSON(500, gin.H{"error": "清理已存在解压目录失败: " + removeErr.Error()}) return } } os.MkdirAll(unzipPath, 0755) if err = archiver.Unarchive(zipPath, unzipPath); err != nil { c.JSON(500, gin.H{"error": "解压 ZIP 失败: " + err.Error()}) return } entries, err := os.ReadDir(unzipPath) if err != nil || len(entries) == 0 { c.JSON(500, gin.H{"error": "读取解压目录失败或目录为空"}) return } if len(entries) == 1 && entries[0].IsDir() { // 说明解压后多了一层目录,把它设为新的 unzipPath unzipPath = filepath.Join(unzipPath, entries[0].Name()) } defer os.RemoveAll(tempDir) // 4. 读取 Excel 画家名单, 匹配视频和图片 artists, err := readArtistVideoInfo(excelPath, unzipPath) if err != nil { c.JSON(500, gin.H{"error": "读取 Excel 失败"}) return } // 5.发布视频 var failedRecords []FailedRecord var artistResp []ArtistVideoDetail for _, artist := range artists { var infoResp *accountFiee.UserInfoResponse var err error list, err := service.AccountFieeProvider.UserList(context.Background(), &accountFiee.UserListRequest{ Name: artist.Name, }) if err != nil { failedRecords = append(failedRecords, FailedRecord{ Name: artist.Name, Msg: fmt.Sprintf("获取用户信息失败: %s", err.Error()), }) log.Printf(fmt.Sprintf("获取用户信息失败: %s", err.Error())) continue } if list != nil && len(list.UserList) > 0 { infoResp, err = service.AccountFieeProvider.Info(context.Background(), &accountFiee.InfoRequest{ ID: list.UserList[0].Id, Domain: "app", }) if err != nil { failedRecords = append(failedRecords, FailedRecord{ Name: artist.Name, Msg: fmt.Sprintf("获取用户信息失败: %s", err.Error()), }) log.Printf(fmt.Sprintf("获取用户信息失败: %s", err.Error())) continue } } if err = cast.CheckUserBundleBalance(int32(list.UserList[0].Id), modelCast.BalanceTypeVideoValue); err != nil { failedRecords = append(failedRecords, FailedRecord{ Name: artist.Name, Msg: fmt.Sprintf("检查用户视频可消耗数量: %s", err.Error()), }) log.Printf(fmt.Sprintf("检查用户视频可消耗数量: %s", err.Error())) continue } //自媒体账号 accountList, err := service.CastProvider.MediaUserList(c, &apiCast.MediaUserListReq{ //ArtistUuid: strconv.FormatUint(list.UserList[0].Id, 10), ArtistVal: artist.Name, Page: 1, PageSize: 10, }) if err != nil { failedRecords = append(failedRecords, FailedRecord{ Name: artist.Name, Msg: fmt.Sprintf("自媒体账号数量获取失败: %s,账号数量:%d", err.Error(), len(accountList.Data)), }) log.Printf(fmt.Sprintf("自媒体账号数量获取失败: %s,账号数量:%d", err.Error(), len(accountList.Data))) continue } if accountList == nil || len(accountList.Data) == 0 { failedRecords = append(failedRecords, FailedRecord{ Name: artist.Name, Msg: "自媒体账号数量为0", }) log.Printf(fmt.Sprintf("自媒体账号,账号数量:%d", len(accountList.Data))) continue } mediaAccountUuids := []string{} mediaAccountNames := []string{} platformIDs := []apiCast.PlatformIDENUM{} for _, info := range accountList.Data { if info.PlatformID == 2 && ((artist.Id == "31" && info.ArtistName == "荣小松") || (artist.Id == "72" && info.ArtistName == "韩风霞")) { continue // 跳过 } mediaAccountUuids = append(mediaAccountUuids, info.MediaAccountUuid) mediaAccountNames = append(mediaAccountNames, info.PlatformUserName) platformIDs = append(platformIDs, apiCast.PlatformIDENUM(info.PlatformID)) } newCtx := cast.NewCtxWithUserInfo(c) resp, err := service.CastProvider.UpdateWorkVideo(newCtx, &apiCast.UpdateWorkVideoReq{ Title: artist.Title, Content: artist.Title, VideoUrl: artist.Video, CoverUrl: artist.Img, MediaAccountUuids: mediaAccountUuids, MediaAccountNames: mediaAccountNames, PlatformIDs: platformIDs, PublishConfig1: &apiCast.PublishConfig{ CanComment: 1, CanJoin: 1, CanQuote: 1, ForbidComment: 2, IsAI: 1, PublicType: 1, }, PublishConfig2: &apiCast.PublishConfig{ CanComment: 1, CanJoin: 1, CanQuote: 1, ForbidComment: 2, IsAI: 1, PublicType: 1, }, PublishConfig3: &apiCast.PublishConfig{ CanComment: 1, CanJoin: 1, CanQuote: 1, ForbidComment: 1, IsAI: 1, PublicType: 1, }, Action: "submit", ArtistUuid: strconv.FormatUint(list.UserList[0].Id, 10), ArtistName: infoResp.Name, ArtistPhone: infoResp.TelNum, ArtistPhoneAreaCode: infoResp.TelAreaCode, Source: 2, }) if err != nil { failedRecords = append(failedRecords, FailedRecord{ Name: artist.Name, Msg: fmt.Sprintf("发布"+artist.Name+"视频"+artist.Title+"失败: %s", err.Error()), }) log.Printf(fmt.Sprintf("发布"+artist.Name+"视频"+artist.Title+"失败: %s", err.Error())) continue } artistResp = append(artistResp, ArtistVideoDetail{ Id: artist.Id, ArtistName: artist.Name, Title: artist.Title, WorkUuid: resp.WorkUuid, Youtube: artist.Youtube, Instagram: artist.Instagram, TikTok: artist.TikTok, }) } excelUrl, err := exportRecordsToExcel(artistResp) if err != nil { service.Error(c, err) return } // 6. 返回结果 service.Success(c, map[string]interface{}{ "excelUrl": excelUrl, "failedRecords": failedRecords, }) } func readArtistVideoInfo(excelPath, unzipPath string) ([]ArtistMedia, error) { log.Println(unzipPath) f, err := excelize.OpenFile(excelPath) if err != nil { return nil, err } defer f.Close() sheetName := f.GetSheetName(0) rows, err := f.GetRows(sheetName) if err != nil { return nil, err } log.Println("start read excel") var artists []ArtistMedia for i, row := range rows { if i == 0 || i == 1 || len(row) < 2 { continue } if i == 165 { break } id, _ := f.GetCellValue(sheetName, fmt.Sprintf("A%d", i+1)) if id != "" { id = strings.TrimSpace(id) } artistName, _ := f.GetCellValue(sheetName, fmt.Sprintf("B%d", i+1)) if artistName != "" { artistName = strings.TrimSpace(artistName) } title, _ := f.GetCellValue(sheetName, fmt.Sprintf("C%d", i+1)) if title != "" { title = strings.TrimSpace(title) } youtube, _ := f.GetCellValue(sheetName, fmt.Sprintf("D%d", i+1)) if youtube != "" { youtube = strings.TrimSpace(youtube) } instagram, _ := f.GetCellValue(sheetName, fmt.Sprintf("E%d", i+1)) if instagram != "" { instagram = strings.TrimSpace(instagram) } tiktok, _ := f.GetCellValue(sheetName, fmt.Sprintf("F%d", i+1)) if tiktok != "" { tiktok = strings.TrimSpace(tiktok) } artists = append(artists, ArtistMedia{ Id: id, Name: artistName, Title: title, Youtube: youtube, Instagram: instagram, TikTok: tiktok, }) } artists, err = matchArtistMedia(artists, unzipPath) return artists, nil } func matchArtistMedia(artists []ArtistMedia, unzipPath string) ([]ArtistMedia, error) { var err error var res []ArtistMedia for _, artist := range artists { oldImgPath := fmt.Sprintf("%s/%s/%s.jpg", unzipPath, artist.Name, artist.Id) oldVideoPath := fmt.Sprintf("%s/%s/%s.mp4", unzipPath, artist.Name, artist.Id) // 检查源文件是否存在 if _, err = os.Stat(oldImgPath); os.IsNotExist(err) { continue } if _, err = os.Stat(oldVideoPath); os.IsNotExist(err) { continue } baseDir := filepath.Join(unzipPath, artist.Name) if err = os.MkdirAll(baseDir, 0755); err != nil { log.Println("创建目录失败:", err) return nil, err } log.Println("创建目录成功:", baseDir) // 重命名 now := time.Now().Unix() imgPath := fmt.Sprintf("%s/%s/%s_%d.jpg", unzipPath, artist.Name, artist.Id, now) videoPath := fmt.Sprintf("%s/%s/%s_%d.mp4", unzipPath, artist.Name, artist.Id, now) if err = os.Rename(oldImgPath, imgPath); err != nil { log.Println("图片:"+oldImgPath+"重命名失败:", err) return nil, err } if err = os.Rename(oldVideoPath, videoPath); err != nil { log.Println("视频:"+oldVideoPath+"重命名失败:", err) return nil, err } //转为url content, err := os.ReadFile(videoPath) if err != nil { return nil, err } if err = UploadToAnotherService(context.Background(), content, filepath.Base(videoPath)); err != nil { log.Println("上传视频失败:", err) return nil, err } var httpType string if config.AppMode == "prod" { url := "saas.fiee.com" httpType = fmt.Sprintf("%s%s", model.HttpsType, url) } else { url := "114.218.158.24:9020" httpType = fmt.Sprintf("%s%s", model.HttpType, url) } baseUrl := fmt.Sprintf("%s/api/fiee/resource/raw/", httpType) videoUrl := baseUrl + filepath.Base(videoPath) imgUrl, err := upload.PutBos(filepath.ToSlash(imgPath), "image", false) if err != nil { log.Println("上传图片失败:", err) return nil, err } tmp := artist tmp.Id = artist.Id tmp.Name = artist.Name tmp.Title = artist.Title tmp.Img = imgUrl //tmp.Video = filepath.ToSlash(videoPath) tmp.Video = videoUrl res = append(res, tmp) } return res, nil } func UploadToAnotherService(ctx context.Context, fileData []byte, path string) error { const chunkSize = 4*1024*1024 - 100 _, err := service.FilesProvider.TusCreate(ctx, &files.TusCreateReq{ Path: path, UserSpacePath: "", Override: true, }) if err != nil { return err } log.Println("create success ......**********") offset := int64(0) totalSize := int64(len(fileData)) for offset < totalSize { end := offset + chunkSize if end > totalSize { end = totalSize } chunk := fileData[offset:end] _, err = service.FilesProvider.TusUpload(ctx, &files.TusUploadReq{ Path: path, UploadOffset: offset, Content: chunk, UserSpacePath: "", }) if err != nil { return fmt.Errorf("上传 offset=%d chunk 失败: %w", offset, err) } log.Printf("upload chunk: %d - %d success\n", offset, end) offset = end } return nil } func exportRecordsToExcel(artistInfos []ArtistVideoDetail) (string, error) { f := excelize.NewFile() sheet := "Sheet1" f.NewSheet(sheet) // 写表头 headers := []string{"序号", "画家名", "标题", "uuid", "youtube", "instagram", "tiktok"} for col, h := range headers { _ = f.SetCellValue(sheet, string(rune('A'+col))+"1", h) } // 写数据 for i, artistInfo := range artistInfos { row := i + 2 _ = f.SetCellValue(sheet, "A"+strconv.Itoa(row), artistInfo.Id) _ = f.SetCellValue(sheet, "B"+strconv.Itoa(row), artistInfo.ArtistName) _ = f.SetCellValue(sheet, "C"+strconv.Itoa(row), artistInfo.Title) _ = f.SetCellValue(sheet, "D"+strconv.Itoa(row), artistInfo.WorkUuid) _ = f.SetCellValue(sheet, "E"+strconv.Itoa(row), artistInfo.Youtube) _ = f.SetCellValue(sheet, "F"+strconv.Itoa(row), artistInfo.Instagram) _ = f.SetCellValue(sheet, "G"+strconv.Itoa(row), artistInfo.TikTok) } // 保存文件 filename := "画家视频详情记录.xlsx" fileDir := "./runtime/import/" // 自定义目录 _ = os.MkdirAll(fileDir, os.ModePerm) filePath := filepath.Join(fileDir, filename) if err := f.SaveAs(filePath); err != nil { return "", err } excelUrl, err := upload.PutBos(filePath, "excel", true) if err != nil { return "", err } return excelUrl, nil } func updateApproval(ctx *gin.Context, artistId uint64, workUuid string, accountInfos []*apiCast.MediaUserInfo, videoUrl, imgUrl, title string) error { var publishAccounts []PublishAccount var publishPlatformIds []int32 for _, v := range accountInfos { publishAccounts = append(publishAccounts, PublishAccount{ AccountName: v.PlatformUserName, AccountID: v.MediaAccountUuid, }) publishPlatformIds = append(publishPlatformIds, int32(v.PlatformID)) } var req CreateRequest var url string saasPublishVideo := &SaasPublishVideo{ Title: title, Describe: title, IsYoutubeSee: 1, IsTiktokScreen: 1, IsTiktokComment: 1, IsTiktokQuote: 1, IsTiktokAiGenerate: 1, Cover: imgUrl, PlatformIds: publishPlatformIds, PublishAccounts: publishAccounts, Videos: []Video{ { VideoAddress: videoUrl, VideoThumbnail: imgUrl, }, }, } if config.AppMode == "prod" { url = "https://erp.fonchain.com/approval/v2/create" req = CreateRequest{ Type: "SaasPublishVideo", DepartmentID: 3, Domain: "7bfa3942cceb20389822af7b57c5798e", MenuType: 2, SaasPublishVideo: saasPublishVideo, } } else { url = "http://114.218.158.24:9020/approval/v2/create" req = CreateRequest{ Type: "SaasPublishVideo", DepartmentID: 3, Domain: "7bfa3942cceb20389822af7b57c5798e", MenuType: 2, SaasPublishVideo: saasPublishVideo, } } jsonBytes, err := json.Marshal(req) if err != nil { return err } res, err := http.Post(url, "application/json", bytes.NewBuffer(jsonBytes)) if err != nil { return err } defer res.Body.Close() responseBodyBytes, err := io.ReadAll(res.Body) if err != nil { return err } var apiResp APIResponse err = json.Unmarshal(responseBodyBytes, &apiResp) if err != nil { // 处理错误 return err } fmt.Println("拿到审批ID:", apiResp.Data.ID) _, err = service.CastProvider.UpdateStatus(ctx, &apiCast.UpdateStatusReq{ WorkUuid: workUuid, WorkAction: apiCast.WorkActionENUM_APPROVAL, ApprovalID: strconv.FormatUint(apiResp.Data.ID, 10), }) if err != nil { return err } _, err = service.CastProvider.UpdateStatus(ctx, &apiCast.UpdateStatusReq{ WorkUuid: workUuid, WorkAction: apiCast.WorkActionENUM_APPROVAL_PASS, ApprovalID: strconv.FormatUint(apiResp.Data.ID, 10), }) if err != nil { return err } _, err = service.BundleProvider.AddBundleBalance(ctx, &bundle.AddBundleBalanceReq{ UserId: int32(artistId), VideoConsumptionNumber: 1, }) if err != nil { log.Printf(fmt.Sprintf("扣除余额失败: %s", err.Error())) return err } _, err = service.CastProvider.UpdateStatus(ctx, &apiCast.UpdateStatusReq{ WorkAction: apiCast.WorkActionENUM_CONFIRM, WorkUuid: workUuid, ConfirmRemark: "", ConfirmStatus: 1, }) if err != nil { log.Printf(fmt.Sprintf("更新状态失败: %s", err.Error())) return err } return nil }