feat: 迁移渠道商务游戏排行榜及同期比较
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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("缺表应明确未接入")
|
||||
}
|
||||
}
|
||||
@@ -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)),
|
||||
|
||||
Reference in New Issue
Block a user