增加相关字段
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)
|
KEY idx_stat_lucky_payer_country (app_code, stat_day, country_id)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='幸运礼物日付费去重表';
|
) 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 (
|
CREATE TABLE IF NOT EXISTS stat_game_day_country (
|
||||||
app_code VARCHAR(32) NOT NULL,
|
app_code VARCHAR(32) NOT NULL,
|
||||||
stat_day DATE NOT NULL,
|
stat_day DATE NOT NULL,
|
||||||
|
|||||||
@ -257,6 +257,7 @@ func luckyGiftRewardEvent(body []byte) (mysqlstorage.LuckyGiftRewardEvent, bool,
|
|||||||
EventType: message.EventType,
|
EventType: message.EventType,
|
||||||
UserID: firstNonZeroInt64(mysqlstorage.Int64(payload, "user_id"), message.UserID),
|
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")),
|
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"),
|
Amount: mysqlstorage.Int64(payload, "amount"),
|
||||||
OccurredAtMS: message.OccurredAtMS,
|
OccurredAtMS: message.OccurredAtMS,
|
||||||
}, true, nil
|
}, 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 {
|
if overview.LuckyGiftTurnover != 1_000 || overview.LuckyGiftPayout != 300 || overview.LuckyGiftProfit != 700 || overview.LuckyGiftPayers != 1 {
|
||||||
t.Fatalf("lucky gift metrics mismatch: %+v", overview)
|
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 {
|
if overview.GameTurnover != 700 || overview.GamePayout != 200 || overview.GameProfit != 500 || overview.GamePlayers != 1 {
|
||||||
t.Fatalf("game metrics mismatch: %+v", overview)
|
t.Fatalf("game metrics mismatch: %+v", overview)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,36 +16,37 @@ type OverviewQuery struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Overview struct {
|
type Overview struct {
|
||||||
AppCode string `json:"app_code"`
|
AppCode string `json:"app_code"`
|
||||||
StartMS int64 `json:"start_ms"`
|
StartMS int64 `json:"start_ms"`
|
||||||
EndMS int64 `json:"end_ms"`
|
EndMS int64 `json:"end_ms"`
|
||||||
CountryID int64 `json:"country_id"`
|
CountryID int64 `json:"country_id"`
|
||||||
NewUsers int64 `json:"new_users"`
|
NewUsers int64 `json:"new_users"`
|
||||||
ActiveUsers int64 `json:"active_users"`
|
ActiveUsers int64 `json:"active_users"`
|
||||||
PaidUsers int64 `json:"paid_users"`
|
PaidUsers int64 `json:"paid_users"`
|
||||||
RechargeUSDMinor int64 `json:"recharge_usd_minor"`
|
RechargeUSDMinor int64 `json:"recharge_usd_minor"`
|
||||||
NewUserRechargeUSDMinor int64 `json:"new_user_recharge_usd_minor"`
|
NewUserRechargeUSDMinor int64 `json:"new_user_recharge_usd_minor"`
|
||||||
CoinSellerRechargeUSDMinor int64 `json:"coin_seller_recharge_usd_minor"`
|
CoinSellerRechargeUSDMinor int64 `json:"coin_seller_recharge_usd_minor"`
|
||||||
MifaPayRechargeUSDMinor int64 `json:"mifapay_recharge_usd_minor"`
|
MifaPayRechargeUSDMinor int64 `json:"mifapay_recharge_usd_minor"`
|
||||||
GoogleRechargeUSDMinor int64 `json:"google_recharge_usd_minor"`
|
GoogleRechargeUSDMinor int64 `json:"google_recharge_usd_minor"`
|
||||||
GiftCoinSpent int64 `json:"gift_coin_spent"`
|
GiftCoinSpent int64 `json:"gift_coin_spent"`
|
||||||
LuckyGiftTurnover int64 `json:"lucky_gift_turnover"`
|
LuckyGiftTurnover int64 `json:"lucky_gift_turnover"`
|
||||||
LuckyGiftPayout int64 `json:"lucky_gift_payout"`
|
LuckyGiftPayout int64 `json:"lucky_gift_payout"`
|
||||||
LuckyGiftPayers int64 `json:"lucky_gift_payers"`
|
LuckyGiftPayers int64 `json:"lucky_gift_payers"`
|
||||||
LuckyGiftProfit int64 `json:"lucky_gift_profit"`
|
LuckyGiftProfit int64 `json:"lucky_gift_profit"`
|
||||||
LuckyGiftPayoutRate float64 `json:"lucky_gift_payout_rate"`
|
LuckyGiftPayoutRate float64 `json:"lucky_gift_payout_rate"`
|
||||||
LuckyGiftProfitRate float64 `json:"lucky_gift_profit_rate"`
|
LuckyGiftProfitRate float64 `json:"lucky_gift_profit_rate"`
|
||||||
GameTurnover int64 `json:"game_turnover"`
|
GameTurnover int64 `json:"game_turnover"`
|
||||||
GamePayout int64 `json:"game_payout"`
|
GamePayout int64 `json:"game_payout"`
|
||||||
GameRefund int64 `json:"game_refund"`
|
GameRefund int64 `json:"game_refund"`
|
||||||
GamePlayers int64 `json:"game_players"`
|
GamePlayers int64 `json:"game_players"`
|
||||||
GameProfit int64 `json:"game_profit"`
|
GameProfit int64 `json:"game_profit"`
|
||||||
GameProfitRate float64 `json:"game_profit_rate"`
|
GameProfitRate float64 `json:"game_profit_rate"`
|
||||||
ARPUUSDMinor int64 `json:"arpu_usd_minor"`
|
ARPUUSDMinor int64 `json:"arpu_usd_minor"`
|
||||||
ARPPUUSDMinor int64 `json:"arppu_usd_minor"`
|
ARPPUUSDMinor int64 `json:"arppu_usd_minor"`
|
||||||
UPValueUSDMinor int64 `json:"up_value_usd_minor"`
|
UPValueUSDMinor int64 `json:"up_value_usd_minor"`
|
||||||
Retention Retention `json:"retention"`
|
Retention Retention `json:"retention"`
|
||||||
GameRanking []GameRank `json:"game_ranking"`
|
GameRanking []GameRank `json:"game_ranking"`
|
||||||
|
LuckyGiftPools []LuckyGiftPoolStat `json:"lucky_gift_pools"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Retention struct {
|
type Retention struct {
|
||||||
@ -69,6 +70,15 @@ type GameRank struct {
|
|||||||
ProfitRate float64 `json:"profit_rate"`
|
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) {
|
func (r *Repository) QueryOverview(ctx context.Context, query OverviewQuery) (Overview, error) {
|
||||||
app := appcode.Normalize(query.AppCode)
|
app := appcode.Normalize(query.AppCode)
|
||||||
if app == "" {
|
if app == "" {
|
||||||
@ -115,6 +125,12 @@ func (r *Repository) QueryOverview(ctx context.Context, query OverviewQuery) (Ov
|
|||||||
return Overview{}, err
|
return Overview{}, err
|
||||||
}
|
}
|
||||||
overview.GameRanking = ranking
|
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
|
return overview, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -174,6 +190,37 @@ func (r *Repository) queryGameRanking(ctx context.Context, app, startDay, endDay
|
|||||||
return out, rows.Err()
|
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) {
|
func normalizeRange(startMS, endMS int64) (int64, int64) {
|
||||||
if startMS <= 0 {
|
if startMS <= 0 {
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
|
|||||||
@ -102,6 +102,13 @@ func (r *Repository) Migrate(ctx context.Context) error {
|
|||||||
PRIMARY KEY (app_code, stat_day, user_id),
|
PRIMARY KEY (app_code, stat_day, user_id),
|
||||||
KEY idx_stat_lucky_payer_country (app_code, stat_day, country_id)
|
KEY idx_stat_lucky_payer_country (app_code, stat_day, country_id)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
|
) 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 (
|
`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,
|
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,
|
platform_code VARCHAR(64) NOT NULL DEFAULT '', game_id VARCHAR(96) NOT NULL,
|
||||||
@ -171,6 +178,7 @@ type LuckyGiftRewardEvent struct {
|
|||||||
EventType string
|
EventType string
|
||||||
UserID int64
|
UserID int64
|
||||||
CountryID int64
|
CountryID int64
|
||||||
|
PoolID string
|
||||||
Amount int64
|
Amount int64
|
||||||
OccurredAtMS int64
|
OccurredAtMS int64
|
||||||
}
|
}
|
||||||
@ -268,7 +276,8 @@ func (r *Repository) ConsumeRoomGift(ctx context.Context, event RoomGiftEvent) e
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
luckyTurnover, luckyPayers := int64(0), int64(0)
|
luckyTurnover, luckyPayers := int64(0), int64(0)
|
||||||
if strings.TrimSpace(event.PoolID) != "" {
|
poolID := strings.TrimSpace(event.PoolID)
|
||||||
|
if poolID != "" {
|
||||||
luckyTurnover = event.GiftValue
|
luckyTurnover = event.GiftValue
|
||||||
affected, err := insertUnique(ctx, tx, `
|
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)
|
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
|
return err
|
||||||
}
|
}
|
||||||
luckyPayers = affected
|
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, `
|
_, err := tx.ExecContext(ctx, `
|
||||||
UPDATE stat_app_day_country
|
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 {
|
if err := ensureAppDay(ctx, tx, event.AppCode, day, countryID, nowMS); err != nil {
|
||||||
return err
|
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, `
|
_, err := tx.ExecContext(ctx, `
|
||||||
UPDATE stat_app_day_country
|
UPDATE stat_app_day_country
|
||||||
SET lucky_gift_payout = lucky_gift_payout + ?, updated_at_ms = ?
|
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
|
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) {
|
func insertUnique(ctx context.Context, tx *sql.Tx, query string, args ...any) (int64, error) {
|
||||||
result, err := tx.ExecContext(ctx, query, args...)
|
result, err := tx.ExecContext(ctx, query, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user