package lottery import ( "context" "encoding/json" "fmt" "strings" ) // 门槛规则类型常量。前端展示时可依据这些 key 组装本地化提示。 const ( RuleTypeHasSubscription = "has_subscription" // 需要有活跃订阅(可带 min_days_remaining) RuleTypeSubscriptionType = "subscription_type" // 订阅套餐必须在 plan_ids 中 RuleTypeInviteCount = "invite_count" // 邀请人数 ≥ min(可带 window_days,由 ContextBuilder 预算) RuleTypeTotalRecharge = "total_recharge" // 累计充值 ≥ min_usdt RuleTypeRegisterDays = "register_days" // 注册天数 ≥ min RuleTypeUserTag = "user_tag" // 用户标签命中 tags[] 中任一 ) const ( OpAND = "AND" OpOR = "OR" ) // defaultEvaluator 是 RuleEvaluator 的开箱实现。 type defaultEvaluator struct{} // NewRuleEvaluator 返回默认门槛评估器。 func NewRuleEvaluator() RuleEvaluator { return &defaultEvaluator{} } func (e *defaultEvaluator) Evaluate(_ context.Context, tree *EligibilityRule, rc RuleContext) (bool, []UnmetReason, error) { if tree == nil { return true, nil, nil } unmet := make([]UnmetReason, 0, 4) passed, err := e.evalNode(tree, rc, &unmet) if err != nil { return false, nil, err } return passed, unmet, nil } func (e *defaultEvaluator) evalNode(node *EligibilityRule, rc RuleContext, unmet *[]UnmetReason) (bool, error) { if node == nil { return true, nil } // 聚合节点 if node.Op != "" { return e.evalGroup(node, rc, unmet) } // 叶子节点 if node.Type == "" { return true, nil } return e.evalLeaf(node, rc, unmet) } func (e *defaultEvaluator) evalGroup(node *EligibilityRule, rc RuleContext, unmet *[]UnmetReason) (bool, error) { op := strings.ToUpper(node.Op) if len(node.Children) == 0 { return true, nil } switch op { case OpAND: allPassed := true for _, child := range node.Children { ok, err := e.evalNode(child, rc, unmet) if err != nil { return false, err } if !ok { allPassed = false } } return allPassed, nil case OpOR: // OR 只收集内部未通过项到一个临时篮子;若整体通过则不冒泡出去。 anyPassed := false local := make([]UnmetReason, 0, len(node.Children)) for _, child := range node.Children { ok, err := e.evalNode(child, rc, &local) if err != nil { return false, err } if ok { anyPassed = true } } if !anyPassed { *unmet = append(*unmet, local...) } return anyPassed, nil default: return false, fmt.Errorf("unknown group op %q", node.Op) } } func (e *defaultEvaluator) evalLeaf(node *EligibilityRule, rc RuleContext, unmet *[]UnmetReason) (bool, error) { switch node.Type { case RuleTypeHasSubscription: return e.evalHasSubscription(node, rc, unmet) case RuleTypeSubscriptionType: return e.evalSubscriptionType(node, rc, unmet) case RuleTypeInviteCount: return e.evalInviteCount(node, rc, unmet) case RuleTypeTotalRecharge: return e.evalTotalRecharge(node, rc, unmet) case RuleTypeRegisterDays: return e.evalRegisterDays(node, rc, unmet) case RuleTypeUserTag: return e.evalUserTag(node, rc, unmet) default: return false, fmt.Errorf("unknown leaf rule type %q", node.Type) } } // ---- 单条规则 ------------------------------------------------------------- func (e *defaultEvaluator) evalHasSubscription(node *EligibilityRule, rc RuleContext, unmet *[]UnmetReason) (bool, error) { var params struct { MinDaysRemaining int64 `json:"min_days_remaining"` } if len(node.Params) > 0 { if err := json.Unmarshal(node.Params, ¶ms); err != nil { return false, fmt.Errorf("has_subscription params: %w", err) } } if !rc.HasActiveSubscription { *unmet = append(*unmet, UnmetReason{ Rule: RuleTypeHasSubscription, Hint: "需要有活跃订阅", }) return false, nil } if params.MinDaysRemaining > 0 { got := rc.SubscriptionExpiresIn / 86400 if got < params.MinDaysRemaining { *unmet = append(*unmet, UnmetReason{ Rule: RuleTypeHasSubscription, Hint: fmt.Sprintf("订阅剩余天数不足,还差 %d 天", params.MinDaysRemaining-got), Current: got, Required: params.MinDaysRemaining, }) return false, nil } } return true, nil } func (e *defaultEvaluator) evalSubscriptionType(node *EligibilityRule, rc RuleContext, unmet *[]UnmetReason) (bool, error) { var params struct { PlanIds []int64 `json:"plan_ids"` } if err := json.Unmarshal(node.Params, ¶ms); err != nil { return false, fmt.Errorf("subscription_type params: %w", err) } want := make(map[int64]struct{}, len(params.PlanIds)) for _, id := range params.PlanIds { want[id] = struct{}{} } for _, id := range rc.SubscriptionPlanIds { if _, ok := want[id]; ok { return true, nil } } *unmet = append(*unmet, UnmetReason{ Rule: RuleTypeSubscriptionType, Hint: "订阅类型不符合活动要求", }) return false, nil } func (e *defaultEvaluator) evalInviteCount(node *EligibilityRule, rc RuleContext, unmet *[]UnmetReason) (bool, error) { var params struct { Min int64 `json:"min"` WindowDays int64 `json:"window_days,omitempty"` } if err := json.Unmarshal(node.Params, ¶ms); err != nil { return false, fmt.Errorf("invite_count params: %w", err) } if rc.InviteCount >= params.Min { return true, nil } *unmet = append(*unmet, UnmetReason{ Rule: RuleTypeInviteCount, Hint: fmt.Sprintf("还需邀请 %d 人(%d/%d)", params.Min-rc.InviteCount, rc.InviteCount, params.Min), Current: rc.InviteCount, Required: params.Min, }) return false, nil } func (e *defaultEvaluator) evalTotalRecharge(node *EligibilityRule, rc RuleContext, unmet *[]UnmetReason) (bool, error) { var params struct { MinUSDT int64 `json:"min_usdt"` } if err := json.Unmarshal(node.Params, ¶ms); err != nil { return false, fmt.Errorf("total_recharge params: %w", err) } if rc.TotalRechargeUSDT >= params.MinUSDT { return true, nil } *unmet = append(*unmet, UnmetReason{ Rule: RuleTypeTotalRecharge, Hint: fmt.Sprintf("累计充值不足,还差 %d USDT", params.MinUSDT-rc.TotalRechargeUSDT), Current: rc.TotalRechargeUSDT, Required: params.MinUSDT, }) return false, nil } func (e *defaultEvaluator) evalRegisterDays(node *EligibilityRule, rc RuleContext, unmet *[]UnmetReason) (bool, error) { var params struct { Min int64 `json:"min"` } if err := json.Unmarshal(node.Params, ¶ms); err != nil { return false, fmt.Errorf("register_days params: %w", err) } if rc.RegisterDays >= params.Min { return true, nil } *unmet = append(*unmet, UnmetReason{ Rule: RuleTypeRegisterDays, Hint: fmt.Sprintf("注册天数不足,还差 %d 天", params.Min-rc.RegisterDays), Current: rc.RegisterDays, Required: params.Min, }) return false, nil } func (e *defaultEvaluator) evalUserTag(node *EligibilityRule, rc RuleContext, unmet *[]UnmetReason) (bool, error) { var params struct { Tags []string `json:"tags"` } if err := json.Unmarshal(node.Params, ¶ms); err != nil { return false, fmt.Errorf("user_tag params: %w", err) } want := make(map[string]struct{}, len(params.Tags)) for _, t := range params.Tags { want[t] = struct{}{} } for _, t := range rc.UserTags { if _, ok := want[t]; ok { return true, nil } } *unmet = append(*unmet, UnmetReason{ Rule: RuleTypeUserTag, Hint: "用户标签不符合活动要求", }) return false, nil }