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
|
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)
|
db := s.repo.GetDbR().WithContext(ctx)
|
||||||
var count int64
|
var count int64
|
||||||
|
|
||||||
if activityID > 0 {
|
if activityID > 0 {
|
||||||
|
// 活动维度:被邀请人需在该活动产生已支付抽奖订单
|
||||||
query := `
|
query := `
|
||||||
SELECT COUNT(DISTINCT ui.invitee_id)
|
SELECT COUNT(*) FROM (
|
||||||
FROM user_invites ui
|
SELECT ui.invitee_id
|
||||||
INNER JOIN orders o ON o.user_id = ui.invitee_id AND o.status = 2 AND o.source_type != 1
|
FROM user_invites ui
|
||||||
INNER JOIN activity_draw_logs dl ON dl.order_id = o.id
|
INNER JOIN orders o ON o.user_id = ui.invitee_id AND o.status = 2 AND o.source_type != 1
|
||||||
INNER JOIN activity_issues ai ON ai.id = dl.issue_id
|
INNER JOIN activity_draw_logs dl ON dl.order_id = o.id
|
||||||
WHERE ui.inviter_id = ? AND ai.activity_id = ?
|
INNER JOIN activity_issues ai ON ai.id = dl.issue_id
|
||||||
|
WHERE ui.inviter_id = ? AND ai.activity_id = ?
|
||||||
`
|
`
|
||||||
args := []interface{}{inviterID, activityID}
|
args := []interface{}{inviterID, activityID}
|
||||||
if start != nil {
|
if start != nil {
|
||||||
@ -311,25 +317,93 @@ func (s *service) countInvites(ctx context.Context, inviterID int64, activityID
|
|||||||
query += " AND o.created_at <= ?"
|
query += " AND o.created_at <= ?"
|
||||||
args = append(args, *end)
|
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 {
|
if err := db.Raw(query, args...).Scan(&count).Error; err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
return count, nil
|
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 {
|
if start != nil {
|
||||||
query = query.Where("created_at >= ?", *start)
|
query += " AND ui.created_at >= ?"
|
||||||
|
args = append(args, *start)
|
||||||
}
|
}
|
||||||
if end != nil {
|
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 0, err
|
||||||
}
|
}
|
||||||
return count, nil
|
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) {
|
func (s *service) countInvitesForActivities(ctx context.Context, inviterID int64, activityIDs []int64) (int64, error) {
|
||||||
db := s.repo.GetDbR().WithContext(ctx)
|
db := s.repo.GetDbR().WithContext(ctx)
|
||||||
var count int64
|
var count int64
|
||||||
@ -622,12 +696,16 @@ func (s *service) GetUserProgress(ctx context.Context, userID int64, taskID int6
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
orderCount, orderAmount := s.aggregateOrderMetrics(rows, perActivity)
|
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 {
|
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{
|
tierProgressMap[tier.ID] = TierProgress{
|
||||||
TierID: tier.ID,
|
TierID: tier.ID,
|
||||||
OrderCount: orderCount,
|
OrderCount: orderCount,
|
||||||
@ -1464,10 +1542,20 @@ func (s *service) matchAndGrantExtended(ctx context.Context, t *tcmodel.Task, p
|
|||||||
hit = false
|
hit = false
|
||||||
}
|
}
|
||||||
case MetricInviteCount:
|
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 {
|
if tier.Operator == OperatorGTE {
|
||||||
hit = p.InviteCount >= tier.Threshold
|
hit = inviteCount >= tier.Threshold
|
||||||
} else {
|
} else {
|
||||||
hit = p.InviteCount == tier.Threshold
|
hit = inviteCount == tier.Threshold
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !hit {
|
if !hit {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user