feat: 迁移渠道商务游戏排行榜及同期比较

This commit is contained in:
Yvan
2026-11-04 01:34:37 +08:00
parent cbf0ee9be3
commit ac4e13e610
27 changed files with 1079 additions and 6 deletions
+201
View File
@@ -0,0 +1,201 @@
package business
import (
"context"
"gorm.io/gorm"
"kra/internal/biz/business"
)
type rankingRepo struct{ data Provider }
func NewRankingRepo(data Provider) business.RankingRepo { return &rankingRepo{data: data} }
// 通过一次关系查询展开各上榜商务的下级绑定,避免旧服务对每个商务查询下级。
func rankingBusinessBindings(db *gorm.DB, scope business.AnalyticsScope, sources []string) *gorm.DB {
q := db.Table("sys_users target").Where("target.deleted_at IS NULL")
if scope.Viewer.All {
q = q.Where("target.level IN (1,2,3)")
} else {
q = q.Where("target.parent_id=?", scope.Viewer.SelfID)
}
prefix := "(COALESCE(NULLIF(target.id_path,''),'0') || ',' || target.id)"
if db.Dialector.Name() == "mysql" {
prefix = "CONCAT(COALESCE(NULLIF(target.id_path,''),'0'),',',target.id)"
}
like := prefix + " || ',%'"
if db.Dialector.Name() == "mysql" {
like = "CONCAT(" + prefix + ",',%')"
}
q = q.Joins("JOIN sys_users child ON child.deleted_at IS NULL AND (child.id=target.id OR (target.level<3 AND (child.id_path=" + prefix + " OR child.id_path LIKE (" + like + "))))").Joins("JOIN sys_user_bind_platform b ON b.user_id=child.id AND b.status='0'")
if len(sources) > 0 {
q = q.Where("b.source_db IN ?", sources)
}
return q
}
func rankingBusinessPromotes(db *gorm.DB, scope business.AnalyticsScope, sources []string) *gorm.DB {
q := rankingBusinessBindings(db, scope, sources).Joins("JOIN tab_promote p ON p.source_db=b.source_db AND p.busier_id=b.platform_busier_id")
if !scope.IncludeSub {
q = q.Where("p.promote_level=1")
}
return q
}
func rankingReportQuery(db *gorm.DB, kind business.RankingKind, scope business.AnalyticsScope, f business.AnalyticsFilter, table string, mode int) *gorm.DB {
if kind != business.BusinessRanking {
return analyticsQuery(db, scope, f, table, mode)
}
all := business.AnalyticsScope{Viewer: business.UserScope{All: true}, IncludeSub: true}
return analyticsQuery(db, all, f, table, mode).Where("EXISTS (?)", rankingBusinessPromotes(db, scope, f.Sources).Select("1").Where("p.source_db=r.source_db AND p.id=r.promote_id"))
}
func (r *rankingRepo) Collect(ctx context.Context, kind business.RankingKind, scope business.AnalyticsScope, f business.RankingRange) (*business.RankingData, error) {
db := r.data.DB().WithContext(ctx)
out := &business.RankingData{Current: map[business.AnalyticsKey]*business.RankingMetrics{}, Previous: map[business.AnalyticsKey]*business.RankingMetrics{}, Available: true}
if kind == business.BusinessRanking {
var found []struct{ ID uint }
if err := rankingBusinessBindings(db, scope, f.Sources).Select("target.id").Limit(1).Scan(&found).Error; err != nil {
return nil, business.ErrStorage
}
if len(found) == 0 {
return out, nil
}
available, err := businessTableAvailable(db, "tab_promote")
if err != nil {
return nil, err
}
if !available {
out.Available = false
return out, nil
}
var pairs []struct {
UserID uint
Username string
Level int
SourceDB string `gorm:"column:source_db"`
PromoteID int64
}
if err := rankingBusinessPromotes(db, scope, f.Sources).Select("DISTINCT target.id AS user_id,target.username,target.level,p.source_db,p.id AS promote_id").Scan(&pairs).Error; err != nil {
return nil, business.ErrStorage
}
businesses := map[uint]*business.RankingBusiness{}
for _, po := range pairs {
if businesses[po.UserID] == nil {
businesses[po.UserID] = &business.RankingBusiness{ID: po.UserID, Name: po.Username, Level: po.Level}
}
target := businesses[po.UserID]
target.Channels = append(target.Channels, business.AnalyticsKey{SourceDB: po.SourceDB, ID: po.PromoteID})
}
for _, target := range businesses {
out.Businesses = append(out.Businesses, target)
}
if len(out.Businesses) == 0 {
return out, nil
}
} else {
empty, available, err := prepareAnalytics(db, scope, f.AnalyticsFilter)
if err != nil {
return nil, err
}
out.Available = available
if empty || !available {
return out, nil
}
}
for _, table := range []string{"tab_sw_report_day", "tab_sw_report_day_key"} {
available, err := businessTableAvailable(db, table)
if err != nil {
return nil, err
}
if !available {
out.Available = false
return out, nil
}
}
for _, period := range []struct {
begin, end string
target map[business.AnalyticsKey]*business.RankingMetrics
}{{f.Begin, f.End, out.Current}, {f.PreviousBegin, f.PreviousEnd, out.Previous}} {
query := f.AnalyticsFilter
query.Begin, query.End = period.begin, period.end
rowFor := func(po analyticsAggPO) *business.RankingMetrics {
key := business.AnalyticsKey{SourceDB: po.SourceDB, ID: *po.ID}
if period.target[key] == nil {
period.target[key] = &business.RankingMetrics{}
}
return period.target[key]
}
for _, pay := range []bool{true, false} {
table, mode := "tab_sw_report_day", 0
if !pay {
table = "tab_sw_report_day_key"
if scope.IDCardDedup {
mode = 1
}
}
q := rankingReportQuery(db, kind, scope, query, table, mode)
id := "r.promote_id"
if kind == business.GameRanking {
id = "r.game_id"
}
if kind == business.ChannelRanking {
q = q.Joins("LEFT JOIN tab_promote root_p ON root_p.source_db=r.source_db AND root_p.id=r.promote_id")
id = "COALESCE(NULLIF(root_p.top_promote_id,0),r.promote_id)"
}
selects := "r.source_db," + id + " AS entity_id"
group := "r.source_db," + id
if pay {
selects += ",COALESCE(SUM(r.total_pay),0) AS amount"
} else {
selects += ",r.metric,COUNT(DISTINCT r.identity_key) AS count"
group += ",r.metric"
}
var rows []analyticsAggPO
if err := q.Select(selects).Group(group).Scan(&rows).Error; err != nil {
return nil, business.ErrStorage
}
for _, po := range rows {
if po.ID == nil || (pay && *po.ID < 0) {
continue
}
row := rowFor(po)
if pay {
row.Pay = po.Amount
} else {
putAnalyticsCount(&row.Counts, po.Metric, po.Count)
}
}
}
}
if kind != business.BusinessRanking && len(out.Current) > 0 {
table, column := "tab_promote", "account"
if kind == business.GameRanking {
table, column = "tab_game", "game_name"
}
available, err := businessTableAvailable(db, table)
if err != nil {
return nil, err
}
if !available {
out.Available = false
return out, nil
}
keys := make([]business.AnalyticsKey, 0, len(out.Current))
for key := range out.Current {
keys = append(keys, key)
}
for start := 0; start < len(keys); start += 300 {
var rows []struct {
ID int64
SourceDB string `gorm:"column:source_db"`
Name string
}
if err := db.Table(table).Select("id,source_db,"+column+" AS name").Where("(source_db,id) IN ?", analyticsPairs(keys[start:min(start+300, len(keys))])).Scan(&rows).Error; err != nil {
return nil, business.ErrStorage
}
for _, po := range rows {
if row := out.Current[business.AnalyticsKey{SourceDB: po.SourceDB, ID: po.ID}]; row != nil {
row.Name = po.Name
}
}
}
}
return out, nil
}
+71
View File
@@ -0,0 +1,71 @@
package business
import (
"context"
"kra/internal/biz/business"
"testing"
)
func TestRankingStorageDifferentRootAndBusinessScopes(t *testing.T) {
db, _, scope, filter := prepareAnalyticsData(t)
r := NewRankingRepo(testData{db})
ctx := context.Background()
f := business.RankingRange{AnalyticsFilter: filter, PreviousBegin: "2026-10-01", PreviousEnd: "2026-10-03"}
data, err := r.Collect(ctx, business.GameRanking, scope, f)
if err != nil || len(data.Current) != 3 || data.Current[business.AnalyticsKey{SourceDB: "lx", ID: 1}].Name != "游戏甲" {
t.Fatalf("游戏排行或当前名称错误: %+v %v", data, err)
}
scope.Viewer = business.UserScope{SelfID: 7, IncludeSelf: true, DescendantPath: "0,7"}
data, err = r.Collect(ctx, business.BusinessRanking, scope, f)
if err != nil {
t.Fatal(err)
}
var child *business.RankingBusiness
for _, target := range data.Businesses {
if target.ID == 8 {
child = target
}
if target.ID != 8 && target.ID != 10 {
t.Fatal("一级只能排行直属下级")
}
}
if child == nil || len(child.Channels) != 5 {
t.Fatalf("二级本人的 hw 和下属三级的 lx 渠道应合并: %+v", child)
}
if _, ok := data.Current[business.AnalyticsKey{SourceDB: "lx", ID: 0}]; ok {
t.Fatal("商务榜渠道映射不应包含官方流水")
}
scope.Viewer = business.UserScope{SelfID: 8, IncludeSelf: true, DescendantPath: "0,7,8"}
data, err = r.Collect(ctx, business.BusinessRanking, scope, f)
if err != nil || len(data.Businesses) != 1 || data.Businesses[0].ID != 9 || len(data.Businesses[0].Channels) != 4 {
t.Fatal("二级商务榜只能展示直属三级及其渠道")
}
scope.Viewer = business.UserScope{All: true}
data, err = r.Collect(ctx, business.BusinessRanking, scope, f)
if err != nil {
t.Fatal(err)
}
seen := map[uint]bool{}
for _, b := range data.Businesses {
seen[b.ID] = true
}
if !seen[7] || !seen[8] || !seen[9] || seen[1] || seen[99] {
t.Fatal("超管应展示所选平台有渠道的各级商务,不能包含普通框架管理员")
}
if err := db.Exec("UPDATE tab_promote SET top_promote_id=777 WHERE source_db='lx' AND id=100").Error; err != nil {
t.Fatal(err)
}
scope = business.AnalyticsScope{Viewer: channelTestScope(), IncludeSub: false, IDCardDedup: true}
data, err = r.Collect(ctx, business.ChannelRanking, scope, f)
root := data.Current[business.AnalyticsKey{SourceDB: "lx", ID: 777}]
if err != nil || root == nil || portraitTestMoney(root.Pay) != "10.10" || root.Counts.Register != 1 {
t.Fatalf("独立渠道榜使用 COALESCE(top,id),不应误用工作台 CASE 根算法: %+v %v", root, err)
}
if err := db.Exec("DROP TABLE tab_sw_report_day_key").Error; err != nil {
t.Fatal(err)
}
data, err = r.Collect(ctx, business.ChannelRanking, scope, f)
if err != nil || data.Available {
t.Fatal("缺表应明确未接入")
}
}
+1
View File
@@ -45,6 +45,7 @@ var ProviderSet = wire.NewSet(
databusiness.NewAnalyticsRepo,
databusiness.NewServerAnalysisRepo,
databusiness.NewWorkbenchRepo,
databusiness.NewRankingRepo,
NewIntegrationRuntime,
wire.Bind(new(datasystem.Provider), new(*Data)),
wire.Bind(new(datasystem.DatabaseProvider), new(*Data)),