fix(task_center): 邀请任务校验被邀请人活动期间消费达标
修复"有效邀请消费门槛(amount_threshold)"配置生效但后端从未判定的缺陷: 此前 invite_count 档位在全局维度仅统计 user_invites 绑定记录,导致 被邀请人零消费也算有效邀请,用户可白嫖邀请奖励。 - countInvites 新增 amountThreshold 参数:门槛>0 时按 "被邀请人在活动窗口内已支付实付金额累计 >= 门槛" 统计有效邀请, 消费时间严格限定在活动窗口内(活动期间消费达标,历史消费不计入); 门槛=0 保持"注册即计"原行为 - GetUserProgress 改为按档位各自的 amount_threshold 独立统计 - matchAndGrantExtended 自动发放路径同步门槛重算(防御性) - 新增 parseInviteAmountThreshold 辅助函数
This commit is contained in:
parent
9f96f235e7
commit
87be56e0c8
@ -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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user