hyapp-server/pkg/walletmq/messages.go

148 lines
6.2 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 walletmq defines RocketMQ payloads owned by wallet-service.
package walletmq
import (
"encoding/json"
"errors"
"fmt"
"sort"
"strings"
)
const (
MessageTypeWalletOutboxEvent = "wallet_outbox_event"
// TagWalletOutboxEvent 是 typed tag 上线前所有钱包事实共用的 legacy Tag。
// 兼容窗口内消费者必须同时订阅它和自己需要的 EventType Tag确保 producer
// 切换前已经进入 Broker 的旧消息仍会被同一 consumer group 完整消费。
TagWalletOutboxEvent = "wallet_outbox_event"
// 以下常量只描述当前 wallet topic 消费者实际处理的事件类型。producer 使用
// outbox.event_type 作为 Tag因此消费者和消息体分派必须复用同一拼写避免
// Tag 过滤成功但 handler 因事件名漂移而静默跳过。
EventTypeWalletRechargeRecorded = "WalletRechargeRecorded"
EventTypeWalletCoinSellerStockPurchased = "WalletCoinSellerStockPurchased"
EventTypeWalletLuckyGiftRewardCredited = "WalletLuckyGiftRewardCredited"
EventTypeWalletTaskRewardCredited = "WalletTaskRewardCredited"
EventTypeWalletWheelRewardCredited = "WalletWheelRewardCredited"
EventTypeWalletRoomTurnoverRewardCredited = "WalletRoomTurnoverRewardCredited"
EventTypeWalletInviteActivityRewardCredited = "WalletInviteActivityRewardCredited"
EventTypeWalletAgencyOpeningRewardCredited = "WalletAgencyOpeningRewardCredited"
EventTypeWalletGiftIncomeCredited = "WalletGiftIncomeCredited"
EventTypeWalletBalanceChanged = "WalletBalanceChanged"
EventTypeWalletGiftDebited = "WalletGiftDebited"
EventTypeWalletRedPacketCreated = "WalletRedPacketCreated"
EventTypeWalletRedPacketClaimed = "WalletRedPacketClaimed"
EventTypeWalletRedPacketRefunded = "WalletRedPacketRefunded"
EventTypeVIPActivated = "VipActivated"
EventTypeVIPDailyCoinRebateAvailable = "VipDailyCoinRebateAvailable"
EventTypeResourceGranted = "ResourceGranted"
EventTypeResourceGroupGranted = "ResourceGroupGranted"
)
// EventTypeTag converts one committed wallet event type into its RocketMQ Tag.
// Event types are controlled identifiers rather than user input; rejecting expression
// operators and punctuation here prevents a malformed outbox row from widening a
// subscription or being published under a Tag no consumer can express safely.
func EventTypeTag(eventType string) (string, error) {
tag := strings.TrimSpace(eventType)
if tag == "" {
return "", errors.New("wallet event_type tag is required")
}
if tag == TagWalletOutboxEvent {
return "", errors.New("wallet event_type cannot reuse the legacy wallet tag")
}
for _, char := range tag {
if (char >= 'a' && char <= 'z') ||
(char >= 'A' && char <= 'Z') ||
(char >= '0' && char <= '9') ||
char == '_' || char == '-' {
continue
}
return "", fmt.Errorf("wallet event_type %q cannot be used as a RocketMQ tag", eventType)
}
return tag, nil
}
// LegacyCompatibleTagExpression builds the compatibility selector used during
// the typed-tag migration: legacy messages remain consumable, while newly published
// messages are filtered by the exact event types handled by this consumer.
// Sorting and deduplication make the expression deterministic so every process in
// the same consumer group registers byte-for-byte identical subscription metadata.
func LegacyCompatibleTagExpression(eventTypes ...string) (string, error) {
typedTags := make([]string, 0, len(eventTypes))
seen := make(map[string]struct{}, len(eventTypes))
for _, eventType := range eventTypes {
tag, err := EventTypeTag(eventType)
if err != nil {
return "", err
}
if _, exists := seen[tag]; exists {
continue
}
seen[tag] = struct{}{}
typedTags = append(typedTags, tag)
}
if len(typedTags) == 0 {
return "", errors.New("at least one typed wallet event tag is required")
}
sort.Strings(typedTags)
return TagWalletOutboxEvent + " || " + strings.Join(typedTags, " || "), nil
}
// WalletOutboxMessage is the MQ representation of one committed wallet_outbox fact.
type WalletOutboxMessage struct {
MessageType string `json:"message_type"`
AppCode string `json:"app_code"`
EventID string `json:"event_id"`
EventType string `json:"event_type"`
TransactionID string `json:"transaction_id"`
CommandID string `json:"command_id"`
UserID int64 `json:"user_id"`
AssetType string `json:"asset_type"`
AvailableDelta int64 `json:"available_delta"`
FrozenDelta int64 `json:"frozen_delta"`
PayloadJSON string `json:"payload_json"`
OccurredAtMS int64 `json:"occurred_at_ms"`
}
// EncodeWalletOutboxMessage serializes a wallet fact for fanout through RocketMQ.
func EncodeWalletOutboxMessage(message WalletOutboxMessage) ([]byte, error) {
message.MessageType = MessageTypeWalletOutboxEvent
if strings.TrimSpace(message.AppCode) == "" ||
strings.TrimSpace(message.EventID) == "" ||
strings.TrimSpace(message.EventType) == "" ||
strings.TrimSpace(message.TransactionID) == "" ||
strings.TrimSpace(message.CommandID) == "" ||
message.OccurredAtMS <= 0 {
return nil, errors.New("wallet outbox message is incomplete")
}
if strings.TrimSpace(message.PayloadJSON) == "" {
message.PayloadJSON = "{}"
}
return json.Marshal(message)
}
// DecodeWalletOutboxMessage validates the MQ body and restores the wallet fact.
func DecodeWalletOutboxMessage(body []byte) (WalletOutboxMessage, error) {
var message WalletOutboxMessage
if err := json.Unmarshal(body, &message); err != nil {
return WalletOutboxMessage{}, err
}
if message.MessageType != MessageTypeWalletOutboxEvent {
return WalletOutboxMessage{}, errors.New("unexpected wallet outbox message_type")
}
if strings.TrimSpace(message.AppCode) == "" ||
strings.TrimSpace(message.EventID) == "" ||
strings.TrimSpace(message.EventType) == "" ||
strings.TrimSpace(message.TransactionID) == "" ||
strings.TrimSpace(message.CommandID) == "" ||
message.OccurredAtMS <= 0 {
return WalletOutboxMessage{}, errors.New("wallet outbox message is incomplete")
}
if strings.TrimSpace(message.PayloadJSON) == "" {
message.PayloadJSON = "{}"
}
return message, nil
}