fix statistics registration active count

This commit is contained in:
zhx 2026-06-24 10:43:47 +08:00
parent 059de783ac
commit 93250e5a7b
3 changed files with 52 additions and 14 deletions

View File

@ -363,12 +363,15 @@ func rebuildRegistrations(ctx context.Context, db *sql.DB, state *rebuildState,
state.registeredByUserDay[userDayKey(row.registeredDay, row.userID)] = struct{}{}
agg := state.ensureAgg(row.registeredDay, row.dim)
agg.newUsers++
// 产品口径要求完成注册即算当日活跃;补数时直接把注册事实写入活跃重建集合,
// 后续房间/游戏活跃若命中同一用户同一天,只会更新更早的 first_active_at_ms不会重复计数。
state.addActive(row.registeredDay, row.userID, row.dim, row.registeredAtMS)
}
return rows.Err()
}
func rebuildActive(ctx context.Context, db *sql.DB, state *rebuildState, app, startDay, endDay string) error {
// 活跃的原始房间事件已经被实时消费进 stat_user_day_activity离线回填只重刷该表的国家/区域快照,避免在大窗口内扫描房间流水
// 房间/游戏活跃已经被实时消费进 stat_user_day_activity离线回填在注册活跃基础上重刷这张表的国家/区域快照
rows, err := db.QueryContext(ctx, `
SELECT DATE_FORMAT(stat_day, '%Y-%m-%d'), user_id, MIN(first_active_at_ms)
FROM stat_user_day_activity
@ -389,13 +392,7 @@ func rebuildActive(ctx context.Context, db *sql.DB, state *rebuildState, app, st
// stat_user_day_activity 是历史活跃读模型,不是用户事实 owner回填时必须丢弃 user-service 已不存在的孤儿活跃,避免脏事实继续汇总到 country_id=0。
continue
}
dim := state.userDims[userID]
key := fmt.Sprintf("%s:%d", day, userID)
if _, exists := state.activeByKey[key]; exists {
continue
}
state.activeByKey[key] = activeRow{day: day, userID: userID, dim: dim, firstMS: firstMS}
state.ensureAgg(day, dim).activeUsers++
state.addActive(day, userID, state.userDims[userID], firstMS)
}
return rows.Err()
}
@ -486,6 +483,21 @@ func (s *rebuildState) addPayer(day string, userID int64, dim countryRegion, fir
s.payersByKey[key] = payerRow{day: day, userID: userID, dim: dim, firstMS: firstMS}
}
func (s *rebuildState) addActive(day string, userID int64, dim countryRegion, firstMS int64) {
key := userDayKey(day, userID)
if existing, exists := s.activeByKey[key]; exists {
// 注册、进房和游戏都能证明同一用户同一天活跃;主键按用户日收敛,聚合只加一次,
// 但 first_active_at_ms 要保留最早事实,便于后续排查“为什么算活跃”时能回到第一条证据。
if firstMS > 0 && (existing.firstMS == 0 || firstMS < existing.firstMS) {
existing.firstMS = firstMS
s.activeByKey[key] = existing
}
return
}
s.activeByKey[key] = activeRow{day: day, userID: userID, dim: dim, firstMS: firstMS}
s.ensureAgg(day, dim).activeUsers++
}
func (s *rebuildState) userRegisteredOn(userID int64, day string) bool {
_, ok := s.registeredByUserDay[userDayKey(day, userID)]
return ok

View File

@ -95,17 +95,36 @@ func TestConsumeUserRegisteredCountsOnlyNewRegistration(t *testing.T) {
t.Fatalf("consume registration replay: %v", err)
}
var newUsers int64
var newUsers, activeUsers int64
if err := repository.db.QueryRowContext(ctx, `
SELECT new_users
SELECT new_users, active_users
FROM stat_app_day_country
WHERE app_code = 'lalu' AND stat_day = '2026-06-22' AND country_id = 15 AND region_id = 10
`).Scan(&newUsers); err != nil {
`).Scan(&newUsers, &activeUsers); err != nil {
t.Fatalf("query app day country: %v", err)
}
if newUsers != 1 {
t.Fatalf("registration replay must not duplicate new users, got %d", newUsers)
}
if activeUsers != 1 {
t.Fatalf("registration must count as one same-day active user, got %d", activeUsers)
}
var activeRows int64
if err := repository.db.QueryRowContext(ctx, `
SELECT COUNT(*)
FROM stat_user_day_activity
WHERE app_code = 'lalu'
AND stat_day = '2026-06-22'
AND country_id = 15
AND region_id = 10
AND user_id = 327300172394536960
`).Scan(&activeRows); err != nil {
t.Fatalf("query registration active row: %v", err)
}
if activeRows != 1 {
t.Fatalf("registration replay must keep one user-day active row, got %d", activeRows)
}
}
func TestQueryOverviewReturnsPreviousPeriodDeltaRates(t *testing.T) {

View File

@ -514,12 +514,19 @@ func (r *Repository) ConsumeUserRegistered(ctx context.Context, event UserRegist
}
// MQ 重投和补偿任务可能带来新的 event_id但同一用户注册 cohort 只能入库一次;
// new_users 必须按 INSERT IGNORE 的真实插入行数累加,避免回放把国家新增人数重复放大。
_, err = tx.ExecContext(ctx, `
if _, err = tx.ExecContext(ctx, `
UPDATE stat_app_day_country
SET new_users = new_users + ?, updated_at_ms = ?
WHERE app_code = ? AND stat_day = ? AND country_id = ? AND region_id = ?
`, inserted, nowMS, appcode.Normalize(event.AppCode), day, countryID, regionID)
return err
`, inserted, nowMS, appcode.Normalize(event.AppCode), day, countryID, regionID); err != nil {
return err
}
if inserted == 0 {
return nil
}
// 产品口径要求“完成注册即产生当日活跃”:注册完成本身已经证明用户打开并完成 App 主流程;
// 这里复用活跃唯一表收敛,若同日已有进房或游戏事件,不会重复增加 active_users。
return applyActive(ctx, tx, event.AppCode, day, countryID, regionID, event.UserID, event.OccurredAtMS, nowMS)
})
}