package app import ( "context" "encoding/json" "errors" "time" "hyapp/pkg/appcode" "hyapp/pkg/rocketmqx" "hyapp/pkg/roommq" "hyapp/pkg/usermq" "hyapp/pkg/walletmq" "hyapp/services/activity-service/internal/config" broadcastservice "hyapp/services/activity-service/internal/service/broadcast" ) func buildMQConsumers(cfg config.Config, services *serviceBundle) ([]*rocketmqx.Consumer, error) { mqConsumers := make([]*rocketmqx.Consumer, 0, 4) if cfg.RocketMQ.RoomOutbox.Enabled { consumer, err := newRoomOutboxConsumer(cfg, services) if err != nil { shutdownConsumers(mqConsumers) return nil, err } mqConsumers = append(mqConsumers, consumer) } if cfg.RocketMQ.UserOutbox.Enabled && !services.broadcastPublisherAvailable { shutdownConsumers(mqConsumers) return nil, errors.New("rocketmq user_outbox consumer requires tencent_im.enabled") } if cfg.RocketMQ.UserOutbox.Enabled { consumer, err := newUserOutboxConsumer(cfg, services.broadcast) if err != nil { shutdownConsumers(mqConsumers) return nil, err } mqConsumers = append(mqConsumers, consumer) } if cfg.FirstRechargeRewardWorker.Enabled && cfg.RocketMQ.WalletOutbox.Enabled { consumer, err := newFirstRechargeWalletConsumer(cfg, services) if err != nil { shutdownConsumers(mqConsumers) return nil, err } mqConsumers = append(mqConsumers, consumer) } if cfg.CumulativeRechargeRewardWorker.Enabled && cfg.RocketMQ.WalletOutbox.Enabled { consumer, err := newCumulativeRechargeWalletConsumer(cfg, services) if err != nil { shutdownConsumers(mqConsumers) return nil, err } mqConsumers = append(mqConsumers, consumer) } if cfg.RedPacketBroadcastWorker.Enabled && cfg.RocketMQ.WalletOutbox.Enabled { consumer, err := newRedPacketWalletConsumer(cfg, services) if err != nil { shutdownConsumers(mqConsumers) return nil, err } mqConsumers = append(mqConsumers, consumer) } return mqConsumers, nil } func newRoomOutboxConsumer(cfg config.Config, services *serviceBundle) (*rocketmqx.Consumer, error) { consumer, err := rocketmqx.NewConsumer(roomOutboxConsumerConfig(cfg.RocketMQ)) if err != nil { return nil, err } if err := consumer.Subscribe(cfg.RocketMQ.RoomOutbox.Topic, roommq.TagRoomOutboxEvent, func(ctx context.Context, message rocketmqx.ConsumedMessage) error { envelope, _, err := roommq.DecodeRoomOutboxMessage(message.Body) if err != nil { return err } // 一个 room outbox envelope 代表房间状态已经提交;下游模块各自写幂等消费表。 // 任何模块失败都返回错误给 MQ,让同一个 envelope 重试,避免“播报成功但活动漏计分”的半完成状态。 eventCtx := appcode.WithContext(ctx, envelope.GetAppCode()) if _, err := services.broadcast.HandleRoomEvent(eventCtx, envelope); err != nil { return err } if _, err := services.growth.HandleRoomEvent(eventCtx, envelope); err != nil { return err } if _, err := services.weeklyStar.HandleRoomEvent(eventCtx, envelope); err != nil { return err } _, err = services.roomTurnoverReward.HandleRoomEvent(eventCtx, envelope) return err }); err != nil { _ = consumer.Shutdown() return nil, err } return consumer, nil } func newUserOutboxConsumer(cfg config.Config, broadcastSvc *broadcastservice.Service) (*rocketmqx.Consumer, error) { consumer, err := rocketmqx.NewConsumer(userOutboxConsumerConfig(cfg.RocketMQ)) if err != nil { return nil, err } if err := consumer.Subscribe(cfg.RocketMQ.UserOutbox.Topic, usermq.TagUserOutboxEvent, func(ctx context.Context, message rocketmqx.ConsumedMessage) error { outboxMessage, err := usermq.DecodeUserOutboxMessage(message.Body) if err != nil { return err } return consumeUserRegionChangedForBroadcast(ctx, broadcastSvc, outboxMessage) }); err != nil { _ = consumer.Shutdown() return nil, err } return consumer, nil } func newFirstRechargeWalletConsumer(cfg config.Config, services *serviceBundle) (*rocketmqx.Consumer, error) { consumer, err := rocketmqx.NewConsumer(walletOutboxConsumerConfig(cfg.RocketMQ, cfg.RocketMQ.WalletOutbox.FirstRechargeConsumerGroup)) if err != nil { return nil, err } if err := consumer.Subscribe(cfg.RocketMQ.WalletOutbox.Topic, walletmq.TagWalletOutboxEvent, func(ctx context.Context, message rocketmqx.ConsumedMessage) error { event, ok, err := firstRechargeEventFromWalletMessage(message.Body) if err != nil || !ok { return err } return services.firstRechargeReward.ConsumeWalletRechargeEvent(appcode.WithContext(ctx, event.AppCode), event) }); err != nil { _ = consumer.Shutdown() return nil, err } return consumer, nil } func newCumulativeRechargeWalletConsumer(cfg config.Config, services *serviceBundle) (*rocketmqx.Consumer, error) { // 累充奖励使用独立 consumer group,避免和首充奖励共享消费位点后互相影响重放和排查。 consumer, err := rocketmqx.NewConsumer(walletOutboxConsumerConfig(cfg.RocketMQ, cfg.RocketMQ.WalletOutbox.CumulativeRechargeConsumerGroup)) if err != nil { return nil, err } if err := consumer.Subscribe(cfg.RocketMQ.WalletOutbox.Topic, walletmq.TagWalletOutboxEvent, func(ctx context.Context, message rocketmqx.ConsumedMessage) error { event, ok, err := cumulativeRechargeEventFromWalletMessage(message.Body) if err != nil || !ok { return err } return services.cumulativeRecharge.ConsumeWalletRechargeEvent(appcode.WithContext(ctx, event.AppCode), event) }); err != nil { _ = consumer.Shutdown() return nil, err } return consumer, nil } func newRedPacketWalletConsumer(cfg config.Config, services *serviceBundle) (*rocketmqx.Consumer, error) { consumer, err := rocketmqx.NewConsumer(walletOutboxConsumerConfig(cfg.RocketMQ, cfg.RocketMQ.WalletOutbox.RedPacketBroadcastConsumerGroup)) if err != nil { return nil, err } if err := consumer.Subscribe(redPacketWalletOutboxTopic(cfg.RocketMQ.WalletOutbox), walletmq.TagWalletOutboxEvent, func(ctx context.Context, message rocketmqx.ConsumedMessage) error { event, ok, err := redPacketEventFromWalletMessage(message.Body) if err != nil || !ok { return err } return services.broadcast.ConsumeRedPacketWalletEvent(appcode.WithContext(ctx, event.AppCode), event) }); err != nil { _ = consumer.Shutdown() return nil, err } return consumer, nil } func roomOutboxConsumerConfig(cfg config.RocketMQConfig) rocketmqx.ConsumerConfig { return rocketMQConsumerConfig(cfg, cfg.RoomOutbox.ConsumerGroup, cfg.RoomOutbox.ConsumerMaxReconsumeTimes) } func userOutboxConsumerConfig(cfg config.RocketMQConfig) rocketmqx.ConsumerConfig { return rocketMQConsumerConfig(cfg, cfg.UserOutbox.ConsumerGroup, cfg.UserOutbox.ConsumerMaxReconsumeTimes) } func walletOutboxConsumerConfig(cfg config.RocketMQConfig, group string) rocketmqx.ConsumerConfig { return rocketMQConsumerConfig(cfg, group, cfg.WalletOutbox.ConsumerMaxReconsumeTimes) } func redPacketWalletOutboxTopic(cfg config.WalletOutboxMQConfig) string { // 钱包实时 topic 显式配置后,红包 worker 只消费实时通道,避免继续被普通账务 topic 的高频消息拖慢。 if cfg.RealtimeTopic != "" { return cfg.RealtimeTopic } return cfg.Topic } func rocketMQConsumerConfig(cfg config.RocketMQConfig, group string, maxReconsume int32) rocketmqx.ConsumerConfig { return rocketmqx.ConsumerConfig{ EndpointConfig: rocketmqx.EndpointConfig{ NameServers: cfg.NameServers, NameServerDomain: cfg.NameServerDomain, AccessKey: cfg.AccessKey, SecretKey: cfg.SecretKey, SecurityToken: cfg.SecurityToken, Namespace: cfg.Namespace, VIPChannel: cfg.VIPChannel, }, GroupName: group, MaxReconsumeTimes: maxReconsume, ConsumeRetryDelay: time.Second, ConsumePullBatch: 32, ConsumePullTimeout: 15 * time.Minute, } } func shutdownConsumers(consumers []*rocketmqx.Consumer) { for _, consumer := range consumers { _ = consumer.Shutdown() } } type userRegionChangedPayload struct { UserID int64 `json:"user_id"` OldRegionID int64 `json:"old_region_id"` NewRegionID int64 `json:"new_region_id"` } func consumeUserRegionChangedForBroadcast(ctx context.Context, service *broadcastservice.Service, message usermq.UserOutboxMessage) error { if message.EventType != "UserRegionChanged" { return nil } var payload userRegionChangedPayload if err := json.Unmarshal([]byte(message.PayloadJSON), &payload); err != nil { return err } if payload.UserID <= 0 || payload.OldRegionID <= 0 || payload.OldRegionID == payload.NewRegionID { return nil } // 区域广播群成员关系是当前读模型;用户切区后只移除旧群,下一次 UserSig 会按新 users.region_id 返回新群。 _, _, err := service.RemoveRegionBroadcastMember(appcode.WithContext(ctx, message.AppCode), payload.UserID, payload.OldRegionID) return err }