148 lines
6.2 KiB
Go
148 lines
6.2 KiB
Go
// 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
|
||
}
|