9933d34bdd
Closes HIF-3 Stage 1 骨架:7 张表迁移 + 门槛规则引擎 + 加权选奖 + 次数入账/消耗(幂等)+ 发奖 handler 抽象与注册表。 - 单测覆盖 75.5%(未覆盖行 = stub handler ErrNotImplemented,合理) - CI 全绿(构建/Vet/测试 + golangci-lint) - 骨架不接入真实业务,vpn_duration/commission handler 在 Dispatch 中返回 ErrNotImplemented;合并后线上零变更 架构师 review 通过,4 项决策已在 issue 上给出: 1. 邀请转化语义 = 首次付款激活 2. 佣金日志类型 = 新增 CommissionTypeLottery=339 3. 家庭组归属 = 穿透到 owner 4. 管理端 IP 白名单 = 不做(推到 nginx/ingress 层) 后续 PR B/C 补真实业务对接 + 用户 API + 后台 CRUD + 集成/并发/概率测试。
253 lines
7.3 KiB
Go
253 lines
7.3 KiB
Go
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
|
||
}
|