feat: 迁移区服分析与游戏区服查询
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
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))
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"kra/internal/biz/business"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestServerAnalysisStorageScopeSnapshotAndPreference(t *testing.T) {
|
||||
db, _ := prepareChannels(t)
|
||||
for _, sql := range []string{
|
||||
"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,'hw',1,'001','汇玩一区',200),(3,'lx',2,'001','游戏乙一区',300),(4,'yq',1,'001','无权区服',400)",
|
||||
"CREATE TABLE tab_sw_report_server_day(source_db TEXT,game_id INTEGER,game_name TEXT,promote_id INTEGER,server_num TEXT,stat_date DATE,dedup_mode INTEGER,total_pay DECIMAL(20,2))",
|
||||
"INSERT INTO tab_sw_report_server_day VALUES('lx',1,'历史游戏',100,'001','2026-11-04',0,10),('lx',1,'历史游戏',101,'001','2026-11-04',0,20),('hw',1,'历史游戏',100,'001','2026-11-04',0,40),('yq',1,'历史游戏',100,'001','2026-11-04',0,999),('lx',1,'历史游戏',100,'001','2026-11-04',1,9999),('lx',1,'历史游戏',0,'official','2026-11-04',0,5),('lx',1,'历史游戏',100,'','2026-11-04',0,9999)",
|
||||
"CREATE TABLE tab_sw_report_server_day_key(source_db TEXT,game_id INTEGER,game_name TEXT,promote_id INTEGER,server_num TEXT,stat_date DATE,dedup_mode INTEGER,metric INTEGER,identity_key TEXT)",
|
||||
"INSERT INTO tab_sw_report_server_day_key VALUES('lx',1,'历史游戏',100,'001','2026-11-04',1,1,'A'),('lx',1,'历史游戏',101,'001','2026-11-04',1,1,'A'),('lx',1,'历史游戏',100,'001','2026-11-04',0,1,'1'),('lx',1,'历史游戏',101,'001','2026-11-04',0,1,'2'),('hw',1,'历史游戏',100,'001','2026-11-04',1,1,'A'),('lx',2,'游戏乙',100,'001','2026-11-04',1,1,'B')",
|
||||
} {
|
||||
if err := db.Exec(sql).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
r := NewServerAnalysisRepo(testData{db})
|
||||
ctx := context.Background()
|
||||
s := business.AnalyticsScope{Viewer: channelTestScope(), IncludeSub: true, IDCardDedup: true}
|
||||
f := business.ServerAnalysisFilter{AnalyticsFilter: business.AnalyticsFilter{Sources: []string{"lx", "hw", "yq"}, GameNames: []string{"历史游戏", "游戏乙"}, Begin: "2026-11-04", End: "2026-11-04"}}
|
||||
p, err := r.Aggregate(ctx, s, f)
|
||||
if err != nil || len(p.Rows) != 3 {
|
||||
t.Fatalf("区服范围错误: %+v %v", p, err)
|
||||
}
|
||||
for _, row := range p.Rows {
|
||||
switch {
|
||||
case row.SourceDB == "lx" && row.GameID == 1:
|
||||
if row.GameName != "历史游戏" || row.ServerName != "龙翔一区" || row.OpenTime == nil || *row.OpenTime != 100 || row.Counts.Register != 1 || portraitTestMoney(row.Pay) != "30.00" {
|
||||
t.Fatalf("去重、资料或金额错误: %+v", row)
|
||||
}
|
||||
case row.SourceDB == "hw":
|
||||
if row.ServerName != "汇玩一区" || portraitTestMoney(row.Pay) != "40.00" {
|
||||
t.Fatalf("跨平台同号串联: %+v", row)
|
||||
}
|
||||
case row.GameID == 2:
|
||||
if row.GameName != "游戏乙" || row.ServerName != "游戏乙一区" {
|
||||
t.Fatalf("只有人数的行应补当前游戏名: %+v", row)
|
||||
}
|
||||
default:
|
||||
t.Fatalf("出现无权行: %+v", row)
|
||||
}
|
||||
}
|
||||
s.IDCardDedup = false
|
||||
f.Sources = []string{"lx"}
|
||||
f.GameNames = []string{"历史游戏"}
|
||||
p, err = r.Aggregate(ctx, s, f)
|
||||
if err != nil || len(p.Rows) != 1 || p.Rows[0].Counts.Register != 2 || portraitTestMoney(p.Rows[0].Pay) != "30.00" {
|
||||
t.Fatal("身份证偏好影响了金额或未切换人数")
|
||||
}
|
||||
s.IncludeSub = false
|
||||
p, err = r.Aggregate(ctx, s, f)
|
||||
if err != nil || p.Rows[0].Counts.Register != 1 || portraitTestMoney(p.Rows[0].Pay) != "10.00" {
|
||||
t.Fatal("下级开关未生效")
|
||||
}
|
||||
s = business.AnalyticsScope{Viewer: business.UserScope{All: true}, IncludeSub: true, IDCardDedup: true}
|
||||
p, err = r.Aggregate(ctx, s, f)
|
||||
if err != nil || len(p.Rows) != 2 {
|
||||
t.Fatal("超管漏掉官方区服流水")
|
||||
}
|
||||
f.ServerNum = "001"
|
||||
p, err = r.Aggregate(ctx, s, f)
|
||||
if err != nil || len(p.Rows) != 1 {
|
||||
t.Fatal("区服编号筛选错误")
|
||||
}
|
||||
options, err := r.Servers(ctx, []string{"lx"}, []string{"游戏甲"}, "一区", 50)
|
||||
if err != nil || len(options) != 1 || options[0].Number != "001" || options[0].Name != "龙翔一区" {
|
||||
t.Fatal("选项应按当前游戏名、同平台游戏关联及区服名称筛选")
|
||||
}
|
||||
f.GameNames = []string{"游戏甲"}
|
||||
p, err = r.Aggregate(ctx, s, f)
|
||||
if err != nil || len(p.Rows) != 0 {
|
||||
t.Fatal("报表必须按快照游戏名筛选")
|
||||
}
|
||||
if err := db.Exec("DROP TABLE tab_sw_report_server_day_key").Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p, err = r.Aggregate(ctx, s, f)
|
||||
if err != nil || p.Available {
|
||||
t.Fatal("缺身份表应明确未接入")
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,7 @@ var ProviderSet = wire.NewSet(
|
||||
databusiness.NewChannelPortraitRepo,
|
||||
databusiness.NewLedgerRepo,
|
||||
databusiness.NewAnalyticsRepo,
|
||||
databusiness.NewServerAnalysisRepo,
|
||||
NewIntegrationRuntime,
|
||||
wire.Bind(new(datasystem.Provider), new(*Data)),
|
||||
wire.Bind(new(datasystem.DatabaseProvider), new(*Data)),
|
||||
|
||||
Reference in New Issue
Block a user