fix: align business reports and record task migration progress

This commit is contained in:
Yvan
2026-11-06 00:47:06 +08:00
parent 1bc2082718
commit b1f14bd525
32 changed files with 501 additions and 53 deletions
+12 -2
View File
@@ -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 {
+30
View File
@@ -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))
+31 -1
View File
@@ -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
+26
View File
@@ -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{
+1 -7
View File
@@ -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")
}
// 游戏可选范围来自可见渠道的有效申请,不受统计时是否包含下级的开关影响。