Initial commit: web_hghl_server

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-10-26 16:28:14 +08:00
co-authored by Claude Opus 4.7
commit 65fca60622
653 changed files with 135901 additions and 0 deletions
+126
View File
@@ -0,0 +1,126 @@
package playletser
import (
"context"
"time"
"videoweb/app/api/proto"
"videoweb/app/service/configser"
"videoweb/app/service/moreser"
"videoweb/app/service/reportser"
"videoweb/app/service/translationser"
"videoweb/pub/common/log"
"videoweb/pub/common/response"
"videoweb/pub/common/srvdef"
"videoweb/pub/common/useragent"
"videoweb/pub/models/playletfavoritemod"
"videoweb/pub/models/playletmod"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo/options"
)
// FavoritePlaylet 收藏或取消收藏上架短剧,重复请求不重复变更收藏数
func FavoritePlaylet(ctx context.Context, userID int64, ip string, ua useragent.UA, req *proto.FavoritePlayletReq) (*proto.FavoritePlayletResp, *srvdef.SrvErr) {
if req == nil || userID <= 0 || req.Id <= 0 || req.IsFavorite == nil {
return nil, srvdef.New(srvdef.ErrParamError, "", "")
}
playlet, err := playletmod.FindOnePlaylet(ctx, bson.M{
playletmod.FN.Playlet.Id: req.Id, playletmod.FN.Playlet.Status: playletmod.StatusUp,
}, options.FindOne().SetProjection(bson.M{
playletmod.FN.Playlet.Id: 1, playletmod.FN.Playlet.Title: 1,
playletmod.FN.Playlet.TagIds: 1, playletmod.FN.Playlet.ThemeIds: 1,
playletmod.FN.Playlet.CountCollect: 1,
}))
if err != nil {
return nil, err
}
changed := false
flag := int64(1)
if *req.IsFavorite {
changed, err = playletfavoritemod.Upsert(ctx, userID, req.Id, time.Now())
} else {
flag = 2
changed, err = playletfavoritemod.Delete(ctx, bson.M{
playletfavoritemod.FieldUserID: userID, playletfavoritemod.FieldPlayletID: req.Id,
})
}
if err != nil {
return nil, err
}
if changed {
delta := int64(1)
if !*req.IsFavorite {
delta = -1
}
if delta > 0 || playlet.CountCollect > 0 {
if err := playletmod.UpdateOnePlaylet(ctx, bson.M{playletmod.FN.Playlet.Id: req.Id}, bson.M{
"$inc": bson.M{playletmod.FN.Playlet.CountCollect: delta},
}); err != nil {
return nil, err
}
playlet.CountCollect += delta
}
if err := reportser.ReportPlayletCollect(userID, ua, ip, playlet, flag); err != nil {
log.Warn("ReportPlayletCollect failed", log.Any("playletId", req.Id), log.Any("userId", userID), log.E(err))
}
}
return &proto.FavoritePlayletResp{Id: req.Id, IsFavorite: *req.IsFavorite, CountCollect: playlet.CountCollect}, nil
}
// QueryFavoritePlayletList 按收藏时间倒序查询当前用户收藏的上架短剧
func QueryFavoritePlayletList(ctx context.Context, userID int64, req *proto.QueryFavoritePlayletListReq) (*proto.QueryFavoritePlayletListResp, *srvdef.SrvErr) {
if userID <= 0 || req == nil {
return nil, srvdef.New(srvdef.ErrParamError, "", "")
}
pageNum, pageSize := moreser.NormalizePage(req.PageNum, req.PageSize, 20, 100)
pipeline := []bson.M{
{"$match": bson.M{playletfavoritemod.FieldUserID: userID}},
{"$lookup": bson.M{
"from": playletmod.TN.Playlet,
"localField": playletfavoritemod.FieldPlayletID,
"foreignField": playletmod.FN.Playlet.Id,
"as": "playlet",
}},
{"$unwind": "$playlet"},
{"$match": bson.M{"playlet." + playletmod.FN.Playlet.Status: playletmod.StatusUp}},
}
countRows := make([]struct {
Total int64 `bson:"total"`
}, 0)
countPipeline := append(append([]bson.M(nil), pipeline...), bson.M{"$count": "total"})
if err := playletfavoritemod.Aggregate(ctx, &countRows, countPipeline); err != nil {
return nil, err
}
total := int64(0)
if len(countRows) > 0 {
total = countRows[0].Total
}
resp := &proto.QueryFavoritePlayletListResp{BasePageResp: response.BasePageResp{
TotalCount: total, PageNum: int(pageNum), PageSize: int(pageSize), List: make([]*proto.PlayletData, 0),
}, Translations: translationser.GetTranslationData(), BottonTranslations: translationser.GetTranslationBottomList()}
if total == 0 || pageNum > (total-1)/pageSize+1 {
return resp, nil
}
pagePipeline := append(append([]bson.M(nil), pipeline...),
bson.M{"$sort": bson.D{{Key: playletfavoritemod.FieldCreatedAt, Value: -1}, {Key: playletfavoritemod.FieldID, Value: -1}}},
bson.M{"$skip": (pageNum - 1) * pageSize}, bson.M{"$limit": pageSize},
bson.M{"$replaceRoot": bson.M{"newRoot": "$playlet"}},
bson.M{"$project": appPlayletRecommendProjection},
)
list := make(playletmod.PlayletList, 0)
if err := playletfavoritemod.Aggregate(ctx, &list, pagePipeline); err != nil {
return nil, err
}
tagNames, themeNames, err := buildPlayletTagThemeNameMaps(ctx, list)
if err != nil {
return nil, err
}
cdn := configser.GetRequestConfig(ctx).ImgCdn
items := make([]*proto.PlayletData, 0, len(list))
for _, item := range list {
data := playletToAppData(item, cdn, tagNames, themeNames)
items = append(items, &data)
}
resp.List = items
return resp, nil
}
+73
View File
@@ -0,0 +1,73 @@
package playletser
import (
"context"
"videoweb/app/api/proto"
"videoweb/app/service/configser"
"videoweb/app/service/moreser"
"videoweb/app/service/translationser"
"videoweb/pub/common/response"
"videoweb/pub/common/srvdef"
"videoweb/pub/models/playletlikemod"
"videoweb/pub/models/playletmod"
"go.mongodb.org/mongo-driver/bson"
)
// QueryLikePlayletList 按点赞时间倒序查询当前用户点赞的上架短剧
func QueryLikePlayletList(ctx context.Context, userID int64, req *proto.QueryLikePlayletListReq) (*proto.QueryLikePlayletListResp, *srvdef.SrvErr) {
if userID <= 0 || req == nil {
return nil, srvdef.New(srvdef.ErrParamError, "", "")
}
pageNum, pageSize := moreser.NormalizePage(req.PageNum, req.PageSize, 20, 100)
pipeline := []bson.M{
{"$match": bson.M{playletlikemod.FN.PlayletLike.UserID: userID}},
{"$lookup": bson.M{
"from": playletmod.TN.Playlet,
"localField": playletlikemod.FN.PlayletLike.PlayletID,
"foreignField": playletmod.FN.Playlet.Id,
"as": "playlet",
}},
{"$unwind": "$playlet"},
{"$match": bson.M{"playlet." + playletmod.FN.Playlet.Status: playletmod.StatusUp}},
}
countRows := make([]struct {
Total int64 `bson:"total"`
}, 0)
if err := playletlikemod.AggregatePlayletLike(ctx, &countRows, append(append([]bson.M(nil), pipeline...), bson.M{"$count": "total"})); err != nil {
return nil, err
}
total := int64(0)
if len(countRows) > 0 {
total = countRows[0].Total
}
resp := &proto.QueryLikePlayletListResp{BasePageResp: response.BasePageResp{
TotalCount: total, PageNum: int(pageNum), PageSize: int(pageSize), List: make([]*proto.PlayletData, 0),
}, Translations: translationser.GetTranslationData(), BottonTranslations: translationser.GetTranslationBottomList()}
if total == 0 || pageNum > (total-1)/pageSize+1 {
return resp, nil
}
pagePipeline := append([]bson.M{pipeline[0], {"$sort": bson.D{{Key: playletlikemod.FN.PlayletLike.CreatedAt, Value: -1}, {Key: playletlikemod.FN.PlayletLike.Id, Value: -1}}}}, pipeline[1:]...)
pagePipeline = append(pagePipeline,
bson.M{"$skip": (pageNum - 1) * pageSize},
bson.M{"$limit": pageSize},
bson.M{"$replaceRoot": bson.M{"newRoot": "$playlet"}},
bson.M{"$project": appPlayletRecommendProjection},
)
list := make(playletmod.PlayletList, 0)
if err := playletlikemod.AggregatePlayletLike(ctx, &list, pagePipeline); err != nil {
return nil, err
}
tagNames, themeNames, err := buildPlayletTagThemeNameMaps(ctx, list)
if err != nil {
return nil, err
}
cdn := configser.GetRequestConfig(ctx).ImgCdn
items := make([]*proto.PlayletData, 0, len(list))
for _, item := range list {
data := playletToAppData(item, cdn, tagNames, themeNames)
items = append(items, &data)
}
resp.List = items
return resp, nil
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,137 @@
package playletser
import (
"context"
"testing"
"videoweb/app/api/proto"
"videoweb/app/service/moreser"
"videoweb/pub/models/playletmod"
"videoweb/pub/models/videoinformationmod"
"videoweb/pub/storage/proxy"
"go.mongodb.org/mongo-driver/bson"
)
func TestPlayletContentListRejectsInvalidDataType(t *testing.T) {
for _, dataType := range []int64{0, 3, -1} {
if _, err := ContentList(context.Background(), &proto.PlayletContentListReq{DataType: dataType}); err == nil {
t.Errorf("dataType %d 未返回参数错误", dataType)
}
}
if _, err := ContentList(context.Background(), nil); err == nil {
t.Fatal("空请求未返回参数错误")
}
}
func TestPlayletContentWatchSort(t *testing.T) {
playletSort := playletContentPlayletSort()
if playletSort[0].Key != playletmod.FN.Playlet.CountBrowse || playletSort[0].Value != -1 {
t.Fatalf("短剧未优先按观看量排序: %v", playletSort)
}
pipeline := playletContentVideoPipeline(7, 10)
if pipeline[0]["$match"].(bson.M)[videoinformationmod.FN.VideoInformation.Status] == nil {
t.Fatalf("视频未限制上架状态: %v", pipeline[0])
}
fields := pipeline[1]["$addFields"].(bson.M)
watchSum := fields["watchScore"].(bson.M)["$add"].([]interface{})
if len(watchSum) != 2 {
t.Fatalf("视频观看量未包含真实数和后台配置数: %v", fields)
}
for i, field := range []string{
videoinformationmod.FN.VideoInformation.Real.WatchTimes,
videoinformationmod.FN.VideoInformation.Fake.WatchTimes,
} {
value := watchSum[i].(bson.M)["$ifNull"].([]interface{})
if value[0] != "$"+field || value[1] != 0 {
t.Fatalf("视频观看量字段 %d 错误: %v", i, value)
}
}
sort := pipeline[2]["$sort"].(bson.D)
if sort[0].Key != "watchScore" || sort[0].Value != -1 ||
sort[1].Key != videoinformationmod.FN.VideoInformation.UpdatedAt ||
sort[2].Key != videoinformationmod.FN.VideoInformation.Id {
t.Fatalf("视频未优先按展示观看量排序: %v", sort)
}
if pipeline[3]["$skip"] != int64(7) || pipeline[4]["$limit"] != int64(10) {
t.Fatalf("视频分页位置错误: %v", pipeline)
}
}
func TestPlayletContentListMixedPagination(t *testing.T) {
for _, dataType := range []int64{1, 2} {
const contentCount int64 = 14
ads := []proxy.AdvitiseInfo{{Id: "ad-1"}, {Id: "ad-2"}}
interval := playletContentAdInterval(dataType)
adCount := playletContentInsertedAdsCount(contentCount, len(ads), interval)
wantAdCount := int64(3)
if dataType == 2 {
wantAdCount = 2
}
if adCount != wantAdCount {
t.Fatalf("dataType %d 广告位数量 = %d, want %d", dataType, adCount, wantAdCount)
}
content := make([]*proto.PlayletContentListItem, 0, contentCount)
for id := int64(1); id <= contentCount; id++ {
item := &proto.PlayletContentListItem{Type: 0}
if dataType == 1 {
item.Playlet = &proto.PlayletData{Id: id}
} else {
item.Video = &proto.VideoInfo{ContentData: proto.ContentData{Id: id}}
}
content = append(content, item)
}
var got []*proto.PlayletContentListItem
for start := int64(0); start < contentCount+adCount; start += 6 {
end := start + 6
if end > contentCount+adCount {
end = contentCount + adCount
}
contentStart, contentLimit := moreser.ContentRange(start, end, contentCount, adCount, interval)
page := buildPlayletContentMixedPage(content[contentStart:contentStart+contentLimit], ads, "",
start, end, contentStart, adCount, interval)
if int64(len(page)) != end-start {
t.Fatalf("dataType %d 分页 [%d,%d) 返回 %d 条", dataType, start, end, len(page))
}
got = append(got, page...)
}
var nextID int64 = 1
for pos, item := range got {
cycle := int(interval + 1)
if pos%cycle == int(interval) && int64(pos/cycle) < adCount {
if item.Type != 2 || item.Advertise == nil || item.Advertise.Id != ads[(pos/cycle)%len(ads)].Id {
t.Fatalf("dataType %d 位置 %d 广告错误: %+v", dataType, pos, item)
}
continue
}
if item.Type != 0 || item.Advertise != nil {
t.Fatalf("dataType %d 位置 %d 内容类型错误: %+v", dataType, pos, item)
}
if dataType == 1 && (item.Playlet == nil || item.Playlet.Id != nextID || item.Video != nil) {
t.Fatalf("位置 %d 短剧错误: %+v", pos, item)
}
if dataType == 2 && (item.Video == nil || item.Video.Id != nextID || item.Playlet != nil) {
t.Fatalf("位置 %d 视频错误: %+v", pos, item)
}
nextID++
}
if nextID != contentCount+1 {
t.Fatalf("dataType %d 内容末尾 ID = %d", dataType, nextID)
}
}
}
func TestPlayletContentAdSlots(t *testing.T) {
for _, tc := range []struct {
dataType int64
content int64
ads int
want int64
}{
{1, 0, 2, 0}, {1, 4, 2, 0}, {1, 5, 2, 1}, {1, 8, 2, 1}, {1, 9, 2, 2}, {1, 13, 1, 3}, {1, 13, 0, 0},
{2, 0, 2, 0}, {2, 5, 2, 0}, {2, 6, 2, 1}, {2, 10, 2, 1}, {2, 11, 2, 2}, {2, 16, 1, 3}, {2, 16, 0, 0},
} {
if got := playletContentInsertedAdsCount(tc.content, tc.ads, playletContentAdInterval(tc.dataType)); got != tc.want {
t.Errorf("dataType %d 内容 %d 条、广告 %d 条得到 %d 个广告位,want %d", tc.dataType, tc.content, tc.ads, got, tc.want)
}
}
}
+119
View File
@@ -0,0 +1,119 @@
package playletser
import (
"context"
"testing"
"videoweb/app/api/proto"
"videoweb/app/service/moreser"
"videoweb/pub/models/playletmod"
"videoweb/pub/storage/proxy"
)
func TestPlayletListSortOptions(t *testing.T) {
for _, sortType := range []int64{0, 1} {
sort := playletListSort(sortType)
if len(sort) == 0 || sort[0].Key != playletmod.FN.Playlet.HeatValue {
t.Fatalf("sortType %d 未按热度优先排序: %v", sortType, sort)
}
}
sort := playletListSort(2)
if len(sort) == 0 || sort[0].Key != playletmod.FN.Playlet.UpdatedAt || sort[0].Value != -1 {
t.Fatalf("sortType 2 未按最近更新降序排序: %v", sort)
}
filter := playletListFilter()
if filter[playletmod.FN.Playlet.Status] != playletmod.StatusUp {
t.Fatalf("未限制上架状态: %v", filter)
}
if _, ok := filter[playletmod.FN.Playlet.UpdatedAt]; ok {
t.Fatalf("最近更新不应限制更新时间: %v", filter)
}
if _, err := PlayletList(context.Background(), &proto.PlayletListReq{SortType: 3}); err == nil {
t.Fatal("未拒绝无效排序方式")
}
}
func TestOfficialEpisodeRange(t *testing.T) {
tests := []struct {
count int64
open []int64
closed []int64
}{
{0, []int64{1, 2}, []int64{0, 3}},
{1, []int64{1}, []int64{2}},
{3, []int64{1, 2, 3}, []int64{4}},
{-1, []int64{1, 1001}, []int64{0}},
}
for _, tt := range tests {
playlet := &playletmod.Playlet{Status: playletmod.StatusUp, FreeEpisodeCount: tt.count}
for _, episode := range tt.open {
if !playlet.IsOfficialEpisodeOpen(episode) {
t.Fatalf("开放范围 %d 应允许第 %d 集", tt.count, episode)
}
}
for _, episode := range tt.closed {
if playlet.IsOfficialEpisodeOpen(episode) {
t.Fatalf("开放范围 %d 不应允许第 %d 集", tt.count, episode)
}
}
}
if (&playletmod.Playlet{Status: playletmod.StatusDown, FreeEpisodeCount: -1}).IsOfficialEpisodeOpen(1) {
t.Fatal("下架短剧不应开放")
}
}
func TestPlayletListMixedPagination(t *testing.T) {
const playletCount int64 = 14
ads := []proxy.AdvitiseInfo{{Id: "ad-1"}, {Id: "ad-2"}}
adCount := playletListInsertedAdsCount(playletCount, len(ads))
if adCount != 3 {
t.Fatalf("广告位数量 = %d, want 3", adCount)
}
all := make(playletmod.PlayletList, 0, playletCount)
for id := int64(1); id <= playletCount; id++ {
all = append(all, &playletmod.Playlet{Id: id})
}
var got []*proto.PlayletRecommendListItem
for start := int64(0); start < playletCount+adCount; start += 6 {
end := start + 6
if end > playletCount+adCount {
end = playletCount + adCount
}
contentStart, contentLimit := moreser.ContentRange(start, end, playletCount, adCount, playletListAdInterval)
page := buildPlayletListMixedPage(all[contentStart:contentStart+contentLimit], ads, "", nil, nil,
start, end, contentStart, adCount)
if int64(len(page)) != end-start {
t.Fatalf("分页 [%d,%d) 返回 %d 条", start, end, len(page))
}
got = append(got, page...)
}
var playletID int64 = 1
for pos, item := range got {
if pos%5 == 4 && pos < 15 {
if item.Type != proto.PlayletRecommendItemTypeAdvertise || item.AdvertiseId != ads[(pos/5)%len(ads)].Id {
t.Fatalf("位置 %d 广告错误: %+v", pos, item)
}
continue
}
if item.Type != proto.PlayletRecommendItemTypePlaylet || item.Id != playletID {
t.Fatalf("位置 %d 短剧错误: %+v, want ID %d", pos, item, playletID)
}
playletID++
}
if playletID != playletCount+1 {
t.Fatalf("短剧末尾 ID = %d, want %d", playletID, playletCount+1)
}
}
func TestPlayletListAdSlots(t *testing.T) {
for _, tc := range []struct {
playlets int64
ads int
want int64
}{
{0, 2, 0}, {4, 2, 0}, {5, 2, 1}, {8, 2, 1}, {9, 2, 2}, {13, 1, 3}, {13, 0, 0},
} {
if got := playletListInsertedAdsCount(tc.playlets, tc.ads); got != tc.want {
t.Errorf("短剧 %d 条、广告 %d 条得到 %d 个广告位,want %d", tc.playlets, tc.ads, got, tc.want)
}
}
}