package tencent import ( "bytes" "context" dao "dataengine/dao/tencent" dto "dataengine/model/dto/tencent" entity "dataengine/model/entity/tencent" "encoding/json" "fmt" "io" "net/http" "time" "github.com/gogf/gf/v2/frame/g" "github.com/sirupsen/logrus" ) type audioService struct{} var AudioService = new(audioService) // API响应结构 type audioResponse struct { Code int `json:"code"` Message string `json:"message"` Data struct { List []struct { AudioId string `json:"audio_id"` CoverImageUrl string `json:"cover_image_url"` AudioName string `json:"audio_name"` Author string `json:"author"` Duration float64 `json:"duration"` ExpireTime int64 `json:"expire_time"` FeelTags []string `json:"feel_tags"` GenreTags []string `json:"genre_tags"` } `json:"list"` PageInfo struct { Page int `json:"page"` PageSize int `json:"page_size"` TotalNumber int `json:"total_number"` TotalPage int `json:"total_page"` } `json:"page_info"` } `json:"data"` TraceId string `json:"trace_id"` } // SyncAll 同步所有音乐素材数据(自动分页) func (s *audioService) SyncAll(ctx context.Context, req *dto.SyncAudioReq) (res *dto.SyncAudioRes, err error) { // 获取access_token accessToken := req.AccessToken if accessToken == "" { accessToken = g.Cfg().MustGet(ctx, "tencent.oauth.access_token").String() } if accessToken == "" { return nil, fmt.Errorf("access_token不能为空") } res = &dto.SyncAudioRes{} totalSynced := 0 // 先获取第一页,得到总页数 firstPageData, err := s.fetchPage(ctx, accessToken, 1, 100) if err != nil { return nil, fmt.Errorf("获取第一页数据失败: %w", err) } totalPage := firstPageData.Data.PageInfo.TotalPage res.TotalNumber = firstPageData.Data.PageInfo.TotalNumber res.TotalPage = totalPage logrus.Infof("开始同步腾讯广告音乐素材 - 总页数: %d, 总记录数: %d", totalPage, res.TotalNumber) // 处理第一页数据 synced, err := s.savePageData(ctx, firstPageData) if err != nil { logrus.Errorf("保存第一页数据失败: %v", err) } totalSynced += synced // 循环获取剩余页 for page := 2; page <= totalPage; page++ { logrus.Infof("正在获取第 %d/%d 页...", page, totalPage) pageData, err := s.fetchPage(ctx, accessToken, page, 100) if err != nil { logrus.Errorf("获取第 %d 页失败: %v,继续下一页", page, err) continue } synced, err := s.savePageData(ctx, pageData) if err != nil { logrus.Errorf("保存第 %d 页数据失败: %v", page, err) continue } totalSynced += synced // 避免请求过快,休眠100ms time.Sleep(100 * time.Millisecond) } res.SyncedCount = totalSynced res.Message = fmt.Sprintf("同步完成,共处理 %d 条记录", totalSynced) logrus.Infof("同步完成 - 总页数: %d, 总记录数: %d, 成功同步: %d", totalPage, res.TotalNumber, totalSynced) return res, nil } // fetchPage 获取单页数据 func (s *audioService) fetchPage(ctx context.Context, accessToken string, page, pageSize int) (*audioResponse, error) { timestamp := time.Now().Unix() nonce := fmt.Sprintf("%d_%d", timestamp, time.Now().UnixNano()) url := fmt.Sprintf("https://api.e.qq.com/v3.0/muse_audios/get?access_token=%s×tamp=%d&nonce=%s", accessToken, timestamp, nonce) // 构建请求体 requestBody := map[string]interface{}{ "fields": []string{ "audio_id", "cover_image_url", "audio_name", "author", "duration", "expire_time", "feel_tags", "genre_tags", }, "page": page, "page_size": pageSize, } jsonBody, err := json.Marshal(requestBody) if err != nil { return nil, fmt.Errorf("序列化请求体失败: %w", err) } httpReq, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonBody)) if err != nil { return nil, fmt.Errorf("创建请求失败: %w", err) } httpReq.Header.Set("Content-Type", "application/json") client := &http.Client{Timeout: 30 * time.Second} resp, err := client.Do(httpReq) if err != nil { return nil, fmt.Errorf("请求失败: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("读取响应失败: %w", err) } var result audioResponse if err := json.Unmarshal(body, &result); err != nil { return nil, fmt.Errorf("解析响应失败: %w", err) } if result.Code != 0 { return nil, fmt.Errorf("API错误: code=%d, message=%s", result.Code, result.Message) } return &result, nil } // savePageData 保存单页数据到数据库 func (s *audioService) savePageData(ctx context.Context, data *audioResponse) (int, error) { if len(data.Data.List) == 0 { return 0, nil } logrus.Infof("准备保存 %d 条音乐素材数据", len(data.Data.List)) var items []*entity.Audio for _, item := range data.Data.List { // 序列化标签数组 feelTagsJSON, _ := json.Marshal(item.FeelTags) genreTagsJSON, _ := json.Marshal(item.GenreTags) audio := &entity.Audio{ TenantId: 1, Creator: "system", Updater: "system", AudioId: item.AudioId, CoverImageUrl: item.CoverImageUrl, AudioName: item.AudioName, Author: item.Author, Duration: item.Duration, ExpireTime: item.ExpireTime, FeelTags: string(feelTagsJSON), GenreTags: string(genreTagsJSON), // 设置默认校验状态为待校验 VerifyStatus: "PENDING", } items = append(items, audio) } logrus.Infof("调用 BatchUpsert...") successCount, err := dao.Audio.BatchUpsert(ctx, items) logrus.Infof("BatchUpsert 返回: successCount=%d, err=%v", successCount, err) return successCount, err } // ListAll 获取所有音乐素材 func (s *audioService) ListAll(ctx context.Context) ([]entity.Audio, error) { return dao.Audio.ListAll(ctx) }