From 1b8241c3d2a1e6b72ef06163c25fe54393fcf158 Mon Sep 17 00:00:00 2001 From: zhx Date: Fri, 12 Jun 2026 01:34:39 +0800 Subject: [PATCH] Allow zero coin game outbox events --- pkg/gamemq/messages.go | 47 +++++++++++++------------- pkg/gamemq/messages_test.go | 67 +++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 24 deletions(-) create mode 100644 pkg/gamemq/messages_test.go diff --git a/pkg/gamemq/messages.go b/pkg/gamemq/messages.go index 2b3060cf..f9741eb6 100644 --- a/pkg/gamemq/messages.go +++ b/pkg/gamemq/messages.go @@ -40,18 +40,8 @@ type GameOutboxMessage struct { // EncodeGameOutboxMessage serializes a game fact for fanout through RocketMQ. func EncodeGameOutboxMessage(message GameOutboxMessage) ([]byte, error) { message.MessageType = MessageTypeGameOutboxEvent - if strings.TrimSpace(message.AppCode) == "" || - strings.TrimSpace(message.EventID) == "" || - strings.TrimSpace(message.EventType) == "" || - strings.TrimSpace(message.GameID) == "" || - message.OccurredAtMS <= 0 { - return nil, 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) { - return nil, errors.New("game order outbox message is incomplete") + if err := validateGameOutboxMessage(message); err != nil { + return nil, err } if strings.TrimSpace(message.PayloadJSON) == "" { message.PayloadJSON = "{}" @@ -68,21 +58,30 @@ func DecodeGameOutboxMessage(body []byte) (GameOutboxMessage, error) { if message.MessageType != MessageTypeGameOutboxEvent { return GameOutboxMessage{}, errors.New("unexpected game outbox message_type") } - if strings.TrimSpace(message.AppCode) == "" || - strings.TrimSpace(message.EventID) == "" || - strings.TrimSpace(message.EventType) == "" || - strings.TrimSpace(message.GameID) == "" || - message.OccurredAtMS <= 0 { - return GameOutboxMessage{}, 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) { - return GameOutboxMessage{}, errors.New("game order outbox message is incomplete") + 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 +} diff --git a/pkg/gamemq/messages_test.go b/pkg/gamemq/messages_test.go new file mode 100644 index 00000000..29390e41 --- /dev/null +++ b/pkg/gamemq/messages_test.go @@ -0,0 +1,67 @@ +package gamemq + +import "testing" + +func TestEncodeGameOutboxMessageAllowsZeroCoinGameOrder(t *testing.T) { + message := GameOutboxMessage{ + AppCode: "lalu", + EventID: "GameOrderSettled:gord_zero_credit", + EventType: EventTypeGameOrderSettled, + OrderID: "gord_zero_credit", + UserID: 42, + PlatformCode: "zeeone", + GameID: "1021", + OpType: "credit", + CoinAmount: 0, + OccurredAtMS: 1781199149295, + } + + body, err := EncodeGameOutboxMessage(message) + if err != nil { + t.Fatalf("zero coin game order should be publishable: %v", err) + } + decoded, err := DecodeGameOutboxMessage(body) + if err != nil { + t.Fatalf("zero coin game order should be decodable: %v", err) + } + if decoded.CoinAmount != 0 || decoded.PayloadJSON != "{}" { + t.Fatalf("decoded zero coin order mismatch: %+v", decoded) + } +} + +func TestEncodeGameOutboxMessageRejectsNegativeGameOrderAmount(t *testing.T) { + _, err := EncodeGameOutboxMessage(GameOutboxMessage{ + AppCode: "lalu", + EventID: "GameOrderSettled:gord_negative", + EventType: EventTypeGameOrderSettled, + OrderID: "gord_negative", + UserID: 42, + PlatformCode: "zeeone", + GameID: "1021", + OpType: "credit", + CoinAmount: -1, + OccurredAtMS: 1781199149295, + }) + if err == nil { + t.Fatalf("negative game order amount should be rejected") + } +} + +func TestEncodeGameOutboxMessageAllowsSelfGameEventWithoutOrderFields(t *testing.T) { + body, err := EncodeGameOutboxMessage(GameOutboxMessage{ + AppCode: "lalu", + EventID: "SelfGameMatchSettled:match_1", + EventType: EventTypeSelfGameMatchSettled, + UserID: 42, + PlatformCode: "self", + GameID: "dice", + PayloadJSON: `{"match_id":"match_1"}`, + OccurredAtMS: 1781199149295, + }) + if err != nil { + t.Fatalf("self game fact should not require order fields: %v", err) + } + if _, err := DecodeGameOutboxMessage(body); err != nil { + t.Fatalf("self game fact should decode: %v", err) + } +}