Files
2026-10-26 16:28:14 +08:00

227 lines
6.4 KiB
Go

package displaycount
import (
"bytes"
"encoding/binary"
"encoding/json"
"hash/fnv"
"math"
"strings"
"videoweb/pub/common/log"
"videoweb/pub/models/videofavoritemod"
"videoweb/pub/models/videoinformationmod"
"videoweb/pub/models/videolikemod"
"github.com/gin-gonic/gin"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo/options"
)
type countRow struct {
Id int64 `bson:"_id"`
Count int64 `bson:"count"`
}
type videoCounts struct {
Watch int64
Like int64
Favorite int64
}
// Value 真实数大于零时放大十倍,零值按内容和指标生成稳定展示数
func Value(real, id int64, kind string) int64 {
if real > 0 {
if real > math.MaxInt64/10 {
return math.MaxInt64
}
return real * 10
}
h := fnv.New64a()
_, _ = h.Write([]byte(kind))
var key [8]byte
binary.LittleEndian.PutUint64(key[:], uint64(id))
_, _ = h.Write(key[:])
return 100 + int64(h.Sum64()%901)
}
// Transform 将 App 内容接口中的现有计数字段转换为展示值
func Transform(c *gin.Context, data interface{}) interface{} {
path := c.Request.URL.Path
if !contentPath(path) || data == nil {
return data
}
raw, err := json.Marshal(data)
if err != nil {
return data
}
decoder := json.NewDecoder(bytes.NewReader(raw))
decoder.UseNumber()
var body interface{}
if err := decoder.Decode(&body); err != nil {
return data
}
ids := make(map[int64]struct{})
collectVideoIDs(body, path, ids)
counts, err := loadVideoCounts(c, ids)
if err != nil {
log.Warn("displaycount: load video counts failed", log.E(err))
return data
}
apply(body, path, c.Query("rankPeriod"), counts)
return body
}
func contentPath(path string) bool {
return strings.HasPrefix(path, "/api/app/playlet/") ||
strings.HasPrefix(path, "/api/app/video/") ||
strings.HasPrefix(path, "/api/app/home") ||
strings.HasPrefix(path, "/api/app/actress/work/") ||
strings.HasPrefix(path, "/search")
}
func collectVideoIDs(node interface{}, path string, ids map[int64]struct{}) {
switch item := node.(type) {
case map[string]interface{}:
if isVideo(item, path) {
if id := number(item["id"]); id > 0 {
ids[id] = struct{}{}
}
}
for _, child := range item {
collectVideoIDs(child, path, ids)
}
case []interface{}:
for _, child := range item {
collectVideoIDs(child, path, ids)
}
}
}
func isVideo(item map[string]interface{}, path string) bool {
if item["type"] != nil && number(item["type"]) == 2 {
return false
}
if _, ok := item["watchNum"]; ok {
_, hasLike := item["likeNum"]
return hasLike
}
if _, ok := item["watchTimes"]; ok {
_, hasVideo := item["video"]
return hasVideo && number(item["type"]) != 2
}
return path == "/api/app/video/like" && item["likeNum"] != nil && item["id"] != nil
}
func loadVideoCounts(c *gin.Context, idSet map[int64]struct{}) (map[int64]videoCounts, error) {
result := make(map[int64]videoCounts, len(idSet))
if len(idSet) == 0 {
return result, nil
}
ids := make([]int64, 0, len(idSet))
for id := range idSet {
ids = append(ids, id)
}
videos, err := videoinformationmod.FindManyVideoInformations(c, bson.M{
videoinformationmod.FN.VideoInformation.Id: bson.M{"$in": ids},
}, options.Find().SetProjection(bson.M{
videoinformationmod.FN.VideoInformation.Id: 1,
videoinformationmod.FN.VideoInformation.Real.WatchTimes: 1,
}))
if err != nil {
return nil, err
}
for _, video := range videos {
result[video.Id] = videoCounts{Watch: video.Real.WatchTimes}
}
likes := make([]countRow, 0)
if err := videolikemod.AggregateVideoLike(c, &likes, countPipeline(videolikemod.FN.VideoLike.VideoID, ids)); err != nil {
return nil, err
}
for _, row := range likes {
value := result[row.Id]
value.Like = row.Count
result[row.Id] = value
}
favorites := make([]countRow, 0)
if err := videofavoritemod.AggregateVideoFavorite(c, &favorites, countPipeline(videofavoritemod.FN.VideoFavorite.VideoID, ids)); err != nil {
return nil, err
}
for _, row := range favorites {
value := result[row.Id]
value.Favorite = row.Count
result[row.Id] = value
}
return result, nil
}
func countPipeline(field string, ids []int64) []bson.M {
return []bson.M{
{"$match": bson.M{field: bson.M{"$in": ids}}},
{"$group": bson.M{"_id": "$" + field, "count": bson.M{"$sum": 1}}},
}
}
func apply(node interface{}, path, rankPeriod string, counts map[int64]videoCounts) {
switch item := node.(type) {
case map[string]interface{}:
id := number(item["id"])
if id > 0 && isVideo(item, path) {
count := counts[id]
if _, ok := item["watchNum"]; ok {
item["watchNum"] = Value(count.Watch, id, "video-watch")
}
if _, ok := item["watchTimes"]; ok {
item["watchTimes"] = Value(count.Watch, id, "video-watch")
}
item["likeNum"] = Value(count.Like, id, "video-like")
item["countCollect"] = Value(count.Favorite, id, "video-favorite")
} else if id > 0 && item["countBrowse"] != nil && item["countCollect"] != nil {
kind := "playlet"
if item["currentEpisode"] != nil {
kind = "playlet-chapter"
}
if item["rankScore"] != nil && path == "/api/app/playlet/rank/list" {
if rankPeriod == "" || rankPeriod == "0" {
item["rankScore"] = Value(number(item["countLike"]), id, kind+"-like")
} else {
item["rankScore"] = Value(number(item["rankScore"]), id, kind+"-period-watch-"+rankPeriod)
}
}
item["countBrowse"] = Value(number(item["countBrowse"]), id, kind+"-watch")
item["countCollect"] = Value(number(item["countCollect"]), id, kind+"-favorite")
if item["displayCountLike"] != nil {
like := Value(number(item["countLike"]), id, kind+"-like")
item["displayCountLike"] = like
item["countLike"] = like
} else if item["countLike"] != nil {
item["countLike"] = Value(number(item["countLike"]), id, kind+"-like")
}
} else if id > 0 && path == "/api/app/playlet/favorite" && item["countCollect"] != nil {
item["countCollect"] = Value(number(item["countCollect"]), id, "playlet-favorite")
} else if id > 0 && path == "/api/app/playlet/like" && item["likeNum"] != nil {
item["likeNum"] = Value(number(item["likeNum"]), id, "playlet-like")
}
for _, child := range item {
apply(child, path, rankPeriod, counts)
}
case []interface{}:
for _, child := range item {
apply(child, path, rankPeriod, counts)
}
}
}
func number(v interface{}) int64 {
switch n := v.(type) {
case json.Number:
value, _ := n.Int64()
return value
case int64:
return n
case float64:
return int64(n)
default:
return 0
}
}