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

282 lines
9.3 KiB
Go

package business
import (
"container/heap"
"context"
"math/big"
"sort"
"strings"
"time"
"github.com/go-kratos/kratos/v3/errors"
)
var (
ErrWorkbenchSource = errors.BadRequest("SW_WORKBENCH_SOURCE", "请选择所属平台后查看数据")
ErrWorkbenchPeriod = errors.BadRequest("SW_WORKBENCH_PERIOD", "时间维度无效,应为 today/yesterday/last7/last30")
ErrWorkbenchUnavailable = errors.BadRequest("SW_WORKBENCH_UNAVAILABLE", "工作台所需业务数据尚未接入")
)
type WorkbenchRankKind string
const (
WorkbenchRegister WorkbenchRankKind = "register"
WorkbenchPayHigh WorkbenchRankKind = "payHigh"
WorkbenchPayLow WorkbenchRankKind = "payLow"
WorkbenchUnlogin WorkbenchRankKind = "unlogin"
)
type WorkbenchWindow struct{ Today, Yesterday, MonthStart, PreviousMonthStart, PreviousMonthEnd time.Time }
type WorkbenchDay struct {
Counts PortraitCounts
Pay string
ActiveChannels, NewChannels int64
}
type WorkbenchTotals struct {
Today, Yesterday WorkbenchDay
MonthPay, PreviousMonthPay string
Available bool
}
type WorkbenchCard struct {
Value, CompareValue, ChangeRate *string
CompareLabel string
}
type WorkbenchSummary struct {
TodayRegister, TodayActive, TodayPay, MonthPay, ActivePromote, NewPromote *WorkbenchCard
Available bool
}
type WorkbenchRankRow struct {
AnalyticsKey
Rank int
Name, Value string
CreatedUnix, LastLoginUnix *int64
}
type WorkbenchRankData struct {
Rows []*WorkbenchRankRow
Available bool
}
type WorkbenchRepo interface {
Summary(context.Context, AnalyticsScope, []string, WorkbenchWindow) (*WorkbenchTotals, error)
Ranks(context.Context, AnalyticsScope, AnalyticsFilter, WorkbenchRankKind) (*WorkbenchRankData, error)
FillRankNames(context.Context, []*WorkbenchRankRow) error
}
type WorkbenchUsecase struct {
repo WorkbenchRepo
accounts *AccountUsecase
preferences *UserPreferenceUsecase
now func() time.Time
}
func NewWorkbenchUsecase(repo WorkbenchRepo, accounts *AccountUsecase, preferences *UserPreferenceUsecase) *WorkbenchUsecase {
return &WorkbenchUsecase{repo: repo, accounts: accounts, preferences: preferences, now: time.Now}
}
func (uc *WorkbenchUsecase) scope(ctx context.Context, sources []string) (AnalyticsScope, error) {
actor, account, err := uc.accounts.actor(ctx)
if err != nil {
return AnalyticsScope{}, err
}
if len(sources) == 0 {
return AnalyticsScope{}, ErrWorkbenchSource
}
pref, err := uc.preferences.Current(ctx)
if err != nil {
return AnalyticsScope{}, err
}
viewer := portraitUserScope(account)
viewer.All = actor.AuthorityID == 888
return AnalyticsScope{Viewer: viewer, IncludeSub: pref.IncludeSubPromote, IDCardDedup: pref.IDCardDedup}, nil
}
func workbenchWindow(now time.Time) WorkbenchWindow {
now = now.In(portraitLocation)
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, portraitLocation)
month := time.Date(today.Year(), today.Month(), 1, 0, 0, 0, 0, portraitLocation)
previous := month.AddDate(0, -1, 0)
lastDay := month.AddDate(0, 0, -1).Day()
previousEnd := previous.AddDate(0, 0, min(today.Day(), lastDay)-1)
return WorkbenchWindow{Today: today, Yesterday: today.AddDate(0, 0, -1), MonthStart: month, PreviousMonthStart: previous, PreviousMonthEnd: previousEnd}
}
func workbenchCard(current, previous, label string, available bool) (*WorkbenchCard, error) {
card := &WorkbenchCard{CompareLabel: label}
if !available {
return card, nil
}
cur, err := portraitNumber(current)
if err != nil {
return nil, err
}
prev, err := portraitNumber(previous)
if err != nil {
return nil, err
}
card.Value, card.CompareValue = stringPointer(portraitDecimal(cur, 2)), stringPointer(portraitDecimal(prev, 2))
cur, _ = portraitNumber(*card.Value)
prev, _ = portraitNumber(*card.CompareValue)
if prev.Sign() != 0 {
diff := new(big.Rat).Sub(cur, prev)
diff.Mul(diff, big.NewRat(100, 1))
diff.Quo(diff, prev)
card.ChangeRate = stringPointer(portraitDecimal(diff, 1))
}
return card, nil
}
func (uc *WorkbenchUsecase) Summary(ctx context.Context, sources []string) (*WorkbenchSummary, error) {
scope, err := uc.scope(ctx, sources)
if err != nil {
return nil, err
}
data, err := uc.repo.Summary(ctx, scope, sources, workbenchWindow(uc.now()))
if err != nil {
return nil, err
}
data.Today.Counts, data.Yesterday.Counts = analyticsCap(data.Today.Counts), analyticsCap(data.Yesterday.Counts)
integer := func(n int64) string { return big.NewInt(n).String() }
out := &WorkbenchSummary{Available: data.Available}
for _, item := range []struct {
field **WorkbenchCard
current, previous, label string
}{
{&out.TodayRegister, integer(data.Today.Counts.Register), integer(data.Yesterday.Counts.Register), "环比昨日"},
{&out.TodayActive, integer(data.Today.Counts.Active), integer(data.Yesterday.Counts.Active), "环比昨日"},
{&out.TodayPay, data.Today.Pay, data.Yesterday.Pay, "环比昨日"},
{&out.MonthPay, data.MonthPay, data.PreviousMonthPay, "环比上月"},
{&out.ActivePromote, integer(data.Today.ActiveChannels), integer(data.Yesterday.ActiveChannels), "环比昨日"},
{&out.NewPromote, integer(data.Today.NewChannels), integer(data.Yesterday.NewChannels), "环比昨日"},
} {
card, err := workbenchCard(item.current, item.previous, item.label, data.Available)
if err != nil {
return nil, err
}
*item.field = card
}
return out, nil
}
func (uc *WorkbenchUsecase) Ranks(ctx context.Context, sources []string, period string, kind WorkbenchRankKind) ([]*WorkbenchRankRow, error) {
scope, err := uc.scope(ctx, sources)
if err != nil {
return nil, err
}
if kind != WorkbenchRegister && kind != WorkbenchPayHigh && kind != WorkbenchPayLow && kind != WorkbenchUnlogin {
return nil, ErrAnalyticsInput
}
period = strings.ToLower(strings.TrimSpace(period))
if period == "" {
period = "today"
if kind == WorkbenchPayLow {
period = "last30"
}
}
w := workbenchWindow(uc.now())
begin, end := w.Today, w.Today
if kind != WorkbenchUnlogin {
switch period {
case "today":
case "yesterday":
begin, end = w.Yesterday, w.Yesterday
case "last7":
begin = end.AddDate(0, 0, -6)
case "last30":
begin = end.AddDate(0, 0, -29)
default:
return nil, ErrWorkbenchPeriod
}
}
data, err := uc.repo.Ranks(ctx, scope, AnalyticsFilter{Sources: sources, Begin: begin.Format("2006-01-02"), End: end.Format("2006-01-02")}, kind)
if err != nil {
return nil, err
}
if !data.Available {
return nil, ErrWorkbenchUnavailable
}
top := &workbenchRankHeap{ascending: kind == WorkbenchPayLow}
for index, row := range data.Rows {
if kind == WorkbenchUnlogin {
row.Value = big.NewInt(workbenchUnloginDays(row, w.Today)).String()
}
n, err := portraitNumber(row.Value)
if err != nil {
return nil, err
}
if kind == WorkbenchRegister && n.Cmp(big.NewRat(2147483647, 1)) > 0 {
n = big.NewRat(2147483647, 1)
}
if (kind == WorkbenchRegister || kind == WorkbenchPayHigh) && n.Sign() <= 0 {
continue
}
candidate := workbenchRankCandidate{row: row, value: n, index: index}
if top.Len() < 10 {
heap.Push(top, candidate)
} else if top.better(candidate, top.rows[0]) {
top.rows[0] = candidate
heap.Fix(top, 0)
}
}
sort.Slice(top.rows, func(i, j int) bool { return top.better(top.rows[i], top.rows[j]) })
rows := make([]*WorkbenchRankRow, len(top.rows))
for i, candidate := range top.rows {
rows[i] = candidate.row
}
if err := uc.repo.FillRankNames(ctx, rows); err != nil {
return nil, err
}
for i, row := range rows {
row.Rank = i + 1
row.Name = (&LedgerNames{PromoteID: &row.ID, PromoteAccount: row.Name}).DisplayPromote()
if kind != WorkbenchUnlogin {
row.Value = portraitDecimal(top.rows[i].value, 2)
}
}
return rows, nil
}
type workbenchRankCandidate struct {
row *WorkbenchRankRow
value *big.Rat
index int
}
// 堆顶保留最差的入选行,只维护 TOP10;比较规则与完整稳定排序一致。
type workbenchRankHeap struct {
rows []workbenchRankCandidate
ascending bool
}
func (h workbenchRankHeap) better(a, b workbenchRankCandidate) bool {
if c := a.value.Cmp(b.value); c != 0 {
if h.ascending {
return c < 0
}
return c > 0
}
if a.row.ID != b.row.ID {
return a.row.ID < b.row.ID
}
if a.row.SourceDB != b.row.SourceDB {
return a.row.SourceDB < b.row.SourceDB
}
return a.index < b.index
}
func (h workbenchRankHeap) Len() int { return len(h.rows) }
func (h workbenchRankHeap) Less(i, j int) bool { return h.better(h.rows[j], h.rows[i]) }
func (h workbenchRankHeap) Swap(i, j int) { h.rows[i], h.rows[j] = h.rows[j], h.rows[i] }
func (h *workbenchRankHeap) Push(v any) { h.rows = append(h.rows, v.(workbenchRankCandidate)) }
func (h *workbenchRankHeap) Pop() any {
last := h.rows[len(h.rows)-1]
h.rows = h.rows[:len(h.rows)-1]
return last
}
func workbenchUnloginDays(row *WorkbenchRankRow, today time.Time) int64 {
stamp := row.LastLoginUnix
if stamp == nil || *stamp <= 0 {
stamp = row.CreatedUnix
}
if stamp == nil || *stamp <= 0 {
return 99999
}
date := time.Unix(*stamp, 0).In(portraitLocation)
date = time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, portraitLocation)
if date.After(today) {
return 0
}
return min(int64(today.Sub(date).Hours()/24), 2147483647)
}