2026-06-26 17:56:30 +08:00

444 lines
17 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package app
import (
"context"
"encoding/json"
"errors"
"time"
"hyapp/pkg/appcode"
"hyapp/pkg/rocketmqx"
"hyapp/pkg/roommq"
servicemq "hyapp/pkg/servicekit/mq"
"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 {
if !cfg.TaskEventWorker.Enabled {
shutdownConsumers(mqConsumers)
return nil, errors.New("rocketmq user_outbox consumer requires tencent_im.enabled")
}
}
if cfg.RocketMQ.UserOutbox.Enabled && services.broadcastPublisherAvailable {
consumer, err := newUserOutboxConsumer(cfg, services.broadcast)
if err != nil {
shutdownConsumers(mqConsumers)
return nil, err
}
mqConsumers = append(mqConsumers, consumer)
}
if cfg.TaskEventWorker.Enabled && cfg.RocketMQ.UserOutbox.Enabled {
consumer, err := newTaskUserOutboxConsumer(cfg, services)
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.InviteActivityRewardWorker.Enabled && cfg.RocketMQ.WalletOutbox.Enabled {
consumer, err := newInviteActivityWalletConsumer(cfg, services)
if err != nil {
shutdownConsumers(mqConsumers)
return nil, err
}
mqConsumers = append(mqConsumers, consumer)
}
if cfg.InviteActivityRewardWorker.Enabled && cfg.RocketMQ.UserOutbox.Enabled {
consumer, err := newInviteActivityUserOutboxConsumer(cfg, services)
if err != nil {
shutdownConsumers(mqConsumers)
return nil, err
}
mqConsumers = append(mqConsumers, consumer)
}
if cfg.MessageActionConfirmWorker.Enabled && cfg.RocketMQ.UserOutbox.Enabled {
consumer, err := newMessageActionUserOutboxConsumer(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)
}
if cfg.UserLeaderboardWorker.Enabled && cfg.RocketMQ.WalletOutbox.Enabled {
consumer, err := newUserLeaderboardWalletConsumer(cfg, services)
if err != nil {
shutdownConsumers(mqConsumers)
return nil, err
}
mqConsumers = append(mqConsumers, consumer)
}
if cfg.TaskEventWorker.Enabled && cfg.RocketMQ.WalletOutbox.Enabled {
consumer, err := newTaskWalletConsumer(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
}
if _, err := services.agencyOpening.HandleRoomEvent(eventCtx, envelope); err != nil {
return err
}
if _, err := services.task.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 newInviteActivityWalletConsumer(cfg config.Config, services *serviceBundle) (*rocketmqx.Consumer, error) {
// 邀请活动单独使用 wallet_outbox consumer group重放累计充值时不影响首充、累充和任务的消费进度。
consumer, err := rocketmqx.NewConsumer(walletOutboxConsumerConfig(cfg.RocketMQ, cfg.RocketMQ.WalletOutbox.InviteActivityConsumerGroup))
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 := inviteActivityRechargeEventFromWalletMessage(message.Body)
if err != nil || !ok {
return err
}
return services.inviteActivityReward.ConsumeWalletRechargeEvent(appcode.WithContext(ctx, event.AppCode), event)
}); err != nil {
_ = consumer.Shutdown()
return nil, err
}
return consumer, nil
}
func newInviteActivityUserOutboxConsumer(cfg config.Config, services *serviceBundle) (*rocketmqx.Consumer, error) {
// 邀请绑定和有效邀请都来自 user-service 已提交事实;消费失败必须让同一 outbox 重试,仓储和钱包命令共同保证幂等。
consumer, err := rocketmqx.NewConsumer(userOutboxInviteActivityConsumerConfig(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 {
inviteEvent, inviteOK, err := inviteActivityInviteEventFromUserMessage(message.Body)
if err != nil {
return err
}
if inviteOK {
return services.inviteActivityReward.ConsumeInviteEvent(appcode.WithContext(ctx, inviteEvent.AppCode), inviteEvent)
}
event, ok, err := inviteActivityValidInviteEventFromUserMessage(message.Body)
if err != nil || !ok {
return err
}
return services.inviteActivityReward.ConsumeValidInviteEvent(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 newUserLeaderboardWalletConsumer(cfg config.Config, services *serviceBundle) (*rocketmqx.Consumer, error) {
if services == nil || services.userLeaderboard == nil {
return nil, errors.New("user leaderboard worker requires redis store")
}
// 用户送礼榜使用独立 consumer group重放榜单读模型不能推进首充、任务、红包等业务位点。
consumer, err := rocketmqx.NewConsumer(walletOutboxUserLeaderboardConsumerConfig(cfg.RocketMQ))
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 := userLeaderboardGiftEventFromWalletMessage(message.Body)
if err != nil || !ok {
return err
}
// Redis 聚合自身用 wallet event_id 做幂等MQ 重投只会重新确认同一事实,不会重复加分。
_, err = services.userLeaderboard.ApplyGiftEvent(appcode.WithContext(ctx, event.AppCode), event)
return err
}); err != nil {
_ = consumer.Shutdown()
return nil, err
}
return consumer, nil
}
func newTaskWalletConsumer(cfg config.Config, services *serviceBundle) (*rocketmqx.Consumer, error) {
consumer, err := rocketmqx.NewConsumer(walletOutboxConsumerConfig(cfg.RocketMQ, cfg.RocketMQ.WalletOutbox.TaskConsumerGroup))
if err != nil {
return nil, err
}
if err := consumer.Subscribe(cfg.RocketMQ.WalletOutbox.Topic, walletmq.TagWalletOutboxEvent, func(ctx context.Context, message rocketmqx.ConsumedMessage) error {
// 钱包任务消费者使用独立 consumer group不能复用红包或其他钱包消费者进度否则充值任务会被其他业务确认掉。
event, appCode, ok, err := taskEventFromWalletMessage(message.Body)
if err != nil || !ok {
return err
}
// ConsumeTaskEvent 内部按 event_id 幂等;这里返回错误给 MQ 才会重试,正常 skipped/duplicate 都视为成功确认。
_, err = services.task.ConsumeTaskEvent(appcode.WithContext(ctx, appCode), event)
return err
}); err != nil {
_ = consumer.Shutdown()
return nil, err
}
return consumer, nil
}
func newTaskUserOutboxConsumer(cfg config.Config, services *serviceBundle) (*rocketmqx.Consumer, error) {
consumer, err := rocketmqx.NewConsumer(userOutboxTaskConsumerConfig(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 {
// user outbox 一条消息可能投影成多条任务事件,例如 CP 双方各一条;只有全部写入成功才确认 MQ。
events, appCode, err := taskEventsFromUserMessage(message.Body)
if err != nil || len(events) == 0 {
return err
}
eventCtx := appcode.WithContext(ctx, appCode)
for _, event := range events {
// 单条失败时让整条 outbox 重试,已写入成功的事件靠 task_event_consumption 幂等表跳过。
if _, err := services.task.ConsumeTaskEvent(eventCtx, event); err != nil {
return err
}
}
return nil
}); 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 userOutboxTaskConsumerConfig(cfg config.RocketMQConfig) rocketmqx.ConsumerConfig {
return rocketMQConsumerConfig(cfg, cfg.UserOutbox.TaskConsumerGroup, cfg.UserOutbox.ConsumerMaxReconsumeTimes)
}
func userOutboxInviteActivityConsumerConfig(cfg config.RocketMQConfig) rocketmqx.ConsumerConfig {
return rocketMQConsumerConfig(cfg, cfg.UserOutbox.InviteActivityConsumerGroup, cfg.UserOutbox.ConsumerMaxReconsumeTimes)
}
func userOutboxMessageActionConsumerConfig(cfg config.RocketMQConfig) rocketmqx.ConsumerConfig {
return rocketMQConsumerConfig(cfg, cfg.UserOutbox.MessageActionConsumerGroup, cfg.UserOutbox.ConsumerMaxReconsumeTimes)
}
func walletOutboxConsumerConfig(cfg config.RocketMQConfig, group string) rocketmqx.ConsumerConfig {
return rocketMQConsumerConfig(cfg, group, cfg.WalletOutbox.ConsumerMaxReconsumeTimes)
}
func walletOutboxUserLeaderboardConsumerConfig(cfg config.RocketMQConfig) rocketmqx.ConsumerConfig {
consumerConfig := walletOutboxConsumerConfig(cfg, cfg.WalletOutbox.UserLeaderboardConsumerGroup)
// 用户榜是 Redis 可重建读模型;新 consumer group 首次上线需要尽量从 broker 保留的最早消息补齐当前周期。
consumerConfig.ConsumeFromFirst = true
return consumerConfig
}
func messageActionProducerConfig(cfg config.RocketMQConfig) rocketmqx.ProducerConfig {
return rocketmqx.ProducerConfig{
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: cfg.MessageActionOutbox.ProducerGroup,
SendTimeout: cfg.MessageActionOutbox.SendTimeout,
Retry: cfg.MessageActionOutbox.Retry,
}
}
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) {
servicemq.ShutdownConsumers(consumers)
}
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
}