970 lines
42 KiB
Go
970 lines
42 KiB
Go
package mysql
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"sort"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"hyapp/pkg/appcode"
|
||
)
|
||
|
||
const (
|
||
SocialSectionNewUsers = "new_users"
|
||
SocialSectionRevenue = "revenue"
|
||
SocialSectionRetention = "retention"
|
||
SocialSectionActive = "active"
|
||
SocialSectionGift = "gift"
|
||
SocialSectionGame = "game"
|
||
)
|
||
|
||
type SocialRequirementsQuery struct {
|
||
AppCode string
|
||
StatTZ string
|
||
StartMS int64
|
||
EndMS int64
|
||
CountryID int64
|
||
RegionID int64
|
||
RegionIDs []int64
|
||
Section string
|
||
UserRole string
|
||
PayerType string
|
||
}
|
||
|
||
type SocialRequirements struct {
|
||
AppCode string `json:"app_code"`
|
||
StatTZ string `json:"stat_tz"`
|
||
StartMS int64 `json:"start_ms"`
|
||
EndMS int64 `json:"end_ms"`
|
||
CountryID int64 `json:"country_id"`
|
||
RegionID int64 `json:"region_id"`
|
||
RegionIDs []int64 `json:"region_ids,omitempty"`
|
||
Sections []SocialRequirementSection `json:"sections"`
|
||
}
|
||
|
||
type SocialRequirementSection struct {
|
||
Key string `json:"key"`
|
||
Label string `json:"label"`
|
||
Filters SocialRequirementFilter `json:"filters"`
|
||
Columns []SocialRequirementColumn `json:"columns"`
|
||
MetricDefinitions []MetricDefinition `json:"metric_definitions"`
|
||
Total SocialRequirementRow `json:"total"`
|
||
DailySeries []SocialRequirementRow `json:"daily_series"`
|
||
}
|
||
|
||
type SocialRequirementColumn struct {
|
||
Key string `json:"key"`
|
||
Label string `json:"label"`
|
||
Type string `json:"type"`
|
||
Tooltip string `json:"tooltip,omitempty"`
|
||
}
|
||
|
||
type SocialRequirementFilter struct {
|
||
UserRole string `json:"user_role"`
|
||
PayerType string `json:"payer_type"`
|
||
}
|
||
|
||
type SocialRequirementRow struct {
|
||
StatDay string `json:"stat_day"`
|
||
Label string `json:"label"`
|
||
Metrics map[string]any `json:"metrics"`
|
||
}
|
||
|
||
func (row SocialRequirementRow) MarshalJSON() ([]byte, error) {
|
||
out := make(map[string]any, len(row.Metrics)+2)
|
||
out["stat_day"] = row.StatDay
|
||
out["label"] = row.Label
|
||
for key, value := range row.Metrics {
|
||
out[key] = value
|
||
}
|
||
return json.Marshal(out)
|
||
}
|
||
|
||
type socialRequirementUserDay struct {
|
||
StatDay string
|
||
Subject string
|
||
UserID int64
|
||
RegionID int64
|
||
UserRole string
|
||
AppFirstOpen int64
|
||
Registered int64
|
||
Active int64
|
||
RoomJoinOK int64
|
||
RoomJoinNG int64
|
||
MicUp int64
|
||
Gift int64
|
||
NormalGift int64
|
||
LuckyGift int64
|
||
SuperGift int64
|
||
Recharge int64
|
||
FirstRecharge int64
|
||
RepeatRecharge int64
|
||
GameBet int64
|
||
SelfGameBet int64
|
||
ProbabilityBet int64
|
||
Chat int64
|
||
FollowUser int64
|
||
FollowRoom int64
|
||
GiftPanel int64
|
||
ProbabilityPanel int64
|
||
AppStayMS int64
|
||
RoomStayMS int64
|
||
MicStayMS int64
|
||
RechargeCount int64
|
||
RechargeUSDMinor int64
|
||
GoogleRechargeUSDMinor int64
|
||
ThirdPartyRechargeUSDMinor int64
|
||
CoinSellerRechargeUSDMinor int64
|
||
FirstRechargeUSDMinor int64
|
||
RepeatRechargeUSDMinor int64
|
||
NormalGiftCoin int64
|
||
LuckyGiftCoin int64
|
||
SuperGiftCoin int64
|
||
LuckyGiftPayoutCoin int64
|
||
SuperGiftPayoutCoin int64
|
||
GameBetCoin int64
|
||
GameBetCount int64
|
||
SelfGameBetCoin int64
|
||
SelfGameBetCount int64
|
||
ProbabilityBetCoin int64
|
||
ProbabilityBetCount int64
|
||
OtherConsumeCoin int64
|
||
OutputCoin int64
|
||
}
|
||
|
||
type socialRequirementAggregate struct {
|
||
AppFirstOpen int64
|
||
Registered int64
|
||
NewHostUsers int64
|
||
NewNormalUsers int64
|
||
Active int64
|
||
RoomJoinOK int64
|
||
RoomJoinNG int64
|
||
RegisteredRoomJoinOK int64
|
||
RegisteredRoomJoinNG int64
|
||
RegisteredMicUp int64
|
||
RegisteredRoomGift int64
|
||
RegisteredRecharge int64
|
||
RegisteredGame int64
|
||
MicUp int64
|
||
Gift int64
|
||
NormalGift int64
|
||
LuckyGift int64
|
||
SuperGift int64
|
||
Recharge int64
|
||
FirstRecharge int64
|
||
RepeatRecharge int64
|
||
GameBet int64
|
||
SelfGameBet int64
|
||
ProbabilityBet int64
|
||
Chat int64
|
||
FollowUser int64
|
||
FollowRoom int64
|
||
GiftPanel int64
|
||
ProbabilityPanel int64
|
||
AppStayMS int64
|
||
RoomStayMS int64
|
||
MicStayMS int64
|
||
RechargeCount int64
|
||
RechargeUSDMinor int64
|
||
GoogleRechargeUSDMinor int64
|
||
ThirdPartyRechargeUSDMinor int64
|
||
CoinSellerRechargeUSDMinor int64
|
||
FirstRechargeUSDMinor int64
|
||
RepeatRechargeUSDMinor int64
|
||
NormalGiftCoin int64
|
||
LuckyGiftCoin int64
|
||
SuperGiftCoin int64
|
||
LuckyGiftPayoutCoin int64
|
||
SuperGiftPayoutCoin int64
|
||
GameBetCoin int64
|
||
GameBetCount int64
|
||
SelfGameBetCoin int64
|
||
SelfGameBetCount int64
|
||
ProbabilityBetCoin int64
|
||
ProbabilityBetCount int64
|
||
OtherConsumeCoin int64
|
||
OutputCoin int64
|
||
}
|
||
|
||
type retentionMetric struct {
|
||
Base int64
|
||
Users int64
|
||
}
|
||
|
||
func (r *Repository) QuerySocialRequirements(ctx context.Context, query SocialRequirementsQuery) (SocialRequirements, error) {
|
||
app := appcode.Normalize(query.AppCode)
|
||
if app == "" {
|
||
app = "lalu"
|
||
}
|
||
statTZ := normalizeStatTZ(query.StatTZ)
|
||
startMS, endMS := query.StartMS, query.EndMS
|
||
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()
|
||
}
|
||
startDay := statDayIn(startMS, statTZ)
|
||
endDay := statDayIn(endMS-1, statTZ)
|
||
days := socialDayRange(startDay, endDay)
|
||
// 留存指标需要读取 cohort 日之后的 D30 行;这些仍是本服务已沉淀的用户日宽表,不回查 owner service。
|
||
rows, err := r.querySocialUserDays(ctx, app, statTZ, startDay, addStatDays(endDay, 30), query.CountryID, query.RegionID, query.RegionIDs)
|
||
if err != nil {
|
||
return SocialRequirements{}, err
|
||
}
|
||
byDay, bySubjectDay := indexSocialRows(rows)
|
||
sections := socialRequestedSections(query.Section)
|
||
out := SocialRequirements{
|
||
AppCode: app,
|
||
StatTZ: statTZ,
|
||
StartMS: startMS,
|
||
EndMS: endMS,
|
||
CountryID: query.CountryID,
|
||
RegionID: query.RegionID,
|
||
RegionIDs: normalizePositiveIDs(query.RegionIDs),
|
||
Sections: make([]SocialRequirementSection, 0, len(sections)),
|
||
}
|
||
for _, section := range sections {
|
||
sectionRows := make([]SocialRequirementRow, 0, len(days))
|
||
totalAgg := socialRequirementAggregate{}
|
||
totalRetention := map[string]retentionMetric{}
|
||
for _, day := range days {
|
||
agg := aggregateSocialRows(byDay[day], section, query.UserRole, query.PayerType)
|
||
totalAgg.add(agg)
|
||
retention := socialRetentionMetrics(day, byDay[day], bySubjectDay, section, query.UserRole, query.PayerType)
|
||
mergeRetentionMetrics(totalRetention, retention)
|
||
sectionRows = append(sectionRows, SocialRequirementRow{
|
||
StatDay: day,
|
||
Label: day,
|
||
Metrics: socialSectionMetrics(section, agg, retention),
|
||
})
|
||
}
|
||
out.Sections = append(out.Sections, SocialRequirementSection{
|
||
Key: section,
|
||
Label: socialSectionLabel(section),
|
||
Filters: SocialRequirementFilter{UserRole: normalizeSocialRequirementRole(query.UserRole), PayerType: normalizeSocialPayerType(query.PayerType)},
|
||
Columns: socialRequirementColumns(section),
|
||
MetricDefinitions: socialRequirementMetricDefinitions(section),
|
||
Total: SocialRequirementRow{StatDay: "total", Label: "汇总", Metrics: socialSectionMetrics(section, totalAgg, totalRetention)},
|
||
DailySeries: sectionRows,
|
||
})
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
func (r *Repository) querySocialUserDays(ctx context.Context, app string, statTZ string, startDay string, endDay string, countryID int64, regionID int64, regionIDs []int64) ([]socialRequirementUserDay, error) {
|
||
args := []any{appcode.Normalize(app), normalizeStatTZ(statTZ), startDay, endDay}
|
||
conditions := []string{"app_code = ?", "stat_tz = ?", "stat_day BETWEEN ? AND ?"}
|
||
if countryID > 0 {
|
||
conditions = append(conditions, "country_id = ?")
|
||
args = append(args, countryID)
|
||
}
|
||
if regionID > 0 {
|
||
conditions = append(conditions, "region_id = ?")
|
||
args = append(args, regionID)
|
||
} else if ids := normalizePositiveIDs(regionIDs); len(ids) > 0 {
|
||
placeholders := make([]string, 0, len(ids))
|
||
for _, id := range ids {
|
||
placeholders = append(placeholders, "?")
|
||
args = append(args, id)
|
||
}
|
||
conditions = append(conditions, "region_id IN ("+strings.Join(placeholders, ",")+")")
|
||
}
|
||
rows, err := r.db.QueryContext(ctx, `
|
||
SELECT DATE_FORMAT(stat_day, '%Y-%m-%d'), subject_key, user_id, region_id, user_role,
|
||
app_first_open, registered_success, active_user, room_join_success, room_join_fail,
|
||
mic_up_success, gift_sent, normal_gift_sent, lucky_gift_sent, super_lucky_gift_sent,
|
||
recharge_user, first_recharge_user, repeat_recharge_user, game_bet_user, self_game_bet_user,
|
||
probability_game_bet_user, chat_sent, follow_user, follow_room, gift_panel_open, probability_game_panel_open,
|
||
app_stay_ms, room_stay_ms, mic_stay_ms, recharge_count, recharge_usd_minor,
|
||
google_recharge_usd_minor, third_party_recharge_usd_minor, coin_seller_recharge_usd_minor,
|
||
first_recharge_usd_minor, repeat_recharge_usd_minor, normal_gift_coin, lucky_gift_coin, super_lucky_gift_coin,
|
||
lucky_gift_payout_coin, super_lucky_gift_payout_coin, game_bet_coin, game_bet_count,
|
||
self_game_bet_coin, self_game_bet_count, probability_game_bet_coin, probability_game_bet_count,
|
||
other_consume_coin, output_coin
|
||
FROM stat_social_user_day
|
||
WHERE `+strings.Join(conditions, " AND ")+`
|
||
ORDER BY stat_day, subject_key
|
||
`, args...)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
out := []socialRequirementUserDay{}
|
||
for rows.Next() {
|
||
var item socialRequirementUserDay
|
||
if err := rows.Scan(&item.StatDay, &item.Subject, &item.UserID, &item.RegionID, &item.UserRole,
|
||
&item.AppFirstOpen, &item.Registered, &item.Active, &item.RoomJoinOK, &item.RoomJoinNG,
|
||
&item.MicUp, &item.Gift, &item.NormalGift, &item.LuckyGift, &item.SuperGift,
|
||
&item.Recharge, &item.FirstRecharge, &item.RepeatRecharge, &item.GameBet, &item.SelfGameBet,
|
||
&item.ProbabilityBet, &item.Chat, &item.FollowUser, &item.FollowRoom, &item.GiftPanel, &item.ProbabilityPanel,
|
||
&item.AppStayMS, &item.RoomStayMS, &item.MicStayMS, &item.RechargeCount, &item.RechargeUSDMinor,
|
||
&item.GoogleRechargeUSDMinor, &item.ThirdPartyRechargeUSDMinor, &item.CoinSellerRechargeUSDMinor,
|
||
&item.FirstRechargeUSDMinor, &item.RepeatRechargeUSDMinor, &item.NormalGiftCoin, &item.LuckyGiftCoin, &item.SuperGiftCoin,
|
||
&item.LuckyGiftPayoutCoin, &item.SuperGiftPayoutCoin, &item.GameBetCoin, &item.GameBetCount,
|
||
&item.SelfGameBetCoin, &item.SelfGameBetCount, &item.ProbabilityBetCoin, &item.ProbabilityBetCount,
|
||
&item.OtherConsumeCoin, &item.OutputCoin); err != nil {
|
||
return nil, err
|
||
}
|
||
out = append(out, item)
|
||
}
|
||
return out, rows.Err()
|
||
}
|
||
|
||
func indexSocialRows(rows []socialRequirementUserDay) (map[string][]socialRequirementUserDay, map[string]map[string]socialRequirementUserDay) {
|
||
byDay := map[string][]socialRequirementUserDay{}
|
||
bySubjectDay := map[string]map[string]socialRequirementUserDay{}
|
||
for _, row := range rows {
|
||
byDay[row.StatDay] = append(byDay[row.StatDay], row)
|
||
if bySubjectDay[row.Subject] == nil {
|
||
bySubjectDay[row.Subject] = map[string]socialRequirementUserDay{}
|
||
}
|
||
bySubjectDay[row.Subject][row.StatDay] = row
|
||
}
|
||
return byDay, bySubjectDay
|
||
}
|
||
|
||
func aggregateSocialRows(rows []socialRequirementUserDay, section string, role string, payerType string) socialRequirementAggregate {
|
||
var agg socialRequirementAggregate
|
||
for _, row := range rows {
|
||
if !socialRowMatchesSection(row, section, role, payerType) {
|
||
continue
|
||
}
|
||
agg.AppFirstOpen += flagValue(row.AppFirstOpen)
|
||
agg.Registered += flagValue(row.Registered)
|
||
if flagValue(row.Registered) > 0 && normalizeSocialUserRole(row.UserRole) == "host" {
|
||
agg.NewHostUsers++
|
||
} else if flagValue(row.Registered) > 0 {
|
||
agg.NewNormalUsers++
|
||
}
|
||
agg.Active += flagValue(row.Active)
|
||
agg.RoomJoinOK += flagValue(row.RoomJoinOK)
|
||
agg.RoomJoinNG += flagValue(row.RoomJoinNG)
|
||
if row.Registered > 0 && row.RoomJoinOK > 0 {
|
||
agg.RegisteredRoomJoinOK++
|
||
}
|
||
if row.Registered > 0 && row.RoomJoinNG > 0 {
|
||
agg.RegisteredRoomJoinNG++
|
||
}
|
||
if row.Registered > 0 && row.RoomJoinOK > 0 && row.MicUp > 0 {
|
||
agg.RegisteredMicUp++
|
||
}
|
||
if row.Registered > 0 && row.RoomJoinOK > 0 && row.Gift > 0 {
|
||
agg.RegisteredRoomGift++
|
||
}
|
||
if row.Registered > 0 && row.Recharge > 0 {
|
||
agg.RegisteredRecharge++
|
||
}
|
||
if row.Registered > 0 && row.GameBet > 0 {
|
||
agg.RegisteredGame++
|
||
}
|
||
agg.MicUp += flagValue(row.MicUp)
|
||
agg.Gift += flagValue(row.Gift)
|
||
agg.NormalGift += flagValue(row.NormalGift)
|
||
agg.LuckyGift += flagValue(row.LuckyGift)
|
||
agg.SuperGift += flagValue(row.SuperGift)
|
||
agg.Recharge += flagValue(row.Recharge)
|
||
agg.FirstRecharge += flagValue(row.FirstRecharge)
|
||
agg.RepeatRecharge += flagValue(row.RepeatRecharge)
|
||
agg.GameBet += flagValue(row.GameBet)
|
||
agg.SelfGameBet += flagValue(row.SelfGameBet)
|
||
agg.ProbabilityBet += flagValue(row.ProbabilityBet)
|
||
agg.Chat += flagValue(row.Chat)
|
||
agg.FollowUser += flagValue(row.FollowUser)
|
||
agg.FollowRoom += flagValue(row.FollowRoom)
|
||
agg.GiftPanel += flagValue(row.GiftPanel)
|
||
agg.ProbabilityPanel += flagValue(row.ProbabilityPanel)
|
||
agg.AppStayMS += row.AppStayMS
|
||
agg.RoomStayMS += row.RoomStayMS
|
||
agg.MicStayMS += row.MicStayMS
|
||
agg.RechargeCount += row.RechargeCount
|
||
agg.RechargeUSDMinor += row.RechargeUSDMinor
|
||
agg.GoogleRechargeUSDMinor += row.GoogleRechargeUSDMinor
|
||
agg.ThirdPartyRechargeUSDMinor += row.ThirdPartyRechargeUSDMinor
|
||
agg.CoinSellerRechargeUSDMinor += row.CoinSellerRechargeUSDMinor
|
||
agg.FirstRechargeUSDMinor += row.FirstRechargeUSDMinor
|
||
agg.RepeatRechargeUSDMinor += row.RepeatRechargeUSDMinor
|
||
agg.NormalGiftCoin += row.NormalGiftCoin
|
||
agg.LuckyGiftCoin += row.LuckyGiftCoin
|
||
agg.SuperGiftCoin += row.SuperGiftCoin
|
||
agg.LuckyGiftPayoutCoin += row.LuckyGiftPayoutCoin
|
||
agg.SuperGiftPayoutCoin += row.SuperGiftPayoutCoin
|
||
agg.GameBetCoin += row.GameBetCoin
|
||
agg.GameBetCount += row.GameBetCount
|
||
agg.SelfGameBetCoin += row.SelfGameBetCoin
|
||
agg.SelfGameBetCount += row.SelfGameBetCount
|
||
agg.ProbabilityBetCoin += row.ProbabilityBetCoin
|
||
agg.ProbabilityBetCount += row.ProbabilityBetCount
|
||
agg.OtherConsumeCoin += row.OtherConsumeCoin
|
||
agg.OutputCoin += row.OutputCoin
|
||
}
|
||
return agg
|
||
}
|
||
|
||
func (a *socialRequirementAggregate) add(other socialRequirementAggregate) {
|
||
a.AppFirstOpen += other.AppFirstOpen
|
||
a.Registered += other.Registered
|
||
a.NewHostUsers += other.NewHostUsers
|
||
a.NewNormalUsers += other.NewNormalUsers
|
||
a.Active += other.Active
|
||
a.RoomJoinOK += other.RoomJoinOK
|
||
a.RoomJoinNG += other.RoomJoinNG
|
||
a.RegisteredRoomJoinOK += other.RegisteredRoomJoinOK
|
||
a.RegisteredRoomJoinNG += other.RegisteredRoomJoinNG
|
||
a.RegisteredMicUp += other.RegisteredMicUp
|
||
a.RegisteredRoomGift += other.RegisteredRoomGift
|
||
a.RegisteredRecharge += other.RegisteredRecharge
|
||
a.RegisteredGame += other.RegisteredGame
|
||
a.MicUp += other.MicUp
|
||
a.Gift += other.Gift
|
||
a.NormalGift += other.NormalGift
|
||
a.LuckyGift += other.LuckyGift
|
||
a.SuperGift += other.SuperGift
|
||
a.Recharge += other.Recharge
|
||
a.FirstRecharge += other.FirstRecharge
|
||
a.RepeatRecharge += other.RepeatRecharge
|
||
a.GameBet += other.GameBet
|
||
a.SelfGameBet += other.SelfGameBet
|
||
a.ProbabilityBet += other.ProbabilityBet
|
||
a.Chat += other.Chat
|
||
a.FollowUser += other.FollowUser
|
||
a.FollowRoom += other.FollowRoom
|
||
a.GiftPanel += other.GiftPanel
|
||
a.ProbabilityPanel += other.ProbabilityPanel
|
||
a.AppStayMS += other.AppStayMS
|
||
a.RoomStayMS += other.RoomStayMS
|
||
a.MicStayMS += other.MicStayMS
|
||
a.RechargeCount += other.RechargeCount
|
||
a.RechargeUSDMinor += other.RechargeUSDMinor
|
||
a.GoogleRechargeUSDMinor += other.GoogleRechargeUSDMinor
|
||
a.ThirdPartyRechargeUSDMinor += other.ThirdPartyRechargeUSDMinor
|
||
a.CoinSellerRechargeUSDMinor += other.CoinSellerRechargeUSDMinor
|
||
a.FirstRechargeUSDMinor += other.FirstRechargeUSDMinor
|
||
a.RepeatRechargeUSDMinor += other.RepeatRechargeUSDMinor
|
||
a.NormalGiftCoin += other.NormalGiftCoin
|
||
a.LuckyGiftCoin += other.LuckyGiftCoin
|
||
a.SuperGiftCoin += other.SuperGiftCoin
|
||
a.LuckyGiftPayoutCoin += other.LuckyGiftPayoutCoin
|
||
a.SuperGiftPayoutCoin += other.SuperGiftPayoutCoin
|
||
a.GameBetCoin += other.GameBetCoin
|
||
a.GameBetCount += other.GameBetCount
|
||
a.SelfGameBetCoin += other.SelfGameBetCoin
|
||
a.SelfGameBetCount += other.SelfGameBetCount
|
||
a.ProbabilityBetCoin += other.ProbabilityBetCoin
|
||
a.ProbabilityBetCount += other.ProbabilityBetCount
|
||
a.OtherConsumeCoin += other.OtherConsumeCoin
|
||
a.OutputCoin += other.OutputCoin
|
||
}
|
||
|
||
func socialRetentionMetrics(day string, rows []socialRequirementUserDay, bySubjectDay map[string]map[string]socialRequirementUserDay, section string, role string, payerType string) map[string]retentionMetric {
|
||
out := map[string]retentionMetric{}
|
||
for _, row := range rows {
|
||
if !socialRowMatchesSection(row, section, role, payerType) {
|
||
continue
|
||
}
|
||
for _, offset := range []int{1, 3, 7, 15, 30} {
|
||
if row.Registered > 0 {
|
||
addSocialRetention(out, "registered_d"+intLabel(offset)+"_active", row, bySubjectDay, offset, func(target socialRequirementUserDay) bool { return target.Active > 0 })
|
||
}
|
||
}
|
||
if row.RoomJoinOK > 0 {
|
||
addSocialRetention(out, "after_room_d1_active", row, bySubjectDay, 1, func(target socialRequirementUserDay) bool { return target.Active > 0 })
|
||
addSocialRetention(out, "after_room_d1_room", row, bySubjectDay, 1, func(target socialRequirementUserDay) bool { return target.RoomJoinOK > 0 })
|
||
}
|
||
if row.MicUp > 0 {
|
||
addSocialRetention(out, "after_mic_d1_active", row, bySubjectDay, 1, func(target socialRequirementUserDay) bool { return target.Active > 0 })
|
||
addSocialRetention(out, "after_mic_d1_mic", row, bySubjectDay, 1, func(target socialRequirementUserDay) bool { return target.MicUp > 0 })
|
||
}
|
||
if row.Gift > 0 {
|
||
addSocialRetention(out, "after_gift_d1_active", row, bySubjectDay, 1, func(target socialRequirementUserDay) bool { return target.Active > 0 })
|
||
addSocialRetention(out, "after_gift_d1_gift", row, bySubjectDay, 1, func(target socialRequirementUserDay) bool { return target.Gift > 0 })
|
||
}
|
||
if row.GameBet > 0 {
|
||
addSocialRetention(out, "after_game_d1_active", row, bySubjectDay, 1, func(target socialRequirementUserDay) bool { return target.Active > 0 })
|
||
addSocialRetention(out, "after_game_d1_game", row, bySubjectDay, 1, func(target socialRequirementUserDay) bool { return target.GameBet > 0 })
|
||
}
|
||
if row.Recharge > 0 {
|
||
addSocialRetention(out, "after_recharge_d1_active", row, bySubjectDay, 1, func(target socialRequirementUserDay) bool { return target.Active > 0 })
|
||
addSocialRetention(out, "after_recharge_d1_recharge", row, bySubjectDay, 1, func(target socialRequirementUserDay) bool { return target.Recharge > 0 })
|
||
}
|
||
if row.Registered > 0 && normalizeSocialUserRole(row.UserRole) == "host" {
|
||
addSocialRetention(out, "new_host_d1_active", row, bySubjectDay, 1, func(target socialRequirementUserDay) bool { return target.Active > 0 })
|
||
}
|
||
if row.Registered > 0 && normalizeSocialUserRole(row.UserRole) != "host" {
|
||
addSocialRetention(out, "new_user_d1_active", row, bySubjectDay, 1, func(target socialRequirementUserDay) bool { return target.Active > 0 })
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
func addSocialRetention(out map[string]retentionMetric, key string, base socialRequirementUserDay, bySubjectDay map[string]map[string]socialRequirementUserDay, offset int, retained func(socialRequirementUserDay) bool) {
|
||
item := out[key]
|
||
item.Base++
|
||
if target, ok := bySubjectDay[base.Subject][addStatDays(base.StatDay, offset)]; ok && retained(target) {
|
||
item.Users++
|
||
}
|
||
out[key] = item
|
||
}
|
||
|
||
func mergeRetentionMetrics(total map[string]retentionMetric, next map[string]retentionMetric) {
|
||
for key, item := range next {
|
||
current := total[key]
|
||
current.Base += item.Base
|
||
current.Users += item.Users
|
||
total[key] = current
|
||
}
|
||
}
|
||
|
||
func socialSectionMetrics(section string, agg socialRequirementAggregate, retention map[string]retentionMetric) map[string]any {
|
||
switch section {
|
||
case SocialSectionRevenue:
|
||
totalConsume := agg.GameBetCoin + agg.NormalGiftCoin + agg.LuckyGiftCoin + agg.SuperGiftCoin + agg.OtherConsumeCoin
|
||
ordinaryRechargeUSDMinor := agg.FirstRechargeUSDMinor + agg.RepeatRechargeUSDMinor
|
||
return map[string]any{
|
||
"recharge_usd_minor": agg.RechargeUSDMinor,
|
||
"google_recharge_usd_minor": agg.GoogleRechargeUSDMinor,
|
||
"third_party_recharge_usd_minor": agg.ThirdPartyRechargeUSDMinor,
|
||
"coin_seller_recharge_usd_minor": agg.CoinSellerRechargeUSDMinor,
|
||
"first_recharge_usd_minor": agg.FirstRechargeUSDMinor,
|
||
"repeat_recharge_usd_minor": agg.RepeatRechargeUSDMinor,
|
||
"recharge_users": agg.Recharge,
|
||
"recharge_count": agg.RechargeCount,
|
||
"first_recharge_users": agg.FirstRecharge,
|
||
"repeat_recharge_users": agg.RepeatRecharge,
|
||
"repurchase_rate": ratio(agg.RepeatRecharge, agg.Recharge),
|
||
"paid_rate": ratio(agg.Recharge, agg.Active),
|
||
"arpu_usd_minor": div(agg.RechargeUSDMinor, agg.Active),
|
||
"arppu_usd_minor": div(ordinaryRechargeUSDMinor, agg.Recharge),
|
||
"consumption_penetration_rate": ratio(agg.Gift+agg.GameBet, agg.Active),
|
||
"game_consumption_ratio": ratio(agg.GameBetCoin, totalConsume),
|
||
"lucky_gift_consumption_ratio": ratio(agg.LuckyGiftCoin+agg.SuperGiftCoin, totalConsume),
|
||
"normal_gift_consumption_ratio": ratio(agg.NormalGiftCoin, totalConsume),
|
||
"other_consumption_ratio": ratio(agg.OtherConsumeCoin, totalConsume),
|
||
}
|
||
case SocialSectionRetention:
|
||
out := map[string]any{}
|
||
for _, key := range sortedRetentionKeys(retention) {
|
||
item := retention[key]
|
||
out[key+"_base_users"] = item.Base
|
||
out[key+"_users"] = item.Users
|
||
out[key+"_rate"] = ratio(item.Users, item.Base)
|
||
}
|
||
return out
|
||
case SocialSectionActive:
|
||
return map[string]any{
|
||
"active_users": agg.Active,
|
||
"room_join_success_users": agg.RoomJoinOK,
|
||
"room_join_fail_users": agg.RoomJoinNG,
|
||
"room_join_success_rate": ratio(agg.RoomJoinOK, agg.RoomJoinOK+agg.RoomJoinNG),
|
||
"room_join_fail_rate": ratio(agg.RoomJoinNG, agg.RoomJoinOK+agg.RoomJoinNG),
|
||
"mic_up_users": agg.MicUp,
|
||
"mic_up_rate": ratio(agg.MicUp, agg.RoomJoinOK),
|
||
"gift_users": agg.Gift,
|
||
"gift_rate": ratio(agg.Gift, agg.RoomJoinOK),
|
||
"game_users": agg.GameBet,
|
||
"game_conversion_rate": ratio(agg.GameBet, agg.Active),
|
||
"chat_users": agg.Chat,
|
||
"chat_conversion_rate": ratio(agg.Chat, agg.Active),
|
||
"avg_app_stay_min": minutesPerUser(agg.AppStayMS, agg.Active),
|
||
}
|
||
case SocialSectionGift:
|
||
return map[string]any{
|
||
"gift_panel_open_users": agg.GiftPanel,
|
||
"gift_panel_conversion_rate": ratio(agg.GiftPanel, agg.RoomJoinOK),
|
||
"room_gift_users": agg.Gift,
|
||
"room_gift_conversion_rate": ratio(agg.Gift, agg.RoomJoinOK),
|
||
"normal_gift_users": agg.NormalGift,
|
||
"normal_gift_coin": agg.NormalGiftCoin,
|
||
"normal_gift_arppu_coin": div(agg.NormalGiftCoin, agg.NormalGift),
|
||
"lucky_gift_users": agg.LuckyGift,
|
||
"lucky_gift_coin": agg.LuckyGiftCoin,
|
||
"lucky_gift_payout_coin": agg.LuckyGiftPayoutCoin,
|
||
"lucky_gift_rtp": ratio(agg.LuckyGiftPayoutCoin, agg.LuckyGiftCoin),
|
||
"lucky_gift_arppu_coin": div(agg.LuckyGiftCoin, agg.LuckyGift),
|
||
"super_lucky_gift_users": agg.SuperGift,
|
||
"super_lucky_gift_coin": agg.SuperGiftCoin,
|
||
"super_lucky_gift_payout_coin": agg.SuperGiftPayoutCoin,
|
||
"super_lucky_gift_rtp": ratio(agg.SuperGiftPayoutCoin, agg.SuperGiftCoin),
|
||
"super_lucky_gift_arppu_coin": div(agg.SuperGiftCoin, agg.SuperGift),
|
||
}
|
||
case SocialSectionGame:
|
||
return map[string]any{
|
||
"active_users": agg.Active,
|
||
"room_join_success_users": agg.RoomJoinOK,
|
||
"self_game_conversion_rate": ratio(agg.SelfGameBet, agg.Active),
|
||
"self_game_bet_users": agg.SelfGameBet,
|
||
"self_game_bet_count": agg.SelfGameBetCount,
|
||
"self_game_bet_count_per_user": ratio(agg.SelfGameBetCount, agg.SelfGameBet),
|
||
"self_game_bet_coin": agg.SelfGameBetCoin,
|
||
"self_game_bet_arppu_coin": div(agg.SelfGameBetCoin, agg.SelfGameBet),
|
||
"probability_game_panel_open_users": agg.ProbabilityPanel,
|
||
"probability_game_panel_conversion_rate": ratio(agg.ProbabilityPanel, agg.RoomJoinOK),
|
||
"probability_game_bet_users": agg.ProbabilityBet,
|
||
"probability_game_conversion_rate": ratio(agg.ProbabilityBet, agg.RoomJoinOK),
|
||
"probability_game_bet_count": agg.ProbabilityBetCount,
|
||
"probability_game_bet_count_per_user": ratio(agg.ProbabilityBetCount, agg.ProbabilityBet),
|
||
"probability_game_bet_coin": agg.ProbabilityBetCoin,
|
||
"probability_game_bet_arppu_coin": div(agg.ProbabilityBetCoin, agg.ProbabilityBet),
|
||
}
|
||
default:
|
||
return map[string]any{
|
||
"app_download_users": agg.AppFirstOpen,
|
||
"registered_users": agg.Registered,
|
||
"new_host_users": agg.NewHostUsers,
|
||
"new_normal_users": agg.NewNormalUsers,
|
||
"registration_success_rate": ratio(agg.Registered, agg.AppFirstOpen),
|
||
"room_join_success_users": agg.RegisteredRoomJoinOK,
|
||
"room_join_fail_users": agg.RegisteredRoomJoinNG,
|
||
"room_join_success_rate": ratio(agg.RegisteredRoomJoinOK, agg.Registered),
|
||
"room_join_fail_rate": ratio(agg.RegisteredRoomJoinNG, agg.Registered),
|
||
"mic_up_users": agg.RegisteredMicUp,
|
||
"mic_up_rate": ratio(agg.RegisteredMicUp, agg.RegisteredRoomJoinOK),
|
||
"gift_users": agg.RegisteredRoomGift,
|
||
"gift_rate": ratio(agg.RegisteredRoomGift, agg.RegisteredRoomJoinOK),
|
||
"recharge_users": agg.RegisteredRecharge,
|
||
"recharge_rate": ratio(agg.RegisteredRecharge, agg.Registered),
|
||
"game_users": agg.RegisteredGame,
|
||
"game_conversion_rate": ratio(agg.RegisteredGame, agg.Registered),
|
||
"avg_app_stay_min": minutesPerUser(agg.AppStayMS, agg.Active),
|
||
"avg_room_stay_min": minutesPerUser(agg.RoomStayMS, agg.RoomJoinOK),
|
||
"avg_mic_stay_min": minutesPerUser(agg.MicStayMS, agg.MicUp),
|
||
"new_user_recharge_usd_minor": agg.FirstRechargeUSDMinor,
|
||
"follow_user_users": agg.FollowUser,
|
||
"follow_room_users": agg.FollowRoom,
|
||
"chat_users": agg.Chat,
|
||
"new_host_d1_retention_rate": retentionRate(retention, "new_host_d1_active"),
|
||
"new_user_d1_retention_rate": retentionRate(retention, "new_user_d1_active"),
|
||
}
|
||
}
|
||
}
|
||
|
||
func socialRowMatchesSection(row socialRequirementUserDay, section string, role string, payerType string) bool {
|
||
switch section {
|
||
case SocialSectionNewUsers:
|
||
return true
|
||
case SocialSectionGift, SocialSectionGame:
|
||
return socialRoleMatches(row, role)
|
||
default:
|
||
return socialRoleMatches(row, role) && socialPayerMatches(row, payerType)
|
||
}
|
||
}
|
||
|
||
func socialRoleMatches(row socialRequirementUserDay, role string) bool {
|
||
switch normalizeSocialRequirementRole(role) {
|
||
case "host":
|
||
return normalizeSocialUserRole(row.UserRole) == "host"
|
||
case "user":
|
||
return normalizeSocialUserRole(row.UserRole) != "host"
|
||
default:
|
||
return true
|
||
}
|
||
}
|
||
|
||
func socialPayerMatches(row socialRequirementUserDay, payerType string) bool {
|
||
switch normalizeSocialPayerType(payerType) {
|
||
case "paid":
|
||
return row.Recharge > 0
|
||
case "unpaid":
|
||
return row.Recharge == 0
|
||
default:
|
||
return true
|
||
}
|
||
}
|
||
|
||
func normalizeSocialRequirementRole(value string) string {
|
||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||
case "host":
|
||
return "host"
|
||
case "user":
|
||
return "user"
|
||
default:
|
||
return "all"
|
||
}
|
||
}
|
||
|
||
func normalizeSocialPayerType(value string) string {
|
||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||
case "paid":
|
||
return "paid"
|
||
case "unpaid":
|
||
return "unpaid"
|
||
default:
|
||
return "all"
|
||
}
|
||
}
|
||
|
||
func socialRequestedSections(value string) []string {
|
||
allowed := map[string]bool{
|
||
SocialSectionNewUsers: true,
|
||
SocialSectionRevenue: true,
|
||
SocialSectionRetention: true,
|
||
SocialSectionActive: true,
|
||
SocialSectionGift: true,
|
||
SocialSectionGame: true,
|
||
}
|
||
parts := strings.Split(value, ",")
|
||
out := []string{}
|
||
for _, part := range parts {
|
||
part = strings.ToLower(strings.TrimSpace(part))
|
||
if allowed[part] {
|
||
out = append(out, part)
|
||
}
|
||
}
|
||
if len(out) > 0 {
|
||
return out
|
||
}
|
||
return []string{SocialSectionNewUsers, SocialSectionRevenue, SocialSectionRetention, SocialSectionActive, SocialSectionGift, SocialSectionGame}
|
||
}
|
||
|
||
func socialSectionLabel(section string) string {
|
||
switch section {
|
||
case SocialSectionRevenue:
|
||
return "P1 营收"
|
||
case SocialSectionRetention:
|
||
return "P2 留存"
|
||
case SocialSectionActive:
|
||
return "P2 活跃"
|
||
case SocialSectionGift:
|
||
return "P4 送礼"
|
||
case SocialSectionGame:
|
||
return "P5 游戏"
|
||
default:
|
||
return "P0 新用户"
|
||
}
|
||
}
|
||
|
||
func socialRequirementMetricDefinitions(section string) []MetricDefinition {
|
||
switch section {
|
||
case SocialSectionRevenue:
|
||
return []MetricDefinition{
|
||
{Metric: "recharge_usd_minor", Definition: "普通 WalletRechargeRecorded 用户充值和 finance verified 币商充值订单汇总,单位 USD minor。"},
|
||
{Metric: "third_party_recharge_usd_minor", Definition: "finance 工作台币商充值订单 verify_status=verified 后按 created_at_ms 计入;普通 MifaPay/V5Pay/USDT 用户充值不进入此列。"},
|
||
{Metric: "repurchase_rate", Definition: "复购充值用户 / 当日充值用户。"},
|
||
{Metric: "paid_rate", Definition: "充值用户 / DAU,支持用户身份和当日付费身份筛选。"},
|
||
{Metric: "arpu_usd_minor", Definition: "充值金额 / DAU。"},
|
||
{Metric: "arppu_usd_minor", Definition: "普通充值金额 / 普通充值用户;finance 币商订单只进入收入金额和订单次数,不污染用户付费均值。"},
|
||
}
|
||
case SocialSectionRetention:
|
||
return []MetricDefinition{
|
||
{Metric: "registered_d1_active_rate", Definition: "注册 cohort 在第 1 个统计日后的活跃留存。D3/D7/D15/D30 同理。"},
|
||
{Metric: "after_room_d1_room_rate", Definition: "进房 cohort 在次日再次进房的留存。"},
|
||
{Metric: "after_recharge_d1_recharge_rate", Definition: "充值 cohort 在次日再次充值的留存。"},
|
||
}
|
||
case SocialSectionActive:
|
||
return []MetricDefinition{
|
||
{Metric: "active_users", Definition: "服务端活跃事实和关键客户端行为汇总后的当日活跃用户。"},
|
||
{Metric: "room_join_success_rate", Definition: "进房成功用户 / 进房尝试用户。"},
|
||
{Metric: "avg_app_stay_min", Definition: "客户端 app_session 时长 / DAU,单位分钟。"},
|
||
}
|
||
case SocialSectionGift:
|
||
return []MetricDefinition{
|
||
{Metric: "gift_panel_conversion_rate", Definition: "打开礼物面板用户 / 进房成功用户。"},
|
||
{Metric: "room_gift_conversion_rate", Definition: "送礼用户 / 进房成功用户。"},
|
||
{Metric: "lucky_gift_rtp", Definition: "幸运礼物返奖 / 幸运礼物流水。"},
|
||
}
|
||
case SocialSectionGame:
|
||
return []MetricDefinition{
|
||
{Metric: "self_game_conversion_rate", Definition: "自研游戏押注用户 / DAU。"},
|
||
{Metric: "probability_game_panel_conversion_rate", Definition: "概率游戏面板打开用户 / 进房成功用户。"},
|
||
{Metric: "probability_game_bet_arppu_coin", Definition: "概率游戏押注金币 / 概率游戏押注用户。"},
|
||
}
|
||
default:
|
||
return []MetricDefinition{
|
||
{Metric: "app_download_users", Definition: "客户端 app_first_open 去重人数;不代表应用商店真实下载量。"},
|
||
{Metric: "registered_users", Definition: "UserRegistered 服务端事实去重人数。"},
|
||
{Metric: "new_user_d1_retention_rate", Definition: "新普通用户次日活跃留存。"},
|
||
}
|
||
}
|
||
}
|
||
|
||
func socialRequirementColumns(section string) []SocialRequirementColumn {
|
||
switch section {
|
||
case SocialSectionRevenue:
|
||
return []SocialRequirementColumn{
|
||
{Key: "recharge_usd_minor", Label: "充值金额", Type: "money_minor"},
|
||
{Key: "google_recharge_usd_minor", Label: "Google充值", Type: "money_minor"},
|
||
{Key: "third_party_recharge_usd_minor", Label: "三方充值", Type: "money_minor"},
|
||
{Key: "coin_seller_recharge_usd_minor", Label: "币商充值", Type: "money_minor"},
|
||
{Key: "first_recharge_usd_minor", Label: "首充金额", Type: "money_minor"},
|
||
{Key: "repeat_recharge_usd_minor", Label: "复购金额", Type: "money_minor"},
|
||
{Key: "recharge_users", Label: "充值用户", Type: "count"},
|
||
{Key: "recharge_count", Label: "充值次数", Type: "count"},
|
||
{Key: "first_recharge_users", Label: "首充用户", Type: "count"},
|
||
{Key: "repeat_recharge_users", Label: "复购用户", Type: "count"},
|
||
{Key: "repurchase_rate", Label: "复购率", Type: "ratio"},
|
||
{Key: "paid_rate", Label: "付费率", Type: "ratio"},
|
||
{Key: "arpu_usd_minor", Label: "ARPU", Type: "money_minor"},
|
||
{Key: "arppu_usd_minor", Label: "ARPPU", Type: "money_minor"},
|
||
{Key: "consumption_penetration_rate", Label: "消费渗透率", Type: "ratio"},
|
||
{Key: "game_consumption_ratio", Label: "游戏消费占比", Type: "ratio"},
|
||
{Key: "lucky_gift_consumption_ratio", Label: "幸运礼物消费占比", Type: "ratio"},
|
||
{Key: "normal_gift_consumption_ratio", Label: "普通礼物消费占比", Type: "ratio"},
|
||
{Key: "other_consumption_ratio", Label: "其他消费占比", Type: "ratio"},
|
||
}
|
||
case SocialSectionRetention:
|
||
return []SocialRequirementColumn{
|
||
{Key: "registered_d1_active_base_users", Label: "新用户D1基数", Type: "count"},
|
||
{Key: "registered_d1_active_users", Label: "新用户D1留存", Type: "count"},
|
||
{Key: "registered_d1_active_rate", Label: "新用户D1留存率", Type: "ratio"},
|
||
{Key: "registered_d3_active_rate", Label: "新用户D3留存率", Type: "ratio"},
|
||
{Key: "registered_d7_active_rate", Label: "新用户D7留存率", Type: "ratio"},
|
||
{Key: "registered_d15_active_rate", Label: "新用户D15留存率", Type: "ratio"},
|
||
{Key: "registered_d30_active_rate", Label: "新用户D30留存率", Type: "ratio"},
|
||
{Key: "after_room_d1_active_rate", Label: "进房后D1活跃留存", Type: "ratio"},
|
||
{Key: "after_room_d1_room_rate", Label: "进房后D1进房留存", Type: "ratio"},
|
||
{Key: "after_mic_d1_active_rate", Label: "上麦后D1活跃留存", Type: "ratio"},
|
||
{Key: "after_mic_d1_mic_rate", Label: "上麦后D1上麦留存", Type: "ratio"},
|
||
{Key: "after_gift_d1_active_rate", Label: "送礼后D1活跃留存", Type: "ratio"},
|
||
{Key: "after_gift_d1_gift_rate", Label: "送礼后D1送礼留存", Type: "ratio"},
|
||
{Key: "after_game_d1_active_rate", Label: "游戏后D1活跃留存", Type: "ratio"},
|
||
{Key: "after_game_d1_game_rate", Label: "游戏后D1游戏留存", Type: "ratio"},
|
||
{Key: "after_recharge_d1_active_rate", Label: "充值后D1活跃留存", Type: "ratio"},
|
||
{Key: "after_recharge_d1_recharge_rate", Label: "充值后D1充值留存", Type: "ratio"},
|
||
}
|
||
case SocialSectionActive:
|
||
return []SocialRequirementColumn{
|
||
{Key: "active_users", Label: "DAU", Type: "count"},
|
||
{Key: "room_join_success_users", Label: "进房成功用户", Type: "count"},
|
||
{Key: "room_join_fail_users", Label: "进房失败用户", Type: "count"},
|
||
{Key: "room_join_success_rate", Label: "进房成功率", Type: "ratio"},
|
||
{Key: "room_join_fail_rate", Label: "进房失败率", Type: "ratio"},
|
||
{Key: "mic_up_users", Label: "上麦用户", Type: "count"},
|
||
{Key: "mic_up_rate", Label: "上麦率", Type: "ratio"},
|
||
{Key: "gift_users", Label: "送礼用户", Type: "count"},
|
||
{Key: "gift_rate", Label: "送礼率", Type: "ratio"},
|
||
{Key: "game_users", Label: "游戏用户", Type: "count"},
|
||
{Key: "game_conversion_rate", Label: "游戏转化率", Type: "ratio"},
|
||
{Key: "chat_users", Label: "发言用户", Type: "count"},
|
||
{Key: "chat_conversion_rate", Label: "发言转化率", Type: "ratio"},
|
||
{Key: "avg_app_stay_min", Label: "人均App停留(分钟)", Type: "number"},
|
||
}
|
||
case SocialSectionGift:
|
||
return []SocialRequirementColumn{
|
||
{Key: "gift_panel_open_users", Label: "礼物面板用户", Type: "count"},
|
||
{Key: "gift_panel_conversion_rate", Label: "礼物面板转化率", Type: "ratio"},
|
||
{Key: "room_gift_users", Label: "送礼用户", Type: "count"},
|
||
{Key: "room_gift_conversion_rate", Label: "房间送礼转化率", Type: "ratio"},
|
||
{Key: "normal_gift_users", Label: "普通礼物用户", Type: "count"},
|
||
{Key: "normal_gift_coin", Label: "普通礼物流水", Type: "coin"},
|
||
{Key: "normal_gift_arppu_coin", Label: "普通礼物ARPPU", Type: "coin"},
|
||
{Key: "lucky_gift_users", Label: "幸运礼物用户", Type: "count"},
|
||
{Key: "lucky_gift_coin", Label: "幸运礼物流水", Type: "coin"},
|
||
{Key: "lucky_gift_payout_coin", Label: "幸运礼物返奖", Type: "coin"},
|
||
{Key: "lucky_gift_rtp", Label: "幸运礼物RTP", Type: "ratio"},
|
||
{Key: "lucky_gift_arppu_coin", Label: "幸运礼物ARPPU", Type: "coin"},
|
||
{Key: "super_lucky_gift_users", Label: "超级幸运用户", Type: "count"},
|
||
{Key: "super_lucky_gift_coin", Label: "超级幸运流水", Type: "coin"},
|
||
{Key: "super_lucky_gift_payout_coin", Label: "超级幸运返奖", Type: "coin"},
|
||
{Key: "super_lucky_gift_rtp", Label: "超级幸运RTP", Type: "ratio"},
|
||
{Key: "super_lucky_gift_arppu_coin", Label: "超级幸运ARPPU", Type: "coin"},
|
||
}
|
||
case SocialSectionGame:
|
||
return []SocialRequirementColumn{
|
||
{Key: "active_users", Label: "DAU", Type: "count"},
|
||
{Key: "room_join_success_users", Label: "进房成功用户", Type: "count"},
|
||
{Key: "self_game_conversion_rate", Label: "自研游戏转化率", Type: "ratio"},
|
||
{Key: "self_game_bet_users", Label: "自研押注用户", Type: "count"},
|
||
{Key: "self_game_bet_count", Label: "自研押注次数", Type: "count"},
|
||
{Key: "self_game_bet_count_per_user", Label: "自研人均押注次数", Type: "number"},
|
||
{Key: "self_game_bet_coin", Label: "自研押注金币", Type: "coin"},
|
||
{Key: "self_game_bet_arppu_coin", Label: "自研押注ARPPU", Type: "coin"},
|
||
{Key: "probability_game_panel_open_users", Label: "概率游戏面板用户", Type: "count"},
|
||
{Key: "probability_game_panel_conversion_rate", Label: "概率游戏面板转化率", Type: "ratio"},
|
||
{Key: "probability_game_bet_users", Label: "概率游戏押注用户", Type: "count"},
|
||
{Key: "probability_game_conversion_rate", Label: "概率游戏转化率", Type: "ratio"},
|
||
{Key: "probability_game_bet_count", Label: "概率游戏押注次数", Type: "count"},
|
||
{Key: "probability_game_bet_count_per_user", Label: "概率游戏人均押注次数", Type: "number"},
|
||
{Key: "probability_game_bet_coin", Label: "概率游戏押注金币", Type: "coin"},
|
||
{Key: "probability_game_bet_arppu_coin", Label: "概率游戏押注ARPPU", Type: "coin"},
|
||
}
|
||
default:
|
||
return []SocialRequirementColumn{
|
||
{Key: "app_download_users", Label: "下载APP人数", Type: "count", Tooltip: "客户端 app_first_open 去重人数"},
|
||
{Key: "registered_users", Label: "注册成功用户", Type: "count"},
|
||
{Key: "new_host_users", Label: "新主播用户", Type: "count"},
|
||
{Key: "new_normal_users", Label: "新普通用户", Type: "count"},
|
||
{Key: "registration_success_rate", Label: "注册成功率", Type: "ratio"},
|
||
{Key: "room_join_success_users", Label: "新用户进房成功", Type: "count"},
|
||
{Key: "room_join_fail_users", Label: "新用户进房失败", Type: "count"},
|
||
{Key: "room_join_success_rate", Label: "新用户进房成功率", Type: "ratio"},
|
||
{Key: "room_join_fail_rate", Label: "新用户进房失败率", Type: "ratio"},
|
||
{Key: "mic_up_users", Label: "新用户上麦", Type: "count"},
|
||
{Key: "mic_up_rate", Label: "新用户上麦率", Type: "ratio"},
|
||
{Key: "gift_users", Label: "新用户送礼", Type: "count"},
|
||
{Key: "gift_rate", Label: "新用户送礼率", Type: "ratio"},
|
||
{Key: "recharge_users", Label: "新用户充值", Type: "count"},
|
||
{Key: "recharge_rate", Label: "新用户充值率", Type: "ratio"},
|
||
{Key: "game_users", Label: "新用户游戏", Type: "count"},
|
||
{Key: "game_conversion_rate", Label: "新用户游戏转化率", Type: "ratio"},
|
||
{Key: "avg_app_stay_min", Label: "人均App停留(分钟)", Type: "number"},
|
||
{Key: "avg_room_stay_min", Label: "人均房间停留(分钟)", Type: "number"},
|
||
{Key: "avg_mic_stay_min", Label: "人均麦上停留(分钟)", Type: "number"},
|
||
{Key: "new_user_recharge_usd_minor", Label: "新用户充值金额", Type: "money_minor"},
|
||
{Key: "follow_user_users", Label: "关注用户人数", Type: "count"},
|
||
{Key: "follow_room_users", Label: "关注房间人数", Type: "count"},
|
||
{Key: "chat_users", Label: "聊天用户", Type: "count"},
|
||
{Key: "new_host_d1_retention_rate", Label: "新主播D1留存", Type: "ratio"},
|
||
{Key: "new_user_d1_retention_rate", Label: "新普通用户D1留存", Type: "ratio"},
|
||
}
|
||
}
|
||
}
|
||
|
||
func retentionRate(values map[string]retentionMetric, key string) float64 {
|
||
item := values[key]
|
||
return ratio(item.Users, item.Base)
|
||
}
|
||
|
||
func minutesPerUser(durationMS int64, users int64) float64 {
|
||
if users <= 0 {
|
||
return 0
|
||
}
|
||
return float64(durationMS) / float64(users) / 60000
|
||
}
|
||
|
||
func sortedRetentionKeys(values map[string]retentionMetric) []string {
|
||
keys := make([]string, 0, len(values))
|
||
for key := range values {
|
||
keys = append(keys, key)
|
||
}
|
||
sort.Strings(keys)
|
||
return keys
|
||
}
|
||
|
||
func intLabel(value int) string {
|
||
return strconv.Itoa(value)
|
||
}
|
||
|
||
func socialDayRange(startDay string, endDay string) []string {
|
||
start, err := time.Parse("2006-01-02", startDay)
|
||
if err != nil {
|
||
return []string{startDay}
|
||
}
|
||
end, err := time.Parse("2006-01-02", endDay)
|
||
if err != nil || end.Before(start) {
|
||
return []string{startDay}
|
||
}
|
||
days := []string{}
|
||
for current := start; !current.After(end); current = current.AddDate(0, 0, 1) {
|
||
days = append(days, current.Format("2006-01-02"))
|
||
}
|
||
return days
|
||
}
|
||
|
||
func normalizePositiveIDs(values []int64) []int64 {
|
||
seen := map[int64]bool{}
|
||
out := []int64{}
|
||
for _, value := range values {
|
||
if value <= 0 || seen[value] {
|
||
continue
|
||
}
|
||
seen[value] = true
|
||
out = append(out, value)
|
||
}
|
||
sort.Slice(out, func(i, j int) bool { return out[i] < out[j] })
|
||
return out
|
||
}
|