package business import ( "context" "crypto/sha256" "fmt" "kra/pkg/task" "math" "math/big" "strconv" "time" "github.com/go-kratos/kratos/v3/errors" ) var ErrEffectivenessConflict = errors.Conflict("SW_EFFECTIVENESS_CONFLICT", "有效端规则或数据已更新,请重试") var ErrEffectivenessUnavailable = errors.BadRequest("SW_EFFECTIVENESS_UNAVAILABLE", "有效端计算所需渠道或充值数据尚未接入") func EffectivenessRuleKey(rule OtherSettings) string { return fmt.Sprintf("%x", sha256.Sum256([]byte(strconv.FormatInt(rule.Amount, 10)+":"+strconv.FormatInt(rule.Days, 10)))) } type EffectivenessUsecase struct { repo EffectivenessRepo settings OtherSettingsRepo gate chan struct{} now func() time.Time } func NewEffectivenessUsecase(repo EffectivenessRepo, settings OtherSettingsRepo) *EffectivenessUsecase { gate := make(chan struct{}, 1) gate <- struct{}{} return &EffectivenessUsecase{repo: repo, settings: settings, gate: gate, now: time.Now} } func (uc *EffectivenessUsecase) Rule(ctx context.Context) (OtherSettings, error) { v, err := uc.settings.Find(ctx) if err != nil { return OtherSettings{}, err } if v == nil { return DefaultOtherSettings(), nil } return *v, nil } func (uc *EffectivenessUsecase) Ensure(ctx context.Context) error { rule, err := uc.Rule(ctx) if err != nil { return err } state, err := uc.repo.State(ctx) if err != nil { return err } if state.RuleKey == EffectivenessRuleKey(rule) && !state.BuiltAt.IsZero() { return nil } _, err = uc.rebuild(ctx, rule, false, false, true) return err } func (uc *EffectivenessUsecase) Refresh(ctx context.Context, full bool) error { rule, err := uc.Rule(ctx) if err != nil { return err } _, err = uc.rebuild(ctx, rule, false, full, false) return err } func (uc *EffectivenessUsecase) Reconfigure(ctx context.Context, rule OtherSettings) (*OtherSettings, error) { if err := requireSettingsAdmin(ctx); err != nil { return nil, err } if err := ValidateOtherSettings(rule); err != nil { return nil, err } return uc.rebuild(ctx, rule, true, true, false) } func (uc *EffectivenessUsecase) rebuild(ctx context.Context, rule OtherSettings, save, full, ensureOnly bool) (*OtherSettings, error) { select { case <-ctx.Done(): return nil, ctx.Err() case <-uc.gate: } defer func() { uc.gate <- struct{}{} }() state, err := uc.repo.State(ctx) if err != nil { return nil, err } key := EffectivenessRuleKey(rule) if ensureOnly && state.RuleKey == key && !state.BuiltAt.IsZero() { return &rule, nil } full = full || state.RuleKey != key duration := int64(math.MaxInt64) if rule.Days <= math.MaxInt64/86400 { duration = rule.Days * 86400 } now := uc.now().Unix() candidates, err := uc.repo.Candidates(ctx, key, duration, now, full) if err != nil { return nil, err } task.Logf(ctx, "有效端计算:全量=%t;注册后 %d 天内满 %d 元;待计算渠道=%d", full, rule.Days, rule.Amount, len(candidates)) type accumulator struct { candidate *EffectivenessCandidate sum *big.Rat at *int64 complete bool } values := map[ChannelKey]*accumulator{} threshold := big.NewRat(rule.Amount, 1) for _, c := range candidates { a := &accumulator{candidate: c, sum: new(big.Rat), at: c.PreviousAt, complete: true} if c.CreatedAt != nil && *c.CreatedAt > now { a.complete = false } if c.CreatedAt != nil && *c.CreatedAt > 0 && *c.CreatedAt <= now { a.complete = duration <= now-*c.CreatedAt if rule.Amount == 0 { at := *c.CreatedAt a.at = &at } } values[c.ChannelKey] = a } charges := 0 err = uc.repo.Charges(ctx, candidates, duration, now, full, func(charge EffectivenessCharge) error { charges++ a := values[charge.ChannelKey] if a == nil { return nil } amount, err := portraitNumber(charge.Amount) if err != nil { return err } a.sum.Add(a.sum, amount) if a.sum.Cmp(threshold) >= 0 && (a.at == nil || charge.PaidAt < *a.at) { at := charge.PaidAt a.at = &at } return nil }) if err != nil { return nil, err } results := make([]EffectivenessResult, 0, len(values)) for _, c := range candidates { a := values[c.ChannelKey] results = append(results, EffectivenessResult{ChannelKey: c.ChannelKey, QualifiedAt: a.at, WindowPay: portraitDecimal(a.sum, 2), Complete: a.complete}) } updated, err := uc.repo.Publish(ctx, state, rule, key, results, save) if err == nil { qualified := 0 for _, result := range results { if result.QualifiedAt != nil { qualified++ } } task.Logf(ctx, "有效端结果已提交;扫描流水=%d;更新渠道=%d;本批达标渠道=%d", charges, len(results), qualified) } return updated, err }