增加相关字段
This commit is contained in:
parent
82dfb9238f
commit
0fdaec9ef9
@ -81,6 +81,18 @@ CREATE TABLE IF NOT EXISTS stat_lucky_gift_day_payers (
|
||||
KEY idx_stat_lucky_payer_country (app_code, stat_day, country_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='幸运礼物日付费去重表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS stat_lucky_gift_pool_day_country (
|
||||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
|
||||
stat_day DATE NOT NULL COMMENT 'UTC 统计日',
|
||||
country_id BIGINT NOT NULL DEFAULT 0 COMMENT '国家或区域维度 ID,0 表示未知或全局',
|
||||
pool_id VARCHAR(96) NOT NULL COMMENT '幸运礼物奖池 ID',
|
||||
turnover_coin BIGINT NOT NULL DEFAULT 0 COMMENT '奖池幸运礼物流水金币',
|
||||
payout_coin BIGINT NOT NULL DEFAULT 0 COMMENT '奖池幸运礼物返奖金币',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, stat_day, country_id, pool_id),
|
||||
KEY idx_stat_lucky_pool_overview (app_code, stat_day, pool_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='幸运礼物奖池国家日统计聚合表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS stat_game_day_country (
|
||||
app_code VARCHAR(32) NOT NULL,
|
||||
stat_day DATE NOT NULL,
|
||||
|
||||
@ -257,6 +257,7 @@ func luckyGiftRewardEvent(body []byte) (mysqlstorage.LuckyGiftRewardEvent, bool,
|
||||
EventType: message.EventType,
|
||||
UserID: firstNonZeroInt64(mysqlstorage.Int64(payload, "user_id"), message.UserID),
|
||||
CountryID: firstNonZeroInt64(mysqlstorage.Int64(payload, "visible_region_id"), mysqlstorage.Int64(payload, "region_id"), mysqlstorage.Int64(payload, "target_region_id")),
|
||||
PoolID: mysqlstorage.String(payload, "pool_id"),
|
||||
Amount: mysqlstorage.Int64(payload, "amount"),
|
||||
OccurredAtMS: message.OccurredAtMS,
|
||||
}, true, nil
|
||||
|
||||
@ -135,6 +135,13 @@ func TestLocalStatisticsDataScreenFlow(t *testing.T) {
|
||||
if overview.LuckyGiftTurnover != 1_000 || overview.LuckyGiftPayout != 300 || overview.LuckyGiftProfit != 700 || overview.LuckyGiftPayers != 1 {
|
||||
t.Fatalf("lucky gift metrics mismatch: %+v", overview)
|
||||
}
|
||||
if len(overview.LuckyGiftPools) != 1 {
|
||||
t.Fatalf("lucky gift pool breakdown missing: %+v", overview.LuckyGiftPools)
|
||||
}
|
||||
pool := overview.LuckyGiftPools[0]
|
||||
if pool.PoolID != "lucky_100" || pool.TurnoverCoin != 1_000 || pool.PayoutCoin != 300 || pool.ProfitCoin != 700 {
|
||||
t.Fatalf("lucky gift pool metrics mismatch: %+v", pool)
|
||||
}
|
||||
if overview.GameTurnover != 700 || overview.GamePayout != 200 || overview.GameProfit != 500 || overview.GamePlayers != 1 {
|
||||
t.Fatalf("game metrics mismatch: %+v", overview)
|
||||
}
|
||||
|
||||
@ -16,36 +16,37 @@ type OverviewQuery struct {
|
||||
}
|
||||
|
||||
type Overview struct {
|
||||
AppCode string `json:"app_code"`
|
||||
StartMS int64 `json:"start_ms"`
|
||||
EndMS int64 `json:"end_ms"`
|
||||
CountryID int64 `json:"country_id"`
|
||||
NewUsers int64 `json:"new_users"`
|
||||
ActiveUsers int64 `json:"active_users"`
|
||||
PaidUsers int64 `json:"paid_users"`
|
||||
RechargeUSDMinor int64 `json:"recharge_usd_minor"`
|
||||
NewUserRechargeUSDMinor int64 `json:"new_user_recharge_usd_minor"`
|
||||
CoinSellerRechargeUSDMinor int64 `json:"coin_seller_recharge_usd_minor"`
|
||||
MifaPayRechargeUSDMinor int64 `json:"mifapay_recharge_usd_minor"`
|
||||
GoogleRechargeUSDMinor int64 `json:"google_recharge_usd_minor"`
|
||||
GiftCoinSpent int64 `json:"gift_coin_spent"`
|
||||
LuckyGiftTurnover int64 `json:"lucky_gift_turnover"`
|
||||
LuckyGiftPayout int64 `json:"lucky_gift_payout"`
|
||||
LuckyGiftPayers int64 `json:"lucky_gift_payers"`
|
||||
LuckyGiftProfit int64 `json:"lucky_gift_profit"`
|
||||
LuckyGiftPayoutRate float64 `json:"lucky_gift_payout_rate"`
|
||||
LuckyGiftProfitRate float64 `json:"lucky_gift_profit_rate"`
|
||||
GameTurnover int64 `json:"game_turnover"`
|
||||
GamePayout int64 `json:"game_payout"`
|
||||
GameRefund int64 `json:"game_refund"`
|
||||
GamePlayers int64 `json:"game_players"`
|
||||
GameProfit int64 `json:"game_profit"`
|
||||
GameProfitRate float64 `json:"game_profit_rate"`
|
||||
ARPUUSDMinor int64 `json:"arpu_usd_minor"`
|
||||
ARPPUUSDMinor int64 `json:"arppu_usd_minor"`
|
||||
UPValueUSDMinor int64 `json:"up_value_usd_minor"`
|
||||
Retention Retention `json:"retention"`
|
||||
GameRanking []GameRank `json:"game_ranking"`
|
||||
AppCode string `json:"app_code"`
|
||||
StartMS int64 `json:"start_ms"`
|
||||
EndMS int64 `json:"end_ms"`
|
||||
CountryID int64 `json:"country_id"`
|
||||
NewUsers int64 `json:"new_users"`
|
||||
ActiveUsers int64 `json:"active_users"`
|
||||
PaidUsers int64 `json:"paid_users"`
|
||||
RechargeUSDMinor int64 `json:"recharge_usd_minor"`
|
||||
NewUserRechargeUSDMinor int64 `json:"new_user_recharge_usd_minor"`
|
||||
CoinSellerRechargeUSDMinor int64 `json:"coin_seller_recharge_usd_minor"`
|
||||
MifaPayRechargeUSDMinor int64 `json:"mifapay_recharge_usd_minor"`
|
||||
GoogleRechargeUSDMinor int64 `json:"google_recharge_usd_minor"`
|
||||
GiftCoinSpent int64 `json:"gift_coin_spent"`
|
||||
LuckyGiftTurnover int64 `json:"lucky_gift_turnover"`
|
||||
LuckyGiftPayout int64 `json:"lucky_gift_payout"`
|
||||
LuckyGiftPayers int64 `json:"lucky_gift_payers"`
|
||||
LuckyGiftProfit int64 `json:"lucky_gift_profit"`
|
||||
LuckyGiftPayoutRate float64 `json:"lucky_gift_payout_rate"`
|
||||
LuckyGiftProfitRate float64 `json:"lucky_gift_profit_rate"`
|
||||
GameTurnover int64 `json:"game_turnover"`
|
||||
GamePayout int64 `json:"game_payout"`
|
||||
GameRefund int64 `json:"game_refund"`
|
||||
GamePlayers int64 `json:"game_players"`
|
||||
GameProfit int64 `json:"game_profit"`
|
||||
GameProfitRate float64 `json:"game_profit_rate"`
|
||||
ARPUUSDMinor int64 `json:"arpu_usd_minor"`
|
||||
ARPPUUSDMinor int64 `json:"arppu_usd_minor"`
|
||||
UPValueUSDMinor int64 `json:"up_value_usd_minor"`
|
||||
Retention Retention `json:"retention"`
|
||||
GameRanking []GameRank `json:"game_ranking"`
|
||||
LuckyGiftPools []LuckyGiftPoolStat `json:"lucky_gift_pools"`
|
||||
}
|
||||
|
||||
type Retention struct {
|
||||
@ -69,6 +70,15 @@ type GameRank struct {
|
||||
ProfitRate float64 `json:"profit_rate"`
|
||||
}
|
||||
|
||||
type LuckyGiftPoolStat struct {
|
||||
PoolID string `json:"pool_id"`
|
||||
TurnoverCoin int64 `json:"turnover_coin"`
|
||||
PayoutCoin int64 `json:"payout_coin"`
|
||||
ProfitCoin int64 `json:"profit_coin"`
|
||||
PayoutRate float64 `json:"payout_rate"`
|
||||
ProfitRate float64 `json:"profit_rate"`
|
||||
}
|
||||
|
||||
func (r *Repository) QueryOverview(ctx context.Context, query OverviewQuery) (Overview, error) {
|
||||
app := appcode.Normalize(query.AppCode)
|
||||
if app == "" {
|
||||
@ -115,6 +125,12 @@ func (r *Repository) QueryOverview(ctx context.Context, query OverviewQuery) (Ov
|
||||
return Overview{}, err
|
||||
}
|
||||
overview.GameRanking = ranking
|
||||
// Pool 明细和顶部汇总来自同一个统计窗口;Databi 用它来下钻展示每个 pool_id 的流水、返奖和利润。
|
||||
pools, err := r.queryLuckyGiftPools(ctx, app, startDay, endDay, query.CountryID)
|
||||
if err != nil {
|
||||
return Overview{}, err
|
||||
}
|
||||
overview.LuckyGiftPools = pools
|
||||
return overview, nil
|
||||
}
|
||||
|
||||
@ -174,6 +190,37 @@ func (r *Repository) queryGameRanking(ctx context.Context, app, startDay, endDay
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (r *Repository) queryLuckyGiftPools(ctx context.Context, app, startDay, endDay string, countryID int64) ([]LuckyGiftPoolStat, error) {
|
||||
args := []any{app, startDay, endDay}
|
||||
filter := ""
|
||||
if countryID > 0 {
|
||||
filter = " AND country_id = ?"
|
||||
args = append(args, countryID)
|
||||
}
|
||||
rows, err := r.db.QueryContext(ctx, `
|
||||
SELECT pool_id, COALESCE(SUM(turnover_coin),0), COALESCE(SUM(payout_coin),0)
|
||||
FROM stat_lucky_gift_pool_day_country
|
||||
WHERE app_code = ? AND stat_day BETWEEN ? AND ?`+filter+`
|
||||
GROUP BY pool_id
|
||||
ORDER BY COALESCE(SUM(turnover_coin),0) DESC, pool_id ASC`, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
out := []LuckyGiftPoolStat{}
|
||||
for rows.Next() {
|
||||
var item LuckyGiftPoolStat
|
||||
if err := rows.Scan(&item.PoolID, &item.TurnoverCoin, &item.PayoutCoin); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item.ProfitCoin = item.TurnoverCoin - item.PayoutCoin
|
||||
item.PayoutRate = ratio(item.PayoutCoin, item.TurnoverCoin)
|
||||
item.ProfitRate = ratio(item.ProfitCoin, item.TurnoverCoin)
|
||||
out = append(out, item)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func normalizeRange(startMS, endMS int64) (int64, int64) {
|
||||
if startMS <= 0 {
|
||||
now := time.Now().UTC()
|
||||
|
||||
@ -102,6 +102,13 @@ func (r *Repository) Migrate(ctx context.Context) error {
|
||||
PRIMARY KEY (app_code, stat_day, user_id),
|
||||
KEY idx_stat_lucky_payer_country (app_code, stat_day, country_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
|
||||
`CREATE TABLE IF NOT EXISTS stat_lucky_gift_pool_day_country (
|
||||
app_code VARCHAR(32) NOT NULL, stat_day DATE NOT NULL, country_id BIGINT NOT NULL DEFAULT 0,
|
||||
pool_id VARCHAR(96) NOT NULL, turnover_coin BIGINT NOT NULL DEFAULT 0, payout_coin BIGINT NOT NULL DEFAULT 0,
|
||||
updated_at_ms BIGINT NOT NULL,
|
||||
PRIMARY KEY (app_code, stat_day, country_id, pool_id),
|
||||
KEY idx_stat_lucky_pool_overview (app_code, stat_day, pool_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
|
||||
`CREATE TABLE IF NOT EXISTS stat_game_day_country (
|
||||
app_code VARCHAR(32) NOT NULL, stat_day DATE NOT NULL, country_id BIGINT NOT NULL DEFAULT 0,
|
||||
platform_code VARCHAR(64) NOT NULL DEFAULT '', game_id VARCHAR(96) NOT NULL,
|
||||
@ -171,6 +178,7 @@ type LuckyGiftRewardEvent struct {
|
||||
EventType string
|
||||
UserID int64
|
||||
CountryID int64
|
||||
PoolID string
|
||||
Amount int64
|
||||
OccurredAtMS int64
|
||||
}
|
||||
@ -268,7 +276,8 @@ func (r *Repository) ConsumeRoomGift(ctx context.Context, event RoomGiftEvent) e
|
||||
return err
|
||||
}
|
||||
luckyTurnover, luckyPayers := int64(0), int64(0)
|
||||
if strings.TrimSpace(event.PoolID) != "" {
|
||||
poolID := strings.TrimSpace(event.PoolID)
|
||||
if poolID != "" {
|
||||
luckyTurnover = event.GiftValue
|
||||
affected, err := insertUnique(ctx, tx, `
|
||||
INSERT IGNORE INTO stat_lucky_gift_day_payers (app_code, stat_day, country_id, user_id, first_paid_at_ms)
|
||||
@ -278,6 +287,17 @@ func (r *Repository) ConsumeRoomGift(ctx context.Context, event RoomGiftEvent) e
|
||||
return err
|
||||
}
|
||||
luckyPayers = affected
|
||||
// Pool 明细是 Databi 点击下钻的真实口径:每笔带 pool_id 的幸运礼物送礼先按 UTC 日、国家和奖池累加流水,返奖事件稍后用相同维度补齐 payout。
|
||||
if err := ensureLuckyGiftPoolDay(ctx, tx, event.AppCode, day, countryID, poolID, nowMS); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, `
|
||||
UPDATE stat_lucky_gift_pool_day_country
|
||||
SET turnover_coin = turnover_coin + ?, updated_at_ms = ?
|
||||
WHERE app_code = ? AND stat_day = ? AND country_id = ? AND pool_id = ?
|
||||
`, luckyTurnover, nowMS, appcode.Normalize(event.AppCode), day, countryID, poolID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
_, err := tx.ExecContext(ctx, `
|
||||
UPDATE stat_app_day_country
|
||||
@ -296,6 +316,20 @@ func (r *Repository) ConsumeLuckyGiftReward(ctx context.Context, event LuckyGift
|
||||
if err := ensureAppDay(ctx, tx, event.AppCode, day, countryID, nowMS); err != nil {
|
||||
return err
|
||||
}
|
||||
poolID := strings.TrimSpace(event.PoolID)
|
||||
if poolID != "" {
|
||||
// 钱包返奖事件带回 pool_id 后,按同一 UTC 日和国家写入奖池返奖;利润不落表,查询时用 turnover-payout 实时计算,避免双写漂移。
|
||||
if err := ensureLuckyGiftPoolDay(ctx, tx, event.AppCode, day, countryID, poolID, nowMS); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, `
|
||||
UPDATE stat_lucky_gift_pool_day_country
|
||||
SET payout_coin = payout_coin + ?, updated_at_ms = ?
|
||||
WHERE app_code = ? AND stat_day = ? AND country_id = ? AND pool_id = ?
|
||||
`, event.Amount, nowMS, appcode.Normalize(event.AppCode), day, countryID, poolID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
_, err := tx.ExecContext(ctx, `
|
||||
UPDATE stat_app_day_country
|
||||
SET lucky_gift_payout = lucky_gift_payout + ?, updated_at_ms = ?
|
||||
@ -470,6 +504,14 @@ func ensureGameDay(ctx context.Context, tx *sql.Tx, app string, day string, coun
|
||||
return err
|
||||
}
|
||||
|
||||
func ensureLuckyGiftPoolDay(ctx context.Context, tx *sql.Tx, app string, day string, countryID int64, poolID string, nowMS int64) error {
|
||||
_, err := tx.ExecContext(ctx, `
|
||||
INSERT IGNORE INTO stat_lucky_gift_pool_day_country (app_code, stat_day, country_id, pool_id, updated_at_ms)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`, appcode.Normalize(app), day, countryID, strings.TrimSpace(poolID), nowMS)
|
||||
return err
|
||||
}
|
||||
|
||||
func insertUnique(ctx context.Context, tx *sql.Tx, query string, args ...any) (int64, error) {
|
||||
result, err := tx.ExecContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user