374 lines
14 KiB
Go
374 lines
14 KiB
Go
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 {
|
||
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.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.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.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 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 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
|
||
}
|