feat: 迁移区服分析与游戏区服查询

This commit is contained in:
Yvan
2026-11-04 01:16:23 +08:00
parent fc27120950
commit 0bef29ae4f
27 changed files with 1113 additions and 6 deletions
+1 -1
View File
@@ -11,4 +11,4 @@ import (
)
// ProviderSet is biz providers.
var ProviderSet = wire.NewSet(system.ProviderSet, payment.ProviderSet, integration.ProviderSet, task.ProviderSet, business.NewUserPreferenceUsecase, business.NewAccountUsecase, business.NewBindingUsecase, business.NewPasswordResetUsecase, business.NewBusinessPromoteUsecase, business.NewAlertRuleUsecase, business.NewAlertRecordUsecase, business.NewActivationUsecase, business.NewChannelUsecase, business.NewBusinessPortraitUsecase, business.NewChannelPortraitUsecase, business.NewLedgerUsecase, business.NewAnalyticsUsecase)
var ProviderSet = wire.NewSet(system.ProviderSet, payment.ProviderSet, integration.ProviderSet, task.ProviderSet, business.NewUserPreferenceUsecase, business.NewAccountUsecase, business.NewBindingUsecase, business.NewPasswordResetUsecase, business.NewBusinessPromoteUsecase, business.NewAlertRuleUsecase, business.NewAlertRecordUsecase, business.NewActivationUsecase, business.NewChannelUsecase, business.NewBusinessPortraitUsecase, business.NewChannelPortraitUsecase, business.NewLedgerUsecase, business.NewAnalyticsUsecase, business.NewServerAnalysisUsecase)
+198
View File
@@ -0,0 +1,198 @@
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, []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) {
if _, _, err := uc.accounts.actor(ctx); err != nil {
return nil, err
}
if len(games) == 0 {
return nil, ErrServerAnalysisGame
}
if limit <= 0 {
limit = 50
}
if limit > 100 {
limit = 100000
}
return uc.repo.Servers(ctx, 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
}
if f.StartDate == nil || f.EndDate == nil {
now := uc.now().In(portraitLocation)
day := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, portraitLocation)
f.StartDate, f.EndDate = &day, &day
}
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 {
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
}
@@ -0,0 +1,69 @@
package business
import (
"context"
"testing"
"time"
)
type serverAnalysisFake struct {
data *ServerAnalysisData
scope AnalyticsScope
filter ServerAnalysisFilter
limit int
}
func (r *serverAnalysisFake) Aggregate(_ context.Context, s AnalyticsScope, f ServerAnalysisFilter) (*ServerAnalysisData, error) {
r.scope, r.filter = s, f
out := &ServerAnalysisData{Available: r.data.Available}
for _, row := range r.data.Rows {
c := *row
out.Rows = append(out.Rows, &c)
}
return out, nil
}
func (r *serverAnalysisFake) Servers(_ context.Context, _, _ []string, _ string, limit int) ([]*ServerOption, error) {
r.limit = limit
return []*ServerOption{}, nil
}
func TestServerAnalysisSummaryZerosSortingAndLimits(t *testing.T) {
r := &serverAnalysisFake{data: &ServerAnalysisData{Available: true, Rows: []*ServerAnalysisRow{
{ServerAnalysisKey: ServerAnalysisKey{SourceDB: "lx", GameID: 1, ServerNum: "02"}, Counts: PortraitCounts{Register: 2, Active: 3, Pay: 1}, Pay: "10.005"},
{ServerAnalysisKey: ServerAnalysisKey{SourceDB: "hw", GameID: 1, ServerNum: "01"}, Counts: PortraitCounts{Register: 3, Active: 2, Pay: 1}, Pay: "20"},
{ServerAnalysisKey: ServerAnalysisKey{SourceDB: "lx", GameID: 1, ServerNum: "zero"}, Pay: "0"},
}}}
a := &accountRepoFake{accounts: map[uint]*Account{7: {ID: 7, Level: 1, IDPath: "0"}}}
uc := NewServerAnalysisUsecase(r, NewAccountUsecase(a), NewUserPreferenceUsecase(&channelPreferenceFake{UserPreference{IncludeSubPromote: true, IDCardDedup: true}}))
uc.now = func() time.Time { return time.Date(2026, 11, 4, 9, 0, 0, 0, portraitLocation) }
f := ServerAnalysisFilter{AnalyticsFilter: AnalyticsFilter{GameNames: []string{"游戏"}}}
p, err := uc.List(accountContext(7, 901), f, 1, 1)
if err != nil || p.Total != 2 || len(p.Rows) != 1 || p.Rows[0].ServerNum != "01" || p.Summary.Pay != "30.01" || p.Summary.Counts.Register != 5 || p.Summary.ARPU != "6.00" || p.Begin != "2026-11-04" {
t.Fatalf("汇总、四舍五入或分页错误: %+v %v", p, err)
}
if !r.scope.IncludeSub || !r.scope.IDCardDedup || r.scope.Viewer.All {
t.Fatal("查看者偏好或范围未传递")
}
p, err = uc.List(accountContext(7, 901), f, 999999999, 10)
if err != nil || len(p.Rows) != 0 || p.Summary.Pay != "30.01" {
t.Fatal("越界分页不应丢失汇总")
}
r.data.Rows = r.data.Rows[2:]
f.ServerNum = "zero"
p, err = uc.List(accountContext(7, 901), f, 1, 0)
if err != nil || p.Total != 1 {
t.Fatal("指定区服时应保留已有全零行")
}
r.data.Available = false
p, err = uc.List(accountContext(7, 901), f, 1, 10)
if err != nil || p.Summary != nil || p.Available {
t.Fatal("未接入不能返回零汇总")
}
if _, err := uc.List(accountContext(7, 901), ServerAnalysisFilter{}, 1, 10); err != ErrServerAnalysisGame {
t.Fatal("区服查询必须选择游戏")
}
for _, sample := range []struct{ in, want int }{{0, 50}, {100, 100}, {101, 100000}} {
if _, err := uc.Servers(accountContext(7, 901), nil, []string{"游戏"}, "", sample.in); err != nil || r.limit != sample.want {
t.Fatal("区服选项上限与旧行为不符")
}
}
}