移除 pub/dataReport、reportser、srv_report、上报定时任务及 eventmod 事件表, 以及注册/登录/点赞/收藏/评论/解锁/订单/上下架等处的上报调用和 reportSdkConfig 配置下发。 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
3801 lines
135 KiB
Go
3801 lines
135 KiB
Go
package playletser
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"strconv"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
"videoweb/app/api/proto"
|
||
"videoweb/app/service/configser"
|
||
"videoweb/app/service/playletcache"
|
||
"videoweb/app/service/videoser"
|
||
"videoweb/pub/common/enum/redisconst"
|
||
"videoweb/pub/common/log"
|
||
"videoweb/pub/common/netutil"
|
||
"videoweb/pub/common/srvdef"
|
||
"videoweb/pub/common/utils"
|
||
"videoweb/pub/esmanager"
|
||
"videoweb/pub/models/cointransmod"
|
||
"videoweb/pub/models/esmod"
|
||
"videoweb/pub/models/homebannermod"
|
||
"videoweb/pub/models/hotsearchmod"
|
||
"videoweb/pub/models/playletcategorymod"
|
||
"videoweb/pub/models/playletchaptercollectmod"
|
||
"videoweb/pub/models/playletchapterlikemod"
|
||
"videoweb/pub/models/playletchaptermod"
|
||
"videoweb/pub/models/playletchapterunlockmod"
|
||
"videoweb/pub/models/playletcollectmod"
|
||
"videoweb/pub/models/playletlikemod"
|
||
"videoweb/pub/models/playletmod"
|
||
"videoweb/pub/models/playletwatchhistorymod"
|
||
"videoweb/pub/models/playletwatchprogressmod"
|
||
"videoweb/pub/models/usermod"
|
||
"videoweb/pub/mongos"
|
||
"videoweb/pub/payment"
|
||
"videoweb/pub/pubdef"
|
||
"videoweb/pub/redis"
|
||
"videoweb/web/service/srv_search"
|
||
|
||
"go.mongodb.org/mongo-driver/bson"
|
||
"go.mongodb.org/mongo-driver/mongo"
|
||
"go.mongodb.org/mongo-driver/mongo/options"
|
||
)
|
||
|
||
const (
|
||
homeBannerLimit int64 = 30 // banner图限制
|
||
homeSectionLimit int64 = 16 // 首页分区数据限制
|
||
homeRecommendThemeLimit int = 4 // 首页推荐主题限制
|
||
defaultPageSize int64 = 10 // 默认分页条数
|
||
recommendPageSize int64 = 20 // 今日推荐/热门好剧默认分页条数
|
||
maxPageSize int64 = 100 // 最大分页条数
|
||
sortOrderHot int64 = 1 // 热度优先
|
||
sortOrderUpdated int64 = 2 // 今日更新
|
||
operateFilterLatest int64 = 1 // 最近更新
|
||
operateFilterHot int64 = 2 // 当前热播
|
||
operateFilterOriginal int64 = 3 // 独家原创
|
||
rankHot int64 = 1 // 热播榜
|
||
rankNew int64 = 2 // 新剧榜
|
||
rankCollect int64 = 3 // 追剧榜
|
||
relatedLimit int64 = 8 // 相关推荐限制
|
||
operateStatusYes int64 = 1 // 已操作
|
||
operateStatusNo int64 = 2 // 未操作
|
||
playletHeatClickInc int64 = 100 // 每次查看详情增加的热度值
|
||
playletHeatLikeInc int64 = 1 // 点赞增加的热度值
|
||
playletHeatCollectInc int64 = 2 // 收藏增加的热度值
|
||
playletHeatBuyInc int64 = 10 // 购买剧集增加的短剧热度值
|
||
playletHeatForwardInc int64 = 5 // 转发增加的热度值
|
||
homePlayletCacheSchema = "banner-v2"
|
||
// playletH5DetailPath 短剧Web/H5详情页路径,供SEO canonical使用。
|
||
playletH5DetailPath = "/detail"
|
||
// playletChapterH5DetailPath 短剧Web/H5播放页路径,供SEO canonical使用。
|
||
playletChapterH5DetailPath = "/play"
|
||
)
|
||
|
||
var errPlayletChapterAlreadyUnlocked = errors.New("playlet chapter already unlocked")
|
||
|
||
type playletChapterAccessContext struct {
|
||
balance int64
|
||
vipValid bool
|
||
unlockedChapterIds map[int64]bool
|
||
}
|
||
|
||
// playletHomeProjection 查询指定字段
|
||
var playletHomeProjection = bson.D{
|
||
{Key: playletmod.FN.Playlet.Id, Value: 1},
|
||
{Key: playletmod.FN.Playlet.Status, Value: 1},
|
||
{Key: playletmod.FN.Playlet.IsFeatured, Value: 1},
|
||
{Key: playletmod.FN.Playlet.IsRecommend, Value: 1},
|
||
{Key: playletmod.FN.Playlet.ShowOnBanner, Value: 1},
|
||
{Key: playletmod.FN.Playlet.Banner, Value: 1},
|
||
{Key: playletmod.FN.Playlet.OnlineAt, Value: 1},
|
||
{Key: playletmod.FN.Playlet.MediaCreatedAt, Value: 1},
|
||
{Key: playletmod.FN.Playlet.MediaUpdatedAt, Value: 1},
|
||
{Key: playletmod.FN.Playlet.ThemeIds, Value: 1},
|
||
{Key: playletmod.FN.Playlet.Title, Value: 1},
|
||
{Key: playletmod.FN.Playlet.PlayName, Value: 1},
|
||
{Key: playletmod.FN.Playlet.Description, Value: 1},
|
||
{Key: playletmod.FN.Playlet.Slug, Value: 1},
|
||
{Key: playletmod.FN.Playlet.Desc, Value: 1},
|
||
{Key: playletmod.FN.Playlet.Anchor, Value: 1},
|
||
{Key: playletmod.FN.Playlet.Cover, Value: 1},
|
||
{Key: playletmod.FN.Playlet.CompressedCover, Value: 1},
|
||
{Key: playletmod.FN.Playlet.TagIds, Value: 1},
|
||
{Key: playletmod.FN.Playlet.TotalEpisode, Value: 1},
|
||
{Key: playletmod.FN.Playlet.UpdateStatus, Value: 1},
|
||
{Key: playletmod.FN.Playlet.CountBrowse, Value: 1},
|
||
{Key: playletmod.FN.Playlet.HeatValue, Value: 1},
|
||
{Key: playletmod.FN.Playlet.CountCollect, Value: 1},
|
||
{Key: playletmod.FN.Playlet.CountLike, Value: 1},
|
||
{Key: playletmod.FN.Playlet.VirtualCountLike, Value: 1},
|
||
{Key: playletmod.FN.Playlet.IsOriginal, Value: 1},
|
||
{Key: playletmod.FN.Playlet.OperateType, Value: 1},
|
||
{Key: playletmod.FN.Playlet.CreatedAt, Value: 1},
|
||
{Key: playletmod.FN.Playlet.UpdatedAt, Value: 1},
|
||
{Key: "seoInfo", Value: 1},
|
||
}
|
||
|
||
// homeBannerAppProjection 查询首页 banner 指定字段
|
||
var homeBannerAppProjection = bson.D{
|
||
{Key: homebannermod.FN.HomeBanner.Id, Value: 1},
|
||
{Key: homebannermod.FN.HomeBanner.Name, Value: 1},
|
||
{Key: homebannermod.FN.HomeBanner.Image, Value: 1},
|
||
{Key: homebannermod.FN.HomeBanner.VerticalImage, Value: 1},
|
||
{Key: homebannermod.FN.HomeBanner.Url, Value: 1},
|
||
{Key: homebannermod.FN.HomeBanner.Rank, Value: 1},
|
||
{Key: homebannermod.FN.HomeBanner.Status, Value: 1},
|
||
}
|
||
|
||
// playletCollectListProjection 我的收藏列表只读取卡片展示和未看集数计算需要的字段。
|
||
var playletCollectListProjection = bson.D{
|
||
{Key: playletmod.FN.Playlet.Id, Value: 1},
|
||
{Key: playletmod.FN.Playlet.Title, Value: 1},
|
||
{Key: playletmod.FN.Playlet.PlayName, Value: 1},
|
||
{Key: playletmod.FN.Playlet.Slug, Value: 1},
|
||
{Key: playletmod.FN.Playlet.Cover, Value: 1},
|
||
{Key: playletmod.FN.Playlet.CompressedCover, Value: 1},
|
||
{Key: playletmod.FN.Playlet.ThemeIds, Value: 1},
|
||
{Key: playletmod.FN.Playlet.TagIds, Value: 1},
|
||
{Key: playletmod.FN.Playlet.TotalEpisode, Value: 1},
|
||
{Key: playletmod.FN.Playlet.UpdateStatus, Value: 1},
|
||
{Key: playletmod.FN.Playlet.CountBrowse, Value: 1},
|
||
{Key: playletmod.FN.Playlet.HeatValue, Value: 1},
|
||
{Key: playletmod.FN.Playlet.CountCollect, Value: 1},
|
||
{Key: playletmod.FN.Playlet.CountLike, Value: 1},
|
||
{Key: playletmod.FN.Playlet.VirtualCountLike, Value: 1},
|
||
{Key: "seoInfo", Value: 1},
|
||
}
|
||
|
||
// playletChapterSEOProjection 查询剧集详情SEO需要的短剧字段,避免拉取整部短剧大字段。
|
||
var playletChapterSEOProjection = bson.D{
|
||
{Key: playletmod.FN.Playlet.Id, Value: 1},
|
||
{Key: playletmod.FN.Playlet.Title, Value: 1},
|
||
{Key: playletmod.FN.Playlet.Slug, Value: 1},
|
||
{Key: playletmod.FN.Playlet.Desc, Value: 1},
|
||
{Key: playletmod.FN.Playlet.IsOriginal, Value: 1},
|
||
{Key: "seoInfo", Value: 1},
|
||
}
|
||
|
||
// playletChapterInfoProjection 查询详情章节集合需要的字段
|
||
var playletChapterInfoProjection = bson.D{
|
||
{Key: playletchaptermod.FN.PlayletChapter.Id, Value: 1},
|
||
{Key: playletchaptermod.FN.PlayletChapter.PlayletId, Value: 1},
|
||
{Key: playletchaptermod.FN.PlayletChapter.CurrentEpisode, Value: 1},
|
||
{Key: playletchaptermod.FN.PlayletChapter.Title, Value: 1},
|
||
{Key: playletchaptermod.FN.PlayletChapter.Md5, Value: 1},
|
||
{Key: playletchaptermod.FN.PlayletChapter.Hash, Value: 1},
|
||
{Key: playletchaptermod.FN.PlayletChapter.Height, Value: 1},
|
||
{Key: playletchaptermod.FN.PlayletChapter.Width, Value: 1},
|
||
{Key: playletchaptermod.FN.PlayletChapter.Ratio, Value: 1},
|
||
{Key: playletchaptermod.FN.PlayletChapter.VideoUrl, Value: 1},
|
||
{Key: playletchaptermod.FN.PlayletChapter.WatermarkVideoUrl, Value: 1},
|
||
{Key: playletchaptermod.FN.PlayletChapter.MediaSize, Value: 1},
|
||
{Key: playletchaptermod.FN.PlayletChapter.MediaTime, Value: 1},
|
||
{Key: playletchaptermod.FN.PlayletChapter.PayType, Value: 1},
|
||
{Key: playletchaptermod.FN.PlayletChapter.SingleCoins, Value: 1},
|
||
}
|
||
|
||
// playletChapterDetailProjection 查询剧集详情需要的字段
|
||
var playletChapterDetailProjection = bson.D{
|
||
{Key: playletchaptermod.FN.PlayletChapter.Id, Value: 1},
|
||
{Key: playletchaptermod.FN.PlayletChapter.PlayletId, Value: 1},
|
||
{Key: playletchaptermod.FN.PlayletChapter.CurrentEpisode, Value: 1},
|
||
{Key: playletchaptermod.FN.PlayletChapter.Title, Value: 1},
|
||
{Key: playletchaptermod.FN.PlayletChapter.Md5, Value: 1},
|
||
{Key: playletchaptermod.FN.PlayletChapter.Hash, Value: 1},
|
||
{Key: playletchaptermod.FN.PlayletChapter.Height, Value: 1},
|
||
{Key: playletchaptermod.FN.PlayletChapter.Width, Value: 1},
|
||
{Key: playletchaptermod.FN.PlayletChapter.Ratio, Value: 1},
|
||
{Key: playletchaptermod.FN.PlayletChapter.VideoUrl, Value: 1},
|
||
{Key: playletchaptermod.FN.PlayletChapter.WatermarkVideoUrl, Value: 1},
|
||
{Key: playletchaptermod.FN.PlayletChapter.MediaSize, Value: 1},
|
||
{Key: playletchaptermod.FN.PlayletChapter.MediaTime, Value: 1},
|
||
{Key: playletchaptermod.FN.PlayletChapter.CountComment, Value: 1},
|
||
{Key: playletchaptermod.FN.PlayletChapter.CountBrowse, Value: 1},
|
||
{Key: playletchaptermod.FN.PlayletChapter.CountCollect, Value: 1},
|
||
{Key: playletchaptermod.FN.PlayletChapter.CountLike, Value: 1},
|
||
{Key: playletchaptermod.FN.PlayletChapter.PayType, Value: 1},
|
||
{Key: playletchaptermod.FN.PlayletChapter.SingleCoins, Value: 1},
|
||
{Key: playletchaptermod.FN.PlayletChapter.WatermarkStatus, Value: 1},
|
||
}
|
||
|
||
// playletOperateProjection 查询短剧操作接口需要的字段
|
||
var playletOperateProjection = bson.D{
|
||
{Key: playletmod.FN.Playlet.Id, Value: 1},
|
||
{Key: playletmod.FN.Playlet.CountLike, Value: 1},
|
||
{Key: playletmod.FN.Playlet.VirtualCountLike, Value: 1},
|
||
{Key: playletmod.FN.Playlet.CountCollect, Value: 1},
|
||
}
|
||
|
||
// playletChapterOperateProjection 查询剧集操作接口需要的字段
|
||
var playletChapterOperateProjection = bson.D{
|
||
{Key: playletchaptermod.FN.PlayletChapter.Id, Value: 1},
|
||
{Key: playletchaptermod.FN.PlayletChapter.PlayletId, Value: 1},
|
||
{Key: playletchaptermod.FN.PlayletChapter.CountLike, Value: 1},
|
||
{Key: playletchaptermod.FN.PlayletChapter.CountCollect, Value: 1},
|
||
}
|
||
|
||
// playletChapterWatchReportProjection 观看上报只需要剧集归属和集数。
|
||
var playletChapterWatchReportProjection = bson.D{
|
||
{Key: playletchaptermod.FN.PlayletChapter.Id, Value: 1},
|
||
{Key: playletchaptermod.FN.PlayletChapter.PlayletId, Value: 1},
|
||
{Key: playletchaptermod.FN.PlayletChapter.CurrentEpisode, Value: 1},
|
||
}
|
||
|
||
// playletWatchReportProjection 观看上报只需要短剧总集数计算未看数量。
|
||
var playletWatchReportProjection = bson.D{
|
||
{Key: playletmod.FN.Playlet.Id, Value: 1},
|
||
{Key: playletmod.FN.Playlet.TotalEpisode, Value: 1},
|
||
}
|
||
|
||
// Home 查询app首页短剧数据
|
||
func Home(ctx context.Context) (*proto.HomePlayletResponse, *srvdef.SrvErr) {
|
||
cacheKey := homePlayletCacheKey()
|
||
if resp := getHomePlayletCache(cacheKey); resp != nil {
|
||
attachHomeAnnouncements(resp)
|
||
attachHomeRecommendThemes(ctx, resp)
|
||
return resp, nil
|
||
}
|
||
resp, err := buildHomePlayletResponse(ctx)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
setHomePlayletCache(cacheKey, resp)
|
||
attachHomeAnnouncements(resp)
|
||
attachHomeRecommendThemes(ctx, resp)
|
||
return resp, nil
|
||
}
|
||
|
||
// RecommendList 分页查询app端分类短剧列表
|
||
func RecommendList(ctx context.Context, req *proto.PlayletRecommendListReq) (*proto.PlayletRecommendListResp, *srvdef.SrvErr) {
|
||
if req == nil {
|
||
return nil, srvdef.New(srvdef.ErrParamError, "", "")
|
||
}
|
||
categoryID, err := resolveCategoryID(ctx, req.CategoryId, req.CategorySlug)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
req.CategoryId = categoryID
|
||
pageNum, pageSize := normalizePlayletRecommendPage(req.PageNum, req.PageSize)
|
||
cacheKey := playletRecommendListCacheKey(req.CategoryId, pageNum, pageSize)
|
||
if resp := getPlayletRecommendListCache(cacheKey); resp != nil {
|
||
return resp, nil
|
||
}
|
||
category, err := findEnabledPlayletCategory(ctx, req.CategoryId)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
filter, sort, err := playletRecommendListQuery(ctx, req.CategoryId)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
totalCount, err := playletmod.CountPlaylet(ctx, filter)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
resp := &proto.PlayletRecommendListResp{
|
||
CategoryId: category.Id,
|
||
CategoryName: category.Name,
|
||
CategorySlug: category.Slug,
|
||
CategoryDesc: category.Desc,
|
||
TotalCount: totalCount,
|
||
PageSize: int(pageSize),
|
||
PageNum: int(pageNum),
|
||
List: make([]proto.CommonData, 0),
|
||
}
|
||
resp.SeoInfo = buildPlayletCategoryPageSeoInfo(category.Id, category.Name)
|
||
if totalCount == 0 {
|
||
return resp, nil
|
||
}
|
||
skip := (pageNum - 1) * pageSize
|
||
if skip >= totalCount {
|
||
return resp, nil
|
||
}
|
||
var list playletmod.PlayletList
|
||
if _, ok := filter[playletmod.FN.Playlet.CategoryIds]; ok {
|
||
list, err = playletmod.FindManyPlayletByCategoryRank(ctx, filter, req.CategoryId, skip, pageSize, playletmod.CategoryRankSort(), playletHomeProjection)
|
||
} else {
|
||
list, err = findHomePlaylets(ctx, filter, sort, pageSize, options.Find().SetSkip(skip))
|
||
}
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
themeNameMap, err := buildHomeThemeNameMap(ctx, list)
|
||
if err != nil {
|
||
log.Error("查询分类短剧题材名称失败", log.Any("分类ID", req.CategoryId), log.E(err))
|
||
return nil, err
|
||
}
|
||
tagNameMap, err := buildHomeTagNameMap(ctx, list)
|
||
if err != nil {
|
||
log.Error("查询分类短剧标签名称失败", log.Any("分类ID", req.CategoryId), log.E(err))
|
||
return nil, err
|
||
}
|
||
resp.List = commonDataValues(playletsToHomeCommonData(ctx, list, themeNameMap, tagNameMap))
|
||
setPlayletRecommendListCache(cacheKey, resp)
|
||
return resp, nil
|
||
}
|
||
|
||
// List 分页查询app端短剧列表
|
||
func List(ctx context.Context, req *proto.PlayletVideoReq) (*proto.PlayletVideoResp, *srvdef.SrvErr) {
|
||
if req == nil {
|
||
return nil, srvdef.New(srvdef.ErrParamError, "", "")
|
||
}
|
||
themeID, err := resolveThemeID(ctx, req.ThemeId, req.ThemeSlug)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
req.ThemeId = themeID
|
||
pageNum, pageSize := normalizePlayletPage(req.PageNum, req.PageSize)
|
||
filter := homeUpPlayletFilter(nil)
|
||
if req.ThemeId > 0 {
|
||
filter[playletmod.FN.Playlet.ThemeIds] = bson.M{"$in": []int64{req.ThemeId}}
|
||
}
|
||
totalCount, err := playletmod.CountPlaylet(ctx, filter)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
resp := &proto.PlayletVideoResp{
|
||
TotalCount: totalCount,
|
||
PageSize: int(pageSize),
|
||
PageNum: int(pageNum),
|
||
List: make([]proto.CommonData, 0),
|
||
}
|
||
if req.ThemeId > 0 {
|
||
resp.SeoInfo = buildPlayletThemePageSeoInfo(req.ThemeId)
|
||
} else {
|
||
resp.SeoInfo = buildPlayletListPageSeoInfo()
|
||
}
|
||
if totalCount == 0 {
|
||
return resp, nil
|
||
}
|
||
skip := (pageNum - 1) * pageSize
|
||
if skip >= totalCount {
|
||
return resp, nil
|
||
}
|
||
list, err := findHomePlaylets(ctx, filter, playletListSort(req.SortOrder), pageSize, options.Find().SetSkip(skip))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
themeNameMap, err := buildHomeThemeNameMap(ctx, list)
|
||
if err != nil {
|
||
log.Error("查询短剧列表题材名称失败", log.E(err))
|
||
return nil, err
|
||
}
|
||
tagNameMap, err := buildHomeTagNameMap(ctx, list)
|
||
if err != nil {
|
||
log.Error("查询短剧列表标签名称失败", log.E(err))
|
||
return nil, err
|
||
}
|
||
resp.List = commonDataValues(playletsToHomeCommonData(ctx, list, themeNameMap, tagNameMap))
|
||
return resp, nil
|
||
}
|
||
|
||
// Rank 分页查询app端短剧排行榜
|
||
func Rank(ctx context.Context, req *proto.PlayletRankReq) (*proto.PlayletRankResp, *srvdef.SrvErr) {
|
||
if req == nil {
|
||
return nil, srvdef.New(srvdef.ErrParamError, "", "")
|
||
}
|
||
pageNum, pageSize := normalizePlayletPage(req.PageNum, req.PageSize)
|
||
filter := homeUpPlayletFilter(nil)
|
||
totalCount, err := playletmod.CountPlaylet(ctx, filter)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
resp := &proto.PlayletRankResp{
|
||
TotalCount: totalCount,
|
||
PageSize: int(pageSize),
|
||
PageNum: int(pageNum),
|
||
List: make([]proto.CommonData, 0),
|
||
}
|
||
resp.SeoInfo = buildPlayletListPageSeoInfo()
|
||
if totalCount == 0 {
|
||
return resp, nil
|
||
}
|
||
skip := (pageNum - 1) * pageSize
|
||
if skip >= totalCount {
|
||
return resp, nil
|
||
}
|
||
list, err := findHomePlaylets(ctx, filter, playletRankSort(req.SortOrder), pageSize, options.Find().SetSkip(skip))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
themeNameMap, err := buildHomeThemeNameMap(ctx, list)
|
||
if err != nil {
|
||
log.Error("查询短剧排行榜题材名称失败", log.E(err))
|
||
return nil, err
|
||
}
|
||
tagNameMap, err := buildHomeTagNameMap(ctx, list)
|
||
if err != nil {
|
||
log.Error("查询短剧排行榜标签名称失败", log.E(err))
|
||
return nil, err
|
||
}
|
||
resp.List = commonDataValues(playletsToHomeCommonData(ctx, list, themeNameMap, tagNameMap))
|
||
return resp, nil
|
||
}
|
||
|
||
// TodayUpdate 分页查询app端今日更新短剧列表
|
||
func TodayUpdate(ctx context.Context, req *proto.PlayletTodayUpdateReq) (*proto.PlayletTodayUpdateResp, *srvdef.SrvErr) {
|
||
if req == nil {
|
||
return nil, srvdef.New(srvdef.ErrParamError, "", "")
|
||
}
|
||
pageNum, pageSize := normalizePlayletPage(req.PageNum, req.PageSize)
|
||
filter := homeUpPlayletFilter(nil)
|
||
totalCount, err := playletmod.CountPlaylet(ctx, filter)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
resp := &proto.PlayletTodayUpdateResp{
|
||
TotalCount: totalCount,
|
||
PageSize: int(pageSize),
|
||
PageNum: int(pageNum),
|
||
List: make([]proto.CommonData, 0),
|
||
}
|
||
resp.SeoInfo = buildPlayletListPageSeoInfo()
|
||
if totalCount == 0 {
|
||
return resp, nil
|
||
}
|
||
skip := (pageNum - 1) * pageSize
|
||
if skip >= totalCount {
|
||
return resp, nil
|
||
}
|
||
list, err := findHomePlaylets(ctx, filter, playletTodayUpdateSort(), pageSize, options.Find().SetSkip(skip))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
themeNameMap, err := buildHomeThemeNameMap(ctx, list)
|
||
if err != nil {
|
||
log.Error("查询今日更新短剧题材名称失败", log.E(err))
|
||
return nil, err
|
||
}
|
||
tagNameMap, err := buildHomeTagNameMap(ctx, list)
|
||
if err != nil {
|
||
log.Error("查询今日更新短剧标签名称失败", log.E(err))
|
||
return nil, err
|
||
}
|
||
resp.List = commonDataValues(playletsToHomeCommonData(ctx, list, themeNameMap, tagNameMap))
|
||
return resp, nil
|
||
}
|
||
|
||
// OperateList 按运营类型分页查询短剧,支持最近更新、当前热播、独家原创。
|
||
func OperateList(ctx context.Context, req *proto.PlayletOperateListReq) (*proto.PlayletOperateListResp, *srvdef.SrvErr) {
|
||
if req == nil {
|
||
return nil, srvdef.New(srvdef.ErrParamError, "", "")
|
||
}
|
||
operateType := playletmod.OperateType(req.OperateType)
|
||
if !playletmod.IsValidOperateType(operateType) {
|
||
return nil, srvdef.New(srvdef.ErrParamError, "运营类型无效", "")
|
||
}
|
||
filterType := req.FilterType
|
||
if filterType == 0 {
|
||
filterType = operateFilterLatest
|
||
}
|
||
if filterType != operateFilterLatest && filterType != operateFilterHot && filterType != operateFilterOriginal {
|
||
return nil, srvdef.New(srvdef.ErrParamError, "筛选类型无效", "")
|
||
}
|
||
pageNum, pageSize := normalizePlayletPage(req.PageNum, req.PageSize)
|
||
filter := homeUpPlayletFilter(operateTypeFilter(operateType))
|
||
if filterType == operateFilterOriginal {
|
||
filter[playletmod.FN.Playlet.IsOriginal] = true
|
||
}
|
||
totalCount, err := playletmod.CountPlaylet(ctx, filter)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
resp := &proto.PlayletOperateListResp{
|
||
TotalCount: totalCount,
|
||
PageSize: int(pageSize),
|
||
PageNum: int(pageNum),
|
||
OperateType: int32(operateType),
|
||
FilterType: filterType,
|
||
List: make([]proto.CommonData, 0),
|
||
}
|
||
if totalCount == 0 {
|
||
return resp, nil
|
||
}
|
||
skip := (pageNum - 1) * pageSize
|
||
if skip >= totalCount {
|
||
return resp, nil
|
||
}
|
||
list, err := findHomePlaylets(ctx, filter, playletOperateListSort(filterType), pageSize, options.Find().SetSkip(skip))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
themeNameMap, err := buildHomeThemeNameMap(ctx, list)
|
||
if err != nil {
|
||
log.Error("查询运营类型短剧题材名称失败", log.E(err))
|
||
return nil, err
|
||
}
|
||
tagNameMap, err := buildHomeTagNameMap(ctx, list)
|
||
if err != nil {
|
||
log.Error("查询运营类型短剧标签名称失败", log.E(err))
|
||
return nil, err
|
||
}
|
||
resp.List = commonDataValues(playletsToHomeCommonData(ctx, list, themeNameMap, tagNameMap))
|
||
return resp, nil
|
||
}
|
||
|
||
// TagVideoList 根据标签ID分页查询app端短剧列表
|
||
func TagVideoList(ctx context.Context, req *proto.PlayletTagVideoReq) (*proto.PlayletTagVideoResp, *srvdef.SrvErr) {
|
||
if req == nil {
|
||
return nil, srvdef.New(srvdef.ErrParamError, "标签ID不能为空", "标签ID不能为空")
|
||
}
|
||
tagID, err := resolveTagID(ctx, req.TagId, req.TagSlug)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
req.TagId = tagID
|
||
pageNum, pageSize := normalizePlayletPage(req.PageNum, req.PageSize)
|
||
filter := homeUpPlayletFilter(bson.M{
|
||
playletmod.FN.Playlet.TagIds: req.TagId,
|
||
})
|
||
totalCount, err := playletmod.CountPlaylet(ctx, filter)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
resp := &proto.PlayletTagVideoResp{
|
||
TotalCount: totalCount,
|
||
PageSize: int(pageSize),
|
||
PageNum: int(pageNum),
|
||
List: make([]proto.CommonData, 0),
|
||
}
|
||
resp.SeoInfo = buildPlayletTagPageSeoInfo(req.TagId)
|
||
if totalCount == 0 {
|
||
return resp, nil
|
||
}
|
||
skip := (pageNum - 1) * pageSize
|
||
if skip >= totalCount {
|
||
return resp, nil
|
||
}
|
||
list, err := findHomePlaylets(ctx, filter, playletTodayUpdateSort(), pageSize, options.Find().SetSkip(skip))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
themeNameMap, err := buildHomeThemeNameMap(ctx, list)
|
||
if err != nil {
|
||
log.Error("查询标签短剧题材名称失败", log.E(err))
|
||
return nil, err
|
||
}
|
||
tagNameMap, err := buildHomeTagNameMap(ctx, list)
|
||
if err != nil {
|
||
log.Error("查询标签短剧标签名称失败", log.E(err))
|
||
return nil, err
|
||
}
|
||
resp.List = commonDataValues(playletsToHomeCommonData(ctx, list, themeNameMap, tagNameMap))
|
||
return resp, nil
|
||
}
|
||
|
||
// Search 使用ES分页搜索app端短剧
|
||
func Search(ctx context.Context, req *proto.PlayletSearchReq) (*proto.PlayletSearchResp, *srvdef.SrvErr) {
|
||
// 校验搜索关键词,避免空关键词扫全量ES索引。
|
||
if req == nil || strings.TrimSpace(req.Keyword) == "" {
|
||
return nil, srvdef.New(srvdef.ErrParamError, "搜索关键词不能为空", "搜索关键词不能为空")
|
||
}
|
||
if esmanager.Instance == nil {
|
||
log.Error("短剧搜索ES未初始化")
|
||
return nil, srvdef.New(srvdef.ErrESSearchError, "", "")
|
||
}
|
||
pageNum, pageSize := normalizePlayletPage(req.PageNum, req.PageSize)
|
||
keyword := strings.TrimSpace(req.Keyword)
|
||
if pageNum == 1 {
|
||
recordHotSearch(keyword)
|
||
}
|
||
query := playletSearchQuery(keyword, (pageNum-1)*pageSize, pageSize)
|
||
var hits esmod.ESPlayletHits
|
||
if err := esmanager.Instance.Search(esmod.GetPlayletIndex(), &hits, query); err != nil {
|
||
if strings.Contains(err.Error(), "index_not_found_exception") {
|
||
log.Warn("短剧ES索引不存在,返回空搜索结果", log.Any("关键词", keyword), log.Any("索引", esmod.GetPlayletIndex()))
|
||
return emptyPlayletSearchResp(pageNum, pageSize), nil
|
||
}
|
||
log.Error("短剧ES搜索失败", log.Any("关键词", keyword), log.E(err))
|
||
return nil, srvdef.New(srvdef.ErrESSearchError, "", "")
|
||
}
|
||
resp := &proto.PlayletSearchResp{
|
||
TotalCount: hits.Total.Value,
|
||
PageSize: int(pageSize),
|
||
PageNum: int(pageNum),
|
||
List: make([]proto.CommonData, 0, len(hits.Hits)),
|
||
}
|
||
for _, hit := range hits.Hits {
|
||
if hit == nil || hit.Source == nil {
|
||
continue
|
||
}
|
||
resp.List = append(resp.List, esPlayletToCommonData(ctx, hit.Source))
|
||
}
|
||
return resp, nil
|
||
}
|
||
|
||
func recordHotSearch(keyword string) {
|
||
keyword = hotsearchmod.NormalizeKeyword(keyword)
|
||
if keyword == "" {
|
||
return
|
||
}
|
||
go func(word string) {
|
||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||
defer cancel()
|
||
if err := hotsearchmod.IncrByKeyword(ctx, word); err != nil {
|
||
log.Warn("记录热搜词失败", log.Any("关键词", word), log.E(err))
|
||
}
|
||
}(keyword)
|
||
}
|
||
|
||
// HotLike 返回缓存的热搜词和猜你喜欢。
|
||
func HotLike(ctx context.Context) *proto.PlayletHotLikeResp {
|
||
hotSearch := playletcache.ListHotSearch()
|
||
resp := &proto.PlayletHotLikeResp{
|
||
HotSearch: make([]proto.PlayletHotSearchItem, 0, len(hotSearch)),
|
||
GuessLike: guessLikeCommonData(ctx),
|
||
}
|
||
for _, item := range hotSearch {
|
||
resp.HotSearch = append(resp.HotSearch, proto.PlayletHotSearchItem{
|
||
Keyword: item.Keyword,
|
||
Count: item.Count,
|
||
})
|
||
}
|
||
return resp
|
||
}
|
||
|
||
// SetLike 设置当前用户短剧点赞状态
|
||
func SetLike(ctx context.Context, userId int64, req *proto.PlayletLikeSetReq) (*proto.PlayletLikeSetResp, *srvdef.SrvErr) {
|
||
// 使用指针布尔值校验是否传参,保证 false 能表示取消点赞。
|
||
if req == nil || req.Id <= 0 || req.IsLike == nil {
|
||
return nil, srvdef.New(srvdef.ErrParamError, "参数错误", "参数错误")
|
||
}
|
||
playlet, changed, err := setPlayletLike(ctx, userId, req.Id, *req.IsLike)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
countLike := displayPlayletCountLike(applyLocalCounter(playlet.CountLike, *req.IsLike, changed), playlet.VirtualCountLike)
|
||
resp := &proto.PlayletLikeSetResp{
|
||
Id: req.Id,
|
||
IsLike: operateStatusByFlag(*req.IsLike),
|
||
CountLike: countLike,
|
||
}
|
||
return resp, nil
|
||
}
|
||
|
||
// SetCollect 设置当前用户短剧收藏状态
|
||
func SetCollect(ctx context.Context, userId int64, req *proto.PlayletCollectSetReq) (*proto.PlayletCollectSetResp, *srvdef.SrvErr) {
|
||
// 使用指针布尔值校验是否传参,保证 false 能表示取消收藏。
|
||
if req == nil || req.Id <= 0 || req.IsCollect == nil {
|
||
return nil, srvdef.New(srvdef.ErrParamError, "参数错误", "参数错误")
|
||
}
|
||
playlet, changed, err := setPlayletCollect(ctx, userId, req.Id, *req.IsCollect)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
countCollect := applyLocalCounter(playlet.CountCollect, *req.IsCollect, changed)
|
||
resp := &proto.PlayletCollectSetResp{
|
||
Id: req.Id,
|
||
IsCollect: operateStatusByFlag(*req.IsCollect),
|
||
CountCollect: countCollect,
|
||
}
|
||
return resp, nil
|
||
}
|
||
|
||
// ForwardHeat 短剧转发增加热度值
|
||
func ForwardHeat(ctx context.Context, userId int64, ip string, req *proto.PlayletForwardHeatReq) (*proto.PlayletForwardHeatResp, *srvdef.SrvErr) {
|
||
if req == nil || req.Id <= 0 {
|
||
return nil, srvdef.New(srvdef.ErrParamError, "短剧ID不能为空", "短剧ID不能为空")
|
||
}
|
||
limited, limitErr := checkPlayletForwardHeatLimit(userId, ip, req.Id)
|
||
if limitErr != nil {
|
||
log.Error("短剧转发热度频控失败", log.Any("用户ID", userId), log.Any("IP", ip), log.Any("短剧ID", req.Id), log.E(limitErr))
|
||
}
|
||
if limited {
|
||
playlet, srvErr := findPlayletForwardHeatTarget(ctx, req.Id)
|
||
if srvErr != nil {
|
||
return nil, srvErr
|
||
}
|
||
return &proto.PlayletForwardHeatResp{
|
||
Id: playlet.Id,
|
||
HeatValue: playlet.HeatValue,
|
||
}, nil
|
||
}
|
||
playlet, srvErr := incrementPlayletHeatValueAndReturn(ctx, req.Id, playletHeatForwardInc)
|
||
if srvErr != nil {
|
||
log.Error("短剧转发增加热度失败", log.Any("短剧ID", req.Id), log.E(srvErr))
|
||
return nil, srvErr
|
||
}
|
||
return &proto.PlayletForwardHeatResp{
|
||
Id: playlet.Id,
|
||
HeatValue: playlet.HeatValue,
|
||
}, nil
|
||
}
|
||
|
||
// SetChapterLike 设置当前用户短剧剧集点赞状态
|
||
func SetChapterLike(ctx context.Context, userId int64, req *proto.PlayletChapterLikeSetReq) (*proto.PlayletChapterLikeSetResp, *srvdef.SrvErr) {
|
||
// 使用指针布尔值校验是否传参,保证 false 能表示取消点赞。
|
||
if req == nil || req.ChapterId <= 0 || req.IsLike == nil {
|
||
return nil, srvdef.New(srvdef.ErrParamError, "参数错误", "参数错误")
|
||
}
|
||
chapter, changed, err := setPlayletChapterLike(ctx, userId, req.ChapterId, *req.IsLike)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
countLike := applyLocalCounter(chapter.CountLike, *req.IsLike, changed)
|
||
resp := &proto.PlayletChapterLikeSetResp{
|
||
ChapterId: req.ChapterId,
|
||
IsLike: operateStatusByFlag(*req.IsLike),
|
||
CountLike: countLike,
|
||
}
|
||
return resp, nil
|
||
}
|
||
|
||
// SetChapterCollect 设置当前用户短剧剧集收藏状态
|
||
func SetChapterCollect(ctx context.Context, userId int64, req *proto.PlayletChapterCollectSetReq) (*proto.PlayletChapterCollectSetResp, *srvdef.SrvErr) {
|
||
// 使用指针布尔值校验是否传参,保证 false 能表示取消收藏。
|
||
if req == nil || req.ChapterId <= 0 || req.IsCollect == nil {
|
||
return nil, srvdef.New(srvdef.ErrParamError, "参数错误", "参数错误")
|
||
}
|
||
chapter, changed, err := setPlayletChapterCollect(ctx, userId, req.ChapterId, *req.IsCollect)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
countCollect := applyLocalCounter(chapter.CountCollect, *req.IsCollect, changed)
|
||
resp := &proto.PlayletChapterCollectSetResp{
|
||
ChapterId: req.ChapterId,
|
||
IsCollect: operateStatusByFlag(*req.IsCollect),
|
||
CountCollect: countCollect,
|
||
}
|
||
return resp, nil
|
||
}
|
||
|
||
// ReportChapterWatch 上报当前用户短剧剧集观看进度
|
||
func ReportChapterWatch(ctx context.Context, userId int64, req *proto.PlayletChapterWatchReportReq) (*proto.PlayletChapterWatchReportResp, *srvdef.SrvErr) {
|
||
if userId <= 0 {
|
||
return nil, srvdef.New(srvdef.InvalidToken, "请先登录", "请先登录")
|
||
}
|
||
if req == nil || req.ChapterId <= 0 {
|
||
return nil, srvdef.New(srvdef.ErrParamError, "参数错误", "参数错误")
|
||
}
|
||
filter := bson.M{
|
||
playletchaptermod.FN.PlayletChapter.Id: req.ChapterId,
|
||
}
|
||
if req.PlayletId > 0 {
|
||
filter[playletchaptermod.FN.PlayletChapter.PlayletId] = req.PlayletId
|
||
}
|
||
chapter, err := playletchaptermod.FindOnePlayletChapter(ctx, filter, options.FindOne().SetProjection(playletChapterWatchReportProjection))
|
||
if err != nil {
|
||
log.Error("查询短剧观看上报剧集失败", log.Any("用户ID", userId), log.Any("短剧ID", req.PlayletId), log.Any("剧集ID", req.ChapterId), log.E(err))
|
||
return nil, err
|
||
}
|
||
if chapter.CurrentEpisode <= 0 {
|
||
return nil, srvdef.New(srvdef.ErrParamError, "剧集集数无效", "剧集集数无效")
|
||
}
|
||
playlet, err := playletmod.FindOnePlaylet(ctx, homeUpPlayletFilter(bson.M{
|
||
playletmod.FN.Playlet.Id: chapter.PlayletId,
|
||
}), options.FindOne().SetProjection(playletWatchReportProjection))
|
||
if err != nil {
|
||
log.Error("查询短剧观看上报所属短剧失败", log.Any("用户ID", userId), log.Any("短剧ID", chapter.PlayletId), log.Any("剧集ID", req.ChapterId), log.E(err))
|
||
return nil, err
|
||
}
|
||
progress, err := playletwatchprogressmod.SavePlayletWatchProgress(ctx, userId, chapter.PlayletId, chapter.Id, chapter.CurrentEpisode, true)
|
||
if err != nil {
|
||
log.Error("保存短剧观看上报进度失败", log.Any("用户ID", userId), log.Any("短剧ID", chapter.PlayletId), log.Any("剧集ID", chapter.Id), log.E(err))
|
||
return nil, err
|
||
}
|
||
if err = playletwatchhistorymod.SavePlayletWatchHistory(ctx, userId, chapter.PlayletId, chapter.Id, chapter.CurrentEpisode); err != nil {
|
||
log.Error("保存短剧观看历史失败", log.Any("用户ID", userId), log.Any("短剧ID", chapter.PlayletId), log.Any("剧集ID", chapter.Id), log.E(err))
|
||
return nil, err
|
||
}
|
||
watchedCount := collectWatchedCount(progress, playlet.TotalEpisode)
|
||
return &proto.PlayletChapterWatchReportResp{
|
||
PlayletId: chapter.PlayletId,
|
||
ChapterId: chapter.Id,
|
||
CurrentEpisode: chapter.CurrentEpisode,
|
||
WatchedCount: watchedCount,
|
||
UnwatchedCount: collectUnwatchedCount(playlet.TotalEpisode, watchedCount),
|
||
LastChapterId: collectLastChapterId(progress),
|
||
LastEpisode: collectLastEpisode(progress),
|
||
}, nil
|
||
}
|
||
|
||
// ChapterDetail 根据ID查询app端短剧剧集详情
|
||
func ChapterDetail(ctx context.Context, userId int64, req *proto.PlayletChapterInfoReq) (*proto.PlayletChapterInfoResp, *srvdef.SrvErr) {
|
||
// 播放页切换剧集只查询当前剧集和所属短剧状态,不重复查询整部短剧详情、相关推荐和全部剧集列表。
|
||
if req == nil || req.ChapterId <= 0 {
|
||
return nil, srvdef.New(srvdef.ErrParamError, "短剧剧集ID不能为空", "短剧剧集ID不能为空")
|
||
}
|
||
playletID, err := resolvePlayletID(ctx, req.PlayletId, req.PlayletSlug, false)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
req.PlayletId = playletID
|
||
filter := bson.M{
|
||
playletchaptermod.FN.PlayletChapter.Id: req.ChapterId,
|
||
}
|
||
if req.PlayletId > 0 {
|
||
filter[playletchaptermod.FN.PlayletChapter.PlayletId] = req.PlayletId
|
||
}
|
||
chapter, err := playletchaptermod.FindOnePlayletChapter(ctx, filter, options.FindOne().SetProjection(playletChapterDetailProjection))
|
||
if err != nil {
|
||
log.Error("查询短剧剧集详情失败", log.Any("短剧ID", req.PlayletId), log.Any("剧集ID", req.ChapterId), log.E(err))
|
||
return nil, err
|
||
}
|
||
playlet, err := playletmod.FindOnePlaylet(ctx, homeUpPlayletFilter(bson.M{
|
||
playletmod.FN.Playlet.Id: chapter.PlayletId,
|
||
}), options.FindOne().SetProjection(playletChapterSEOProjection))
|
||
if err != nil {
|
||
log.Error("查询短剧剧集详情所属短剧失败", log.Any("短剧ID", chapter.PlayletId), log.Any("剧集ID", chapter.Id), log.E(err))
|
||
return nil, err
|
||
}
|
||
if err = ensureOriginalChapterWatermarkReady(playlet, chapter); err != nil {
|
||
return nil, err
|
||
}
|
||
isLike, isCollect, err := findPlayletChapterUserOperateStatus(ctx, userId, chapter.Id)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
resp := playletChapterToInfoResp(chapter)
|
||
resp.IsLike = isLike
|
||
resp.IsCollect = isCollect
|
||
resp.SeoInfo = buildPlayletChapterDetailSeoInfo(playlet, chapter)
|
||
if err = applyPlayletChapterWatchPermission(ctx, userId, chapter, resp); err != nil {
|
||
return nil, err
|
||
}
|
||
if !resp.CanWatch {
|
||
resp.VideoUrl = ""
|
||
return resp, nil
|
||
}
|
||
if err = incrementPlayletChapterBrowse(ctx, chapter.Id); err != nil {
|
||
log.Error("增加短剧剧集观看量失败", log.Any("剧集ID", chapter.Id), log.E(err))
|
||
return nil, err
|
||
}
|
||
resp.CountBrowse++
|
||
if resp.VideoUrl != "" {
|
||
resp.VideoUrl = videoser.GetPlayletM3U8AuthParam(
|
||
chapter.Id,
|
||
resp.VideoUrl,
|
||
videoser.H5M3U8RequestIP(ctx),
|
||
strings.TrimSpace(chapter.WatermarkVideoUrl) == "",
|
||
userId,
|
||
)
|
||
}
|
||
return resp, nil
|
||
}
|
||
|
||
// ChapterList 根据短剧ID查询app端短剧剧集列表
|
||
func ChapterList(ctx context.Context, req *proto.PlayletChapterListReq) (*proto.PlayletChapterListResp, *srvdef.SrvErr) {
|
||
if req == nil {
|
||
return nil, srvdef.New(srvdef.ErrParamError, "短剧ID不能为空", "短剧ID不能为空")
|
||
}
|
||
playletID, err := resolvePlayletID(ctx, req.PlayletId, req.PlayletSlug, true)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
req.PlayletId = playletID
|
||
playlet, err := playletmod.FindOnePlaylet(ctx, homeUpPlayletFilter(bson.M{
|
||
playletmod.FN.Playlet.Id: req.PlayletId,
|
||
}), options.FindOne().SetProjection(bson.D{
|
||
{Key: playletmod.FN.Playlet.Id, Value: 1},
|
||
{Key: playletmod.FN.Playlet.IsOriginal, Value: 1},
|
||
}))
|
||
if err != nil {
|
||
log.Error("查询短剧剧集列表所属短剧失败", log.Any("短剧ID", req.PlayletId), log.E(err))
|
||
return nil, err
|
||
}
|
||
chapterInfo, err := findPlayletChapters(ctx, req.PlayletId, 0, playlet != nil && playlet.IsOriginal)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return &proto.PlayletChapterListResp{
|
||
PlayletId: req.PlayletId,
|
||
List: chapterInfo,
|
||
}, nil
|
||
}
|
||
|
||
// UnlockChapter 使用金币逐集解锁短剧剧集。
|
||
//
|
||
// 幂等性约束:
|
||
// 1. 事务前先查一次已解锁记录,命中时直接返回成功;
|
||
// 2. 事务内先插入 userId+chapterId 唯一解锁记录,再扣金币;
|
||
// 3. 并发重复请求只有一个能插入成功,其他请求按已解锁成功处理,不重复扣费。
|
||
func UnlockChapter(ctx context.Context, userId int64, req *proto.PlayletChapterUnlockReq) (*proto.PlayletChapterUnlockResp, *srvdef.SrvErr) {
|
||
if userId <= 0 {
|
||
return nil, srvdef.New(srvdef.InvalidToken, "请先登录", "请先登录")
|
||
}
|
||
if req == nil || req.PlayletId <= 0 || req.ChapterId <= 0 {
|
||
return nil, srvdef.New(srvdef.ErrParamError, "短剧ID和剧集ID不能为空", "短剧ID和剧集ID不能为空")
|
||
}
|
||
chapter, playlet, err := findPlayletChapterForUnlock(ctx, req.PlayletId, req.ChapterId)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if chapter.PayType == playletchaptermod.PayTypeFree {
|
||
user, err := findPlayletChapterAccessUser(ctx, userId)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return &proto.PlayletChapterUnlockResp{
|
||
PlayletId: chapter.PlayletId,
|
||
ChapterId: chapter.Id,
|
||
Unlocked: true,
|
||
CanWatch: true,
|
||
Balance: user.Recharge,
|
||
CostCoins: 0,
|
||
ChangeCoin: 0,
|
||
}, nil
|
||
}
|
||
if chapter.PayType == playletchaptermod.PayTypeVip {
|
||
return nil, srvdef.New(srvdef.ErrParamError, "VIP剧集需开通会员后观看", "VIP剧集需开通会员后观看")
|
||
}
|
||
if chapter.PayType != playletchaptermod.PayTypeCoin {
|
||
return nil, srvdef.New(srvdef.ErrParamError, "剧集支付方式无效", "剧集支付方式无效")
|
||
}
|
||
if chapter.SingleCoins <= 0 {
|
||
return nil, srvdef.New(srvdef.ErrParamError, "剧集金币价格无效", "剧集金币价格无效")
|
||
}
|
||
|
||
unlocked, err := playletchapterunlockmod.ExistsPlayletChapterUnlock(ctx, userId, chapter.Id)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
user, err := findPlayletChapterAccessUser(ctx, userId)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if unlocked {
|
||
return playletChapterUnlockResp(chapter, user.Recharge, 0), nil
|
||
}
|
||
if user.Recharge < chapter.SingleCoins {
|
||
return nil, srvdef.New(srvdef.UserNotEnoughMoney, "金币不足,请先充值", "金币不足,请先充值")
|
||
}
|
||
|
||
orderNo := playletChapterUnlockOrderNo(userId, chapter.Id)
|
||
var txSrvErr *srvdef.SrvErr
|
||
var balanceAfter int64
|
||
txErr := mongos.Shop.Transaction(ctx, func(sctx mongo.SessionContext) error {
|
||
inserted, srvErr := playletchapterunlockmod.InsertOnePlayletChapterUnlock(sctx, &playletchapterunlockmod.PlayletChapterUnlock{
|
||
UserId: userId,
|
||
PlayletId: chapter.PlayletId,
|
||
ChapterId: chapter.Id,
|
||
Coins: chapter.SingleCoins,
|
||
OrderNo: orderNo,
|
||
})
|
||
if srvErr != nil {
|
||
txSrvErr = srvErr
|
||
return srvErr
|
||
}
|
||
if !inserted {
|
||
return errPlayletChapterAlreadyUnlocked
|
||
}
|
||
|
||
updatedUser, srvErr := usermod.UserPay(sctx, userId, chapter.SingleCoins)
|
||
if srvErr != nil {
|
||
txSrvErr = srvdef.New(srvdef.UserNotEnoughMoney, "金币不足,请先充值", "金币不足,请先充值")
|
||
return txSrvErr
|
||
}
|
||
balanceAfter = updatedUser.Recharge
|
||
|
||
srvErr = payment.WriteCoinTrans(sctx, payment.WriteCoinTransOpt{
|
||
UserId: userId,
|
||
UserName: updatedUser.NickName,
|
||
FlowDir: cointransmod.FlowOut,
|
||
TranType: cointransmod.TranTypeVideoUnlock,
|
||
Amount: chapter.SingleCoins,
|
||
OrderNo: orderNo,
|
||
RelatedId: chapter.Id,
|
||
Remark: fmt.Sprintf("解锁《%s》", playlet.Title),
|
||
}, balanceAfter)
|
||
if srvErr != nil {
|
||
txSrvErr = srvErr
|
||
return srvErr
|
||
}
|
||
return nil
|
||
})
|
||
if txErr != nil {
|
||
if errors.Is(txErr, errPlayletChapterAlreadyUnlocked) {
|
||
user, err = findPlayletChapterAccessUser(ctx, userId)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return playletChapterUnlockResp(chapter, user.Recharge, 0), nil
|
||
}
|
||
if txSrvErr != nil {
|
||
return nil, txSrvErr
|
||
}
|
||
log.Error("短剧剧集金币解锁事务失败", log.Any("userId", userId), log.Any("chapterId", chapter.Id), log.E(txErr))
|
||
return nil, srvdef.New(srvdef.DBTransaction, "解锁失败,请稍后重试", "解锁失败,请稍后重试")
|
||
}
|
||
updatePlayletChapterUnlockStatsAsync(chapter.PlayletId, chapter.Id)
|
||
return playletChapterUnlockResp(chapter, balanceAfter, -chapter.SingleCoins), nil
|
||
}
|
||
|
||
// updatePlayletChapterUnlockStatsAsync 异步更新剧集购买数和短剧热度
|
||
func updatePlayletChapterUnlockStatsAsync(playletId int64, chapterId int64) {
|
||
if playletId <= 0 || chapterId <= 0 {
|
||
return
|
||
}
|
||
go func() {
|
||
if err := playletchaptermod.UpdateOnePlayletChapter(context.Background(), bson.M{
|
||
playletchaptermod.FN.PlayletChapter.Id: chapterId,
|
||
}, bson.M{
|
||
"$inc": bson.M{playletchaptermod.FN.PlayletChapter.CountBuy: 1},
|
||
}); err != nil {
|
||
log.Error("短剧剧集解锁后更新购买数失败", log.Any("短剧ID", playletId), log.Any("剧集ID", chapterId), log.E(err))
|
||
}
|
||
if err := incrementPlayletHeatValue(context.Background(), playletId, playletHeatBuyInc); err != nil {
|
||
log.Error("短剧剧集解锁后更新短剧热度失败", log.Any("短剧ID", playletId), log.Any("剧集ID", chapterId), log.E(err))
|
||
return
|
||
}
|
||
syncPlayletToESAsync(playletId, "短剧剧集解锁后同步ES失败")
|
||
}()
|
||
}
|
||
|
||
// applyPlayletChapterWatchPermission 根据剧集支付方式填充播放权限,并在未授权时清空播放地址。
|
||
func applyPlayletChapterWatchPermission(ctx context.Context,
|
||
userId int64,
|
||
chapter *playletchaptermod.PlayletChapter,
|
||
resp *proto.PlayletChapterInfoResp,
|
||
) *srvdef.SrvErr {
|
||
if chapter == nil || resp == nil {
|
||
return srvdef.New(srvdef.ErrNoData, "剧集不存在", "剧集不存在")
|
||
}
|
||
resp.CostCoins = chapter.SingleCoins
|
||
resp.CanWatch = false
|
||
resp.NeedUnlock = false
|
||
resp.NeedVip = false
|
||
resp.VipValid = false
|
||
resp.IsUnlocked = false
|
||
|
||
if chapter.PayType == playletchaptermod.PayTypeFree {
|
||
resp.CanWatch = true
|
||
return nil
|
||
}
|
||
|
||
var user *usermod.User
|
||
if userId > 0 {
|
||
var err *srvdef.SrvErr
|
||
user, err = findPlayletChapterAccessUser(ctx, userId)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
resp.Balance = user.Recharge
|
||
resp.VipValid = isUserVipValid(user)
|
||
}
|
||
|
||
switch chapter.PayType {
|
||
case playletchaptermod.PayTypeCoin:
|
||
if userId <= 0 {
|
||
resp.NeedUnlock = true
|
||
return nil
|
||
}
|
||
unlocked, err := playletchapterunlockmod.ExistsPlayletChapterUnlock(ctx, userId, chapter.Id)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
resp.IsUnlocked = unlocked
|
||
resp.CanWatch = unlocked
|
||
resp.NeedUnlock = !unlocked
|
||
case playletchaptermod.PayTypeVip:
|
||
resp.CanWatch = resp.VipValid
|
||
resp.NeedVip = !resp.VipValid
|
||
default:
|
||
log.Warn("未知短剧剧集支付方式", log.Any("playletId", chapter.PlayletId), log.Any("chapterId", chapter.Id), log.Any("payType", chapter.PayType))
|
||
resp.NeedUnlock = true
|
||
}
|
||
|
||
if !resp.CanWatch {
|
||
resp.VideoUrl = ""
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// findPlayletChapterForUnlock 查询解锁所需的剧集和所属短剧,限制短剧必须已上架。
|
||
func findPlayletChapterForUnlock(ctx context.Context, playletId int64, chapterId int64) (*playletchaptermod.PlayletChapter, *playletmod.Playlet, *srvdef.SrvErr) {
|
||
chapter, err := playletchaptermod.FindOnePlayletChapter(ctx, bson.M{
|
||
playletchaptermod.FN.PlayletChapter.Id: chapterId,
|
||
playletchaptermod.FN.PlayletChapter.PlayletId: playletId,
|
||
}, options.FindOne().SetProjection(playletChapterDetailProjection))
|
||
if err != nil {
|
||
log.Error("查询待解锁短剧剧集失败", log.Any("playletId", playletId), log.Any("chapterId", chapterId), log.E(err))
|
||
return nil, nil, err
|
||
}
|
||
playlet, err := playletmod.FindOnePlaylet(ctx, homeUpPlayletFilter(bson.M{
|
||
playletmod.FN.Playlet.Id: chapter.PlayletId,
|
||
}), options.FindOne().SetProjection(playletChapterSEOProjection))
|
||
if err != nil {
|
||
log.Error("查询待解锁剧集所属短剧失败", log.Any("playletId", chapter.PlayletId), log.Any("chapterId", chapter.Id), log.E(err))
|
||
return nil, nil, err
|
||
}
|
||
if err = ensureOriginalChapterWatermarkReady(playlet, chapter); err != nil {
|
||
return nil, nil, err
|
||
}
|
||
return chapter, playlet, nil
|
||
}
|
||
|
||
// findPlayletChapterAccessUser 查询播放权限判断需要的用户字段,避免读取完整用户文档。
|
||
func findPlayletChapterAccessUser(ctx context.Context, userId int64) (*usermod.User, *srvdef.SrvErr) {
|
||
user, err := usermod.FindOneUser(ctx, bson.M{
|
||
usermod.FN.User.Id: userId,
|
||
}, options.FindOne().SetProjection(bson.D{
|
||
{Key: usermod.FN.User.Id, Value: 1},
|
||
{Key: usermod.FN.User.NickName, Value: 1},
|
||
{Key: usermod.FN.User.Recharge, Value: 1},
|
||
{Key: usermod.FN.User.CurrentVipExpire, Value: 1},
|
||
}))
|
||
if err != nil || user == nil {
|
||
return nil, srvdef.New(srvdef.ErrUserNotExsit, "", "")
|
||
}
|
||
return user, nil
|
||
}
|
||
|
||
func isUserVipValid(user *usermod.User) bool {
|
||
return user != nil && user.CurrentVipExpire.After(time.Now())
|
||
}
|
||
|
||
func playletChapterUnlockOrderNo(userId int64, chapterId int64) string {
|
||
return fmt.Sprintf("JJS-%d-%d-%d", userId, chapterId, time.Now().UnixNano())
|
||
}
|
||
|
||
func playletChapterUnlockResp(chapter *playletchaptermod.PlayletChapter, balance int64, changeCoin int64) *proto.PlayletChapterUnlockResp {
|
||
return &proto.PlayletChapterUnlockResp{
|
||
PlayletId: chapter.PlayletId,
|
||
ChapterId: chapter.Id,
|
||
Unlocked: true,
|
||
CanWatch: true,
|
||
Balance: balance,
|
||
CostCoins: chapter.SingleCoins,
|
||
ChangeCoin: changeCoin,
|
||
}
|
||
}
|
||
|
||
// setPlayletLike 设置短剧点赞关系
|
||
func setPlayletLike(ctx context.Context, userId int64, playletId int64, enabled bool) (*playletmod.Playlet, bool, *srvdef.SrvErr) {
|
||
// 操作接口必须登录,先查询短剧确保目标存在且已上架,再按点赞表真实变更决定是否更新点赞数。
|
||
if userId <= 0 {
|
||
return nil, false, srvdef.New(srvdef.InvalidToken, "请先登录", "请先登录")
|
||
}
|
||
playlet, err := findPlayletOperateTarget(ctx, userId, playletId)
|
||
if err != nil {
|
||
return nil, false, err
|
||
}
|
||
filter := bson.M{
|
||
playletlikemod.FN.PlayletLike.UserId: userId,
|
||
playletlikemod.FN.PlayletLike.PlayletId: playletId,
|
||
}
|
||
changed := false
|
||
if enabled {
|
||
inserted, err := playletlikemod.InsertOnePlayletLike(ctx, &playletlikemod.PlayletLike{
|
||
UserId: userId,
|
||
PlayletId: playletId,
|
||
})
|
||
if err != nil {
|
||
log.Error("新增短剧点赞关系失败", log.Any("用户ID", userId), log.Any("短剧ID", playletId), log.E(err))
|
||
return nil, false, err
|
||
}
|
||
changed = inserted
|
||
} else {
|
||
deleted, err := playletlikemod.DeleteOnePlayletLike(ctx, filter)
|
||
if err != nil {
|
||
log.Error("删除短剧点赞关系失败", log.Any("用户ID", userId), log.Any("短剧ID", playletId), log.E(err))
|
||
return nil, false, err
|
||
}
|
||
changed = deleted > 0
|
||
}
|
||
if changed {
|
||
delta := int64(1)
|
||
if !enabled {
|
||
delta = -1
|
||
}
|
||
heatDelta := playletHeatLikeInc
|
||
if !enabled {
|
||
heatDelta = -playletHeatLikeInc
|
||
}
|
||
if err := incrementPlayletOperateCounter(ctx, playletId, playletmod.FN.Playlet.CountLike, delta, heatDelta); err != nil {
|
||
rollbackPlayletLike(ctx, userId, playletId, enabled)
|
||
return nil, false, err
|
||
}
|
||
// 真实点赞数和热度值已变更,异步同步 ES,保证搜索和榜单数据最终一致
|
||
syncPlayletToESAsync(playletId, "短剧点赞状态同步ES失败")
|
||
}
|
||
return playlet, changed, nil
|
||
}
|
||
|
||
// setPlayletCollect 设置短剧收藏关系
|
||
func setPlayletCollect(ctx context.Context, userId int64, playletId int64, enabled bool) (*playletmod.Playlet, bool, *srvdef.SrvErr) {
|
||
// 操作接口必须登录,先查询短剧确保目标存在且已上架,再按收藏表真实变更决定是否更新收藏数。
|
||
if userId <= 0 {
|
||
return nil, false, srvdef.New(srvdef.InvalidToken, "请先登录", "请先登录")
|
||
}
|
||
playlet, err := findPlayletOperateTarget(ctx, userId, playletId)
|
||
if err != nil {
|
||
return nil, false, err
|
||
}
|
||
filter := bson.M{
|
||
playletcollectmod.FN.PlayletCollect.UserId: userId,
|
||
playletcollectmod.FN.PlayletCollect.PlayletId: playletId,
|
||
}
|
||
changed := false
|
||
if enabled {
|
||
inserted, err := playletcollectmod.InsertOnePlayletCollect(ctx, &playletcollectmod.PlayletCollect{
|
||
UserId: userId,
|
||
PlayletId: playletId,
|
||
})
|
||
if err != nil {
|
||
log.Error("新增短剧收藏关系失败", log.Any("用户ID", userId), log.Any("短剧ID", playletId), log.E(err))
|
||
return nil, false, err
|
||
}
|
||
changed = inserted
|
||
} else {
|
||
deleted, err := playletcollectmod.DeleteOnePlayletCollect(ctx, filter)
|
||
if err != nil {
|
||
log.Error("删除短剧收藏关系失败", log.Any("用户ID", userId), log.Any("短剧ID", playletId), log.E(err))
|
||
return nil, false, err
|
||
}
|
||
changed = deleted > 0
|
||
}
|
||
if changed {
|
||
delta := int64(1)
|
||
if !enabled {
|
||
delta = -1
|
||
}
|
||
heatDelta := playletHeatCollectInc
|
||
if !enabled {
|
||
heatDelta = -playletHeatCollectInc
|
||
}
|
||
if err := incrementPlayletOperateCounter(ctx, playletId, playletmod.FN.Playlet.CountCollect, delta, heatDelta); err != nil {
|
||
rollbackPlayletCollect(ctx, userId, playletId, enabled)
|
||
return nil, false, err
|
||
}
|
||
// 真实收藏数和热度值已变更,异步同步 ES,保证追剧榜和搜索数据最终一致
|
||
syncPlayletToESAsync(playletId, "短剧收藏状态同步ES失败")
|
||
}
|
||
return playlet, changed, nil
|
||
}
|
||
|
||
// setPlayletChapterLike 设置短剧剧集点赞关系
|
||
func setPlayletChapterLike(ctx context.Context, userId int64, chapterId int64, enabled bool) (*playletchaptermod.PlayletChapter, bool, *srvdef.SrvErr) {
|
||
// 剧集点赞必须登录,先校验剧集存在且所属短剧已上架,再按点赞关系真实变化更新 playlet_chapter.countLike。
|
||
if userId <= 0 {
|
||
return nil, false, srvdef.New(srvdef.InvalidToken, "请先登录", "请先登录")
|
||
}
|
||
chapter, err := findPlayletChapterOperateTarget(ctx, userId, chapterId)
|
||
if err != nil {
|
||
return nil, false, err
|
||
}
|
||
filter := bson.M{
|
||
playletchapterlikemod.FN.PlayletChapterLike.UserId: userId,
|
||
playletchapterlikemod.FN.PlayletChapterLike.ChapterId: chapterId,
|
||
}
|
||
changed := false
|
||
if enabled {
|
||
inserted, err := playletchapterlikemod.InsertOnePlayletChapterLike(ctx, &playletchapterlikemod.PlayletChapterLike{
|
||
UserId: userId,
|
||
PlayletId: chapter.PlayletId,
|
||
ChapterId: chapterId,
|
||
})
|
||
if err != nil {
|
||
log.Error("新增短剧剧集点赞关系失败", log.Any("用户ID", userId), log.Any("短剧ID", chapter.PlayletId), log.Any("剧集ID", chapterId), log.E(err))
|
||
return nil, false, err
|
||
}
|
||
changed = inserted
|
||
} else {
|
||
deleted, err := playletchapterlikemod.DeleteOnePlayletChapterLike(ctx, filter)
|
||
if err != nil {
|
||
log.Error("删除短剧剧集点赞关系失败", log.Any("用户ID", userId), log.Any("短剧ID", chapter.PlayletId), log.Any("剧集ID", chapterId), log.E(err))
|
||
return nil, false, err
|
||
}
|
||
changed = deleted > 0
|
||
}
|
||
if changed {
|
||
delta := int64(1)
|
||
if !enabled {
|
||
delta = -1
|
||
}
|
||
if err := incrementPlayletChapterOperateCounter(ctx, chapterId, playletchaptermod.FN.PlayletChapter.CountLike, delta); err != nil {
|
||
rollbackPlayletChapterLike(ctx, userId, chapter.PlayletId, chapterId, enabled)
|
||
return nil, false, err
|
||
}
|
||
heatDelta := playletHeatLikeInc
|
||
if !enabled {
|
||
heatDelta = -playletHeatLikeInc
|
||
}
|
||
if err := incrementPlayletHeatValue(ctx, chapter.PlayletId, heatDelta); err != nil {
|
||
log.Error("更新短剧剧集点赞对应短剧热度失败", log.Any("短剧ID", chapter.PlayletId), log.Any("剧集ID", chapterId), log.Any("增量", heatDelta), log.E(err))
|
||
rollbackPlayletChapterLike(ctx, userId, chapter.PlayletId, chapterId, enabled)
|
||
rollbackPlayletChapterOperateCounter(ctx, chapterId, playletchaptermod.FN.PlayletChapter.CountLike, delta)
|
||
return nil, false, err
|
||
}
|
||
}
|
||
return chapter, changed, nil
|
||
}
|
||
|
||
// setPlayletChapterCollect 设置短剧剧集收藏关系
|
||
func setPlayletChapterCollect(ctx context.Context, userId int64, chapterId int64, enabled bool) (*playletchaptermod.PlayletChapter, bool, *srvdef.SrvErr) {
|
||
// 剧集收藏必须登录,先校验剧集存在且所属短剧已上架,再按收藏关系真实变化更新 playlet_chapter.countCollect。
|
||
if userId <= 0 {
|
||
return nil, false, srvdef.New(srvdef.InvalidToken, "请先登录", "请先登录")
|
||
}
|
||
chapter, err := findPlayletChapterOperateTarget(ctx, userId, chapterId)
|
||
if err != nil {
|
||
return nil, false, err
|
||
}
|
||
filter := bson.M{
|
||
playletchaptercollectmod.FN.PlayletChapterCollect.UserId: userId,
|
||
playletchaptercollectmod.FN.PlayletChapterCollect.ChapterId: chapterId,
|
||
}
|
||
changed := false
|
||
if enabled {
|
||
inserted, err := playletchaptercollectmod.InsertOnePlayletChapterCollect(ctx, &playletchaptercollectmod.PlayletChapterCollect{
|
||
UserId: userId,
|
||
PlayletId: chapter.PlayletId,
|
||
ChapterId: chapterId,
|
||
})
|
||
if err != nil {
|
||
log.Error("新增短剧剧集收藏关系失败", log.Any("用户ID", userId), log.Any("短剧ID", chapter.PlayletId), log.Any("剧集ID", chapterId), log.E(err))
|
||
return nil, false, err
|
||
}
|
||
changed = inserted
|
||
} else {
|
||
deleted, err := playletchaptercollectmod.DeleteOnePlayletChapterCollect(ctx, filter)
|
||
if err != nil {
|
||
log.Error("删除短剧剧集收藏关系失败", log.Any("用户ID", userId), log.Any("短剧ID", chapter.PlayletId), log.Any("剧集ID", chapterId), log.E(err))
|
||
return nil, false, err
|
||
}
|
||
changed = deleted > 0
|
||
}
|
||
if changed {
|
||
delta := int64(1)
|
||
if !enabled {
|
||
delta = -1
|
||
}
|
||
if err := incrementPlayletChapterOperateCounter(ctx, chapterId, playletchaptermod.FN.PlayletChapter.CountCollect, delta); err != nil {
|
||
rollbackPlayletChapterCollect(ctx, userId, chapter.PlayletId, chapterId, enabled)
|
||
return nil, false, err
|
||
}
|
||
heatDelta := playletHeatCollectInc
|
||
if !enabled {
|
||
heatDelta = -playletHeatCollectInc
|
||
}
|
||
if err := incrementPlayletHeatValue(ctx, chapter.PlayletId, heatDelta); err != nil {
|
||
log.Error("更新短剧剧集收藏对应短剧热度失败", log.Any("短剧ID", chapter.PlayletId), log.Any("剧集ID", chapterId), log.Any("增量", heatDelta), log.E(err))
|
||
rollbackPlayletChapterCollect(ctx, userId, chapter.PlayletId, chapterId, enabled)
|
||
rollbackPlayletChapterOperateCounter(ctx, chapterId, playletchaptermod.FN.PlayletChapter.CountCollect, delta)
|
||
return nil, false, err
|
||
}
|
||
}
|
||
return chapter, changed, nil
|
||
}
|
||
|
||
// findPlayletOperateTarget 查询短剧操作目标
|
||
func findPlayletOperateTarget(ctx context.Context, userId int64, playletId int64) (*playletmod.Playlet, *srvdef.SrvErr) {
|
||
// 操作前只读取计数字段,减少Mongo返回体;状态过滤保证下架短剧不能被点赞或收藏。
|
||
playlet, err := playletmod.FindOnePlaylet(ctx, homeUpPlayletFilter(bson.M{
|
||
playletmod.FN.Playlet.Id: playletId,
|
||
}), options.FindOne().SetProjection(playletOperateProjection))
|
||
if err != nil {
|
||
log.Error("查询短剧操作目标失败", log.Any("用户ID", userId), log.Any("短剧ID", playletId), log.E(err))
|
||
return nil, err
|
||
}
|
||
return playlet, nil
|
||
}
|
||
|
||
// findPlayletChapterOperateTarget 查询短剧剧集操作目标
|
||
func findPlayletChapterOperateTarget(ctx context.Context, userId int64, chapterId int64) (*playletchaptermod.PlayletChapter, *srvdef.SrvErr) {
|
||
// 先用章节ID读取最小字段,再校验所属短剧已上架,避免用户操作已下架短剧的剧集。
|
||
chapter, err := playletchaptermod.FindOnePlayletChapter(ctx, bson.M{
|
||
playletchaptermod.FN.PlayletChapter.Id: chapterId,
|
||
}, options.FindOne().SetProjection(playletChapterOperateProjection))
|
||
if err != nil {
|
||
log.Error("查询短剧剧集操作目标失败", log.Any("用户ID", userId), log.Any("剧集ID", chapterId), log.E(err))
|
||
return nil, err
|
||
}
|
||
if _, err = playletmod.FindOnePlaylet(ctx, homeUpPlayletFilter(bson.M{
|
||
playletmod.FN.Playlet.Id: chapter.PlayletId,
|
||
}), options.FindOne().SetProjection(bson.D{
|
||
{Key: playletmod.FN.Playlet.Id, Value: 1},
|
||
})); err != nil {
|
||
log.Error("查询短剧剧集所属短剧失败", log.Any("用户ID", userId), log.Any("短剧ID", chapter.PlayletId), log.Any("剧集ID", chapterId), log.E(err))
|
||
return nil, err
|
||
}
|
||
return chapter, nil
|
||
}
|
||
|
||
// rollbackPlayletLike 回滚短剧点赞关系
|
||
func rollbackPlayletLike(ctx context.Context, userId int64, playletId int64, enabled bool) {
|
||
// 点赞表已经变化但短剧点赞数更新失败时做补偿,尽量保持点赞关系和点赞数一致。
|
||
filter := bson.M{
|
||
playletlikemod.FN.PlayletLike.UserId: userId,
|
||
playletlikemod.FN.PlayletLike.PlayletId: playletId,
|
||
}
|
||
if enabled {
|
||
if _, err := playletlikemod.DeleteOnePlayletLike(ctx, filter); err != nil {
|
||
log.Error("回滚短剧点赞关系失败", log.Any("用户ID", userId), log.Any("短剧ID", playletId), log.E(err))
|
||
}
|
||
return
|
||
}
|
||
if _, err := playletlikemod.InsertOnePlayletLike(ctx, &playletlikemod.PlayletLike{
|
||
UserId: userId,
|
||
PlayletId: playletId,
|
||
}); err != nil {
|
||
log.Error("恢复短剧点赞关系失败", log.Any("用户ID", userId), log.Any("短剧ID", playletId), log.E(err))
|
||
}
|
||
}
|
||
|
||
// rollbackPlayletCollect 回滚短剧收藏关系
|
||
func rollbackPlayletCollect(ctx context.Context, userId int64, playletId int64, enabled bool) {
|
||
// 收藏表已经变化但短剧收藏数更新失败时做补偿,尽量保持收藏关系和收藏数一致。
|
||
filter := bson.M{
|
||
playletcollectmod.FN.PlayletCollect.UserId: userId,
|
||
playletcollectmod.FN.PlayletCollect.PlayletId: playletId,
|
||
}
|
||
if enabled {
|
||
if _, err := playletcollectmod.DeleteOnePlayletCollect(ctx, filter); err != nil {
|
||
log.Error("回滚短剧收藏关系失败", log.Any("用户ID", userId), log.Any("短剧ID", playletId), log.E(err))
|
||
}
|
||
return
|
||
}
|
||
if _, err := playletcollectmod.InsertOnePlayletCollect(ctx, &playletcollectmod.PlayletCollect{
|
||
UserId: userId,
|
||
PlayletId: playletId,
|
||
}); err != nil {
|
||
log.Error("恢复短剧收藏关系失败", log.Any("用户ID", userId), log.Any("短剧ID", playletId), log.E(err))
|
||
}
|
||
}
|
||
|
||
// rollbackPlayletChapterLike 回滚短剧剧集点赞关系
|
||
func rollbackPlayletChapterLike(ctx context.Context, userId int64, playletId int64, chapterId int64, enabled bool) {
|
||
// 剧集点赞关系已经变化但剧集点赞数更新失败时做补偿,尽量保持点赞关系和点赞数一致。
|
||
filter := bson.M{
|
||
playletchapterlikemod.FN.PlayletChapterLike.UserId: userId,
|
||
playletchapterlikemod.FN.PlayletChapterLike.ChapterId: chapterId,
|
||
}
|
||
if enabled {
|
||
if _, err := playletchapterlikemod.DeleteOnePlayletChapterLike(ctx, filter); err != nil {
|
||
log.Error("回滚短剧剧集点赞关系失败", log.Any("用户ID", userId), log.Any("短剧ID", playletId), log.Any("剧集ID", chapterId), log.E(err))
|
||
}
|
||
return
|
||
}
|
||
if _, err := playletchapterlikemod.InsertOnePlayletChapterLike(ctx, &playletchapterlikemod.PlayletChapterLike{
|
||
UserId: userId,
|
||
PlayletId: playletId,
|
||
ChapterId: chapterId,
|
||
}); err != nil {
|
||
log.Error("恢复短剧剧集点赞关系失败", log.Any("用户ID", userId), log.Any("短剧ID", playletId), log.Any("剧集ID", chapterId), log.E(err))
|
||
}
|
||
}
|
||
|
||
// rollbackPlayletChapterCollect 回滚短剧剧集收藏关系
|
||
func rollbackPlayletChapterCollect(ctx context.Context, userId int64, playletId int64, chapterId int64, enabled bool) {
|
||
// 剧集收藏关系已经变化但剧集收藏数更新失败时做补偿,尽量保持收藏关系和收藏数一致。
|
||
filter := bson.M{
|
||
playletchaptercollectmod.FN.PlayletChapterCollect.UserId: userId,
|
||
playletchaptercollectmod.FN.PlayletChapterCollect.ChapterId: chapterId,
|
||
}
|
||
if enabled {
|
||
if _, err := playletchaptercollectmod.DeleteOnePlayletChapterCollect(ctx, filter); err != nil {
|
||
log.Error("回滚短剧剧集收藏关系失败", log.Any("用户ID", userId), log.Any("短剧ID", playletId), log.Any("剧集ID", chapterId), log.E(err))
|
||
}
|
||
return
|
||
}
|
||
if _, err := playletchaptercollectmod.InsertOnePlayletChapterCollect(ctx, &playletchaptercollectmod.PlayletChapterCollect{
|
||
UserId: userId,
|
||
PlayletId: playletId,
|
||
ChapterId: chapterId,
|
||
}); err != nil {
|
||
log.Error("恢复短剧剧集收藏关系失败", log.Any("用户ID", userId), log.Any("短剧ID", playletId), log.Any("剧集ID", chapterId), log.E(err))
|
||
}
|
||
}
|
||
|
||
// incrementPlayletOperateCounter 增减短剧点赞或收藏计数
|
||
func incrementPlayletOperateCounter(ctx context.Context, playletId int64, countField string, delta int64, heatDelta int64) *srvdef.SrvErr {
|
||
// 取消操作时增加 countField > 0 条件,避免历史脏数据或并发场景把计数减成负数。
|
||
filter := bson.M{
|
||
playletmod.FN.Playlet.Id: playletId,
|
||
}
|
||
if delta < 0 {
|
||
filter[countField] = bson.M{"$gt": 0}
|
||
}
|
||
incDoc := bson.M{
|
||
countField: delta,
|
||
}
|
||
if heatDelta != 0 {
|
||
incDoc[playletmod.FN.Playlet.HeatValue] = heatDelta
|
||
}
|
||
err := playletmod.UpdateOnePlaylet(ctx, filter, bson.M{
|
||
"$inc": incDoc,
|
||
})
|
||
if err != nil {
|
||
if delta < 0 && err.Code == srvdef.ErrNoData {
|
||
log.Warn("短剧操作计数已为0,跳过扣减", log.Any("短剧ID", playletId), log.Any("计数字段", countField))
|
||
return nil
|
||
}
|
||
log.Error("更新短剧操作计数失败", log.Any("短剧ID", playletId), log.Any("计数字段", countField), log.Any("增量", delta), log.E(err))
|
||
return err
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// incrementPlayletChapterOperateCounter 增减短剧剧集点赞或收藏计数
|
||
func incrementPlayletChapterOperateCounter(ctx context.Context, chapterId int64, countField string, delta int64) *srvdef.SrvErr {
|
||
// 取消操作时增加 countField > 0 条件,避免历史脏数据或并发场景把计数减成负数。
|
||
filter := bson.M{
|
||
playletchaptermod.FN.PlayletChapter.Id: chapterId,
|
||
}
|
||
if delta < 0 {
|
||
filter[countField] = bson.M{"$gt": 0}
|
||
}
|
||
err := playletchaptermod.UpdateOnePlayletChapter(ctx, filter, bson.M{
|
||
"$inc": bson.M{
|
||
countField: delta,
|
||
},
|
||
})
|
||
if err != nil {
|
||
if delta < 0 && err.Code == srvdef.ErrNoData {
|
||
log.Warn("短剧剧集操作计数已为0,跳过扣减", log.Any("剧集ID", chapterId), log.Any("计数字段", countField))
|
||
return nil
|
||
}
|
||
log.Error("更新短剧剧集操作计数失败", log.Any("剧集ID", chapterId), log.Any("计数字段", countField), log.Any("增量", delta), log.E(err))
|
||
return err
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func rollbackPlayletChapterOperateCounter(ctx context.Context, chapterId int64, countField string, delta int64) {
|
||
rollbackDelta := -delta
|
||
if err := incrementPlayletChapterOperateCounter(ctx, chapterId, countField, rollbackDelta); err != nil {
|
||
log.Error("回滚短剧剧集操作计数失败", log.Any("剧集ID", chapterId), log.Any("计数字段", countField), log.Any("增量", rollbackDelta), log.E(err))
|
||
}
|
||
}
|
||
|
||
// applyLocalCounter 根据本次关系变更结果计算响应计数
|
||
func applyLocalCounter(current int64, enabled bool, changed bool) int64 {
|
||
// 响应计数基于操作前短剧计数和本次真实变更计算,避免为返回值再查一次短剧表。
|
||
if !changed {
|
||
return current
|
||
}
|
||
if enabled {
|
||
return current + 1
|
||
}
|
||
if current <= 0 {
|
||
return 0
|
||
}
|
||
return current - 1
|
||
}
|
||
|
||
func displayPlayletCountLike(countLike int64, virtualCountLike int64) int64 {
|
||
return countLike + virtualCountLike
|
||
}
|
||
|
||
// syncPlayletToESAsync 异步同步短剧到ES
|
||
func syncPlayletToESAsync(playletId int64, failMsg string) {
|
||
if playletId <= 0 {
|
||
return
|
||
}
|
||
go func() {
|
||
// 点赞、收藏入口只允许操作已上架短剧
|
||
if err := srv_search.SyncPlayletStatusToES(context.Background(), []int64{playletId}, playletmod.StatusUp); err != nil {
|
||
log.Error(failMsg, log.Any("短剧ID", playletId), log.E(err))
|
||
}
|
||
}()
|
||
}
|
||
|
||
// operateStatusByFlag 转换操作状态响应值
|
||
func operateStatusByFlag(enabled bool) int64 {
|
||
if enabled {
|
||
return operateStatusYes
|
||
}
|
||
return operateStatusNo
|
||
}
|
||
|
||
// Detail 根据ID查询app端短剧详情
|
||
func Detail(ctx context.Context, userId int64, req *proto.PlayletInfoReq) (*proto.PlayletInfoResp, *srvdef.SrvErr) {
|
||
if req == nil {
|
||
return nil, srvdef.New(srvdef.ErrParamError, "短剧ID不能为空", "短剧ID不能为空")
|
||
}
|
||
playletID, err := resolvePlayletID(ctx, req.Id, req.Slug, true)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
req.Id = playletID
|
||
item, err := playletmod.FindOnePlaylet(ctx, homeUpPlayletFilter(bson.M{
|
||
playletmod.FN.Playlet.Id: req.Id,
|
||
}), options.FindOne().SetProjection(playletHomeProjection))
|
||
if err != nil {
|
||
log.Error("查询短剧详情失败", log.Any("短剧ID", req.Id), log.E(err))
|
||
return nil, err
|
||
}
|
||
if err = incrementPlayletBrowse(ctx, item.Id); err != nil {
|
||
log.Error("增加短剧观看量失败", log.Any("短剧ID", item.Id), log.E(err))
|
||
return nil, err
|
||
}
|
||
item.CountBrowse++
|
||
item.HeatValue += playletHeatClickInc
|
||
relatedPlaylets, err := findRelatedRecommendPlaylets(ctx, item)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
allPlaylets := mergeHomePlayletLists(playletmod.PlayletList{item}, relatedPlaylets)
|
||
themeNameMap, err := buildHomeThemeNameMap(ctx, allPlaylets)
|
||
if err != nil {
|
||
log.Error("查询短剧详情题材名称失败", log.Any("短剧ID", item.Id), log.E(err))
|
||
return nil, err
|
||
}
|
||
tagNameMap, err := buildHomeTagNameMap(ctx, allPlaylets)
|
||
if err != nil {
|
||
log.Error("查询短剧详情标签名称失败", log.Any("短剧ID", item.Id), log.E(err))
|
||
return nil, err
|
||
}
|
||
commonList := playletsToHomeCommonData(ctx, playletmod.PlayletList{item}, themeNameMap, tagNameMap)
|
||
if len(commonList) == 0 || commonList[0] == nil {
|
||
return nil, srvdef.New(srvdef.ErrNoData, "", "")
|
||
}
|
||
chapterInfo, err := findPlayletChapters(ctx, item.Id, userId, item.IsOriginal)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
isLike, isCollect, err := findPlayletUserOperateStatus(ctx, userId, item.Id)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
resp := &proto.PlayletInfoResp{
|
||
CommonData: *commonList[0],
|
||
IsLike: isLike,
|
||
IsCollect: isCollect,
|
||
RelatedRecommendData: playletsToHomeCommonData(ctx, relatedPlaylets, themeNameMap, tagNameMap),
|
||
PlayletChapterInfo: chapterInfo,
|
||
SeoInfo: buildPlayletDetailSeoInfo(item, themeNameMap, tagNameMap),
|
||
}
|
||
return resp, nil
|
||
}
|
||
|
||
// findPlayletUserOperateStatus 查询当前用户短剧点赞和收藏状态
|
||
func findPlayletUserOperateStatus(ctx context.Context, userId int64, playletId int64) (int64, int64, *srvdef.SrvErr) {
|
||
// 游客或无有效token时没有用户ID,直接返回未点赞、未收藏,保证详情接口仍可匿名访问。
|
||
if userId <= 0 {
|
||
return operateStatusNo, operateStatusNo, nil
|
||
}
|
||
likeExists, err := playletlikemod.ExistsPlayletLike(ctx, bson.M{
|
||
playletlikemod.FN.PlayletLike.UserId: userId,
|
||
playletlikemod.FN.PlayletLike.PlayletId: playletId,
|
||
})
|
||
if err != nil {
|
||
log.Error("查询短剧用户点赞状态失败", log.Any("用户ID", userId), log.Any("短剧ID", playletId), log.E(err))
|
||
return operateStatusNo, operateStatusNo, err
|
||
}
|
||
collectExists, err := playletcollectmod.ExistsPlayletCollect(ctx, bson.M{
|
||
playletcollectmod.FN.PlayletCollect.UserId: userId,
|
||
playletcollectmod.FN.PlayletCollect.PlayletId: playletId,
|
||
})
|
||
if err != nil {
|
||
log.Error("查询短剧用户收藏状态失败", log.Any("用户ID", userId), log.Any("短剧ID", playletId), log.E(err))
|
||
return operateStatusNo, operateStatusNo, err
|
||
}
|
||
isLike := operateStatusByFlag(likeExists)
|
||
isCollect := operateStatusByFlag(collectExists)
|
||
return isLike, isCollect, nil
|
||
}
|
||
|
||
// CollectList 分页查询当前用户的短剧收藏列表
|
||
func CollectList(ctx context.Context, userId int64, req *proto.PlayletCollectListReq) (*proto.PlayletCollectListResp, *srvdef.SrvErr) {
|
||
if userId <= 0 {
|
||
return nil, srvdef.New(srvdef.InvalidToken, "请先登录", "请先登录")
|
||
}
|
||
if req == nil {
|
||
return nil, srvdef.New(srvdef.ErrParamError, "", "")
|
||
}
|
||
pageNum, pageSize := normalizePlayletPage(req.PageNum, req.PageSize)
|
||
filter := bson.M{
|
||
playletcollectmod.FN.PlayletCollect.UserId: userId,
|
||
}
|
||
totalCount, err := playletcollectmod.CountPlayletCollect(ctx, filter)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
resp := &proto.PlayletCollectListResp{
|
||
TotalCount: totalCount,
|
||
PageSize: int(pageSize),
|
||
PageNum: int(pageNum),
|
||
List: make([]proto.PlayletCollectData, 0),
|
||
}
|
||
if totalCount == 0 {
|
||
return resp, nil
|
||
}
|
||
skip := (pageNum - 1) * pageSize
|
||
if skip >= totalCount {
|
||
return resp, nil
|
||
}
|
||
collectList, err := playletcollectmod.FindManyPlayletCollect(ctx, filter, options.Find().
|
||
SetProjection(bson.D{
|
||
{Key: playletcollectmod.FN.PlayletCollect.Id, Value: 1},
|
||
{Key: playletcollectmod.FN.PlayletCollect.PlayletId, Value: 1},
|
||
{Key: playletcollectmod.FN.PlayletCollect.CreatedAt, Value: 1},
|
||
}).
|
||
SetSort(bson.D{
|
||
{Key: playletcollectmod.FN.PlayletCollect.CreatedAt, Value: -1},
|
||
{Key: playletcollectmod.FN.PlayletCollect.Id, Value: -1},
|
||
}).
|
||
SetSkip(skip).
|
||
SetLimit(pageSize))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if len(collectList) == 0 {
|
||
return resp, nil
|
||
}
|
||
playletIds := collectPlayletIdsFromCollects(collectList)
|
||
playletMap, err := findCollectPlayletMap(ctx, playletIds)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
progressMap, err := findCollectWatchProgressMap(ctx, userId, playletIds)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
playletList := playletListFromMapByCollects(collectList, playletMap)
|
||
themeNameMap, err := buildHomeThemeNameMap(ctx, playletList)
|
||
if err != nil {
|
||
log.Error("查询我的收藏题材名称失败", log.Any("用户ID", userId), log.E(err))
|
||
return nil, err
|
||
}
|
||
tagNameMap, err := buildHomeTagNameMap(ctx, playletList)
|
||
if err != nil {
|
||
log.Error("查询我的收藏标签名称失败", log.Any("用户ID", userId), log.E(err))
|
||
return nil, err
|
||
}
|
||
resp.List = buildPlayletCollectData(ctx, collectList, playletMap, progressMap, themeNameMap, tagNameMap)
|
||
return resp, nil
|
||
}
|
||
|
||
// ChapterCollectList 分页查询当前用户收藏的短剧剧集列表
|
||
func ChapterCollectList(ctx context.Context, userId int64, req *proto.PlayletChapterCollectListReq) (*proto.PlayletChapterCollectListResp, *srvdef.SrvErr) {
|
||
if userId <= 0 {
|
||
return nil, srvdef.New(srvdef.InvalidToken, "请先登录", "请先登录")
|
||
}
|
||
if req == nil {
|
||
return nil, srvdef.New(srvdef.ErrParamError, "", "")
|
||
}
|
||
pageNum, pageSize := normalizePlayletPage(req.PageNum, req.PageSize)
|
||
filter := bson.M{
|
||
playletchaptercollectmod.FN.PlayletChapterCollect.UserId: userId,
|
||
}
|
||
totalCount, err := playletchaptercollectmod.CountPlayletChapterCollect(ctx, filter)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
resp := &proto.PlayletChapterCollectListResp{
|
||
TotalCount: totalCount,
|
||
PageSize: int(pageSize),
|
||
PageNum: int(pageNum),
|
||
List: make([]proto.PlayletChapterCollectData, 0),
|
||
}
|
||
if totalCount == 0 {
|
||
return resp, nil
|
||
}
|
||
skip := (pageNum - 1) * pageSize
|
||
if skip >= totalCount {
|
||
return resp, nil
|
||
}
|
||
collectList, err := playletchaptercollectmod.FindManyPlayletChapterCollect(ctx, filter, options.Find().
|
||
SetProjection(bson.D{
|
||
{Key: playletchaptercollectmod.FN.PlayletChapterCollect.Id, Value: 1},
|
||
{Key: playletchaptercollectmod.FN.PlayletChapterCollect.PlayletId, Value: 1},
|
||
{Key: playletchaptercollectmod.FN.PlayletChapterCollect.ChapterId, Value: 1},
|
||
{Key: playletchaptercollectmod.FN.PlayletChapterCollect.CreatedAt, Value: 1},
|
||
}).
|
||
SetSort(bson.D{
|
||
{Key: playletchaptercollectmod.FN.PlayletChapterCollect.CreatedAt, Value: -1},
|
||
{Key: playletchaptercollectmod.FN.PlayletChapterCollect.Id, Value: -1},
|
||
}).
|
||
SetSkip(skip).
|
||
SetLimit(pageSize))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if len(collectList) == 0 {
|
||
return resp, nil
|
||
}
|
||
chapterIds := collectChapterIdsFromChapterCollects(collectList)
|
||
chapterMap, err := findChapterCollectChapterMap(ctx, chapterIds)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
playletIds := collectPlayletIdsFromChapterCollects(collectList, chapterMap)
|
||
playletMap, err := findChapterCollectPlayletMap(ctx, playletIds)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
resp.List = buildPlayletChapterCollectData(ctx, collectList, chapterMap, playletMap)
|
||
return resp, nil
|
||
}
|
||
|
||
// HistoryList 分页查询当前用户的短剧观看历史
|
||
func HistoryList(ctx context.Context, userId int64, req *proto.PlayletHistoryListReq) (*proto.PlayletHistoryListResp, *srvdef.SrvErr) {
|
||
if userId <= 0 {
|
||
return nil, srvdef.New(srvdef.InvalidToken, "请先登录", "请先登录")
|
||
}
|
||
if req == nil {
|
||
return nil, srvdef.New(srvdef.ErrParamError, "", "")
|
||
}
|
||
pageNum, pageSize := normalizePlayletHistoryPage(req.PageNum, req.PageSize)
|
||
filter := bson.M{
|
||
playletwatchhistorymod.FN.PlayletWatchHistory.UserId: userId,
|
||
}
|
||
totalCount, err := playletwatchhistorymod.CountPlayletWatchHistory(ctx, filter)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
resp := &proto.PlayletHistoryListResp{
|
||
TotalCount: totalCount,
|
||
PageSize: int(pageSize),
|
||
PageNum: int(pageNum),
|
||
List: make([]proto.PlayletHistoryData, 0),
|
||
}
|
||
if totalCount == 0 {
|
||
return resp, nil
|
||
}
|
||
skip := (pageNum - 1) * pageSize
|
||
if skip >= totalCount {
|
||
return resp, nil
|
||
}
|
||
historyList, err := playletwatchhistorymod.FindManyPlayletWatchHistory(ctx, filter, options.Find().
|
||
SetProjection(bson.D{
|
||
{Key: playletwatchhistorymod.FN.PlayletWatchHistory.Id, Value: 1},
|
||
{Key: playletwatchhistorymod.FN.PlayletWatchHistory.PlayletId, Value: 1},
|
||
{Key: playletwatchhistorymod.FN.PlayletWatchHistory.LastChapterId, Value: 1},
|
||
{Key: playletwatchhistorymod.FN.PlayletWatchHistory.LastEpisode, Value: 1},
|
||
{Key: playletwatchhistorymod.FN.PlayletWatchHistory.UpdatedAt, Value: 1},
|
||
}).
|
||
SetSort(bson.D{
|
||
{Key: playletwatchhistorymod.FN.PlayletWatchHistory.UpdatedAt, Value: -1},
|
||
{Key: playletwatchhistorymod.FN.PlayletWatchHistory.Id, Value: -1},
|
||
}).
|
||
SetSkip(skip).
|
||
SetLimit(pageSize))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if len(historyList) == 0 {
|
||
return resp, nil
|
||
}
|
||
playletIds := collectPlayletIdsFromHistories(historyList)
|
||
playletMap, err := findHistoryPlayletMap(ctx, playletIds)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
progressMap, err := findCollectWatchProgressMap(ctx, userId, playletIds)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
playletList := playletListFromMapByHistories(historyList, playletMap)
|
||
themeNameMap, err := buildHomeThemeNameMap(ctx, playletList)
|
||
if err != nil {
|
||
log.Error("查询观看历史题材名称失败", log.Any("用户ID", userId), log.E(err))
|
||
return nil, err
|
||
}
|
||
tagNameMap, err := buildHomeTagNameMap(ctx, playletList)
|
||
if err != nil {
|
||
log.Error("查询观看历史标签名称失败", log.Any("用户ID", userId), log.E(err))
|
||
return nil, err
|
||
}
|
||
resp.List = buildPlayletHistoryData(ctx, historyList, playletMap, progressMap, themeNameMap, tagNameMap)
|
||
return resp, nil
|
||
}
|
||
|
||
// ClearHistory 清空当前用户的短剧观看历史,不影响观看进度和收藏列表。
|
||
func ClearHistory(ctx context.Context, userId int64) (*proto.PlayletHistoryClearResp, *srvdef.SrvErr) {
|
||
if userId <= 0 {
|
||
return nil, srvdef.New(srvdef.InvalidToken, "请先登录", "请先登录")
|
||
}
|
||
deleted, err := playletwatchhistorymod.DeleteManyPlayletWatchHistory(ctx, bson.M{
|
||
playletwatchhistorymod.FN.PlayletWatchHistory.UserId: userId,
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return &proto.PlayletHistoryClearResp{DeletedCount: deleted}, nil
|
||
}
|
||
|
||
// collectPlayletIdsFromCollects 按收藏列表顺序收集短剧ID并去重,避免批量查询重复ID。
|
||
func collectPlayletIdsFromCollects(list playletcollectmod.PlayletCollectList) []int64 {
|
||
result := make([]int64, 0, len(list))
|
||
seen := make(map[int64]struct{}, len(list))
|
||
for _, item := range list {
|
||
if item == nil || item.PlayletId <= 0 {
|
||
continue
|
||
}
|
||
if _, ok := seen[item.PlayletId]; ok {
|
||
continue
|
||
}
|
||
seen[item.PlayletId] = struct{}{}
|
||
result = append(result, item.PlayletId)
|
||
}
|
||
return result
|
||
}
|
||
|
||
// collectPlayletIdsFromHistories 按观看历史顺序收集短剧ID并去重,避免批量查询重复ID。
|
||
func collectPlayletIdsFromHistories(list playletwatchhistorymod.PlayletWatchHistoryList) []int64 {
|
||
result := make([]int64, 0, len(list))
|
||
seen := make(map[int64]struct{}, len(list))
|
||
for _, item := range list {
|
||
if item == nil || item.PlayletId <= 0 {
|
||
continue
|
||
}
|
||
if _, ok := seen[item.PlayletId]; ok {
|
||
continue
|
||
}
|
||
seen[item.PlayletId] = struct{}{}
|
||
result = append(result, item.PlayletId)
|
||
}
|
||
return result
|
||
}
|
||
|
||
// collectChapterIdsFromChapterCollects 按剧集收藏顺序收集剧集ID并去重。
|
||
func collectChapterIdsFromChapterCollects(list playletchaptercollectmod.PlayletChapterCollectList) []int64 {
|
||
result := make([]int64, 0, len(list))
|
||
seen := make(map[int64]struct{}, len(list))
|
||
for _, item := range list {
|
||
if item == nil || item.ChapterId <= 0 {
|
||
continue
|
||
}
|
||
if _, ok := seen[item.ChapterId]; ok {
|
||
continue
|
||
}
|
||
seen[item.ChapterId] = struct{}{}
|
||
result = append(result, item.ChapterId)
|
||
}
|
||
return result
|
||
}
|
||
|
||
// collectPlayletIdsFromChapterCollects 从收藏关系和剧集数据中收集短剧ID,兼容收藏关系里playletId缺失的历史数据。
|
||
func collectPlayletIdsFromChapterCollects(
|
||
list playletchaptercollectmod.PlayletChapterCollectList,
|
||
chapterMap map[int64]*playletchaptermod.PlayletChapter,
|
||
) []int64 {
|
||
result := make([]int64, 0, len(list))
|
||
seen := make(map[int64]struct{}, len(list))
|
||
for _, item := range list {
|
||
if item == nil {
|
||
continue
|
||
}
|
||
playletId := item.PlayletId
|
||
if playletId <= 0 {
|
||
if chapter := chapterMap[item.ChapterId]; chapter != nil {
|
||
playletId = chapter.PlayletId
|
||
}
|
||
}
|
||
if playletId <= 0 {
|
||
continue
|
||
}
|
||
if _, ok := seen[playletId]; ok {
|
||
continue
|
||
}
|
||
seen[playletId] = struct{}{}
|
||
result = append(result, playletId)
|
||
}
|
||
return result
|
||
}
|
||
|
||
// findCollectPlayletMap 批量查询收藏关联的短剧数据,返回map便于按收藏时间顺序组装。
|
||
func findCollectPlayletMap(ctx context.Context, playletIds []int64) (map[int64]*playletmod.Playlet, *srvdef.SrvErr) {
|
||
result := make(map[int64]*playletmod.Playlet, len(playletIds))
|
||
if len(playletIds) == 0 {
|
||
return result, nil
|
||
}
|
||
list, err := playletmod.FindManyPlaylet(ctx, bson.M{
|
||
playletmod.FN.Playlet.Id: bson.M{"$in": playletIds},
|
||
}, options.Find().SetProjection(playletCollectListProjection))
|
||
if err != nil {
|
||
log.Error("批量查询我的收藏短剧失败", log.Any("短剧ID集合", playletIds), log.E(err))
|
||
return nil, err
|
||
}
|
||
for _, item := range list {
|
||
if item == nil {
|
||
continue
|
||
}
|
||
result[item.Id] = item
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
// findChapterCollectPlayletMap 批量查询剧集收藏关联的短剧数据。
|
||
func findChapterCollectPlayletMap(ctx context.Context, playletIds []int64) (map[int64]*playletmod.Playlet, *srvdef.SrvErr) {
|
||
result := make(map[int64]*playletmod.Playlet, len(playletIds))
|
||
if len(playletIds) == 0 {
|
||
return result, nil
|
||
}
|
||
list, err := playletmod.FindManyPlaylet(ctx, bson.M{
|
||
playletmod.FN.Playlet.Id: bson.M{"$in": playletIds},
|
||
}, options.Find().SetProjection(bson.D{
|
||
{Key: playletmod.FN.Playlet.Id, Value: 1},
|
||
{Key: playletmod.FN.Playlet.Title, Value: 1},
|
||
{Key: playletmod.FN.Playlet.Cover, Value: 1},
|
||
{Key: playletmod.FN.Playlet.CompressedCover, Value: 1},
|
||
{Key: playletmod.FN.Playlet.TotalEpisode, Value: 1},
|
||
{Key: playletmod.FN.Playlet.UpdateStatus, Value: 1},
|
||
}))
|
||
if err != nil {
|
||
log.Error("批量查询我的剧集收藏短剧失败", log.Any("短剧ID集合", playletIds), log.E(err))
|
||
return nil, err
|
||
}
|
||
for _, item := range list {
|
||
if item == nil {
|
||
continue
|
||
}
|
||
result[item.Id] = item
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
// findChapterCollectChapterMap 批量查询收藏关联的剧集数据。
|
||
func findChapterCollectChapterMap(ctx context.Context, chapterIds []int64) (map[int64]*playletchaptermod.PlayletChapter, *srvdef.SrvErr) {
|
||
result := make(map[int64]*playletchaptermod.PlayletChapter, len(chapterIds))
|
||
if len(chapterIds) == 0 {
|
||
return result, nil
|
||
}
|
||
list, err := playletchaptermod.FindManyPlayletChapter(ctx, bson.M{
|
||
playletchaptermod.FN.PlayletChapter.Id: bson.M{"$in": chapterIds},
|
||
}, options.Find().SetProjection(bson.D{
|
||
{Key: playletchaptermod.FN.PlayletChapter.Id, Value: 1},
|
||
{Key: playletchaptermod.FN.PlayletChapter.PlayletId, Value: 1},
|
||
{Key: playletchaptermod.FN.PlayletChapter.CurrentEpisode, Value: 1},
|
||
{Key: playletchaptermod.FN.PlayletChapter.Title, Value: 1},
|
||
{Key: playletchaptermod.FN.PlayletChapter.VideoUrl, Value: 1},
|
||
{Key: playletchaptermod.FN.PlayletChapter.WatermarkVideoUrl, Value: 1},
|
||
{Key: playletchaptermod.FN.PlayletChapter.MediaTime, Value: 1},
|
||
{Key: playletchaptermod.FN.PlayletChapter.PayType, Value: 1},
|
||
{Key: playletchaptermod.FN.PlayletChapter.SingleCoins, Value: 1},
|
||
{Key: playletchaptermod.FN.PlayletChapter.CountBrowse, Value: 1},
|
||
{Key: playletchaptermod.FN.PlayletChapter.CountCollect, Value: 1},
|
||
}))
|
||
if err != nil {
|
||
log.Error("批量查询我的剧集收藏剧集失败", log.Any("剧集ID集合", chapterIds), log.E(err))
|
||
return nil, err
|
||
}
|
||
for _, item := range list {
|
||
if item == nil {
|
||
continue
|
||
}
|
||
result[item.Id] = item
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
// findHistoryPlayletMap 批量查询观看历史关联的短剧数据,返回map便于按观看时间顺序组装。
|
||
func findHistoryPlayletMap(ctx context.Context, playletIds []int64) (map[int64]*playletmod.Playlet, *srvdef.SrvErr) {
|
||
result := make(map[int64]*playletmod.Playlet, len(playletIds))
|
||
if len(playletIds) == 0 {
|
||
return result, nil
|
||
}
|
||
list, err := playletmod.FindManyPlaylet(ctx, bson.M{
|
||
playletmod.FN.Playlet.Id: bson.M{"$in": playletIds},
|
||
}, options.Find().SetProjection(playletHomeProjection))
|
||
if err != nil {
|
||
log.Error("批量查询观看历史短剧失败", log.Any("短剧ID集合", playletIds), log.E(err))
|
||
return nil, err
|
||
}
|
||
for _, item := range list {
|
||
if item == nil {
|
||
continue
|
||
}
|
||
result[item.Id] = item
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
// findCollectWatchProgressMap 批量查询当前用户收藏短剧的观看进度,避免收藏列表逐条查询。
|
||
func findCollectWatchProgressMap(ctx context.Context, userId int64, playletIds []int64) (map[int64]*playletwatchprogressmod.PlayletWatchProgress, *srvdef.SrvErr) {
|
||
result := make(map[int64]*playletwatchprogressmod.PlayletWatchProgress, len(playletIds))
|
||
if userId <= 0 || len(playletIds) == 0 {
|
||
return result, nil
|
||
}
|
||
list, err := playletwatchprogressmod.FindManyPlayletWatchProgress(ctx, bson.M{
|
||
playletwatchprogressmod.FN.PlayletWatchProgress.UserId: userId,
|
||
playletwatchprogressmod.FN.PlayletWatchProgress.PlayletId: bson.M{"$in": playletIds},
|
||
}, options.Find().SetProjection(bson.D{
|
||
{Key: playletwatchprogressmod.FN.PlayletWatchProgress.PlayletId, Value: 1},
|
||
{Key: playletwatchprogressmod.FN.PlayletWatchProgress.LastChapterId, Value: 1},
|
||
{Key: playletwatchprogressmod.FN.PlayletWatchProgress.LastEpisode, Value: 1},
|
||
{Key: playletwatchprogressmod.FN.PlayletWatchProgress.WatchedEpisodes, Value: 1},
|
||
{Key: playletwatchprogressmod.FN.PlayletWatchProgress.WatchedCount, Value: 1},
|
||
}))
|
||
if err != nil {
|
||
log.Error("批量查询我的收藏观看进度失败", log.Any("用户ID", userId), log.Any("短剧ID集合", playletIds), log.E(err))
|
||
return nil, err
|
||
}
|
||
for _, item := range list {
|
||
if item == nil {
|
||
continue
|
||
}
|
||
result[item.PlayletId] = item
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
// playletListFromMapByCollects 按收藏列表顺序构造短剧列表,用于批量查询题材和标签名称。
|
||
func playletListFromMapByCollects(collectList playletcollectmod.PlayletCollectList, playletMap map[int64]*playletmod.Playlet) playletmod.PlayletList {
|
||
result := make(playletmod.PlayletList, 0, len(collectList))
|
||
for _, collect := range collectList {
|
||
if collect == nil {
|
||
continue
|
||
}
|
||
if item := playletMap[collect.PlayletId]; item != nil {
|
||
result = append(result, item)
|
||
}
|
||
}
|
||
return result
|
||
}
|
||
|
||
// playletListFromMapByHistories 按观看历史顺序构造短剧列表,用于批量查询题材和标签名称。
|
||
func playletListFromMapByHistories(historyList playletwatchhistorymod.PlayletWatchHistoryList, playletMap map[int64]*playletmod.Playlet) playletmod.PlayletList {
|
||
result := make(playletmod.PlayletList, 0, len(historyList))
|
||
for _, history := range historyList {
|
||
if history == nil {
|
||
continue
|
||
}
|
||
if item := playletMap[history.PlayletId]; item != nil {
|
||
result = append(result, item)
|
||
}
|
||
}
|
||
return result
|
||
}
|
||
|
||
// buildPlayletCollectData 组装我的收藏响应,保持playlet_collect的倒序分页顺序。
|
||
func buildPlayletCollectData(
|
||
ctx context.Context,
|
||
collectList playletcollectmod.PlayletCollectList,
|
||
playletMap map[int64]*playletmod.Playlet,
|
||
progressMap map[int64]*playletwatchprogressmod.PlayletWatchProgress,
|
||
themeNameMap map[int64]string,
|
||
tagNameMap map[int64]string,
|
||
) []proto.PlayletCollectData {
|
||
result := make([]proto.PlayletCollectData, 0, len(collectList))
|
||
for _, collect := range collectList {
|
||
if collect == nil {
|
||
continue
|
||
}
|
||
item := playletMap[collect.PlayletId]
|
||
if item == nil {
|
||
continue
|
||
}
|
||
progress := progressMap[item.Id]
|
||
watchedCount := collectWatchedCount(progress, item.TotalEpisode)
|
||
result = append(result, proto.PlayletCollectData{
|
||
Id: item.Id,
|
||
Title: item.Title,
|
||
PlayName: item.PlayName,
|
||
Slug: item.Slug,
|
||
Cover: withImageDomain(ctx, item.DisplayCover()),
|
||
ThemeIds: item.ThemeIds,
|
||
ThemeNames: namesByIds(item.ThemeIds, themeNameMap),
|
||
ThemeSlugs: slugsByIds(item.ThemeIds, playletcache.ThemeSlugMap(item.ThemeIds)),
|
||
TagIds: item.TagIds,
|
||
TagNames: namesByIds(item.TagIds, tagNameMap),
|
||
TagSlugs: slugsByIds(item.TagIds, playletcache.TagSlugMap(item.TagIds)),
|
||
TotalEpisode: item.TotalEpisode,
|
||
UpdateStatus: item.UpdateStatus,
|
||
CountBrowse: item.CountBrowse,
|
||
HeatValue: item.HeatValue,
|
||
CountCollect: item.CountCollect,
|
||
IsCollect: operateStatusYes,
|
||
CollectedAt: collect.CreatedAt,
|
||
WatchedCount: watchedCount,
|
||
UnwatchedCount: collectUnwatchedCount(item.TotalEpisode, watchedCount),
|
||
LastChapterId: collectLastChapterId(progress),
|
||
LastEpisode: collectLastEpisode(progress),
|
||
SeoInfo: item.SeoInfo,
|
||
})
|
||
}
|
||
return result
|
||
}
|
||
|
||
// buildPlayletChapterCollectData 组装我的剧集收藏响应,保持playlet_chapter_collect的倒序分页顺序。
|
||
func buildPlayletChapterCollectData(
|
||
ctx context.Context,
|
||
collectList playletchaptercollectmod.PlayletChapterCollectList,
|
||
chapterMap map[int64]*playletchaptermod.PlayletChapter,
|
||
playletMap map[int64]*playletmod.Playlet,
|
||
) []proto.PlayletChapterCollectData {
|
||
result := make([]proto.PlayletChapterCollectData, 0, len(collectList))
|
||
for _, collect := range collectList {
|
||
if collect == nil {
|
||
continue
|
||
}
|
||
chapter := chapterMap[collect.ChapterId]
|
||
if chapter == nil {
|
||
continue
|
||
}
|
||
playletId := collect.PlayletId
|
||
if playletId <= 0 {
|
||
playletId = chapter.PlayletId
|
||
}
|
||
playlet := playletMap[playletId]
|
||
if playlet == nil {
|
||
continue
|
||
}
|
||
result = append(result, proto.PlayletChapterCollectData{
|
||
PlayletId: playlet.Id,
|
||
PlayletTitle: playlet.Title,
|
||
PlayletCover: withImageDomain(ctx, playlet.DisplayCover()),
|
||
TotalEpisode: playlet.TotalEpisode,
|
||
UpdateStatus: playlet.UpdateStatus,
|
||
ChapterId: chapter.Id,
|
||
CurrentEpisode: chapter.CurrentEpisode,
|
||
ChapterTitle: chapter.Title,
|
||
VideoUrl: playletChapterPlayVideoUrl(chapter),
|
||
MediaTime: chapter.MediaTime,
|
||
PayType: int32(chapter.PayType),
|
||
SingleCoins: chapter.SingleCoins,
|
||
CountBrowse: chapter.CountBrowse,
|
||
CountCollect: chapter.CountCollect,
|
||
IsCollect: operateStatusYes,
|
||
CollectedAt: collect.CreatedAt,
|
||
})
|
||
}
|
||
return result
|
||
}
|
||
|
||
// buildPlayletHistoryData 组装观看历史响应,保持playlet_watch_history的倒序分页顺序。
|
||
func buildPlayletHistoryData(
|
||
ctx context.Context,
|
||
historyList playletwatchhistorymod.PlayletWatchHistoryList,
|
||
playletMap map[int64]*playletmod.Playlet,
|
||
progressMap map[int64]*playletwatchprogressmod.PlayletWatchProgress,
|
||
themeNameMap map[int64]string,
|
||
tagNameMap map[int64]string,
|
||
) []proto.PlayletHistoryData {
|
||
result := make([]proto.PlayletHistoryData, 0, len(historyList))
|
||
for _, history := range historyList {
|
||
if history == nil {
|
||
continue
|
||
}
|
||
item := playletMap[history.PlayletId]
|
||
if item == nil {
|
||
continue
|
||
}
|
||
progress := progressMap[item.Id]
|
||
watchedCount := collectWatchedCount(progress, item.TotalEpisode)
|
||
result = append(result, proto.PlayletHistoryData{
|
||
CommonData: proto.CommonData{
|
||
Id: item.Id,
|
||
Status: int32(item.Status),
|
||
IsFeatured: item.IsFeatured,
|
||
IsRecommend: item.IsRecommend,
|
||
IsNew: isOnlineThisWeek(item.OnlineAt),
|
||
OperateType: int32(playletmod.NormalizeOperateType(item.OperateType)),
|
||
ShowOnBanner: item.ShowOnBanner,
|
||
Banner: withImageDomain(ctx, item.Banner),
|
||
OnlineAt: item.OnlineAt,
|
||
MediaCreatedAt: item.MediaCreatedAt,
|
||
MediaUpdatedAt: item.MediaUpdatedAt,
|
||
ThemeIds: item.ThemeIds,
|
||
ThemeNames: namesByIds(item.ThemeIds, themeNameMap),
|
||
ThemeSlugs: slugsByIds(item.ThemeIds, playletcache.ThemeSlugMap(item.ThemeIds)),
|
||
Title: item.Title,
|
||
PlayName: item.PlayName,
|
||
Description: item.Description,
|
||
Slug: item.Slug,
|
||
Desc: item.Desc,
|
||
Anchor: item.Anchor,
|
||
Cover: withImageDomain(ctx, item.DisplayCover()),
|
||
TagIds: item.TagIds,
|
||
TagNames: namesByIds(item.TagIds, tagNameMap),
|
||
TagSlugs: slugsByIds(item.TagIds, playletcache.TagSlugMap(item.TagIds)),
|
||
TotalEpisode: item.TotalEpisode,
|
||
UpdateStatus: item.UpdateStatus,
|
||
CountBrowse: item.CountBrowse,
|
||
CountCollect: item.CountCollect,
|
||
CountLike: displayPlayletCountLike(item.CountLike, item.VirtualCountLike),
|
||
CreatedAt: item.CreatedAt,
|
||
UpdatedAt: item.UpdatedAt,
|
||
SeoInfo: item.SeoInfo,
|
||
},
|
||
LastChapterId: history.LastChapterId,
|
||
LastEpisode: history.LastEpisode,
|
||
WatchedCount: watchedCount,
|
||
UnwatchedCount: collectUnwatchedCount(item.TotalEpisode, watchedCount),
|
||
IsFinished: isPlayletFinished(item.TotalEpisode, watchedCount),
|
||
HistoryAt: history.UpdatedAt,
|
||
})
|
||
}
|
||
return result
|
||
}
|
||
|
||
// collectWatchedCount 读取已看集数,兼容仅保存watchedEpisodes但未写watchedCount的历史数据。
|
||
func collectWatchedCount(progress *playletwatchprogressmod.PlayletWatchProgress, totalEpisode int64) int64 {
|
||
if progress == nil {
|
||
return 0
|
||
}
|
||
watchedCount := progress.WatchedCount
|
||
if watchedCount <= 0 && len(progress.WatchedEpisodes) > 0 {
|
||
watchedCount = int64(len(progress.WatchedEpisodes))
|
||
}
|
||
if watchedCount < 0 {
|
||
return 0
|
||
}
|
||
if totalEpisode > 0 && watchedCount > totalEpisode {
|
||
return totalEpisode
|
||
}
|
||
return watchedCount
|
||
}
|
||
|
||
// collectUnwatchedCount 计算未看集数,避免历史脏数据导致负数。
|
||
func collectUnwatchedCount(totalEpisode int64, watchedCount int64) int64 {
|
||
if totalEpisode <= watchedCount {
|
||
return 0
|
||
}
|
||
return totalEpisode - watchedCount
|
||
}
|
||
|
||
// isPlayletFinished 判断短剧是否已看完。
|
||
func isPlayletFinished(totalEpisode int64, watchedCount int64) bool {
|
||
return totalEpisode > 0 && watchedCount >= totalEpisode
|
||
}
|
||
|
||
// collectLastChapterId 获取最近观看剧集ID,无有效进度时返回0。
|
||
func collectLastChapterId(progress *playletwatchprogressmod.PlayletWatchProgress) int64 {
|
||
if progress == nil || progress.LastChapterId < 0 {
|
||
return 0
|
||
}
|
||
return progress.LastChapterId
|
||
}
|
||
|
||
// collectLastEpisode 获取最近观看集数,无有效进度时返回0。
|
||
func collectLastEpisode(progress *playletwatchprogressmod.PlayletWatchProgress) int64 {
|
||
if progress == nil || progress.LastEpisode < 0 {
|
||
return 0
|
||
}
|
||
return progress.LastEpisode
|
||
}
|
||
|
||
// findPlayletChapterUserOperateStatus 查询当前用户短剧剧集点赞和收藏状态
|
||
func findPlayletChapterUserOperateStatus(ctx context.Context, userId int64, chapterId int64) (int64, int64, *srvdef.SrvErr) {
|
||
// 游客或无有效token时没有用户ID,直接返回未点赞、未收藏,保证剧集详情接口可匿名访问。
|
||
if userId <= 0 {
|
||
return operateStatusNo, operateStatusNo, nil
|
||
}
|
||
likeList, err := playletchapterlikemod.FindManyPlayletChapterLike(ctx, bson.M{
|
||
playletchapterlikemod.FN.PlayletChapterLike.UserId: userId,
|
||
playletchapterlikemod.FN.PlayletChapterLike.ChapterId: chapterId,
|
||
}, options.Find().
|
||
SetLimit(1).
|
||
SetProjection(bson.D{{Key: playletchapterlikemod.FN.PlayletChapterLike.Id, Value: 1}}))
|
||
if err != nil {
|
||
log.Error("查询短剧剧集用户点赞状态失败", log.Any("用户ID", userId), log.Any("剧集ID", chapterId), log.E(err))
|
||
return operateStatusNo, operateStatusNo, err
|
||
}
|
||
collectList, err := playletchaptercollectmod.FindManyPlayletChapterCollect(ctx, bson.M{
|
||
playletchaptercollectmod.FN.PlayletChapterCollect.UserId: userId,
|
||
playletchaptercollectmod.FN.PlayletChapterCollect.ChapterId: chapterId,
|
||
}, options.Find().
|
||
SetLimit(1).
|
||
SetProjection(bson.D{{Key: playletchaptercollectmod.FN.PlayletChapterCollect.Id, Value: 1}}))
|
||
if err != nil {
|
||
log.Error("查询短剧剧集用户收藏状态失败", log.Any("用户ID", userId), log.Any("剧集ID", chapterId), log.E(err))
|
||
return operateStatusNo, operateStatusNo, err
|
||
}
|
||
isLike := operateStatusByFlag(len(likeList) > 0)
|
||
isCollect := operateStatusByFlag(len(collectList) > 0)
|
||
return isLike, isCollect, nil
|
||
}
|
||
|
||
// incrementPlayletBrowse 原子增加短剧观看量和热度值
|
||
func incrementPlayletBrowse(ctx context.Context, id int64) *srvdef.SrvErr {
|
||
// 不更新updatedAt,避免详情访问影响今日更新排序。
|
||
return playletmod.UpdateOnePlaylet(ctx, homeUpPlayletFilter(bson.M{
|
||
playletmod.FN.Playlet.Id: id,
|
||
}), bson.M{
|
||
"$inc": bson.M{
|
||
playletmod.FN.Playlet.CountBrowse: 1,
|
||
playletmod.FN.Playlet.HeatValue: playletHeatClickInc,
|
||
},
|
||
})
|
||
}
|
||
|
||
func incrementPlayletHeatValue(ctx context.Context, id int64, delta int64) *srvdef.SrvErr {
|
||
if id <= 0 || delta == 0 {
|
||
return nil
|
||
}
|
||
return playletmod.UpdateOnePlaylet(ctx, bson.M{
|
||
playletmod.FN.Playlet.Id: id,
|
||
}, bson.M{
|
||
"$inc": bson.M{
|
||
playletmod.FN.Playlet.HeatValue: delta,
|
||
},
|
||
})
|
||
}
|
||
|
||
// checkPlayletForwardHeatLimit 校验同一用户或IP对同一短剧的转发加热频控
|
||
func checkPlayletForwardHeatLimit(userId int64, ip string, playletId int64) (bool, error) {
|
||
operator := strings.TrimSpace(ip)
|
||
if userId > 0 {
|
||
operator = "user-" + strconv.FormatInt(userId, 10)
|
||
}
|
||
if operator == "" {
|
||
operator = "unknown"
|
||
}
|
||
ok, err := redis.Setnx_NewOK(
|
||
redisconst.AppPlayletForwardHeatKey(operator, playletId),
|
||
1,
|
||
redisconst.AppPlayletForwardHeatExpire,
|
||
)
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
return !ok, nil
|
||
}
|
||
|
||
// findPlayletForwardHeatTarget 查询转发加热目标短剧,用于频控命中时返回当前热度
|
||
func findPlayletForwardHeatTarget(ctx context.Context, id int64) (*playletmod.Playlet, *srvdef.SrvErr) {
|
||
playlet, err := playletmod.FindOnePlaylet(ctx, homeUpPlayletFilter(bson.M{
|
||
playletmod.FN.Playlet.Id: id,
|
||
}), options.FindOne().SetProjection(bson.D{
|
||
{Key: playletmod.FN.Playlet.Id, Value: 1},
|
||
{Key: playletmod.FN.Playlet.HeatValue, Value: 1},
|
||
}))
|
||
if err != nil {
|
||
log.Error("查询短剧转发热度目标失败", log.Any("短剧ID", id), log.E(err))
|
||
return nil, err
|
||
}
|
||
return playlet, nil
|
||
}
|
||
|
||
// incrementPlayletHeatValueAndReturn 原子增加短剧热度值并返回更新后的热度
|
||
func incrementPlayletHeatValueAndReturn(ctx context.Context, id int64, delta int64) (*playletmod.Playlet, *srvdef.SrvErr) {
|
||
if id <= 0 || delta == 0 {
|
||
return nil, srvdef.New(srvdef.ErrParamError, "短剧ID不能为空", "短剧ID不能为空")
|
||
}
|
||
return playletmod.FindOneAndUpdatePlaylet(ctx, homeUpPlayletFilter(bson.M{
|
||
playletmod.FN.Playlet.Id: id,
|
||
}), bson.M{
|
||
"$inc": bson.M{
|
||
playletmod.FN.Playlet.HeatValue: delta,
|
||
},
|
||
}, options.FindOneAndUpdate().SetProjection(bson.D{
|
||
{Key: playletmod.FN.Playlet.Id, Value: 1},
|
||
{Key: playletmod.FN.Playlet.HeatValue, Value: 1},
|
||
}))
|
||
}
|
||
|
||
// incrementPlayletChapterBrowse 原子增加短剧剧集观看量
|
||
func incrementPlayletChapterBrowse(ctx context.Context, id int64) *srvdef.SrvErr {
|
||
// 只递增剧集countBrowse,不更新updatedAt,避免播放行为影响后台维护时间和排序逻辑。
|
||
return playletchaptermod.UpdateOnePlayletChapter(ctx, bson.M{
|
||
playletchaptermod.FN.PlayletChapter.Id: id,
|
||
}, bson.M{
|
||
"$inc": bson.M{
|
||
playletchaptermod.FN.PlayletChapter.CountBrowse: 1,
|
||
},
|
||
})
|
||
}
|
||
|
||
// findPlayletChapters 查询短剧全部章节,并批量填充当前用户播放权限。
|
||
// 原创短剧只返回已有水印地址的剧集:首次未处理好的不露出,换水印期间仍可播旧地址。
|
||
func findPlayletChapters(ctx context.Context, playletId int64, userId int64, isOriginal bool) ([]proto.PlayletChapterData, *srvdef.SrvErr) {
|
||
filter := bson.M{
|
||
playletchaptermod.FN.PlayletChapter.PlayletId: playletId,
|
||
}
|
||
if isOriginal {
|
||
filter[playletchaptermod.FN.PlayletChapter.WatermarkVideoUrl] = bson.M{"$gt": ""}
|
||
}
|
||
// 详情页播放区需要展示完整选集列表,这里按剧集序号一次性查询全部章节,避免前端再发一次章节请求
|
||
list, err := playletchaptermod.FindManyPlayletChapter(ctx, filter, options.Find().
|
||
SetProjection(playletChapterInfoProjection).
|
||
SetSort(bson.D{
|
||
{Key: playletchaptermod.FN.PlayletChapter.CurrentEpisode, Value: 1},
|
||
{Key: playletchaptermod.FN.PlayletChapter.Id, Value: 1},
|
||
}))
|
||
if err != nil {
|
||
log.Error("查询短剧章节列表失败", log.Any("短剧ID", playletId), log.E(err))
|
||
return nil, err
|
||
}
|
||
access, err := buildPlayletChapterAccessContext(ctx, userId, playletId, list)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
result := make([]proto.PlayletChapterData, 0, len(list))
|
||
for _, chapter := range list {
|
||
if chapter == nil {
|
||
continue
|
||
}
|
||
result = append(result, playletChapterToData(chapter, access))
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
// buildPlayletChapterAccessContext 批量查询详情章节列表所需的用户权限数据。
|
||
func buildPlayletChapterAccessContext(ctx context.Context, userId int64, playletId int64, chapters playletchaptermod.PlayletChapterList) (*playletChapterAccessContext, *srvdef.SrvErr) {
|
||
access := &playletChapterAccessContext{
|
||
unlockedChapterIds: make(map[int64]bool),
|
||
}
|
||
if userId <= 0 {
|
||
return access, nil
|
||
}
|
||
user, err := findPlayletChapterAccessUser(ctx, userId)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
access.balance = user.Recharge
|
||
access.vipValid = isUserVipValid(user)
|
||
|
||
coinChapterIds := collectCoinPayChapterIds(chapters)
|
||
if len(coinChapterIds) == 0 {
|
||
return access, nil
|
||
}
|
||
unlocks, err := playletchapterunlockmod.FindManyPlayletChapterUnlock(ctx, bson.M{
|
||
playletchapterunlockmod.FN.PlayletChapterUnlock.UserId: userId,
|
||
playletchapterunlockmod.FN.PlayletChapterUnlock.PlayletId: playletId,
|
||
playletchapterunlockmod.FN.PlayletChapterUnlock.ChapterId: bson.M{"$in": coinChapterIds},
|
||
}, options.Find().SetProjection(bson.D{
|
||
{Key: playletchapterunlockmod.FN.PlayletChapterUnlock.ChapterId, Value: 1},
|
||
}))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
for _, item := range unlocks {
|
||
if item == nil || item.ChapterId <= 0 {
|
||
continue
|
||
}
|
||
access.unlockedChapterIds[item.ChapterId] = true
|
||
}
|
||
return access, nil
|
||
}
|
||
|
||
// collectCoinPayChapterIds 收集金币付费剧集ID,减少解锁记录查询范围。
|
||
func collectCoinPayChapterIds(chapters playletchaptermod.PlayletChapterList) []int64 {
|
||
ids := make([]int64, 0)
|
||
for _, chapter := range chapters {
|
||
if chapter == nil || chapter.Id <= 0 || chapter.PayType != playletchaptermod.PayTypeCoin {
|
||
continue
|
||
}
|
||
ids = append(ids, chapter.Id)
|
||
}
|
||
return ids
|
||
}
|
||
|
||
// findRelatedRecommendPlaylets 查询相关推荐短剧
|
||
func findRelatedRecommendPlaylets(ctx context.Context, item *playletmod.Playlet) (playletmod.PlayletList, *srvdef.SrvErr) {
|
||
// 按相同题材匹配相关推荐,排除当前短剧,并按热度值倒序返回8条数据。
|
||
result := make(playletmod.PlayletList, 0)
|
||
if item == nil || len(item.ThemeIds) == 0 {
|
||
return result, nil
|
||
}
|
||
list, err := findHomePlaylets(ctx, homeUpPlayletFilter(bson.M{
|
||
playletmod.FN.Playlet.Id: bson.M{"$ne": item.Id},
|
||
playletmod.FN.Playlet.ThemeIds: bson.M{"$in": item.ThemeIds},
|
||
}), bson.D{
|
||
{Key: playletmod.FN.Playlet.HeatValue, Value: -1},
|
||
{Key: playletmod.FN.Playlet.Id, Value: -1},
|
||
}, relatedLimit)
|
||
if err != nil {
|
||
log.Error("查询相关推荐短剧失败", log.Any("短剧ID", item.Id), log.E(err))
|
||
return nil, err
|
||
}
|
||
return list, nil
|
||
}
|
||
|
||
// playletChapterToData 转换短剧章节响应数据
|
||
func playletChapterToData(item *playletchaptermod.PlayletChapter, access *playletChapterAccessContext) proto.PlayletChapterData {
|
||
if item == nil {
|
||
return proto.PlayletChapterData{}
|
||
}
|
||
videoURL := playletChapterPlayVideoUrl(item)
|
||
data := proto.PlayletChapterData{
|
||
Id: item.Id,
|
||
PlayletId: item.PlayletId,
|
||
CurrentEpisode: item.CurrentEpisode,
|
||
Title: item.Title,
|
||
Md5: item.Md5,
|
||
Hash: item.Hash,
|
||
Height: item.Height,
|
||
Width: item.Width,
|
||
Ratio: item.Ratio,
|
||
VideoUrl: videoURL,
|
||
MediaSize: item.MediaSize,
|
||
MediaTime: item.MediaTime,
|
||
PayType: int32(item.PayType),
|
||
SingleCoins: item.SingleCoins,
|
||
CostCoins: item.SingleCoins,
|
||
}
|
||
applyPlayletChapterDataWatchPermission(item, access, &data)
|
||
if !data.CanWatch {
|
||
videoURL = ""
|
||
}
|
||
data.VideoUrl = videoURL
|
||
return data
|
||
}
|
||
|
||
// applyPlayletChapterDataWatchPermission 填充详情页章节列表的播放权限状态。
|
||
func applyPlayletChapterDataWatchPermission(item *playletchaptermod.PlayletChapter, access *playletChapterAccessContext, data *proto.PlayletChapterData) {
|
||
if item == nil || data == nil {
|
||
return
|
||
}
|
||
if access != nil {
|
||
data.Balance = access.balance
|
||
data.VipValid = access.vipValid
|
||
}
|
||
switch item.PayType {
|
||
case playletchaptermod.PayTypeFree:
|
||
data.CanWatch = true
|
||
case playletchaptermod.PayTypeCoin:
|
||
unlocked := access != nil && access.unlockedChapterIds[item.Id]
|
||
data.IsUnlocked = unlocked
|
||
data.CanWatch = unlocked
|
||
data.NeedUnlock = !unlocked
|
||
case playletchaptermod.PayTypeVip:
|
||
vipValid := access != nil && access.vipValid
|
||
data.CanWatch = vipValid
|
||
data.NeedVip = !vipValid
|
||
default:
|
||
data.NeedUnlock = true
|
||
}
|
||
}
|
||
|
||
// playletChapterToInfoResp 转换短剧剧集详情响应数据
|
||
func playletChapterToInfoResp(item *playletchaptermod.PlayletChapter) *proto.PlayletChapterInfoResp {
|
||
// 剧集详情只返回播放页需要的当前剧集字段;播放地址优先水印地址,为空回退原片。
|
||
if item == nil {
|
||
return &proto.PlayletChapterInfoResp{}
|
||
}
|
||
return &proto.PlayletChapterInfoResp{
|
||
Id: item.Id,
|
||
PlayletId: item.PlayletId,
|
||
CurrentEpisode: item.CurrentEpisode,
|
||
Title: item.Title,
|
||
Md5: item.Md5,
|
||
Hash: item.Hash,
|
||
Height: item.Height,
|
||
Width: item.Width,
|
||
Ratio: item.Ratio,
|
||
VideoUrl: playletChapterPlayVideoUrl(item),
|
||
MediaSize: item.MediaSize,
|
||
MediaTime: item.MediaTime,
|
||
CountComment: item.CountComment,
|
||
CountBrowse: item.CountBrowse,
|
||
CountCollect: item.CountCollect,
|
||
CountLike: item.CountLike,
|
||
IsLike: operateStatusNo,
|
||
IsCollect: operateStatusNo,
|
||
PayType: int32(item.PayType),
|
||
SingleCoins: item.SingleCoins,
|
||
}
|
||
}
|
||
|
||
// ensureOriginalChapterWatermarkReady 原创剧集必须已有水印地址才允许播放/解锁。
|
||
func ensureOriginalChapterWatermarkReady(playlet *playletmod.Playlet, chapter *playletchaptermod.PlayletChapter) *srvdef.SrvErr {
|
||
if playlet == nil || !playlet.IsOriginal || chapter == nil {
|
||
return nil
|
||
}
|
||
if strings.TrimSpace(chapter.WatermarkVideoUrl) != "" {
|
||
return nil
|
||
}
|
||
return srvdef.New(srvdef.ErrNoData, "剧集处理中,请稍后观看", "剧集处理中,请稍后观看")
|
||
}
|
||
|
||
// playletChapterPlayVideoUrl 优先返回水印地址,为空时回退原片地址。
|
||
func playletChapterPlayVideoUrl(item *playletchaptermod.PlayletChapter) string {
|
||
if item == nil {
|
||
return ""
|
||
}
|
||
if u := strings.TrimSpace(item.WatermarkVideoUrl); u != "" {
|
||
return u
|
||
}
|
||
return item.VideoUrl
|
||
}
|
||
|
||
// buildPlayletDetailSeoInfo 组装短剧详情页SEO信息
|
||
func buildPlayletDetailSeoInfo(item *playletmod.Playlet, themeNameMap map[int64]string, tagNameMap map[int64]string) *proto.SeoInfo {
|
||
seo := configser.GetSiteSeoInfo()
|
||
if item == nil {
|
||
return &seo
|
||
}
|
||
// 优先使用后台配置的 SeoInfo,缺失字段再回退剧名/简介自动生成。
|
||
if title := strings.TrimSpace(item.SeoInfo.Title); title != "" {
|
||
seo.Title = title
|
||
} else if title := strings.TrimSpace(item.Title); title != "" {
|
||
seo.Title = title
|
||
}
|
||
if desc := strings.TrimSpace(item.Description); desc != "" {
|
||
seo.Description = desc
|
||
} else if desc := strings.TrimSpace(item.SeoInfo.Description); desc != "" {
|
||
seo.Description = desc
|
||
} else if desc := truncatePlayletSEODescription(strings.TrimSpace(item.Desc), 120); desc != "" {
|
||
seo.Description = desc
|
||
}
|
||
if len(item.SeoInfo.Keyworld) > 0 {
|
||
seo.Keywords = append([]string{}, item.SeoInfo.Keyworld...)
|
||
} else {
|
||
seo.Keywords = buildPlayletSEOKeywords(item, themeNameMap, tagNameMap, seo.Keywords)
|
||
}
|
||
seo.CanonicalURL = buildPlayletDetailCanonicalURL(seo.CanonicalURL, item.Slug, item.Id)
|
||
return &seo
|
||
}
|
||
|
||
// buildPlayletChapterDetailSeoInfo 组装短剧剧集详情页SEO信息
|
||
func buildPlayletChapterDetailSeoInfo(playlet *playletmod.Playlet, chapter *playletchaptermod.PlayletChapter) *proto.SeoInfo {
|
||
seo := configser.GetSiteSeoInfo()
|
||
if playlet == nil && chapter == nil {
|
||
return &seo
|
||
}
|
||
// 优先使用短剧后台配置的 SeoInfo,再回退到剧名/简介自动生成。
|
||
if playlet != nil {
|
||
if title := strings.TrimSpace(playlet.SeoInfo.Title); title != "" {
|
||
seo.Title = title
|
||
}
|
||
if desc := strings.TrimSpace(playlet.Description); desc != "" {
|
||
seo.Description = desc
|
||
} else if desc := strings.TrimSpace(playlet.SeoInfo.Description); desc != "" {
|
||
seo.Description = desc
|
||
}
|
||
if len(playlet.SeoInfo.Keyworld) > 0 {
|
||
seo.Keywords = append([]string{}, playlet.SeoInfo.Keyworld...)
|
||
}
|
||
}
|
||
if strings.TrimSpace(seo.Title) == "" {
|
||
if title := buildPlayletChapterSEOTitle(playlet, chapter); title != "" {
|
||
seo.Title = title
|
||
}
|
||
} else if chapter != nil {
|
||
// 已有短剧 SEO 标题时,补充剧集名便于播放页区分。
|
||
if episodeTitle := strings.TrimSpace(chapter.Title); episodeTitle != "" {
|
||
seo.Title = seo.Title + " " + episodeTitle
|
||
} else if chapter.CurrentEpisode > 0 {
|
||
seo.Title = seo.Title + " 第" + strconv.FormatInt(chapter.CurrentEpisode, 10) + "集"
|
||
}
|
||
}
|
||
if strings.TrimSpace(seo.Description) == "" && playlet != nil {
|
||
if desc := truncatePlayletSEODescription(strings.TrimSpace(playlet.Desc), 120); desc != "" {
|
||
seo.Description = desc
|
||
}
|
||
}
|
||
if len(seo.Keywords) == 0 {
|
||
if playlet != nil {
|
||
seo.Keywords = buildPlayletSEOKeywords(playlet, nil, nil, seo.Keywords)
|
||
} else {
|
||
seo.Keywords = buildPlayletChapterSEOKeywords(chapter, seo.Keywords)
|
||
}
|
||
}
|
||
if chapter != nil {
|
||
slug := ""
|
||
if playlet != nil {
|
||
slug = playlet.Slug
|
||
}
|
||
seo.CanonicalURL = buildPlayletChapterDetailCanonicalURL(seo.CanonicalURL, slug, chapter.PlayletId, chapter.Id)
|
||
}
|
||
return &seo
|
||
}
|
||
|
||
// buildPlayletListPageSeoInfo 组装短剧总列表页 SEO(无题材/分类筛选)。
|
||
func buildPlayletListPageSeoInfo() *proto.SeoInfo {
|
||
return buildPageSeoInfoFromValue(pubdef.SeoValue{}, "", "", "/list")
|
||
}
|
||
|
||
// buildPlayletThemePageSeoInfo 组装题材短剧列表页SEO信息(读题材缓存,避免每次查库)
|
||
func buildPlayletThemePageSeoInfo(themeId int64) *proto.SeoInfo {
|
||
if themeId <= 0 {
|
||
return buildPlayletListPageSeoInfo()
|
||
}
|
||
theme := playletcache.GetEnabledTheme(themeId)
|
||
if theme == nil {
|
||
return buildPlayletListPageSeoInfo()
|
||
}
|
||
return buildPageSeoInfoFromValue(theme.SeoInfo, theme.Name, "", buildPlayletThemeListCanonicalURL(theme.Slug, theme.Id))
|
||
}
|
||
|
||
// buildPlayletTagPageSeoInfo 组装标签短剧列表页SEO信息(读标签缓存,避免每次查库)
|
||
func buildPlayletTagPageSeoInfo(tagId int64) *proto.SeoInfo {
|
||
if tagId <= 0 {
|
||
return buildPlayletListPageSeoInfo()
|
||
}
|
||
tag := playletcache.GetEnabledTag(tagId)
|
||
if tag == nil {
|
||
return buildPageSeoInfoFromValue(pubdef.SeoValue{}, "短剧标签", "", buildPlayletTagListCanonicalURL("", tagId))
|
||
}
|
||
return buildPageSeoInfoFromValue(tag.SeoInfo, tag.Name, "", buildPlayletTagListCanonicalURL(tag.Slug, tag.Id))
|
||
}
|
||
|
||
// buildPlayletCategoryPageSeoInfo 组装分类短剧列表页SEO信息(读分类缓存,避免每次查库)
|
||
func buildPlayletCategoryPageSeoInfo(categoryId int64, fallbackName string) *proto.SeoInfo {
|
||
if categoryId <= 0 {
|
||
return buildPlayletListPageSeoInfo()
|
||
}
|
||
category := playletcache.GetEnabledCategory(categoryId)
|
||
canonical := buildPlayletCategoryListCanonicalURL("", categoryId)
|
||
if category != nil {
|
||
canonical = buildPlayletCategoryListCanonicalURL(category.Slug, category.Id)
|
||
}
|
||
if category == nil {
|
||
return buildPageSeoInfoFromValue(pubdef.SeoValue{}, fallbackName, "", canonical)
|
||
}
|
||
name := strings.TrimSpace(category.Name)
|
||
if name == "" {
|
||
name = fallbackName
|
||
}
|
||
return buildPageSeoInfoFromValue(category.SeoInfo, name, "", canonical)
|
||
}
|
||
|
||
// buildPageSeoInfoFromValue 将后台 SeoValue 转为页面 SeoInfo,缺失字段回退站点默认。
|
||
func buildPageSeoInfoFromValue(custom pubdef.SeoValue, fallbackTitle, fallbackDesc, canonicalURL string) *proto.SeoInfo {
|
||
seo := configser.GetSiteSeoInfo()
|
||
if title := strings.TrimSpace(custom.Title); title != "" {
|
||
seo.Title = title
|
||
} else if title := strings.TrimSpace(fallbackTitle); title != "" {
|
||
seo.Title = title
|
||
}
|
||
if desc := strings.TrimSpace(custom.Description); desc != "" {
|
||
seo.Description = desc
|
||
} else if desc := strings.TrimSpace(fallbackDesc); desc != "" {
|
||
seo.Description = desc
|
||
}
|
||
if len(custom.Keyworld) > 0 {
|
||
seo.Keywords = append([]string{}, custom.Keyworld...)
|
||
}
|
||
if canonicalURL != "" {
|
||
seo.CanonicalURL = buildPlayletCanonicalURL(seo.CanonicalURL, canonicalURL)
|
||
}
|
||
return &seo
|
||
}
|
||
|
||
// buildPlayletThemeListCanonicalURL 专题详情页相对路径(对齐 jha-1043-hgdj-web /topic/:slug/)
|
||
func buildPlayletThemeListCanonicalURL(slug string, themeId int64) string {
|
||
return playletSiteEntityPath("/topic", slug, themeId)
|
||
}
|
||
|
||
// buildPlayletTagListCanonicalURL 标签列表页相对路径(对齐 jha-1043-hgdj-web /tag/:slug/)
|
||
func buildPlayletTagListCanonicalURL(slug string, tagId int64) string {
|
||
return playletSiteEntityPath("/tag", slug, tagId)
|
||
}
|
||
|
||
// buildPlayletCategoryListCanonicalURL 分类列表页相对路径(对齐 jha-1043-hgdj-web /category/:slug/)
|
||
func buildPlayletCategoryListCanonicalURL(slug string, categoryId int64) string {
|
||
return playletSiteEntityPath("/category", slug, categoryId)
|
||
}
|
||
|
||
func playletSiteEntityPath(prefix, slug string, id int64) string {
|
||
if segment := utils.PathSegment(slug, id); segment != "" {
|
||
return strings.TrimSuffix(prefix, "/") + "/" + segment + "/"
|
||
}
|
||
if strings.HasSuffix(prefix, "/") {
|
||
return prefix
|
||
}
|
||
return prefix + "/"
|
||
}
|
||
|
||
// buildPlayletChapterSEOTitle 生成剧集详情页标题,优先展示“短剧名 + 剧集名”。
|
||
func buildPlayletChapterSEOTitle(playlet *playletmod.Playlet, chapter *playletchaptermod.PlayletChapter) string {
|
||
playletTitle := ""
|
||
if playlet != nil {
|
||
playletTitle = strings.TrimSpace(playlet.Title)
|
||
}
|
||
chapterTitle := ""
|
||
if chapter != nil {
|
||
chapterTitle = strings.TrimSpace(chapter.Title)
|
||
if chapterTitle == "" && chapter.CurrentEpisode > 0 {
|
||
chapterTitle = "第" + strconv.FormatInt(chapter.CurrentEpisode, 10) + "集"
|
||
}
|
||
}
|
||
if playletTitle == "" {
|
||
return chapterTitle
|
||
}
|
||
if chapterTitle == "" {
|
||
return playletTitle
|
||
}
|
||
return playletTitle + " " + chapterTitle
|
||
}
|
||
|
||
// buildPlayletSEOKeywords 合并短剧标题、题材、标签和全站关键词,并去重。
|
||
func buildPlayletSEOKeywords(item *playletmod.Playlet, themeNameMap map[int64]string, tagNameMap map[int64]string, siteKeywords []string) []string {
|
||
capacity := len(siteKeywords) + 1
|
||
if item != nil {
|
||
capacity += len(item.ThemeIds) + len(item.TagIds)
|
||
}
|
||
keywords := make([]string, 0, capacity)
|
||
seen := make(map[string]struct{}, cap(keywords))
|
||
add := func(value string) {
|
||
value = strings.TrimSpace(value)
|
||
if value == "" {
|
||
return
|
||
}
|
||
if _, ok := seen[value]; ok {
|
||
return
|
||
}
|
||
seen[value] = struct{}{}
|
||
keywords = append(keywords, value)
|
||
}
|
||
if item != nil {
|
||
add(item.Title)
|
||
for _, name := range namesByIds(item.ThemeIds, themeNameMap) {
|
||
add(name)
|
||
}
|
||
for _, name := range namesByIds(item.TagIds, tagNameMap) {
|
||
add(name)
|
||
}
|
||
}
|
||
for _, keyword := range siteKeywords {
|
||
add(keyword)
|
||
}
|
||
return keywords
|
||
}
|
||
|
||
// buildPlayletChapterSEOKeywords 合并剧集标题和全站关键词,并去重。
|
||
func buildPlayletChapterSEOKeywords(chapter *playletchaptermod.PlayletChapter, siteKeywords []string) []string {
|
||
keywords := make([]string, 0, len(siteKeywords)+1)
|
||
seen := make(map[string]struct{}, cap(keywords))
|
||
add := func(value string) {
|
||
value = strings.TrimSpace(value)
|
||
if value == "" {
|
||
return
|
||
}
|
||
if _, ok := seen[value]; ok {
|
||
return
|
||
}
|
||
seen[value] = struct{}{}
|
||
keywords = append(keywords, value)
|
||
}
|
||
if chapter != nil {
|
||
add(chapter.Title)
|
||
}
|
||
for _, keyword := range siteKeywords {
|
||
add(keyword)
|
||
}
|
||
return keywords
|
||
}
|
||
|
||
// truncatePlayletSEODescription 限制meta description长度,避免详情长文本直接写进SEO描述。
|
||
func truncatePlayletSEODescription(desc string, maxRunes int) string {
|
||
if maxRunes <= 0 || desc == "" {
|
||
return ""
|
||
}
|
||
runes := []rune(desc)
|
||
if len(runes) <= maxRunes {
|
||
return desc
|
||
}
|
||
return string(runes[:maxRunes])
|
||
}
|
||
|
||
// buildPlayletDetailCanonicalURL 拼接短剧详情页标准URL。
|
||
func buildPlayletDetailCanonicalURL(baseURL, slug string, id int64) string {
|
||
return buildPlayletCanonicalURL(baseURL, playletSiteEntityPath(playletH5DetailPath, slug, id))
|
||
}
|
||
|
||
// buildPlayletChapterDetailCanonicalURL 拼接短剧剧集详情页标准URL。
|
||
func buildPlayletChapterDetailCanonicalURL(baseURL, slug string, playletId int64, chapterId int64) string {
|
||
pagePath := playletSiteEntityPath(playletChapterH5DetailPath, slug, playletId)
|
||
if chapterId > 0 {
|
||
pagePath += strconv.FormatInt(chapterId, 10) + "/"
|
||
}
|
||
return buildPlayletCanonicalURL(baseURL, pagePath)
|
||
}
|
||
|
||
// buildPlayletCanonicalURL 使用全站Web地址拼接页面路径;未配置域名时返回相对路径。
|
||
func buildPlayletCanonicalURL(baseURL string, pagePath string) string {
|
||
baseURL = strings.TrimRight(strings.TrimSpace(baseURL), "/")
|
||
if baseURL == "" {
|
||
return pagePath
|
||
}
|
||
return baseURL + pagePath
|
||
}
|
||
|
||
// emptyPlayletSearchResp 构建短剧ES空分页响应
|
||
func emptyPlayletSearchResp(pageNum int64, pageSize int64) *proto.PlayletSearchResp {
|
||
// ES索引未创建或暂无数据时返回空列表,保持接口响应结构稳定。
|
||
return &proto.PlayletSearchResp{
|
||
TotalCount: 0,
|
||
PageSize: int(pageSize),
|
||
PageNum: int(pageNum),
|
||
List: make([]proto.CommonData, 0),
|
||
}
|
||
}
|
||
|
||
// buildHomePlayletResponse 查询并组装app首页短剧数据
|
||
func buildHomePlayletResponse(ctx context.Context) (*proto.HomePlayletResponse, *srvdef.SrvErr) {
|
||
// 定义返回
|
||
resp := &proto.HomePlayletResponse{
|
||
HomeBannerData: make([]*proto.HomeBannerData, 0),
|
||
HomeTodayUpdateData: make([]*proto.CommonData, 0),
|
||
HomeCategoryData: make([]*proto.HomeCategoryData, 0),
|
||
HomeRecommendThemes: make([]*proto.HomeRecommendTheme, 0),
|
||
Announcements: make([]*proto.AnnouncementItem, 0),
|
||
}
|
||
// Home首页数据响应
|
||
var bannerList homebannermod.HomeBannerList
|
||
var updateList playletmod.PlayletList
|
||
categoryPlayletMap := make(map[int64]playletmod.PlayletList)
|
||
var queryErr *srvdef.SrvErr
|
||
categories, err := findHomeCategories(ctx)
|
||
if err != nil {
|
||
log.Error("查询首页短剧分类失败", log.E(err))
|
||
return nil, err
|
||
}
|
||
var mu sync.Mutex
|
||
var wg sync.WaitGroup
|
||
runQuery := func(name string, target *playletmod.PlayletList, filter bson.M, sort bson.D, limit int64) {
|
||
defer wg.Done()
|
||
list, err := findHomePlaylets(ctx, filter, sort, limit)
|
||
if err != nil {
|
||
log.Error("查询首页短剧数据失败", log.Any("分区", name), log.E(err))
|
||
mu.Lock()
|
||
if queryErr == nil {
|
||
queryErr = err
|
||
}
|
||
mu.Unlock()
|
||
return
|
||
}
|
||
mu.Lock()
|
||
*target = list
|
||
mu.Unlock()
|
||
}
|
||
runCategoryQuery := func(category *playletcategorymod.PlayletCategory) {
|
||
defer wg.Done()
|
||
if category == nil {
|
||
return
|
||
}
|
||
filter, sort, qerr := playletRecommendListQuery(ctx, category.Id)
|
||
if qerr != nil {
|
||
log.Error("构建首页分类短剧查询失败", log.Any("分类ID", category.Id), log.Any("分类名称", category.Name), log.E(qerr))
|
||
mu.Lock()
|
||
if queryErr == nil {
|
||
queryErr = qerr
|
||
}
|
||
mu.Unlock()
|
||
return
|
||
}
|
||
var list playletmod.PlayletList
|
||
var err *srvdef.SrvErr
|
||
if _, ok := filter[playletmod.FN.Playlet.CategoryIds]; ok {
|
||
list, err = playletmod.FindManyPlayletByCategoryRank(ctx, filter, category.Id, 0, homeSectionLimit, playletmod.CategoryRankSort(), playletHomeProjection)
|
||
} else {
|
||
list, err = findHomePlaylets(ctx, filter, sort, homeSectionLimit)
|
||
}
|
||
if err != nil {
|
||
log.Error("查询首页分类短剧数据失败", log.Any("分类ID", category.Id), log.Any("分类名称", category.Name), log.E(err))
|
||
mu.Lock()
|
||
if queryErr == nil {
|
||
queryErr = err
|
||
}
|
||
mu.Unlock()
|
||
return
|
||
}
|
||
mu.Lock()
|
||
categoryPlayletMap[category.Id] = list
|
||
mu.Unlock()
|
||
}
|
||
runBannerQuery := func() {
|
||
defer wg.Done()
|
||
list, err := findHomeBanners(ctx)
|
||
if err != nil {
|
||
log.Error("查询首页banner数据失败", log.E(err))
|
||
mu.Lock()
|
||
if queryErr == nil {
|
||
queryErr = err
|
||
}
|
||
mu.Unlock()
|
||
return
|
||
}
|
||
mu.Lock()
|
||
bannerList = list
|
||
mu.Unlock()
|
||
}
|
||
wg.Add(2 + len(categories))
|
||
go runBannerQuery()
|
||
// 最近更新:只展示已上架短剧,按上架时间从近到远排序。
|
||
go runQuery("最近更新", &updateList, homeUpPlayletFilter(nil), bson.D{
|
||
{Key: playletmod.FN.Playlet.OnlineAt, Value: -1},
|
||
{Key: playletmod.FN.Playlet.Id, Value: -1},
|
||
}, homeSectionLimit)
|
||
for _, category := range categories {
|
||
go runCategoryQuery(category)
|
||
}
|
||
wg.Wait()
|
||
if queryErr != nil {
|
||
return nil, queryErr
|
||
}
|
||
allPlaylets := mergeHomePlayletLists(updateList, mergeHomeCategoryPlayletLists(categoryPlayletMap))
|
||
themeNameMap, err := buildHomeThemeNameMap(ctx, allPlaylets)
|
||
if err != nil {
|
||
log.Error("查询首页短剧题材名称失败", log.E(err))
|
||
return nil, err
|
||
}
|
||
tagNameMap, err := buildHomeTagNameMap(ctx, allPlaylets)
|
||
if err != nil {
|
||
log.Error("查询首页短剧标签名称失败", log.E(err))
|
||
return nil, err
|
||
}
|
||
resp.HomeBannerData = homeBannersToHomeBannerData(ctx, bannerList)
|
||
resp.HomeTodayUpdateData = playletsToHomeCommonData(ctx, updateList, themeNameMap, tagNameMap)
|
||
resp.HomeCategoryData = playletCategoriesToHomeData(ctx, categories, categoryPlayletMap, themeNameMap, tagNameMap)
|
||
seo := configser.GetSiteSeoInfo()
|
||
resp.SeoInfo = &seo
|
||
return resp, nil
|
||
}
|
||
|
||
func attachHomeAnnouncements(resp *proto.HomePlayletResponse) {
|
||
if resp == nil {
|
||
return
|
||
}
|
||
resp.Announcements = configser.GetAnnouncements()
|
||
}
|
||
|
||
func attachHomeRecommendThemes(ctx context.Context, resp *proto.HomePlayletResponse) {
|
||
if resp == nil {
|
||
return
|
||
}
|
||
list := playletcache.ListRecommendedThemes(homeRecommendThemeLimit)
|
||
items := make([]*proto.HomeRecommendTheme, 0, len(list))
|
||
for _, item := range list {
|
||
if item == nil {
|
||
continue
|
||
}
|
||
items = append(items, &proto.HomeRecommendTheme{
|
||
Id: item.Id,
|
||
Name: item.Name,
|
||
Slug: item.Slug,
|
||
Icon: withImageDomain(ctx, item.Icon),
|
||
Rank: item.Rank,
|
||
PlayletCount: item.PlayletCount,
|
||
})
|
||
}
|
||
resp.HomeRecommendThemes = items
|
||
}
|
||
|
||
func guessLikeCommonData(ctx context.Context) []*proto.CommonData {
|
||
list := playletcache.ListGuessLike()
|
||
themeNameMap, _ := buildHomeThemeNameMap(ctx, list)
|
||
tagNameMap, _ := buildHomeTagNameMap(ctx, list)
|
||
return playletsToHomeCommonData(ctx, list, themeNameMap, tagNameMap)
|
||
}
|
||
|
||
// findHomeCategories 查询首页展示的启用短剧分类,按分类 rank 倒序排序。
|
||
func findHomeCategories(ctx context.Context) (playletcategorymod.PlayletCategoryList, *srvdef.SrvErr) {
|
||
return playletcategorymod.FindManyPlayletCategory(ctx, bson.M{
|
||
playletcategorymod.FN.PlayletCategory.Status: true,
|
||
}, options.Find().
|
||
SetProjection(bson.D{
|
||
{Key: playletcategorymod.FN.PlayletCategory.Id, Value: 1},
|
||
{Key: playletcategorymod.FN.PlayletCategory.Name, Value: 1},
|
||
{Key: playletcategorymod.FN.PlayletCategory.Slug, Value: 1},
|
||
{Key: playletcategorymod.FN.PlayletCategory.Desc, Value: 1},
|
||
{Key: playletcategorymod.FN.PlayletCategory.Icon, Value: 1},
|
||
{Key: playletcategorymod.FN.PlayletCategory.Rank, Value: 1},
|
||
}).
|
||
SetSort(bson.D{
|
||
{Key: playletcategorymod.FN.PlayletCategory.Rank, Value: -1},
|
||
{Key: playletcategorymod.FN.PlayletCategory.Id, Value: -1},
|
||
}))
|
||
}
|
||
|
||
// findHomeBanners 查询启用的首页 banner
|
||
func findHomeBanners(ctx context.Context) (homebannermod.HomeBannerList, *srvdef.SrvErr) {
|
||
return homebannermod.FindManyHomeBanner(ctx, bson.M{
|
||
homebannermod.FN.HomeBanner.Status: true,
|
||
}, options.Find().
|
||
SetProjection(homeBannerAppProjection).
|
||
SetSort(bson.D{
|
||
{Key: homebannermod.FN.HomeBanner.Rank, Value: -1},
|
||
{Key: homebannermod.FN.HomeBanner.CreatedAt, Value: -1},
|
||
{Key: homebannermod.FN.HomeBanner.Id, Value: -1},
|
||
}).
|
||
SetLimit(homeBannerLimit))
|
||
}
|
||
|
||
// homeCategoryPlayletFilter 构建首页分类分区短剧过滤条件。
|
||
func homeCategoryPlayletFilter(categoryId int64) bson.M {
|
||
return homeUpPlayletFilter(bson.M{
|
||
playletmod.FN.Playlet.CategoryIds: categoryId,
|
||
})
|
||
}
|
||
|
||
// homeUpPlayletFilter 构建首页短剧上架过滤条件
|
||
func homeUpPlayletFilter(extra bson.M) bson.M {
|
||
filter := bson.M{playletmod.FN.Playlet.Status: playletmod.StatusUp}
|
||
for key, value := range extra {
|
||
filter[key] = value
|
||
}
|
||
return filter
|
||
}
|
||
|
||
// homePlayletCacheKey 获取app首页短剧缓存key
|
||
func homePlayletCacheKey() string {
|
||
return redisconst.AppPlayletHomeCacheDataKey(homePlayletCacheVersion() + ":" + homePlayletCacheSchema)
|
||
}
|
||
|
||
// playletRecommendListCacheKey 获取app分类短剧列表缓存key
|
||
func playletRecommendListCacheKey(categoryId int64, pageNum int64, pageSize int64) string {
|
||
return redisconst.AppPlayletRecommendListCacheKey(homePlayletCacheVersion(), categoryId, pageNum, pageSize)
|
||
}
|
||
|
||
// homePlayletCacheVersion 获取app首页短剧缓存版本
|
||
func homePlayletCacheVersion() string {
|
||
if redis.Client() == nil {
|
||
return "0"
|
||
}
|
||
value, err := redis.Get(redisconst.AppPlayletHomeCacheVersionKey())
|
||
if err != nil {
|
||
log.Warn("读取app首页短剧缓存版本失败", log.E(err))
|
||
return "0"
|
||
}
|
||
if value == nil || strings.TrimSpace(*value) == "" {
|
||
return "0"
|
||
}
|
||
return strings.TrimSpace(*value)
|
||
}
|
||
|
||
// getHomePlayletCache 读取app首页短剧缓存
|
||
func getHomePlayletCache(cacheKey string) *proto.HomePlayletResponse {
|
||
if redis.Client() == nil || cacheKey == "" {
|
||
return nil
|
||
}
|
||
value, err := redis.Get(cacheKey)
|
||
if err != nil {
|
||
log.Warn("读取app首页短剧缓存失败", log.E(err))
|
||
return nil
|
||
}
|
||
if value == nil || *value == "" {
|
||
return nil
|
||
}
|
||
var resp proto.HomePlayletResponse
|
||
if err = json.Unmarshal([]byte(*value), &resp); err != nil {
|
||
log.Warn("解析app首页短剧缓存失败", log.E(err))
|
||
return nil
|
||
}
|
||
return &resp
|
||
}
|
||
|
||
// setHomePlayletCache 写入app首页短剧缓存
|
||
func setHomePlayletCache(cacheKey string, resp *proto.HomePlayletResponse) {
|
||
if redis.Client() == nil || cacheKey == "" || resp == nil {
|
||
return
|
||
}
|
||
data, err := json.Marshal(resp)
|
||
if err != nil {
|
||
log.Warn("序列化app首页短剧缓存失败", log.E(err))
|
||
return
|
||
}
|
||
if err = redis.Set(cacheKey, data, redisconst.AppPlayletHomeCacheExpire); err != nil {
|
||
log.Warn("写入app首页短剧缓存失败", log.E(err))
|
||
}
|
||
}
|
||
|
||
// getPlayletRecommendListCache 读取app分类短剧列表缓存
|
||
func getPlayletRecommendListCache(cacheKey string) *proto.PlayletRecommendListResp {
|
||
if redis.Client() == nil || cacheKey == "" {
|
||
return nil
|
||
}
|
||
value, err := redis.Get(cacheKey)
|
||
if err != nil {
|
||
log.Warn("读取app分类短剧列表缓存失败", log.E(err))
|
||
return nil
|
||
}
|
||
if value == nil || *value == "" {
|
||
return nil
|
||
}
|
||
var resp proto.PlayletRecommendListResp
|
||
if err = json.Unmarshal([]byte(*value), &resp); err != nil {
|
||
log.Warn("解析app分类短剧列表缓存失败", log.E(err))
|
||
return nil
|
||
}
|
||
return &resp
|
||
}
|
||
|
||
// setPlayletRecommendListCache 写入app分类短剧列表缓存
|
||
func setPlayletRecommendListCache(cacheKey string, resp *proto.PlayletRecommendListResp) {
|
||
if redis.Client() == nil || cacheKey == "" || resp == nil || len(resp.List) == 0 {
|
||
return
|
||
}
|
||
data, err := json.Marshal(resp)
|
||
if err != nil {
|
||
log.Warn("序列化app分类短剧列表缓存失败", log.E(err))
|
||
return
|
||
}
|
||
if err = redis.Set(cacheKey, data, redisconst.AppPlayletRecommendListExpire); err != nil {
|
||
log.Warn("写入app分类短剧列表缓存失败", log.E(err))
|
||
}
|
||
}
|
||
|
||
// findHomePlaylets 查询首页分区或列表短剧数据
|
||
func findHomePlaylets(ctx context.Context, filter bson.M, sort bson.D, limit int64, extraOpts ...*options.FindOptions) (playletmod.PlayletList, *srvdef.SrvErr) {
|
||
opt := options.Find().
|
||
SetProjection(playletHomeProjection).
|
||
SetSort(sort).
|
||
SetLimit(limit)
|
||
for _, extra := range extraOpts {
|
||
if extra == nil {
|
||
continue
|
||
}
|
||
if extra.Skip != nil {
|
||
opt.SetSkip(*extra.Skip)
|
||
}
|
||
}
|
||
return playletmod.FindManyPlaylet(ctx, filter, opt)
|
||
}
|
||
|
||
// mergeHomePlayletLists 合并首页各分区短剧,便于后续批量查询题材和标签名称
|
||
func mergeHomePlayletLists(lists ...playletmod.PlayletList) playletmod.PlayletList {
|
||
total := 0
|
||
for _, list := range lists {
|
||
total += len(list)
|
||
}
|
||
result := make(playletmod.PlayletList, 0, total)
|
||
for _, list := range lists {
|
||
result = append(result, list...)
|
||
}
|
||
return result
|
||
}
|
||
|
||
// mergeHomeCategoryPlayletLists 合并首页所有分类短剧列表。
|
||
func mergeHomeCategoryPlayletLists(categoryPlayletMap map[int64]playletmod.PlayletList) playletmod.PlayletList {
|
||
total := 0
|
||
for _, list := range categoryPlayletMap {
|
||
total += len(list)
|
||
}
|
||
result := make(playletmod.PlayletList, 0, total)
|
||
for _, list := range categoryPlayletMap {
|
||
result = append(result, list...)
|
||
}
|
||
return result
|
||
}
|
||
|
||
// playletCategoriesToHomeData 转换首页分类短剧响应数据。
|
||
func playletCategoriesToHomeData(ctx context.Context, categories playletcategorymod.PlayletCategoryList, categoryPlayletMap map[int64]playletmod.PlayletList, themeNameMap map[int64]string, tagNameMap map[int64]string) []*proto.HomeCategoryData {
|
||
result := make([]*proto.HomeCategoryData, 0, len(categories))
|
||
for _, category := range categories {
|
||
if category == nil {
|
||
continue
|
||
}
|
||
result = append(result, &proto.HomeCategoryData{
|
||
Id: category.Id,
|
||
Name: category.Name,
|
||
Slug: category.Slug,
|
||
Desc: category.Desc,
|
||
Icon: category.Icon,
|
||
Rank: category.Rank,
|
||
List: playletsToHomeCommonData(ctx, categoryPlayletMap[category.Id], themeNameMap, tagNameMap),
|
||
})
|
||
}
|
||
return result
|
||
}
|
||
|
||
// buildHomeThemeNameMap 从缓存批量取首页短剧题材名称
|
||
func buildHomeThemeNameMap(ctx context.Context, list playletmod.PlayletList) (map[int64]string, *srvdef.SrvErr) {
|
||
themeIds := collectHomeIds(list, func(item *playletmod.Playlet) []int64 {
|
||
return item.ThemeIds
|
||
})
|
||
return playletcache.ThemeNameMap(themeIds), nil
|
||
}
|
||
|
||
// buildHomeTagNameMap 从缓存批量取首页短剧标签名称
|
||
func buildHomeTagNameMap(ctx context.Context, list playletmod.PlayletList) (map[int64]string, *srvdef.SrvErr) {
|
||
tagIds := collectHomeIds(list, func(item *playletmod.Playlet) []int64 {
|
||
return item.TagIds
|
||
})
|
||
return playletcache.TagNameMap(tagIds), nil
|
||
}
|
||
|
||
// collectHomeIds 汇总并去重首页短剧的题材或标签ID
|
||
func collectHomeIds(list playletmod.PlayletList, pick func(item *playletmod.Playlet) []int64) []int64 {
|
||
result := make([]int64, 0)
|
||
seen := make(map[int64]struct{})
|
||
for _, item := range list {
|
||
if item == nil {
|
||
continue
|
||
}
|
||
for _, id := range pick(item) {
|
||
if id <= 0 {
|
||
continue
|
||
}
|
||
if _, ok := seen[id]; ok {
|
||
continue
|
||
}
|
||
seen[id] = struct{}{}
|
||
result = append(result, id)
|
||
}
|
||
}
|
||
return result
|
||
}
|
||
|
||
// homeBannersToHomeBannerData 转换首页banner响应数据
|
||
func homeBannersToHomeBannerData(ctx context.Context, list homebannermod.HomeBannerList) []*proto.HomeBannerData {
|
||
result := make([]*proto.HomeBannerData, 0, len(list))
|
||
for _, item := range list {
|
||
if item == nil {
|
||
continue
|
||
}
|
||
result = append(result, &proto.HomeBannerData{
|
||
Id: item.Id,
|
||
Name: item.Name,
|
||
Image: withImageDomain(ctx, item.Image),
|
||
VerticalImage: withImageDomain(ctx, item.VerticalImage),
|
||
Url: item.Url,
|
||
Rank: item.Rank,
|
||
Status: item.Status,
|
||
})
|
||
}
|
||
return result
|
||
}
|
||
|
||
// playletsToHomeCommonData 转换首页通用短剧响应数据
|
||
func playletsToHomeCommonData(ctx context.Context, list playletmod.PlayletList, themeNameMap map[int64]string, tagNameMap map[int64]string) []*proto.CommonData {
|
||
result := make([]*proto.CommonData, 0, len(list))
|
||
for _, item := range list {
|
||
if item == nil {
|
||
continue
|
||
}
|
||
if len(item.Description) > 0 {
|
||
item.SeoInfo.Description = item.Description
|
||
}
|
||
result = append(result, &proto.CommonData{
|
||
Id: item.Id,
|
||
Status: int32(item.Status),
|
||
IsFeatured: item.IsFeatured,
|
||
IsRecommend: item.IsRecommend,
|
||
IsNew: isOnlineThisWeek(item.OnlineAt),
|
||
OperateType: int32(playletmod.NormalizeOperateType(item.OperateType)),
|
||
ShowOnBanner: item.ShowOnBanner,
|
||
Banner: withImageDomain(ctx, item.Banner),
|
||
OnlineAt: item.OnlineAt,
|
||
MediaCreatedAt: item.MediaCreatedAt,
|
||
MediaUpdatedAt: item.MediaUpdatedAt,
|
||
ThemeIds: item.ThemeIds,
|
||
ThemeNames: namesByIds(item.ThemeIds, themeNameMap),
|
||
ThemeSlugs: slugsByIds(item.ThemeIds, playletcache.ThemeSlugMap(item.ThemeIds)),
|
||
Title: item.Title,
|
||
PlayName: item.PlayName,
|
||
Description: item.Description,
|
||
Slug: item.Slug,
|
||
Desc: item.Desc,
|
||
Anchor: item.Anchor,
|
||
Cover: withImageDomain(ctx, item.DisplayCover()),
|
||
TagIds: item.TagIds,
|
||
TagNames: namesByIds(item.TagIds, tagNameMap),
|
||
TagSlugs: slugsByIds(item.TagIds, playletcache.TagSlugMap(item.TagIds)),
|
||
TotalEpisode: item.TotalEpisode,
|
||
UpdateStatus: item.UpdateStatus,
|
||
CountBrowse: item.CountBrowse,
|
||
HeatValue: item.HeatValue,
|
||
CountCollect: item.CountCollect,
|
||
CountLike: displayPlayletCountLike(item.CountLike, item.VirtualCountLike),
|
||
CreatedAt: item.CreatedAt,
|
||
UpdatedAt: item.UpdatedAt,
|
||
SeoInfo: item.SeoInfo,
|
||
})
|
||
}
|
||
return result
|
||
}
|
||
|
||
// slugsByIds 按ID顺序返回 slug
|
||
func slugsByIds(ids []int64, slugMap map[int64]string) []string {
|
||
result := make([]string, 0, len(ids))
|
||
for _, id := range ids {
|
||
if slug := slugMap[id]; slug != "" {
|
||
result = append(result, slug)
|
||
}
|
||
}
|
||
return result
|
||
}
|
||
|
||
// namesByIds 按ID顺序返回名称
|
||
func namesByIds(ids []int64, nameMap map[int64]string) []string {
|
||
result := make([]string, 0, len(ids))
|
||
for _, id := range ids {
|
||
if name := nameMap[id]; name != "" {
|
||
result = append(result, name)
|
||
}
|
||
}
|
||
return result
|
||
}
|
||
|
||
// isOnlineThisWeek 判断短剧是否为本周内上架,用于app端展示“上新”标识。
|
||
func isOnlineThisWeek(onlineAt time.Time) bool {
|
||
if onlineAt.IsZero() {
|
||
return false
|
||
}
|
||
now := time.Now()
|
||
weekday := int(now.Weekday())
|
||
if weekday == 0 {
|
||
weekday = 7
|
||
}
|
||
weekStart := time.Date(now.Year(), now.Month(), now.Day()-weekday+1, 0, 0, 0, 0, now.Location())
|
||
return !onlineAt.Before(weekStart) && !onlineAt.After(now)
|
||
}
|
||
|
||
// commonDataValues 将短剧指针列表转换为响应值列表
|
||
func commonDataValues(list []*proto.CommonData) []proto.CommonData {
|
||
result := make([]proto.CommonData, 0, len(list))
|
||
for _, item := range list {
|
||
if item == nil {
|
||
continue
|
||
}
|
||
result = append(result, *item)
|
||
}
|
||
return result
|
||
}
|
||
|
||
// playletRecommendListQuery 按短剧分类构建分页查询条件。
|
||
// 分类下没有已上架短剧时,回退为全部已上架短剧,并按上架时间倒序。
|
||
func playletRecommendListQuery(ctx context.Context, categoryId int64) (bson.M, bson.D, *srvdef.SrvErr) {
|
||
if categoryId <= 0 {
|
||
return nil, nil, srvdef.New(srvdef.ErrParamError, "分类ID无效", "分类ID无效")
|
||
}
|
||
filter := homeCategoryPlayletFilter(categoryId)
|
||
count, err := playletmod.CountPlaylet(ctx, filter)
|
||
if err != nil {
|
||
return nil, nil, err
|
||
}
|
||
if count == 0 {
|
||
return homeUpPlayletFilter(nil), playletTodayUpdateSort(), nil
|
||
}
|
||
return filter, playletTodayRecommendSort(), nil
|
||
}
|
||
|
||
// findEnabledPlayletCategory 查询启用的短剧分类。
|
||
func findEnabledPlayletCategory(ctx context.Context, categoryId int64) (*playletcategorymod.PlayletCategory, *srvdef.SrvErr) {
|
||
if categoryId <= 0 {
|
||
return nil, srvdef.New(srvdef.ErrParamError, "分类ID无效", "分类ID无效")
|
||
}
|
||
return playletcategorymod.FindOnePlayletCategory(ctx, bson.M{
|
||
playletcategorymod.FN.PlayletCategory.Id: categoryId,
|
||
playletcategorymod.FN.PlayletCategory.Status: true,
|
||
}, options.FindOne().SetProjection(bson.D{
|
||
{Key: playletcategorymod.FN.PlayletCategory.Id, Value: 1},
|
||
{Key: playletcategorymod.FN.PlayletCategory.Name, Value: 1},
|
||
{Key: playletcategorymod.FN.PlayletCategory.Slug, Value: 1},
|
||
{Key: playletcategorymod.FN.PlayletCategory.Desc, Value: 1},
|
||
}))
|
||
}
|
||
|
||
// playletSearchQuery 构建短剧ES搜索查询条件
|
||
func playletSearchQuery(keyword string, from int64, size int64) esmanager.M {
|
||
return esmanager.M{
|
||
"from": from,
|
||
"size": size,
|
||
"query": esmanager.M{
|
||
"bool": esmanager.M{
|
||
"must": esmanager.A{
|
||
{
|
||
"bool": esmanager.M{
|
||
"should": esmanager.A{
|
||
{
|
||
"multi_match": esmanager.M{
|
||
"query": keyword,
|
||
"fields": playletSearchFields(),
|
||
"type": "phrase",
|
||
"boost": 3,
|
||
},
|
||
},
|
||
{
|
||
"multi_match": esmanager.M{
|
||
"query": keyword,
|
||
"fields": playletSearchFields(),
|
||
"type": "best_fields",
|
||
"operator": "and",
|
||
},
|
||
},
|
||
},
|
||
"minimum_should_match": 1,
|
||
},
|
||
},
|
||
},
|
||
"filter": esmanager.A{
|
||
{
|
||
"term": esmanager.M{
|
||
esmod.FN.ESPlaylet.Status: int32(playletmod.StatusUp),
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
"sort": esmanager.A{
|
||
{
|
||
"_score": esmanager.M{"order": "desc"},
|
||
},
|
||
{
|
||
esmod.FN.ESPlaylet.OnlineAt: esmanager.M{"order": "desc"},
|
||
},
|
||
{
|
||
esmod.FN.ESPlaylet.Id: esmanager.M{"order": "desc"},
|
||
},
|
||
},
|
||
}
|
||
}
|
||
|
||
// playletSearchFields 返回短剧ES搜索字段和权重
|
||
func playletSearchFields() []string {
|
||
// 剧名权重最高,题材名和标签名次之,简介兜底命中。
|
||
return []string{
|
||
esmod.FN.ESPlaylet.Title + "^3",
|
||
esmod.FN.ESPlaylet.PlayName + "^3",
|
||
esmod.FN.ESPlaylet.ThemeNames + "^2",
|
||
esmod.FN.ESPlaylet.TagNames + "^2",
|
||
esmod.FN.ESPlaylet.Desc,
|
||
}
|
||
}
|
||
|
||
// esPlayletToCommonData 转换ES短剧文档为app通用短剧响应
|
||
func esPlayletToCommonData(ctx context.Context, item *esmod.ESPlaylet) proto.CommonData {
|
||
return proto.CommonData{
|
||
Id: item.Id,
|
||
Status: int32(item.Status),
|
||
IsFeatured: item.IsFeatured,
|
||
IsRecommend: item.IsRecommend,
|
||
OperateType: int32(playletmod.NormalizeOperateType(item.OperateType)),
|
||
ShowOnBanner: item.ShowOnBanner,
|
||
Banner: withImageDomain(ctx, item.Banner),
|
||
OnlineAt: item.OnlineAt,
|
||
ThemeIds: item.ThemeIds,
|
||
ThemeNames: item.ThemeNames,
|
||
ThemeSlugs: slugsByIds(item.ThemeIds, playletcache.ThemeSlugMap(item.ThemeIds)),
|
||
Title: item.Title,
|
||
PlayName: item.PlayName,
|
||
Description: item.Description,
|
||
Slug: item.Slug,
|
||
Desc: item.Desc,
|
||
Anchor: item.Anchor,
|
||
Cover: withImageDomain(ctx, item.Cover),
|
||
TagIds: item.TagIds,
|
||
TagNames: item.TagNames,
|
||
TagSlugs: slugsByIds(item.TagIds, playletcache.TagSlugMap(item.TagIds)),
|
||
TotalEpisode: item.TotalEpisode,
|
||
UpdateStatus: item.UpdateStatus,
|
||
CountBrowse: item.CountBrowse,
|
||
HeatValue: item.HeatValue,
|
||
CountCollect: item.CountCollect,
|
||
CountLike: item.CountLike,
|
||
CreatedAt: item.CreatedAt,
|
||
UpdatedAt: item.UpdatedAt,
|
||
SeoInfo: item.SeoInfo,
|
||
}
|
||
}
|
||
|
||
// playletListSort 构建app短剧列表排序
|
||
func playletListSort(sortOrder int64) bson.D {
|
||
switch sortOrder {
|
||
case sortOrderHot:
|
||
return bson.D{
|
||
{Key: playletmod.FN.Playlet.CountCollect, Value: -1},
|
||
{Key: playletmod.FN.Playlet.Id, Value: -1},
|
||
}
|
||
case sortOrderUpdated:
|
||
return bson.D{
|
||
{Key: playletmod.FN.Playlet.OnlineAt, Value: -1},
|
||
{Key: playletmod.FN.Playlet.Id, Value: -1},
|
||
}
|
||
default:
|
||
return bson.D{
|
||
{Key: playletmod.FN.Playlet.OnlineAt, Value: -1},
|
||
{Key: playletmod.FN.Playlet.Id, Value: -1},
|
||
}
|
||
}
|
||
}
|
||
|
||
// playletRankSort 构建app短剧排行榜排序
|
||
func playletRankSort(sortOrder int64) bson.D {
|
||
switch sortOrder {
|
||
case rankHot:
|
||
return bson.D{
|
||
{Key: playletmod.FN.Playlet.HeatValue, Value: -1},
|
||
{Key: playletmod.FN.Playlet.Id, Value: -1},
|
||
}
|
||
case rankNew:
|
||
return bson.D{
|
||
{Key: playletmod.FN.Playlet.OnlineAt, Value: -1},
|
||
{Key: playletmod.FN.Playlet.Id, Value: -1},
|
||
}
|
||
case rankCollect:
|
||
return bson.D{
|
||
{Key: playletmod.FN.Playlet.CountCollect, Value: -1},
|
||
{Key: playletmod.FN.Playlet.Id, Value: -1},
|
||
}
|
||
default:
|
||
return bson.D{
|
||
{Key: playletmod.FN.Playlet.HeatValue, Value: -1},
|
||
{Key: playletmod.FN.Playlet.Id, Value: -1},
|
||
}
|
||
}
|
||
}
|
||
|
||
// playletTodayRecommendSort 构建今日推荐短剧排序
|
||
func playletTodayRecommendSort() bson.D {
|
||
return bson.D{
|
||
{Key: playletmod.FN.Playlet.RecommendRank, Value: -1},
|
||
{Key: playletmod.FN.Playlet.OnlineAt, Value: -1},
|
||
{Key: playletmod.FN.Playlet.Id, Value: -1},
|
||
}
|
||
}
|
||
|
||
// playletHitSeriesSort 构建热门好剧短剧排序
|
||
func playletHitSeriesSort() bson.D {
|
||
return bson.D{
|
||
{Key: playletmod.FN.Playlet.CountBrowse, Value: -1},
|
||
{Key: playletmod.FN.Playlet.Id, Value: -1},
|
||
}
|
||
}
|
||
|
||
// playletTodayUpdateSort 构建今日更新短剧排序
|
||
func playletTodayUpdateSort() bson.D {
|
||
return bson.D{
|
||
{Key: playletmod.FN.Playlet.OnlineAt, Value: -1},
|
||
{Key: playletmod.FN.Playlet.Id, Value: -1},
|
||
}
|
||
}
|
||
|
||
// operateTypeFilter 构建运营类型过滤条件;历史未写入字段按短剧处理。
|
||
func operateTypeFilter(operateType playletmod.OperateType) bson.M {
|
||
if operateType == playletmod.OperateTypeComic {
|
||
return bson.M{playletmod.FN.Playlet.OperateType: playletmod.OperateTypeComic}
|
||
}
|
||
return bson.M{playletmod.FN.Playlet.OperateType: bson.M{"$ne": playletmod.OperateTypeComic}}
|
||
}
|
||
|
||
// playletOperateListSort 构建运营类型列表排序
|
||
func playletOperateListSort(filterType int64) bson.D {
|
||
switch filterType {
|
||
case operateFilterHot:
|
||
return bson.D{
|
||
{Key: playletmod.FN.Playlet.CountCollect, Value: -1},
|
||
{Key: playletmod.FN.Playlet.Id, Value: -1},
|
||
}
|
||
case operateFilterOriginal:
|
||
return bson.D{
|
||
{Key: playletmod.FN.Playlet.RecommendRank, Value: -1},
|
||
{Key: playletmod.FN.Playlet.Id, Value: -1},
|
||
}
|
||
default:
|
||
return bson.D{
|
||
{Key: playletmod.FN.Playlet.OnlineAt, Value: -1},
|
||
{Key: playletmod.FN.Playlet.Id, Value: -1},
|
||
}
|
||
}
|
||
}
|
||
|
||
// normalizePlayletPage 规范化短剧分页参数
|
||
func normalizePlayletPage(pageNum, pageSize int64) (int64, int64) {
|
||
if pageNum <= 0 {
|
||
pageNum = 1
|
||
}
|
||
if pageSize <= 0 {
|
||
pageSize = defaultPageSize
|
||
}
|
||
if pageSize > maxPageSize {
|
||
pageSize = maxPageSize
|
||
}
|
||
return pageNum, pageSize
|
||
}
|
||
|
||
// normalizePlayletRecommendPage 规范化分类短剧分页参数
|
||
func normalizePlayletRecommendPage(pageNum, pageSize int64) (int64, int64) {
|
||
if pageNum <= 0 {
|
||
pageNum = 1
|
||
}
|
||
if pageSize <= 0 {
|
||
pageSize = defaultPageSize
|
||
}
|
||
if pageSize > maxPageSize {
|
||
pageSize = maxPageSize
|
||
}
|
||
return pageNum, pageSize
|
||
}
|
||
|
||
// normalizePlayletHistoryPage 规范化观看历史分页参数
|
||
func normalizePlayletHistoryPage(pageNum, pageSize int64) (int64, int64) {
|
||
if pageNum <= 0 {
|
||
pageNum = 1
|
||
}
|
||
if pageSize <= 0 {
|
||
pageSize = recommendPageSize
|
||
}
|
||
if pageSize > maxPageSize {
|
||
pageSize = maxPageSize
|
||
}
|
||
return pageNum, pageSize
|
||
}
|
||
|
||
// withImageDomain 给图片相对路径拼接图片域名,海外 IP 使用 imgAppUsCdn。
|
||
func withImageDomain(ctx context.Context, path string) string {
|
||
path = strings.TrimSpace(path)
|
||
if path == "" {
|
||
return ""
|
||
}
|
||
if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") {
|
||
return path
|
||
}
|
||
imageDomain := configser.GetAppImageCdn(configser.ClientIPFromCtx(ctx))
|
||
if imageDomain == "" {
|
||
return path
|
||
}
|
||
return netutil.MergeURL(imageDomain, path)
|
||
}
|