package srv_playlet import ( "context" "hkdj-web-server/pub/common/srvdef" "hkdj-web-server/pub/common/utils" "hkdj-web-server/pub/models/playletchapterunlockmod" "hkdj-web-server/pub/models/playletcollectmod" "hkdj-web-server/pub/models/playletlikemod" "hkdj-web-server/pub/models/playletmod" "hkdj-web-server/pub/pubdef" "hkdj-web-server/web/api/proto" "regexp" "sort" "strings" "time" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo/options" ) const playletSalesLimit = 50 type playletSalesAggRow struct { PlayletId int64 `bson:"_id"` Count int64 `bson:"count"` Coins int64 `bson:"coins"` } type playletSalesRow struct { Id int64 UnlockCount int64 UnlockCoins int64 LikeCount int64 CollectCount int64 } // ListPlayletSales 按时间段汇总短剧金币解锁、点赞、收藏,最多返回 50 条。 func ListPlayletSales(ctx context.Context, req *proto.PlayletSalesReq) (*proto.PlayletSalesResp, *srvdef.SrvErr) { if req == nil { return nil, srvdef.New(srvdef.ErrParamError, "", "") } start, end, serr := resolvePlayletSalesTimeRange(req.StartTime, req.EndTime) if serr != nil { return nil, serr } resp := &proto.PlayletSalesResp{ StartTime: start.In(utils.LocBJ).Format(pubdef.TimeISO), EndTime: end.In(utils.LocBJ).Format(pubdef.TimeISO), List: make([]*proto.PlayletSalesItem, 0), } filterIDs, playlets, serr := loadPlayletSalesFilter(ctx, req) if serr != nil { return nil, serr } if filterIDs != nil && len(filterIDs) == 0 { return resp, nil } unlocks, serr := aggregatePlayletUnlocks(ctx, start, end, filterIDs) if serr != nil { return nil, serr } likes, serr := aggregatePlayletMetric(ctx, start, end, filterIDs, playletlikemod.FN.PlayletLike.CreatedAt, playletlikemod.FN.PlayletLike.PlayletId, playletlikemod.AggregatePlayletLike) if serr != nil { return nil, serr } collects, serr := aggregatePlayletMetric(ctx, start, end, filterIDs, playletcollectmod.FN.PlayletCollect.CreatedAt, playletcollectmod.FN.PlayletCollect.PlayletId, playletcollectmod.AggregatePlayletCollect) if serr != nil { return nil, serr } rows := mergePlayletSalesRows(filterIDs, unlocks, likes, collects) sortPlayletSalesRows(rows, req.SortType) rows = takePlayletSalesRows(rows, playletSalesLimit) if len(rows) == 0 { return resp, nil } if playlets == nil { playlets, serr = loadPlayletsByIDs(ctx, playletSalesIDs(rows)) if serr != nil { return nil, serr } } for _, row := range rows { item := &proto.PlayletSalesItem{ Id: row.Id, UnlockCount: row.UnlockCount, UnlockCoins: row.UnlockCoins, LikeCount: row.LikeCount, CollectCount: row.CollectCount, } if playlet := playlets[row.Id]; playlet != nil { item.Title = playlet.Title item.PlayName = playlet.PlayName item.Cover = playlet.Cover item.Slug = playlet.Slug item.Status = playlet.Status } resp.List = append(resp.List, item) } return resp, nil } func resolvePlayletSalesTimeRange(startRaw, endRaw string) (time.Time, time.Time, *srvdef.SrvErr) { startRaw = strings.TrimSpace(startRaw) endRaw = strings.TrimSpace(endRaw) if startRaw == "" && endRaw == "" { day := time.Now().In(utils.LocBJ).AddDate(0, 0, -1) return utils.BeginOfTime(day), utils.EndOfTime(day), nil } if startRaw == "" || endRaw == "" { return time.Time{}, time.Time{}, srvdef.New(srvdef.ErrParamError, "开始时间和结束时间需同时传入", "开始时间和结束时间需同时传入") } return parsePlayletSalesTimeRange(startRaw, endRaw) } func parsePlayletSalesTimeRange(startRaw, endRaw string) (time.Time, time.Time, *srvdef.SrvErr) { start, ok := parsePlayletSalesTime(startRaw, true) if !ok { return time.Time{}, time.Time{}, srvdef.New(srvdef.ErrParamError, "开始时间无效", "开始时间无效") } end, ok := parsePlayletSalesTime(endRaw, false) if !ok { return time.Time{}, time.Time{}, srvdef.New(srvdef.ErrParamError, "结束时间无效", "结束时间无效") } if end.Before(start) { return time.Time{}, time.Time{}, srvdef.New(srvdef.ErrParamError, "结束时间不能早于开始时间", "结束时间不能早于开始时间") } return start, end, nil } func parsePlayletSalesTime(raw string, isStart bool) (time.Time, bool) { raw = strings.TrimSpace(raw) if raw == "" { return time.Time{}, false } layouts := []string{ pubdef.TimeISO, time.RFC3339, time.RFC3339Nano, "2006-01-02 15:04:05 -0700 MST", "2006-01-02 15:04:05.999999999 -0700 MST", } for _, layout := range layouts { if t, err := time.ParseInLocation(layout, raw, utils.LocBJ); err == nil { return t, true } } if t, err := time.ParseInLocation(pubdef.DateISO, raw, utils.LocBJ); err == nil { if isStart { return utils.BeginOfTime(t), true } return utils.EndOfTime(t), true } return time.Time{}, false } func loadPlayletSalesFilter(ctx context.Context, req *proto.PlayletSalesReq) ([]int64, map[int64]*playletmod.Playlet, *srvdef.SrvErr) { id := int64(0) if req.Id != nil { id = *req.Id } title := strings.TrimSpace(req.Title) if id <= 0 && title == "" { return nil, nil, nil } filter := bson.M{} if id > 0 { filter[playletmod.FN.Playlet.Id] = id } if title != "" { pattern := regexp.QuoteMeta(title) filter["$or"] = []bson.M{ {playletmod.FN.Playlet.Title: bson.M{"$regex": pattern}}, {playletmod.FN.Playlet.PlayName: bson.M{"$regex": pattern}}, } } list, serr := playletmod.FindManyPlaylet(ctx, filter, options.Find().SetProjection(playletSalesProjection())) if serr != nil { return nil, nil, serr } ids := make([]int64, 0, len(list)) playlets := make(map[int64]*playletmod.Playlet, len(list)) for _, item := range list { if item == nil || item.Id <= 0 { continue } ids = append(ids, item.Id) playlets[item.Id] = item } return ids, playlets, nil } func playletSalesProjection() bson.D { return bson.D{ {Key: playletmod.FN.Playlet.Id, Value: 1}, {Key: playletmod.FN.Playlet.Title, Value: 1}, {Key: playletmod.FN.Playlet.PlayName, Value: 1}, {Key: playletmod.FN.Playlet.Cover, Value: 1}, {Key: playletmod.FN.Playlet.Slug, Value: 1}, {Key: playletmod.FN.Playlet.Status, Value: 1}, } } func loadPlayletsByIDs(ctx context.Context, ids []int64) (map[int64]*playletmod.Playlet, *srvdef.SrvErr) { out := make(map[int64]*playletmod.Playlet, len(ids)) if len(ids) == 0 { return out, nil } list, serr := playletmod.FindManyPlaylet(ctx, bson.M{ playletmod.FN.Playlet.Id: bson.M{"$in": ids}, }, options.Find().SetProjection(playletSalesProjection())) if serr != nil { return nil, serr } for _, item := range list { if item == nil || item.Id <= 0 { continue } out[item.Id] = item } return out, nil } func aggregatePlayletUnlocks(ctx context.Context, start, end time.Time, playletIDs []int64) (map[int64]playletSalesAggRow, *srvdef.SrvErr) { match := bson.M{ playletchapterunlockmod.FN.PlayletChapterUnlock.CreatedAt: bson.M{ "$gte": start, "$lte": end, }, } if len(playletIDs) > 0 { match[playletchapterunlockmod.FN.PlayletChapterUnlock.PlayletId] = bson.M{"$in": playletIDs} } pipeline := []bson.M{ {"$match": match}, {"$group": bson.M{ "_id": "$" + playletchapterunlockmod.FN.PlayletChapterUnlock.PlayletId, "count": bson.M{"$sum": 1}, "coins": bson.M{"$sum": "$" + playletchapterunlockmod.FN.PlayletChapterUnlock.Coins}, }}, } rows := make([]playletSalesAggRow, 0) if serr := playletchapterunlockmod.AggregatePlayletChapterUnlock(ctx, &rows, pipeline); serr != nil { return nil, serr } return playletSalesAggMap(rows), nil } func aggregatePlayletMetric(ctx context.Context, start, end time.Time, playletIDs []int64, createdAtField, playletIDField string, agg func(context.Context, interface{}, []bson.M) *srvdef.SrvErr) (map[int64]playletSalesAggRow, *srvdef.SrvErr) { match := bson.M{ createdAtField: bson.M{ "$gte": start, "$lte": end, }, } if len(playletIDs) > 0 { match[playletIDField] = bson.M{"$in": playletIDs} } pipeline := []bson.M{ {"$match": match}, {"$group": bson.M{ "_id": "$" + playletIDField, "count": bson.M{"$sum": 1}, }}, } rows := make([]playletSalesAggRow, 0) if serr := agg(ctx, &rows, pipeline); serr != nil { return nil, serr } return playletSalesAggMap(rows), nil } func playletSalesAggMap(rows []playletSalesAggRow) map[int64]playletSalesAggRow { out := make(map[int64]playletSalesAggRow, len(rows)) for _, row := range rows { if row.PlayletId <= 0 { continue } out[row.PlayletId] = row } return out } func mergePlayletSalesRows(filterIDs []int64, unlocks, likes, collects map[int64]playletSalesAggRow) []playletSalesRow { seen := make(map[int64]struct{}) if filterIDs != nil { for _, id := range filterIDs { if id > 0 { seen[id] = struct{}{} } } } else { for id := range unlocks { seen[id] = struct{}{} } for id := range likes { seen[id] = struct{}{} } for id := range collects { seen[id] = struct{}{} } } rows := make([]playletSalesRow, 0, len(seen)) for id := range seen { row := playletSalesRow{Id: id} if item, ok := unlocks[id]; ok { row.UnlockCount = item.Count row.UnlockCoins = item.Coins } if item, ok := likes[id]; ok { row.LikeCount = item.Count } if item, ok := collects[id]; ok { row.CollectCount = item.Count } rows = append(rows, row) } return rows } func sortPlayletSalesRows(rows []playletSalesRow, sortType proto.PlayletSalesSortType) { sort.SliceStable(rows, func(i, j int) bool { left, right := rows[i], rows[j] switch sortType { case proto.PlayletSalesSortLike: if left.LikeCount != right.LikeCount { return left.LikeCount > right.LikeCount } case proto.PlayletSalesSortCollect: if left.CollectCount != right.CollectCount { return left.CollectCount > right.CollectCount } default: if left.UnlockCount != right.UnlockCount { return left.UnlockCount > right.UnlockCount } if left.UnlockCoins != right.UnlockCoins { return left.UnlockCoins > right.UnlockCoins } } if left.UnlockCount != right.UnlockCount { return left.UnlockCount > right.UnlockCount } if left.LikeCount != right.LikeCount { return left.LikeCount > right.LikeCount } if left.CollectCount != right.CollectCount { return left.CollectCount > right.CollectCount } return left.Id > right.Id }) } func takePlayletSalesRows(rows []playletSalesRow, limit int) []playletSalesRow { if limit <= 0 || len(rows) <= limit { return rows } return rows[:limit] } func playletSalesIDs(rows []playletSalesRow) []int64 { ids := make([]int64, 0, len(rows)) for _, row := range rows { if row.Id > 0 { ids = append(ids, row.Id) } } return ids }