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