2026-06-06 13:50:46 +08:00

297 lines
10 KiB
Go
Raw 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 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))
}