Files
data-engine/service/tencent/audio_service.go
2026-05-07 09:07:20 +08:00

215 lines
5.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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&timestamp=%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)
}