feat: add register reward service support
This commit is contained in:
parent
46ddaca893
commit
2e48d6dc67
@ -33,9 +33,15 @@ func main() {
|
|||||||
javaClient := integration.New(cfg)
|
javaClient := integration.New(cfg)
|
||||||
inviteService := service.NewInviteService(cfg, repository, javaClient)
|
inviteService := service.NewInviteService(cfg, repository, javaClient)
|
||||||
baishunService := service.NewBaishunService(cfg, repository, javaClient)
|
baishunService := service.NewBaishunService(cfg, repository, javaClient)
|
||||||
|
registerRewardService := service.NewRegisterRewardService(cfg, repository, javaClient)
|
||||||
|
workerCtx, workerCancel := context.WithCancel(context.Background())
|
||||||
|
defer workerCancel()
|
||||||
|
if err := registerRewardService.Start(workerCtx); err != nil {
|
||||||
|
log.Fatalf("start register reward consumer failed: %v", err)
|
||||||
|
}
|
||||||
router := invitehttp.NewRouter(cfg, repository, javaClient, inviteService, baishunService)
|
router := invitehttp.NewRouter(cfg, repository, javaClient, inviteService, baishunService)
|
||||||
|
|
||||||
log.Printf("chatapp3-golang invite api listening on %s", cfg.ListenAddr)
|
log.Printf("chatapp3-golang api listening on %s", cfg.ListenAddr)
|
||||||
if err := router.Run(cfg.ListenAddr); err != nil {
|
if err := router.Run(cfg.ListenAddr); err != nil {
|
||||||
log.Fatalf("run server failed: %v", err)
|
log.Fatalf("run server failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package config
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -29,6 +30,12 @@ type Config struct {
|
|||||||
BaishunLaunchCodeTTLSeconds int
|
BaishunLaunchCodeTTLSeconds int
|
||||||
BaishunSSTokenTTLSeconds int
|
BaishunSSTokenTTLSeconds int
|
||||||
InternalCallbackSecret string
|
InternalCallbackSecret string
|
||||||
|
RegisterRewardStreamKey string
|
||||||
|
RegisterRewardConsumerGroup string
|
||||||
|
RegisterRewardConsumerName string
|
||||||
|
RegisterRewardBatchSize int64
|
||||||
|
RegisterRewardBlock time.Duration
|
||||||
|
RegisterRewardMinIdle time.Duration
|
||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,6 +63,12 @@ func Load() Config {
|
|||||||
BaishunLaunchCodeTTLSeconds: getEnvInt("BAISHUN_LAUNCH_CODE_TTL_SECONDS", 300),
|
BaishunLaunchCodeTTLSeconds: getEnvInt("BAISHUN_LAUNCH_CODE_TTL_SECONDS", 300),
|
||||||
BaishunSSTokenTTLSeconds: getEnvInt("BAISHUN_SS_TOKEN_TTL_SECONDS", 86400),
|
BaishunSSTokenTTLSeconds: getEnvInt("BAISHUN_SS_TOKEN_TTL_SECONDS", 86400),
|
||||||
InternalCallbackSecret: getEnv("GAME_INTERNAL_CALLBACK_SECRET", ""),
|
InternalCallbackSecret: getEnv("GAME_INTERNAL_CALLBACK_SECRET", ""),
|
||||||
|
RegisterRewardStreamKey: getEnv("REGISTER_REWARD_STREAM_KEY", "activity:register:reward"),
|
||||||
|
RegisterRewardConsumerGroup: getEnv("REGISTER_REWARD_CONSUMER_GROUP", "register-reward"),
|
||||||
|
RegisterRewardConsumerName: getEnv("REGISTER_REWARD_CONSUMER_NAME", defaultConsumerName("register-reward")),
|
||||||
|
RegisterRewardBatchSize: int64(getEnvInt("REGISTER_REWARD_BATCH_SIZE", 10)),
|
||||||
|
RegisterRewardBlock: time.Duration(getEnvInt("REGISTER_REWARD_BLOCK_MILLIS", 5000)) * time.Millisecond,
|
||||||
|
RegisterRewardMinIdle: time.Duration(getEnvInt("REGISTER_REWARD_MIN_IDLE_MILLIS", 60000)) * time.Millisecond,
|
||||||
Timeout: time.Duration(getEnvInt("INVITE_HTTP_TIMEOUT_SECONDS", 8)) * time.Second,
|
Timeout: time.Duration(getEnvInt("INVITE_HTTP_TIMEOUT_SECONDS", 8)) * time.Second,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -102,3 +115,15 @@ func getEnvBool(key string, fallback bool) bool {
|
|||||||
}
|
}
|
||||||
return parsed
|
return parsed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func defaultConsumerName(prefix string) string {
|
||||||
|
host, err := os.Hostname()
|
||||||
|
if err != nil {
|
||||||
|
return prefix
|
||||||
|
}
|
||||||
|
host = strings.TrimSpace(host)
|
||||||
|
if host == "" {
|
||||||
|
return prefix
|
||||||
|
}
|
||||||
|
return prefix + "-" + host
|
||||||
|
}
|
||||||
|
|||||||
@ -21,6 +21,34 @@ type UserBaseInfo struct {
|
|||||||
|
|
||||||
func (UserBaseInfo) TableName() string { return "user_base_info" }
|
func (UserBaseInfo) TableName() string { return "user_base_info" }
|
||||||
|
|
||||||
|
type ResidentRegisterRewardConfig struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_resident_register_reward_sys_origin"`
|
||||||
|
Enabled bool `gorm:"column:enabled"`
|
||||||
|
GoldAmount int64 `gorm:"column:gold_amount"`
|
||||||
|
RewardGroupID *int64 `gorm:"column:reward_group_id"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_timestamp"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ResidentRegisterRewardConfig) TableName() string { return "resident_register_reward_config" }
|
||||||
|
|
||||||
|
type ResidentRegisterRewardLog struct {
|
||||||
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
|
EventID string `gorm:"column:event_id;size:128;uniqueIndex:uk_register_reward_event"`
|
||||||
|
StreamMessageID string `gorm:"column:stream_message_id;size:64"`
|
||||||
|
SysOrigin string `gorm:"column:sys_origin;size:32;index:idx_register_reward_sys_user,priority:1"`
|
||||||
|
UserID int64 `gorm:"column:user_id;index:idx_register_reward_sys_user,priority:2"`
|
||||||
|
GoldAmount int64 `gorm:"column:gold_amount"`
|
||||||
|
RewardGroupID *int64 `gorm:"column:reward_group_id"`
|
||||||
|
Status string `gorm:"column:status;size:32"`
|
||||||
|
ErrorMessage string `gorm:"column:error_message;size:255"`
|
||||||
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ResidentRegisterRewardLog) TableName() string { return "resident_register_reward_log" }
|
||||||
|
|
||||||
type InviteCampaignConfig struct {
|
type InviteCampaignConfig struct {
|
||||||
ID int64 `gorm:"column:id;primaryKey"`
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_invite_campaign_sys_origin"`
|
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_invite_campaign_sys_origin"`
|
||||||
@ -71,45 +99,45 @@ type InviteCampaignRelation struct {
|
|||||||
func (InviteCampaignRelation) TableName() string { return "invite_campaign_relation" }
|
func (InviteCampaignRelation) TableName() string { return "invite_campaign_relation" }
|
||||||
|
|
||||||
type InviteCampaignInviteeProgress struct {
|
type InviteCampaignInviteeProgress struct {
|
||||||
ID int64 `gorm:"column:id;primaryKey"`
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
RelationID int64 `gorm:"column:relation_id;uniqueIndex:uk_invitee_progress_relation"`
|
RelationID int64 `gorm:"column:relation_id;uniqueIndex:uk_invitee_progress_relation"`
|
||||||
SysOrigin string `gorm:"column:sys_origin;size:32"`
|
SysOrigin string `gorm:"column:sys_origin;size:32"`
|
||||||
InviterUserID int64 `gorm:"column:inviter_user_id;index:idx_invitee_progress_inviter"`
|
InviterUserID int64 `gorm:"column:inviter_user_id;index:idx_invitee_progress_inviter"`
|
||||||
InviteeUserID int64 `gorm:"column:invitee_user_id;uniqueIndex:uk_invitee_progress_invitee"`
|
InviteeUserID int64 `gorm:"column:invitee_user_id;uniqueIndex:uk_invitee_progress_invitee"`
|
||||||
TotalRecharge int64 `gorm:"column:total_recharge_coins"`
|
TotalRecharge int64 `gorm:"column:total_recharge_coins"`
|
||||||
ValidUser bool `gorm:"column:valid_user"`
|
ValidUser bool `gorm:"column:valid_user"`
|
||||||
ValidTime *time.Time `gorm:"column:valid_time"`
|
ValidTime *time.Time `gorm:"column:valid_time"`
|
||||||
RewardedRuleIDs string `gorm:"column:rewarded_rule_ids;size:255"`
|
RewardedRuleIDs string `gorm:"column:rewarded_rule_ids;size:255"`
|
||||||
CreateTime time.Time `gorm:"column:create_time"`
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
UpdateTime time.Time `gorm:"column:update_time"`
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (InviteCampaignInviteeProgress) TableName() string { return "invite_campaign_invitee_progress" }
|
func (InviteCampaignInviteeProgress) TableName() string { return "invite_campaign_invitee_progress" }
|
||||||
|
|
||||||
type InviteCampaignMonthlyProgress struct {
|
type InviteCampaignMonthlyProgress struct {
|
||||||
ID int64 `gorm:"column:id;primaryKey"`
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_invite_monthly_progress,priority:1"`
|
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_invite_monthly_progress,priority:1"`
|
||||||
InviterUserID int64 `gorm:"column:inviter_user_id;uniqueIndex:uk_invite_monthly_progress,priority:2"`
|
InviterUserID int64 `gorm:"column:inviter_user_id;uniqueIndex:uk_invite_monthly_progress,priority:2"`
|
||||||
MonthKey string `gorm:"column:month_key;size:6;uniqueIndex:uk_invite_monthly_progress,priority:3"`
|
MonthKey string `gorm:"column:month_key;size:6;uniqueIndex:uk_invite_monthly_progress,priority:3"`
|
||||||
InviteCount int64 `gorm:"column:invite_count"`
|
InviteCount int64 `gorm:"column:invite_count"`
|
||||||
ValidUserCount int64 `gorm:"column:valid_user_count"`
|
ValidUserCount int64 `gorm:"column:valid_user_count"`
|
||||||
TotalRecharge int64 `gorm:"column:total_recharge_coins"`
|
TotalRecharge int64 `gorm:"column:total_recharge_coins"`
|
||||||
RewardGoldCoins int64 `gorm:"column:reward_gold_coins"`
|
RewardGoldCoins int64 `gorm:"column:reward_gold_coins"`
|
||||||
CreateTime time.Time `gorm:"column:create_time"`
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
UpdateTime time.Time `gorm:"column:update_time"`
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (InviteCampaignMonthlyProgress) TableName() string { return "invite_campaign_monthly_progress" }
|
func (InviteCampaignMonthlyProgress) TableName() string { return "invite_campaign_monthly_progress" }
|
||||||
|
|
||||||
type InviteCampaignTaskClaim struct {
|
type InviteCampaignTaskClaim struct {
|
||||||
ID int64 `gorm:"column:id;primaryKey"`
|
ID int64 `gorm:"column:id;primaryKey"`
|
||||||
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_invite_task_claim,priority:1"`
|
SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_invite_task_claim,priority:1"`
|
||||||
InviterUserID int64 `gorm:"column:inviter_user_id;uniqueIndex:uk_invite_task_claim,priority:2"`
|
InviterUserID int64 `gorm:"column:inviter_user_id;uniqueIndex:uk_invite_task_claim,priority:2"`
|
||||||
MonthKey string `gorm:"column:month_key;size:6;uniqueIndex:uk_invite_task_claim,priority:3"`
|
MonthKey string `gorm:"column:month_key;size:6;uniqueIndex:uk_invite_task_claim,priority:3"`
|
||||||
RuleID int64 `gorm:"column:rule_id;uniqueIndex:uk_invite_task_claim,priority:4"`
|
RuleID int64 `gorm:"column:rule_id;uniqueIndex:uk_invite_task_claim,priority:4"`
|
||||||
ClaimTime time.Time `gorm:"column:claim_time"`
|
ClaimTime time.Time `gorm:"column:claim_time"`
|
||||||
CreateTime time.Time `gorm:"column:create_time"`
|
CreateTime time.Time `gorm:"column:create_time"`
|
||||||
UpdateTime time.Time `gorm:"column:update_time"`
|
UpdateTime time.Time `gorm:"column:update_time"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (InviteCampaignTaskClaim) TableName() string { return "invite_campaign_task_claim" }
|
func (InviteCampaignTaskClaim) TableName() string { return "invite_campaign_task_claim" }
|
||||||
|
|||||||
@ -44,6 +44,8 @@ func (r *Repository) Ping(ctx context.Context) error {
|
|||||||
|
|
||||||
func (r *Repository) AutoMigrate() error {
|
func (r *Repository) AutoMigrate() error {
|
||||||
if err := r.DB.AutoMigrate(
|
if err := r.DB.AutoMigrate(
|
||||||
|
&model.ResidentRegisterRewardConfig{},
|
||||||
|
&model.ResidentRegisterRewardLog{},
|
||||||
&model.InviteCampaignConfig{},
|
&model.InviteCampaignConfig{},
|
||||||
&model.InviteCampaignRewardRule{},
|
&model.InviteCampaignRewardRule{},
|
||||||
&model.InviteCampaignRelation{},
|
&model.InviteCampaignRelation{},
|
||||||
|
|||||||
458
internal/service/register_reward.go
Normal file
458
internal/service/register_reward.go
Normal file
@ -0,0 +1,458 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"chatapp3-golang/internal/config"
|
||||||
|
"chatapp3-golang/internal/integration"
|
||||||
|
"chatapp3-golang/internal/model"
|
||||||
|
"chatapp3-golang/internal/repo"
|
||||||
|
"chatapp3-golang/internal/util"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
registerRewardStatusPending = "PENDING"
|
||||||
|
registerRewardStatusSuccess = "SUCCESS"
|
||||||
|
registerRewardStatusSkipped = "SKIPPED"
|
||||||
|
registerRewardStatusFailed = "FAILED"
|
||||||
|
registerRewardGoldOrigin = "REGISTER_REWARDS"
|
||||||
|
registerRewardGoldDesc = "Register reward"
|
||||||
|
registerRewardPropsOrigin = "ACTIVITY_REWARD"
|
||||||
|
)
|
||||||
|
|
||||||
|
type registerRewardEvent struct {
|
||||||
|
EventID string
|
||||||
|
SysOrigin string
|
||||||
|
UserID int64
|
||||||
|
CountryID int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type registerRewardResolvedConfig struct {
|
||||||
|
GoldAmount int64
|
||||||
|
RewardGroupID *int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type RegisterRewardService struct {
|
||||||
|
cfg config.Config
|
||||||
|
repo *repo.Repository
|
||||||
|
java *integration.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRegisterRewardService(cfg config.Config, repository *repo.Repository, javaClient *integration.Client) *RegisterRewardService {
|
||||||
|
return &RegisterRewardService{
|
||||||
|
cfg: cfg,
|
||||||
|
repo: repository,
|
||||||
|
java: javaClient,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *RegisterRewardService) Start(ctx context.Context) error {
|
||||||
|
if strings.TrimSpace(s.cfg.RegisterRewardStreamKey) == "" {
|
||||||
|
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.RegisterRewardStreamKey,
|
||||||
|
s.cfg.RegisterRewardConsumerGroup,
|
||||||
|
s.cfg.RegisterRewardConsumerName,
|
||||||
|
)
|
||||||
|
go s.consumeLoop(ctx)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *RegisterRewardService) ensureConsumerGroup(ctx context.Context) error {
|
||||||
|
err := s.repo.Redis.XGroupCreateMkStream(
|
||||||
|
ctx,
|
||||||
|
s.cfg.RegisterRewardStreamKey,
|
||||||
|
s.cfg.RegisterRewardConsumerGroup,
|
||||||
|
"0",
|
||||||
|
).Err()
|
||||||
|
if err != nil && !strings.Contains(strings.ToUpper(err.Error()), "BUSYGROUP") {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *RegisterRewardService) reclaimPending(ctx context.Context) (int, error) {
|
||||||
|
messages, _, err := s.repo.Redis.XAutoClaim(ctx, &redis.XAutoClaimArgs{
|
||||||
|
Stream: s.cfg.RegisterRewardStreamKey,
|
||||||
|
Group: s.cfg.RegisterRewardConsumerGroup,
|
||||||
|
Consumer: s.cfg.RegisterRewardConsumerName,
|
||||||
|
MinIdle: s.cfg.RegisterRewardMinIdle,
|
||||||
|
Start: "0-0",
|
||||||
|
Count: s.cfg.RegisterRewardBatchSize,
|
||||||
|
}).Result()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if len(messages) == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
return len(messages), s.processMessages(ctx, messages)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *RegisterRewardService) readNewMessages(ctx context.Context) error {
|
||||||
|
streams, err := s.repo.Redis.XReadGroup(ctx, &redis.XReadGroupArgs{
|
||||||
|
Group: s.cfg.RegisterRewardConsumerGroup,
|
||||||
|
Consumer: s.cfg.RegisterRewardConsumerName,
|
||||||
|
Streams: []string{s.cfg.RegisterRewardStreamKey, ">"},
|
||||||
|
Count: s.cfg.RegisterRewardBatchSize,
|
||||||
|
Block: s.cfg.RegisterRewardBlock,
|
||||||
|
}).Result()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, stream := range streams {
|
||||||
|
if err := s.processMessages(ctx, stream.Messages); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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.RegisterRewardStreamKey,
|
||||||
|
s.cfg.RegisterRewardConsumerGroup,
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *RegisterRewardService) resolveRewardConfig(ctx context.Context, logRecord *model.ResidentRegisterRewardLog, event registerRewardEvent) (registerRewardResolvedConfig, bool, error) {
|
||||||
|
if logRecord.Status == registerRewardStatusFailed && (logRecord.GoldAmount > 0 || logRecord.RewardGroupID != nil) {
|
||||||
|
return registerRewardResolvedConfig{
|
||||||
|
GoldAmount: logRecord.GoldAmount,
|
||||||
|
RewardGroupID: logRecord.RewardGroupID,
|
||||||
|
}, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := s.loadRegisterRewardConfig(ctx, event.SysOrigin)
|
||||||
|
if err != nil {
|
||||||
|
return registerRewardResolvedConfig{}, false, err
|
||||||
|
}
|
||||||
|
if cfg == nil || !cfg.Enabled {
|
||||||
|
err = s.updateLogStatus(
|
||||||
|
ctx,
|
||||||
|
logRecord.ID,
|
||||||
|
registerRewardStatusSkipped,
|
||||||
|
"register reward config missing or disabled",
|
||||||
|
registerRewardResolvedConfig{},
|
||||||
|
)
|
||||||
|
return registerRewardResolvedConfig{}, err == nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resolved := registerRewardResolvedConfig{
|
||||||
|
GoldAmount: cfg.GoldAmount,
|
||||||
|
RewardGroupID: cfg.RewardGroupID,
|
||||||
|
}
|
||||||
|
if resolved.GoldAmount <= 0 && resolved.RewardGroupID == nil {
|
||||||
|
err = s.updateLogStatus(
|
||||||
|
ctx,
|
||||||
|
logRecord.ID,
|
||||||
|
registerRewardStatusSkipped,
|
||||||
|
"register reward config has no reward payload",
|
||||||
|
resolved,
|
||||||
|
)
|
||||||
|
return registerRewardResolvedConfig{}, err == nil, err
|
||||||
|
}
|
||||||
|
if err := s.updateLogStatus(ctx, logRecord.ID, registerRewardStatusPending, "", resolved); err != nil {
|
||||||
|
return registerRewardResolvedConfig{}, false, err
|
||||||
|
}
|
||||||
|
return resolved, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *RegisterRewardService) loadRegisterRewardConfig(ctx context.Context, sysOrigin string) (*model.ResidentRegisterRewardConfig, error) {
|
||||||
|
var cfg model.ResidentRegisterRewardConfig
|
||||||
|
err := s.repo.DB.WithContext(ctx).
|
||||||
|
Where("sys_origin = ?", strings.ToUpper(strings.TrimSpace(sysOrigin))).
|
||||||
|
First(&cfg).Error
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *RegisterRewardService) dispatchGold(ctx context.Context, event registerRewardEvent, resolved registerRewardResolvedConfig) error {
|
||||||
|
if resolved.GoldAmount <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return s.java.ChangeGoldBalance(ctx, integration.GoldReceiptCommand{
|
||||||
|
ReceiptType: "INCOME",
|
||||||
|
UserID: event.UserID,
|
||||||
|
SysOrigin: event.SysOrigin,
|
||||||
|
EventID: event.EventID + ":gold",
|
||||||
|
Remark: registerRewardGoldDesc,
|
||||||
|
Amount: integration.NewPennyAmountPayloadFromDollar(resolved.GoldAmount),
|
||||||
|
CloseDelayAsset: false,
|
||||||
|
OpUserType: "APP",
|
||||||
|
CustomizeOrigin: registerRewardGoldOrigin,
|
||||||
|
CustomizeOriginDesc: registerRewardGoldDesc,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *RegisterRewardService) dispatchProps(ctx context.Context, logID int64, event registerRewardEvent, resolved registerRewardResolvedConfig) error {
|
||||||
|
if resolved.RewardGroupID == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return s.java.GrantProps(ctx, integration.GrantPropsRequest{
|
||||||
|
TrackID: logID,
|
||||||
|
AcceptUserID: event.UserID,
|
||||||
|
SysOrigin: event.SysOrigin,
|
||||||
|
Origin: registerRewardPropsOrigin,
|
||||||
|
SourceGroupID: *resolved.RewardGroupID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *RegisterRewardService) getOrCreateLog(ctx context.Context, event registerRewardEvent, streamMessageID string) (*model.ResidentRegisterRewardLog, bool, error) {
|
||||||
|
var record model.ResidentRegisterRewardLog
|
||||||
|
err := s.repo.DB.WithContext(ctx).
|
||||||
|
Where("event_id = ?", event.EventID).
|
||||||
|
First(&record).Error
|
||||||
|
if err == nil {
|
||||||
|
if record.StreamMessageID != streamMessageID {
|
||||||
|
if updateErr := s.repo.DB.WithContext(ctx).
|
||||||
|
Model(&model.ResidentRegisterRewardLog{}).
|
||||||
|
Where("id = ?", record.ID).
|
||||||
|
Updates(map[string]any{
|
||||||
|
"stream_message_id": streamMessageID,
|
||||||
|
"update_time": time.Now(),
|
||||||
|
}).Error; updateErr == nil {
|
||||||
|
record.StreamMessageID = streamMessageID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &record, isRegisterRewardTerminalStatus(record.Status), nil
|
||||||
|
}
|
||||||
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
id, idErr := util.NextID()
|
||||||
|
if idErr != nil {
|
||||||
|
return nil, false, idErr
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
record = model.ResidentRegisterRewardLog{
|
||||||
|
ID: id,
|
||||||
|
EventID: event.EventID,
|
||||||
|
StreamMessageID: streamMessageID,
|
||||||
|
SysOrigin: event.SysOrigin,
|
||||||
|
UserID: event.UserID,
|
||||||
|
Status: registerRewardStatusPending,
|
||||||
|
CreateTime: now,
|
||||||
|
UpdateTime: now,
|
||||||
|
}
|
||||||
|
if err := s.repo.DB.WithContext(ctx).Create(&record).Error; err != nil {
|
||||||
|
if strings.Contains(strings.ToLower(err.Error()), "duplicate") {
|
||||||
|
return s.getOrCreateLog(ctx, event, streamMessageID)
|
||||||
|
}
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
return &record, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *RegisterRewardService) updateLogStatus(ctx context.Context, logID int64, status, errorMessage string, resolved registerRewardResolvedConfig) error {
|
||||||
|
return s.repo.DB.WithContext(ctx).
|
||||||
|
Model(&model.ResidentRegisterRewardLog{}).
|
||||||
|
Where("id = ?", logID).
|
||||||
|
Updates(map[string]any{
|
||||||
|
"status": status,
|
||||||
|
"error_message": truncateRegisterRewardMessage(errorMessage),
|
||||||
|
"gold_amount": resolved.GoldAmount,
|
||||||
|
"reward_group_id": resolved.RewardGroupID,
|
||||||
|
"update_time": time.Now(),
|
||||||
|
}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRegisterRewardEvent(message redis.XMessage) (registerRewardEvent, error) {
|
||||||
|
eventID := strings.TrimSpace(toRegisterRewardString(message.Values["eventId"]))
|
||||||
|
if eventID == "" {
|
||||||
|
return registerRewardEvent{}, fmt.Errorf("eventId is required")
|
||||||
|
}
|
||||||
|
sysOrigin := strings.ToUpper(strings.TrimSpace(toRegisterRewardString(message.Values["sysOrigin"])))
|
||||||
|
if sysOrigin == "" {
|
||||||
|
return registerRewardEvent{}, fmt.Errorf("sysOrigin is required")
|
||||||
|
}
|
||||||
|
userID, err := toRegisterRewardInt64(message.Values["userId"])
|
||||||
|
if err != nil {
|
||||||
|
return registerRewardEvent{}, fmt.Errorf("invalid userId: %w", err)
|
||||||
|
}
|
||||||
|
if userID <= 0 {
|
||||||
|
return registerRewardEvent{}, fmt.Errorf("invalid userId")
|
||||||
|
}
|
||||||
|
countryID, _ := toRegisterRewardInt64(message.Values["countryId"])
|
||||||
|
return registerRewardEvent{
|
||||||
|
EventID: eventID,
|
||||||
|
SysOrigin: sysOrigin,
|
||||||
|
UserID: userID,
|
||||||
|
CountryID: countryID,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func toRegisterRewardString(value any) string {
|
||||||
|
switch typed := value.(type) {
|
||||||
|
case nil:
|
||||||
|
return ""
|
||||||
|
case string:
|
||||||
|
return typed
|
||||||
|
case []byte:
|
||||||
|
return string(typed)
|
||||||
|
default:
|
||||||
|
return fmt.Sprint(typed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toRegisterRewardInt64(value any) (int64, error) {
|
||||||
|
switch typed := value.(type) {
|
||||||
|
case nil:
|
||||||
|
return 0, fmt.Errorf("value is nil")
|
||||||
|
case int64:
|
||||||
|
return typed, nil
|
||||||
|
case int32:
|
||||||
|
return int64(typed), nil
|
||||||
|
case int:
|
||||||
|
return int64(typed), nil
|
||||||
|
case uint64:
|
||||||
|
return int64(typed), nil
|
||||||
|
case float64:
|
||||||
|
return int64(typed), nil
|
||||||
|
case string:
|
||||||
|
return strconv.ParseInt(strings.TrimSpace(typed), 10, 64)
|
||||||
|
case []byte:
|
||||||
|
return strconv.ParseInt(strings.TrimSpace(string(typed)), 10, 64)
|
||||||
|
default:
|
||||||
|
return strconv.ParseInt(strings.TrimSpace(fmt.Sprint(typed)), 10, 64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isRegisterRewardTerminalStatus(status string) bool {
|
||||||
|
switch strings.ToUpper(strings.TrimSpace(status)) {
|
||||||
|
case registerRewardStatusSuccess, registerRewardStatusSkipped:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isRegisterRewardNoGroupErr(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.Contains(strings.ToUpper(err.Error()), "NOGROUP")
|
||||||
|
}
|
||||||
|
|
||||||
|
func truncateRegisterRewardMessage(message string) string {
|
||||||
|
message = strings.TrimSpace(message)
|
||||||
|
if len(message) <= 255 {
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
return message[:255]
|
||||||
|
}
|
||||||
|
|
||||||
|
func sleepWithContext(ctx context.Context, duration time.Duration) {
|
||||||
|
timer := time.NewTimer(duration)
|
||||||
|
defer timer.Stop()
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
case <-timer.C:
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user