- 移除 买点 (user acquisition tracking) 子系统:dataReport SDK、eventmod 事件模型、reportdata 定时上报 job、reportser/srv_report 包装层,以及业务 侧的 ~20 处 Report* 调用点;去掉 proto.ConfigInfo 的 ReportSDKConfig 字段、DataReport yaml 配置块和 pubdef.TrackerAppId。 - 广告获取改走 versionUrl /api/stat/advitise/v2/get(appId=12),移除 adcenter(Jt) 调用;新增 POST /api/app/ads/click 异步上报; proto.AdvitiseData 别名 proxy.AdvitiseData,AList/BList 改名 AppInfoAList/AppInfoBList。 - web 后台登录取消谷歌验证码(srv_mwacc.AccLogin 去掉 code 参数),密码 正确即发放 token。 - app 端取消注册/登录/找回密码/游客登录接口及密码、token 相关代码;鉴权 中间件按 X-User-Agent 中的 devID 识别用户,首次访问自动建号,Redis 缓存 10 分钟;修改资料不再支持改密码。 - app m3u8 ticket 校验失败不再降级到 fallback 路径,仍按原 m3u8Name 返回。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
4618 lines
163 KiB
Go
4618 lines
163 KiB
Go
package playletser
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"strconv"
|
||
"strings"
|
||
"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/homeconfigmod"
|
||
"videoweb/pub/models/hotsearchmod"
|
||
"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/pub/seo/h5path"
|
||
"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 = 5 // banner图限制
|
||
homeConfigPlayletLimit int64 = 12 // 旧首页配置短剧默认数量
|
||
homeConfigThemeLimit int64 = 8 // 首页配置题材默认数量
|
||
defaultPageSize int64 = 10 // 默认分页条数
|
||
guessLikePageSize int64 = 6 // 猜你喜欢默认分页条数
|
||
hotListPageSize int64 = 6 // 热门短剧默认分页条数
|
||
hotRankPageSize int64 = 20 // 热榜默认分页条数
|
||
recommendPageSize int64 = 20 // 今日推荐/热门好剧默认分页条数
|
||
maxPageSize int64 = 100 // 最大分页条数
|
||
allListSortLatest int64 = 1 // 最新
|
||
allListSortHot int64 = 2 // 最热
|
||
allListSortCollect int64 = 3 // 收藏最多
|
||
topicListSortHot int64 = 2 // 最热
|
||
topicListPlayletLimit int64 = 3 // 专题卡片短剧数量限制
|
||
topicDetailSortEdit int64 = 1 // 编辑排序
|
||
topicDetailSortLatest int64 = 2 // 最新
|
||
topicDetailSortHot int64 = 3 // 最热
|
||
topicDetailPageSize int64 = 12 // 专题详情默认分页条数
|
||
topicDetailCoverLimit int64 = 3 // 专题详情顶部封面数量限制
|
||
topicDetailMoreLimit int64 = 3 // 更多专题数量限制
|
||
operateListLatest int64 = 1 // 最新更新
|
||
operateListHot int64 = 2 // 当前热播
|
||
operateListOriginal int64 = 3 // 独家原创
|
||
operateListRandom int64 = 4 // 随机推荐
|
||
rankHot int64 = 1 // 热播榜
|
||
rankNew int64 = 2 // 新剧榜
|
||
rankCollect int64 = 3 // 追剧榜
|
||
operateStatusYes int64 = 1 // 已操作
|
||
operateStatusNo int64 = 2 // 未操作
|
||
playletHeatClickInc int64 = 100 // 每次查看详情增加的热度值
|
||
playletHeatLikeInc int64 = 1 // 点赞增加的热度值
|
||
playletHeatCollectInc int64 = 2 // 收藏增加的热度值
|
||
playletHeatBuyInc int64 = 10 // 购买剧集增加的短剧热度值
|
||
playletHeatForwardInc int64 = 5 // 转发增加的热度值
|
||
homePlayletCacheSchema = "playlet-banner-home-config-modules-v13"
|
||
// playletH5DetailPath 短剧Web/H5播放页路径,供SEO canonical使用。
|
||
playletH5DetailPath = "/play"
|
||
// 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
|
||
}
|
||
|
||
type homeConfigThemePlaylets struct {
|
||
Theme *playletcache.ThemeItem
|
||
Playlets playletmod.PlayletList
|
||
TotalCount int64
|
||
}
|
||
|
||
type playletBoughtAggRow struct {
|
||
PlayletId int64 `bson:"_id"`
|
||
BoughtChapterCount int64 `bson:"boughtChapterCount"`
|
||
CostCoins int64 `bson:"costCoins"`
|
||
BoughtAt time.Time `bson:"boughtAt"`
|
||
LatestUnlockId int64 `bson:"latestUnlockId"`
|
||
}
|
||
|
||
type playletBoughtCountRow struct {
|
||
Count int64 `bson:"count"`
|
||
}
|
||
|
||
// playletHomeProjection 查询指定字段
|
||
var playletHomeProjection = bson.D{
|
||
{Key: playletmod.FN.Playlet.Id, Value: 1},
|
||
{Key: playletmod.FN.Playlet.MediaCenterId, 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.CategoryIds, 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.HorizontalCover, Value: 1},
|
||
{Key: playletmod.FN.Playlet.CompressedCover, Value: 1},
|
||
{Key: playletmod.FN.Playlet.CompressedHorizontalCover, Value: 1},
|
||
{Key: playletmod.FN.Playlet.Preview, 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.ShowOriginal, 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},
|
||
}
|
||
|
||
// homeConfigAppProjection 查询首页配置指定字段
|
||
var homeConfigAppProjection = bson.D{
|
||
{Key: homeconfigmod.FN.HomeConfig.Id, Value: 1},
|
||
{Key: homeconfigmod.FN.HomeConfig.Name, Value: 1},
|
||
{Key: homeconfigmod.FN.HomeConfig.Slug, Value: 1},
|
||
{Key: homeconfigmod.FN.HomeConfig.Desc, Value: 1},
|
||
{Key: homeconfigmod.FN.HomeConfig.Label, Value: 1},
|
||
{Key: homeconfigmod.FN.HomeConfig.Style, Value: 1},
|
||
{Key: homeconfigmod.FN.HomeConfig.CardBackgroundImage, Value: 1},
|
||
{Key: homeconfigmod.FN.HomeConfig.TopicIds, Value: 1},
|
||
{Key: homeconfigmod.FN.HomeConfig.CategoryId, Value: 1},
|
||
{Key: homeconfigmod.FN.HomeConfig.RecommendType, Value: 1},
|
||
{Key: homeconfigmod.FN.HomeConfig.Type, Value: 1},
|
||
{Key: homeconfigmod.FN.HomeConfig.Rank, Value: 1},
|
||
{Key: homeconfigmod.FN.HomeConfig.Limit, Value: 1},
|
||
{Key: homeconfigmod.FN.HomeConfig.SortType, Value: 1},
|
||
{Key: homeconfigmod.FN.HomeConfig.Status, Value: 1},
|
||
{Key: homeconfigmod.FN.HomeConfig.SeoInfo.Title, Value: 1},
|
||
{Key: homeconfigmod.FN.HomeConfig.SeoInfo.Description, Value: 1},
|
||
{Key: homeconfigmod.FN.HomeConfig.SeoInfo.Keyworld, Value: 1},
|
||
{Key: homeconfigmod.FN.HomeConfig.CreatedAt, Value: 1},
|
||
{Key: homeconfigmod.FN.HomeConfig.UpdatedAt, Value: 1},
|
||
}
|
||
|
||
// playletCollectListProjection 我的收藏列表只读取卡片展示和未看集数计算需要的字段。
|
||
var playletCollectListProjection = bson.D{
|
||
{Key: playletmod.FN.Playlet.Id, Value: 1},
|
||
{Key: playletmod.FN.Playlet.MediaCenterId, Value: 1},
|
||
{Key: playletmod.FN.Playlet.IsOriginal, Value: 1},
|
||
{Key: playletmod.FN.Playlet.ShowOriginal, 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)
|
||
attachHomeAdvitise(resp)
|
||
return resp, nil
|
||
}
|
||
resp, err := buildHomePlayletResponse(ctx)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
setHomePlayletCache(cacheKey, resp)
|
||
attachHomeAnnouncements(resp)
|
||
attachHomeAdvitise(resp)
|
||
return resp, nil
|
||
}
|
||
|
||
// AllList 分页查询app端全部短剧列表
|
||
func AllList(ctx context.Context, req *proto.PlayletAllListReq) (*proto.PlayletAllListResp, *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
|
||
filter, err := playletAllListFilter(req)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
pageNum, pageSize := normalizePlayletPage(req.PageNum, req.PageSize)
|
||
totalCount, err := playletmod.CountPlaylet(ctx, filter)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
resp := &proto.PlayletAllListResp{
|
||
TotalCount: totalCount,
|
||
PageSize: int(pageSize),
|
||
PageNum: int(pageNum),
|
||
List: make([]proto.CommonData, 0),
|
||
Advitise: configser.GetAdvitise(),
|
||
}
|
||
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, playletAllListSort(req.SortType), 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
|
||
}
|
||
|
||
// HotList 分页查询热门短剧
|
||
func HotList(ctx context.Context, req *proto.PlayletHotListReq) (*proto.PlayletHotListResp, *srvdef.SrvErr) {
|
||
if req == nil || req.ExcludePlayletId < 0 {
|
||
return nil, srvdef.New(srvdef.ErrParamError, "排除的短剧ID无效", "")
|
||
}
|
||
pageSize := req.PageSize
|
||
if pageSize <= 0 {
|
||
pageSize = hotListPageSize
|
||
}
|
||
pageNum, pageSize := normalizePlayletPage(req.PageNum, pageSize)
|
||
filter := homeUpPlayletFilter(bson.M{
|
||
playletmod.FN.Playlet.OperateType: bson.M{"$ne": playletmod.OperateTypeComic},
|
||
})
|
||
if req.ExcludePlayletId > 0 {
|
||
filter[playletmod.FN.Playlet.Id] = bson.M{"$ne": req.ExcludePlayletId}
|
||
}
|
||
totalCount, err := playletmod.CountPlaylet(ctx, filter)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
resp := &proto.PlayletHotListResp{
|
||
TotalCount: totalCount,
|
||
PageSize: int(pageSize),
|
||
PageNum: int(pageNum),
|
||
List: make([]proto.CommonData, 0),
|
||
}
|
||
skip := (pageNum - 1) * pageSize
|
||
if skip >= totalCount {
|
||
return resp, nil
|
||
}
|
||
list, err := findHomePlaylets(ctx, filter, playletAllListSort(allListSortHot), 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
|
||
}
|
||
|
||
// TopicList 分页查询app端专题剧单
|
||
func TopicList(ctx context.Context, req *proto.PlayletTopicPageListReq) (*proto.PlayletTopicPageListResp, *srvdef.SrvErr) {
|
||
if req == nil {
|
||
return nil, srvdef.New(srvdef.ErrParamError, "", "")
|
||
}
|
||
pageNum, pageSize := normalizePlayletPage(req.PageNum, req.PageSize)
|
||
skip := (pageNum - 1) * pageSize
|
||
groups, err := enabledTopicGroups(ctx)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
resp := &proto.PlayletTopicPageListResp{
|
||
PageSize: int(pageSize),
|
||
PageNum: int(pageNum),
|
||
List: make([]*proto.PlayletTopicGroupPageItem, 0, len(groups)),
|
||
SeoInfo: buildPlayletListPageSeoInfo(),
|
||
}
|
||
for _, group := range groups {
|
||
if group == nil {
|
||
continue
|
||
}
|
||
page, err := topicGroupPageData(ctx, group, skip, pageSize)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
resp.List = append(resp.List, page)
|
||
}
|
||
return resp, nil
|
||
}
|
||
|
||
// TopicDetail 查询app端专题详情
|
||
func TopicDetail(ctx context.Context, req *proto.PlayletTopicDetailReq) (*proto.PlayletTopicDetailResp, *srvdef.SrvErr) {
|
||
if req == nil || (req.Id <= 0 && strings.TrimSpace(req.TopicSlug) == "") || (req.DataType != 0 && req.DataType != 1) {
|
||
return nil, srvdef.New(srvdef.ErrParamError, "专题ID无效", "")
|
||
}
|
||
id, serr := resolveThemeID(ctx, req.Id, req.TopicSlug)
|
||
if serr != nil {
|
||
return nil, serr
|
||
}
|
||
req.Id = id
|
||
theme := playletcache.GetEnabledTheme(req.Id)
|
||
if theme == nil {
|
||
return nil, srvdef.New(srvdef.ErrNoData, "专题不存在或已下架", "专题不存在或已下架")
|
||
}
|
||
pageNum, pageSize := normalizePlayletTopicDetailPage(req.PageNum, req.PageSize)
|
||
filter := topicDetailPlayletFilter(req.Id)
|
||
totalCount, err := playletmod.CountPlaylet(ctx, filter)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
resp := &proto.PlayletTopicDetailResp{
|
||
Id: theme.Id,
|
||
DataType: 1,
|
||
Advitise: configser.GetAdvitise(),
|
||
Name: theme.Name,
|
||
Slug: theme.Slug,
|
||
Desc: strings.TrimSpace(theme.SeoInfo.Description),
|
||
Icon: withImageDomain(ctx, theme.Icon),
|
||
Rank: theme.Rank,
|
||
PlayletCount: 0,
|
||
Covers: make([]string, 0),
|
||
TotalCount: totalCount,
|
||
PageSize: int(pageSize),
|
||
PageNum: int(pageNum),
|
||
List: make([]proto.CommonData, 0),
|
||
MoreTopics: make([]*proto.PlayletTopicPageItem, 0),
|
||
SeoInfo: buildPageSeoInfoFromValue(theme.SeoInfo, theme.Name, "", buildPlayletThemeListCanonicalURL(theme.Slug, theme.Id)),
|
||
}
|
||
if totalCount == 0 {
|
||
return fillPlayletTopicDetailMore(ctx, resp, theme.Id, nil)
|
||
}
|
||
coverList, err := findHomePlaylets(ctx, filter, playletTopicDetailSort(topicDetailSortEdit), topicDetailCoverLimit)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
skip := (pageNum - 1) * pageSize
|
||
pageList := make(playletmod.PlayletList, 0)
|
||
if skip < totalCount {
|
||
pageList, err = findHomePlaylets(ctx, filter, playletTopicDetailSort(req.SortType), pageSize, options.Find().SetSkip(skip))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
allPlaylets := mergeHomePlayletLists(coverList, pageList)
|
||
resp, err = fillPlayletTopicDetailMore(ctx, resp, theme.Id, allPlaylets)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
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.Covers = playletTopicCovers(playletsToHomeCommonData(ctx, coverList, themeNameMap, tagNameMap))
|
||
resp.List = commonDataValues(playletsToHomeCommonData(ctx, pageList, themeNameMap, tagNameMap))
|
||
resp.PlayletCount = int64(len(resp.List))
|
||
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
|
||
}
|
||
|
||
// HotRankList 分页查询app端热榜短剧列表
|
||
func HotRankList(ctx context.Context, req *proto.PlayletHotRankListReq) (*proto.PlayletHotRankListResp, *srvdef.SrvErr) {
|
||
if req == nil {
|
||
return nil, srvdef.New(srvdef.ErrParamError, "", "")
|
||
}
|
||
rankType := normalizePlayletHotRankType(req.RankType)
|
||
if rankType == 0 {
|
||
return nil, srvdef.New(srvdef.ErrParamError, "榜单类型无效", "")
|
||
}
|
||
pageNum, pageSize := normalizePlayletHotRankPage(req.PageNum, req.PageSize)
|
||
filter := homeUpPlayletFilter(nil)
|
||
totalCount, err := playletmod.CountPlaylet(ctx, filter)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
resp := &proto.PlayletHotRankListResp{
|
||
TotalCount: totalCount,
|
||
PageSize: int(pageSize),
|
||
PageNum: int(pageNum),
|
||
RankType: rankType,
|
||
List: make([]proto.CommonData, 0),
|
||
SeoInfo: buildPlayletListPageSeoInfo(),
|
||
}
|
||
if totalCount == 0 {
|
||
return resp, nil
|
||
}
|
||
skip := (pageNum - 1) * pageSize
|
||
if skip >= totalCount {
|
||
return resp, nil
|
||
}
|
||
list, err := findHomePlaylets(ctx, filter, playletHotRankSort(rankType), 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, "", "")
|
||
}
|
||
typ := req.Type
|
||
if typ == 0 {
|
||
typ = operateListLatest
|
||
}
|
||
if typ < operateListLatest || typ > operateListRandom {
|
||
return nil, srvdef.New(srvdef.ErrParamError, "类型无效", "")
|
||
}
|
||
pageNum, pageSize := normalizePlayletPage(req.PageNum, req.PageSize)
|
||
filter := operateListFilter(typ)
|
||
totalCount, err := playletmod.CountPlaylet(ctx, filter)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
resp := &proto.PlayletOperateListResp{
|
||
TotalCount: totalCount, PageNum: int(pageNum), PageSize: int(pageSize), Type: typ,
|
||
List: make([]proto.CommonData, 0),
|
||
}
|
||
skip := (pageNum - 1) * pageSize
|
||
if totalCount == 0 || skip >= totalCount {
|
||
return resp, nil
|
||
}
|
||
var list playletmod.PlayletList
|
||
if typ == operateListRandom {
|
||
list, err = findRandomOperatePlaylets(ctx, filter, skip, pageSize, time.Now())
|
||
} else {
|
||
list, err = findHomePlaylets(ctx, filter, operateListSort(typ), 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, playletAllListSort(req.SortType), 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
|
||
}
|
||
|
||
// GuessLikeList 分页查询同题材猜你喜欢短剧
|
||
func GuessLikeList(ctx context.Context, req *proto.PlayletGuessLikeListReq) (*proto.PlayletGuessLikeListResp, *srvdef.SrvErr) {
|
||
if req == nil || req.PlayletId <= 0 {
|
||
return nil, srvdef.New(srvdef.ErrParamError, "短剧ID不能为空", "短剧ID不能为空")
|
||
}
|
||
current, err := findGuessLikeCurrentPlaylet(ctx, req.PlayletId)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
pageNum, pageSize := normalizePlayletGuessLikePage(req.PageNum, req.PageSize)
|
||
resp := &proto.PlayletGuessLikeListResp{
|
||
PageSize: int(pageSize),
|
||
PageNum: int(pageNum),
|
||
List: make([]proto.CommonData, 0),
|
||
}
|
||
if current == nil || len(current.ThemeIds) == 0 {
|
||
return resp, nil
|
||
}
|
||
filter := playletGuessLikeFilter(current)
|
||
totalCount, err := playletmod.CountPlaylet(ctx, filter)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
resp.TotalCount = totalCount
|
||
if totalCount == 0 {
|
||
return resp, nil
|
||
}
|
||
skip := (pageNum - 1) * pageSize
|
||
if skip >= totalCount {
|
||
return resp, nil
|
||
}
|
||
list, err := findHomePlaylets(ctx, filter, playletGuessLikeSort(), 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(ctx, keyword, 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),
|
||
Keyword: keyword,
|
||
List: make([]proto.CommonData, 0, len(hits.Hits)),
|
||
EveryoneWatching: make([]*proto.CommonData, 0),
|
||
}
|
||
if hits.Total.Value == 0 {
|
||
resp.EveryoneWatching = guessLikeCommonData(ctx)
|
||
return resp, nil
|
||
}
|
||
labels, err := loadESPlayletLabels(ctx, hits.Hits)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
for _, hit := range hits.Hits {
|
||
if hit == nil || hit.Source == nil {
|
||
continue
|
||
}
|
||
resp.List = append(resp.List, esPlayletToCommonData(ctx, hit.Source, labels[hit.Source.Id]))
|
||
}
|
||
return resp, nil
|
||
}
|
||
|
||
// SearchSuggest 查询短剧搜索框联想数据
|
||
func SearchSuggest(ctx context.Context, req *proto.PlayletSearchSuggestReq) (*proto.PlayletSearchSuggestResp, *srvdef.SrvErr) {
|
||
if req == nil || strings.TrimSpace(req.Keyword) == "" {
|
||
return nil, srvdef.New(srvdef.ErrParamError, "搜索关键词不能为空", "搜索关键词不能为空")
|
||
}
|
||
keyword := strings.TrimSpace(req.Keyword)
|
||
resp := &proto.PlayletSearchSuggestResp{
|
||
Keyword: keyword,
|
||
List: make([]proto.CommonData, 0),
|
||
}
|
||
if esmanager.Instance == nil {
|
||
return resp, nil
|
||
}
|
||
query := playletSearchQuery(keyword, 0, maxPageSize)
|
||
var hits esmod.ESPlayletHits
|
||
if err := esmanager.Instance.Search(esmod.GetPlayletIndex(), &hits, query); err != nil {
|
||
log.Warn("短剧搜索联想ES查询失败", log.Any("关键词", keyword), log.E(err))
|
||
return resp, nil
|
||
}
|
||
labels, err := loadESPlayletLabels(ctx, hits.Hits)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
for _, hit := range hits.Hits {
|
||
if hit == nil || hit.Source == nil {
|
||
continue
|
||
}
|
||
resp.List = append(resp.List, esPlayletToCommonData(ctx, hit.Source, labels[hit.Source.Id]))
|
||
}
|
||
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,
|
||
playletchaptermod.FN.PlayletChapter.Status: playletchaptermod.StatusUp,
|
||
}
|
||
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, playlet.TotalEpisode, 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,
|
||
playletchaptermod.FN.PlayletChapter.Status: playletchaptermod.StatusUp,
|
||
}
|
||
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,
|
||
playletchaptermod.FN.PlayletChapter.Status: playletchaptermod.StatusUp,
|
||
}, 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,
|
||
playletchaptermod.FN.PlayletChapter.Status: playletchaptermod.StatusUp,
|
||
}, 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
|
||
themeNameMap, err := buildHomeThemeNameMap(ctx, playletmod.PlayletList{item})
|
||
if err != nil {
|
||
log.Error("查询短剧详情题材名称失败", log.Any("短剧ID", item.Id), log.E(err))
|
||
return nil, err
|
||
}
|
||
tagNameMap, err := buildHomeTagNameMap(ctx, playletmod.PlayletList{item})
|
||
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,
|
||
PlayletChapterInfo: chapterInfo,
|
||
SeoInfo: buildPlayletDetailSeoInfo(item, themeNameMap, tagNameMap),
|
||
Advitise: configser.GetAdvitise(),
|
||
}
|
||
if resp.Preview != "" {
|
||
resp.Preview = videoser.GetM3U8AuthParam(
|
||
item.Id,
|
||
resp.Preview,
|
||
videoser.H5M3U8RequestIP(ctx),
|
||
"",
|
||
userId,
|
||
)
|
||
}
|
||
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,
|
||
}
|
||
if err := applyCollectWatchStatusFilter(ctx, userId, req.WatchStatus, filter); err != nil {
|
||
return nil, err
|
||
}
|
||
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
|
||
}
|
||
|
||
func applyCollectWatchStatusFilter(ctx context.Context, userId int64, watchStatus int64, filter bson.M) *srvdef.SrvErr {
|
||
if watchStatus == 0 {
|
||
return nil
|
||
}
|
||
if watchStatus != 1 && watchStatus != 2 {
|
||
return srvdef.New(srvdef.ErrParamError, "观看状态错误", "观看状态错误")
|
||
}
|
||
playletIds, err := findCollectWatchStatusPlayletIds(ctx, userId, watchStatus == 1)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
filter[playletcollectmod.FN.PlayletCollect.PlayletId] = bson.M{"$in": playletIds}
|
||
return nil
|
||
}
|
||
|
||
func findCollectWatchStatusPlayletIds(ctx context.Context, userId int64, isChasing bool) ([]int64, *srvdef.SrvErr) {
|
||
list, err := playletwatchprogressmod.FindManyPlayletWatchProgress(ctx, bson.M{
|
||
playletwatchprogressmod.FN.PlayletWatchProgress.UserId: userId,
|
||
playletwatchprogressmod.FN.PlayletWatchProgress.IsChasing: isChasing,
|
||
}, options.Find().SetProjection(bson.D{
|
||
{Key: playletwatchprogressmod.FN.PlayletWatchProgress.PlayletId, Value: 1},
|
||
}))
|
||
if err != nil {
|
||
log.Error("查询收藏列表观看状态短剧ID失败", log.Any("用户ID", userId), log.Any("是否追剧中", isChasing), log.E(err))
|
||
return nil, err
|
||
}
|
||
playletIds := 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{}{}
|
||
playletIds = append(playletIds, item.PlayletId)
|
||
}
|
||
return playletIds, nil
|
||
}
|
||
|
||
// BoughtList 分页查询当前用户已购短剧列表
|
||
func BoughtList(ctx context.Context, userId int64, req *proto.PlayletBoughtListReq) (*proto.PlayletBoughtListResp, *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)
|
||
totalCount, err := countBoughtPlaylets(ctx, userId)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
resp := &proto.PlayletBoughtListResp{
|
||
TotalCount: totalCount,
|
||
PageSize: int(pageSize),
|
||
PageNum: int(pageNum),
|
||
List: make([]proto.PlayletBoughtData, 0),
|
||
}
|
||
if totalCount == 0 {
|
||
return resp, nil
|
||
}
|
||
skip := (pageNum - 1) * pageSize
|
||
if skip >= totalCount {
|
||
return resp, nil
|
||
}
|
||
rows, err := findBoughtPlayletRows(ctx, userId, skip, pageSize)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if len(rows) == 0 {
|
||
return resp, nil
|
||
}
|
||
playletIds := collectPlayletIdsFromBoughtRows(rows)
|
||
playletMap, err := findCollectPlayletMap(ctx, playletIds)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
progressMap, err := findCollectWatchProgressMap(ctx, userId, playletIds)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
playletList := playletListFromBoughtRows(rows, 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 = buildPlayletBoughtData(ctx, rows, playletMap, progressMap, themeNameMap, tagNameMap)
|
||
return resp, nil
|
||
}
|
||
|
||
// countBoughtPlaylets 统计当前用户已购短剧数量
|
||
func countBoughtPlaylets(ctx context.Context, userId int64) (int64, *srvdef.SrvErr) {
|
||
rows := make([]playletBoughtCountRow, 0, 1)
|
||
pipeline := []bson.M{
|
||
{
|
||
"$match": bson.M{
|
||
playletchapterunlockmod.FN.PlayletChapterUnlock.UserId: userId,
|
||
playletchapterunlockmod.FN.PlayletChapterUnlock.PlayletId: bson.M{"$gt": 0},
|
||
},
|
||
},
|
||
{
|
||
"$group": bson.M{
|
||
"_id": "$" + playletchapterunlockmod.FN.PlayletChapterUnlock.PlayletId,
|
||
},
|
||
},
|
||
{
|
||
"$count": "count",
|
||
},
|
||
}
|
||
if err := playletchapterunlockmod.AggregatePlayletChapterUnlock(ctx, &rows, pipeline); err != nil {
|
||
return 0, err
|
||
}
|
||
if len(rows) == 0 {
|
||
return 0, nil
|
||
}
|
||
return rows[0].Count, nil
|
||
}
|
||
|
||
// findBoughtPlayletRows 聚合查询当前用户已购短剧分页数据
|
||
func findBoughtPlayletRows(ctx context.Context, userId int64, skip int64, limit int64) ([]playletBoughtAggRow, *srvdef.SrvErr) {
|
||
rows := make([]playletBoughtAggRow, 0)
|
||
pipeline := []bson.M{
|
||
{
|
||
"$match": bson.M{
|
||
playletchapterunlockmod.FN.PlayletChapterUnlock.UserId: userId,
|
||
playletchapterunlockmod.FN.PlayletChapterUnlock.PlayletId: bson.M{"$gt": 0},
|
||
},
|
||
},
|
||
{
|
||
"$group": bson.M{
|
||
"_id": "$" + playletchapterunlockmod.FN.PlayletChapterUnlock.PlayletId,
|
||
"boughtChapterCount": bson.M{"$sum": 1},
|
||
"costCoins": bson.M{"$sum": "$" + playletchapterunlockmod.FN.PlayletChapterUnlock.Coins},
|
||
"boughtAt": bson.M{"$max": "$" + playletchapterunlockmod.FN.PlayletChapterUnlock.CreatedAt},
|
||
"latestUnlockId": bson.M{"$max": "$" + playletchapterunlockmod.FN.PlayletChapterUnlock.Id},
|
||
},
|
||
},
|
||
{
|
||
"$sort": bson.D{
|
||
{Key: "boughtAt", Value: -1},
|
||
{Key: "latestUnlockId", Value: -1},
|
||
{Key: "_id", Value: -1},
|
||
},
|
||
},
|
||
{
|
||
"$skip": skip,
|
||
},
|
||
{
|
||
"$limit": limit,
|
||
},
|
||
}
|
||
if err := playletchapterunlockmod.AggregatePlayletChapterUnlock(ctx, &rows, pipeline); err != nil {
|
||
return nil, err
|
||
}
|
||
return rows, nil
|
||
}
|
||
|
||
// collectPlayletIdsFromBoughtRows 从已购短剧聚合结果中提取短剧ID集合
|
||
func collectPlayletIdsFromBoughtRows(rows []playletBoughtAggRow) []int64 {
|
||
result := make([]int64, 0, len(rows))
|
||
seen := make(map[int64]struct{}, len(rows))
|
||
for _, row := range rows {
|
||
if row.PlayletId <= 0 {
|
||
continue
|
||
}
|
||
if _, ok := seen[row.PlayletId]; ok {
|
||
continue
|
||
}
|
||
seen[row.PlayletId] = struct{}{}
|
||
result = append(result, row.PlayletId)
|
||
}
|
||
return result
|
||
}
|
||
|
||
// playletListFromBoughtRows 按已购短剧聚合顺序返回短剧集合
|
||
func playletListFromBoughtRows(rows []playletBoughtAggRow, playletMap map[int64]*playletmod.Playlet) playletmod.PlayletList {
|
||
result := make(playletmod.PlayletList, 0, len(rows))
|
||
for _, row := range rows {
|
||
item := playletMap[row.PlayletId]
|
||
if item == nil {
|
||
continue
|
||
}
|
||
result = append(result, item)
|
||
}
|
||
return result
|
||
}
|
||
|
||
// 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
|
||
}
|
||
|
||
// DeleteHistory 删除当前用户指定短剧观看历史,不影响观看进度和收藏列表
|
||
func DeleteHistory(ctx context.Context, userId int64, req *proto.PlayletHistoryDeleteReq) (*proto.PlayletHistoryDeleteResp, *srvdef.SrvErr) {
|
||
if userId <= 0 {
|
||
return nil, srvdef.New(srvdef.InvalidToken, "请先登录", "请先登录")
|
||
}
|
||
if req == nil || req.PlayletId <= 0 {
|
||
return nil, srvdef.New(srvdef.ErrParamError, "短剧ID不能为空", "短剧ID不能为空")
|
||
}
|
||
deleted, err := playletwatchhistorymod.DeleteManyPlayletWatchHistory(ctx, bson.M{
|
||
playletwatchhistorymod.FN.PlayletWatchHistory.UserId: userId,
|
||
playletwatchhistorymod.FN.PlayletWatchHistory.PlayletId: req.PlayletId,
|
||
})
|
||
if err != nil {
|
||
log.Error("删除单条短剧观看历史失败", log.Any("用户ID", userId), log.Any("短剧ID", req.PlayletId), log.E(err))
|
||
return nil, err
|
||
}
|
||
return &proto.PlayletHistoryDeleteResp{DeletedCount: deleted}, 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},
|
||
playletchaptermod.FN.PlayletChapter.Status: playletchaptermod.StatusUp,
|
||
}, 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
|
||
}
|
||
|
||
// buildPlayletBoughtData 组装我的已购短剧响应,保持最近购买时间倒序
|
||
func buildPlayletBoughtData(
|
||
ctx context.Context,
|
||
rows []playletBoughtAggRow,
|
||
playletMap map[int64]*playletmod.Playlet,
|
||
progressMap map[int64]*playletwatchprogressmod.PlayletWatchProgress,
|
||
themeNameMap map[int64]string,
|
||
tagNameMap map[int64]string,
|
||
) []proto.PlayletBoughtData {
|
||
result := make([]proto.PlayletBoughtData, 0, len(rows))
|
||
for _, row := range rows {
|
||
item := playletMap[row.PlayletId]
|
||
if item == nil {
|
||
continue
|
||
}
|
||
if len(item.Description) > 0 {
|
||
item.SeoInfo.Description = item.Description
|
||
}
|
||
progress := progressMap[item.Id]
|
||
watchedCount := collectWatchedCount(progress, item.TotalEpisode)
|
||
result = append(result, proto.PlayletBoughtData{
|
||
CommonData: proto.CommonData{
|
||
Id: item.Id,
|
||
Status: int32(item.Status),
|
||
IsFeatured: item.IsFeatured,
|
||
IsOriginal: item.IsOriginal || item.MediaCenterId == 0,
|
||
ShowOriginal: item.ShowOriginal,
|
||
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)),
|
||
CategoryIds: item.CategoryIds,
|
||
Title: item.Title,
|
||
PlayName: item.PlayName,
|
||
Description: item.Description,
|
||
Slug: item.Slug,
|
||
Desc: item.Desc,
|
||
Anchor: item.Anchor,
|
||
Cover: withImageDomain(ctx, item.DisplayCover()),
|
||
HorizontalCover: withImageDomain(ctx, item.DisplayHorizontalCover()),
|
||
Preview: item.Preview,
|
||
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,
|
||
},
|
||
BoughtChapterCount: row.BoughtChapterCount,
|
||
CostCoins: row.CostCoins,
|
||
BoughtAt: row.BoughtAt,
|
||
LastChapterId: collectLastChapterId(progress),
|
||
LastEpisode: collectLastEpisode(progress),
|
||
WatchedCount: watchedCount,
|
||
UnwatchedCount: collectUnwatchedCount(item.TotalEpisode, watchedCount),
|
||
IsFinished: isPlayletFinished(item.TotalEpisode, watchedCount),
|
||
})
|
||
}
|
||
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,
|
||
IsOriginal: item.IsOriginal || item.MediaCenterId == 0,
|
||
ShowOriginal: item.ShowOriginal,
|
||
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)),
|
||
CategoryIds: item.CategoryIds,
|
||
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: formatPlayletHistoryTime(history.UpdatedAt),
|
||
})
|
||
}
|
||
return result
|
||
}
|
||
|
||
func formatPlayletHistoryTime(t time.Time) string {
|
||
if t.IsZero() {
|
||
return ""
|
||
}
|
||
return t.In(utils.LocBJ).Format(pubdef.TimeISO)
|
||
}
|
||
|
||
// 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,
|
||
playletchaptermod.FN.PlayletChapter.Status: playletchaptermod.StatusUp,
|
||
}
|
||
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
|
||
}
|
||
|
||
// findGuessLikeCurrentPlaylet 查询猜你喜欢当前短剧
|
||
func findGuessLikeCurrentPlaylet(ctx context.Context, playletId int64) (*playletmod.Playlet, *srvdef.SrvErr) {
|
||
item, err := playletmod.FindOnePlaylet(ctx, homeUpPlayletFilter(bson.M{
|
||
playletmod.FN.Playlet.Id: playletId,
|
||
}), options.FindOne().SetProjection(bson.D{
|
||
{Key: playletmod.FN.Playlet.Id, Value: 1},
|
||
{Key: playletmod.FN.Playlet.ThemeIds, Value: 1},
|
||
}))
|
||
if err != nil {
|
||
log.Error("查询猜你喜欢当前短剧失败", log.Any("短剧ID", playletId), log.E(err))
|
||
return nil, err
|
||
}
|
||
if item == nil {
|
||
return nil, srvdef.New(srvdef.ErrNoData, "短剧不存在或已下架", "短剧不存在或已下架")
|
||
}
|
||
return item, nil
|
||
}
|
||
|
||
// playletGuessLikeFilter 构建猜你喜欢过滤条件
|
||
func playletGuessLikeFilter(item *playletmod.Playlet) bson.M {
|
||
filter := homeUpPlayletFilter(bson.M{
|
||
playletmod.FN.Playlet.Id: bson.M{"$ne": item.Id},
|
||
})
|
||
if item != nil && len(item.ThemeIds) > 0 {
|
||
filter[playletmod.FN.Playlet.ThemeIds] = bson.M{"$in": item.ThemeIds}
|
||
}
|
||
return filter
|
||
}
|
||
|
||
// playletGuessLikeSort 构建猜你喜欢排序
|
||
func playletGuessLikeSort() bson.D {
|
||
return bson.D{
|
||
{Key: playletmod.FN.Playlet.HeatValue, Value: -1},
|
||
{Key: playletmod.FN.Playlet.CountCollect, Value: -1},
|
||
{Key: playletmod.FN.Playlet.Id, Value: -1},
|
||
}
|
||
}
|
||
|
||
// 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))
|
||
}
|
||
|
||
// 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-1045-hgdjnet-web /topic-detail/:slug)
|
||
func buildPlayletThemeListCanonicalURL(slug string, themeId int64) string {
|
||
return h5path.TopicDetail(slug, themeId)
|
||
}
|
||
|
||
// buildPlayletTagListCanonicalURL 标签列表页相对路径(对齐 jha-1045-hgdjnet-web /tags/:slug)
|
||
func buildPlayletTagListCanonicalURL(slug string, tagId int64) string {
|
||
return h5path.Tag(slug, tagId)
|
||
}
|
||
|
||
func playletSiteEntityPath(prefix, slug string, id int64) string {
|
||
return h5path.Entity(prefix, slug, id)
|
||
}
|
||
|
||
// 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 := h5path.PlayletChapter(slug, playletId, chapterId)
|
||
if pagePath == "" {
|
||
pagePath = h5path.Playlet(slug, playletId)
|
||
}
|
||
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(ctx context.Context, keyword string, pageNum int64, pageSize int64) *proto.PlayletSearchResp {
|
||
// ES索引未创建或暂无数据时返回空列表,保持接口响应结构稳定。
|
||
return &proto.PlayletSearchResp{
|
||
TotalCount: 0,
|
||
PageSize: int(pageSize),
|
||
PageNum: int(pageNum),
|
||
Keyword: keyword,
|
||
List: make([]proto.CommonData, 0),
|
||
EveryoneWatching: guessLikeCommonData(ctx),
|
||
}
|
||
}
|
||
|
||
// buildHomePlayletResponse 查询并组装app首页短剧数据
|
||
func buildHomePlayletResponse(ctx context.Context) (*proto.HomePlayletResponse, *srvdef.SrvErr) {
|
||
// 定义返回
|
||
resp := &proto.HomePlayletResponse{
|
||
HomeBannerData: make([]*proto.CommonData, 0),
|
||
Modules: make([]*proto.HomeModuleData, 0),
|
||
Announcements: make([]*proto.AnnouncementItem, 0),
|
||
}
|
||
// Home首页数据响应
|
||
bannerList, err := findHomeBanners(ctx)
|
||
if err != nil {
|
||
log.Error("查询首页banner数据失败", log.E(err))
|
||
return nil, err
|
||
}
|
||
configList, err := findHomeConfigs(ctx)
|
||
if err != nil {
|
||
log.Error("查询首页配置数据失败", log.E(err))
|
||
return nil, err
|
||
}
|
||
configPlayletMap, configThemeMap, configPlaylets, err := findHomeConfigPlaylets(ctx, configList)
|
||
if err != nil {
|
||
log.Error("查询首页配置短剧数据失败", log.E(err))
|
||
return nil, err
|
||
}
|
||
allPlaylets := mergeHomePlayletLists(bannerList, configPlaylets)
|
||
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 = playletsToHomeCommonData(ctx, bannerList, themeNameMap, tagNameMap)
|
||
for _, item := range homeConfigsToHomeData(ctx, configList, configPlayletMap, configThemeMap, themeNameMap, tagNameMap) {
|
||
resp.Modules = append(resp.Modules, homeConfigToModule(item))
|
||
}
|
||
seo := configser.GetSiteSeoInfo()
|
||
resp.SeoInfo = &seo
|
||
return resp, nil
|
||
}
|
||
|
||
func attachHomeAnnouncements(resp *proto.HomePlayletResponse) {
|
||
if resp == nil {
|
||
return
|
||
}
|
||
resp.Announcements = configser.GetAnnouncements()
|
||
}
|
||
|
||
func attachHomeAdvitise(resp *proto.HomePlayletResponse) {
|
||
if resp == nil {
|
||
return
|
||
}
|
||
resp.Advitise = configser.GetAdvitise()
|
||
}
|
||
|
||
func guessLikeCommonData(ctx context.Context) []*proto.CommonData {
|
||
list := playletcache.ListGuessLike()
|
||
themeNameMap, _ := buildHomeThemeNameMap(ctx, list)
|
||
tagNameMap, _ := buildHomeTagNameMap(ctx, list)
|
||
return playletsToHomeCommonData(ctx, list, themeNameMap, tagNameMap)
|
||
}
|
||
|
||
// findHomeBanners 查询展示在首页 banner 的上架短剧
|
||
func findHomeBanners(ctx context.Context) (playletmod.PlayletList, *srvdef.SrvErr) {
|
||
return findHomePlaylets(ctx, homeUpPlayletFilter(bson.M{
|
||
playletmod.FN.Playlet.ShowOnBanner: true,
|
||
}), bson.D{
|
||
{Key: playletmod.FN.Playlet.TopRank, Value: -1},
|
||
{Key: playletmod.FN.Playlet.CreatedAt, Value: -1},
|
||
{Key: playletmod.FN.Playlet.Id, Value: -1},
|
||
}, homeBannerLimit)
|
||
}
|
||
|
||
// findHomeConfigs 查询启用的首页配置
|
||
func findHomeConfigs(ctx context.Context) (homeconfigmod.HomeConfigList, *srvdef.SrvErr) {
|
||
return homeconfigmod.FindManyHomeConfig(ctx, bson.M{
|
||
homeconfigmod.FN.HomeConfig.Status: true,
|
||
homeconfigmod.FN.HomeConfig.Type: bson.M{"$in": []int32{homeconfigmod.TypePlaylet, homeconfigmod.TypeTheme}},
|
||
}, options.Find().
|
||
SetProjection(homeConfigAppProjection).
|
||
SetSort(bson.D{
|
||
{Key: homeconfigmod.FN.HomeConfig.Rank, Value: -1},
|
||
{Key: homeconfigmod.FN.HomeConfig.Id, Value: -1},
|
||
}))
|
||
}
|
||
|
||
// findHomeConfigPlaylets 按首页配置查询短剧集合
|
||
func findHomeConfigPlaylets(ctx context.Context, configs homeconfigmod.HomeConfigList) (map[int64]playletmod.PlayletList, map[int64][]homeConfigThemePlaylets, playletmod.PlayletList, *srvdef.SrvErr) {
|
||
configPlayletMap := make(map[int64]playletmod.PlayletList, len(configs))
|
||
configThemeMap := make(map[int64][]homeConfigThemePlaylets, len(configs))
|
||
allPlaylets := make(playletmod.PlayletList, 0)
|
||
for _, config := range configs {
|
||
if config == nil {
|
||
continue
|
||
}
|
||
switch config.Type {
|
||
case homeconfigmod.TypePlaylet:
|
||
list, err := findHomeConfigPlayletListForConfig(ctx, config, homeConfigDisplayLimit(config))
|
||
if err != nil {
|
||
return nil, nil, nil, err
|
||
}
|
||
configPlayletMap[config.Id] = list
|
||
allPlaylets = append(allPlaylets, list...)
|
||
case homeconfigmod.TypeTheme:
|
||
groups, list, err := findHomeConfigThemePlayletsForConfig(ctx, config, 0, homeConfigDisplayLimit(config))
|
||
if err != nil {
|
||
return nil, nil, nil, err
|
||
}
|
||
configThemeMap[config.Id] = groups
|
||
allPlaylets = append(allPlaylets, list...)
|
||
}
|
||
}
|
||
return configPlayletMap, configThemeMap, allPlaylets, nil
|
||
}
|
||
|
||
func homeConfigDisplayLimit(config *homeconfigmod.HomeConfig) int64 {
|
||
if config != nil && config.Limit > 0 {
|
||
if config.Limit > maxPageSize {
|
||
return maxPageSize
|
||
}
|
||
return config.Limit
|
||
}
|
||
if config != nil && config.Type == homeconfigmod.TypeTheme {
|
||
return homeConfigThemeLimit
|
||
}
|
||
return homeConfigPlayletLimit
|
||
}
|
||
|
||
func findHomeConfigPlayletList(ctx context.Context, sortType int32, categoryId, limit int64) (playletmod.PlayletList, *srvdef.SrvErr) {
|
||
filter, sort := homeConfigPlayletQuery(sortType, categoryId)
|
||
list, err := findHomePlayletsByHomeConfigSort(ctx, filter, sortType, categoryId, sort, 0, limit)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if sortType != homeconfigmod.SortTypeRecommend || int64(len(list)) >= limit {
|
||
return list, nil
|
||
}
|
||
filter, sort = homeConfigPlayletQuery(homeconfigmod.SortTypeOnlineAt, 0)
|
||
if len(list) > 0 {
|
||
filter[playletmod.FN.Playlet.Id] = bson.M{"$nin": playletIdsFromList(list)}
|
||
}
|
||
moreList, err := findHomePlaylets(ctx, filter, sort, limit-int64(len(list)))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return append(list, moreList...), nil
|
||
}
|
||
|
||
func findHomePlayletsByHomeConfigSort(ctx context.Context, filter bson.M, sortType int32, categoryId int64, sort bson.D, skip, limit int64) (playletmod.PlayletList, *srvdef.SrvErr) {
|
||
if sortType == homeconfigmod.SortTypeCategory && categoryId > 0 {
|
||
return playletmod.FindManyPlayletByCategoryRank(ctx, filter, categoryId, skip, limit, playletmod.CategoryRankSort(), playletHomeProjection)
|
||
}
|
||
return findHomePlaylets(ctx, filter, sort, limit, options.Find().SetSkip(skip))
|
||
}
|
||
|
||
func playletIdsFromList(list playletmod.PlayletList) []int64 {
|
||
ids := make([]int64, 0, len(list))
|
||
for _, item := range list {
|
||
if item == nil || item.Id <= 0 {
|
||
continue
|
||
}
|
||
ids = append(ids, item.Id)
|
||
}
|
||
return ids
|
||
}
|
||
|
||
func findHomeConfigThemePlaylets(ctx context.Context, limit int64) ([]homeConfigThemePlaylets, playletmod.PlayletList, *srvdef.SrvErr) {
|
||
themes := playletcache.ListEnabledThemesByPlayletCount(0)
|
||
result := make([]homeConfigThemePlaylets, 0)
|
||
allPlaylets := make(playletmod.PlayletList, 0)
|
||
for _, theme := range themes {
|
||
if theme == nil {
|
||
continue
|
||
}
|
||
filter := topicDetailPlayletFilter(theme.Id)
|
||
count, err := playletmod.CountPlaylet(ctx, filter)
|
||
if err != nil {
|
||
return nil, nil, err
|
||
}
|
||
if count == 0 {
|
||
continue
|
||
}
|
||
list, err := findHomePlaylets(ctx, filter, playletTopicDetailSort(topicDetailSortEdit), homeConfigPlayletLimit)
|
||
if err != nil {
|
||
return nil, nil, err
|
||
}
|
||
if len(list) == 0 {
|
||
continue
|
||
}
|
||
result = append(result, homeConfigThemePlaylets{
|
||
Theme: theme,
|
||
Playlets: list,
|
||
TotalCount: count,
|
||
})
|
||
allPlaylets = append(allPlaylets, list...)
|
||
if int64(len(result)) >= limit {
|
||
break
|
||
}
|
||
}
|
||
return result, allPlaylets, nil
|
||
}
|
||
|
||
// findHomeConfigThemePlayletsPage 分页查询有短剧数据的首页配置题材
|
||
func findHomeConfigThemePlayletsPage(ctx context.Context, skip, limit int64) ([]homeConfigThemePlaylets, int64, playletmod.PlayletList, *srvdef.SrvErr) {
|
||
themes := playletcache.ListEnabledThemesByPlayletCount(0)
|
||
result := make([]homeConfigThemePlaylets, 0)
|
||
allPlaylets := make(playletmod.PlayletList, 0)
|
||
totalCount := int64(0)
|
||
for _, theme := range themes {
|
||
if theme == nil {
|
||
continue
|
||
}
|
||
filter := topicDetailPlayletFilter(theme.Id)
|
||
count, err := playletmod.CountPlaylet(ctx, filter)
|
||
if err != nil {
|
||
return nil, 0, nil, err
|
||
}
|
||
if count == 0 {
|
||
continue
|
||
}
|
||
if totalCount >= skip && int64(len(result)) < limit {
|
||
list, err := findHomePlaylets(ctx, filter, playletTopicDetailSort(topicDetailSortEdit), homeConfigPlayletLimit)
|
||
if err != nil {
|
||
return nil, 0, nil, err
|
||
}
|
||
result = append(result, homeConfigThemePlaylets{
|
||
Theme: theme,
|
||
Playlets: list,
|
||
TotalCount: count,
|
||
})
|
||
allPlaylets = append(allPlaylets, list...)
|
||
}
|
||
totalCount++
|
||
}
|
||
return result, totalCount, allPlaylets, nil
|
||
}
|
||
|
||
// fillPlayletTopicDetailMore 填充专题详情更多专题
|
||
func fillPlayletTopicDetailMore(ctx context.Context, resp *proto.PlayletTopicDetailResp, currentThemeID int64, seedPlaylets playletmod.PlayletList) (*proto.PlayletTopicDetailResp, *srvdef.SrvErr) {
|
||
if resp == nil {
|
||
return resp, nil
|
||
}
|
||
groups, morePlaylets, err := findPlayletTopicMoreGroups(ctx, currentThemeID, topicDetailMoreLimit)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
allPlaylets := mergeHomePlayletLists(seedPlaylets, morePlaylets)
|
||
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.MoreTopics = playletTopicsToPageData(ctx, groups, themeNameMap, tagNameMap)
|
||
return resp, nil
|
||
}
|
||
|
||
// findPlayletTopicMoreGroups 查询更多专题
|
||
func findPlayletTopicMoreGroups(ctx context.Context, currentThemeID int64, limit int64) ([]homeConfigThemePlaylets, playletmod.PlayletList, *srvdef.SrvErr) {
|
||
themes := playletcache.ListEnabledThemesByPlayletCount(0)
|
||
selected := make([]*playletcache.ThemeItem, 0, limit)
|
||
for _, theme := range themes {
|
||
if theme == nil || theme.Id == currentThemeID {
|
||
continue
|
||
}
|
||
selected = append(selected, theme)
|
||
if int64(len(selected)) >= limit {
|
||
break
|
||
}
|
||
}
|
||
groups, _, playlets, err := buildPlayletTopicGroups(ctx, selected, int64(len(selected)), topicListSortHot, topicListPlayletLimit)
|
||
if err != nil {
|
||
return nil, nil, err
|
||
}
|
||
return groups, playlets, nil
|
||
}
|
||
|
||
// buildPlayletTopicGroups 查询专题卡片需要的短剧数据
|
||
func buildPlayletTopicGroups(ctx context.Context, themes []*playletcache.ThemeItem, totalCount int64, sortType, playletLimit int64) ([]homeConfigThemePlaylets, int64, playletmod.PlayletList, *srvdef.SrvErr) {
|
||
result := make([]homeConfigThemePlaylets, 0, len(themes))
|
||
allPlaylets := make(playletmod.PlayletList, 0)
|
||
for _, theme := range themes {
|
||
if theme == nil {
|
||
continue
|
||
}
|
||
filter, sort := playletTopicPagePlayletQuery(sortType, theme.Id)
|
||
list, err := findHomePlaylets(ctx, filter, sort, playletLimit)
|
||
if err != nil {
|
||
return nil, 0, nil, err
|
||
}
|
||
if len(list) == 0 {
|
||
continue
|
||
}
|
||
result = append(result, homeConfigThemePlaylets{
|
||
Theme: theme,
|
||
Playlets: list,
|
||
})
|
||
allPlaylets = append(allPlaylets, list...)
|
||
}
|
||
return result, totalCount, allPlaylets, nil
|
||
}
|
||
|
||
// playletTopicPagePlayletQuery 构建专题卡片短剧查询条件
|
||
func playletTopicPagePlayletQuery(sortType int64, themeID int64) (bson.M, bson.D) {
|
||
filter := homeUpPlayletFilter(nil)
|
||
filter[playletmod.FN.Playlet.ThemeIds] = themeID
|
||
if sortType == topicListSortHot {
|
||
return filter, bson.D{
|
||
{Key: playletmod.FN.Playlet.HeatValue, Value: -1},
|
||
{Key: playletmod.FN.Playlet.Id, Value: -1},
|
||
}
|
||
}
|
||
return filter, bson.D{
|
||
{Key: playletmod.FN.Playlet.OnlineAt, Value: -1},
|
||
{Key: playletmod.FN.Playlet.Id, Value: -1},
|
||
}
|
||
}
|
||
|
||
// topicDetailPlayletFilter 构建专题详情和首页题材共用的短剧过滤条件
|
||
func topicDetailPlayletFilter(themeID int64) bson.M {
|
||
return homeUpPlayletFilter(bson.M{playletmod.FN.Playlet.ThemeIds: themeID})
|
||
}
|
||
|
||
func homeConfigCategoryID(sortType int32, configId int64) int64 {
|
||
if sortType == homeconfigmod.SortTypeCategory && configId > 0 {
|
||
return configId
|
||
}
|
||
return 0
|
||
}
|
||
|
||
func homeConfigPlayletQuery(sortType int32, categoryId int64) (bson.M, bson.D) {
|
||
filter := homeUpPlayletFilter(nil)
|
||
switch sortType {
|
||
case homeconfigmod.SortTypeRecommend:
|
||
filter[playletmod.FN.Playlet.IsRecommend] = true
|
||
return filter, playletTodayRecommendSort()
|
||
case homeconfigmod.SortTypeHot:
|
||
return filter, playletHitSeriesSort()
|
||
case homeconfigmod.SortTypeLatest:
|
||
return filter, playletRecentlyUpdatedSort()
|
||
case homeconfigmod.SortTypeOnlineAt:
|
||
return filter, playletTodayUpdateSort()
|
||
case homeconfigmod.SortTypeCategory:
|
||
if categoryId > 0 {
|
||
filter[playletmod.FN.Playlet.CategoryIds] = bson.M{"$in": []int64{categoryId}}
|
||
}
|
||
return filter, playletTodayUpdateSort()
|
||
default:
|
||
return filter, playletTodayUpdateSort()
|
||
}
|
||
}
|
||
|
||
// 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)
|
||
}
|
||
|
||
// 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))
|
||
}
|
||
}
|
||
|
||
// 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
|
||
}
|
||
|
||
// homeConfigsToHomeData 转换首页配置响应数据
|
||
func homeConfigsToHomeData(ctx context.Context, list homeconfigmod.HomeConfigList, configPlayletMap map[int64]playletmod.PlayletList, configThemeMap map[int64][]homeConfigThemePlaylets, themeNameMap map[int64]string, tagNameMap map[int64]string) []*proto.HomeConfigData {
|
||
result := make([]*proto.HomeConfigData, 0, len(list))
|
||
for _, item := range list {
|
||
if item == nil {
|
||
continue
|
||
}
|
||
if item.SortType == homeconfigmod.SortTypeCategory {
|
||
if item.Type == homeconfigmod.TypePlaylet && len(configPlayletMap[item.Id]) == 0 {
|
||
continue
|
||
}
|
||
}
|
||
data := &proto.HomeConfigData{
|
||
Id: item.Id,
|
||
Name: item.Name,
|
||
Slug: item.Slug,
|
||
Desc: item.Desc,
|
||
Keyword: homeconfigmod.NormalizeKeywords(item.Keyword),
|
||
Label: item.Label,
|
||
Style: item.Style,
|
||
CardBackgroundImage: withImageDomain(ctx, item.CardBackgroundImage),
|
||
TopicIds: item.TopicIds,
|
||
CategoryId: item.CategoryId,
|
||
RecommendType: item.RecommendType,
|
||
Type: item.Type,
|
||
Rank: item.Rank,
|
||
Limit: homeConfigDisplayLimit(item),
|
||
SortType: item.SortType,
|
||
Status: item.Status,
|
||
CreatedAt: item.CreatedAt,
|
||
UpdatedAt: item.UpdatedAt,
|
||
List: playletsToHomeCommonData(ctx, configPlayletMap[item.Id], themeNameMap, tagNameMap),
|
||
ThemeList: make([]*proto.HomeConfigThemeData, 0),
|
||
SeoInfo: item.SeoInfo,
|
||
}
|
||
switch item.Type {
|
||
case homeconfigmod.TypeTheme:
|
||
data.List = make([]*proto.CommonData, 0)
|
||
data.ThemeList = homeConfigThemesToHomeData(ctx, configThemeMap[item.Id], themeNameMap, tagNameMap)
|
||
}
|
||
result = append(result, data)
|
||
}
|
||
return result
|
||
}
|
||
|
||
// homeConfigThemesToHomeData 转换首页配置题材响应数据
|
||
func homeConfigThemesToHomeData(ctx context.Context, groups []homeConfigThemePlaylets, themeNameMap map[int64]string, tagNameMap map[int64]string) []*proto.HomeConfigThemeData {
|
||
result := make([]*proto.HomeConfigThemeData, 0, len(groups))
|
||
for _, group := range groups {
|
||
if group.Theme == nil || len(group.Playlets) == 0 {
|
||
continue
|
||
}
|
||
list := playletsToHomeCommonData(ctx, group.Playlets, themeNameMap, tagNameMap)
|
||
result = append(result, &proto.HomeConfigThemeData{
|
||
Id: group.Theme.Id,
|
||
Name: group.Theme.Name,
|
||
Slug: group.Theme.Slug,
|
||
Icon: withImageDomain(ctx, group.Theme.Icon),
|
||
Rank: group.Theme.Rank,
|
||
PlayletCount: group.TotalCount,
|
||
List: list,
|
||
SeoInfo: buildPageSeoInfoFromValue(group.Theme.SeoInfo, group.Theme.Name, "", buildPlayletThemeListCanonicalURL(group.Theme.Slug, group.Theme.Id)),
|
||
})
|
||
}
|
||
return result
|
||
}
|
||
|
||
// playletTopicsToPageData 转换专题剧单分页响应
|
||
func playletTopicsToPageData(ctx context.Context, groups []homeConfigThemePlaylets, themeNameMap map[int64]string, tagNameMap map[int64]string) []*proto.PlayletTopicPageItem {
|
||
result := make([]*proto.PlayletTopicPageItem, 0, len(groups))
|
||
for _, group := range groups {
|
||
if group.Theme == nil || len(group.Playlets) == 0 {
|
||
continue
|
||
}
|
||
list := playletsToHomeCommonData(ctx, group.Playlets, themeNameMap, tagNameMap)
|
||
result = append(result, &proto.PlayletTopicPageItem{
|
||
Id: group.Theme.Id,
|
||
Name: group.Theme.Name,
|
||
Slug: group.Theme.Slug,
|
||
Desc: strings.TrimSpace(group.Theme.SeoInfo.Description),
|
||
Icon: withImageDomain(ctx, group.Theme.Icon),
|
||
Rank: group.Theme.Rank,
|
||
PlayletCount: int64(len(list)),
|
||
Covers: playletTopicCovers(list),
|
||
List: list,
|
||
SeoInfo: buildPageSeoInfoFromValue(group.Theme.SeoInfo, group.Theme.Name, "", buildPlayletThemeListCanonicalURL(group.Theme.Slug, group.Theme.Id)),
|
||
})
|
||
}
|
||
return result
|
||
}
|
||
|
||
// playletTopicCovers 提取专题卡片封面
|
||
func playletTopicCovers(list []*proto.CommonData) []string {
|
||
result := make([]string, 0, len(list))
|
||
for _, item := range list {
|
||
if item == nil || strings.TrimSpace(item.Cover) == "" {
|
||
continue
|
||
}
|
||
result = append(result, item.Cover)
|
||
}
|
||
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
|
||
}
|
||
|
||
// 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,
|
||
IsOriginal: item.IsOriginal || item.MediaCenterId == 0,
|
||
ShowOriginal: item.ShowOriginal,
|
||
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)),
|
||
CategoryIds: item.CategoryIds,
|
||
Title: item.Title,
|
||
PlayName: item.PlayName,
|
||
Description: item.Description,
|
||
Slug: item.Slug,
|
||
Desc: item.Desc,
|
||
Anchor: item.Anchor,
|
||
Cover: withImageDomain(ctx, item.DisplayCover()),
|
||
HorizontalCover: withImageDomain(ctx, item.DisplayHorizontalCover()),
|
||
Preview: item.Preview,
|
||
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
|
||
}
|
||
|
||
// playletAllListFilter 构建全部短剧分页过滤条件
|
||
func playletAllListFilter(req *proto.PlayletAllListReq) (bson.M, *srvdef.SrvErr) {
|
||
filter := homeUpPlayletFilter(nil)
|
||
if req.ThemeId > 0 {
|
||
filter[playletmod.FN.Playlet.ThemeIds] = bson.M{"$in": []int64{req.ThemeId}}
|
||
}
|
||
switch req.UpdateStatus {
|
||
case 0:
|
||
case playletmod.UpdateStatusSerial, playletmod.UpdateStatusDone:
|
||
filter[playletmod.FN.Playlet.UpdateStatus] = req.UpdateStatus
|
||
default:
|
||
return nil, srvdef.New(srvdef.ErrParamError, "更新状态无效", "更新状态无效")
|
||
}
|
||
return filter, nil
|
||
}
|
||
|
||
// playletAllListSort 构建全部短剧分页排序
|
||
func playletAllListSort(sortType int64) bson.D {
|
||
switch sortType {
|
||
case allListSortHot:
|
||
return bson.D{
|
||
{Key: playletmod.FN.Playlet.HeatValue, Value: -1},
|
||
{Key: playletmod.FN.Playlet.Id, Value: -1},
|
||
}
|
||
case allListSortCollect:
|
||
return bson.D{
|
||
{Key: playletmod.FN.Playlet.CountCollect, Value: -1},
|
||
{Key: playletmod.FN.Playlet.Id, Value: -1},
|
||
}
|
||
case allListSortLatest:
|
||
fallthrough
|
||
default:
|
||
return bson.D{
|
||
{Key: playletmod.FN.Playlet.OnlineAt, Value: -1},
|
||
{Key: playletmod.FN.Playlet.Id, Value: -1},
|
||
}
|
||
}
|
||
}
|
||
|
||
// playletTopicDetailSort 构建专题详情短剧排序
|
||
func playletTopicDetailSort(sortType int64) bson.D {
|
||
switch sortType {
|
||
case topicDetailSortLatest:
|
||
return bson.D{
|
||
{Key: playletmod.FN.Playlet.OnlineAt, Value: -1},
|
||
{Key: playletmod.FN.Playlet.Id, Value: -1},
|
||
}
|
||
case topicDetailSortHot:
|
||
return bson.D{
|
||
{Key: playletmod.FN.Playlet.HeatValue, Value: -1},
|
||
{Key: playletmod.FN.Playlet.Id, Value: -1},
|
||
}
|
||
case topicDetailSortEdit:
|
||
fallthrough
|
||
default:
|
||
return bson.D{
|
||
{Key: playletmod.FN.Playlet.TopRank, Value: -1},
|
||
{Key: playletmod.FN.Playlet.RecommendRank, Value: -1},
|
||
{Key: playletmod.FN.Playlet.OnlineAt, Value: -1},
|
||
{Key: playletmod.FN.Playlet.Id, 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,
|
||
},
|
||
},
|
||
{
|
||
// 与剧名入库的 ik_max_word 一致。查询默认的 ik_smart 会把长标题收成索引里没有的整词。
|
||
// 1~2 个词仍需全部命中;更长的标题至少命中 75%,避免括号和数字切出的多余词把结果清掉。
|
||
"multi_match": esmanager.M{
|
||
"query": keyword,
|
||
"fields": playletSearchFields(),
|
||
"type": "best_fields",
|
||
"analyzer": esmanager.AnalyzerIkMaxWord,
|
||
"minimum_should_match": "2<75%",
|
||
},
|
||
},
|
||
},
|
||
"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,
|
||
}
|
||
}
|
||
|
||
// loadESPlayletLabels 从Mongo批量读取标识,兼容尚未重建的ES文档
|
||
func loadESPlayletLabels(ctx context.Context, hits []*esmod.ESPlayletSource) (map[int64]*playletmod.Playlet, *srvdef.SrvErr) {
|
||
labels := make(map[int64]*playletmod.Playlet, len(hits))
|
||
ids := make([]int64, 0, len(hits))
|
||
for _, hit := range hits {
|
||
if hit != nil && hit.Source != nil {
|
||
ids = append(ids, hit.Source.Id)
|
||
}
|
||
}
|
||
if len(ids) == 0 {
|
||
return labels, nil
|
||
}
|
||
list, err := playletmod.FindManyPlaylet(ctx, bson.M{
|
||
playletmod.FN.Playlet.Id: bson.M{"$in": ids},
|
||
}, options.Find().SetProjection(bson.D{
|
||
{Key: playletmod.FN.Playlet.Id, Value: 1},
|
||
{Key: playletmod.FN.Playlet.MediaCenterId, Value: 1},
|
||
{Key: playletmod.FN.Playlet.IsOriginal, Value: 1},
|
||
{Key: playletmod.FN.Playlet.ShowOriginal, Value: 1},
|
||
}))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
for _, item := range list {
|
||
if item != nil {
|
||
labels[item.Id] = item
|
||
}
|
||
}
|
||
return labels, nil
|
||
}
|
||
|
||
// esPlayletToCommonData 转换ES短剧文档为app通用短剧响应
|
||
func esPlayletToCommonData(ctx context.Context, item *esmod.ESPlaylet, label *playletmod.Playlet) proto.CommonData {
|
||
isOriginal, showOriginal := false, false
|
||
if label != nil {
|
||
isOriginal = label.IsOriginal || label.MediaCenterId == 0
|
||
showOriginal = label.ShowOriginal
|
||
}
|
||
return proto.CommonData{
|
||
Id: item.Id,
|
||
Status: int32(item.Status),
|
||
IsFeatured: item.IsFeatured,
|
||
IsOriginal: isOriginal,
|
||
ShowOriginal: showOriginal,
|
||
IsRecommend: item.IsRecommend,
|
||
IsNew: isOnlineThisWeek(item.OnlineAt),
|
||
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)),
|
||
CategoryIds: item.CategoryIds,
|
||
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,
|
||
}
|
||
}
|
||
|
||
// 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},
|
||
}
|
||
}
|
||
}
|
||
|
||
// normalizePlayletHotRankType 规范化热榜类型
|
||
func normalizePlayletHotRankType(rankType int64) int64 {
|
||
if rankType == 0 {
|
||
return rankHot
|
||
}
|
||
switch rankType {
|
||
case rankHot, rankNew, rankCollect:
|
||
return rankType
|
||
default:
|
||
return 0
|
||
}
|
||
}
|
||
|
||
// playletHotRankSort 构建热榜排序
|
||
func playletHotRankSort(rankType int64) bson.D {
|
||
return playletRankSort(rankType)
|
||
}
|
||
|
||
// 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},
|
||
}
|
||
}
|
||
|
||
// playletRecentlyUpdatedSort 按短剧记录更新时间排序。
|
||
func playletRecentlyUpdatedSort() bson.D {
|
||
return bson.D{
|
||
{Key: playletmod.FN.Playlet.UpdatedAt, 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},
|
||
}
|
||
}
|
||
|
||
// 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
|
||
}
|
||
|
||
// normalizePlayletGuessLikePage 规范化猜你喜欢分页参数
|
||
func normalizePlayletGuessLikePage(pageNum, pageSize int64) (int64, int64) {
|
||
if pageNum <= 0 {
|
||
pageNum = 1
|
||
}
|
||
if pageSize <= 0 {
|
||
pageSize = guessLikePageSize
|
||
}
|
||
if pageSize > maxPageSize {
|
||
pageSize = maxPageSize
|
||
}
|
||
return pageNum, pageSize
|
||
}
|
||
|
||
// normalizePlayletHotRankPage 规范化热榜分页参数
|
||
func normalizePlayletHotRankPage(pageNum, pageSize int64) (int64, int64) {
|
||
if pageNum <= 0 {
|
||
pageNum = 1
|
||
}
|
||
if pageSize <= 0 {
|
||
pageSize = hotRankPageSize
|
||
}
|
||
if pageSize > maxPageSize {
|
||
pageSize = maxPageSize
|
||
}
|
||
return pageNum, pageSize
|
||
}
|
||
|
||
// normalizePlayletTopicDetailPage 规范化专题详情分页参数
|
||
func normalizePlayletTopicDetailPage(pageNum, pageSize int64) (int64, int64) {
|
||
if pageNum <= 0 {
|
||
pageNum = 1
|
||
}
|
||
if pageSize <= 0 {
|
||
pageSize = topicDetailPageSize
|
||
}
|
||
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)
|
||
}
|