package business import ( "context" "math/big" "sort" "strings" "time" "github.com/go-kratos/kratos/v3/errors" ) var ( ErrServerAnalysisGame = errors.BadRequest("SW_SERVER_ANALYSIS_GAME", "请选择游戏") ErrServerOptionsUnavailable = errors.BadRequest("SW_SERVER_OPTIONS_UNAVAILABLE", "游戏或区服数据尚未接入") ) type ServerAnalysisKey struct { SourceDB string GameID int64 ServerNum string } type ServerAnalysisFilter struct { AnalyticsFilter ServerNum string } type ServerAnalysisRow struct { ServerAnalysisKey GameName, ServerName string OpenTime *int64 Counts PortraitCounts Pay, ARPU string } type ServerAnalysisData struct { Rows []*ServerAnalysisRow Available bool } type ServerAnalysisPage struct { ServerAnalysisData Total int64 Summary *ServerAnalysisRow Begin, End string } type ServerOption struct { Number, Name string OpenTime *int64 } type ServerAnalysisRepo interface { Aggregate(context.Context, AnalyticsScope, ServerAnalysisFilter) (*ServerAnalysisData, error) Servers(context.Context, UserScope, []string, []string, string, int) ([]*ServerOption, error) } type ServerAnalysisUsecase struct { repo ServerAnalysisRepo accounts *AccountUsecase preferences *UserPreferenceUsecase now func() time.Time } func NewServerAnalysisUsecase(repo ServerAnalysisRepo, accounts *AccountUsecase, preferences *UserPreferenceUsecase) *ServerAnalysisUsecase { return &ServerAnalysisUsecase{repo: repo, accounts: accounts, preferences: preferences, now: time.Now} } func (uc *ServerAnalysisUsecase) Servers(ctx context.Context, sources, games []string, keyword string, limit int) ([]*ServerOption, error) { actor, account, err := uc.accounts.actor(ctx) if err != nil { return nil, err } viewer := portraitUserScope(account) viewer.All = actor.AuthorityID == 888 if len(games) == 0 { return nil, ErrServerAnalysisGame } if limit <= 0 { limit = 50 } if limit > 100 { limit = 100000 } return uc.repo.Servers(ctx, viewer, sources, games, strings.TrimSpace(keyword), limit) } func (uc *ServerAnalysisUsecase) List(ctx context.Context, f ServerAnalysisFilter, page, size int) (*ServerAnalysisPage, error) { actor, account, err := uc.accounts.actor(ctx) if err != nil { return nil, err } if len(f.GameNames) == 0 { return nil, ErrServerAnalysisGame } pref, err := uc.preferences.Current(ctx) if err != nil { return nil, err } f.StartDate, f.EndDate = reportDateDefaults(f.StartDate, f.EndDate, 30, uc.now()) if f.EndDate.Before(*f.StartDate) { return nil, ErrAnalyticsDateOrder } f.Begin, f.End = f.StartDate.Format("2006-01-02"), f.EndDate.Format("2006-01-02") f.ServerNum = strings.TrimSpace(f.ServerNum) viewer := portraitUserScope(account) viewer.All = actor.AuthorityID == 888 data, err := uc.repo.Aggregate(ctx, AnalyticsScope{Viewer: viewer, IncludeSub: pref.IncludeSubPromote, IDCardDedup: pref.IDCardDedup}, f) if err != nil { return nil, err } out := &ServerAnalysisPage{ServerAnalysisData: ServerAnalysisData{Rows: []*ServerAnalysisRow{}, Available: data.Available}, Begin: f.Begin, End: f.End} if !data.Available { return out, nil } rows := make([]*ServerAnalysisRow, 0, len(data.Rows)) summary := &ServerAnalysisRow{ServerName: "合计"} sumPay := new(big.Rat) for _, row := range data.Rows { if strings.TrimSpace(row.ServerNum) == "" { continue } row.Counts = analyticsCap(row.Counts) pay, err := portraitNumber(row.Pay) if err != nil { return nil, err } row.Pay = portraitDecimal(pay, 2) pay, _ = portraitNumber(row.Pay) // 未指定区服时忽略全零行;指定区服仍只返回报表中已有的分组。 if f.ServerNum == "" && row.Counts.Register == 0 && row.Counts.Active == 0 && row.Counts.Pay == 0 && pay.Sign() == 0 { continue } row.ARPU = analyticsRatio(pay, row.Counts.Active, false) summary.Counts = analyticsAdd(summary.Counts, row.Counts) sumPay.Add(sumPay, pay) rows = append(rows, row) } summary.Pay = portraitDecimal(sumPay, 2) summary.ARPU = analyticsRatio(sumPay, summary.Counts.Active, false) out.Summary, out.Total = summary, int64(len(rows)) if err := sortServerAnalysis(rows, f.SortField, f.Ascending); err != nil { return nil, err } if page < 1 { page = 1 } if size <= 0 || size > 500 { size = 500 } if page > len(rows)/size+1 { return out, nil } from := (page - 1) * size if from < len(rows) { out.Rows = rows[from:min(from+size, len(rows))] } return out, nil } func sortServerAnalysis(rows []*ServerAnalysisRow, field string, ascending bool) error { if field == "" { sort.SliceStable(rows, func(i, j int) bool { a, b := rows[i], rows[j] if a.SourceDB != b.SourceDB { return a.SourceDB < b.SourceDB } if a.GameID != b.GameID { return a.GameID < b.GameID } return a.ServerNum < b.ServerNum }) return nil } values := map[*ServerAnalysisRow]*big.Rat{} for _, row := range rows { var n *big.Rat var err error switch field { case "openTime": v := int64(0) if row.OpenTime != nil { v = *row.OpenTime } n = big.NewRat(v, 1) case "newRegisterCount": n = big.NewRat(row.Counts.Register, 1) case "activeCount": n = big.NewRat(row.Counts.Active, 1) case "payUserCount": n = big.NewRat(row.Counts.Pay, 1) case "arpu": n, err = portraitNumber(row.ARPU) default: n, err = portraitNumber(row.Pay) } if err != nil { return err } values[row] = n } sort.SliceStable(rows, func(i, j int) bool { a, b := rows[i], rows[j] if c := values[a].Cmp(values[b]); c != 0 { if ascending { return c < 0 } return c > 0 } if a.ServerNum != b.ServerNum { return strings.TrimSpace(a.ServerNum) < strings.TrimSpace(b.ServerNum) } if a.SourceDB != b.SourceDB { return a.SourceDB < b.SourceDB } return a.GameID < b.GameID }) return nil }