Files
web_hgtv_server/web/service/srv_mediaresource/resource.go
T
2026-10-22 16:44:05 +08:00

1433 lines
49 KiB
Go
Raw Blame History

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