package mysql import ( "context" "database/sql" "strconv" "strings" "hyapp/pkg/appcode" ) type socialUserDayDelta struct { AppCode string StatTZ string StatDay string UserID int64 DeviceID string CountryID int64 RegionID int64 UserRole string UpdatedAt int64 FirstOpen 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 } func applySocialUserDayDelta(ctx context.Context, tx *sql.Tx, delta socialUserDayDelta) error { subjectKey := socialSubjectKey(delta.UserID, delta.DeviceID) if subjectKey == "" { return nil } app := appcode.Normalize(delta.AppCode) statTZ := normalizeStatTZ(delta.StatTZ) day := strings.TrimSpace(delta.StatDay) if app == "" || day == "" { return nil } countryID, regionID := normalizeDimension(delta.CountryID, delta.RegionID) role := normalizeSocialUserRole(delta.UserRole) // 这张表是 P0-P5 的唯一宽表投影。外层 withEvent 或 app_tracking_events 的 INSERT IGNORE 已经完成事件级幂等; // 这里允许同一事务内分步骤 upsert+update,保证角色和维度快照可以随任意事实补齐,但金额类字段只随已幂等的事实累加一次。 if _, err := tx.ExecContext(ctx, ` INSERT INTO stat_social_user_day ( app_code, stat_tz, stat_day, subject_key, user_id, device_id, country_id, region_id, user_role, updated_at_ms ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE user_id = CASE WHEN VALUES(user_id) > 0 THEN VALUES(user_id) ELSE user_id END, device_id = CASE WHEN VALUES(device_id) <> '' THEN VALUES(device_id) ELSE device_id END, country_id = CASE WHEN VALUES(country_id) > 0 THEN VALUES(country_id) ELSE country_id END, region_id = CASE WHEN VALUES(region_id) > 0 THEN VALUES(region_id) ELSE region_id END, user_role = CASE WHEN VALUES(user_role) = 'host' OR user_role = 'host' THEN 'host' ELSE 'user' END, updated_at_ms = VALUES(updated_at_ms) `, app, statTZ, day, subjectKey, normalizeID(delta.UserID), strings.TrimSpace(delta.DeviceID), countryID, regionID, role, delta.UpdatedAt); err != nil { return err } _, err := tx.ExecContext(ctx, ` UPDATE stat_social_user_day SET app_first_open = GREATEST(app_first_open, ?), registered_success = GREATEST(registered_success, ?), active_user = GREATEST(active_user, ?), room_join_success = GREATEST(room_join_success, ?), room_join_fail = GREATEST(room_join_fail, ?), mic_up_success = GREATEST(mic_up_success, ?), gift_sent = GREATEST(gift_sent, ?), normal_gift_sent = GREATEST(normal_gift_sent, ?), lucky_gift_sent = GREATEST(lucky_gift_sent, ?), super_lucky_gift_sent = GREATEST(super_lucky_gift_sent, ?), recharge_user = GREATEST(recharge_user, ?), first_recharge_user = GREATEST(first_recharge_user, ?), repeat_recharge_user = GREATEST(repeat_recharge_user, ?), game_bet_user = GREATEST(game_bet_user, ?), self_game_bet_user = GREATEST(self_game_bet_user, ?), probability_game_bet_user = GREATEST(probability_game_bet_user, ?), chat_sent = GREATEST(chat_sent, ?), follow_user = GREATEST(follow_user, ?), follow_room = GREATEST(follow_room, ?), gift_panel_open = GREATEST(gift_panel_open, ?), probability_game_panel_open = GREATEST(probability_game_panel_open, ?), app_stay_ms = app_stay_ms + ?, room_stay_ms = room_stay_ms + ?, mic_stay_ms = mic_stay_ms + ?, recharge_count = recharge_count + ?, recharge_usd_minor = recharge_usd_minor + ?, google_recharge_usd_minor = google_recharge_usd_minor + ?, third_party_recharge_usd_minor = third_party_recharge_usd_minor + ?, coin_seller_recharge_usd_minor = coin_seller_recharge_usd_minor + ?, first_recharge_usd_minor = first_recharge_usd_minor + ?, repeat_recharge_usd_minor = repeat_recharge_usd_minor + ?, normal_gift_coin = normal_gift_coin + ?, lucky_gift_coin = lucky_gift_coin + ?, super_lucky_gift_coin = super_lucky_gift_coin + ?, lucky_gift_payout_coin = lucky_gift_payout_coin + ?, super_lucky_gift_payout_coin = super_lucky_gift_payout_coin + ?, game_bet_coin = game_bet_coin + ?, game_bet_count = game_bet_count + ?, self_game_bet_coin = self_game_bet_coin + ?, self_game_bet_count = self_game_bet_count + ?, probability_game_bet_coin = probability_game_bet_coin + ?, probability_game_bet_count = probability_game_bet_count + ?, other_consume_coin = other_consume_coin + ?, output_coin = output_coin + ?, updated_at_ms = ? WHERE app_code = ? AND stat_tz = ? AND stat_day = ? AND subject_key = ? `, flagValue(delta.FirstOpen), flagValue(delta.Registered), flagValue(delta.Active), flagValue(delta.RoomJoinOK), flagValue(delta.RoomJoinNG), flagValue(delta.MicUp), flagValue(delta.Gift), flagValue(delta.NormalGift), flagValue(delta.LuckyGift), flagValue(delta.SuperGift), flagValue(delta.Recharge), flagValue(delta.FirstRecharge), flagValue(delta.RepeatRecharge), flagValue(delta.GameBet), flagValue(delta.SelfGameBet), flagValue(delta.ProbabilityBet), flagValue(delta.Chat), flagValue(delta.FollowUser), flagValue(delta.FollowRoom), flagValue(delta.GiftPanel), flagValue(delta.ProbabilityPanel), normalizeID(delta.AppStayMS), normalizeID(delta.RoomStayMS), normalizeID(delta.MicStayMS), normalizeID(delta.RechargeCount), normalizeID(delta.RechargeUSDMinor), normalizeID(delta.GoogleRechargeUSDMinor), normalizeID(delta.ThirdPartyRechargeUSDMinor), normalizeID(delta.CoinSellerRechargeUSDMinor), normalizeID(delta.FirstRechargeUSDMinor), normalizeID(delta.RepeatRechargeUSDMinor), normalizeID(delta.NormalGiftCoin), normalizeID(delta.LuckyGiftCoin), normalizeID(delta.SuperGiftCoin), normalizeID(delta.LuckyGiftPayoutCoin), normalizeID(delta.SuperGiftPayoutCoin), normalizeID(delta.GameBetCoin), normalizeID(delta.GameBetCount), normalizeID(delta.SelfGameBetCoin), normalizeID(delta.SelfGameBetCount), normalizeID(delta.ProbabilityBetCoin), normalizeID(delta.ProbabilityBetCount), normalizeID(delta.OtherConsumeCoin), normalizeID(delta.OutputCoin), delta.UpdatedAt, app, statTZ, day, subjectKey) return err } func socialSubjectKey(userID int64, deviceID string) string { if userID > 0 { return "u:" + strconv.FormatInt(userID, 10) } deviceID = strings.TrimSpace(deviceID) if deviceID == "" { return "" } return "d:" + deviceID } func flagValue(value int64) int64 { if value > 0 { return 1 } return 0 } func normalizeSocialUserRole(value string) string { switch strings.ToLower(strings.TrimSpace(value)) { case "host", "anchor", "owner", "room_owner", "agency", "guild", "guild_leader": return "host" default: return "user" } } func socialGiftKind(event RoomGiftEvent) (normalFlag, luckyFlag, superFlag int64, normalCoin, luckyCoin, superCoin int64) { coin := normalizeID(event.CoinSpent) if coin == 0 { coin = normalizeID(event.GiftValue) } switch realRoomRobotGiftKind(event) { case "super_lucky": return 0, 0, 1, 0, 0, coin case "lucky": return 0, 1, 0, 0, coin, 0 default: return 1, 0, 0, coin, 0, 0 } } func socialDeltaForAppTrackingEvent(event AppTrackingEvent, scope statDayScope, nowMS int64) (socialUserDayDelta, bool) { delta := socialUserDayDelta{ AppCode: event.AppCode, StatTZ: scope.tz, StatDay: scope.day, UserID: event.UserID, DeviceID: event.DeviceID, CountryID: event.CountryID, RegionID: event.RegionID, UpdatedAt: nowMS, } switch strings.ToLower(strings.TrimSpace(event.EventName)) { case "app_first_open": delta.FirstOpen = 1 delta.Active = 1 case "home_view": delta.Active = 1 case "app_session": delta.Active = 1 delta.AppStayMS = event.DurationMS case "room_join_success": delta.Active = 1 delta.RoomJoinOK = 1 case "room_join_fail": delta.Active = 1 delta.RoomJoinNG = 1 case "room_stay": delta.Active = 1 delta.RoomJoinOK = 1 delta.RoomStayMS = event.DurationMS case "mic_up": if !event.Success && strings.TrimSpace(event.ErrorCode) != "" { return socialUserDayDelta{}, false } delta.Active = 1 delta.MicUp = 1 case "send_message": if !event.Success && strings.TrimSpace(event.ErrorCode) != "" { return socialUserDayDelta{}, false } delta.Active = 1 delta.Chat = 1 case "follow_host", "add_friend": if !event.Success && strings.TrimSpace(event.ErrorCode) != "" { return socialUserDayDelta{}, false } delta.Active = 1 delta.FollowUser = 1 case "follow_room": if !event.Success && strings.TrimSpace(event.ErrorCode) != "" { return socialUserDayDelta{}, false } delta.Active = 1 delta.FollowRoom = 1 case "gift_panel_open": delta.Active = 1 delta.GiftPanel = 1 case "probability_game_panel_open": delta.Active = 1 delta.ProbabilityPanel = 1 default: return socialUserDayDelta{}, false } return delta, true }