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) }