144 lines
5.9 KiB
Go
144 lines
5.9 KiB
Go
// Package luckygiftmq defines the RocketMQ wire contract owned by lucky-gift-service.
|
|
package luckygiftmq
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"google.golang.org/protobuf/proto"
|
|
luckygifteventsv1 "hyapp.local/api/proto/events/luckygift/v1"
|
|
)
|
|
|
|
const (
|
|
MessageTypeLuckyGiftOutboxEvent = "lucky_gift_outbox_event"
|
|
EventTypeLuckyGiftDrawn = "LuckyGiftDrawn"
|
|
TagLuckyGiftDrawn = EventTypeLuckyGiftDrawn
|
|
|
|
// LuckyGiftDisplayMinMultiplierPPM keeps room and regional top-banner policy on the same exact 10x boundary.
|
|
// The owner still publishes every granted positive reward; consumers apply this presentation threshold independently.
|
|
LuckyGiftDisplayMinMultiplierPPM int64 = 10_000_000
|
|
)
|
|
|
|
// LuckyGiftOutboxMessage is the inspectable JSON wrapper around one protobuf owner envelope.
|
|
// Metadata is duplicated outside envelope only for RocketMQ diagnostics and routing; the protobuf envelope remains authoritative.
|
|
type LuckyGiftOutboxMessage struct {
|
|
MessageType string `json:"message_type"`
|
|
AppCode string `json:"app_code"`
|
|
EventID string `json:"event_id"`
|
|
EventType string `json:"event_type"`
|
|
DrawID string `json:"draw_id"`
|
|
OccurredAtMS int64 `json:"occurred_at_ms"`
|
|
Envelope []byte `json:"envelope"`
|
|
}
|
|
|
|
// EventTypeTag validates an owner-controlled event type before it becomes a broker-side typed Tag.
|
|
// Only registered fact names are accepted so malformed outbox data cannot publish to an unconsumed routing key.
|
|
func EventTypeTag(eventType string) (string, error) {
|
|
switch tag := strings.TrimSpace(eventType); tag {
|
|
case EventTypeLuckyGiftDrawn:
|
|
return tag, nil
|
|
case "":
|
|
return "", errors.New("lucky gift event_type tag is required")
|
|
default:
|
|
return "", fmt.Errorf("unsupported lucky gift event_type tag %q", eventType)
|
|
}
|
|
}
|
|
|
|
// EncodeLuckyGiftOutboxMessage serializes the protobuf envelope without converting int64 business identifiers through JSON numbers.
|
|
func EncodeLuckyGiftOutboxMessage(envelope *luckygifteventsv1.EventEnvelope) ([]byte, error) {
|
|
if err := validateLuckyGiftEnvelope(envelope); err != nil {
|
|
return nil, err
|
|
}
|
|
envelopeBytes, err := proto.Marshal(envelope)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return json.Marshal(LuckyGiftOutboxMessage{
|
|
MessageType: MessageTypeLuckyGiftOutboxEvent,
|
|
AppCode: envelope.GetAppCode(),
|
|
EventID: envelope.GetEventId(),
|
|
EventType: envelope.GetEventType(),
|
|
DrawID: envelope.GetDrawId(),
|
|
OccurredAtMS: envelope.GetOccurredAtMs(),
|
|
Envelope: envelopeBytes,
|
|
})
|
|
}
|
|
|
|
// DecodeLuckyGiftOutboxMessage restores and validates the owner envelope.
|
|
// Wrapper/envelope equality is checked explicitly so operators never diagnose one set of visible metadata while consumers process another.
|
|
func DecodeLuckyGiftOutboxMessage(body []byte) (*luckygifteventsv1.EventEnvelope, LuckyGiftOutboxMessage, error) {
|
|
var message LuckyGiftOutboxMessage
|
|
if err := json.Unmarshal(body, &message); err != nil {
|
|
return nil, LuckyGiftOutboxMessage{}, err
|
|
}
|
|
if message.MessageType != MessageTypeLuckyGiftOutboxEvent {
|
|
return nil, LuckyGiftOutboxMessage{}, errors.New("unexpected lucky gift outbox message_type")
|
|
}
|
|
if len(message.Envelope) == 0 {
|
|
return nil, LuckyGiftOutboxMessage{}, errors.New("lucky gift outbox envelope is required")
|
|
}
|
|
var envelope luckygifteventsv1.EventEnvelope
|
|
if err := proto.Unmarshal(message.Envelope, &envelope); err != nil {
|
|
return nil, LuckyGiftOutboxMessage{}, err
|
|
}
|
|
if err := validateLuckyGiftEnvelope(&envelope); err != nil {
|
|
return nil, LuckyGiftOutboxMessage{}, err
|
|
}
|
|
if message.AppCode != envelope.GetAppCode() ||
|
|
message.EventID != envelope.GetEventId() ||
|
|
message.EventType != envelope.GetEventType() ||
|
|
message.DrawID != envelope.GetDrawId() ||
|
|
message.OccurredAtMS != envelope.GetOccurredAtMs() {
|
|
return nil, LuckyGiftOutboxMessage{}, errors.New("lucky gift outbox wrapper does not match envelope")
|
|
}
|
|
return &envelope, message, nil
|
|
}
|
|
|
|
func validateLuckyGiftEnvelope(envelope *luckygifteventsv1.EventEnvelope) error {
|
|
if envelope == nil {
|
|
return errors.New("lucky gift outbox envelope is required")
|
|
}
|
|
if strings.TrimSpace(envelope.GetAppCode()) == "" ||
|
|
strings.TrimSpace(envelope.GetEventId()) == "" ||
|
|
strings.TrimSpace(envelope.GetDrawId()) == "" ||
|
|
envelope.GetOccurredAtMs() <= 0 ||
|
|
len(envelope.GetBody()) == 0 {
|
|
return errors.New("lucky gift outbox envelope is incomplete")
|
|
}
|
|
// draw_id 是一次中奖事实的天然幂等键;固定 event_id 规则后,所有消费者都能用同一个键收敛重投,
|
|
// 也避免某个 producer 临时生成随机 ID 导致 room/activity 各自重复派生外部飘屏。
|
|
if envelope.GetEventId() != "lucky_gift_drawn:"+envelope.GetDrawId() {
|
|
return errors.New("lucky gift outbox event_id does not match draw_id")
|
|
}
|
|
if _, err := EventTypeTag(envelope.GetEventType()); err != nil {
|
|
return err
|
|
}
|
|
|
|
// The current topic carries only granted winning facts. Validate the protobuf body here so poison data fails before broker publish
|
|
// and is retried from the owner outbox instead of being acknowledged and rejected independently by every consumer.
|
|
var drawn luckygifteventsv1.LuckyGiftDrawn
|
|
if err := proto.Unmarshal(envelope.GetBody(), &drawn); err != nil {
|
|
return fmt.Errorf("decode lucky gift drawn body: %w", err)
|
|
}
|
|
if strings.TrimSpace(drawn.GetDrawId()) == "" ||
|
|
drawn.GetDrawId() != envelope.GetDrawId() ||
|
|
strings.TrimSpace(drawn.GetCommandId()) == "" ||
|
|
strings.TrimSpace(drawn.GetRoomId()) == "" ||
|
|
strings.TrimSpace(drawn.GetGiftId()) == "" ||
|
|
drawn.GetGiftCount() <= 0 ||
|
|
drawn.GetSenderUserId() <= 0 ||
|
|
drawn.GetTargetUserId() <= 0 ||
|
|
drawn.GetCoinSpent() <= 0 ||
|
|
drawn.GetMultiplierPpm() <= 0 ||
|
|
drawn.GetEffectiveRewardCoins() <= 0 ||
|
|
strings.ToLower(strings.TrimSpace(drawn.GetRewardStatus())) != "granted" ||
|
|
drawn.GetDrawCreatedAtMs() <= 0 ||
|
|
drawn.GetDrawCreatedAtMs() != envelope.GetOccurredAtMs() ||
|
|
drawn.GetRewardGrantedAtMs() <= 0 {
|
|
return errors.New("lucky gift drawn body is incomplete")
|
|
}
|
|
return nil
|
|
}
|