Files
web_huangguo2_server/web/service/srv_playlet/sales.go
T
2026-09-29 10:47:38 +08:00

372 lines
10 KiB
Go

package srv_playlet
import (
"context"
"regexp"
"sort"
"strings"
"time"
"videoweb/pub/common/srvdef"
"videoweb/pub/common/utils"
"videoweb/pub/models/playletchapterunlockmod"
"videoweb/pub/models/playletcollectmod"
"videoweb/pub/models/playletlikemod"
"videoweb/pub/models/playletmod"
"videoweb/pub/pubdef"
"videoweb/web/api/proto"
"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
}