1433 lines
49 KiB
Go
1433 lines
49 KiB
Go
package srv_mediaresource
|
||
|
||
import (
|
||
"context"
|
||
"math/rand"
|
||
"regexp"
|
||
"strings"
|
||
"time"
|
||
"videoweb/pub/common/log"
|
||
"videoweb/pub/common/srvdef"
|
||
"videoweb/pub/common/utils"
|
||
"videoweb/pub/models/mwacc"
|
||
"videoweb/pub/models/playletchaptermod"
|
||
"videoweb/pub/models/playletmod"
|
||
"videoweb/pub/models/playlettagmod"
|
||
"videoweb/web/api/proto"
|
||
"videoweb/web/middleware/auth"
|
||
"videoweb/web/webg"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"go.mongodb.org/mongo-driver/bson"
|
||
"go.mongodb.org/mongo-driver/mongo/options"
|
||
)
|
||
|
||
const (
|
||
defaultResourceSyncType = 1
|
||
defaultResourcePageNum = 1
|
||
defaultResourcePageSize = 20
|
||
maxResourcePageSize = 100
|
||
maxResourceImportIds = 100
|
||
resourceAddStatusScanMaxPages = 20
|
||
resourceAddedIDBatchSize = 100
|
||
playletResourceImportMsg = "同步任务已提交,后台正在处理"
|
||
playletHeatMinValue = 1000
|
||
playletHeatMaxValue = 5000
|
||
playletVirtualLikeMinValue = 10
|
||
playletVirtualLikeMaxValue = 20
|
||
|
||
playletResourceMediaTypeAnime = 1 // 动漫
|
||
playletResourceMediaTypeManga = 2 // 漫画
|
||
playletResourceMediaTypeComic = 4 // 漫画短剧
|
||
playletResourceMediaTypeShort = 5 // 短剧
|
||
playletResourceMediaTypeNormal = 6 // 普通短剧
|
||
)
|
||
|
||
// ListPlayletResource 分页查询短剧媒资列表
|
||
func ListPlayletResource(ctx context.Context, req *proto.PlayletResourceListReq) (*proto.PlayletResourceListResp, *srvdef.SrvErr) {
|
||
if req == nil {
|
||
return nil, srvdef.New(srvdef.ErrParamError, "", "")
|
||
}
|
||
syncType := defaultResourceSyncType
|
||
if req.SyncType != nil {
|
||
syncType = *req.SyncType
|
||
}
|
||
pageNumber := req.PageNumber
|
||
if pageNumber <= 0 {
|
||
pageNumber = defaultResourcePageNum
|
||
}
|
||
pageSize := req.PageSize
|
||
if pageSize <= 0 {
|
||
pageSize = defaultResourcePageSize
|
||
}
|
||
if pageSize > maxResourcePageSize {
|
||
pageSize = maxResourcePageSize
|
||
}
|
||
mediaType, srvErr := resolvePlayletResourceMediaType(req.MediaType)
|
||
if srvErr != nil {
|
||
return nil, srvErr
|
||
}
|
||
// MediaCenterService 当前只保存配置值,按请求创建成本很低,也避免全局对象在配置热更新场景下读到旧配置
|
||
ms := newPlayletMediaCenterFromConfig()
|
||
if req.IsAdd != nil {
|
||
return listPlayletResourceByAddStatus(ctx, ms, req, mediaType, syncType, pageNumber, pageSize)
|
||
}
|
||
data, err := ms.mediaList(&playletMediaListReq{
|
||
MediaType: mediaType,
|
||
Title: req.Title,
|
||
Tag: req.Tag,
|
||
SyncType: syncType,
|
||
Page: uint(pageNumber),
|
||
Size: uint(pageSize),
|
||
})
|
||
if err != nil {
|
||
return nil, srvdef.New(srvdef.Failure, err.Error(), "")
|
||
}
|
||
if err := fillPlayletResourceAddStatus(ctx, data.Data); err != nil {
|
||
return nil, err
|
||
}
|
||
return &proto.PlayletResourceListResp{
|
||
List: data.Data,
|
||
Total: data.Pagination.Count,
|
||
}, nil
|
||
}
|
||
|
||
// ListPlayletJHResource 分页查询嘉华短剧媒资列表
|
||
func ListPlayletJHResource(ctx context.Context, req *proto.PlayletResourceJHListReq) (*proto.PlayletResourceJHListResp, *srvdef.SrvErr) {
|
||
if req == nil {
|
||
return nil, srvdef.New(srvdef.ErrParamError, "", "")
|
||
}
|
||
syncType := defaultResourceSyncType
|
||
if req.SyncType != nil {
|
||
syncType = *req.SyncType
|
||
}
|
||
pageNumber := req.PageNumber
|
||
if pageNumber <= 0 {
|
||
pageNumber = defaultResourcePageNum
|
||
}
|
||
pageSize := req.PageSize
|
||
if pageSize <= 0 {
|
||
pageSize = defaultResourcePageSize
|
||
}
|
||
ms := newPlayletJHMediaCenterFromConfig()
|
||
if req.IsAdd != nil {
|
||
return listPlayletJHResourceByAddStatus(ctx, ms, req, syncType, pageNumber, pageSize)
|
||
}
|
||
data, err := ms.mediaJHList(&playletMediaListReq{
|
||
MediaType: playletResourceMediaTypeShort,
|
||
Title: req.Title,
|
||
Tag: req.Tag,
|
||
SyncType: syncType,
|
||
Page: uint(pageNumber),
|
||
Size: uint(pageSize),
|
||
})
|
||
if err != nil {
|
||
return nil, srvdef.New(srvdef.Failure, err.Error(), "")
|
||
}
|
||
if err := fillPlayletJHResourceAddStatus(ctx, data.Data); err != nil {
|
||
return nil, err
|
||
}
|
||
return &proto.PlayletResourceJHListResp{
|
||
List: data.Data,
|
||
Total: data.Pagination.Count,
|
||
}, nil
|
||
}
|
||
|
||
// ListPlayletResourceChapters 查询短剧媒资剧集信息(复用入库时的 mediaSync)
|
||
func ListPlayletResourceChapters(ctx context.Context, req *proto.PlayletResourceChapterListReq) (*proto.PlayletResourceChapterListResp, *srvdef.SrvErr) {
|
||
if req == nil || req.Id == 0 {
|
||
return nil, srvdef.New(srvdef.ErrParamError, "媒资ID不能为空", "")
|
||
}
|
||
ms := newPlayletMediaCenterFromConfig()
|
||
result, err := ms.mediaSync(&playletMediaSyncReq{
|
||
Id: req.Id,
|
||
SyncTime: 0,
|
||
})
|
||
if err != nil {
|
||
return nil, srvdef.New(srvdef.Failure, err.Error(), "")
|
||
}
|
||
if result.IsDelete || result.Media == nil {
|
||
return nil, srvdef.New(srvdef.ErrNoData, "媒资不存在或已删除", "")
|
||
}
|
||
list := make([]*proto.PlayletResourceChapterItem, 0, len(result.MediaContentList))
|
||
for _, item := range result.MediaContentList {
|
||
videoUrl := strings.TrimSpace(item.M3u8Url)
|
||
if videoUrl == "" {
|
||
videoUrl = strings.TrimSpace(item.Urls)
|
||
}
|
||
list = append(list, &proto.PlayletResourceChapterItem{
|
||
Id: item.ID,
|
||
EpisodeNumber: item.EpisodeNumber,
|
||
Title: strings.TrimSpace(item.Title),
|
||
Cover: item.Cover,
|
||
VideoUrl: videoUrl,
|
||
Height: item.Height,
|
||
Width: item.Width,
|
||
MediaSize: item.MediaSize,
|
||
PlayTime: item.PlayTime,
|
||
SaleCoin: item.SaleCoin,
|
||
HashId: item.HashId,
|
||
CreateTime: item.CreateTime,
|
||
UpdateTime: item.UpdateTime,
|
||
})
|
||
}
|
||
return &proto.PlayletResourceChapterListResp{
|
||
MediaId: req.Id,
|
||
Title: result.Media.Title,
|
||
List: list,
|
||
Total: len(list),
|
||
}, nil
|
||
}
|
||
|
||
// ListPlayletJHResourceChapters 查询嘉华短剧媒资剧集信息
|
||
func ListPlayletJHResourceChapters(ctx context.Context, req *proto.PlayletResourceChapterListReq) (*proto.PlayletResourceChapterListResp, *srvdef.SrvErr) {
|
||
if req == nil || req.Id == 0 {
|
||
return nil, srvdef.New(srvdef.ErrParamError, "媒资ID不能为空", "")
|
||
}
|
||
ms := newPlayletJHMediaCenterFromConfig()
|
||
result, err := ms.mediaSync(&playletMediaSyncReq{
|
||
Id: req.Id,
|
||
SyncTime: 0,
|
||
})
|
||
if err != nil {
|
||
return nil, srvdef.New(srvdef.Failure, err.Error(), "")
|
||
}
|
||
if result.IsDelete || result.Media == nil {
|
||
return nil, srvdef.New(srvdef.ErrNoData, "媒资不存在或已删除", "")
|
||
}
|
||
list := make([]*proto.PlayletResourceChapterItem, 0, len(result.MediaContentList))
|
||
for _, item := range result.MediaContentList {
|
||
videoUrl := strings.TrimSpace(item.M3u8Url)
|
||
if videoUrl == "" {
|
||
videoUrl = strings.TrimSpace(item.Urls)
|
||
}
|
||
list = append(list, &proto.PlayletResourceChapterItem{
|
||
Id: item.ID,
|
||
EpisodeNumber: item.EpisodeNumber,
|
||
Title: strings.TrimSpace(item.Title),
|
||
Cover: item.Cover,
|
||
VideoUrl: videoUrl,
|
||
Height: item.Height,
|
||
Width: item.Width,
|
||
MediaSize: item.MediaSize,
|
||
PlayTime: item.PlayTime,
|
||
SaleCoin: item.SaleCoin,
|
||
HashId: item.HashId,
|
||
CreateTime: item.CreateTime,
|
||
UpdateTime: item.UpdateTime,
|
||
})
|
||
}
|
||
return &proto.PlayletResourceChapterListResp{
|
||
MediaId: req.Id,
|
||
Title: result.Media.Title,
|
||
List: list,
|
||
Total: len(list),
|
||
}, nil
|
||
}
|
||
|
||
// resolvePlayletResourceMediaType 校验后台显式传入的媒资类型。
|
||
func resolvePlayletResourceMediaType(mediaType *int) (int, *srvdef.SrvErr) {
|
||
if mediaType == nil {
|
||
return 0, srvdef.New(srvdef.ErrParamError, "媒体类型不能为空", "媒体类型不能为空")
|
||
}
|
||
if !validPlayletResourceMediaType(*mediaType) {
|
||
return 0, srvdef.New(srvdef.ErrParamError, "媒体类型无效,仅支持1-动漫、2-漫画、4-漫画短剧、5-短剧、6-普通短剧", "媒体类型无效,仅支持1-动漫、2-漫画、4-漫画短剧、5-短剧、6-普通短剧")
|
||
}
|
||
return *mediaType, nil
|
||
}
|
||
|
||
// validPlayletResourceMediaType 媒资列表当前允许的媒体类型。
|
||
func validPlayletResourceMediaType(mediaType int) bool {
|
||
switch mediaType {
|
||
case playletResourceMediaTypeAnime, playletResourceMediaTypeManga, playletResourceMediaTypeComic, playletResourceMediaTypeShort, playletResourceMediaTypeNormal:
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
|
||
// validPlayletImportMediaType 允许入库的媒资类型。
|
||
func validPlayletImportMediaType(mediaType int) bool {
|
||
switch mediaType {
|
||
case playletResourceMediaTypeAnime,
|
||
playletResourceMediaTypeManga, playletResourceMediaTypeComic, playletResourceMediaTypeShort, playletResourceMediaTypeNormal:
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
|
||
// contentTypeFromMediaType 媒资中心 media_type 映射为本项目 contentType。
|
||
// 1-动漫 → 3;2-漫画 → 4;4-漫画短剧 → 2;5-短剧 → 0;6-普通短剧 → 1。
|
||
func contentTypeFromMediaType(mediaType int) playletmod.ContentType {
|
||
switch mediaType {
|
||
case playletResourceMediaTypeAnime:
|
||
return playletmod.ContentTypeAnime
|
||
case playletResourceMediaTypeManga:
|
||
return playletmod.ContentTypeManga
|
||
case playletResourceMediaTypeComic:
|
||
return playletmod.ContentTypeComic
|
||
case playletResourceMediaTypeNormal:
|
||
return playletmod.ContentTypeAlbum
|
||
default:
|
||
return playletmod.ContentTypeTheater
|
||
}
|
||
}
|
||
|
||
// fillPlayletResourceAddStatus 回填媒资列表是否已加入后台短剧库
|
||
func fillPlayletResourceAddStatus(ctx context.Context, list []*proto.PlayletResourceListItem) *srvdef.SrvErr {
|
||
mediaIds := make([]uint, 0, len(list))
|
||
seen := make(map[uint]struct{}, len(list))
|
||
for _, item := range list {
|
||
// 只收集当前页有效媒资ID,空数据和无效ID不参与本地查询。
|
||
if item == nil || item.ID == 0 {
|
||
continue
|
||
}
|
||
// 当前页可能出现重复媒资ID,去重后可以减少 Mongo $in 查询条件。
|
||
if _, ok := seen[item.ID]; ok {
|
||
continue
|
||
}
|
||
seen[item.ID] = struct{}{}
|
||
mediaIds = append(mediaIds, item.ID)
|
||
}
|
||
if len(mediaIds) == 0 {
|
||
return nil
|
||
}
|
||
// 按当前页媒资ID批量查询本地短剧库,避免逐条查询影响列表性能。
|
||
filter := applyPlayletMediaSourceFilter(bson.M{
|
||
playletmod.FN.Playlet.MediaCenterId: bson.M{"$in": mediaIds},
|
||
}, playletmod.MediaSourceHG)
|
||
playletList, err := playletmod.FindManyPlaylet(ctx, filter)
|
||
if err != nil {
|
||
log.Error("查询短剧媒资加入状态失败", log.Any("mediaIds", mediaIds), log.Any("err", err))
|
||
return err
|
||
}
|
||
addedMap := make(map[uint]bool, len(playletList))
|
||
for _, item := range playletList {
|
||
// mediaCenterId 是本地短剧表保存的媒资中心ID,用它判断当前媒资是否已入库。
|
||
if item == nil || item.MediaCenterId == 0 {
|
||
continue
|
||
}
|
||
addedMap[item.MediaCenterId] = true
|
||
}
|
||
for _, item := range list {
|
||
if item == nil {
|
||
continue
|
||
}
|
||
// map 没命中时 bool 零值为 false,表示未加入后台短剧库。
|
||
item.IsAdd = addedMap[item.ID]
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// fillPlayletJHResourceAddStatus 回填嘉华媒资列表是否已入库
|
||
func fillPlayletJHResourceAddStatus(ctx context.Context, list []*proto.PlayletResourceJHListItem) *srvdef.SrvErr {
|
||
mediaIds := make([]uint, 0, len(list))
|
||
seen := make(map[uint]struct{}, len(list))
|
||
for _, item := range list {
|
||
if item == nil || item.ID == 0 {
|
||
continue
|
||
}
|
||
if _, ok := seen[item.ID]; ok {
|
||
continue
|
||
}
|
||
seen[item.ID] = struct{}{}
|
||
mediaIds = append(mediaIds, item.ID)
|
||
}
|
||
if len(mediaIds) == 0 {
|
||
return nil
|
||
}
|
||
importedMap, err := findImportedPlayletResourceMap(ctx, mediaIds, playletmod.MediaSourceDefault)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
for _, item := range list {
|
||
if item == nil {
|
||
continue
|
||
}
|
||
item.IsAdd = importedMap[item.ID]
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// listPlayletJHResourceByAddStatus 按是否已入库过滤嘉华媒资列表
|
||
func listPlayletJHResourceByAddStatus(
|
||
ctx context.Context,
|
||
ms *playletMediaCenterService,
|
||
req *proto.PlayletResourceJHListReq,
|
||
syncType int,
|
||
pageNumber int,
|
||
pageSize int,
|
||
) (*proto.PlayletResourceJHListResp, *srvdef.SrvErr) {
|
||
if *req.IsAdd && !playletJHResourceRequiresUpstreamAddStatusScan(req, syncType) {
|
||
return listAddedPlayletJHResourceByLocalFilter(ctx, ms, req, syncType, pageNumber, pageSize)
|
||
}
|
||
return scanPlayletJHResourceByAddStatus(ctx, ms, req, syncType, pageNumber, pageSize)
|
||
}
|
||
|
||
// playletJHResourceRequiresUpstreamAddStatusScan 判断嘉华列表是否必须扫描上游才能保证筛选结果准确。
|
||
func playletJHResourceRequiresUpstreamAddStatusScan(req *proto.PlayletResourceJHListReq, syncType int) bool {
|
||
if syncType != defaultResourceSyncType {
|
||
return true
|
||
}
|
||
return req != nil && req.Tag != nil && strings.TrimSpace(*req.Tag) != ""
|
||
}
|
||
|
||
// listAddedPlayletJHResourceByLocalFilter 查询已入库嘉华媒资,优先走本地playlet表分页
|
||
func listAddedPlayletJHResourceByLocalFilter(
|
||
ctx context.Context,
|
||
ms *playletMediaCenterService,
|
||
req *proto.PlayletResourceJHListReq,
|
||
syncType int,
|
||
pageNumber int,
|
||
pageSize int,
|
||
) (*proto.PlayletResourceJHListResp, *srvdef.SrvErr) {
|
||
filter := buildAddedPlayletJHResourceFilter(req)
|
||
totalCount, err := playletmod.CountPlaylet(ctx, filter)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
resp := &proto.PlayletResourceJHListResp{
|
||
List: make([]*proto.PlayletResourceJHListItem, 0, pageSize),
|
||
Total: uint(totalCount),
|
||
}
|
||
if totalCount == 0 {
|
||
return resp, nil
|
||
}
|
||
start := int64((pageNumber - 1) * pageSize)
|
||
needCount := int64(pageNumber * pageSize)
|
||
matchedCount := int64(0)
|
||
for batchStart := int64(0); batchStart < totalCount && matchedCount < needCount; batchStart += resourceAddedIDBatchSize {
|
||
list, err := queryAddedPlayletResourceIds(ctx, filter, batchStart)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if len(list) == 0 {
|
||
break
|
||
}
|
||
items, err := queryAddedPlayletJHResourceItems(ms, list, syncType)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
for _, item := range items {
|
||
if item == nil {
|
||
continue
|
||
}
|
||
if matchedCount >= start && len(resp.List) < pageSize {
|
||
resp.List = append(resp.List, item)
|
||
}
|
||
matchedCount++
|
||
if matchedCount >= needCount {
|
||
break
|
||
}
|
||
}
|
||
}
|
||
return resp, nil
|
||
}
|
||
|
||
// buildAddedPlayletJHResourceFilter 构建本地已入库嘉华媒资查询条件。
|
||
func buildAddedPlayletJHResourceFilter(req *proto.PlayletResourceJHListReq) bson.M {
|
||
filter := applyPlayletMediaSourceFilter(bson.M{
|
||
playletmod.FN.Playlet.MediaCenterId: bson.M{"$gt": 0},
|
||
playletmod.FN.Playlet.ContentType: contentTypeFromMediaType(playletResourceMediaTypeShort),
|
||
}, playletmod.MediaSourceDefault)
|
||
if req != nil && req.Title != nil {
|
||
if title := strings.TrimSpace(*req.Title); title != "" {
|
||
filter[playletmod.FN.Playlet.Title] = bson.M{"$regex": regexp.QuoteMeta(title)}
|
||
}
|
||
}
|
||
return filter
|
||
}
|
||
|
||
// queryAddedPlayletJHResourceItems 按本地短剧的媒资中心ID回查嘉华媒资数据。
|
||
func queryAddedPlayletJHResourceItems(ms *playletMediaCenterService, list playletmod.PlayletList, syncType int) ([]*proto.PlayletResourceJHListItem, *srvdef.SrvErr) {
|
||
result := make([]*proto.PlayletResourceJHListItem, 0, len(list))
|
||
for _, item := range list {
|
||
if item == nil || item.MediaCenterId == 0 {
|
||
continue
|
||
}
|
||
media, err := queryPlayletJHResourceByMediaCenterId(ms, item.MediaCenterId, syncType)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if media == nil {
|
||
continue
|
||
}
|
||
media.IsAdd = true
|
||
result = append(result, media)
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
// queryPlayletJHResourceByMediaCenterId 按媒资中心ID查询单条嘉华媒资列表数据。
|
||
func queryPlayletJHResourceByMediaCenterId(ms *playletMediaCenterService, mediaId uint, syncType int) (*proto.PlayletResourceJHListItem, *srvdef.SrvErr) {
|
||
data, err := ms.mediaJHList(&playletMediaListReq{
|
||
MediaType: playletResourceMediaTypeShort,
|
||
Id: &mediaId,
|
||
SyncType: syncType,
|
||
Page: 1,
|
||
Size: 1,
|
||
})
|
||
if err != nil {
|
||
return nil, srvdef.New(srvdef.Failure, err.Error(), "")
|
||
}
|
||
for _, item := range data.Data {
|
||
if item != nil && item.ID == mediaId {
|
||
return item, nil
|
||
}
|
||
}
|
||
if len(data.Data) > 0 {
|
||
return data.Data[0], nil
|
||
}
|
||
return nil, nil
|
||
}
|
||
|
||
// scanPlayletJHResourceByAddStatus 扫描嘉华媒资分页并按入库状态精确过滤。
|
||
func scanPlayletJHResourceByAddStatus(
|
||
ctx context.Context,
|
||
ms *playletMediaCenterService,
|
||
req *proto.PlayletResourceJHListReq,
|
||
syncType int,
|
||
pageNumber int,
|
||
pageSize int,
|
||
) (*proto.PlayletResourceJHListResp, *srvdef.SrvErr) {
|
||
offset := (pageNumber - 1) * pageSize
|
||
list := make([]*proto.PlayletResourceJHListItem, 0, pageSize)
|
||
matchedCount := 0
|
||
scanPage := uint(1)
|
||
scanSize := uint(maxResourcePageSize)
|
||
for {
|
||
data, err := ms.mediaJHList(&playletMediaListReq{
|
||
MediaType: playletResourceMediaTypeShort,
|
||
Title: req.Title,
|
||
Tag: req.Tag,
|
||
SyncType: syncType,
|
||
Page: scanPage,
|
||
Size: scanSize,
|
||
})
|
||
if err != nil {
|
||
return nil, srvdef.New(srvdef.Failure, err.Error(), "")
|
||
}
|
||
if err := fillPlayletJHResourceAddStatus(ctx, data.Data); err != nil {
|
||
return nil, err
|
||
}
|
||
for _, item := range data.Data {
|
||
if item == nil || item.IsAdd != *req.IsAdd {
|
||
continue
|
||
}
|
||
if matchedCount >= offset && len(list) < pageSize {
|
||
list = append(list, item)
|
||
}
|
||
matchedCount++
|
||
}
|
||
if data.Pagination.IsLastPage || len(data.Data) == 0 {
|
||
break
|
||
}
|
||
if data.Pagination.Count > 0 && scanPage*scanSize >= data.Pagination.Count {
|
||
break
|
||
}
|
||
scanPage++
|
||
}
|
||
return &proto.PlayletResourceJHListResp{
|
||
List: list,
|
||
Total: uint(matchedCount),
|
||
}, nil
|
||
}
|
||
|
||
// listPlayletResourceByAddStatus 按是否已加入后台短剧库过滤媒资列表,并按过滤后的结果分页
|
||
func listPlayletResourceByAddStatus(
|
||
ctx context.Context,
|
||
ms *playletMediaCenterService,
|
||
req *proto.PlayletResourceListReq,
|
||
mediaType int,
|
||
syncType int,
|
||
pageNumber int,
|
||
pageSize int,
|
||
) (*proto.PlayletResourceListResp, *srvdef.SrvErr) {
|
||
if *req.IsAdd && !playletResourceRequiresUpstreamAddStatusScan(req, syncType) {
|
||
return listAddedPlayletResourceByLocalFilter(ctx, ms, req, mediaType, syncType, pageNumber, pageSize)
|
||
}
|
||
if !*req.IsAdd && !playletResourceRequiresUpstreamAddStatusScan(req, syncType) {
|
||
return listNotAddedPlayletResourceByLocalEstimate(ctx, ms, req, mediaType, syncType, pageNumber, pageSize)
|
||
}
|
||
return scanPlayletResourceByAddStatus(ctx, ms, req, mediaType, syncType, pageNumber, pageSize)
|
||
}
|
||
|
||
// listAddedPlayletResourceByLocalFilter 查询已加入后台短剧库的媒资,优先走本地playlet表分页
|
||
func listAddedPlayletResourceByLocalFilter(
|
||
ctx context.Context,
|
||
ms *playletMediaCenterService,
|
||
req *proto.PlayletResourceListReq,
|
||
mediaType int,
|
||
syncType int,
|
||
pageNumber int,
|
||
pageSize int,
|
||
) (*proto.PlayletResourceListResp, *srvdef.SrvErr) {
|
||
filter := buildAddedPlayletResourceFilter(req, mediaType)
|
||
totalCount, err := playletmod.CountPlaylet(ctx, filter)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
resp := &proto.PlayletResourceListResp{
|
||
List: make([]*proto.PlayletResourceListItem, 0, pageSize),
|
||
Total: uint(totalCount),
|
||
}
|
||
if totalCount == 0 {
|
||
return resp, nil
|
||
}
|
||
start := int64((pageNumber - 1) * pageSize)
|
||
needCount := int64(pageNumber * pageSize)
|
||
matchedCount := int64(0)
|
||
for batchStart := int64(0); batchStart < totalCount && matchedCount < needCount; batchStart += resourceAddedIDBatchSize {
|
||
list, err := queryAddedPlayletResourceIds(ctx, filter, batchStart)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if len(list) == 0 {
|
||
break
|
||
}
|
||
items, err := queryAddedPlayletResourceItems(ms, list, mediaType, syncType)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
for _, item := range items {
|
||
if item == nil {
|
||
continue
|
||
}
|
||
if matchedCount >= start && len(resp.List) < pageSize {
|
||
resp.List = append(resp.List, item)
|
||
}
|
||
matchedCount++
|
||
if matchedCount >= needCount {
|
||
break
|
||
}
|
||
}
|
||
}
|
||
return resp, nil
|
||
}
|
||
|
||
// listNotAddedPlayletResourceByLocalEstimate 查询未加入媒资,total按上游总数减本地已加入数估算。
|
||
func listNotAddedPlayletResourceByLocalEstimate(
|
||
ctx context.Context,
|
||
ms *playletMediaCenterService,
|
||
req *proto.PlayletResourceListReq,
|
||
mediaType int,
|
||
syncType int,
|
||
pageNumber int,
|
||
pageSize int,
|
||
) (*proto.PlayletResourceListResp, *srvdef.SrvErr) {
|
||
scanSize := uint(maxResourcePageSize)
|
||
firstPage, err := ms.mediaList(&playletMediaListReq{
|
||
MediaType: mediaType,
|
||
Title: req.Title,
|
||
SyncType: syncType,
|
||
Page: 1,
|
||
Size: scanSize,
|
||
})
|
||
if err != nil {
|
||
return nil, srvdef.New(srvdef.Failure, err.Error(), "")
|
||
}
|
||
addedTotal, countErr := playletmod.CountPlaylet(ctx, buildAddedPlayletResourceFilter(req, mediaType))
|
||
if countErr != nil {
|
||
return nil, countErr
|
||
}
|
||
total := int64(firstPage.Pagination.Count) - addedTotal
|
||
if total < 0 {
|
||
total = 0
|
||
}
|
||
resp := &proto.PlayletResourceListResp{
|
||
List: make([]*proto.PlayletResourceListItem, 0, pageSize),
|
||
Total: uint(total),
|
||
}
|
||
start := (pageNumber - 1) * pageSize
|
||
needCount := start + pageSize
|
||
matchedCount := 0
|
||
lastPage := calcPlayletResourceLastPage(firstPage.Pagination.Count, scanSize)
|
||
scanLimit := lastPage
|
||
if scanLimit > resourceAddStatusScanMaxPages {
|
||
scanLimit = resourceAddStatusScanMaxPages
|
||
}
|
||
for scanPage := uint(1); scanPage <= scanLimit; scanPage++ {
|
||
currentPage := firstPage
|
||
if scanPage > 1 {
|
||
data, err := ms.mediaList(&playletMediaListReq{
|
||
MediaType: mediaType,
|
||
Title: req.Title,
|
||
SyncType: syncType,
|
||
Page: scanPage,
|
||
Size: scanSize,
|
||
})
|
||
if err != nil {
|
||
log.Warn("查询未加入短剧媒资后续分页失败,停止继续扫描", log.Any("页码", scanPage), log.E(err))
|
||
break
|
||
}
|
||
currentPage = data
|
||
}
|
||
if len(currentPage.Data) == 0 {
|
||
break
|
||
}
|
||
if err := fillPlayletResourceByAddStatusPage(ctx, currentPage.Data, false, start, pageSize, resp, &matchedCount); err != nil {
|
||
return nil, err
|
||
}
|
||
if matchedCount >= needCount || currentPage.Pagination.IsLastPage {
|
||
break
|
||
}
|
||
}
|
||
if scanLimit < lastPage && matchedCount < needCount {
|
||
log.Warn("短剧媒资加入状态筛选达到最大扫描页数", log.Any("最大扫描页数", resourceAddStatusScanMaxPages), log.Any("上游总页数", lastPage), log.Any("命中数量", matchedCount), log.Any("需要数量", needCount))
|
||
}
|
||
return resp, nil
|
||
}
|
||
|
||
// scanPlayletResourceByAddStatus 扫描媒资中心分页并按加入状态精确过滤,适用于本地无法独立判断的筛选。
|
||
func scanPlayletResourceByAddStatus(
|
||
ctx context.Context,
|
||
ms *playletMediaCenterService,
|
||
req *proto.PlayletResourceListReq,
|
||
mediaType int,
|
||
syncType int,
|
||
pageNumber int,
|
||
pageSize int,
|
||
) (*proto.PlayletResourceListResp, *srvdef.SrvErr) {
|
||
offset := (pageNumber - 1) * pageSize
|
||
list := make([]*proto.PlayletResourceListItem, 0, pageSize)
|
||
matchedCount := 0
|
||
scanPage := uint(1)
|
||
scanSize := uint(maxResourcePageSize)
|
||
for {
|
||
data, err := ms.mediaList(&playletMediaListReq{
|
||
MediaType: mediaType,
|
||
Title: req.Title,
|
||
Tag: req.Tag,
|
||
SyncType: syncType,
|
||
Page: scanPage,
|
||
Size: scanSize,
|
||
})
|
||
if err != nil {
|
||
return nil, srvdef.New(srvdef.Failure, err.Error(), "")
|
||
}
|
||
if err := fillPlayletResourceAddStatus(ctx, data.Data); err != nil {
|
||
return nil, err
|
||
}
|
||
for _, item := range data.Data {
|
||
if item == nil || item.IsAdd != *req.IsAdd {
|
||
continue
|
||
}
|
||
if matchedCount >= offset && len(list) < pageSize {
|
||
list = append(list, item)
|
||
}
|
||
matchedCount++
|
||
}
|
||
if data.Pagination.IsLastPage || len(data.Data) == 0 {
|
||
break
|
||
}
|
||
if data.Pagination.Count > 0 && scanPage*scanSize >= data.Pagination.Count {
|
||
break
|
||
}
|
||
scanPage++
|
||
}
|
||
return &proto.PlayletResourceListResp{
|
||
List: list,
|
||
Total: uint(matchedCount),
|
||
}, nil
|
||
}
|
||
|
||
// calcPlayletResourceLastPage 计算上游媒资列表最后一页页码
|
||
func calcPlayletResourceLastPage(total uint, pageSize uint) uint {
|
||
if total == 0 || pageSize == 0 {
|
||
return 0
|
||
}
|
||
return (total + pageSize - 1) / pageSize
|
||
}
|
||
|
||
// playletResourceRequiresUpstreamAddStatusScan 判断是否必须扫描上游才能保证筛选结果准确。
|
||
func playletResourceRequiresUpstreamAddStatusScan(req *proto.PlayletResourceListReq, syncType int) bool {
|
||
if syncType != defaultResourceSyncType {
|
||
return true
|
||
}
|
||
return req != nil && req.Tag != nil && strings.TrimSpace(*req.Tag) != ""
|
||
}
|
||
|
||
// buildAddedPlayletResourceFilter 构建本地已加入短剧媒资查询条件。
|
||
func buildAddedPlayletResourceFilter(req *proto.PlayletResourceListReq, mediaType int) bson.M {
|
||
filter := applyPlayletMediaSourceFilter(bson.M{
|
||
playletmod.FN.Playlet.MediaCenterId: bson.M{"$gt": 0},
|
||
playletmod.FN.Playlet.ContentType: contentTypeFromMediaType(mediaType),
|
||
}, playletmod.MediaSourceHG)
|
||
if req != nil && req.Title != nil {
|
||
if title := strings.TrimSpace(*req.Title); title != "" {
|
||
filter[playletmod.FN.Playlet.Title] = bson.M{"$regex": regexp.QuoteMeta(title)}
|
||
}
|
||
}
|
||
return filter
|
||
}
|
||
|
||
// queryAddedPlayletResourceIds 分批查询已加入短剧的媒资中心ID。
|
||
func queryAddedPlayletResourceIds(ctx context.Context, filter bson.M, skip int64) (playletmod.PlayletList, *srvdef.SrvErr) {
|
||
list, err := playletmod.FindManyPlaylet(ctx, filter, options.Find().
|
||
SetProjection(bson.D{{Key: playletmod.FN.Playlet.MediaCenterId, Value: 1}}).
|
||
SetSort(bson.D{
|
||
{Key: playletmod.FN.Playlet.CreatedAt, Value: -1},
|
||
{Key: playletmod.FN.Playlet.Id, Value: -1},
|
||
}).
|
||
SetSkip(skip).
|
||
SetLimit(resourceAddedIDBatchSize))
|
||
if err != nil {
|
||
log.Warn("查询已加入短剧媒资ID失败", log.Any("filter", filter), log.E(err))
|
||
return nil, err
|
||
}
|
||
return list, nil
|
||
}
|
||
|
||
// queryAddedPlayletResourceItems 按本地短剧的媒资中心ID回查媒资数据。
|
||
func queryAddedPlayletResourceItems(ms *playletMediaCenterService, list playletmod.PlayletList, mediaType int, syncType int) ([]*proto.PlayletResourceListItem, *srvdef.SrvErr) {
|
||
result := make([]*proto.PlayletResourceListItem, 0, len(list))
|
||
for _, item := range list {
|
||
if item == nil || item.MediaCenterId == 0 {
|
||
continue
|
||
}
|
||
media, err := queryPlayletResourceByMediaCenterId(ms, item.MediaCenterId, mediaType, syncType)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if media == nil {
|
||
continue
|
||
}
|
||
media.IsAdd = true
|
||
result = append(result, media)
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
// queryPlayletResourceByMediaCenterId 按媒资中心ID查询单条媒资列表数据。
|
||
func queryPlayletResourceByMediaCenterId(ms *playletMediaCenterService, mediaId uint, mediaType int, syncType int) (*proto.PlayletResourceListItem, *srvdef.SrvErr) {
|
||
data, err := ms.mediaList(&playletMediaListReq{
|
||
MediaType: mediaType,
|
||
Id: &mediaId,
|
||
SyncType: syncType,
|
||
Page: 1,
|
||
Size: 1,
|
||
})
|
||
if err != nil {
|
||
return nil, srvdef.New(srvdef.Failure, err.Error(), "")
|
||
}
|
||
for _, item := range data.Data {
|
||
if item != nil && item.ID == mediaId {
|
||
return item, nil
|
||
}
|
||
}
|
||
if len(data.Data) > 0 {
|
||
return data.Data[0], nil
|
||
}
|
||
return nil, nil
|
||
}
|
||
|
||
// fillPlayletResourceByAddStatusPage 按加入状态累计过滤,并截取请求页数据。
|
||
func fillPlayletResourceByAddStatusPage(ctx context.Context, list []*proto.PlayletResourceListItem, wantAdd bool, start int, pageSize int, resp *proto.PlayletResourceListResp, matchedCount *int) *srvdef.SrvErr {
|
||
if err := fillPlayletResourceAddStatus(ctx, list); err != nil {
|
||
return err
|
||
}
|
||
for _, item := range list {
|
||
if item == nil || item.IsAdd != wantAdd {
|
||
continue
|
||
}
|
||
if *matchedCount >= start && len(resp.List) < pageSize {
|
||
resp.List = append(resp.List, item)
|
||
}
|
||
*matchedCount = *matchedCount + 1
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// ImportPlayletResource 提交短剧媒资入库任务
|
||
func ImportPlayletResource(ctx context.Context, req *proto.PlayletResourceUseReq) (string, *srvdef.SrvErr) {
|
||
// 先做整批请求校验,避免后续产生无效的外部同步请求和数据库写入。
|
||
ids, err := validatePlayletResourceImportReq(req)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
operatorId, operatorName := resolveImportOperator(ctx)
|
||
if err := pushPlayletResourceImportJob(ctx, ids, operatorId, operatorName); err != nil {
|
||
return "", err
|
||
}
|
||
return playletResourceImportMsg, nil
|
||
}
|
||
|
||
// ImportPlayletJHResource 提交嘉华短剧媒资入库任务
|
||
func ImportPlayletJHResource(ctx context.Context, req *proto.PlayletResourceUseReq) (string, *srvdef.SrvErr) {
|
||
ids, err := validatePlayletResourceImportReq(req)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
operatorId, operatorName := resolveImportOperator(ctx)
|
||
if err := pushPlayletResourceJHImportJob(ctx, ids, operatorId, operatorName); err != nil {
|
||
return "", err
|
||
}
|
||
return playletResourceImportMsg, nil
|
||
}
|
||
|
||
// importPlayletResourceBatch 批量处理短剧媒资入库任务
|
||
func importPlayletResourceBatch(ctx context.Context, ids []uint, operatorId int64, operatorName string) *proto.PlayletResourceUseResp {
|
||
// 先批量查询已导入媒资,避免对已存在数据重复调用媒资中心同步接口。
|
||
resp := &proto.PlayletResourceUseResp{
|
||
Success: make([]*proto.PlayletResourceUseSuccess, 0, len(ids)),
|
||
Failed: make([]*proto.PlayletResourceUseFailed, 0),
|
||
}
|
||
importedMap, err := findImportedPlayletResourceMap(ctx, ids, playletmod.MediaSourceHG)
|
||
if err != nil {
|
||
for _, id := range ids {
|
||
resp.Failed = append(resp.Failed, &proto.PlayletResourceUseFailed{
|
||
MediaId: id,
|
||
Reason: "查询导入状态失败",
|
||
})
|
||
}
|
||
return resp
|
||
}
|
||
ms := newPlayletMediaCenterFromConfig()
|
||
for _, id := range ids {
|
||
// 已入库媒资直接跳过,不再请求媒资中心。
|
||
if importedMap[id] {
|
||
resp.Failed = append(resp.Failed, &proto.PlayletResourceUseFailed{
|
||
MediaId: id,
|
||
Reason: "该媒资已导入",
|
||
})
|
||
continue
|
||
}
|
||
// 单条媒资独立导入,失败不影响本批次剩余媒资;单条内部还有锁和最终去重校验。
|
||
success, reason := importSinglePlayletResource(ctx, ms, id, operatorId, operatorName)
|
||
if reason != "" {
|
||
resp.Failed = append(resp.Failed, &proto.PlayletResourceUseFailed{
|
||
MediaId: id,
|
||
Reason: reason,
|
||
})
|
||
continue
|
||
}
|
||
resp.Success = append(resp.Success, success)
|
||
}
|
||
return resp
|
||
}
|
||
|
||
// importPlayletJHResourceBatch 批量处理嘉华短剧媒资入库任务
|
||
func importPlayletJHResourceBatch(ctx context.Context, ids []uint, operatorId int64, operatorName string) *proto.PlayletResourceUseResp {
|
||
resp := &proto.PlayletResourceUseResp{
|
||
Success: make([]*proto.PlayletResourceUseSuccess, 0, len(ids)),
|
||
Failed: make([]*proto.PlayletResourceUseFailed, 0),
|
||
}
|
||
importedMap, err := findImportedPlayletResourceMap(ctx, ids, playletmod.MediaSourceDefault)
|
||
if err != nil {
|
||
for _, id := range ids {
|
||
resp.Failed = append(resp.Failed, &proto.PlayletResourceUseFailed{
|
||
MediaId: id,
|
||
Reason: "查询导入状态失败",
|
||
})
|
||
}
|
||
return resp
|
||
}
|
||
ms := newPlayletJHMediaCenterFromConfig()
|
||
for _, id := range ids {
|
||
if importedMap[id] {
|
||
resp.Failed = append(resp.Failed, &proto.PlayletResourceUseFailed{
|
||
MediaId: id,
|
||
Reason: "该媒资已导入",
|
||
})
|
||
continue
|
||
}
|
||
success, reason := importSinglePlayletJHResource(ctx, ms, id, operatorId, operatorName)
|
||
if reason != "" {
|
||
resp.Failed = append(resp.Failed, &proto.PlayletResourceUseFailed{
|
||
MediaId: id,
|
||
Reason: reason,
|
||
})
|
||
continue
|
||
}
|
||
resp.Success = append(resp.Success, success)
|
||
}
|
||
return resp
|
||
}
|
||
|
||
// findImportedPlayletResourceMap 批量查询指定来源已经导入的媒资ID
|
||
func findImportedPlayletResourceMap(ctx context.Context, ids []uint, mediaSource string) (map[uint]bool, *srvdef.SrvErr) {
|
||
result := make(map[uint]bool, len(ids))
|
||
if len(ids) == 0 {
|
||
return result, nil
|
||
}
|
||
filter := applyPlayletMediaSourceFilter(bson.M{
|
||
playletmod.FN.Playlet.MediaCenterId: bson.M{"$in": ids},
|
||
}, mediaSource)
|
||
playletList, err := playletmod.FindManyPlaylet(ctx, filter)
|
||
if err != nil {
|
||
log.Error("批量查询短剧媒资导入状态失败", log.Any("mediaIds", ids), log.Any("err", err))
|
||
return nil, err
|
||
}
|
||
for _, item := range playletList {
|
||
// 只记录有效的媒资中心ID,供批量任务跳过已入库数据。
|
||
if item == nil || item.MediaCenterId == 0 {
|
||
continue
|
||
}
|
||
result[item.MediaCenterId] = true
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
func newPlayletMediaCenterFromConfig() *playletMediaCenterService {
|
||
return newPlayletMediaCenterService(playletMediaCenterConfig{
|
||
Url: webg.Conf.MediaResourceDBCfg.BaseUrl,
|
||
ProId: webg.Conf.MediaResourceDBCfg.Merchat,
|
||
Secret: webg.Conf.MediaResourceDBCfg.Secret,
|
||
})
|
||
}
|
||
|
||
func newPlayletJHMediaCenterFromConfig() *playletMediaCenterService {
|
||
return newPlayletMediaCenterService(playletMediaCenterConfig{
|
||
Url: webg.Conf.PlayletMediaResourceDBCfg.BaseUrl,
|
||
ProId: webg.Conf.PlayletMediaResourceDBCfg.Merchat,
|
||
Secret: webg.Conf.PlayletMediaResourceDBCfg.Secret,
|
||
})
|
||
}
|
||
|
||
func newPlayletMediaCenterBySource(mediaSource string) *playletMediaCenterService {
|
||
if mediaSource == playletmod.MediaSourceHG {
|
||
return newPlayletMediaCenterFromConfig()
|
||
}
|
||
return newPlayletJHMediaCenterFromConfig()
|
||
}
|
||
|
||
func applyPlayletMediaSourceFilter(filter bson.M, mediaSource string) bson.M {
|
||
if filter == nil {
|
||
filter = bson.M{}
|
||
}
|
||
if mediaSource == playletmod.MediaSourceHG {
|
||
filter[playletmod.FN.Playlet.MediaSource] = playletmod.MediaSourceHG
|
||
return filter
|
||
}
|
||
filter["$or"] = []bson.M{
|
||
{playletmod.FN.Playlet.MediaSource: playletmod.MediaSourceDefault},
|
||
{playletmod.FN.Playlet.MediaSource: bson.M{"$exists": false}},
|
||
}
|
||
return filter
|
||
}
|
||
|
||
// validatePlayletResourceImportReq 校验短剧媒资入库请求
|
||
func validatePlayletResourceImportReq(req *proto.PlayletResourceUseReq) ([]uint, *srvdef.SrvErr) {
|
||
if req == nil {
|
||
return nil, srvdef.New(srvdef.ErrParamError, "参数不能为空", "")
|
||
}
|
||
// 过滤空ID和重复ID,避免重复导入同一条媒资。
|
||
ids := dedupePlayletResourceIds(req.Ids)
|
||
if len(ids) == 0 {
|
||
return nil, srvdef.New(srvdef.ErrParamError, "媒资ID不能为空", "")
|
||
}
|
||
// 后台选择入库属于人工操作,限制单次数量可以避免队列任务过大。
|
||
if len(ids) > maxResourceImportIds {
|
||
return nil, srvdef.New(srvdef.ErrParamError, "单次最多导入100条媒资", "")
|
||
}
|
||
return ids, nil
|
||
}
|
||
|
||
// dedupePlayletResourceIds 去重并过滤无效媒资ID
|
||
func dedupePlayletResourceIds(ids []uint) []uint {
|
||
result := make([]uint, 0, len(ids))
|
||
seen := make(map[uint]struct{}, len(ids))
|
||
for _, id := range ids {
|
||
// 0 不是有效媒资ID,直接丢弃。
|
||
if id == 0 {
|
||
continue
|
||
}
|
||
// 同一批次重复选择的ID只处理一次。
|
||
if _, ok := seen[id]; ok {
|
||
continue
|
||
}
|
||
seen[id] = struct{}{}
|
||
result = append(result, id)
|
||
}
|
||
return result
|
||
}
|
||
|
||
type playletResourceImportLockFunc func(uint) (bool, error)
|
||
type playletResourceImportUnlockFunc func(uint)
|
||
|
||
// importSinglePlayletResource 导入单条短剧媒资
|
||
func importSinglePlayletResource(ctx context.Context, ms *playletMediaCenterService, mediaId uint, operatorId int64, operatorName string) (*proto.PlayletResourceUseSuccess, string) {
|
||
return importSinglePlayletResourceWithLock(ctx, ms, mediaId, operatorId, operatorName, playletmod.MediaSourceHG, lockPlayletResourceImportMedia, unlockPlayletResourceImportMedia)
|
||
}
|
||
|
||
// importSinglePlayletJHResource 导入单条嘉华短剧媒资
|
||
func importSinglePlayletJHResource(ctx context.Context, ms *playletMediaCenterService, mediaId uint, operatorId int64, operatorName string) (*proto.PlayletResourceUseSuccess, string) {
|
||
return importSinglePlayletResourceWithLock(ctx, ms, mediaId, operatorId, operatorName, playletmod.MediaSourceDefault, lockPlayletResourceJHImportMedia, unlockPlayletResourceJHImportMedia)
|
||
}
|
||
|
||
// importSinglePlayletResourceWithLock 导入单条短剧媒资并使用指定锁
|
||
func importSinglePlayletResourceWithLock(ctx context.Context, ms *playletMediaCenterService, mediaId uint, operatorId int64, operatorName string, mediaSource string, lockFn playletResourceImportLockFunc, unlockFn playletResourceImportUnlockFunc) (*proto.PlayletResourceUseSuccess, string) {
|
||
locked, err := lockFn(mediaId)
|
||
if err != nil {
|
||
log.Error("获取短剧媒资导入锁失败", log.Any("mediaId", mediaId), log.E(err))
|
||
return nil, "获取导入锁失败"
|
||
}
|
||
if !locked {
|
||
return nil, "该媒资正在导入"
|
||
}
|
||
defer unlockFn(mediaId)
|
||
|
||
// 本地已导入的媒资不再请求媒资中心,减少无效网络调用。
|
||
if reason := checkPlayletResourceImported(ctx, mediaId, mediaSource); reason != "" {
|
||
return nil, reason
|
||
}
|
||
// sync_time 传 0,表示拉取该媒资当前完整详情和章节列表。
|
||
result, err := ms.mediaSync(&playletMediaSyncReq{
|
||
Id: mediaId,
|
||
SyncTime: 0,
|
||
})
|
||
if err != nil {
|
||
log.Error("同步短剧媒资失败", log.Any("mediaId", mediaId), log.E(err))
|
||
return nil, "同步媒资失败"
|
||
}
|
||
if result.IsDelete || result.Media == nil {
|
||
return nil, "媒资不存在或已删除"
|
||
}
|
||
if !validPlayletImportMediaType(result.Media.MediaType) {
|
||
return nil, "该媒资类型不支持导入短剧"
|
||
}
|
||
if len(result.MediaContentList) == 0 {
|
||
return nil, "媒资章节为空"
|
||
}
|
||
|
||
// 标签按媒资中心 tags_text 自动生成或复用,不再由请求参数控制。
|
||
//tagIds, serr := buildPlayletResourceTagIds(ctx, result.Media.TagsText)
|
||
//if serr != nil {
|
||
// log.Error("处理短剧媒资标签失败", log.Any("mediaId", mediaId), log.Any("err", serr))
|
||
// return nil, "处理标签失败"
|
||
//}
|
||
// 将媒资中心主资源转换成本项目短剧主表模型。
|
||
playlet := buildPlayletFromMedia(result.Media, nil, operatorId, operatorName)
|
||
playlet.MediaSource = mediaSource
|
||
if serr := ensureImportedPlayletSlug(ctx, playlet); serr != nil {
|
||
log.Error("生成短剧媒资slug失败", log.Any("mediaId", mediaId), log.Any("err", serr))
|
||
return nil, "生成slug失败"
|
||
}
|
||
playlet.MediaSyncTime = maxPlayletMediaContentUpdateTime(0, result.MediaContentList)
|
||
// 原图继续写入 Cover;压缩成功才写 CompressedCover,失败保持空。
|
||
playlet.CompressedCover = compressPlayletCoverOrEmpty(playlet.Cover)
|
||
if playlet.Title == "" {
|
||
return nil, "媒资标题为空"
|
||
}
|
||
// 章节转换会过滤没有播放地址的数据,过滤后为空则不写主表。
|
||
chapters := buildPlayletChaptersFromMedia(result.MediaContentList)
|
||
if len(chapters) == 0 {
|
||
return nil, "媒资章节为空"
|
||
}
|
||
// 先插入主表,拿到本项目自增 playlet.Id 后再写章节。
|
||
if serr := playletmod.InsertOnePlaylet(ctx, playlet); serr != nil {
|
||
log.Error("短剧媒资主表入库失败", log.Any("mediaId", mediaId), log.Any("err", serr))
|
||
return nil, "主资源入库失败"
|
||
}
|
||
for _, chapter := range chapters {
|
||
// 每个章节都绑定刚生成的短剧ID。
|
||
chapter.PlayletId = playlet.Id
|
||
}
|
||
// 章节批量写入,减少同一部短剧大量章节逐条访问 Mongo 的开销。
|
||
if serr := playletchaptermod.InsertManyPlayletChapter(ctx, chapters); serr != nil {
|
||
log.Error("短剧媒资章节批量入库失败", log.Any("mediaId", mediaId), log.Any("playletId", playlet.Id), log.Any("chapterCount", len(chapters)), log.Any("err", serr))
|
||
// 批量写入可能已经落入部分章节,按预分配的章节ID回滚,避免半成品短剧留库。
|
||
rollbackImportedPlayletResource(ctx, playlet.Id, collectPlayletChapterIds(chapters))
|
||
return nil, "章节入库失败"
|
||
}
|
||
return &proto.PlayletResourceUseSuccess{
|
||
MediaId: mediaId,
|
||
PlayletId: playlet.Id,
|
||
}, ""
|
||
}
|
||
|
||
// collectPlayletChapterIds 收集章节ID
|
||
func collectPlayletChapterIds(chapters playletchaptermod.PlayletChapterList) []int64 {
|
||
ids := make([]int64, 0, len(chapters))
|
||
for _, chapter := range chapters {
|
||
// 批量插入前已预分配章节ID,回滚时只删除本次任务生成的数据。
|
||
if chapter == nil || chapter.Id == 0 {
|
||
continue
|
||
}
|
||
ids = append(ids, chapter.Id)
|
||
}
|
||
return ids
|
||
}
|
||
|
||
// checkPlayletResourceImported 检查指定来源媒资是否已经导入到本项目短剧表
|
||
func checkPlayletResourceImported(ctx context.Context, mediaId uint, mediaSource string) string {
|
||
// mediaCenterId 是媒资中心ID,不是本项目自增ID,用它判断是否导入过。
|
||
filter := applyPlayletMediaSourceFilter(bson.M{playletmod.FN.Playlet.MediaCenterId: mediaId}, mediaSource)
|
||
_, err := playletmod.FindOnePlaylet(ctx, filter)
|
||
if err == nil {
|
||
return "该媒资已导入"
|
||
}
|
||
if err.Code != srvdef.ErrNoData {
|
||
log.Error("查询短剧媒资导入状态失败", log.Any("mediaId", mediaId), log.Any("err", err))
|
||
return "查询导入状态失败"
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// buildPlayletResourceTagIds 生成短剧入库标签ID列表
|
||
func buildPlayletResourceTagIds(ctx context.Context, tagsText string) ([]int64, *srvdef.SrvErr) {
|
||
tagIds := make([]int64, 0)
|
||
seen := make(map[int64]struct{})
|
||
// 解析媒资中心 tags_text,并自动创建或复用本项目标签。
|
||
for _, tagName := range splitPlayletResourceTags(tagsText) {
|
||
tagId, err := findOrCreatePlayletTag(ctx, tagName)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if tagId <= 0 {
|
||
continue
|
||
}
|
||
if _, ok := seen[tagId]; ok {
|
||
continue
|
||
}
|
||
seen[tagId] = struct{}{}
|
||
tagIds = append(tagIds, tagId)
|
||
}
|
||
return tagIds, nil
|
||
}
|
||
|
||
// splitPlayletResourceTags 拆分媒资中心标签文本
|
||
func splitPlayletResourceTags(tagsText string) []string {
|
||
if strings.TrimSpace(tagsText) == "" {
|
||
return nil
|
||
}
|
||
// 兼容媒资中心可能返回的多种标签分隔符。
|
||
parts := strings.FieldsFunc(tagsText, func(r rune) bool {
|
||
return r == ',' || r == ',' || r == ';' || r == ';' || r == '|'
|
||
})
|
||
result := make([]string, 0, len(parts))
|
||
seen := make(map[string]struct{}, len(parts))
|
||
for _, part := range parts {
|
||
tagName := strings.TrimSpace(part)
|
||
// 空标签不入库,避免产生脏标签。
|
||
if tagName == "" {
|
||
continue
|
||
}
|
||
// 同一个媒资标签文本里重复的标签只保留一次。
|
||
if _, ok := seen[tagName]; ok {
|
||
continue
|
||
}
|
||
seen[tagName] = struct{}{}
|
||
result = append(result, tagName)
|
||
}
|
||
return result
|
||
}
|
||
|
||
// findOrCreatePlayletTag 查询或创建短剧标签
|
||
func findOrCreatePlayletTag(ctx context.Context, tagName string) (int64, *srvdef.SrvErr) {
|
||
// 先按名称查询,存在则直接复用本项目标签ID。
|
||
tag, err := playlettagmod.FindOnePlayletTag(ctx, bson.M{playlettagmod.FN.PlayletTag.Name: tagName})
|
||
if err == nil {
|
||
return tag.Id, nil
|
||
}
|
||
if err.Code != srvdef.ErrNoData {
|
||
return 0, err
|
||
}
|
||
// 标签不存在时创建默认上架标签,方便导入后的短剧直接关联。
|
||
tag = &playlettagmod.PlayletTag{
|
||
Name: tagName,
|
||
Status: true,
|
||
}
|
||
source := utils.SlugSourceFromKeywordsOrTitle(nil, tag.Name)
|
||
tag.Slug = utils.MakeUniqueSlug(source, func(slug string) bool {
|
||
exist, _ := playlettagmod.FindOnePlayletTag(ctx, bson.M{
|
||
playlettagmod.FN.PlayletTag.Slug: slug,
|
||
})
|
||
return exist != nil
|
||
})
|
||
if err = playlettagmod.InsertOnePlayletTag(ctx, tag); err != nil {
|
||
return 0, err
|
||
}
|
||
return tag.Id, nil
|
||
}
|
||
|
||
// buildPlayletFromMedia 将媒资中心主资源转换为本项目 playlet 主表模型
|
||
func buildPlayletFromMedia(media *proto.PlayletResourceListItem, tagIds []int64, operatorId int64, operatorName string) *playletmod.Playlet {
|
||
// 封面优先使用竖图,适配短剧列表和详情页常见展示比例。
|
||
cover := strings.TrimSpace(media.VerticalCover)
|
||
if cover == "" {
|
||
// 竖图缺失时回退到横图,避免短剧没有封面。
|
||
cover = strings.TrimSpace(media.HorizontalCover)
|
||
}
|
||
return &playletmod.Playlet{
|
||
// 保存媒资中心ID,用于重复导入拦截和后续追溯来源。
|
||
MediaCenterId: uint(media.ID),
|
||
MediaCreatedAt: mediaResourceTime(media.CreateTime),
|
||
MediaUpdatedAt: mediaResourceTime(media.UpdateTime),
|
||
Title: strings.TrimSpace(media.Title),
|
||
Desc: strings.TrimSpace(media.Summary),
|
||
Anchor: strings.TrimSpace(media.Anchor),
|
||
Cover: cover,
|
||
TagIds: tagIds,
|
||
ContentType: contentTypeFromMediaType(media.MediaType),
|
||
OperateType: playletmod.OperateTypePlaylet,
|
||
TotalEpisode: int64(media.TotalEpisode),
|
||
UpdateStatus: normalizePlayletUpdateStatus(media.UpdateStatus),
|
||
HeatValue: randomPlayletHeatValue(),
|
||
VirtualCountLike: randomPlayletVirtualLike(),
|
||
Status: playletmod.StatusDown,
|
||
OperatorId: operatorId,
|
||
OperatorName: operatorName,
|
||
}
|
||
}
|
||
|
||
func ensureImportedPlayletSlug(ctx context.Context, playlet *playletmod.Playlet) *srvdef.SrvErr {
|
||
if playlet == nil {
|
||
return srvdef.New(srvdef.ErrParamError, "短剧数据为空", "")
|
||
}
|
||
playlet.Slug = utils.MakeAbbrSlug(playlet.Title, func(slug string) bool {
|
||
exist, _ := playletmod.FindOnePlaylet(ctx, bson.M{
|
||
playletmod.FN.Playlet.Slug: slug,
|
||
})
|
||
return exist != nil
|
||
})
|
||
return nil
|
||
}
|
||
|
||
func randomPlayletHeatValue() int64 {
|
||
return int64(rand.Intn(playletHeatMaxValue-playletHeatMinValue+1) + playletHeatMinValue)
|
||
}
|
||
|
||
func randomPlayletVirtualLike() int64 {
|
||
return int64(rand.Intn(playletVirtualLikeMaxValue-playletVirtualLikeMinValue+1) + playletVirtualLikeMinValue)
|
||
}
|
||
|
||
// mediaResourceTime 转换媒资中心时间戳
|
||
func mediaResourceTime(ts int64) time.Time {
|
||
// 媒资中心可能返回秒或毫秒时间戳,这里做兼容转换,避免毫秒时间被当成秒导致年份异常。
|
||
if ts <= 0 {
|
||
return time.Time{}
|
||
}
|
||
if ts > 1e12 {
|
||
return time.UnixMilli(ts)
|
||
}
|
||
return time.Unix(ts, 0)
|
||
}
|
||
|
||
// buildPlayletChaptersFromMedia 将媒资中心章节资源转换为本项目 playlet_chapter 模型
|
||
func buildPlayletChaptersFromMedia(mediaContentList []playletMediaContent) playletchaptermod.PlayletChapterList {
|
||
chapters := make(playletchaptermod.PlayletChapterList, 0, len(mediaContentList))
|
||
seenMediaContentIds := make(map[uint]struct{}, len(mediaContentList))
|
||
seenEpisodes := make(map[int]struct{}, len(mediaContentList))
|
||
seenHashIds := make(map[string]struct{}, len(mediaContentList))
|
||
seenVideoUrls := make(map[string]struct{}, len(mediaContentList))
|
||
for _, item := range mediaContentList {
|
||
videoUrl := strings.TrimSpace(item.M3u8Url)
|
||
if videoUrl == "" {
|
||
// m3u8_url 缺失时回退到 urls,兼容不同资源格式。
|
||
videoUrl = strings.TrimSpace(item.Urls)
|
||
}
|
||
if videoUrl == "" {
|
||
// 没有播放地址的章节不能正常播放,直接跳过。
|
||
continue
|
||
}
|
||
if isDuplicatePlayletMediaChapter(item, videoUrl, seenMediaContentIds, seenEpisodes, seenHashIds, seenVideoUrls) {
|
||
continue
|
||
}
|
||
episodeNumber := int64(item.EpisodeNumber)
|
||
if episodeNumber <= 0 {
|
||
// 媒资中心没给集数时,在去重后按有效章节顺序补齐,保证章节可排序。
|
||
episodeNumber = int64(len(chapters) + 1)
|
||
}
|
||
height := int64(item.Height)
|
||
width := int64(item.Width)
|
||
ratio := item.Ratio
|
||
if ratio <= 0 && height > 0 {
|
||
// 宽高比缺失时按宽高计算,供前端展示使用。
|
||
ratio = float64(width) / float64(height)
|
||
}
|
||
chapters = append(chapters, &playletchaptermod.PlayletChapter{
|
||
MediaContentId: uint(item.ID),
|
||
CurrentEpisode: episodeNumber,
|
||
Title: strings.TrimSpace(item.Title),
|
||
Hash: item.HashId,
|
||
Height: height,
|
||
Width: width,
|
||
Ratio: ratio,
|
||
VideoUrl: videoUrl,
|
||
MediaSize: int64(item.MediaSize),
|
||
MediaTime: int64(item.PlayTime),
|
||
PayType: playletchaptermod.PayTypeFree,
|
||
SingleCoins: int64(item.SaleCoin),
|
||
WatermarkStatus: playletchaptermod.WatermarkStatusPending,
|
||
})
|
||
}
|
||
return chapters
|
||
}
|
||
|
||
// isDuplicatePlayletMediaChapter 判断媒资中心章节是否重复
|
||
func isDuplicatePlayletMediaChapter(item playletMediaContent, videoUrl string, seenMediaContentIds map[uint]struct{}, seenEpisodes map[int]struct{}, seenHashIds map[string]struct{}, seenVideoUrls map[string]struct{}) bool {
|
||
if item.ID > 0 {
|
||
// 媒资中心章节ID是最稳定的来源章节标识,优先用它做幂等去重。
|
||
if _, ok := seenMediaContentIds[item.ID]; ok {
|
||
log.Info("跳过重复短剧媒资章节", log.Any("mediaContentId", item.ID), log.Any("episodeNumber", item.EpisodeNumber))
|
||
return true
|
||
}
|
||
seenMediaContentIds[item.ID] = struct{}{}
|
||
return false
|
||
}
|
||
if item.EpisodeNumber > 0 {
|
||
// 老数据如果没有媒资章节ID,则按集数去重,避免同一集重复入库。
|
||
if _, ok := seenEpisodes[item.EpisodeNumber]; ok {
|
||
log.Info("跳过重复短剧媒资章节", log.Any("episodeNumber", item.EpisodeNumber))
|
||
return true
|
||
}
|
||
seenEpisodes[item.EpisodeNumber] = struct{}{}
|
||
return false
|
||
}
|
||
hashId := strings.TrimSpace(item.HashId)
|
||
if hashId != "" {
|
||
// 集数缺失时使用资源hash兜底。
|
||
if _, ok := seenHashIds[hashId]; ok {
|
||
log.Info("跳过重复短剧媒资章节", log.Any("hashId", hashId))
|
||
return true
|
||
}
|
||
seenHashIds[hashId] = struct{}{}
|
||
return false
|
||
}
|
||
// 最后用播放地址兜底,避免无ID、无集数、无hash的重复资源写入。
|
||
if _, ok := seenVideoUrls[videoUrl]; ok {
|
||
log.Info("跳过重复短剧媒资章节", log.Any("videoUrl", videoUrl))
|
||
return true
|
||
}
|
||
seenVideoUrls[videoUrl] = struct{}{}
|
||
return false
|
||
}
|
||
|
||
// rollbackImportedPlayletResource 回滚单条媒资入库产生的数据
|
||
func rollbackImportedPlayletResource(ctx context.Context, playletId int64, chapterIds []int64) {
|
||
for _, chapterId := range chapterIds {
|
||
// 只删除本次成功插入的章节,避免误删已有数据。
|
||
if _, err := playletchaptermod.DelOnePlayletChapter(ctx, bson.M{playletchaptermod.FN.PlayletChapter.Id: chapterId}); err != nil {
|
||
log.Error("回滚短剧媒资章节失败", log.Any("playletId", playletId), log.Any("chapterId", chapterId), log.Any("err", err))
|
||
}
|
||
}
|
||
// 章节失败后删除刚写入的主表,避免后台看到没有章节的短剧。
|
||
if _, err := playletmod.DelOnePlaylet(ctx, bson.M{playletmod.FN.Playlet.Id: playletId}); err != nil {
|
||
log.Error("回滚短剧媒资主表失败", log.Any("playletId", playletId), log.Any("err", err))
|
||
}
|
||
}
|
||
|
||
// resolveImportOperator 从后台登录态解析当前操作人,避免与 srv_playlet 循环依赖。
|
||
func resolveImportOperator(ctx context.Context) (int64, string) {
|
||
c, ok := ctx.(*gin.Context)
|
||
if !ok || c == nil {
|
||
return 0, ""
|
||
}
|
||
accId := auth.GetAccount(c)
|
||
if accId <= 0 {
|
||
return 0, ""
|
||
}
|
||
acc, err := mwacc.FindOneAccByFilter(c, bson.M{
|
||
mwacc.FN.MwAcc.Id: accId,
|
||
}, options.FindOne().SetProjection(bson.D{
|
||
{Key: mwacc.FN.MwAcc.Id, Value: 1},
|
||
{Key: mwacc.FN.MwAcc.UserName, Value: 1},
|
||
}))
|
||
if err != nil || acc == nil {
|
||
return accId, ""
|
||
}
|
||
return acc.Id, acc.UserName
|
||
}
|