package app import ( "encoding/json" "strings" "hyapp/pkg/appcode" "hyapp/pkg/walletmq" broadcastdomain "hyapp/services/activity-service/internal/domain/broadcast" cumulativerechargedomain "hyapp/services/activity-service/internal/domain/cumulativerecharge" firstrechargedomain "hyapp/services/activity-service/internal/domain/firstrecharge" ) func firstRechargeEventFromWalletMessage(body []byte) (firstrechargedomain.RechargeEvent, bool, error) { message, err := walletmq.DecodeWalletOutboxMessage(body) if err != nil { return firstrechargedomain.RechargeEvent{}, false, err } if message.EventType != firstrechargedomain.RechargeEventWalletRecorded { return firstrechargedomain.RechargeEvent{}, false, nil } sequence, rechargeType := rechargeFactFromWalletPayload(message.PayloadJSON) return firstrechargedomain.RechargeEvent{ AppCode: appcode.Normalize(message.AppCode), EventID: message.EventID, EventType: message.EventType, TransactionID: message.TransactionID, CommandID: message.CommandID, UserID: message.UserID, RechargeCoinAmount: message.AvailableDelta, RechargeSequence: sequence, RechargeType: rechargeType, PayloadJSON: message.PayloadJSON, OccurredAtMS: message.OccurredAtMS, }, true, nil } func cumulativeRechargeEventFromWalletMessage(body []byte) (cumulativerechargedomain.RechargeEvent, bool, error) { message, err := walletmq.DecodeWalletOutboxMessage(body) if err != nil { return cumulativerechargedomain.RechargeEvent{}, false, err } if message.EventType != cumulativerechargedomain.RechargeEventWalletRecorded { return cumulativerechargedomain.RechargeEvent{}, false, nil } // WalletRechargeRecorded 是累充唯一事实源;payload 里没有可折算 USD 金额时直接跳过,避免把普通钱包流水计入活动。 fields := cumulativeRechargeFactFromWalletPayload(message.PayloadJSON, message.AvailableDelta) if fields.qualifyingUSDMinor <= 0 { return cumulativerechargedomain.RechargeEvent{}, false, nil } return cumulativerechargedomain.RechargeEvent{ AppCode: appcode.Normalize(message.AppCode), EventID: message.EventID, EventType: message.EventType, TransactionID: message.TransactionID, CommandID: message.CommandID, UserID: message.UserID, RechargeCoinAmount: fields.coinAmount, RechargeSequence: fields.sequence, RechargeType: fields.rechargeType, QualifyingUSDMinor: fields.qualifyingUSDMinor, PayloadJSON: message.PayloadJSON, OccurredAtMS: message.OccurredAtMS, }, true, nil } func redPacketEventFromWalletMessage(body []byte) (broadcastdomain.RedPacketWalletEvent, bool, error) { message, err := walletmq.DecodeWalletOutboxMessage(body) if err != nil { return broadcastdomain.RedPacketWalletEvent{}, false, err } if message.EventType != "WalletRedPacketCreated" && message.EventType != "WalletRedPacketClaimed" && message.EventType != "WalletRedPacketRefunded" { return broadcastdomain.RedPacketWalletEvent{}, false, nil } fields := redPacketFieldsFromWalletPayload(message.PayloadJSON, message.EventType) if fields.PacketID == "" { return broadcastdomain.RedPacketWalletEvent{}, false, nil } return broadcastdomain.RedPacketWalletEvent{ AppCode: appcode.Normalize(message.AppCode), EventID: message.EventID, EventType: message.EventType, TransactionID: message.TransactionID, CommandID: message.CommandID, PayloadJSON: message.PayloadJSON, PacketID: fields.PacketID, PacketType: fields.PacketType, RoomID: fields.RoomID, RegionID: fields.RegionID, RegionCode: fields.RegionCode, SenderUserID: fields.SenderUserID, TotalAmount: fields.TotalAmount, TotalCount: fields.TotalCount, RemainingAmount: fields.RemainingAmount, RemainingCount: fields.RemainingCount, RefundedAmount: fields.RefundedAmount, Status: fields.Status, OpenAtMS: fields.OpenAtMS, ExpiresAtMS: fields.ExpiresAtMS, CreatedAtMS: message.OccurredAtMS, }, true, nil } func rechargeFactFromWalletPayload(payload string) (int64, string) { var decoded map[string]any if err := json.Unmarshal([]byte(payload), &decoded); err != nil { return 0, firstrechargedomain.RechargeTypeCoinSeller } var sequence int64 switch value := decoded["recharge_sequence"].(type) { case float64: sequence = int64(value) case int64: sequence = value case json.Number: sequence, _ = value.Int64() } rechargeType := firstrechargedomain.RechargeTypeCoinSeller if value, ok := decoded["recharge_type"].(string); ok && value != "" { rechargeType = value } return sequence, rechargeType } type cumulativeRechargePayloadFields struct { sequence int64 rechargeType string coinAmount int64 qualifyingUSDMinor int64 } func cumulativeRechargeFactFromWalletPayload(payload string, availableDelta int64) cumulativeRechargePayloadFields { var decoded map[string]any if err := json.Unmarshal([]byte(payload), &decoded); err != nil { decoded = map[string]any{} } // 来源字段兼容 wallet 历史 payload;没有明确来源时按币商转账处理,因为币商链路通常只带金币增量。 rechargeType := firstNonEmptyString( stringFromDecoded(decoded, "recharge_type"), stringFromDecoded(decoded, "channel"), stringFromDecoded(decoded, "provider"), cumulativerechargedomain.RechargeTypeCoinSeller, ) // coinAmount 用于审计和币商折算,优先使用 payload 明细,最后回退 wallet outbox 的 available_delta。 coinAmount := firstNonZeroInt64( int64FromDecoded(decoded, "coin_amount"), int64FromDecoded(decoded, "amount"), int64FromDecoded(decoded, "available_delta"), availableDelta, ) qualifyingUSDMinor := cumulativeRechargeUSDMinorFromPayload(decoded, rechargeType, coinAmount) return cumulativeRechargePayloadFields{ sequence: int64FromDecoded(decoded, "recharge_sequence"), rechargeType: rechargeType, coinAmount: coinAmount, qualifyingUSDMinor: qualifyingUSDMinor, } } func cumulativeRechargeUSDMinorFromPayload(decoded map[string]any, rechargeType string, coinAmount int64) int64 { if isCumulativeCoinSellerRecharge(rechargeType) { // 币商给用户转账按 90000 coins = 1 USD 折算;这里返回美分并向下取整,尾差只留在原始金币字段审计。 return coinAmount * 100 / 90000 } // Google/Mifapay 充值优先使用支付侧传入的美元美分,避免用金币包配置反推造成汇率误差。 if value := firstNonZeroInt64( int64FromDecoded(decoded, "recharge_usd_minor"), int64FromDecoded(decoded, "usd_minor_amount"), int64FromDecoded(decoded, "exchange_usd_minor_amount"), int64FromDecoded(decoded, "amount_usd_minor"), ); value > 0 { return value } if isCumulativeGoogleRecharge(rechargeType) { // Google Billing 常见 amount_micro 是 USD 微单位,除以 10000 后得到美分。 return int64FromDecoded(decoded, "amount_micro") / 10000 } return 0 } func isCumulativeCoinSellerRecharge(value string) bool { switch strings.ToLower(strings.TrimSpace(value)) { case "coin_seller_transfer", "coin_seller": return true default: return false } } func isCumulativeGoogleRecharge(value string) bool { switch strings.ToLower(strings.TrimSpace(value)) { case "google", "google_play": return true default: return false } } type redPacketPayloadFields struct { PacketID string PacketType string RoomID string RegionID int64 RegionCode string SenderUserID int64 TotalAmount int64 TotalCount int32 RemainingAmount int64 RemainingCount int32 RefundedAmount int64 Status string OpenAtMS int64 ExpiresAtMS int64 } func redPacketFieldsFromWalletPayload(payload string, eventType string) redPacketPayloadFields { var decoded map[string]any if err := json.Unmarshal([]byte(payload), &decoded); err != nil { return redPacketPayloadFields{} } packetID := stringFromDecoded(decoded, "packet_id") if packetID == "" { packetID = stringFromDecoded(decoded, "packetId") } packetType := stringFromDecoded(decoded, "packet_type") roomID := stringFromDecoded(decoded, "room_id") regionID := int64FromDecoded(decoded, "region_id") status := stringFromDecoded(decoded, "status") openAtMS := int64FromDecoded(decoded, "open_at_ms") expiresAtMS := int64FromDecoded(decoded, "expires_at_ms") if eventType == "WalletRedPacketClaimed" || eventType == "WalletRedPacketRefunded" { status = stringFromDecoded(decoded, "packet_status") } totalCount := int32FromDecoded(decoded, "packet_count") remainingCount := int32FromDecoded(decoded, "remaining_count") if remainingCount == 0 && eventType == "WalletRedPacketCreated" { remainingCount = totalCount } return redPacketPayloadFields{ PacketID: packetID, PacketType: packetType, RoomID: roomID, RegionID: regionID, RegionCode: stringFromDecoded(decoded, "region_code"), SenderUserID: int64FromDecoded(decoded, "sender_user_id"), TotalAmount: int64FromDecoded(decoded, "total_amount"), TotalCount: totalCount, RemainingAmount: int64FromDecoded(decoded, "remaining_amount"), RemainingCount: remainingCount, RefundedAmount: int64FromDecoded(decoded, "refund_amount"), Status: status, OpenAtMS: openAtMS, ExpiresAtMS: expiresAtMS, } } func stringFromDecoded(decoded map[string]any, key string) string { value, _ := decoded[key].(string) return value } func firstNonEmptyString(values ...string) string { for _, value := range values { if value = strings.TrimSpace(value); value != "" { return value } } return "" } func firstNonZeroInt64(values ...int64) int64 { for _, value := range values { if value != 0 { return value } } return 0 } func int64FromDecoded(decoded map[string]any, key string) int64 { switch value := decoded[key].(type) { case float64: return int64(value) case int64: return value case json.Number: parsed, _ := value.Int64() return parsed default: return 0 } } func int32FromDecoded(decoded map[string]any, key string) int32 { return int32(int64FromDecoded(decoded, key)) }