2026-04-24 13:04:06 +08:00

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"
}
}