2026-07-03 18:49:44 +08:00

271 lines
7.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}