perf: 减少统计页面资料读取并优化区服选项查询
This commit is contained in:
@@ -17,12 +17,13 @@
|
|||||||
- 普通商务渠道 summary 累加可见行。超管渠道 summary 使用筛选范围内全局去重;模式 0 按平台前缀区分身份,模式 1 跨平台去重。
|
- 普通商务渠道 summary 累加可见行。超管渠道 summary 使用筛选范围内全局去重;模式 0 按平台前缀区分身份,模式 1 跨平台去重。
|
||||||
- 付费率=充值人数÷活跃人数×100,ARPU=充值金额÷活跃人数,ARPPU=充值金额÷充值人数。分母为零时为零。
|
- 付费率=充值人数÷活跃人数×100,ARPU=充值金额÷活跃人数,ARPPU=充值金额÷充值人数。分母为零时为零。
|
||||||
- 保留原逐行人数的 Java int 上限,游戏汇总改用 int64 避免相加溢出。相同排序值使用平台、ID 稳定排序。
|
- 保留原逐行人数的 Java int 上限,游戏汇总改用 int64 避免相加溢出。相同排序值使用平台、ID 稳定排序。
|
||||||
- 后端任一日期缺失时周期为上海当天;前端默认近 30 天含今天。后端页长缺失、非正或超过上限时为 500,页面显式传 10。
|
- 后端不传日期时周期为上海当天,只传一端会拒绝;前端默认近 30 天含今天。后端页长缺失、非正或超过上限时为 500,页面显式传 10。
|
||||||
- 缺少必要数据表时 `available=false`、`summary=null`,顶部指标为 null,不把缺表当成零。
|
- 缺少必要数据表时 `available=false`、`summary=null`,顶部指标为 null,不把缺表当成零。
|
||||||
|
|
||||||
## 验证与优化
|
## 验证与优化
|
||||||
|
|
||||||
- 使用聚合查询和批量资料查询代替逐行查询,不改变业务口径。日期非法或倒置返回明确错误。
|
- 使用聚合查询和批量资料查询代替逐行查询,不改变业务口径。日期非法或倒置返回明确错误。
|
||||||
|
- 数字指标先按全部统计行计算汇总、排序和分页,再补当前页的渠道/游戏名称及商务资料;按渠道名称或渠道类型排序时仍先补全体资料。空页不读取资料,官方及未知渠道标签保持原规则。
|
||||||
- 隔离 TCP HTTP 和仓储测试覆盖全量汇总与分页、跨平台同号、官方/孤立渠道、根渠道树去重、类型交集、历史游戏名过滤、当前资料展示、偏好切换及空范围。
|
- 隔离 TCP HTTP 和仓储测试覆盖全量汇总与分页、跨平台同号、官方/孤立渠道、根渠道树去重、类型交集、历史游戏名过滤、当前资料展示、偏好切换及空范围。
|
||||||
- 领域测试覆盖大金额精确排序、分母和人数上限。前端检查筛选回第一页、30 天默认值、汇总不随分页变化、缺数据提示、没有额外导出入口。
|
- 领域测试覆盖大金额精确排序、分母和人数上限。前端检查筛选回第一页、30 天默认值、汇总不随分页变化、缺数据提示、没有额外导出入口。
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
- `/sw/serverAnalysis/games` 复用已迁移游戏选项;新增 `/servers`、`/list`。两者均要求游戏名称;来源支持逗号分隔或重复参数。
|
- `/sw/serverAnalysis/games` 复用已迁移游戏选项;新增 `/servers`、`/list`。两者均要求游戏名称;来源支持逗号分隔或重复参数。
|
||||||
- 选项通过当前 `tab_game.game_name` 确定平台+游戏,匹配区服名称;保留区服编号的字符串及前导零。原接口不按商务绑定裁剪资料选项,统计查询独立按可见范围隔离。
|
- 选项通过当前 `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` 限制。
|
- 统计读取 `tab_sw_report_server_day` 和身份键表,按平台+游戏+区服编号分组;空白编号不展示。不额外加日游戏报表的 `game_id > 0` 限制。
|
||||||
- 人数在每个区服内 COUNT DISTINCT,金额固定模式 0;筛选历史报表游戏名。展示优先金额汇总的历史游戏名,无金额行时回退当前游戏名,保留旧行为。
|
- 人数在每个区服内 COUNT DISTINCT,金额固定模式 0;筛选历史报表游戏名。展示优先金额汇总的历史游戏名,无金额行时回退当前游戏名,保留旧行为。
|
||||||
- 区服名称/开服日期按三元组批量补充;重复资料固定取最小 ID,提高原不确定顺序的稳定性。相同主排序后保留区服编号顺序,再按平台、游戏 ID 稳定排列。
|
- 区服名称/开服日期按三元组批量补充;重复资料固定取最小 ID,提高原不确定顺序的稳定性。相同主排序后保留区服编号顺序,再按平台、游戏 ID 稳定排列。
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ type AnalyticsPage struct {
|
|||||||
}
|
}
|
||||||
type AnalyticsRepo interface {
|
type AnalyticsRepo interface {
|
||||||
Aggregate(context.Context, AnalyticsKind, AnalyticsScope, AnalyticsFilter) (*AnalyticsData, error)
|
Aggregate(context.Context, AnalyticsKind, AnalyticsScope, AnalyticsFilter) (*AnalyticsData, error)
|
||||||
|
FillNames(context.Context, AnalyticsKind, AnalyticsScope, AnalyticsFilter, []*AnalyticsRow) error
|
||||||
}
|
}
|
||||||
type AnalyticsUsecase struct {
|
type AnalyticsUsecase struct {
|
||||||
repo AnalyticsRepo
|
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.Begin = filter.StartDate.Format("2006-01-02")
|
||||||
filter.End = filter.EndDate.Format("2006-01-02")
|
filter.End = filter.EndDate.Format("2006-01-02")
|
||||||
out := &AnalyticsPage{Rows: []*AnalyticsRow{}, Begin: filter.Begin, End: filter.End, Available: true}
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -165,9 +167,6 @@ func (uc *AnalyticsUsecase) List(ctx context.Context, kind AnalyticsKind, filter
|
|||||||
pay.Add(pay, rounded)
|
pay.Add(pay, rounded)
|
||||||
}
|
}
|
||||||
counts = analyticsAdd(counts, row.Counts)
|
counts = analyticsAdd(counts, row.Counts)
|
||||||
if kind == ChannelAnalytics {
|
|
||||||
row.Name = (&LedgerNames{PromoteID: &row.ID, PromoteAccount: row.Name}).DisplayPromote()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if kind == ChannelAnalytics {
|
if kind == ChannelAnalytics {
|
||||||
counts = analyticsCap(counts)
|
counts = analyticsCap(counts)
|
||||||
@@ -187,6 +186,28 @@ func (uc *AnalyticsUsecase) List(ctx context.Context, kind AnalyticsKind, filter
|
|||||||
if err := analyticsValues(out.Summary); err != nil {
|
if err := analyticsValues(out.Summary); err != nil {
|
||||||
return nil, err
|
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 {
|
if err := sortAnalytics(data.Rows, kind, filter.SortField, filter.Ascending); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -205,6 +226,11 @@ func (uc *AnalyticsUsecase) List(ctx context.Context, kind AnalyticsKind, filter
|
|||||||
if from < len(data.Rows) {
|
if from < len(data.Rows) {
|
||||||
out.Rows = data.Rows[from:to]
|
out.Rows = data.Rows[from:to]
|
||||||
}
|
}
|
||||||
|
if !namesBeforeSort {
|
||||||
|
if err := fillNames(out.Rows); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
func sortAnalytics(rows []*AnalyticsRow, kind AnalyticsKind, field string, ascending bool) error {
|
func sortAnalytics(rows []*AnalyticsRow, kind AnalyticsKind, field string, ascending bool) error {
|
||||||
|
|||||||
@@ -7,9 +7,23 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type analyticsRepoFake struct {
|
type analyticsRepoFake struct {
|
||||||
data *AnalyticsData
|
data *AnalyticsData
|
||||||
scope AnalyticsScope
|
scope AnalyticsScope
|
||||||
filter AnalyticsFilter
|
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) {
|
func (r *analyticsRepoFake) Aggregate(_ context.Context, _ AnalyticsKind, s AnalyticsScope, f AnalyticsFilter) (*AnalyticsData, error) {
|
||||||
@@ -76,3 +90,75 @@ func TestAnalyticsSortAndMissingData(t *testing.T) {
|
|||||||
t.Fatal("倒置日期应被拒绝")
|
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
|
return result, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := fillAnalyticsNames(db, kind, scope, f, rowMap); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for _, row := range rowMap {
|
for _, row := range rowMap {
|
||||||
result.Rows = append(result.Rows, row)
|
result.Rows = append(result.Rows, row)
|
||||||
}
|
}
|
||||||
@@ -228,6 +225,18 @@ func (r *analyticsRepo) Aggregate(ctx context.Context, kind business.AnalyticsKi
|
|||||||
})
|
})
|
||||||
return result, nil
|
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 {
|
func analyticsPairs(keys []business.AnalyticsKey) [][]any {
|
||||||
result := make([][]any, 0, len(keys))
|
result := make([][]any, 0, len(keys))
|
||||||
for _, key := range keys {
|
for _, key := range keys {
|
||||||
@@ -269,6 +278,12 @@ func fillAnalyticsNames(db *gorm.DB, kind business.AnalyticsKind, scope business
|
|||||||
for key := range rows {
|
for key := range rows {
|
||||||
keys = append(keys, key)
|
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 {
|
type entityPO struct {
|
||||||
ID int64
|
ID int64
|
||||||
SourceDB string `gorm:"column:source_db"`
|
SourceDB string `gorm:"column:source_db"`
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ func TestAnalyticsChannelTreeAndAdminOfficialRows(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
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 {
|
if len(data.Rows) != 3 || data.GlobalCounts != nil {
|
||||||
t.Fatalf("普通商务统计范围错误: %+v", data)
|
t.Fatalf("普通商务统计范围错误: %+v", data)
|
||||||
}
|
}
|
||||||
@@ -69,6 +72,12 @@ func TestAnalyticsFiltersKeepHierarchyAndSnapshotNames(t *testing.T) {
|
|||||||
kind := 1
|
kind := 1
|
||||||
filter.PromoteType = &kind
|
filter.PromoteType = &kind
|
||||||
data, err = repo.Aggregate(context.Background(), business.ChannelAnalytics, scope, filter)
|
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 {
|
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)
|
t.Fatalf("类型筛选应限制参与渠道,同时显示归根后的当前资料: %+v %v", data, err)
|
||||||
}
|
}
|
||||||
@@ -78,6 +87,9 @@ func TestAnalyticsFiltersKeepHierarchyAndSnapshotNames(t *testing.T) {
|
|||||||
if err != nil || len(data.Rows) != 2 {
|
if err != nil || len(data.Rows) != 2 {
|
||||||
t.Fatalf("按游戏快照名筛选错误: %+v %v", data, err)
|
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 {
|
for _, row := range data.Rows {
|
||||||
if row.SourceDB == "lx" && (row.ID != 1 || row.Name != "游戏甲" || row.Counts.Register != 1) {
|
if row.SourceDB == "lx" && (row.ID != 1 || row.Name != "游戏甲" || row.Counts.Register != 1) {
|
||||||
t.Fatalf("应按报表快照筛选、用当前游戏名展示: %+v", row)
|
t.Fatalf("应按报表快照筛选、用当前游戏名展示: %+v", row)
|
||||||
|
|||||||
@@ -155,7 +155,11 @@ func (r *serverAnalysisRepo) Servers(ctx context.Context, sources, games []strin
|
|||||||
return nil, business.ErrServerOptionsUnavailable
|
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 {
|
if len(sources) > 0 {
|
||||||
q = q.Where("s.source_db IN ?", sources)
|
q = q.Where("s.source_db IN ?", sources)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,3 +85,30 @@ func TestServerAnalysisStorageScopeSnapshotAndPreference(t *testing.T) {
|
|||||||
t.Fatal("缺身份表应明确未接入")
|
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