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 } }