package business import ( "context" "strings" "gorm.io/gorm" "kra/internal/biz/business" ) type serverAnalysisRepo struct{ data Provider } func NewServerAnalysisRepo(data Provider) business.ServerAnalysisRepo { return &serverAnalysisRepo{data: data} } func serverReportQuery(db *gorm.DB, scope business.AnalyticsScope, f business.ServerAnalysisFilter, table string, mode int) *gorm.DB { q := ledgerScopeQuery(db, business.LedgerScope{Viewer: scope.Viewer, IncludeSub: scope.IncludeSub, Unrestricted: scope.Viewer.All}, business.LedgerFilter{Sources: f.Sources}, table) q = q.Where("r.stat_date >= ? AND r.stat_date <= ? AND r.dedup_mode=? AND r.server_num != ''", f.Begin, f.End, mode).Where("r.game_name IN ?", f.GameNames) if f.ServerNum != "" { q = q.Where("r.server_num=?", f.ServerNum) } return q } type serverAnalysisPO struct { SourceDB string `gorm:"column:source_db"` GameID int64 ServerNum, GameName, ServerName string StartTime *int64 Metric int Count int64 Amount string } func (r *serverAnalysisRepo) Aggregate(ctx context.Context, scope business.AnalyticsScope, f business.ServerAnalysisFilter) (*business.ServerAnalysisData, error) { db := r.data.DB().WithContext(ctx) out := &business.ServerAnalysisData{Rows: []*business.ServerAnalysisRow{}, Available: true} empty, available, err := prepareAnalytics(db, scope, business.AnalyticsFilter{Sources: f.Sources}) if err != nil { return nil, err } out.Available = available if empty || !available { return out, nil } for _, table := range []string{"tab_sw_report_server_day", "tab_sw_report_server_day_key", "tab_game_server", "tab_game"} { available, err := businessTableAvailable(db, table) if err != nil { return nil, err } if !available { out.Available = false return out, nil } } rows := map[business.ServerAnalysisKey]*business.ServerAnalysisRow{} rowFor := func(po serverAnalysisPO) *business.ServerAnalysisRow { key := business.ServerAnalysisKey{SourceDB: po.SourceDB, GameID: po.GameID, ServerNum: po.ServerNum} if rows[key] == nil { rows[key] = &business.ServerAnalysisRow{ServerAnalysisKey: key, Pay: "0"} } return rows[key] } mode := 0 if scope.IDCardDedup { mode = 1 } var counts []serverAnalysisPO if err := serverReportQuery(db, scope, f, "tab_sw_report_server_day_key", mode).Select("r.source_db,r.game_id,r.server_num,r.metric,COUNT(DISTINCT r.identity_key) AS count").Group("r.source_db,r.game_id,r.server_num,r.metric").Scan(&counts).Error; err != nil { return nil, business.ErrStorage } for _, po := range counts { if strings.TrimSpace(po.ServerNum) != "" { putAnalyticsCount(&rowFor(po).Counts, po.Metric, po.Count) } } var pays []serverAnalysisPO if err := serverReportQuery(db, scope, f, "tab_sw_report_server_day", 0).Select("r.source_db,r.game_id,r.server_num,MAX(r.game_name) AS game_name,COALESCE(SUM(r.total_pay),0) AS amount").Group("r.source_db,r.game_id,r.server_num").Scan(&pays).Error; err != nil { return nil, business.ErrStorage } for _, po := range pays { if strings.TrimSpace(po.ServerNum) == "" { continue } row := rowFor(po) row.Pay = po.Amount row.GameName = strings.TrimSpace(po.GameName) } if err := fillServerAnalysisNames(db, rows); err != nil { return nil, err } for _, row := range rows { out.Rows = append(out.Rows, row) } return out, nil } func fillServerAnalysisNames(db *gorm.DB, rows map[business.ServerAnalysisKey]*business.ServerAnalysisRow) error { keys := make([][]any, 0, len(rows)) missingGames := map[business.AnalyticsKey]bool{} for key, row := range rows { keys = append(keys, []any{key.SourceDB, key.GameID, key.ServerNum}) if row.GameName == "" { missingGames[business.AnalyticsKey{SourceDB: key.SourceDB, ID: key.GameID}] = true } } // 三元组限定资料查询,避免同号游戏或同名区服跨平台串联。 for start := 0; start < len(keys); start += 250 { var meta []serverAnalysisPO if err := db.Table("tab_game_server").Select("source_db,game_id,server_num,server_name,start_time").Where("(source_db,game_id,server_num) IN ?", keys[start:min(start+250, len(keys))]).Order("id").Scan(&meta).Error; err != nil { return business.ErrStorage } seen := map[business.ServerAnalysisKey]bool{} for _, po := range meta { key := business.ServerAnalysisKey{SourceDB: po.SourceDB, GameID: po.GameID, ServerNum: po.ServerNum} if row := rows[key]; row != nil && !seen[key] { row.ServerName, row.OpenTime = po.ServerName, po.StartTime seen[key] = true } } } pairs := make([]business.AnalyticsKey, 0, len(missingGames)) for key := range missingGames { pairs = append(pairs, key) } names := map[business.AnalyticsKey]string{} for start := 0; start < len(pairs); start += 300 { var meta []struct { SourceDB string `gorm:"column:source_db"` ID int64 GameName string } if err := db.Table("tab_game").Select("source_db,id,game_name").Where("(source_db,id) IN ?", analyticsPairs(pairs[start:min(start+300, len(pairs))])).Scan(&meta).Error; err != nil { return business.ErrStorage } for _, po := range meta { names[business.AnalyticsKey{SourceDB: po.SourceDB, ID: po.ID}] = po.GameName } } for _, row := range rows { if row.GameName == "" { row.GameName = names[business.AnalyticsKey{SourceDB: row.SourceDB, ID: row.GameID}] } } return nil } func (r *serverAnalysisRepo) Servers(ctx context.Context, sources, games []string, keyword string, limit int) ([]*business.ServerOption, error) { db := r.data.DB().WithContext(ctx) for _, table := range []string{"tab_game", "tab_game_server"} { available, err := businessTableAvailable(db, table) if err != nil { return nil, err } if !available { return nil, business.ErrServerOptionsUnavailable } } // 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) } if keyword != "" { q = q.Where("s.server_name LIKE ?", "%"+keyword+"%") } var rows []serverAnalysisPO if err := q.Select("s.server_num,s.server_name,s.start_time").Order("s.server_num,s.source_db,s.game_id,s.id").Limit(limit).Scan(&rows).Error; err != nil { return nil, business.ErrStorage } out := make([]*business.ServerOption, 0, len(rows)) for _, row := range rows { out = append(out, &business.ServerOption{Number: row.ServerNum, Name: row.ServerName, OpenTime: row.StartTime}) } return out, nil }