Files
kra-new/internal/biz/business/effectiveness.go
T

149 lines
4.1 KiB
Go

package business
import (
"context"
"crypto/sha256"
"fmt"
"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
}
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
}
err = uc.repo.Charges(ctx, candidates, duration, now, full, func(charge EffectivenessCharge) error {
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})
}
return uc.repo.Publish(ctx, state, rule, key, results, save)
}