// Package gamemq defines RocketMQ payloads owned by game-service. package gamemq import ( "encoding/json" "errors" "strings" ) const ( MessageTypeGameOutboxEvent = "game_outbox_event" TagGameOutboxEvent = "game_outbox_event" EventTypeGameOrderSettled = "GameOrderSettled" EventTypeSelfGameMatchCreated = "SelfGameMatchCreated" EventTypeSelfGameMatchReady = "SelfGameMatchReady" EventTypeSelfGameMatchSettled = "SelfGameMatchSettled" EventTypeSelfGameMatchCanceled = "SelfGameMatchCanceled" EventTypeSelfGameMatchFailed = "SelfGameMatchFailed" EventTypeSelfGamePoolAdjusted = "SelfGamePoolAdjusted" ) // GameOutboxMessage is the MQ representation of one committed game_outbox fact. type GameOutboxMessage struct { MessageType string `json:"message_type"` AppCode string `json:"app_code"` EventID string `json:"event_id"` EventType string `json:"event_type"` OrderID string `json:"order_id"` UserID int64 `json:"user_id"` PlatformCode string `json:"platform_code"` GameID string `json:"game_id"` OpType string `json:"op_type"` CoinAmount int64 `json:"coin_amount"` PayloadJSON string `json:"payload_json"` OccurredAtMS int64 `json:"occurred_at_ms"` } // EncodeGameOutboxMessage serializes a game fact for fanout through RocketMQ. func EncodeGameOutboxMessage(message GameOutboxMessage) ([]byte, error) { message.MessageType = MessageTypeGameOutboxEvent if err := validateGameOutboxMessage(message); err != nil { return nil, err } if strings.TrimSpace(message.PayloadJSON) == "" { message.PayloadJSON = "{}" } return json.Marshal(message) } // DecodeGameOutboxMessage validates the MQ body and restores the game fact. func DecodeGameOutboxMessage(body []byte) (GameOutboxMessage, error) { var message GameOutboxMessage if err := json.Unmarshal(body, &message); err != nil { return GameOutboxMessage{}, err } if message.MessageType != MessageTypeGameOutboxEvent { return GameOutboxMessage{}, errors.New("unexpected game outbox message_type") } if err := validateGameOutboxMessage(message); err != nil { return GameOutboxMessage{}, err } if strings.TrimSpace(message.PayloadJSON) == "" { message.PayloadJSON = "{}" } return message, nil } func validateGameOutboxMessage(message GameOutboxMessage) error { if strings.TrimSpace(message.AppCode) == "" || strings.TrimSpace(message.EventID) == "" || strings.TrimSpace(message.EventType) == "" || strings.TrimSpace(message.GameID) == "" || message.OccurredAtMS <= 0 { return errors.New("game outbox message is incomplete") } if message.EventType == EventTypeGameOrderSettled && (strings.TrimSpace(message.OrderID) == "" || message.UserID <= 0 || strings.TrimSpace(message.OpType) == "" || message.CoinAmount < 0) { // 厂商可能发送 0 金额派奖或 0 金额结算确认;这类订单是已落库事实,必须允许 MQ 投递。 // 负数仍然不是合法订单金额,扣减方向必须由 op_type=debit 表达,不能把符号塞进金额字段。 return errors.New("game order outbox message is incomplete") } return nil }