217 lines
6.6 KiB
Go
217 lines
6.6 KiB
Go
package invite
|
|
|
|
import (
|
|
"chatapp3-golang/internal/integration"
|
|
"chatapp3-golang/internal/model"
|
|
"chatapp3-golang/internal/utils"
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
)
|
|
|
|
// dispatchReward 根据 RewardView 类型发放金币或道具奖励。
|
|
func (s *InviteService) dispatchReward(ctx context.Context, input rewardDispatchInput) (*rewardDispatchResult, error) {
|
|
if input.Reward.GoldAmount <= 0 && input.Reward.RewardGroupID == nil {
|
|
return &rewardDispatchResult{Success: true}, nil
|
|
}
|
|
if input.Reward.GoldAmount > 0 {
|
|
if err := s.dispatchGold(ctx, input); err != nil {
|
|
return &rewardDispatchResult{Success: false}, err
|
|
}
|
|
}
|
|
if input.Reward.RewardGroupID != nil {
|
|
if err := s.dispatchProps(ctx, input); err != nil {
|
|
return &rewardDispatchResult{Success: false}, err
|
|
}
|
|
}
|
|
return &rewardDispatchResult{Success: true}, nil
|
|
}
|
|
|
|
// dispatchGold 调用钱包接口发放金币奖励。
|
|
func (s *InviteService) dispatchGold(ctx context.Context, input rewardDispatchInput) error {
|
|
businessKey := input.BusinessKey + ":gold"
|
|
logRecord, err := s.getOrCreateRewardLog(ctx, businessKey, input, input.Reward.GoldAmount, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if logRecord.Status == rewardLogStatusSuccess {
|
|
return nil
|
|
}
|
|
|
|
err = s.java.GrantGold(ctx, integration.GrantGoldRequest{
|
|
UserID: input.TargetUserID,
|
|
SysOrigin: input.SysOrigin,
|
|
EventID: businessKey,
|
|
Origin: input.GoldOrigin,
|
|
GoldAmount: input.Reward.GoldAmount,
|
|
Remark: input.Remark,
|
|
})
|
|
if err != nil {
|
|
if logErr := s.updateRewardLogStatus(ctx, logRecord.ID, rewardLogStatusFailed, err.Error()); logErr != nil {
|
|
return logErr
|
|
}
|
|
return err
|
|
}
|
|
if err := s.updateRewardLogStatus(ctx, logRecord.ID, rewardLogStatusSuccess, ""); err != nil {
|
|
return err
|
|
}
|
|
if input.MonthKey != "" && input.InviterUserID != nil && *input.InviterUserID == input.TargetUserID {
|
|
return s.repo.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
return s.incrementMonthlyProgressTx(tx, input.SysOrigin, input.TargetUserID, input.MonthKey, func(item *model.InviteCampaignMonthlyProgress) {
|
|
item.RewardGoldCoins += input.Reward.GoldAmount
|
|
})
|
|
})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// dispatchProps 调用道具奖励接口发放道具奖励。
|
|
func (s *InviteService) dispatchProps(ctx context.Context, input rewardDispatchInput) error {
|
|
if input.Reward.RewardGroupID == nil {
|
|
return nil
|
|
}
|
|
businessKey := input.BusinessKey + ":props"
|
|
logRecord, err := s.getOrCreateRewardLog(ctx, businessKey, input, 0, input.Reward.RewardGroupID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if logRecord.Status == rewardLogStatusSuccess {
|
|
return nil
|
|
}
|
|
err = s.java.GrantProps(ctx, integration.GrantPropsRequest{
|
|
TrackID: logRecord.ID,
|
|
AcceptUserID: input.TargetUserID,
|
|
SysOrigin: input.SysOrigin,
|
|
Origin: input.PropsOrigin,
|
|
SourceGroupID: *input.Reward.RewardGroupID,
|
|
})
|
|
if err != nil {
|
|
if logErr := s.updateRewardLogStatus(ctx, logRecord.ID, rewardLogStatusFailed, err.Error()); logErr != nil {
|
|
return logErr
|
|
}
|
|
return err
|
|
}
|
|
return s.updateRewardLogStatus(ctx, logRecord.ID, rewardLogStatusSuccess, "")
|
|
}
|
|
|
|
// getOrCreateRewardLog 创建或复用发奖日志,保证同 businessKey 幂等。
|
|
func (s *InviteService) getOrCreateRewardLog(ctx context.Context, businessKey string, input rewardDispatchInput, goldAmount int64, rewardGroupID *int64) (*model.InviteCampaignRewardLog, error) {
|
|
var record model.InviteCampaignRewardLog
|
|
err := s.repo.DB.WithContext(ctx).
|
|
Where("business_key = ?", businessKey).
|
|
First(&record).Error
|
|
if err == nil {
|
|
return &record, nil
|
|
}
|
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, err
|
|
}
|
|
id, idErr := utils.NextID()
|
|
if idErr != nil {
|
|
return nil, idErr
|
|
}
|
|
now := time.Now()
|
|
record = model.InviteCampaignRewardLog{
|
|
ID: id,
|
|
SysOrigin: input.SysOrigin,
|
|
RelationID: input.RelationID,
|
|
InviterUserID: input.InviterUserID,
|
|
InviteeUserID: input.InviteeUserID,
|
|
TargetUserID: input.TargetUserID,
|
|
RuleID: input.RuleID,
|
|
RewardScene: input.Scene,
|
|
BusinessKey: businessKey,
|
|
GoldAmount: goldAmount,
|
|
RewardGroupID: rewardGroupID,
|
|
Status: rewardLogStatusPending,
|
|
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 nil, err
|
|
}
|
|
if err := s.repo.DB.WithContext(ctx).Where("business_key = ?", businessKey).First(&record).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return &record, nil
|
|
}
|
|
|
|
// updateRewardLogStatus 更新发奖日志状态和错误信息。
|
|
func (s *InviteService) updateRewardLogStatus(ctx context.Context, id int64, status, message string) error {
|
|
return s.repo.DB.WithContext(ctx).
|
|
Model(&model.InviteCampaignRewardLog{}).
|
|
Where("id = ?", id).
|
|
Updates(map[string]any{
|
|
"status": status,
|
|
"error_message": truncate(message, 255),
|
|
"update_time": time.Now(),
|
|
}).Error
|
|
}
|
|
|
|
// mergeRewardedRuleIDs 把新命中的规则 ID 合并到进度表。
|
|
func (s *InviteService) mergeRewardedRuleIDs(ctx context.Context, relationID int64, ruleIDs []int64) error {
|
|
return s.repo.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
var progress model.InviteCampaignInviteeProgress
|
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
|
Where("relation_id = ?", relationID).
|
|
First(&progress).Error; err != nil {
|
|
return err
|
|
}
|
|
current := parseRewardedRuleIDs(progress.RewardedRuleIDs)
|
|
for _, ruleID := range ruleIDs {
|
|
current[ruleID] = struct{}{}
|
|
}
|
|
progress.RewardedRuleIDs = formatRewardedRuleIDs(current)
|
|
progress.UpdateTime = time.Now()
|
|
return tx.Save(&progress).Error
|
|
})
|
|
}
|
|
|
|
// optionalRuleID 把规则指针安全转换成规则 ID 指针。
|
|
func optionalRuleID(rule *model.InviteCampaignRewardRule) *int64 {
|
|
if rule == nil || rule.ID == 0 {
|
|
return nil
|
|
}
|
|
return refInt64(rule.ID)
|
|
}
|
|
|
|
// refInt64 返回 int64 指针。
|
|
func refInt64(value int64) *int64 {
|
|
return &value
|
|
}
|
|
|
|
// refTime 返回 time.Time 指针。
|
|
func refTime(value time.Time) *time.Time {
|
|
return &value
|
|
}
|
|
|
|
// mapGoldOrigin 把规则类型映射成金币发奖来源。
|
|
func mapGoldOrigin(ruleType string) string {
|
|
switch ruleType {
|
|
case ruleTypeInviterBind:
|
|
return "INVITED_NEW_USER_REWARD"
|
|
case ruleTypeInviteeRecharge:
|
|
return "INVITED_USER_FIRST_RECHARGE_REWARD"
|
|
case ruleTypeInviterTotalRecharge:
|
|
return "CUMULATIVE_RECHARGE_REWARDS"
|
|
default:
|
|
return "INVITE_USER_REWARDS"
|
|
}
|
|
}
|
|
|
|
// mapPropsOrigin 把规则类型映射成道具发奖来源。
|
|
func mapPropsOrigin(ruleType string) string {
|
|
switch ruleType {
|
|
case ruleTypeInviterTotalRecharge:
|
|
return "CUMULATIVE_RECHARGE_REWARDS"
|
|
default:
|
|
return "INVITE_USER_REWARDS"
|
|
}
|
|
}
|