fix: align business reports and record task migration progress
This commit is contained in:
@@ -262,12 +262,22 @@ func loadAlertLoss(db *gorm.DB, keys []business.ChannelKey, window business.Aler
|
||||
if err := db.Table("tab_promote").Select("id,last_login_time,create_time").Where("source_db=? AND id IN ?", source, batch).Scan(&channels).Error; err != nil {
|
||||
return nil, business.ErrStorage
|
||||
}
|
||||
// Login expiry already establishes loss; only the remaining channels
|
||||
// need the potentially expensive payment scan on a cold database.
|
||||
paymentIDs := make([]uint64, 0, len(channels))
|
||||
for _, channel := range channels {
|
||||
if !window.LoginExpired(channel.LastLoginTime, channel.CreateTime) {
|
||||
paymentIDs = append(paymentIDs, channel.ID)
|
||||
}
|
||||
}
|
||||
var amounts []struct {
|
||||
PromoteID uint64
|
||||
RecentAmount string
|
||||
}
|
||||
if err := db.Table("tab_spend").Select("promote_id,COALESCE(SUM(pay_amount),0) AS recent_amount").Where("source_db=? AND promote_id IN ? AND pay_status=1 AND pay_time>=? AND pay_time<?", source, batch, window.PayBegin, window.PayEnd).Group("promote_id").Scan(&amounts).Error; err != nil {
|
||||
return nil, business.ErrStorage
|
||||
if len(paymentIDs) > 0 {
|
||||
if err := db.Table("tab_spend").Select("promote_id,COALESCE(SUM(pay_amount),0) AS recent_amount").Where("source_db=? AND promote_id IN ? AND pay_status=1 AND pay_time>=? AND pay_time<?", source, paymentIDs, window.PayBegin, window.PayEnd).Group("promote_id").Scan(&amounts).Error; err != nil {
|
||||
return nil, business.ErrStorage
|
||||
}
|
||||
}
|
||||
payments := map[uint64]string{}
|
||||
for _, amount := range amounts {
|
||||
|
||||
@@ -2,6 +2,7 @@ package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -9,6 +10,35 @@ import (
|
||||
"kra/internal/biz/business"
|
||||
)
|
||||
|
||||
func TestAlertLossSkipsPaymentReadsForExpiredLogins(t *testing.T) {
|
||||
db := openTestDB(t)
|
||||
now := prepareAlertV2(t, db)
|
||||
if err := db.Exec("UPDATE tab_promote SET last_login_time=0,create_time=1").Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var paymentReads atomic.Int32
|
||||
if err := db.Callback().Row().Before("gorm:row").Register("count_loss_payment_reads", func(tx *gorm.DB) {
|
||||
if tx.Statement.Table == "tab_spend" {
|
||||
paymentReads.Add(1)
|
||||
}
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
keys := []business.ChannelKey{{SourceDB: "lx", ID: 1}, {SourceDB: "hw", ID: 1}}
|
||||
window := business.AlertLossAt(now)
|
||||
states, err := loadAlertLoss(db, keys, window)
|
||||
if err != nil || paymentReads.Load() != 0 || len(states) != 2 || !*states[keys[0]] || !*states[keys[1]] {
|
||||
t.Fatalf("expired logins should establish loss without payment reads: %+v %v reads=%d", states, err, paymentReads.Load())
|
||||
}
|
||||
if err := db.Exec("UPDATE tab_promote SET last_login_time=? WHERE source_db='hw'", now.Unix()).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
states, err = loadAlertLoss(db, keys, window)
|
||||
if err != nil || paymentReads.Load() != 1 || !*states[keys[0]] || *states[keys[1]] {
|
||||
t.Fatalf("active login must still check payments within its own platform: %+v %v reads=%d", states, err, paymentReads.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func prepareAlertV2(t *testing.T, db *gorm.DB) time.Time {
|
||||
t.Helper()
|
||||
now := time.Date(2026, 11, 5, 0, 0, 0, 0, time.FixedZone("Asia/Shanghai", 8*3600))
|
||||
|
||||
@@ -189,7 +189,8 @@ func (r *analyticsRepo) Aggregate(ctx context.Context, kind business.AnalyticsKi
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if withGlobal {
|
||||
metricFilter := kind == business.ChannelAnalytics && (f.HasRegister != nil || f.HasPay != nil)
|
||||
if withGlobal && !metricFilter {
|
||||
group.Go(func() error {
|
||||
q := analyticsQuery(queryDB, scope, f, "tab_sw_report_day_key", mode)
|
||||
if err := q.Select("r.metric,COUNT(DISTINCT " + portraitIdentity(queryDB, scope.IDCardDedup) + ") AS count").Group("r.metric").Scan(&global).Error; err != nil {
|
||||
@@ -226,6 +227,35 @@ func (r *analyticsRepo) Aggregate(ctx context.Context, kind business.AnalyticsKi
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if metricFilter {
|
||||
for key, row := range rowMap {
|
||||
match, err := f.MatchesMetrics(row)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !match {
|
||||
delete(rowMap, key)
|
||||
}
|
||||
}
|
||||
if withGlobal {
|
||||
keys := make([]business.AnalyticsKey, 0, len(rowMap))
|
||||
for key := range rowMap {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
// Deduplicate identities across all matched channels, before pagination.
|
||||
// Summing per-channel counts would double-count shared players.
|
||||
if len(keys) > 0 {
|
||||
q, id := analyticsRoot(db, kind, scope, f, "tab_sw_report_day_key", mode, false)
|
||||
if err := q.Where("(r.source_db,"+id+") IN ?", analyticsPairs(keys)).Select("r.metric,COUNT(DISTINCT " + portraitIdentity(db, scope.IDCardDedup) + ") AS count").Group("r.metric").Scan(&global).Error; err != nil {
|
||||
return nil, business.ErrStorage
|
||||
}
|
||||
q, id = analyticsRoot(db, kind, scope, f, "tab_sw_report_day", 0, true)
|
||||
if err := q.Where("(r.source_db,"+id+") IN ?", analyticsPairs(keys)).Select("COALESCE(SUM(r.total_pay),0)").Scan(&globalPay).Error; err != nil {
|
||||
return nil, business.ErrStorage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if withGlobal {
|
||||
totals := toPortraitCounts(global)
|
||||
result.GlobalCounts, result.GlobalPay = &totals, globalPay
|
||||
|
||||
@@ -61,6 +61,32 @@ func TestAnalyticsChannelTreeAndAdminOfficialRows(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnalyticsMetricFiltersPreserveGlobalDedupAndEffectiveCount(t *testing.T) {
|
||||
db, repo, scope, filter := prepareAnalyticsData(t)
|
||||
scope.Viewer = business.UserScope{All: true}
|
||||
yes, no := true, false
|
||||
filter.HasRegister, filter.HasPay = &yes, &yes
|
||||
// The same real identity occurs under different root channels/platforms.
|
||||
// All rows match, so the filtered total must keep the original global count.
|
||||
data, err := repo.Aggregate(context.Background(), business.ChannelAnalytics, scope, filter)
|
||||
if err != nil || len(data.Rows) != 5 || data.GlobalCounts.Register != 4 || portraitTestMoney(data.GlobalPay) != "2070.60" {
|
||||
t.Fatalf("filtered summary lost global deduplication: %+v %v", data, err)
|
||||
}
|
||||
if err := db.Exec("UPDATE tab_sw_report_day SET total_pay=0 WHERE source_db='lx' AND promote_id IN (100,101,102)").Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
filter.HasPay = &no
|
||||
data, err = repo.Aggregate(context.Background(), business.ChannelAnalytics, scope, filter)
|
||||
if err != nil || len(data.Rows) != 1 || data.Rows[0].ID != 100 || portraitTestMoney(data.GlobalPay) != "0.00" || data.GlobalCounts.Register != 2 {
|
||||
t.Fatalf("no-payment filter must keep the whole root scope and matching totals: %+v %v", data, err)
|
||||
}
|
||||
filter.HasRegister = &no
|
||||
data, err = repo.Aggregate(context.Background(), business.ChannelAnalytics, scope, filter)
|
||||
if err != nil || len(data.Rows) != 0 || data.GlobalCounts.Register != 0 || data.EffectiveCount != 0 {
|
||||
t.Fatalf("combined filters must use AND and return empty totals: %+v %v", data, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnalyticsRowsOnlyComeFromSelectedPeriod(t *testing.T) {
|
||||
db, repo, scope, filter := prepareAnalyticsData(t)
|
||||
for _, sql := range []string{
|
||||
|
||||
@@ -6,13 +6,7 @@ import (
|
||||
)
|
||||
|
||||
func approvedGamesAvailable(db *gorm.DB) (bool, error) {
|
||||
for _, table := range []string{"tab_game", "tab_promote", "tab_promote_apply"} {
|
||||
available, err := businessTableAvailable(db, table)
|
||||
if err != nil || !available {
|
||||
return available, err
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
return businessTablesAvailable(db, "tab_game", "tab_promote", "tab_promote_apply")
|
||||
}
|
||||
|
||||
// 游戏可选范围来自可见渠道的有效申请,不受统计时是否包含下级的开关影响。
|
||||
|
||||
Reference in New Issue
Block a user