2026-05-06 16:19:22 +08:00
|
|
|
|
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),
|
2026-05-07 09:07:20 +08:00
|
|
|
|
// 设置默认校验状态为待校验
|
|
|
|
|
|
VerifyStatus: "PENDING",
|
2026-05-06 16:19:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
}
|