diff --git a/internal/service/task_center/service.go b/internal/service/task_center/service.go index 8bb349b..eee99c0 100755 --- a/internal/service/task_center/service.go +++ b/internal/service/task_center/service.go @@ -289,18 +289,24 @@ func (s *service) aggregateOrderMetrics(rows []orderMetricRow, perActivity bool) return count, amount } -func (s *service) countInvites(ctx context.Context, inviterID int64, activityID int64, start, end *time.Time) (int64, error) { +// countInvites 统计某邀请人的有效邀请数。 +// amountThreshold > 0 时,要求"被邀请人在活动窗口 [start, end] 内累计已支付消费(实付金额, 单位分) >= amountThreshold" +// 才算 1 个有效邀请;amountThreshold <= 0 时表示"注册即计",被邀请人无需消费。 +// 关键:消费达标判定严格限定在活动窗口内(活动期间消费达标),历史消费不计入。 +func (s *service) countInvites(ctx context.Context, inviterID int64, activityID int64, amountThreshold int64, start, end *time.Time) (int64, error) { db := s.repo.GetDbR().WithContext(ctx) var count int64 if activityID > 0 { + // 活动维度:被邀请人需在该活动产生已支付抽奖订单 query := ` - SELECT COUNT(DISTINCT ui.invitee_id) - FROM user_invites ui - INNER JOIN orders o ON o.user_id = ui.invitee_id AND o.status = 2 AND o.source_type != 1 - INNER JOIN activity_draw_logs dl ON dl.order_id = o.id - INNER JOIN activity_issues ai ON ai.id = dl.issue_id - WHERE ui.inviter_id = ? AND ai.activity_id = ? + SELECT COUNT(*) FROM ( + SELECT ui.invitee_id + FROM user_invites ui + INNER JOIN orders o ON o.user_id = ui.invitee_id AND o.status = 2 AND o.source_type != 1 + INNER JOIN activity_draw_logs dl ON dl.order_id = o.id + INNER JOIN activity_issues ai ON ai.id = dl.issue_id + WHERE ui.inviter_id = ? AND ai.activity_id = ? ` args := []interface{}{inviterID, activityID} if start != nil { @@ -311,25 +317,93 @@ func (s *service) countInvites(ctx context.Context, inviterID int64, activityID query += " AND o.created_at <= ?" args = append(args, *end) } + query += " GROUP BY ui.invitee_id" + if amountThreshold > 0 { + // 活动期间消费达标:被邀请人在该活动下实付金额累计 >= 门槛 + query += " HAVING SUM(o.actual_amount) >= ?" + args = append(args, amountThreshold) + } + query += ") t" if err := db.Raw(query, args...).Scan(&count).Error; err != nil { return 0, err } return count, nil } - query := db.Model(&model.UserInvites{}).Where("inviter_id = ?", inviterID) + // 全局维度(activity_id = 0) + if amountThreshold <= 0 { + // 门槛为 0:注册即计,任意绑定的被邀请人都算 1 个有效邀请 + query := db.Model(&model.UserInvites{}).Where("inviter_id = ?", inviterID) + if start != nil { + query = query.Where("created_at >= ?", *start) + } + if end != nil { + query = query.Where("created_at <= ?", *end) + } + if err := query.Count(&count).Error; err != nil { + return 0, err + } + return count, nil + } + + // 门槛 > 0:被邀请人需在活动期间累计已支付消费(实付金额)达标,才算 1 个有效邀请 + query := ` + SELECT COUNT(*) FROM ( + SELECT ui.invitee_id + FROM user_invites ui + INNER JOIN orders o ON o.user_id = ui.invitee_id AND o.status = 2 + WHERE ui.inviter_id = ? + ` + args := []interface{}{inviterID} + // 拉新动作(邀请绑定)须发生在活动窗口内 if start != nil { - query = query.Where("created_at >= ?", *start) + query += " AND ui.created_at >= ?" + args = append(args, *start) } if end != nil { - query = query.Where("created_at <= ?", *end) + query += " AND ui.created_at <= ?" + args = append(args, *end) } - if err := query.Count(&count).Error; err != nil { + // 被邀请人消费须发生在活动窗口内(以支付时间为准,回退创建时间) + paidExpr := "COALESCE(NULLIF(o.paid_at, '1970-01-01 00:00:00'), o.created_at)" + if start != nil { + query += " AND " + paidExpr + " >= ?" + args = append(args, *start) + } + if end != nil { + query += " AND " + paidExpr + " <= ?" + args = append(args, *end) + } + query += ` + GROUP BY ui.invitee_id + HAVING SUM(o.actual_amount) >= ? + ) t + ` + args = append(args, amountThreshold) + if err := db.Raw(query, args...).Scan(&count).Error; err != nil { return 0, err } return count, nil } +// parseInviteAmountThreshold 从档位 ExtraParams 中解析"有效邀请消费门槛"(单位:分)。 +// 字段缺失、为负或解析失败时返回 0(注册即计)。 +func parseInviteAmountThreshold(extra datatypes.JSON) int64 { + if len(extra) == 0 { + return 0 + } + var p struct { + AmountThreshold int64 `json:"amount_threshold"` + } + if err := json.Unmarshal([]byte(extra), &p); err != nil { + return 0 + } + if p.AmountThreshold < 0 { + return 0 + } + return p.AmountThreshold +} + func (s *service) countInvitesForActivities(ctx context.Context, inviterID int64, activityIDs []int64) (int64, error) { db := s.repo.GetDbR().WithContext(ctx) var count int64 @@ -622,12 +696,16 @@ func (s *service) GetUserProgress(ctx context.Context, userID int64, taskID int6 return nil, err } orderCount, orderAmount := s.aggregateOrderMetrics(rows, perActivity) - inviteCount, err := s.countInvites(ctx, userID, wk.ActivityID, wStart, wEnd) - if err != nil { - return nil, err - } for _, tier := range groupTiers { + // 邀请人数:按该档位配置的"有效邀请消费门槛"(amount_threshold, 单位分)独立统计, + // 要求被邀请人在活动窗口内消费达标才计入;门槛为 0 时注册即计。 + // 非邀请档位 ExtraParams 通常为空 → 门槛 0 → 退化为按窗口内绑定数统计(向后兼容)。 + amountThreshold := parseInviteAmountThreshold(tier.ExtraParams) + inviteCount, err := s.countInvites(ctx, userID, wk.ActivityID, amountThreshold, wStart, wEnd) + if err != nil { + return nil, err + } tierProgressMap[tier.ID] = TierProgress{ TierID: tier.ID, OrderCount: orderCount, @@ -1464,10 +1542,20 @@ func (s *service) matchAndGrantExtended(ctx context.Context, t *tcmodel.Task, p hit = false } case MetricInviteCount: + inviteCount := p.InviteCount + // 有效邀请消费门槛:要求被邀请人在活动窗口内消费达标才计入 + if amountThreshold := parseInviteAmountThreshold(tier.ExtraParams); amountThreshold > 0 { + wStart, wEnd := computeTimeWindow(normalizeWindow(tier.Window), t.StartTime, t.EndTime) + vc, cerr := s.countInvites(ctx, p.UserID, tier.ActivityID, amountThreshold, wStart, wEnd) + if cerr != nil { + return cerr + } + inviteCount = vc + } if tier.Operator == OperatorGTE { - hit = p.InviteCount >= tier.Threshold + hit = inviteCount >= tier.Threshold } else { - hit = p.InviteCount == tier.Threshold + hit = inviteCount == tier.Threshold } } if !hit {