271 lines
7.4 KiB
Go
271 lines
7.4 KiB
Go
package databi
|
||
|
||
import (
|
||
"encoding/json"
|
||
"math"
|
||
"strconv"
|
||
"strings"
|
||
)
|
||
|
||
// additiveMetricKeys 是各统计源(statistics-service / dashboard 外接源)国家行里可以线性相加的指标;
|
||
// 国家按用户归属互斥,所以计数与金额按国家求和即为区域/整体口径,比例类字段一律丢弃后重算。
|
||
var additiveMetricKeys = []string{
|
||
"new_users",
|
||
"registered_users",
|
||
"registrations",
|
||
"active_users",
|
||
"paid_users",
|
||
"new_paid_users",
|
||
"recharge_users",
|
||
"recharge_usd_minor",
|
||
"total_recharge_usd_minor",
|
||
"user_recharge_usd_minor",
|
||
"new_user_recharge_usd_minor",
|
||
"coin_seller_recharge_usd_minor",
|
||
"coin_seller_stock_coin",
|
||
"coin_seller_transfer_coin",
|
||
"new_dealer_recharge_usd_minor",
|
||
"mifapay_recharge_usd_minor",
|
||
"google_recharge_usd_minor",
|
||
"coin_total",
|
||
"consumed_coin",
|
||
"output_coin",
|
||
"platform_grant_coin",
|
||
"manual_grant_coin",
|
||
"salary_usd_minor",
|
||
"salary_exchange_usd_minor",
|
||
"salary_transfer_usd_minor",
|
||
"salary_transfer_coin",
|
||
"mic_online_ms",
|
||
"mic_online_users",
|
||
"gift_coin_spent",
|
||
"lucky_gift_turnover",
|
||
"lucky_gift_payout",
|
||
"lucky_gift_payers",
|
||
"lucky_gift_anchor_share",
|
||
"room_lucky_gift_turnover",
|
||
"game_turnover",
|
||
"game_payout",
|
||
"game_refund",
|
||
"game_players",
|
||
"super_lucky_gift_turnover",
|
||
"super_lucky_gift_payout",
|
||
"d1_retention_users",
|
||
"d1_retention_base_users",
|
||
"d7_retention_users",
|
||
"d7_retention_base_users",
|
||
"d30_retention_users",
|
||
"d30_retention_base_users",
|
||
}
|
||
|
||
type metricAccumulator struct {
|
||
values map[string]int64
|
||
present map[string]bool
|
||
}
|
||
|
||
func newMetricAccumulator() *metricAccumulator {
|
||
return &metricAccumulator{values: map[string]int64{}, present: map[string]bool{}}
|
||
}
|
||
|
||
func (acc *metricAccumulator) addRow(row map[string]any) {
|
||
if row == nil {
|
||
return
|
||
}
|
||
for _, key := range additiveMetricKeys {
|
||
raw, ok := row[key]
|
||
if !ok || raw == nil {
|
||
continue
|
||
}
|
||
value, ok := metricInt64(raw)
|
||
if !ok {
|
||
continue
|
||
}
|
||
acc.values[key] += value
|
||
acc.present[key] = true
|
||
}
|
||
}
|
||
|
||
// toMap 输出与统计源同名的指标字段:可加字段直接给累加值,派生字段(比例/均值/利润)统一重算;
|
||
// 某个字段在所有来源行里都缺失(legacy 口径未接入)时输出 nil,前端照旧显示 "--"。
|
||
func (acc *metricAccumulator) toMap() map[string]any {
|
||
out := map[string]any{}
|
||
for _, key := range additiveMetricKeys {
|
||
if acc.present[key] {
|
||
out[key] = acc.values[key]
|
||
} else {
|
||
out[key] = nil
|
||
}
|
||
}
|
||
|
||
recharge, hasRecharge := acc.value("recharge_usd_minor")
|
||
active, hasActive := acc.value("active_users")
|
||
paid, hasPaid := acc.value("paid_users")
|
||
rechargeUsers, hasRechargeUsers := acc.value("recharge_users")
|
||
|
||
out["arpu_usd_minor"] = nilUnless(hasRecharge && hasActive, divideRound(recharge, active))
|
||
out["arppu_usd_minor"] = nilUnless(hasRecharge && hasPaid, divideRound(recharge, paid))
|
||
out["payer_rate"] = nilUnless(hasPaid && hasActive, safeRatio(paid, active))
|
||
out["paid_conversion_rate"] = nilUnless(hasPaid && hasActive, safeRatio(paid, active))
|
||
out["recharge_conversion_rate"] = nilUnless(hasRechargeUsers && hasActive, safeRatio(rechargeUsers, active))
|
||
|
||
gameTurnover, hasGameTurnover := acc.value("game_turnover")
|
||
gamePayout, hasGamePayout := acc.value("game_payout")
|
||
if hasGameTurnover && hasGamePayout {
|
||
profit := gameTurnover - gamePayout
|
||
out["game_profit"] = profit
|
||
out["game_profit_rate"] = safeRatio(profit, gameTurnover)
|
||
} else {
|
||
out["game_profit"] = nil
|
||
out["game_profit_rate"] = nil
|
||
}
|
||
|
||
luckyTurnover, hasLuckyTurnover := acc.value("lucky_gift_turnover")
|
||
luckyPayout, hasLuckyPayout := acc.value("lucky_gift_payout")
|
||
if hasLuckyTurnover && hasLuckyPayout {
|
||
profit := luckyTurnover - luckyPayout
|
||
out["lucky_gift_profit"] = profit
|
||
out["lucky_gift_payout_rate"] = safeRatio(luckyPayout, luckyTurnover)
|
||
out["lucky_gift_profit_rate"] = safeRatio(profit, luckyTurnover)
|
||
} else {
|
||
out["lucky_gift_profit"] = nil
|
||
out["lucky_gift_payout_rate"] = nil
|
||
out["lucky_gift_profit_rate"] = nil
|
||
}
|
||
|
||
superTurnover, hasSuperTurnover := acc.value("super_lucky_gift_turnover")
|
||
superPayout, hasSuperPayout := acc.value("super_lucky_gift_payout")
|
||
if hasSuperTurnover && hasSuperPayout {
|
||
profit := superTurnover - superPayout
|
||
out["super_lucky_gift_profit"] = profit
|
||
out["super_lucky_gift_profit_rate"] = safeRatio(profit, superTurnover)
|
||
} else {
|
||
out["super_lucky_gift_profit"] = nil
|
||
out["super_lucky_gift_profit_rate"] = nil
|
||
}
|
||
|
||
consumed, hasConsumed := acc.value("consumed_coin")
|
||
output, hasOutput := acc.value("output_coin")
|
||
out["consume_output_ratio"] = nilUnless(hasConsumed && hasOutput, safeRatio(consumed, output))
|
||
out["consume_output_delta"] = nilUnless(hasConsumed && hasOutput, consumed-output)
|
||
|
||
micMS, hasMicMS := acc.value("mic_online_ms")
|
||
micUsers, hasMicUsers := acc.value("mic_online_users")
|
||
out["avg_mic_online_ms"] = nilUnless(hasMicMS && hasMicUsers, divideRound(micMS, micUsers))
|
||
|
||
for _, day := range []string{"d1", "d7", "d30"} {
|
||
users, hasUsers := acc.value(day + "_retention_users")
|
||
base, hasBase := acc.value(day + "_retention_base_users")
|
||
out[day+"_retention_rate"] = nilUnless(hasUsers && hasBase, safeRatio(users, base))
|
||
}
|
||
return out
|
||
}
|
||
|
||
func (acc *metricAccumulator) value(key string) (int64, bool) {
|
||
return acc.values[key], acc.present[key]
|
||
}
|
||
|
||
func nilUnless(condition bool, value any) any {
|
||
if !condition {
|
||
return nil
|
||
}
|
||
return value
|
||
}
|
||
|
||
func divideRound(numerator int64, denominator int64) int64 {
|
||
if denominator == 0 {
|
||
return 0
|
||
}
|
||
return int64(math.Round(float64(numerator) / float64(denominator)))
|
||
}
|
||
|
||
func safeRatio(numerator int64, denominator int64) float64 {
|
||
if denominator == 0 {
|
||
return 0
|
||
}
|
||
return float64(numerator) / float64(denominator)
|
||
}
|
||
|
||
func metricInt64(value any) (int64, bool) {
|
||
switch typed := value.(type) {
|
||
case nil:
|
||
return 0, false
|
||
case int64:
|
||
return typed, true
|
||
case int32:
|
||
return int64(typed), true
|
||
case int:
|
||
return int64(typed), true
|
||
case uint:
|
||
return int64(typed), true
|
||
case uint64:
|
||
return int64(typed), true
|
||
case float64:
|
||
return int64(math.Round(typed)), true
|
||
case float32:
|
||
return int64(math.Round(float64(typed))), true
|
||
case json.Number:
|
||
if parsed, err := typed.Int64(); err == nil {
|
||
return parsed, true
|
||
}
|
||
if parsed, err := typed.Float64(); err == nil {
|
||
return int64(math.Round(parsed)), true
|
||
}
|
||
return 0, false
|
||
case string:
|
||
trimmed := strings.TrimSpace(typed)
|
||
if trimmed == "" {
|
||
return 0, false
|
||
}
|
||
if parsed, err := strconv.ParseInt(trimmed, 10, 64); err == nil {
|
||
return parsed, true
|
||
}
|
||
if parsed, err := strconv.ParseFloat(trimmed, 64); err == nil {
|
||
return int64(math.Round(parsed)), true
|
||
}
|
||
return 0, false
|
||
default:
|
||
return 0, false
|
||
}
|
||
}
|
||
|
||
func anyRowSlice(value any) []map[string]any {
|
||
switch typed := value.(type) {
|
||
case []map[string]any:
|
||
return typed
|
||
case []any:
|
||
out := make([]map[string]any, 0, len(typed))
|
||
for _, item := range typed {
|
||
if row, ok := item.(map[string]any); ok {
|
||
out = append(out, row)
|
||
}
|
||
}
|
||
return out
|
||
default:
|
||
return nil
|
||
}
|
||
}
|
||
|
||
func rowString(row map[string]any, keys ...string) string {
|
||
for _, key := range keys {
|
||
if raw, ok := row[key]; ok && raw != nil {
|
||
if text, ok := raw.(string); ok {
|
||
if trimmed := strings.TrimSpace(text); trimmed != "" {
|
||
return trimmed
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func rowInt64(row map[string]any, keys ...string) (int64, bool) {
|
||
for _, key := range keys {
|
||
if raw, ok := row[key]; ok && raw != nil {
|
||
if value, ok := metricInt64(raw); ok {
|
||
return value, true
|
||
}
|
||
}
|
||
}
|
||
return 0, false
|
||
}
|