282 lines
9.3 KiB
Go
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)
|
|
}
|