203 lines
6.0 KiB
Go
203 lines
6.0 KiB
Go
package registerreward
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"log"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/redis/go-redis/v9"
|
||
)
|
||
|
||
// Start 启动注册奖励消费任务;若未配置 Stream,则直接跳过。
|
||
func (s *RegisterRewardService) Start(ctx context.Context) error {
|
||
if strings.TrimSpace(s.cfg.Worker.RegisterReward.StreamKey) == "" {
|
||
log.Printf("register reward consumer disabled: stream key is blank")
|
||
return nil
|
||
}
|
||
if err := s.ensureConsumerGroup(ctx); err != nil {
|
||
return err
|
||
}
|
||
log.Printf(
|
||
"register reward consumer started. stream=%s group=%s consumer=%s",
|
||
s.cfg.Worker.RegisterReward.StreamKey,
|
||
s.cfg.Worker.RegisterReward.ConsumerGroup,
|
||
s.cfg.Worker.RegisterReward.ConsumerName,
|
||
)
|
||
go s.consumeLoop(ctx)
|
||
return nil
|
||
}
|
||
|
||
// consumeLoop 先认领超时消息,再持续读取新消息。
|
||
func (s *RegisterRewardService) consumeLoop(ctx context.Context) {
|
||
for {
|
||
if ctx.Err() != nil {
|
||
return
|
||
}
|
||
|
||
reclaimed, err := s.reclaimPending(ctx)
|
||
if err != nil {
|
||
s.handleLoopError(ctx, "reclaim register reward messages", err)
|
||
continue
|
||
}
|
||
if reclaimed > 0 {
|
||
continue
|
||
}
|
||
|
||
err = s.readNewMessages(ctx)
|
||
if err == nil || errors.Is(err, redis.Nil) {
|
||
continue
|
||
}
|
||
s.handleLoopError(ctx, "read register reward messages", err)
|
||
}
|
||
}
|
||
|
||
// handleLoopError 统一处理消费循环中的错误,并在必要时重建消费者组。
|
||
func (s *RegisterRewardService) handleLoopError(ctx context.Context, action string, err error) {
|
||
if ctx.Err() != nil {
|
||
return
|
||
}
|
||
if isRegisterRewardNoGroupErr(err) {
|
||
if ensureErr := s.ensureConsumerGroup(ctx); ensureErr != nil {
|
||
log.Printf("%s failed and recreate consumer group failed: %v", action, ensureErr)
|
||
sleepWithContext(ctx, time.Second)
|
||
return
|
||
}
|
||
return
|
||
}
|
||
log.Printf("%s failed: %v", action, err)
|
||
sleepWithContext(ctx, time.Second)
|
||
}
|
||
|
||
// ensureConsumerGroup 确保 Redis Stream 消费组存在。
|
||
func (s *RegisterRewardService) ensureConsumerGroup(ctx context.Context) error {
|
||
err := s.repo.Redis.XGroupCreateMkStream(
|
||
ctx,
|
||
s.cfg.Worker.RegisterReward.StreamKey,
|
||
s.cfg.Worker.RegisterReward.ConsumerGroup,
|
||
"0",
|
||
).Err()
|
||
if err != nil && !strings.Contains(strings.ToUpper(err.Error()), "BUSYGROUP") {
|
||
return err
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// reclaimPending 认领长时间未确认的 pending 消息,避免消息堆积。
|
||
func (s *RegisterRewardService) reclaimPending(ctx context.Context) (int, error) {
|
||
messages, _, err := s.repo.Redis.XAutoClaim(ctx, &redis.XAutoClaimArgs{
|
||
Stream: s.cfg.Worker.RegisterReward.StreamKey,
|
||
Group: s.cfg.Worker.RegisterReward.ConsumerGroup,
|
||
Consumer: s.cfg.Worker.RegisterReward.ConsumerName,
|
||
MinIdle: s.cfg.Worker.RegisterReward.MinIdle,
|
||
Start: "0-0",
|
||
Count: s.cfg.Worker.RegisterReward.BatchSize,
|
||
}).Result()
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
if len(messages) == 0 {
|
||
return 0, nil
|
||
}
|
||
return len(messages), s.processMessages(ctx, messages)
|
||
}
|
||
|
||
// readNewMessages 按消费者组协议读取新消息。
|
||
func (s *RegisterRewardService) readNewMessages(ctx context.Context) error {
|
||
streams, err := s.repo.Redis.XReadGroup(ctx, &redis.XReadGroupArgs{
|
||
Group: s.cfg.Worker.RegisterReward.ConsumerGroup,
|
||
Consumer: s.cfg.Worker.RegisterReward.ConsumerName,
|
||
Streams: []string{s.cfg.Worker.RegisterReward.StreamKey, ">"},
|
||
Count: s.cfg.Worker.RegisterReward.BatchSize,
|
||
Block: s.cfg.Worker.RegisterReward.Block,
|
||
}).Result()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
for _, stream := range streams {
|
||
if err := s.processMessages(ctx, stream.Messages); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// processMessages 顺序处理消息,并在可确认时回 ACK。
|
||
func (s *RegisterRewardService) processMessages(ctx context.Context, messages []redis.XMessage) error {
|
||
for _, message := range messages {
|
||
ack, err := s.processMessage(ctx, message)
|
||
if ack {
|
||
if ackErr := s.repo.Redis.XAck(
|
||
context.Background(),
|
||
s.cfg.Worker.RegisterReward.StreamKey,
|
||
s.cfg.Worker.RegisterReward.ConsumerGroup,
|
||
message.ID,
|
||
).Err(); ackErr != nil {
|
||
log.Printf("ack register reward message failed. streamId=%s err=%v", message.ID, ackErr)
|
||
}
|
||
}
|
||
if err != nil {
|
||
return err
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// processMessage 处理单条注册奖励消息,流程包含解析、幂等、解析配置、发奖与状态更新。
|
||
func (s *RegisterRewardService) processMessage(ctx context.Context, message redis.XMessage) (bool, error) {
|
||
event, err := parseRegisterRewardEvent(message)
|
||
if err != nil {
|
||
log.Printf("drop malformed register reward message. streamId=%s err=%v values=%v", message.ID, err, message.Values)
|
||
return true, nil
|
||
}
|
||
|
||
// 先创建或读取日志,保证失败重试时仍能延续同一条处理记录。
|
||
logRecord, terminal, err := s.getOrCreateLog(ctx, event, message.ID)
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
if terminal {
|
||
return true, nil
|
||
}
|
||
|
||
resolvedConfig, ack, err := s.resolveRewardConfig(ctx, logRecord, event)
|
||
if err != nil || ack {
|
||
return ack, err
|
||
}
|
||
|
||
grantPlan, err := s.resolveGrantPlan(ctx, logRecord, event, resolvedConfig)
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
if grantPlan.SkipDispatch {
|
||
if err := s.updateLogStatus(ctx, logRecord.ID, registerRewardStatusSuccess, "", resolvedConfig); err != nil {
|
||
return false, err
|
||
}
|
||
return true, nil
|
||
}
|
||
|
||
// 金币和道具奖励分开发放,任何一步失败都保留失败状态供后续重试。
|
||
if err := s.dispatchGold(ctx, event, resolvedConfig); err != nil {
|
||
_ = s.updateLogStatus(ctx, logRecord.ID, registerRewardStatusFailed, err.Error(), resolvedConfig)
|
||
return false, err
|
||
}
|
||
if err := s.dispatchProps(ctx, logRecord.ID, event, resolvedConfig); err != nil {
|
||
_ = s.updateLogStatus(ctx, logRecord.ID, registerRewardStatusFailed, err.Error(), resolvedConfig)
|
||
return false, err
|
||
}
|
||
if err := s.updateLogStatus(ctx, logRecord.ID, registerRewardStatusSuccess, "", resolvedConfig); err != nil {
|
||
return false, err
|
||
}
|
||
if err := s.pushRewardArrivalNotice(ctx, event, logRecord, resolvedConfig); err != nil {
|
||
log.Printf(
|
||
"push register reward notice failed. logId=%d eventId=%s userId=%d err=%v",
|
||
logRecord.ID,
|
||
event.EventID,
|
||
event.UserID,
|
||
err,
|
||
)
|
||
}
|
||
return true, nil
|
||
}
|