package mysql import ( "context" "database/sql" "time" "hyapp/pkg/appcode" ) type OverviewQuery struct { AppCode string StartMS int64 EndMS int64 CountryID int64 } 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"` LuckyGiftPayers int64 `json:"lucky_gift_payers"` 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"` } type Retention struct { RegisteredUsers int64 `json:"registered_users"` Day1Users int64 `json:"day1_users"` Day7Users int64 `json:"day7_users"` Day30Users int64 `json:"day30_users"` Day1Rate float64 `json:"day1_rate"` Day7Rate float64 `json:"day7_rate"` Day30Rate float64 `json:"day30_rate"` } type GameRank struct { PlatformCode string `json:"platform_code"` GameID string `json:"game_id"` TurnoverCoin int64 `json:"turnover_coin"` PayoutCoin int64 `json:"payout_coin"` RefundCoin int64 `json:"refund_coin"` PlayerCount int64 `json:"player_count"` ProfitCoin int64 `json:"profit_coin"` ProfitRate float64 `json:"profit_rate"` } func (r *Repository) QueryOverview(ctx context.Context, query OverviewQuery) (Overview, error) { app := appcode.Normalize(query.AppCode) if app == "" { app = "lalu" } startDay := statDay(query.StartMS) endDay := statDay(query.EndMS - 1) args := []any{app, startDay, endDay} countryFilter := "" if query.CountryID > 0 { countryFilter = " AND country_id = ?" args = append(args, query.CountryID) } row := r.db.QueryRowContext(ctx, ` SELECT COALESCE(SUM(new_users),0), COALESCE(SUM(active_users),0), COALESCE(SUM(paid_users),0), COALESCE(SUM(recharge_usd_minor),0), COALESCE(SUM(new_user_recharge_usd_minor),0), COALESCE(SUM(coin_seller_recharge_usd_minor),0), COALESCE(SUM(mifapay_recharge_usd_minor),0), COALESCE(SUM(google_recharge_usd_minor),0), COALESCE(SUM(gift_coin_spent),0), COALESCE(SUM(lucky_gift_turnover),0), COALESCE(SUM(lucky_gift_payers),0), COALESCE(SUM(game_turnover),0), COALESCE(SUM(game_payout),0), COALESCE(SUM(game_refund),0), COALESCE(SUM(game_players),0) FROM stat_app_day_country WHERE app_code = ? AND stat_day BETWEEN ? AND ?`+countryFilter, args...) var overview Overview overview.AppCode, overview.StartMS, overview.EndMS, overview.CountryID = app, query.StartMS, query.EndMS, query.CountryID if err := row.Scan(&overview.NewUsers, &overview.ActiveUsers, &overview.PaidUsers, &overview.RechargeUSDMinor, &overview.NewUserRechargeUSDMinor, &overview.CoinSellerRechargeUSDMinor, &overview.MifaPayRechargeUSDMinor, &overview.GoogleRechargeUSDMinor, &overview.GiftCoinSpent, &overview.LuckyGiftTurnover, &overview.LuckyGiftPayers, &overview.GameTurnover, &overview.GamePayout, &overview.GameRefund, &overview.GamePlayers); err != nil { return Overview{}, err } overview.GameProfit = overview.GameTurnover - overview.GamePayout - overview.GameRefund overview.GameProfitRate = ratio(overview.GameProfit, overview.GameTurnover) overview.ARPUUSDMinor = div(overview.RechargeUSDMinor, overview.ActiveUsers) overview.ARPPUUSDMinor = div(overview.RechargeUSDMinor, overview.PaidUsers) overview.UPValueUSDMinor = overview.ARPPUUSDMinor retention, err := r.queryRetention(ctx, app, startDay, endDay, query.CountryID) if err != nil { return Overview{}, err } overview.Retention = retention ranking, err := r.queryGameRanking(ctx, app, startDay, endDay, query.CountryID) if err != nil { return Overview{}, err } overview.GameRanking = ranking return overview, nil } func (r *Repository) queryRetention(ctx context.Context, app, startDay, endDay string, countryID int64) (Retention, error) { filter := "" args := []any{app, startDay, endDay} if countryID > 0 { filter = " AND r.country_id = ?" args = append(args, countryID) } row := r.db.QueryRowContext(ctx, ` SELECT COUNT(*), COALESCE(SUM(EXISTS(SELECT 1 FROM stat_user_day_activity a WHERE a.app_code=r.app_code AND a.user_id=r.user_id AND a.stat_day=DATE_ADD(r.registered_day, INTERVAL 1 DAY))),0), COALESCE(SUM(EXISTS(SELECT 1 FROM stat_user_day_activity a WHERE a.app_code=r.app_code AND a.user_id=r.user_id AND a.stat_day=DATE_ADD(r.registered_day, INTERVAL 7 DAY))),0), COALESCE(SUM(EXISTS(SELECT 1 FROM stat_user_day_activity a WHERE a.app_code=r.app_code AND a.user_id=r.user_id AND a.stat_day=DATE_ADD(r.registered_day, INTERVAL 30 DAY))),0) FROM stat_user_registration r WHERE r.app_code = ? AND r.registered_day BETWEEN ? AND ?`+filter, args...) var out Retention if err := row.Scan(&out.RegisteredUsers, &out.Day1Users, &out.Day7Users, &out.Day30Users); err != nil && err != sql.ErrNoRows { return Retention{}, err } out.Day1Rate = ratio(out.Day1Users, out.RegisteredUsers) out.Day7Rate = ratio(out.Day7Users, out.RegisteredUsers) out.Day30Rate = ratio(out.Day30Users, out.RegisteredUsers) return out, nil } func (r *Repository) queryGameRanking(ctx context.Context, app, startDay, endDay string, countryID int64) ([]GameRank, 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 platform_code, game_id, COALESCE(SUM(turnover_coin),0), COALESCE(SUM(payout_coin),0), COALESCE(SUM(refund_coin),0), COALESCE(SUM(player_count),0) FROM stat_game_day_country WHERE app_code = ? AND stat_day BETWEEN ? AND ?`+filter+` GROUP BY platform_code, game_id ORDER BY COALESCE(SUM(turnover_coin),0) DESC LIMIT 50`, args...) if err != nil { return nil, err } defer rows.Close() out := []GameRank{} for rows.Next() { var item GameRank if err := rows.Scan(&item.PlatformCode, &item.GameID, &item.TurnoverCoin, &item.PayoutCoin, &item.RefundCoin, &item.PlayerCount); err != nil { return nil, err } item.ProfitCoin = item.TurnoverCoin - item.PayoutCoin - item.RefundCoin 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() startMS = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC).UnixMilli() } if endMS <= startMS { endMS = startMS + 24*time.Hour.Milliseconds() } return startMS, endMS } func div(numerator, denominator int64) int64 { if denominator <= 0 { return 0 } return numerator / denominator } func ratio(numerator, denominator int64) float64 { if denominator <= 0 { return 0 } return float64(numerator) / float64(denominator) }