2026-04-27 19:12:00 +08:00

168 lines
5.3 KiB
Go

package invite
import (
"chatapp3-golang/internal/model"
"chatapp3-golang/internal/utils"
"context"
"errors"
"fmt"
"time"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// ClaimTask 处理邀请任务奖励领取请求。
func (s *InviteService) ClaimTask(ctx context.Context, user AuthUser, req TaskClaimRequest) (*TaskClaimResponse, error) {
if req.RuleID == 0 {
return nil, NewAppError(400, "invalid_rule_id", "ruleId is required")
}
bundle, err := s.loadBundle(ctx, user.SysOrigin)
if err != nil {
return nil, err
}
rule := findRuleByID(bundle.Rules, req.RuleID)
if rule == nil || !rule.Enabled {
return nil, NewAppError(404, "rule_not_found", "rule not found")
}
if rule.RuleType != ruleTypeInviterValidCount && rule.RuleType != ruleTypeInviterTotalRecharge {
return nil, NewAppError(400, "invalid_rule_type", "rule cannot be claimed manually")
}
periodKey, _, _ := thirtyDayPeriodInfoAt(time.Now(), normalizeTimezone(bundle.Config.Timezone), bundle.Config.CreateTime)
claimed, err := s.hasTaskClaim(ctx, user.SysOrigin, user.UserID, periodKey, rule.ID)
if err != nil {
return nil, err
}
monthly, err := s.findMonthlyProgress(ctx, user.SysOrigin, user.UserID, periodKey)
if err != nil {
return nil, err
}
progressValue := monthly.TotalRecharge
if rule.RuleType == ruleTypeInviterValidCount {
progressValue = monthly.ValidUserCount
}
if claimed {
return &TaskClaimResponse{
Claimed: true,
AlreadyClaimed: true,
RuleID: rule.ID,
RuleType: rule.RuleType,
Progress: progressValue,
Threshold: rule.ThresholdValue,
Reward: rewardFromRule(rule),
}, nil
}
if progressValue < rule.ThresholdValue {
return nil, NewAppError(400, "task_not_reached", "task threshold not reached")
}
// 先发奖,再落领取记录;发奖侧通过 businessKey 兜底幂等。
_, err = s.dispatchReward(ctx, rewardDispatchInput{
SysOrigin: user.SysOrigin,
MonthKey: periodKey,
InviterUserID: refInt64(user.UserID),
TargetUserID: user.UserID,
Reward: rewardFromRule(rule),
Scene: rule.RuleType,
RuleID: refInt64(rule.ID),
BusinessKey: fmt.Sprintf("claim:%d:%s:%d", user.UserID, periodKey, rule.ID),
GoldOrigin: mapGoldOrigin(rule.RuleType),
PropsOrigin: mapPropsOrigin(rule.RuleType),
Remark: "resident invite 30-day task reward",
})
if err != nil {
return nil, err
}
now := time.Now()
err = s.repo.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := s.createTaskClaimIfAbsentTx(tx, user.SysOrigin, user.UserID, periodKey, rule.ID, now); err != nil {
return err
}
return nil
})
if err != nil {
return nil, err
}
return &TaskClaimResponse{
Claimed: true,
AlreadyClaimed: false,
RuleID: rule.ID,
RuleType: rule.RuleType,
Progress: progressValue,
Threshold: rule.ThresholdValue,
Reward: rewardFromRule(rule),
}, nil
}
// hasTaskClaim 判断任务是否已经被领取。
func (s *InviteService) hasTaskClaim(ctx context.Context, sysOrigin string, inviterUserID int64, monthKey string, ruleID int64) (bool, error) {
var count int64
err := s.repo.DB.WithContext(ctx).
Model(&model.InviteCampaignTaskClaim{}).
Where("sys_origin = ? AND inviter_user_id = ? AND month_key = ? AND rule_id = ?", sysOrigin, inviterUserID, monthKey, ruleID).
Count(&count).Error
return count > 0, err
}
// incrementMonthlyProgressTx 在事务里更新当前任务周期进度;函数名沿用月度命名以减少表结构迁移影响。
func (s *InviteService) incrementMonthlyProgressTx(tx *gorm.DB, sysOrigin string, inviterUserID int64, monthKey string, mutate func(item *model.InviteCampaignMonthlyProgress)) error {
var progress model.InviteCampaignMonthlyProgress
err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("sys_origin = ? AND inviter_user_id = ? AND month_key = ?", sysOrigin, inviterUserID, monthKey).
First(&progress).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
id, idErr := utils.NextID()
if idErr != nil {
return idErr
}
now := time.Now()
progress = model.InviteCampaignMonthlyProgress{
ID: id,
SysOrigin: sysOrigin,
InviterUserID: inviterUserID,
MonthKey: monthKey,
CreateTime: now,
UpdateTime: now,
}
mutate(&progress)
return tx.Create(&progress).Error
}
if err != nil {
return err
}
mutate(&progress)
progress.UpdateTime = time.Now()
return tx.Save(&progress).Error
}
// createTaskClaimIfAbsentTx 在事务里创建领取记录,避免重复领取。
func (s *InviteService) createTaskClaimIfAbsentTx(tx *gorm.DB, sysOrigin string, inviterUserID int64, monthKey string, ruleID int64, claimTime time.Time) error {
var existing model.InviteCampaignTaskClaim
err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("sys_origin = ? AND inviter_user_id = ? AND month_key = ? AND rule_id = ?", sysOrigin, inviterUserID, monthKey, ruleID).
First(&existing).Error
if err == nil {
return nil
}
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
id, idErr := utils.NextID()
if idErr != nil {
return idErr
}
return tx.Create(&model.InviteCampaignTaskClaim{
ID: id,
SysOrigin: sysOrigin,
InviterUserID: inviterUserID,
MonthKey: monthKey,
RuleID: ruleID,
ClaimTime: claimTime,
CreateTime: claimTime,
UpdateTime: claimTime,
}).Error
}