同步音频和图片
This commit is contained in:
@@ -52,3 +52,10 @@ consul:
|
|||||||
# pass: jiahui8888
|
# pass: jiahui8888
|
||||||
jaeger: #链路追踪
|
jaeger: #链路追踪
|
||||||
addr: 116.204.74.41:4318
|
addr: 116.204.74.41:4318
|
||||||
|
|
||||||
|
tencent:
|
||||||
|
oauth:
|
||||||
|
client_id: "1112038234"
|
||||||
|
client_secret: "GxyjXFbZAs5dnsNQ"
|
||||||
|
refresh_token: "afd77374e4b0a305a1a5c1ce349f7105"
|
||||||
|
access_token: "55c1371d16c65921b1448b91ce688a49"
|
||||||
@@ -18,5 +18,8 @@ const (
|
|||||||
UnitReportDetailTable = "unit_report_detail" // 广告单元数据detail表
|
UnitReportDetailTable = "unit_report_detail" // 广告单元数据detail表
|
||||||
CampaignReportSumTable = "campaign_report_sum" // 广告计划数据detail表
|
CampaignReportSumTable = "campaign_report_sum" // 广告计划数据detail表
|
||||||
CampaignReportDetailTable = "campaign_report_detail" // 广告计划数据detail表
|
CampaignReportDetailTable = "campaign_report_detail" // 广告计划数据detail表
|
||||||
SyncTaskLogTable = "sync_task_log" // 广告计划数据detail表
|
SyncTaskLogTable = "sync_task_log" // 同步任务日志表
|
||||||
|
TencentAccountRelationTable = "tencent_account_relation" // 腾讯广告账户关系表
|
||||||
|
TencentAudioTable = "tencent_audio" // 腾讯广告音乐素材表
|
||||||
|
TencentImageTable = "tencent_image" // 腾讯广告图片素材表
|
||||||
)
|
)
|
||||||
|
|||||||
94
controller/tencent/oauth_controller.go
Normal file
94
controller/tencent/oauth_controller.go
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
package tencent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
dto "dataengine/model/dto/tencent"
|
||||||
|
entity "dataengine/model/entity/tencent"
|
||||||
|
service "dataengine/service/tencent"
|
||||||
|
|
||||||
|
"gitea.com/red-future/common/beans"
|
||||||
|
)
|
||||||
|
|
||||||
|
type oauthController struct{}
|
||||||
|
|
||||||
|
// OauthController OAuth控制器
|
||||||
|
var OauthController = new(oauthController)
|
||||||
|
|
||||||
|
// RefreshToken 刷新腾讯广告Token
|
||||||
|
func (c *oauthController) RefreshToken(ctx context.Context, req *dto.RefreshTokenReq) (res *dto.RefreshTokenRes, err error) {
|
||||||
|
ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin"})
|
||||||
|
return service.OauthService.RefreshToken(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SyncAccountRelation 同步账户关系(自动分页获取所有数据)
|
||||||
|
func (c *oauthController) SyncAccountRelation(ctx context.Context, req *dto.SyncAccountRelationReq) (res *dto.SyncAccountRelationRes, err error) {
|
||||||
|
ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin"})
|
||||||
|
return service.AccountRelationService.SyncAll(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAccountRelation 获取所有账户关系
|
||||||
|
func (c *oauthController) ListAccountRelation(ctx context.Context, req *dto.ListAccountRelationReq) (res *dto.ListAccountRelationRes, err error) {
|
||||||
|
ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin"})
|
||||||
|
|
||||||
|
list, err := service.AccountRelationService.ListAll(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为DTO
|
||||||
|
items := make([]dto.AccountRelationItem, 0, len(list))
|
||||||
|
for _, item := range list {
|
||||||
|
items = append(items, dto.AccountRelationItem{
|
||||||
|
ID: item.Id,
|
||||||
|
AccountID: item.AccountID,
|
||||||
|
CorporationName: item.CorporationName,
|
||||||
|
IsAdx: item.IsAdx,
|
||||||
|
IsBid: item.IsBid,
|
||||||
|
IsMp: item.IsMp,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
res = &dto.ListAccountRelationRes{
|
||||||
|
List: items,
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SyncAudio 同步音乐素材(自动分页获取所有数据)
|
||||||
|
func (c *oauthController) SyncAudio(ctx context.Context, req *dto.SyncAudioReq) (res *dto.SyncAudioRes, err error) {
|
||||||
|
ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin"})
|
||||||
|
return service.AudioService.SyncAll(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAudio 获取所有音乐素材
|
||||||
|
func (c *oauthController) ListAudio(ctx context.Context, req *dto.ListAudioReq) (res []entity.Audio, err error) {
|
||||||
|
ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin"})
|
||||||
|
return service.AudioService.ListAll(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SyncImage 同步图片素材(遍历所有账户,自动分页)
|
||||||
|
func (c *oauthController) SyncImage(ctx context.Context, req *dto.SyncImageReq) (res *dto.SyncImageRes, err error) {
|
||||||
|
ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin"})
|
||||||
|
return service.ImageService.SyncAll(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListImage 获取所有图片素材(旧接口,保留兼容)
|
||||||
|
func (c *oauthController) ListImage(ctx context.Context, req *dto.ListImageReq) (res []entity.Image, err error) {
|
||||||
|
ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin"})
|
||||||
|
return service.ImageService.ListAll(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListImagePage 分页查询图片素材(支持时间过滤)
|
||||||
|
func (c *oauthController) ListImagePage(ctx context.Context, req *dto.ListImagePageReq) (res *dto.ListImageRes, err error) {
|
||||||
|
ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin"})
|
||||||
|
// 转换请求参数为Service层使用的类型
|
||||||
|
queryReq := &dto.ListImageQueryReq{
|
||||||
|
Page: req.Page,
|
||||||
|
PageSize: req.PageSize,
|
||||||
|
AccountId: req.AccountId,
|
||||||
|
StartTime: req.StartTime,
|
||||||
|
EndTime: req.EndTime,
|
||||||
|
Status: req.Status,
|
||||||
|
}
|
||||||
|
return service.ImageService.ListWithPage(ctx, queryReq)
|
||||||
|
}
|
||||||
118
dao/tencent/account_relation_dao.go
Normal file
118
dao/tencent/account_relation_dao.go
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
package tencent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
consts "dataengine/consts/public"
|
||||||
|
entity "dataengine/model/entity/tencent"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.com/red-future/common/db/gfdb"
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type accountRelationDao struct{}
|
||||||
|
|
||||||
|
var AccountRelation = new(accountRelationDao)
|
||||||
|
|
||||||
|
// Upsert 插入或更新账户关系(根据account_id判断)
|
||||||
|
func (d *accountRelationDao) Upsert(ctx context.Context, item *entity.AccountRelation) error {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
// 检查是否已存在
|
||||||
|
var existing entity.AccountRelation
|
||||||
|
err := gfdb.DB(ctx).Model(ctx, consts.TencentAccountRelationTable).
|
||||||
|
Where("tenant_id", item.TenantId).
|
||||||
|
Where(entity.AccountRelationCols.AccountID, item.AccountID).
|
||||||
|
WhereNull(entity.AccountRelationCols.DeletedAt).
|
||||||
|
Scan(&existing)
|
||||||
|
|
||||||
|
// Scan找不到记录时err不为nil,但这是正常情况,需要继续执行插入
|
||||||
|
if err != nil && existing.Id == 0 {
|
||||||
|
// 记录不存在,执行插入
|
||||||
|
item.CreatedAt = &now
|
||||||
|
item.UpdatedAt = &now
|
||||||
|
|
||||||
|
_, err = gfdb.DB(ctx).Model(ctx, consts.TencentAccountRelationTable).
|
||||||
|
Data(item).
|
||||||
|
Insert()
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录存在,执行更新
|
||||||
|
item.UpdatedAt = &now
|
||||||
|
_, err = gfdb.DB(ctx).Model(ctx, consts.TencentAccountRelationTable).
|
||||||
|
Where("id", existing.Id).
|
||||||
|
Data(g.Map{
|
||||||
|
entity.AccountRelationCols.CorporationName: item.CorporationName,
|
||||||
|
entity.AccountRelationCols.CommentDataList: item.CommentDataList,
|
||||||
|
entity.AccountRelationCols.IsAdx: item.IsAdx,
|
||||||
|
entity.AccountRelationCols.IsBid: item.IsBid,
|
||||||
|
entity.AccountRelationCols.IsMp: item.IsMp,
|
||||||
|
entity.AccountRelationCols.UpdatedAt: now,
|
||||||
|
}).
|
||||||
|
Update()
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchUpsert 批量插入或更新(使用 OnConflict 实现 Upsert)
|
||||||
|
func (d *accountRelationDao) BatchUpsert(ctx context.Context, items []*entity.AccountRelation) (successCount int, err error) {
|
||||||
|
if len(items) == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Infof("开始批量Upsert: %d 条记录", len(items))
|
||||||
|
|
||||||
|
// 分批处理,每批100条
|
||||||
|
batchSize := 100
|
||||||
|
successCount = 0
|
||||||
|
|
||||||
|
for i := 0; i < len(items); i += batchSize {
|
||||||
|
end := i + batchSize
|
||||||
|
if end > len(items) {
|
||||||
|
end = len(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
batch := items[i:end]
|
||||||
|
|
||||||
|
logrus.Infof("处理第 %d-%d 条记录", i+1, end)
|
||||||
|
|
||||||
|
// 执行批量插入,使用 OnConflict 实现 Upsert
|
||||||
|
result, err := gfdb.DB(ctx).Model(ctx, consts.TencentAccountRelationTable).
|
||||||
|
Data(batch).
|
||||||
|
OnConflict(entity.AccountRelationCols.AccountID).
|
||||||
|
Save()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("批量Upsert失败: %v,尝试逐条处理", err)
|
||||||
|
// 批量失败,逐条处理
|
||||||
|
for _, item := range batch {
|
||||||
|
if upsertErr := d.Upsert(ctx, item); upsertErr != nil {
|
||||||
|
logrus.Errorf("逐条Upsert失败: account_id=%d, err=%v", item.AccountID, upsertErr)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
successCount++
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
affected, _ := result.RowsAffected()
|
||||||
|
successCount += int(affected)
|
||||||
|
logrus.Infof("批量Upsert成功: 影响 %d 条记录", affected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Infof("批量Upsert完成: 成功 %d 条", successCount)
|
||||||
|
return successCount, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAll 获取所有账户关系
|
||||||
|
func (d *accountRelationDao) ListAll(ctx context.Context) ([]entity.AccountRelation, error) {
|
||||||
|
var list []entity.AccountRelation
|
||||||
|
err := gfdb.DB(ctx).Model(ctx, consts.TencentAccountRelationTable).
|
||||||
|
WhereNull(entity.AccountRelationCols.DeletedAt).
|
||||||
|
OrderAsc(entity.AccountRelationCols.AccountID).
|
||||||
|
Scan(&list)
|
||||||
|
|
||||||
|
return list, err
|
||||||
|
}
|
||||||
111
dao/tencent/audio_dao.go
Normal file
111
dao/tencent/audio_dao.go
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
package tencent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
consts "dataengine/consts/public"
|
||||||
|
entity "dataengine/model/entity/tencent"
|
||||||
|
|
||||||
|
"gitea.com/red-future/common/db/gfdb"
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type audioDao struct{}
|
||||||
|
|
||||||
|
var Audio = new(audioDao)
|
||||||
|
|
||||||
|
// BatchUpsert 批量插入或更新(使用 OnConflict 实现 Upsert)
|
||||||
|
func (d *audioDao) BatchUpsert(ctx context.Context, items []*entity.Audio) (successCount int, err error) {
|
||||||
|
if len(items) == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Infof("开始批量Upsert音乐素材: %d 条记录", len(items))
|
||||||
|
|
||||||
|
// 分批处理,每批100条
|
||||||
|
batchSize := 100
|
||||||
|
successCount = 0
|
||||||
|
|
||||||
|
for i := 0; i < len(items); i += batchSize {
|
||||||
|
end := i + batchSize
|
||||||
|
if end > len(items) {
|
||||||
|
end = len(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
batch := items[i:end]
|
||||||
|
|
||||||
|
logrus.Infof("处理第 %d-%d 条音乐素材记录", i+1, end)
|
||||||
|
|
||||||
|
// 执行批量插入,使用 OnConflict 实现 Upsert
|
||||||
|
result, err := gfdb.DB(ctx).Model(ctx, consts.TencentAudioTable).
|
||||||
|
Data(batch).
|
||||||
|
OnConflict(entity.AudioCols.AudioId).
|
||||||
|
Save()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("批量Upsert音乐素材失败: %v,尝试逐条处理", err)
|
||||||
|
// 批量失败,逐条处理
|
||||||
|
for _, item := range batch {
|
||||||
|
if upsertErr := d.upsertSingle(ctx, item); upsertErr != nil {
|
||||||
|
logrus.Errorf("逐条Upsert音乐素材失败: audio_id=%s, err=%v", item.AudioId, upsertErr)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
successCount++
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
affected, _ := result.RowsAffected()
|
||||||
|
successCount += int(affected)
|
||||||
|
logrus.Infof("批量Upsert音乐素材成功: 影响 %d 条记录", affected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Infof("批量Upsert音乐素材完成: 成功 %d 条", successCount)
|
||||||
|
return successCount, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// upsertSingle 单条插入或更新
|
||||||
|
func (d *audioDao) upsertSingle(ctx context.Context, item *entity.Audio) error {
|
||||||
|
var existing entity.Audio
|
||||||
|
err := gfdb.DB(ctx).Model(ctx, consts.TencentAudioTable).
|
||||||
|
Where("tenant_id", item.TenantId).
|
||||||
|
Where(entity.AudioCols.AudioId, item.AudioId).
|
||||||
|
WhereNull(entity.AudioCols.DeletedAt).
|
||||||
|
Scan(&existing)
|
||||||
|
|
||||||
|
if err != nil && existing.Id == 0 {
|
||||||
|
// 记录不存在,执行插入
|
||||||
|
_, err = gfdb.DB(ctx).Model(ctx, consts.TencentAudioTable).
|
||||||
|
Data(item).
|
||||||
|
Insert()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录存在,执行更新
|
||||||
|
_, err = gfdb.DB(ctx).Model(ctx, consts.TencentAudioTable).
|
||||||
|
Where("id", existing.Id).
|
||||||
|
Data(g.Map{
|
||||||
|
entity.AudioCols.CoverImageUrl: item.CoverImageUrl,
|
||||||
|
entity.AudioCols.AudioName: item.AudioName,
|
||||||
|
entity.AudioCols.Author: item.Author,
|
||||||
|
entity.AudioCols.Duration: item.Duration,
|
||||||
|
entity.AudioCols.ExpireTime: item.ExpireTime,
|
||||||
|
entity.AudioCols.FeelTags: item.FeelTags,
|
||||||
|
entity.AudioCols.GenreTags: item.GenreTags,
|
||||||
|
entity.AudioCols.Updater: item.Updater,
|
||||||
|
entity.AudioCols.UpdatedAt: item.UpdatedAt,
|
||||||
|
}).
|
||||||
|
Update()
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAll 获取所有音乐素材
|
||||||
|
func (d *audioDao) ListAll(ctx context.Context) ([]entity.Audio, error) {
|
||||||
|
var list []entity.Audio
|
||||||
|
err := gfdb.DB(ctx).Model(ctx, consts.TencentAudioTable).
|
||||||
|
WhereNull(entity.AudioCols.DeletedAt).
|
||||||
|
OrderAsc(entity.AudioCols.AudioId).
|
||||||
|
Scan(&list)
|
||||||
|
|
||||||
|
return list, err
|
||||||
|
}
|
||||||
159
dao/tencent/image_dao.go
Normal file
159
dao/tencent/image_dao.go
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
package tencent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
consts "dataengine/consts/public"
|
||||||
|
entity "dataengine/model/entity/tencent"
|
||||||
|
|
||||||
|
"gitea.com/red-future/common/db/gfdb"
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type imageDao struct{}
|
||||||
|
|
||||||
|
var Image = new(imageDao)
|
||||||
|
|
||||||
|
// BatchUpsert 批量插入或更新(使用 OnConflict 实现 Upsert)
|
||||||
|
func (d *imageDao) BatchUpsert(ctx context.Context, items []*entity.Image) (successCount int, err error) {
|
||||||
|
if len(items) == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Infof("开始批量Upsert图片素材: %d 条记录", len(items))
|
||||||
|
|
||||||
|
// 分批处理,每批100条
|
||||||
|
batchSize := 100
|
||||||
|
successCount = 0
|
||||||
|
|
||||||
|
for i := 0; i < len(items); i += batchSize {
|
||||||
|
end := i + batchSize
|
||||||
|
if end > len(items) {
|
||||||
|
end = len(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
batch := items[i:end]
|
||||||
|
|
||||||
|
logrus.Infof("处理第 %d-%d 条图片素材记录", i+1, end)
|
||||||
|
|
||||||
|
// 执行批量插入,使用 OnConflict 实现 Upsert
|
||||||
|
result, err := gfdb.DB(ctx).Model(ctx, consts.TencentImageTable).
|
||||||
|
Data(batch).
|
||||||
|
OnConflict("(image_id, account_id)").
|
||||||
|
Save()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("批量Upsert图片素材失败: %v,尝试逐条处理", err)
|
||||||
|
// 批量失败,逐条处理
|
||||||
|
for _, item := range batch {
|
||||||
|
if upsertErr := d.upsertSingle(ctx, item); upsertErr != nil {
|
||||||
|
logrus.Errorf("逐条Upsert图片素材失败: image_id=%s, account_id=%d, err=%v", item.ImageId, item.AccountId, upsertErr)
|
||||||
|
} else {
|
||||||
|
successCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
affected, _ := result.RowsAffected()
|
||||||
|
successCount += int(affected)
|
||||||
|
logrus.Infof("批量Upsert图片素材成功: 影响 %d 条记录", affected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Infof("批量Upsert图片素材完成: 成功 %d 条", successCount)
|
||||||
|
return successCount, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// upsertSingle 单条插入或更新
|
||||||
|
func (d *imageDao) upsertSingle(ctx context.Context, item *entity.Image) error {
|
||||||
|
var existing entity.Image
|
||||||
|
err := gfdb.DB(ctx).Model(ctx, consts.TencentImageTable).
|
||||||
|
Where(entity.ImageCols.ImageId, item.ImageId).
|
||||||
|
Where(entity.ImageCols.AccountId, item.AccountId).
|
||||||
|
WhereNull("deleted_at").
|
||||||
|
Scan(&existing)
|
||||||
|
|
||||||
|
if err != nil && existing.Id == 0 {
|
||||||
|
// 记录不存在,执行插入
|
||||||
|
_, err = gfdb.DB(ctx).Model(ctx, consts.TencentImageTable).
|
||||||
|
Data(item).
|
||||||
|
Insert()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录存在,执行更新
|
||||||
|
_, err = gfdb.DB(ctx).Model(ctx, consts.TencentImageTable).
|
||||||
|
Where("id", existing.Id).
|
||||||
|
Data(g.Map{
|
||||||
|
entity.ImageCols.Width: item.Width,
|
||||||
|
entity.ImageCols.Height: item.Height,
|
||||||
|
entity.ImageCols.FileSize: item.FileSize,
|
||||||
|
entity.ImageCols.Type: item.Type,
|
||||||
|
entity.ImageCols.Signature: item.Signature,
|
||||||
|
entity.ImageCols.Description: item.Description,
|
||||||
|
entity.ImageCols.PreviewUrl: item.PreviewUrl,
|
||||||
|
entity.ImageCols.ThumbPreviewUrl: item.ThumbPreviewUrl,
|
||||||
|
entity.ImageCols.Status: item.Status,
|
||||||
|
entity.ImageCols.LastModifiedTime: item.LastModifiedTime,
|
||||||
|
}).
|
||||||
|
Update()
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAll 获取所有图片素材
|
||||||
|
func (d *imageDao) ListAll(ctx context.Context) ([]entity.Image, error) {
|
||||||
|
var list []entity.Image
|
||||||
|
err := gfdb.DB(ctx).Model(ctx, consts.TencentImageTable).
|
||||||
|
WhereNull("deleted_at").
|
||||||
|
OrderAsc(entity.ImageCols.ImageId).
|
||||||
|
Scan(&list)
|
||||||
|
|
||||||
|
return list, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListWithPage 分页查询图片素材(支持时间过滤)
|
||||||
|
func (d *imageDao) ListWithPage(ctx context.Context, page, pageSize int, accountId *int64, startTime, endTime *int64, status string) ([]entity.Image, int, error) {
|
||||||
|
model := gfdb.DB(ctx).Model(ctx, consts.TencentImageTable).
|
||||||
|
WhereNull("deleted_at")
|
||||||
|
|
||||||
|
// 账户ID过滤
|
||||||
|
if accountId != nil && *accountId > 0 {
|
||||||
|
model = model.Where(entity.ImageCols.AccountId, *accountId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态过滤
|
||||||
|
if status != "" {
|
||||||
|
model = model.Where(entity.ImageCols.Status, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 时间范围过滤(根据 last_modified_time)
|
||||||
|
if startTime != nil && *startTime > 0 {
|
||||||
|
model = model.WhereGTE(entity.ImageCols.LastModifiedTime, *startTime)
|
||||||
|
}
|
||||||
|
if endTime != nil && *endTime > 0 {
|
||||||
|
model = model.WhereLTE(entity.ImageCols.LastModifiedTime, *endTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置排序(按最后修改时间降序)
|
||||||
|
model = model.OrderDesc(entity.ImageCols.LastModifiedTime)
|
||||||
|
|
||||||
|
// 获取总数
|
||||||
|
total, err := model.Count()
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页查询
|
||||||
|
var list []entity.Image
|
||||||
|
if page > 0 && pageSize > 0 {
|
||||||
|
err = model.Page(page, pageSize).Scan(&list)
|
||||||
|
} else {
|
||||||
|
err = model.Scan(&list)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return list, total, nil
|
||||||
|
}
|
||||||
3
main.go
3
main.go
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"dataengine/controller/copydata"
|
"dataengine/controller/copydata"
|
||||||
"dataengine/controller/dict"
|
"dataengine/controller/dict"
|
||||||
|
"dataengine/controller/tencent"
|
||||||
|
|
||||||
"gitea.com/red-future/common/http"
|
"gitea.com/red-future/common/http"
|
||||||
"gitea.com/red-future/common/jaeger"
|
"gitea.com/red-future/common/jaeger"
|
||||||
@@ -29,6 +30,8 @@ func main() {
|
|||||||
copydata.CreativeReport,
|
copydata.CreativeReport,
|
||||||
copydata.UnitReport,
|
copydata.UnitReport,
|
||||||
copydata.CampaignReport,
|
copydata.CampaignReport,
|
||||||
|
// 腾讯广告OAuth
|
||||||
|
tencent.OauthController,
|
||||||
})
|
})
|
||||||
select {}
|
select {}
|
||||||
}
|
}
|
||||||
|
|||||||
48
model/dto/tencent/account_relation_dto.go
Normal file
48
model/dto/tencent/account_relation_dto.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package tencent
|
||||||
|
|
||||||
|
import "github.com/gogf/gf/v2/frame/g"
|
||||||
|
|
||||||
|
// GetAccountRelationReq 获取账户关系请求
|
||||||
|
type GetAccountRelationReq struct {
|
||||||
|
g.Meta `path:"/getAccountRelation" method:"post" tags:"腾讯广告账户关系" summary:"获取账户关系列表" dc:"从腾讯广告API获取账户关系数据"`
|
||||||
|
AccessToken string `json:"access_token" dc:"访问令牌"`
|
||||||
|
Timestamp int64 `json:"timestamp" dc:"时间戳"`
|
||||||
|
Nonce string `json:"nonce" dc:"随机字符串"`
|
||||||
|
PaginationMode string `json:"pagination_mode" dc:"分页模式" d:"PAGINATION_MODE_NORMAL"`
|
||||||
|
Page int `json:"page" dc:"页码" d:"1"`
|
||||||
|
PageSize int `json:"page_size" dc:"每页数量" d:"100"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SyncAccountRelationReq 同步账户关系请求
|
||||||
|
type SyncAccountRelationReq struct {
|
||||||
|
g.Meta `path:"/syncAccountRelation" method:"post" tags:"腾讯广告账户关系" summary:"同步账户关系" dc:"自动分页获取所有账户关系并保存到数据库"`
|
||||||
|
AccessToken string `json:"access_token" dc:"访问令牌(可选,不传则从配置读取)"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SyncAccountRelationRes 同步账户关系响应
|
||||||
|
type SyncAccountRelationRes struct {
|
||||||
|
TotalNumber int `json:"total_number" dc:"总记录数"`
|
||||||
|
TotalPage int `json:"total_page" dc:"总页数"`
|
||||||
|
SyncedCount int `json:"synced_count" dc:"同步成功数量"`
|
||||||
|
Message string `json:"message" dc:"消息"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAccountRelationReq 获取账户关系列表请求
|
||||||
|
type ListAccountRelationReq struct {
|
||||||
|
g.Meta `path:"/listAccountRelation" method:"post" tags:"腾讯广告账户关系" summary:"获取账户关系列表" dc:"从本地数据库查询账户关系列表"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAccountRelationRes 获取账户关系列表响应
|
||||||
|
type ListAccountRelationRes struct {
|
||||||
|
List []AccountRelationItem `json:"list" dc:"账户关系列表"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AccountRelationItem 账户关系项
|
||||||
|
type AccountRelationItem struct {
|
||||||
|
ID int64 `json:"id" dc:"主键ID"`
|
||||||
|
AccountID int64 `json:"account_id" dc:"账户ID"`
|
||||||
|
CorporationName string `json:"corporation_name" dc:"公司名称"`
|
||||||
|
IsAdx bool `json:"is_adx" dc:"是否ADX"`
|
||||||
|
IsBid bool `json:"is_bid" dc:"是否BID"`
|
||||||
|
IsMp bool `json:"is_mp" dc:"是否MP"`
|
||||||
|
}
|
||||||
22
model/dto/tencent/audio_dto.go
Normal file
22
model/dto/tencent/audio_dto.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package tencent
|
||||||
|
|
||||||
|
import "github.com/gogf/gf/v2/frame/g"
|
||||||
|
|
||||||
|
// SyncAudioReq 同步音乐素材请求
|
||||||
|
type SyncAudioReq struct {
|
||||||
|
g.Meta `path:"/syncAudio" method:"post" tags:"腾讯广告音乐素材" summary:"同步音乐素材" dc:"自动分页获取所有音乐素材并保存到数据库"`
|
||||||
|
AccessToken string `json:"access_token" dc:"访问令牌(可选,不传则从配置读取)"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAudioReq 获取音乐素材列表请求
|
||||||
|
type ListAudioReq struct {
|
||||||
|
g.Meta `path:"/listAudio" method:"post" tags:"腾讯广告音乐素材" summary:"获取音乐素材列表" dc:"从本地数据库查询音乐素材列表"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SyncAudioRes 同步音乐素材响应
|
||||||
|
type SyncAudioRes struct {
|
||||||
|
TotalNumber int `json:"total_number" dc:"总记录数"`
|
||||||
|
TotalPage int `json:"total_page" dc:"总页数"`
|
||||||
|
SyncedCount int `json:"synced_count" dc:"同步成功数量"`
|
||||||
|
Message string `json:"message" dc:"消息"`
|
||||||
|
}
|
||||||
72
model/dto/tencent/image_dto.go
Normal file
72
model/dto/tencent/image_dto.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
package tencent
|
||||||
|
|
||||||
|
import "github.com/gogf/gf/v2/frame/g"
|
||||||
|
|
||||||
|
// SyncImageReq 同步图片素材请求
|
||||||
|
type SyncImageReq struct {
|
||||||
|
g.Meta `path:"/syncImage" method:"post" tags:"腾讯广告图片素材" summary:"同步图片素材" dc:"遍历所有账户,自动分页获取图片素材并保存到数据库"`
|
||||||
|
AccessToken string `json:"access_token" dc:"访问令牌(可选,不传则从配置读取)"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SyncImageRes 同步图片素材响应
|
||||||
|
type SyncImageRes struct {
|
||||||
|
TotalAccounts int `json:"total_accounts" dc:"处理的账户数"`
|
||||||
|
TotalImages int `json:"total_images" dc:"总图片数"`
|
||||||
|
SyncedCount int `json:"synced_count" dc:"同步成功数量"`
|
||||||
|
Message string `json:"message" dc:"消息"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListImageReq 获取图片素材列表请求(旧接口,无分页)
|
||||||
|
type ListImageReq struct {
|
||||||
|
g.Meta `path:"/listImage" method:"post" tags:"腾讯广告图片素材" summary:"获取图片素材列表" dc:"从本地数据库查询所有图片素材(无分页)"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListImagePageReq 分页查询图片素材请求
|
||||||
|
type ListImagePageReq struct {
|
||||||
|
g.Meta `path:"/listImagePage" method:"post" tags:"腾讯广告图片素材" summary:"分页查询图片素材" dc:"支持分页、时间过滤、账户过滤等条件查询"`
|
||||||
|
Page int `json:"page" dc:"页码" d:"1"`
|
||||||
|
PageSize int `json:"page_size" dc:"每页数量" d:"20"`
|
||||||
|
AccountId *int64 `json:"account_id,omitempty" dc:"账户ID(可选)"`
|
||||||
|
StartTime *int64 `json:"start_time,omitempty" dc:"开始时间戳(秒,可选)"`
|
||||||
|
EndTime *int64 `json:"end_time,omitempty" dc:"结束时间戳(秒,可选)"`
|
||||||
|
Status string `json:"status,omitempty" dc:"状态筛选(可选)"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListImageQueryReq 图片素材查询请求(Service层使用)
|
||||||
|
type ListImageQueryReq struct {
|
||||||
|
Page int `json:"page" dc:"页码"`
|
||||||
|
PageSize int `json:"page_size" dc:"每页数量"`
|
||||||
|
AccountId *int64 `json:"account_id,omitempty" dc:"账户ID(可选)"`
|
||||||
|
StartTime *int64 `json:"start_time,omitempty" dc:"开始时间戳(秒,可选)"`
|
||||||
|
EndTime *int64 `json:"end_time,omitempty" dc:"结束时间戳(秒,可选)"`
|
||||||
|
Status string `json:"status,omitempty" dc:"状态筛选(可选)"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListImageRes 获取图片素材列表响应
|
||||||
|
type ListImageRes struct {
|
||||||
|
List []ImageItem `json:"list" dc:"图片素材列表"`
|
||||||
|
Total int `json:"total" dc:"总记录数"`
|
||||||
|
Page int `json:"page" dc:"当前页码"`
|
||||||
|
PageSize int `json:"page_size" dc:"每页数量"`
|
||||||
|
TotalPages int `json:"total_pages" dc:"总页数"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImageItem 图片素材项
|
||||||
|
type ImageItem struct {
|
||||||
|
Id int64 `json:"id" dc:"主键ID"`
|
||||||
|
ImageId string `json:"image_id" dc:"图片ID"`
|
||||||
|
AccountId int64 `json:"account_id" dc:"账户ID"`
|
||||||
|
Width int `json:"width" dc:"宽度"`
|
||||||
|
Height int `json:"height" dc:"高度"`
|
||||||
|
FileSize int64 `json:"file_size" dc:"文件大小"`
|
||||||
|
Type string `json:"type" dc:"图片类型"`
|
||||||
|
Signature string `json:"signature" dc:"签名"`
|
||||||
|
Description string `json:"description" dc:"描述"`
|
||||||
|
PreviewUrl string `json:"preview_url" dc:"预览URL"`
|
||||||
|
ThumbPreviewUrl string `json:"thumb_preview_url" dc:"缩略图URL"`
|
||||||
|
Status string `json:"status" dc:"状态"`
|
||||||
|
CreatedTime int64 `json:"created_time" dc:"创建时间戳"`
|
||||||
|
LastModifiedTime int64 `json:"last_modified_time" dc:"最后修改时间戳"`
|
||||||
|
CreatedAt string `json:"created_at" dc:"数据库创建时间"`
|
||||||
|
UpdatedAt string `json:"updated_at" dc:"数据库更新时间"`
|
||||||
|
}
|
||||||
19
model/dto/tencent/oauth_dto.go
Normal file
19
model/dto/tencent/oauth_dto.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package tencent
|
||||||
|
|
||||||
|
import "github.com/gogf/gf/v2/frame/g"
|
||||||
|
|
||||||
|
// RefreshTokenReq 刷新Token请求
|
||||||
|
type RefreshTokenReq struct {
|
||||||
|
g.Meta `path:"/refreshToken" method:"post" tags:"腾讯广告OAuth" summary:"刷新访问令牌" dc:"使用refresh_token获取新的access_token"`
|
||||||
|
ClientID string `json:"client_id" dc:"客户端ID"`
|
||||||
|
ClientSecret string `json:"client_secret" dc:"客户端密钥"`
|
||||||
|
RefreshToken string `json:"refresh_token" dc:"刷新令牌"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefreshTokenRes 刷新Token响应
|
||||||
|
type RefreshTokenRes struct {
|
||||||
|
AccessToken string `json:"access_token" dc:"访问令牌"`
|
||||||
|
RefreshToken string `json:"refresh_token" dc:"新的刷新令牌"`
|
||||||
|
AccessTokenExpiresIn int64 `json:"access_token_expires_in" dc:"访问令牌过期时间(秒)"`
|
||||||
|
RefreshTokenExpiresIn int64 `json:"refresh_token_expires_in" dc:"刷新令牌过期时间(秒)"`
|
||||||
|
}
|
||||||
56
model/entity/tencent/account_relation.go
Normal file
56
model/entity/tencent/account_relation.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package tencent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AccountRelation 腾讯广告账户关系实体
|
||||||
|
type AccountRelation struct {
|
||||||
|
Id int64 `orm:"id" json:"id" description:"主键ID"`
|
||||||
|
TenantId int64 `orm:"tenant_id" json:"tenantId" description:"租户ID"`
|
||||||
|
Creator string `orm:"creator" json:"creator" description:"创建人"`
|
||||||
|
CreatedAt *time.Time `orm:"created_at" json:"createdAt" description:"创建时间"`
|
||||||
|
Updater string `orm:"updater" json:"updater" description:"更新人"`
|
||||||
|
UpdatedAt *time.Time `orm:"updated_at" json:"updatedAt" description:"更新时间"`
|
||||||
|
DeletedAt *time.Time `orm:"deleted_at" json:"deletedAt" description:"软删除时间"`
|
||||||
|
AccountID int64 `orm:"account_id" json:"accountId" description:"账户ID"`
|
||||||
|
CorporationName string `orm:"corporation_name" json:"corporationName" description:"公司名称"`
|
||||||
|
CommentDataList string `orm:"comment_data_list" json:"commentDataList" description:"备注数据列表JSON"`
|
||||||
|
IsAdx bool `orm:"is_adx" json:"isAdx" description:"是否ADX"`
|
||||||
|
IsBid bool `orm:"is_bid" json:"isBid" description:"是否BID"`
|
||||||
|
IsMp bool `orm:"is_mp" json:"isMp" description:"是否MP"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AccountRelationCol 账户关系表字段定义
|
||||||
|
type AccountRelationCol struct {
|
||||||
|
ID string
|
||||||
|
TenantID string
|
||||||
|
Creator string
|
||||||
|
CreatedAt string
|
||||||
|
Updater string
|
||||||
|
UpdatedAt string
|
||||||
|
DeletedAt string
|
||||||
|
AccountID string
|
||||||
|
CorporationName string
|
||||||
|
CommentDataList string
|
||||||
|
IsAdx string
|
||||||
|
IsBid string
|
||||||
|
IsMp string
|
||||||
|
}
|
||||||
|
|
||||||
|
// AccountRelationCols 账户关系表字段常量
|
||||||
|
var AccountRelationCols = AccountRelationCol{
|
||||||
|
ID: "id",
|
||||||
|
TenantID: "tenant_id",
|
||||||
|
Creator: "creator",
|
||||||
|
CreatedAt: "created_at",
|
||||||
|
Updater: "updater",
|
||||||
|
UpdatedAt: "updated_at",
|
||||||
|
DeletedAt: "deleted_at",
|
||||||
|
AccountID: "account_id",
|
||||||
|
CorporationName: "corporation_name",
|
||||||
|
CommentDataList: "comment_data_list",
|
||||||
|
IsAdx: "is_adx",
|
||||||
|
IsBid: "is_bid",
|
||||||
|
IsMp: "is_mp",
|
||||||
|
}
|
||||||
62
model/entity/tencent/audio.go
Normal file
62
model/entity/tencent/audio.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package tencent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Audio 腾讯广告音乐素材实体
|
||||||
|
type Audio struct {
|
||||||
|
Id int64 `orm:"id" json:"id" description:"主键ID"`
|
||||||
|
TenantId int64 `orm:"tenant_id" json:"tenantId" description:"租户ID"`
|
||||||
|
Creator string `orm:"creator" json:"creator" description:"创建人"`
|
||||||
|
CreatedAt *time.Time `orm:"created_at" json:"createdAt" description:"创建时间"`
|
||||||
|
Updater string `orm:"updater" json:"updater" description:"更新人"`
|
||||||
|
UpdatedAt *time.Time `orm:"updated_at" json:"updatedAt" description:"更新时间"`
|
||||||
|
DeletedAt *time.Time `orm:"deleted_at" json:"deletedAt" description:"软删除时间"`
|
||||||
|
AudioId string `orm:"audio_id" json:"audioId" description:"音乐ID"`
|
||||||
|
CoverImageUrl string `orm:"cover_image_url" json:"coverImageUrl" description:"封面图片URL"`
|
||||||
|
AudioName string `orm:"audio_name" json:"audioName" description:"音乐名称"`
|
||||||
|
Author string `orm:"author" json:"author" description:"作者"`
|
||||||
|
Duration float64 `orm:"duration" json:"duration" description:"时长(秒)"`
|
||||||
|
ExpireTime int64 `orm:"expire_time" json:"expireTime" description:"过期时间戳"`
|
||||||
|
FeelTags string `orm:"feel_tags" json:"feelTags" description:"情感标签数组JSON"`
|
||||||
|
GenreTags string `orm:"genre_tags" json:"genreTags" description:"风格标签数组JSON"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AudioCol 音乐素材表字段定义
|
||||||
|
type AudioCol struct {
|
||||||
|
Id string
|
||||||
|
TenantId string
|
||||||
|
Creator string
|
||||||
|
CreatedAt string
|
||||||
|
Updater string
|
||||||
|
UpdatedAt string
|
||||||
|
DeletedAt string
|
||||||
|
AudioId string
|
||||||
|
CoverImageUrl string
|
||||||
|
AudioName string
|
||||||
|
Author string
|
||||||
|
Duration string
|
||||||
|
ExpireTime string
|
||||||
|
FeelTags string
|
||||||
|
GenreTags string
|
||||||
|
}
|
||||||
|
|
||||||
|
// AudioCols 音乐素材表字段常量
|
||||||
|
var AudioCols = AudioCol{
|
||||||
|
Id: "id",
|
||||||
|
TenantId: "tenant_id",
|
||||||
|
Creator: "creator",
|
||||||
|
CreatedAt: "created_at",
|
||||||
|
Updater: "updater",
|
||||||
|
UpdatedAt: "updated_at",
|
||||||
|
DeletedAt: "deleted_at",
|
||||||
|
AudioId: "audio_id",
|
||||||
|
CoverImageUrl: "cover_image_url",
|
||||||
|
AudioName: "audio_name",
|
||||||
|
Author: "author",
|
||||||
|
Duration: "duration",
|
||||||
|
ExpireTime: "expire_time",
|
||||||
|
FeelTags: "feel_tags",
|
||||||
|
GenreTags: "genre_tags",
|
||||||
|
}
|
||||||
117
model/entity/tencent/image.go
Normal file
117
model/entity/tencent/image.go
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
package tencent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gitea.com/red-future/common/beans"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Image 腾讯广告图片素材实体
|
||||||
|
type Image struct {
|
||||||
|
beans.SQLBaseDO `orm:",inherit"`
|
||||||
|
|
||||||
|
ImageId string `orm:"image_id" json:"imageId" description:"图片ID"`
|
||||||
|
AccountId int64 `orm:"account_id" json:"accountId" description:"账户ID"`
|
||||||
|
Width int `orm:"width" json:"width" description:"宽度"`
|
||||||
|
Height int `orm:"height" json:"height" description:"高度"`
|
||||||
|
FileSize int64 `orm:"file_size" json:"fileSize" description:"文件大小"`
|
||||||
|
Type string `orm:"type" json:"type" description:"图片类型"`
|
||||||
|
Signature string `orm:"signature" json:"signature" description:"签名"`
|
||||||
|
Description string `orm:"description" json:"description" description:"描述"`
|
||||||
|
SourceSignature string `orm:"source_signature" json:"sourceSignature" description:"源签名"`
|
||||||
|
PreviewUrl string `orm:"preview_url" json:"previewUrl" description:"预览URL"`
|
||||||
|
ThumbPreviewUrl string `orm:"thumb_preview_url" json:"thumbPreviewUrl" description:"缩略图URL"`
|
||||||
|
SourceType string `orm:"source_type" json:"sourceType" description:"来源类型"`
|
||||||
|
ImageUsage string `orm:"image_usage" json:"imageUsage" description:"图片用途"`
|
||||||
|
CreatedTime int64 `orm:"created_time" json:"createdTime" description:"创建时间戳"`
|
||||||
|
LastModifiedTime int64 `orm:"last_modified_time" json:"lastModifiedTime" description:"最后修改时间戳"`
|
||||||
|
ProductCatalogId int64 `orm:"product_catalog_id" json:"productCatalogId" description:"产品目录ID"`
|
||||||
|
ProductOuterId string `orm:"product_outer_id" json:"productOuterId" description:"产品外部ID"`
|
||||||
|
SourceReferenceId string `orm:"source_reference_id" json:"sourceReferenceId" description:"源引用ID"`
|
||||||
|
OwnerAccountId string `orm:"owner_account_id" json:"ownerAccountId" description:"所有者账户ID"`
|
||||||
|
Status string `orm:"status" json:"status" description:"状态"`
|
||||||
|
SampleAspectRatio string `orm:"sample_aspect_ratio" json:"sampleAspectRatio" description:"示例宽高比"`
|
||||||
|
SourceMaterialId string `orm:"source_material_id" json:"sourceMaterialId" description:"源素材ID"`
|
||||||
|
NewSourceType string `orm:"new_source_type" json:"newSourceType" description:"新来源类型"`
|
||||||
|
FirstPublicationStatus string `orm:"first_publication_status" json:"firstPublicationStatus" description:"首次发布状态"`
|
||||||
|
QualityStatus string `orm:"quality_status" json:"qualityStatus" description:"质量状态"`
|
||||||
|
SimilarityStatus string `orm:"similarity_status" json:"similarityStatus" description:"相似度状态"`
|
||||||
|
UserAigcStatus string `orm:"user_aigc_status" json:"userAigcStatus" description:"用户AIGC状态"`
|
||||||
|
SystemAigcStatus string `orm:"system_aigc_status" json:"systemAigcStatus" description:"系统AIGC状态"`
|
||||||
|
AigcSource string `orm:"aigc_source" json:"aigcSource" description:"AIGC来源"`
|
||||||
|
AigcFlag string `orm:"aigc_flag" json:"aigcFlag" description:"AIGC标志"`
|
||||||
|
MuseAigcVersion int `orm:"muse_aigc_version" json:"museAigcVersion" description:"Muse AIGC版本"`
|
||||||
|
AigcType int `orm:"aigc_type" json:"aigcType" description:"AIGC类型"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImageCol 图片素材表字段定义
|
||||||
|
type ImageCol struct {
|
||||||
|
beans.SQLBaseCol
|
||||||
|
ImageId string
|
||||||
|
AccountId string
|
||||||
|
Width string
|
||||||
|
Height string
|
||||||
|
FileSize string
|
||||||
|
Type string
|
||||||
|
Signature string
|
||||||
|
Description string
|
||||||
|
SourceSignature string
|
||||||
|
PreviewUrl string
|
||||||
|
ThumbPreviewUrl string
|
||||||
|
SourceType string
|
||||||
|
ImageUsage string
|
||||||
|
CreatedTime string
|
||||||
|
LastModifiedTime string
|
||||||
|
ProductCatalogId string
|
||||||
|
ProductOuterId string
|
||||||
|
SourceReferenceId string
|
||||||
|
OwnerAccountId string
|
||||||
|
Status string
|
||||||
|
SampleAspectRatio string
|
||||||
|
SourceMaterialId string
|
||||||
|
NewSourceType string
|
||||||
|
FirstPublicationStatus string
|
||||||
|
QualityStatus string
|
||||||
|
SimilarityStatus string
|
||||||
|
UserAigcStatus string
|
||||||
|
SystemAigcStatus string
|
||||||
|
AigcSource string
|
||||||
|
AigcFlag string
|
||||||
|
MuseAigcVersion string
|
||||||
|
AigcType string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImageCols 图片素材表字段常量
|
||||||
|
var ImageCols = ImageCol{
|
||||||
|
SQLBaseCol: beans.DefSQLBaseCol,
|
||||||
|
ImageId: "image_id",
|
||||||
|
AccountId: "account_id",
|
||||||
|
Width: "width",
|
||||||
|
Height: "height",
|
||||||
|
FileSize: "file_size",
|
||||||
|
Type: "type",
|
||||||
|
Signature: "signature",
|
||||||
|
Description: "description",
|
||||||
|
SourceSignature: "source_signature",
|
||||||
|
PreviewUrl: "preview_url",
|
||||||
|
ThumbPreviewUrl: "thumb_preview_url",
|
||||||
|
SourceType: "source_type",
|
||||||
|
ImageUsage: "image_usage",
|
||||||
|
CreatedTime: "created_time",
|
||||||
|
LastModifiedTime: "last_modified_time",
|
||||||
|
ProductCatalogId: "product_catalog_id",
|
||||||
|
ProductOuterId: "product_outer_id",
|
||||||
|
SourceReferenceId: "source_reference_id",
|
||||||
|
OwnerAccountId: "owner_account_id",
|
||||||
|
Status: "status",
|
||||||
|
SampleAspectRatio: "sample_aspect_ratio",
|
||||||
|
SourceMaterialId: "source_material_id",
|
||||||
|
NewSourceType: "new_source_type",
|
||||||
|
FirstPublicationStatus: "first_publication_status",
|
||||||
|
QualityStatus: "quality_status",
|
||||||
|
SimilarityStatus: "similarity_status",
|
||||||
|
UserAigcStatus: "user_aigc_status",
|
||||||
|
SystemAigcStatus: "system_aigc_status",
|
||||||
|
AigcSource: "aigc_source",
|
||||||
|
AigcFlag: "aigc_flag",
|
||||||
|
MuseAigcVersion: "muse_aigc_version",
|
||||||
|
AigcType: "aigc_type",
|
||||||
|
}
|
||||||
4056
resource/log/server/2026-05-06.log
Normal file
4056
resource/log/server/2026-05-06.log
Normal file
File diff suppressed because it is too large
Load Diff
186
service/tencent/account_relation_service.go
Normal file
186
service/tencent/account_relation_service.go
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
package tencent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"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 accountRelationService struct{}
|
||||||
|
|
||||||
|
var AccountRelationService = new(accountRelationService)
|
||||||
|
|
||||||
|
// API响应结构
|
||||||
|
type accountRelationResponse struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data struct {
|
||||||
|
List []struct {
|
||||||
|
AccountID int64 `json:"account_id"`
|
||||||
|
CorporationName string `json:"corporation_name"`
|
||||||
|
CommentDataList json.RawMessage `json:"comment_data_list"`
|
||||||
|
IsAdx bool `json:"is_adx"`
|
||||||
|
IsBid bool `json:"is_bid"`
|
||||||
|
IsMp bool `json:"is_mp"`
|
||||||
|
} `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 *accountRelationService) SyncAll(ctx context.Context, req *dto.SyncAccountRelationReq) (res *dto.SyncAccountRelationRes, 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.SyncAccountRelationRes{}
|
||||||
|
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 *accountRelationService) fetchPage(ctx context.Context, accessToken string, page, pageSize int) (*accountRelationResponse, error) {
|
||||||
|
timestamp := time.Now().Unix()
|
||||||
|
// 使用时间戳+随机数生成唯一的nonce
|
||||||
|
nonce := fmt.Sprintf("%d_%d", timestamp, time.Now().UnixNano())
|
||||||
|
|
||||||
|
url := fmt.Sprintf("https://api.e.qq.com/v3.0/organization_account_relation/get?"+
|
||||||
|
"access_token=%s×tamp=%d&nonce=%s&pagination_mode=PAGINATION_MODE_NORMAL&page=%d&page_size=%d",
|
||||||
|
accessToken, timestamp, nonce, page, pageSize)
|
||||||
|
|
||||||
|
httpReq, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("创建请求失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 accountRelationResponse
|
||||||
|
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 *accountRelationService) savePageData(ctx context.Context, data *accountRelationResponse) (int, error) {
|
||||||
|
if len(data.Data.List) == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Infof("准备保存 %d 条账户关系数据", len(data.Data.List))
|
||||||
|
|
||||||
|
var items []*entity.AccountRelation
|
||||||
|
for _, item := range data.Data.List {
|
||||||
|
commentJSON := "{}"
|
||||||
|
if len(item.CommentDataList) > 0 {
|
||||||
|
commentJSON = string(item.CommentDataList)
|
||||||
|
}
|
||||||
|
|
||||||
|
accountRelation := &entity.AccountRelation{
|
||||||
|
AccountID: item.AccountID,
|
||||||
|
CorporationName: item.CorporationName,
|
||||||
|
CommentDataList: commentJSON,
|
||||||
|
IsAdx: item.IsAdx,
|
||||||
|
IsBid: item.IsBid,
|
||||||
|
IsMp: item.IsMp,
|
||||||
|
}
|
||||||
|
// 设置 TenantID(框架将0视为空值,所以使用1)
|
||||||
|
accountRelation.TenantId = 1
|
||||||
|
|
||||||
|
items = append(items, accountRelation)
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Infof("调用 BatchUpsert...")
|
||||||
|
successCount, err := dao.AccountRelation.BatchUpsert(ctx, items)
|
||||||
|
logrus.Infof("BatchUpsert 返回: successCount=%d, err=%v", successCount, err)
|
||||||
|
|
||||||
|
return successCount, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAll 获取所有账户关系
|
||||||
|
func (s *accountRelationService) ListAll(ctx context.Context) ([]entity.AccountRelation, error) {
|
||||||
|
return dao.AccountRelation.ListAll(ctx)
|
||||||
|
}
|
||||||
212
service/tencent/audio_service.go
Normal file
212
service/tencent/audio_service.go
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
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),
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
363
service/tencent/image_service.go
Normal file
363
service/tencent/image_service.go
Normal file
@@ -0,0 +1,363 @@
|
|||||||
|
package tencent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
dao "dataengine/dao/tencent"
|
||||||
|
dto "dataengine/model/dto/tencent"
|
||||||
|
entity "dataengine/model/entity/tencent"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.com/red-future/common/db/gfdb"
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type imageService struct{}
|
||||||
|
|
||||||
|
var ImageService = new(imageService)
|
||||||
|
|
||||||
|
// API响应结构
|
||||||
|
type imageResponse struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data struct {
|
||||||
|
List []struct {
|
||||||
|
ImageId string `json:"image_id"`
|
||||||
|
Width int `json:"width"`
|
||||||
|
Height int `json:"height"`
|
||||||
|
FileSize int64 `json:"file_size"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Signature string `json:"signature"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
SourceSignature string `json:"source_signature"`
|
||||||
|
PreviewUrl string `json:"preview_url"`
|
||||||
|
ThumbPreviewUrl string `json:"thumb_preview_url"`
|
||||||
|
SourceType string `json:"source_type"`
|
||||||
|
ImageUsage string `json:"image_usage"`
|
||||||
|
CreatedTime int64 `json:"created_time"`
|
||||||
|
LastModifiedTime int64 `json:"last_modified_time"`
|
||||||
|
ProductCatalogId int64 `json:"product_catalog_id"`
|
||||||
|
ProductOuterId string `json:"product_outer_id"`
|
||||||
|
SourceReferenceId string `json:"source_reference_id"`
|
||||||
|
OwnerAccountId string `json:"owner_account_id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
SampleAspectRatio string `json:"sample_aspect_ratio"`
|
||||||
|
SourceMaterialId string `json:"source_material_id"`
|
||||||
|
NewSourceType string `json:"new_source_type"`
|
||||||
|
FirstPublicationStatus string `json:"first_publication_status"`
|
||||||
|
QualityStatus string `json:"quality_status"`
|
||||||
|
SimilarityStatus string `json:"similarity_status"`
|
||||||
|
UserAigcStatus string `json:"user_aigc_status"`
|
||||||
|
SystemAigcStatus string `json:"system_aigc_status"`
|
||||||
|
AigcSource string `json:"aigc_source"`
|
||||||
|
AigcFlag string `json:"aigc_flag"`
|
||||||
|
MuseAigcVersion int `json:"muse_aigc_version"`
|
||||||
|
AigcType int `json:"aigc_type"`
|
||||||
|
} `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 *imageService) SyncAll(ctx context.Context, req *dto.SyncImageReq) (res *dto.SyncImageRes, err error) {
|
||||||
|
// 创建独立的context,避免HTTP请求超时导致context被取消
|
||||||
|
// 设置30分钟超时,足够完成421个账户的同步任务
|
||||||
|
independentCtx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// 保留原context中的user信息,供数据库中间件使用
|
||||||
|
if user := ctx.Value("user"); user != nil {
|
||||||
|
independentCtx = context.WithValue(independentCtx, "user", user)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取access_token
|
||||||
|
accessToken := req.AccessToken
|
||||||
|
if accessToken == "" {
|
||||||
|
accessToken = g.Cfg().MustGet(independentCtx, "tencent.oauth.access_token").String()
|
||||||
|
}
|
||||||
|
|
||||||
|
if accessToken == "" {
|
||||||
|
return nil, fmt.Errorf("access_token不能为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
res = &dto.SyncImageRes{}
|
||||||
|
totalSynced := 0
|
||||||
|
totalImages := 0
|
||||||
|
|
||||||
|
// 获取所有账户列表
|
||||||
|
accounts, err := s.getAccountList(independentCtx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("获取账户列表失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
res.TotalAccounts = len(accounts)
|
||||||
|
logrus.Infof("开始同步腾讯广告图片素材 - 账户数: %d", len(accounts))
|
||||||
|
|
||||||
|
// 遍历每个账户
|
||||||
|
for _, account := range accounts {
|
||||||
|
logrus.Infof("========== 开始处理账户: %d (%s) ==========", account.AccountID, account.CorporationName)
|
||||||
|
|
||||||
|
// 获取该账户的所有图片(分页)
|
||||||
|
accountImages, err := s.syncAccountImages(independentCtx, accessToken, account.AccountID)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("账户 %d 同步失败: %v,继续下一个账户", account.AccountID, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
totalImages += accountImages
|
||||||
|
totalSynced += accountImages
|
||||||
|
|
||||||
|
// 避免请求过快,休眠200ms
|
||||||
|
time.Sleep(200 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
res.TotalImages = totalImages
|
||||||
|
res.SyncedCount = totalSynced
|
||||||
|
res.Message = fmt.Sprintf("同步完成,共处理 %d 个账户,%d 条图片记录", res.TotalAccounts, totalSynced)
|
||||||
|
|
||||||
|
logrus.Infof("同步完成 - 账户数: %d, 总图片数: %d, 成功同步: %d", res.TotalAccounts, totalImages, totalSynced)
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getAccountList 获取所有账户列表
|
||||||
|
func (s *imageService) getAccountList(ctx context.Context) ([]entity.AccountRelation, error) {
|
||||||
|
var accounts []entity.AccountRelation
|
||||||
|
err := gfdb.DB(ctx).Model(ctx, "tencent_account_relation").
|
||||||
|
WhereNull("deleted_at").
|
||||||
|
Scan(&accounts)
|
||||||
|
|
||||||
|
return accounts, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// syncAccountImages 同步单个账户的图片数据
|
||||||
|
func (s *imageService) syncAccountImages(ctx context.Context, accessToken string, accountId int64) (int, error) {
|
||||||
|
totalSynced := 0
|
||||||
|
|
||||||
|
// 先获取第一页,得到总页数
|
||||||
|
firstPageData, err := s.fetchPage(ctx, accessToken, accountId, 1, 100)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("获取第一页数据失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
totalPage := firstPageData.Data.PageInfo.TotalPage
|
||||||
|
logrus.Infof("账户 %d - 总页数: %d, 总记录数: %d", accountId, totalPage, firstPageData.Data.PageInfo.TotalNumber)
|
||||||
|
|
||||||
|
// 处理第一页数据
|
||||||
|
synced, err := s.savePageData(ctx, firstPageData, accountId)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("保存第一页数据失败: %v", err)
|
||||||
|
}
|
||||||
|
totalSynced += synced
|
||||||
|
|
||||||
|
// 循环获取剩余页
|
||||||
|
for page := 2; page <= totalPage; page++ {
|
||||||
|
logrus.Infof("账户 %d - 正在获取第 %d/%d 页...", accountId, page, totalPage)
|
||||||
|
|
||||||
|
pageData, err := s.fetchPage(ctx, accessToken, accountId, page, 100)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("账户 %d - 获取第 %d 页失败: %v,继续下一页", accountId, page, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
synced, err := s.savePageData(ctx, pageData, accountId)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("账户 %d - 保存第 %d 页数据失败: %v", accountId, page, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
totalSynced += synced
|
||||||
|
|
||||||
|
// 避免请求过快,休眠100ms
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Infof("账户 %d - 同步完成,共 %d 条记录", accountId, totalSynced)
|
||||||
|
return totalSynced, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchPage 获取单页数据
|
||||||
|
func (s *imageService) fetchPage(ctx context.Context, accessToken string, accountId int64, page, pageSize int) (*imageResponse, error) {
|
||||||
|
// 构建filtering参数:状态为正常
|
||||||
|
filtering := `[{"field":"status","operator":"EQUALS","values":["ADSTATUS_NORMAL"]}]`
|
||||||
|
|
||||||
|
// URL编码filtering参数
|
||||||
|
encodedFiltering := url.QueryEscape(filtering)
|
||||||
|
|
||||||
|
// 在发送请求前生成最新的时间戳和nonce,避免时间戳过期
|
||||||
|
timestamp := time.Now().Unix()
|
||||||
|
// 使用时间戳+纳秒后6位+随机数,确保唯一性且不超过32字符
|
||||||
|
nanoSuffix := time.Now().UnixNano() % 1000000 // 取纳秒的后6位
|
||||||
|
nonce := fmt.Sprintf("%d%06d%d", timestamp, nanoSuffix, rand.Intn(1000))
|
||||||
|
|
||||||
|
urlStr := fmt.Sprintf("https://api.e.qq.com/v3.0/images/get?access_token=%s&nonce=%s×tamp=%d&account_id=%d&filtering=%s&page=%d&page_size=%d",
|
||||||
|
accessToken, nonce, timestamp, accountId, encodedFiltering, page, pageSize)
|
||||||
|
|
||||||
|
logrus.Debugf("请求URL: %s", urlStr)
|
||||||
|
|
||||||
|
httpReq, err := http.NewRequestWithContext(ctx, "GET", urlStr, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("创建请求失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Debugf("API响应: %s", string(body))
|
||||||
|
|
||||||
|
var result imageResponse
|
||||||
|
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 *imageService) savePageData(ctx context.Context, data *imageResponse, accountId int64) (int, error) {
|
||||||
|
if len(data.Data.List) == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Infof("准备保存 %d 条图片素材数据", len(data.Data.List))
|
||||||
|
|
||||||
|
var items []*entity.Image
|
||||||
|
for _, item := range data.Data.List {
|
||||||
|
image := &entity.Image{
|
||||||
|
ImageId: item.ImageId,
|
||||||
|
AccountId: accountId,
|
||||||
|
Width: item.Width,
|
||||||
|
Height: item.Height,
|
||||||
|
FileSize: item.FileSize,
|
||||||
|
Type: item.Type,
|
||||||
|
Signature: item.Signature,
|
||||||
|
Description: item.Description,
|
||||||
|
SourceSignature: item.SourceSignature,
|
||||||
|
PreviewUrl: item.PreviewUrl,
|
||||||
|
ThumbPreviewUrl: item.ThumbPreviewUrl,
|
||||||
|
SourceType: item.SourceType,
|
||||||
|
ImageUsage: item.ImageUsage,
|
||||||
|
CreatedTime: item.CreatedTime,
|
||||||
|
LastModifiedTime: item.LastModifiedTime,
|
||||||
|
ProductCatalogId: item.ProductCatalogId,
|
||||||
|
ProductOuterId: item.ProductOuterId,
|
||||||
|
SourceReferenceId: item.SourceReferenceId,
|
||||||
|
OwnerAccountId: item.OwnerAccountId,
|
||||||
|
Status: item.Status,
|
||||||
|
SampleAspectRatio: item.SampleAspectRatio,
|
||||||
|
SourceMaterialId: item.SourceMaterialId,
|
||||||
|
NewSourceType: item.NewSourceType,
|
||||||
|
FirstPublicationStatus: item.FirstPublicationStatus,
|
||||||
|
QualityStatus: item.QualityStatus,
|
||||||
|
SimilarityStatus: item.SimilarityStatus,
|
||||||
|
UserAigcStatus: item.UserAigcStatus,
|
||||||
|
SystemAigcStatus: item.SystemAigcStatus,
|
||||||
|
AigcSource: item.AigcSource,
|
||||||
|
AigcFlag: item.AigcFlag,
|
||||||
|
MuseAigcVersion: item.MuseAigcVersion,
|
||||||
|
AigcType: item.AigcType,
|
||||||
|
}
|
||||||
|
// 设置 TenantID(框架将0视为空值,所以使用1)
|
||||||
|
image.TenantId = 1
|
||||||
|
|
||||||
|
items = append(items, image)
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Infof("调用 BatchUpsert...")
|
||||||
|
successCount, err := dao.Image.BatchUpsert(ctx, items)
|
||||||
|
logrus.Infof("BatchUpsert 返回: successCount=%d, err=%v", successCount, err)
|
||||||
|
|
||||||
|
return successCount, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAll 获取所有图片素材
|
||||||
|
func (s *imageService) ListAll(ctx context.Context) ([]entity.Image, error) {
|
||||||
|
return dao.Image.ListAll(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListWithPage 分页查询图片素材(支持时间过滤)
|
||||||
|
func (s *imageService) ListWithPage(ctx context.Context, req *dto.ListImageQueryReq) (*dto.ListImageRes, error) {
|
||||||
|
// 设置默认值
|
||||||
|
page := req.Page
|
||||||
|
if page <= 0 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
pageSize := req.PageSize
|
||||||
|
if pageSize <= 0 {
|
||||||
|
pageSize = 20
|
||||||
|
}
|
||||||
|
if pageSize > 100 {
|
||||||
|
pageSize = 100 // 限制最大每页数量
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用DAO层查询
|
||||||
|
list, total, err := dao.Image.ListWithPage(ctx, page, pageSize, req.AccountId, req.StartTime, req.EndTime, req.Status)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("查询图片素材失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算总页数
|
||||||
|
totalPages := (total + pageSize - 1) / pageSize
|
||||||
|
if totalPages == 0 && total > 0 {
|
||||||
|
totalPages = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为DTO
|
||||||
|
items := make([]dto.ImageItem, 0, len(list))
|
||||||
|
for _, item := range list {
|
||||||
|
items = append(items, dto.ImageItem{
|
||||||
|
Id: item.Id,
|
||||||
|
ImageId: item.ImageId,
|
||||||
|
AccountId: item.AccountId,
|
||||||
|
Width: item.Width,
|
||||||
|
Height: item.Height,
|
||||||
|
FileSize: item.FileSize,
|
||||||
|
Type: item.Type,
|
||||||
|
Signature: item.Signature,
|
||||||
|
Description: item.Description,
|
||||||
|
PreviewUrl: item.PreviewUrl,
|
||||||
|
ThumbPreviewUrl: item.ThumbPreviewUrl,
|
||||||
|
Status: item.Status,
|
||||||
|
CreatedTime: item.CreatedTime,
|
||||||
|
LastModifiedTime: item.LastModifiedTime,
|
||||||
|
CreatedAt: item.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||||
|
UpdatedAt: item.UpdatedAt.Format("2006-01-02 15:04:05"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
res := &dto.ListImageRes{
|
||||||
|
List: items,
|
||||||
|
Total: total,
|
||||||
|
Page: page,
|
||||||
|
PageSize: pageSize,
|
||||||
|
TotalPages: totalPages,
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Infof("查询图片素材 - 页码: %d, 每页: %d, 总数: %d, 总页数: %d", page, pageSize, total, totalPages)
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
78
service/tencent/oauth_service.go
Normal file
78
service/tencent/oauth_service.go
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
package tencent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
dto "dataengine/model/dto/tencent"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
)
|
||||||
|
|
||||||
|
type oauthService struct{}
|
||||||
|
|
||||||
|
var OauthService = new(oauthService)
|
||||||
|
|
||||||
|
// RefreshToken 刷新腾讯广告Token
|
||||||
|
func (s *oauthService) RefreshToken(ctx context.Context, req *dto.RefreshTokenReq) (res *dto.RefreshTokenRes, err error) {
|
||||||
|
// 如果请求中没有提供参数,则从配置文件读取
|
||||||
|
clientID := req.ClientID
|
||||||
|
clientSecret := req.ClientSecret
|
||||||
|
refreshToken := req.RefreshToken
|
||||||
|
|
||||||
|
if clientID == "" || clientSecret == "" || refreshToken == "" {
|
||||||
|
clientID = g.Cfg().MustGet(ctx, "tencent.oauth.client_id").String()
|
||||||
|
clientSecret = g.Cfg().MustGet(ctx, "tencent.oauth.client_secret").String()
|
||||||
|
refreshToken = g.Cfg().MustGet(ctx, "tencent.oauth.refresh_token").String()
|
||||||
|
}
|
||||||
|
|
||||||
|
url := fmt.Sprintf("https://api.e.qq.com/oauth/refresh_token?client_id=%s&client_secret=%s&refresh_token=%s",
|
||||||
|
clientID, clientSecret, refreshToken)
|
||||||
|
|
||||||
|
httpReq, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("创建请求失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
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 struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
AccessTokenExpiresIn int64 `json:"access_token_expires_in"`
|
||||||
|
RefreshTokenExpiresIn int64 `json:"refresh_token_expires_in"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
res = &dto.RefreshTokenRes{
|
||||||
|
AccessToken: result.Data.AccessToken,
|
||||||
|
RefreshToken: result.Data.RefreshToken,
|
||||||
|
AccessTokenExpiresIn: result.Data.AccessTokenExpiresIn,
|
||||||
|
RefreshTokenExpiresIn: result.Data.RefreshTokenExpiresIn,
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
41
sql/09_tencent_account_relation.sql
Normal file
41
sql/09_tencent_account_relation.sql
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
-- 腾讯广告账户关系表
|
||||||
|
CREATE SEQUENCE IF NOT EXISTS tencent_account_relation_id_seq START WITH 1 INCREMENT BY 1;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS tencent_account_relation (
|
||||||
|
id BIGINT NOT NULL DEFAULT nextval('tencent_account_relation_id_seq'::regclass),
|
||||||
|
tenant_id BIGINT NOT NULL DEFAULT 0,
|
||||||
|
creator VARCHAR(100) DEFAULT '',
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updater VARCHAR(100) DEFAULT '',
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
deleted_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
|
||||||
|
-- 业务字段
|
||||||
|
account_id BIGINT NOT NULL,
|
||||||
|
corporation_name VARCHAR(500),
|
||||||
|
comment_data_list JSONB,
|
||||||
|
is_adx BOOLEAN DEFAULT FALSE,
|
||||||
|
is_bid BOOLEAN DEFAULT FALSE,
|
||||||
|
is_mp BOOLEAN DEFAULT FALSE,
|
||||||
|
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE tencent_account_relation IS '腾讯广告账户关系表';
|
||||||
|
COMMENT ON COLUMN tencent_account_relation.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN tencent_account_relation.tenant_id IS '租户ID';
|
||||||
|
COMMENT ON COLUMN tencent_account_relation.creator IS '创建人';
|
||||||
|
COMMENT ON COLUMN tencent_account_relation.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN tencent_account_relation.updater IS '更新人';
|
||||||
|
COMMENT ON COLUMN tencent_account_relation.updated_at IS '更新时间';
|
||||||
|
COMMENT ON COLUMN tencent_account_relation.deleted_at IS '软删除时间';
|
||||||
|
COMMENT ON COLUMN tencent_account_relation.account_id IS '账户ID';
|
||||||
|
COMMENT ON COLUMN tencent_account_relation.corporation_name IS '公司名称';
|
||||||
|
COMMENT ON COLUMN tencent_account_relation.comment_data_list IS '备注数据列表';
|
||||||
|
COMMENT ON COLUMN tencent_account_relation.is_adx IS '是否ADX';
|
||||||
|
COMMENT ON COLUMN tencent_account_relation.is_bid IS '是否BID';
|
||||||
|
COMMENT ON COLUMN tencent_account_relation.is_mp IS '是否MP';
|
||||||
|
|
||||||
|
-- 唯一索引:根据account_id判断是否存在
|
||||||
|
CREATE UNIQUE INDEX idx_tencent_account_relation_account_id ON tencent_account_relation(tenant_id, account_id);
|
||||||
|
CREATE INDEX idx_tencent_account_relation_corporation ON tencent_account_relation(tenant_id, corporation_name);
|
||||||
46
sql/10_tencent_audio.sql
Normal file
46
sql/10_tencent_audio.sql
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
-- 腾讯广告音乐素材表
|
||||||
|
CREATE SEQUENCE IF NOT EXISTS tencent_audio_id_seq START WITH 1 INCREMENT BY 1;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS tencent_audio (
|
||||||
|
id BIGINT NOT NULL DEFAULT nextval('tencent_audio_id_seq'::regclass),
|
||||||
|
tenant_id BIGINT NOT NULL DEFAULT 0,
|
||||||
|
creator VARCHAR(100) DEFAULT '',
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updater VARCHAR(100) DEFAULT '',
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
deleted_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
|
||||||
|
-- 业务字段
|
||||||
|
audio_id VARCHAR(100) NOT NULL,
|
||||||
|
cover_image_url TEXT,
|
||||||
|
audio_name VARCHAR(500),
|
||||||
|
author VARCHAR(200),
|
||||||
|
duration NUMERIC(10, 2),
|
||||||
|
expire_time BIGINT,
|
||||||
|
feel_tags JSONB,
|
||||||
|
genre_tags JSONB,
|
||||||
|
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE tencent_audio IS '腾讯广告音乐素材表';
|
||||||
|
COMMENT ON COLUMN tencent_audio.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN tencent_audio.tenant_id IS '租户ID';
|
||||||
|
COMMENT ON COLUMN tencent_audio.creator IS '创建人';
|
||||||
|
COMMENT ON COLUMN tencent_audio.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN tencent_audio.updater IS '更新人';
|
||||||
|
COMMENT ON COLUMN tencent_audio.updated_at IS '更新时间';
|
||||||
|
COMMENT ON COLUMN tencent_audio.deleted_at IS '软删除时间';
|
||||||
|
COMMENT ON COLUMN tencent_audio.audio_id IS '音乐ID';
|
||||||
|
COMMENT ON COLUMN tencent_audio.cover_image_url IS '封面图片URL';
|
||||||
|
COMMENT ON COLUMN tencent_audio.audio_name IS '音乐名称';
|
||||||
|
COMMENT ON COLUMN tencent_audio.author IS '作者';
|
||||||
|
COMMENT ON COLUMN tencent_audio.duration IS '时长(秒)';
|
||||||
|
COMMENT ON COLUMN tencent_audio.expire_time IS '过期时间戳';
|
||||||
|
COMMENT ON COLUMN tencent_audio.feel_tags IS '情感标签数组';
|
||||||
|
COMMENT ON COLUMN tencent_audio.genre_tags IS '风格标签数组';
|
||||||
|
|
||||||
|
-- 唯一索引:根据audio_id判断是否存在
|
||||||
|
CREATE UNIQUE INDEX idx_tencent_audio_audio_id ON tencent_audio(tenant_id, audio_id);
|
||||||
|
CREATE INDEX idx_tencent_audio_author ON tencent_audio(tenant_id, author);
|
||||||
|
CREATE INDEX idx_tencent_audio_expire_time ON tencent_audio(expire_time);
|
||||||
95
sql/11_tencent_image.sql
Normal file
95
sql/11_tencent_image.sql
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
-- 腾讯广告图片素材表
|
||||||
|
CREATE SEQUENCE IF NOT EXISTS tencent_image_id_seq START WITH 1 INCREMENT BY 1;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS tencent_image (
|
||||||
|
id BIGINT NOT NULL DEFAULT nextval('tencent_image_id_seq'::regclass),
|
||||||
|
tenant_id BIGINT NOT NULL DEFAULT 0,
|
||||||
|
creator VARCHAR(100) DEFAULT '',
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updater VARCHAR(100) DEFAULT '',
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
deleted_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
|
||||||
|
-- 业务字段
|
||||||
|
image_id VARCHAR(100) NOT NULL,
|
||||||
|
account_id BIGINT NOT NULL,
|
||||||
|
width INT,
|
||||||
|
height INT,
|
||||||
|
file_size BIGINT,
|
||||||
|
type VARCHAR(50),
|
||||||
|
signature VARCHAR(200),
|
||||||
|
description TEXT,
|
||||||
|
source_signature VARCHAR(200),
|
||||||
|
preview_url TEXT,
|
||||||
|
thumb_preview_url TEXT,
|
||||||
|
source_type VARCHAR(100),
|
||||||
|
image_usage VARCHAR(100),
|
||||||
|
created_time BIGINT,
|
||||||
|
last_modified_time BIGINT,
|
||||||
|
product_catalog_id BIGINT,
|
||||||
|
product_outer_id VARCHAR(200),
|
||||||
|
source_reference_id VARCHAR(200),
|
||||||
|
owner_account_id VARCHAR(100),
|
||||||
|
status VARCHAR(50),
|
||||||
|
sample_aspect_ratio VARCHAR(50),
|
||||||
|
source_material_id VARCHAR(100),
|
||||||
|
new_source_type VARCHAR(100),
|
||||||
|
first_publication_status VARCHAR(100),
|
||||||
|
quality_status VARCHAR(100),
|
||||||
|
similarity_status VARCHAR(100),
|
||||||
|
user_aigc_status VARCHAR(100),
|
||||||
|
system_aigc_status VARCHAR(100),
|
||||||
|
aigc_source VARCHAR(200),
|
||||||
|
aigc_flag VARCHAR(50),
|
||||||
|
muse_aigc_version INT,
|
||||||
|
aigc_type INT,
|
||||||
|
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE tencent_image IS '腾讯广告图片素材表';
|
||||||
|
COMMENT ON COLUMN tencent_image.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN tencent_image.tenant_id IS '租户ID';
|
||||||
|
COMMENT ON COLUMN tencent_image.creator IS '创建人';
|
||||||
|
COMMENT ON COLUMN tencent_image.created_at IS '创建时间';
|
||||||
|
COMMENT ON COLUMN tencent_image.updater IS '更新人';
|
||||||
|
COMMENT ON COLUMN tencent_image.updated_at IS '更新时间';
|
||||||
|
COMMENT ON COLUMN tencent_image.deleted_at IS '软删除时间';
|
||||||
|
COMMENT ON COLUMN tencent_image.image_id IS '图片ID';
|
||||||
|
COMMENT ON COLUMN tencent_image.account_id IS '账户ID';
|
||||||
|
COMMENT ON COLUMN tencent_image.width IS '宽度';
|
||||||
|
COMMENT ON COLUMN tencent_image.height IS '高度';
|
||||||
|
COMMENT ON COLUMN tencent_image.file_size IS '文件大小';
|
||||||
|
COMMENT ON COLUMN tencent_image.type IS '图片类型';
|
||||||
|
COMMENT ON COLUMN tencent_image.signature IS '签名';
|
||||||
|
COMMENT ON COLUMN tencent_image.description IS '描述';
|
||||||
|
COMMENT ON COLUMN tencent_image.source_signature IS '源签名';
|
||||||
|
COMMENT ON COLUMN tencent_image.preview_url IS '预览URL';
|
||||||
|
COMMENT ON COLUMN tencent_image.thumb_preview_url IS '缩略图URL';
|
||||||
|
COMMENT ON COLUMN tencent_image.source_type IS '来源类型';
|
||||||
|
COMMENT ON COLUMN tencent_image.image_usage IS '图片用途';
|
||||||
|
COMMENT ON COLUMN tencent_image.created_time IS '创建时间戳';
|
||||||
|
COMMENT ON COLUMN tencent_image.last_modified_time IS '最后修改时间戳';
|
||||||
|
COMMENT ON COLUMN tencent_image.product_catalog_id IS '产品目录ID';
|
||||||
|
COMMENT ON COLUMN tencent_image.product_outer_id IS '产品外部ID';
|
||||||
|
COMMENT ON COLUMN tencent_image.source_reference_id IS '源引用ID';
|
||||||
|
COMMENT ON COLUMN tencent_image.owner_account_id IS '所有者账户ID';
|
||||||
|
COMMENT ON COLUMN tencent_image.status IS '状态';
|
||||||
|
COMMENT ON COLUMN tencent_image.sample_aspect_ratio IS '示例宽高比';
|
||||||
|
COMMENT ON COLUMN tencent_image.source_material_id IS '源素材ID';
|
||||||
|
COMMENT ON COLUMN tencent_image.new_source_type IS '新来源类型';
|
||||||
|
COMMENT ON COLUMN tencent_image.first_publication_status IS '首次发布状态';
|
||||||
|
COMMENT ON COLUMN tencent_image.quality_status IS '质量状态';
|
||||||
|
COMMENT ON COLUMN tencent_image.similarity_status IS '相似度状态';
|
||||||
|
COMMENT ON COLUMN tencent_image.user_aigc_status IS '用户AIGC状态';
|
||||||
|
COMMENT ON COLUMN tencent_image.system_aigc_status IS '系统AIGC状态';
|
||||||
|
COMMENT ON COLUMN tencent_image.aigc_source IS 'AIGC来源';
|
||||||
|
COMMENT ON COLUMN tencent_image.aigc_flag IS 'AIGC标志';
|
||||||
|
COMMENT ON COLUMN tencent_image.muse_aigc_version IS 'Muse AIGC版本';
|
||||||
|
COMMENT ON COLUMN tencent_image.aigc_type IS 'AIGC类型';
|
||||||
|
|
||||||
|
-- 唯一索引:根据image_id和account_id判断是否存在
|
||||||
|
CREATE UNIQUE INDEX idx_tencent_image_image_account ON tencent_image(tenant_id, image_id, account_id);
|
||||||
|
CREATE INDEX idx_tencent_image_account_id ON tencent_image(account_id);
|
||||||
|
CREATE INDEX idx_tencent_image_last_modified ON tencent_image(last_modified_time);
|
||||||
|
CREATE INDEX idx_tencent_image_status ON tencent_image(status);
|
||||||
Reference in New Issue
Block a user