163 lines
4.6 KiB
Go
163 lines
4.6 KiB
Go
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
|
|
}
|