perf: 减少统计页面资料读取并优化区服选项查询
This commit is contained in:
@@ -17,12 +17,13 @@
|
||||
- 普通商务渠道 summary 累加可见行。超管渠道 summary 使用筛选范围内全局去重;模式 0 按平台前缀区分身份,模式 1 跨平台去重。
|
||||
- 付费率=充值人数÷活跃人数×100,ARPU=充值金额÷活跃人数,ARPPU=充值金额÷充值人数。分母为零时为零。
|
||||
- 保留原逐行人数的 Java int 上限,游戏汇总改用 int64 避免相加溢出。相同排序值使用平台、ID 稳定排序。
|
||||
- 后端任一日期缺失时周期为上海当天;前端默认近 30 天含今天。后端页长缺失、非正或超过上限时为 500,页面显式传 10。
|
||||
- 后端不传日期时周期为上海当天,只传一端会拒绝;前端默认近 30 天含今天。后端页长缺失、非正或超过上限时为 500,页面显式传 10。
|
||||
- 缺少必要数据表时 `available=false`、`summary=null`,顶部指标为 null,不把缺表当成零。
|
||||
|
||||
## 验证与优化
|
||||
|
||||
- 使用聚合查询和批量资料查询代替逐行查询,不改变业务口径。日期非法或倒置返回明确错误。
|
||||
- 数字指标先按全部统计行计算汇总、排序和分页,再补当前页的渠道/游戏名称及商务资料;按渠道名称或渠道类型排序时仍先补全体资料。空页不读取资料,官方及未知渠道标签保持原规则。
|
||||
- 隔离 TCP HTTP 和仓储测试覆盖全量汇总与分页、跨平台同号、官方/孤立渠道、根渠道树去重、类型交集、历史游戏名过滤、当前资料展示、偏好切换及空范围。
|
||||
- 领域测试覆盖大金额精确排序、分母和人数上限。前端检查筛选回第一页、30 天默认值、汇总不随分页变化、缺数据提示、没有额外导出入口。
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
- `/sw/serverAnalysis/games` 复用已迁移游戏选项;新增 `/servers`、`/list`。两者均要求游戏名称;来源支持逗号分隔或重复参数。
|
||||
- 选项通过当前 `tab_game.game_name` 确定平台+游戏,匹配区服名称;保留区服编号的字符串及前导零。原接口不按商务绑定裁剪资料选项,统计查询独立按可见范围隔离。
|
||||
- 选项默认 50,超过 100 时允许 100000,沿用旧批量选项请求。使用 EXISTS 代替为每个游戏生成 OR,保持成对关联并避免重复行。
|
||||
- 选项默认 50,超过 100 时允许 100000,沿用旧批量选项请求。使用 `(source_db,game_id) IN (SELECT source_db,id FROM tab_game …)` 保持成对关联并避免重复行;MySQL 5.7 可先按游戏名索引定位,再使用区服的 game_id 索引,避免相关 EXISTS 扫描全部区服。
|
||||
- 统计读取 `tab_sw_report_server_day` 和身份键表,按平台+游戏+区服编号分组;空白编号不展示。不额外加日游戏报表的 `game_id > 0` 限制。
|
||||
- 人数在每个区服内 COUNT DISTINCT,金额固定模式 0;筛选历史报表游戏名。展示优先金额汇总的历史游戏名,无金额行时回退当前游戏名,保留旧行为。
|
||||
- 区服名称/开服日期按三元组批量补充;重复资料固定取最小 ID,提高原不确定顺序的稳定性。相同主排序后保留区服编号顺序,再按平台、游戏 ID 稳定排列。
|
||||
|
||||
@@ -69,6 +69,7 @@ type AnalyticsPage struct {
|
||||
}
|
||||
type AnalyticsRepo interface {
|
||||
Aggregate(context.Context, AnalyticsKind, AnalyticsScope, AnalyticsFilter) (*AnalyticsData, error)
|
||||
FillNames(context.Context, AnalyticsKind, AnalyticsScope, AnalyticsFilter, []*AnalyticsRow) error
|
||||
}
|
||||
type AnalyticsUsecase struct {
|
||||
repo AnalyticsRepo
|
||||
@@ -138,7 +139,8 @@ func (uc *AnalyticsUsecase) List(ctx context.Context, kind AnalyticsKind, filter
|
||||
filter.Begin = filter.StartDate.Format("2006-01-02")
|
||||
filter.End = filter.EndDate.Format("2006-01-02")
|
||||
out := &AnalyticsPage{Rows: []*AnalyticsRow{}, Begin: filter.Begin, End: filter.End, Available: true}
|
||||
data, err := uc.repo.Aggregate(ctx, kind, AnalyticsScope{Viewer: viewer, IncludeSub: pref.IncludeSubPromote, IDCardDedup: pref.IDCardDedup}, filter)
|
||||
scope := AnalyticsScope{Viewer: viewer, IncludeSub: pref.IncludeSubPromote, IDCardDedup: pref.IDCardDedup}
|
||||
data, err := uc.repo.Aggregate(ctx, kind, scope, filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -165,9 +167,6 @@ func (uc *AnalyticsUsecase) List(ctx context.Context, kind AnalyticsKind, filter
|
||||
pay.Add(pay, rounded)
|
||||
}
|
||||
counts = analyticsAdd(counts, row.Counts)
|
||||
if kind == ChannelAnalytics {
|
||||
row.Name = (&LedgerNames{PromoteID: &row.ID, PromoteAccount: row.Name}).DisplayPromote()
|
||||
}
|
||||
}
|
||||
if kind == ChannelAnalytics {
|
||||
counts = analyticsCap(counts)
|
||||
@@ -187,6 +186,28 @@ func (uc *AnalyticsUsecase) List(ctx context.Context, kind AnalyticsKind, filter
|
||||
if err := analyticsValues(out.Summary); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 仅按渠道名称或类型排序时需要提前读取全部资料;数字指标排序
|
||||
// 确定当前页后再补名称和商务信息,汇总仍使用全部可见统计行。
|
||||
namesBeforeSort := kind == ChannelAnalytics && (filter.SortField == "promoteName" || filter.SortField == "promoteType")
|
||||
fillNames := func(rows []*AnalyticsRow) error {
|
||||
if len(rows) == 0 {
|
||||
return nil
|
||||
}
|
||||
if err := uc.repo.FillNames(ctx, kind, scope, filter, rows); err != nil {
|
||||
return err
|
||||
}
|
||||
if kind == ChannelAnalytics {
|
||||
for _, row := range rows {
|
||||
row.Name = (&LedgerNames{PromoteID: &row.ID, PromoteAccount: row.Name}).DisplayPromote()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if namesBeforeSort {
|
||||
if err := fillNames(data.Rows); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err := sortAnalytics(data.Rows, kind, filter.SortField, filter.Ascending); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -205,6 +226,11 @@ func (uc *AnalyticsUsecase) List(ctx context.Context, kind AnalyticsKind, filter
|
||||
if from < len(data.Rows) {
|
||||
out.Rows = data.Rows[from:to]
|
||||
}
|
||||
if !namesBeforeSort {
|
||||
if err := fillNames(out.Rows); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
func sortAnalytics(rows []*AnalyticsRow, kind AnalyticsKind, field string, ascending bool) error {
|
||||
|
||||
@@ -7,9 +7,23 @@ import (
|
||||
)
|
||||
|
||||
type analyticsRepoFake struct {
|
||||
data *AnalyticsData
|
||||
scope AnalyticsScope
|
||||
filter AnalyticsFilter
|
||||
data *AnalyticsData
|
||||
scope AnalyticsScope
|
||||
filter AnalyticsFilter
|
||||
fillCalls [][]AnalyticsKey
|
||||
fill func([]*AnalyticsRow) error
|
||||
}
|
||||
|
||||
func (r *analyticsRepoFake) FillNames(_ context.Context, _ AnalyticsKind, _ AnalyticsScope, _ AnalyticsFilter, rows []*AnalyticsRow) error {
|
||||
keys := make([]AnalyticsKey, len(rows))
|
||||
for i, row := range rows {
|
||||
keys[i] = row.AnalyticsKey
|
||||
}
|
||||
r.fillCalls = append(r.fillCalls, keys)
|
||||
if r.fill != nil {
|
||||
return r.fill(rows)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *analyticsRepoFake) Aggregate(_ context.Context, _ AnalyticsKind, s AnalyticsScope, f AnalyticsFilter) (*AnalyticsData, error) {
|
||||
@@ -76,3 +90,75 @@ func TestAnalyticsSortAndMissingData(t *testing.T) {
|
||||
t.Fatal("倒置日期应被拒绝")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnalyticsLoadsMetadataOnlyForSelectedPage(t *testing.T) {
|
||||
for _, kind := range []AnalyticsKind{ChannelAnalytics, GameAnalytics} {
|
||||
t.Run(string(kind), func(t *testing.T) {
|
||||
uc, repo := analyticsFixture()
|
||||
for _, row := range repo.data.Rows {
|
||||
row.Name = ""
|
||||
}
|
||||
repo.fill = func(rows []*AnalyticsRow) error {
|
||||
for _, row := range rows {
|
||||
row.Name = "本页资料"
|
||||
row.BindingAccount = "本页商务"
|
||||
}
|
||||
return nil
|
||||
}
|
||||
page, err := uc.List(accountContext(7, 901), kind, AnalyticsFilter{SortField: "totalPay"}, 1, 1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(repo.fillCalls) != 1 || len(repo.fillCalls[0]) != 1 || repo.fillCalls[0][0].ID != 2 {
|
||||
t.Fatalf("应只读取排序后当前页的资料: %+v", repo.fillCalls)
|
||||
}
|
||||
if page.Total != 2 || page.Summary.Pay != "50.00" || page.Rows[0].Name != "本页资料" || page.Rows[0].BindingAccount != "本页商务" {
|
||||
t.Fatalf("延迟读取影响汇总或展示资料: %+v", page)
|
||||
}
|
||||
repo.fillCalls = nil
|
||||
page, err = uc.List(accountContext(7, 901), kind, AnalyticsFilter{}, 3, 1)
|
||||
if err != nil || len(page.Rows) != 0 || len(repo.fillCalls) != 0 || page.Summary.Pay != "50.00" {
|
||||
t.Fatal("空页不应读取资料,汇总仍须保留")
|
||||
}
|
||||
repo.fill = func([]*AnalyticsRow) error { return ErrStorage }
|
||||
if _, err := uc.List(accountContext(7, 901), kind, AnalyticsFilter{}, 1, 1); err != ErrStorage {
|
||||
t.Fatalf("资料读取错误必须向上传递: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnalyticsLoadsMetadataBeforeNameOrTypeSort(t *testing.T) {
|
||||
for _, field := range []string{"promoteName", "promoteType"} {
|
||||
t.Run(field, func(t *testing.T) {
|
||||
uc, repo := analyticsFixture()
|
||||
for _, row := range repo.data.Rows {
|
||||
row.Name = ""
|
||||
}
|
||||
repo.fill = func(rows []*AnalyticsRow) error {
|
||||
for _, row := range rows {
|
||||
name, kind := "Zulu", 9
|
||||
if row.ID == 2 {
|
||||
name, kind = "Alpha", 1
|
||||
}
|
||||
row.Name, row.PromoteType = name, &kind
|
||||
}
|
||||
return nil
|
||||
}
|
||||
page, err := uc.List(accountContext(7, 901), ChannelAnalytics, AnalyticsFilter{SortField: field, Ascending: true}, 1, 1)
|
||||
if err != nil || len(page.Rows) != 1 || page.Rows[0].ID != 2 || len(repo.fillCalls) != 1 || len(repo.fillCalls[0]) != 2 {
|
||||
t.Fatalf("按资料字段排序前必须读取全体资料: page=%+v calls=%+v err=%v", page, repo.fillCalls, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnalyticsDeferredNamesPreserveOfficialAndUnknownLabels(t *testing.T) {
|
||||
uc, repo := analyticsFixture()
|
||||
repo.data.Rows[0].ID, repo.data.Rows[0].Name = 0, ""
|
||||
repo.data.Rows[1].Name = ""
|
||||
page, err := uc.List(accountContext(1, 888), ChannelAnalytics, AnalyticsFilter{SortField: "promoteId", Ascending: true}, 1, 10)
|
||||
if err != nil || page.Rows[0].Name != "官方渠道" || page.Rows[1].Name != "未知渠道(ID:2)" {
|
||||
t.Fatalf("默认渠道名丢失: %+v %v", page, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,9 +213,6 @@ func (r *analyticsRepo) Aggregate(ctx context.Context, kind business.AnalyticsKi
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
if err := fillAnalyticsNames(db, kind, scope, f, rowMap); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, row := range rowMap {
|
||||
result.Rows = append(result.Rows, row)
|
||||
}
|
||||
@@ -228,6 +225,18 @@ func (r *analyticsRepo) Aggregate(ctx context.Context, kind business.AnalyticsKi
|
||||
})
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *analyticsRepo) FillNames(ctx context.Context, kind business.AnalyticsKind, scope business.AnalyticsScope, f business.AnalyticsFilter, rows []*business.AnalyticsRow) error {
|
||||
if len(rows) == 0 {
|
||||
return nil
|
||||
}
|
||||
byKey := make(map[business.AnalyticsKey]*business.AnalyticsRow, len(rows))
|
||||
for _, row := range rows {
|
||||
byKey[row.AnalyticsKey] = row
|
||||
}
|
||||
return fillAnalyticsNames(r.data.DB().WithContext(ctx), kind, scope, f, byKey)
|
||||
}
|
||||
|
||||
func analyticsPairs(keys []business.AnalyticsKey) [][]any {
|
||||
result := make([][]any, 0, len(keys))
|
||||
for _, key := range keys {
|
||||
@@ -269,6 +278,12 @@ func fillAnalyticsNames(db *gorm.DB, kind business.AnalyticsKind, scope business
|
||||
for key := range rows {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Slice(keys, func(i, j int) bool {
|
||||
if keys[i].ID != keys[j].ID {
|
||||
return keys[i].ID < keys[j].ID
|
||||
}
|
||||
return keys[i].SourceDB < keys[j].SourceDB
|
||||
})
|
||||
type entityPO struct {
|
||||
ID int64
|
||||
SourceDB string `gorm:"column:source_db"`
|
||||
|
||||
@@ -34,6 +34,9 @@ func TestAnalyticsChannelTreeAndAdminOfficialRows(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repo.FillNames(context.Background(), business.ChannelAnalytics, scope, filter, data.Rows); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(data.Rows) != 3 || data.GlobalCounts != nil {
|
||||
t.Fatalf("普通商务统计范围错误: %+v", data)
|
||||
}
|
||||
@@ -69,6 +72,12 @@ func TestAnalyticsFiltersKeepHierarchyAndSnapshotNames(t *testing.T) {
|
||||
kind := 1
|
||||
filter.PromoteType = &kind
|
||||
data, err = repo.Aggregate(context.Background(), business.ChannelAnalytics, scope, filter)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repo.FillNames(context.Background(), business.ChannelAnalytics, scope, filter, data.Rows); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err != nil || len(data.Rows) != 1 || data.Rows[0].ID != 100 || portraitTestMoney(data.Rows[0].Pay) != "20.20" || data.Rows[0].PromoteType == nil || *data.Rows[0].PromoteType != 0 {
|
||||
t.Fatalf("类型筛选应限制参与渠道,同时显示归根后的当前资料: %+v %v", data, err)
|
||||
}
|
||||
@@ -78,6 +87,9 @@ func TestAnalyticsFiltersKeepHierarchyAndSnapshotNames(t *testing.T) {
|
||||
if err != nil || len(data.Rows) != 2 {
|
||||
t.Fatalf("按游戏快照名筛选错误: %+v %v", data, err)
|
||||
}
|
||||
if err := repo.FillNames(context.Background(), business.GameAnalytics, scope, filter, data.Rows); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, row := range data.Rows {
|
||||
if row.SourceDB == "lx" && (row.ID != 1 || row.Name != "游戏甲" || row.Counts.Register != 1) {
|
||||
t.Fatalf("应按报表快照筛选、用当前游戏名展示: %+v", row)
|
||||
|
||||
@@ -155,7 +155,11 @@ func (r *serverAnalysisRepo) Servers(ctx context.Context, sources, games []strin
|
||||
return nil, business.ErrServerOptionsUnavailable
|
||||
}
|
||||
}
|
||||
q := db.Table("tab_game_server s").Where("EXISTS (?)", db.Table("tab_game g").Select("1").Where("g.source_db=s.source_db AND g.id=s.game_id AND g.game_name IN ?", games))
|
||||
// MySQL 5.7 不会将相关 EXISTS 展开为半连接,导致扫描全部区服。
|
||||
// 多列 IN 让优化器先按游戏名索引定位,再使用区服的 game_id 索引,
|
||||
// 同时保留平台匹配和重复游戏资料不扩增结果的语义。
|
||||
gamesQuery := db.Table("tab_game g").Select("g.source_db,g.id").Where("g.game_name IN ?", games)
|
||||
q := db.Table("tab_game_server s").Where("(s.source_db,s.game_id) IN (?)", gamesQuery)
|
||||
if len(sources) > 0 {
|
||||
q = q.Where("s.source_db IN ?", sources)
|
||||
}
|
||||
|
||||
@@ -85,3 +85,30 @@ func TestServerAnalysisStorageScopeSnapshotAndPreference(t *testing.T) {
|
||||
t.Fatal("缺身份表应明确未接入")
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerOptionsPreservePlatformFilterAndDoNotDuplicateMatches(t *testing.T) {
|
||||
db := openTestDB(t)
|
||||
for _, sql := range []string{
|
||||
"CREATE TABLE tab_game(id INTEGER,source_db TEXT,game_name TEXT)",
|
||||
"INSERT INTO tab_game VALUES(1,'lx','甲'),(1,'lx','甲'),(1,'hw','乙'),(2,'lx','乙')",
|
||||
"CREATE TABLE tab_game_server(id INTEGER,source_db TEXT,game_id INTEGER,server_num TEXT,server_name TEXT,start_time INTEGER)",
|
||||
"INSERT INTO tab_game_server VALUES(1,'lx',1,'001','一区',100),(2,'lx',1,'002','二区',200),(3,'hw',1,'001','其他平台',300),(4,'lx',2,'001','其他游戏',400)",
|
||||
} {
|
||||
if err := db.Exec(sql).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
repo := NewServerAnalysisRepo(testData{db})
|
||||
rows, err := repo.Servers(context.Background(), nil, []string{"甲"}, "", 100)
|
||||
if err != nil || len(rows) != 2 || rows[0].Name != "一区" || rows[1].Number != "002" {
|
||||
t.Fatalf("相同游戏资料不能扩增区服,异平台同号不能匹配: %+v %v", rows, err)
|
||||
}
|
||||
rows, err = repo.Servers(context.Background(), []string{"lx"}, []string{"甲", "乙"}, "区", 1)
|
||||
if err != nil || len(rows) != 1 || rows[0].Name != "一区" {
|
||||
t.Fatalf("名称筛选、排序或数量限制改变: %+v %v", rows, err)
|
||||
}
|
||||
rows, err = repo.Servers(context.Background(), []string{"hw"}, []string{"甲"}, "", 100)
|
||||
if err != nil || len(rows) != 0 {
|
||||
t.Fatalf("平台筛选被忽略: %+v %v", rows, err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user