package business import ( "context" "math/big" "sort" "strings" "time" "github.com/go-kratos/kratos/v3/errors" ) var ErrPerformanceInput = errors.BadRequest("SW_PERFORMANCE_INPUT", "绩效统计区间或查询对象不正确") type PerformanceUsecase struct { repo PerformanceRepo teams *TeamUsecase effective *EffectivenessUsecase goals GoalRepo now func() time.Time } func NewPerformanceUsecase(repo PerformanceRepo, teams *TeamUsecase, effective *EffectivenessUsecase, goals GoalRepo) *PerformanceUsecase { return &PerformanceUsecase{repo: repo, teams: teams, effective: effective, goals: goals, now: time.Now} } type performanceAccumulator struct { open, effective int64 pay *big.Rat } func newPerformanceAccumulator() *performanceAccumulator { return &performanceAccumulator{pay: new(big.Rat)} } func (a *performanceAccumulator) metrics() PerformanceMetrics { return PerformanceMetrics{Open: a.open, Effective: a.effective, Pay: portraitDecimal(a.pay, 2)} } func performanceRate(m PerformanceMetrics) *string { if m.Open == 0 { return nil } return stringPointer(portraitDecimal(new(big.Rat).Mul(big.NewRat(m.Effective, m.Open), big.NewRat(100, 1)), 1)) } func addPerformance(a, b PerformanceMetrics) PerformanceMetrics { x, _ := portraitNumber(a.Pay) y, _ := portraitNumber(b.Pay) if x == nil { x = new(big.Rat) } if y == nil { y = new(big.Rat) } return PerformanceMetrics{Open: a.Open + b.Open, Effective: a.Effective + b.Effective, Pay: portraitDecimal(x.Add(x, y), 2)} } type performanceBuckets struct { total *performanceAccumulator periods map[string]*performanceAccumulator } func newPerformanceBuckets(periods []PerformancePeriod) *performanceBuckets { b := &performanceBuckets{total: newPerformanceAccumulator(), periods: map[string]*performanceAccumulator{}} for _, p := range periods { b.periods[p.Key] = newPerformanceAccumulator() } return b } func (b *performanceBuckets) add(key, metric string, pay *big.Rat) { a := b.periods[key] if a == nil { return } switch metric { case "open": a.open++ b.total.open++ case "effective": a.effective++ b.total.effective++ case "pay": a.pay.Add(a.pay, pay) b.total.pay.Add(b.total.pay, pay) } } func (b *performanceBuckets) values() map[string]PerformanceMetrics { m := map[string]PerformanceMetrics{} for key, value := range b.periods { m[key] = value.metrics() } return m } func performanceWindow(now time.Time, f PerformanceFilter) (time.Time, time.Time, []PerformancePeriod, string, error) { now = now.In(portraitLocation) if f.ForManagement { begin, end := DefaultPerformanceRange(now) periods := []PerformancePeriod{} for m := begin; !m.After(end); m = m.AddDate(0, 1, 0) { last := m.AddDate(0, 1, -1) periods = append(periods, PerformancePeriod{Key: m.Format("2006-01"), Label: m.Format("1月"), Begin: m.Format("2006-01-02"), End: last.Format("2006-01-02")}) } return begin, end, periods, "month", nil } if f.Year == 0 { f.Year = now.Year() } if f.BeginMonth == 0 { f.BeginMonth = int(now.Month()) } if f.EndMonth == 0 { f.EndMonth = f.BeginMonth } if f.Year < 1970 || f.Year > now.Year()+1 || f.BeginMonth < 1 || f.EndMonth > 12 || f.EndMonth < f.BeginMonth { return time.Time{}, time.Time{}, nil, "", ErrPerformanceInput } begin := time.Date(f.Year, time.Month(f.BeginMonth), 1, 0, 0, 0, 0, portraitLocation) end := time.Date(f.Year, time.Month(f.EndMonth)+1, 0, 0, 0, 0, 0, portraitLocation) granularity := "month" periods := []PerformancePeriod{} if f.BeginMonth == f.EndMonth { granularity = "day" today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, portraitLocation) for day := begin; !day.After(end) && !day.After(today); day = day.AddDate(0, 0, 1) { date := day.Format("2006-01-02") periods = append(periods, PerformancePeriod{Key: date, Label: day.Format("1月2日"), Begin: date, End: date}) } } else { for month := begin; !month.After(end); month = month.AddDate(0, 1, 0) { last := month.AddDate(0, 1, -1) periods = append(periods, PerformancePeriod{Key: month.Format("2006-01"), Label: month.Format("1月"), Begin: month.Format("2006-01-02"), End: last.Format("2006-01-02")}) } } return begin, end, periods, granularity, nil } func (uc *PerformanceUsecase) Query(ctx context.Context, f PerformanceFilter) (*PerformancePage, error) { return uc.query(ctx, f, nil) } func (uc *PerformanceUsecase) query(ctx context.Context, f PerformanceFilter, ownerIDs []uint) (*PerformancePage, error) { view, err := uc.teams.View(ctx) if err != nil { return nil, err } scope := view.Scope snapshotTime := uc.now().In(portraitLocation) begin, end, periods, grain, err := performanceWindow(snapshotTime, f) if err != nil { return nil, err } if !begin.After(end) { if err := uc.effective.Ensure(ctx); err != nil { return nil, err } } settings, err := uc.effective.Rule(ctx) if err != nil { return nil, err } priorBegin := begin.AddDate(0, -1, 0) priorKey := priorBegin.Format("2006-01") if grain == "day" { priorBegin = begin.AddDate(0, 0, -1) priorKey = priorBegin.Format("2006-01-02") } facts := &PerformanceFacts{Available: true} if !begin.After(end) { facts, err = uc.repo.Facts(ctx, scope, ownerIDs, nil, priorBegin, end) } if err != nil { return nil, err } out := &PerformancePage{Rows: []*PerformanceRow{}, Groups: []*PerformanceGroupRow{}, Periods: periods, Trend: map[string]PerformanceMetrics{}, Begin: begin.Format("2006-01-02"), End: end.Format("2006-01-02"), Granularity: grain, Settings: settings, Available: facts.Available} if !facts.Available { return out, nil } all := newPerformanceBuckets(periods) matrix := newPerformanceBuckets(periods) needle := strings.ToLower(strings.TrimSpace(f.Name)) visible := func(m *BusinessMember) bool { return (f.Level == nil || m.Level == *f.Level) && (needle == "" || strings.Contains(strings.ToLower(memberName(m)+" "+m.Username+" "+m.RealName), needle)) } matrixVisible := func(m *BusinessMember) bool { return visible(m) && (f.GroupID == nil || m.GroupID == *f.GroupID) } prior := newPerformanceAccumulator() memberBuckets := map[uint]*performanceBuckets{} groupBuckets := map[uint64]*performanceBuckets{} byID := map[uint]*BusinessMember{} families := map[uint][]uint{} for _, member := range view.Members { byID[member.ID] = member memberBuckets[member.ID] = newPerformanceBuckets(periods) } descendants := map[uint]int{} for _, owner := range view.Members { families[owner.ID] = memberAncestors(owner, byID) for _, ancestor := range families[owner.ID] { if ancestor != owner.ID { descendants[ancestor]++ } } } groupsByMember := map[uint][]uint64{} for _, group := range view.Groups { groupBuckets[group.ID] = newPerformanceBuckets(periods) for _, id := range group.MemberIDs { if matrixVisible(byID[id]) { groupsByMember[id] = append(groupsByMember[id], group.ID) } } } type attribution struct { members []uint groups []uint64 } owners := map[ChannelKey]attribution{} forOwners := func(ownerIDs []uint) attribution { members, groups := map[uint]bool{}, map[uint64]bool{} for _, owner := range ownerIDs { for _, id := range families[owner] { members[id] = true for _, g := range groupsByMember[id] { groups[g] = true } } } a := attribution{} for id := range members { a.members = append(a.members, id) } for id := range groups { a.groups = append(a.groups, id) } return a } productive := map[uint]bool{} periodKey := func(date string) string { if grain == "month" && len(date) >= 7 { return date[:7] } return date } dateKey := func(value *int64) string { if value == nil || *value <= 0 || *value > snapshotTime.Unix() { return "" } return periodKey(time.Unix(*value, 0).In(portraitLocation).Format("2006-01-02")) } add := func(a attribution, key, metric string, pay *big.Rat) { if key == priorKey { switch metric { case "open": prior.open++ case "effective": prior.effective++ case "pay": prior.pay.Add(prior.pay, pay) } } if all.periods[key] == nil { return } all.add(key, metric, pay) // The matrix is the union of the displayed members' descendant scopes; // overlapping parent/child rows must not count the same channel twice. for _, id := range a.members { if member := byID[id]; member != nil && matrixVisible(member) { matrix.add(key, metric, pay) break } } for _, id := range a.members { memberBuckets[id].add(key, metric, pay) } for _, id := range a.groups { groupBuckets[id].add(key, metric, pay) } } for _, channel := range facts.Channels { a := forOwners(channel.OwnerIDs) owners[channel.ChannelKey] = a open, effective := dateKey(channel.CreatedAt), dateKey(channel.QualifiedAt) add(a, open, "open", nil) add(a, effective, "effective", nil) if all.periods[open] != nil || all.periods[effective] != nil { for _, owner := range channel.OwnerIDs { if byID[owner] != nil { productive[owner] = true } } } } ownerAttributions := map[ChannelKey]attribution{} for _, cash := range facts.Cash { if cash.Date > snapshotTime.Format("2006-01-02") { continue } a, ok := owners[cash.ChannelKey] if cash.OwnerIDs != nil { a, ok = ownerAttributions[cash.ChannelKey] if !ok { a = forOwners(cash.OwnerIDs) ownerAttributions[cash.ChannelKey] = a } ok = true } if !ok { continue } pay, err := portraitNumber(cash.Pay) if err != nil { return nil, err } add(a, periodKey(cash.Date), "pay", pay) } goalEnd := end.Format("2006-01") if f.ForManagement { // The business list shows historical performance and this month's goal. goalEnd = snapshotTime.Format("2006-01") } goals, err := uc.goals.List(ctx, begin.Format("2006-01"), goalEnd) if err != nil { return nil, err } personGoals, groupGoals := map[uint64]map[string]*PerformanceGoal{}, map[uint64]map[string]*PerformanceGoal{} for _, goal := range goals { target := personGoals if goal.Kind == "group" { target = groupGoals } if target[goal.TargetID] == nil { target[goal.TargetID] = map[string]*PerformanceGoal{} } target[goal.TargetID][goal.Month] = goal } rows := map[uint]*PerformanceRow{} for _, member := range view.Members { b := memberBuckets[member.ID] total := b.total.metrics() row := &PerformanceRow{Member: member, Total: total, Rate: performanceRate(total), Periods: b.values(), Goals: personGoals[uint64(member.ID)]} row.DescendantCount = descendants[member.ID] rows[member.ID] = row if matrixVisible(member) { out.Rows = append(out.Rows, row) } } for _, group := range view.Groups { if len(group.MemberIDs) == 0 { continue } b := groupBuckets[group.ID] g := &PerformanceGroupRow{Group: group, Total: PerformanceMetrics{Pay: "0.00"}, UniqueTotal: b.total.metrics(), UniquePeriods: b.values(), Periods: map[string]PerformanceMetrics{}, Members: []*PerformanceRow{}, Goals: groupGoals[group.ID]} for _, period := range periods { g.Periods[period.Key] = PerformanceMetrics{Pay: "0.00"} } match := false for _, id := range group.MemberIDs { if row := rows[id]; row != nil { g.Members = append(g.Members, row) g.Total = addPerformance(g.Total, row.Total) for key, value := range row.Periods { g.Periods[key] = addPerformance(g.Periods[key], value) } if visible(row.Member) { match = true } } } if match { out.Groups = append(out.Groups, g) } } out.Total = all.total.metrics() out.MatrixTotal = matrix.total.metrics() out.MatrixRate = performanceRate(out.MatrixTotal) out.MatrixTrend = matrix.values() out.Prior = prior.metrics() out.Rate = performanceRate(out.Total) out.Trend = all.values() out.ProductiveMembers = int64(len(productive)) groupOrder := map[uint64]int{} for i, g := range view.Groups { groupOrder[g.ID] = i + 1 } sort.SliceStable(out.Rows, func(i, j int) bool { a, b := out.Rows[i], out.Rows[j] ga, gb := a.Member.GroupID, b.Member.GroupID if ga != gb { if ga == 0 { return false } if gb == 0 { return true } return groupOrder[ga] < groupOrder[gb] } comparison := int64(0) switch f.SortField { case "open": comparison = a.Total.Open - b.Total.Open case "effective": comparison = a.Total.Effective - b.Total.Effective case "rate": // Undefined rates always follow defined rates in either direction. if a.Total.Open == 0 || b.Total.Open == 0 { return a.Total.Open != 0 && b.Total.Open == 0 } comparison = int64(big.NewRat(a.Total.Effective, a.Total.Open).Cmp(big.NewRat(b.Total.Effective, b.Total.Open))) } if comparison == 0 { return false } if f.Ascending { return comparison < 0 } return comparison > 0 }) return out, nil } func (uc *PerformanceUsecase) Target(ctx context.Context, kind string, id uint64) (UserScope, []uint, string, bool, error) { view, err := uc.teams.View(ctx) if err != nil { return UserScope{}, nil, "", false, err } selection, name, editable, err := performanceTarget(view, kind, id, nil) return view.Scope, selection.OwnerIDs, name, editable, err } func performanceTarget(view *TeamView, kind string, id uint64, selected []uint) (PerformanceDetailSelection, string, bool, error) { scope := view.Scope rootIDs := []uint{} name := "" editable := false if kind == "person" { for _, m := range view.Members { if uint64(m.ID) == id { rootIDs = append(rootIDs, m.ID) name = memberName(m) editable = m.CanSetGoal } } } else if kind == "group" { for _, g := range view.Groups { if g.ID == id { rootIDs = append(rootIDs, g.MemberIDs...) name = g.Name editable = g.CanSetGoal } } } else if kind == "scope" && id == 0 { if scope.All { return PerformanceDetailSelection{}, "可见范围", false, nil } for _, m := range view.Members { rootIDs = append(rootIDs, m.ID) } name = "可见范围" } else { return PerformanceDetailSelection{}, "", false, ErrPerformanceInput } if len(rootIDs) == 0 { return PerformanceDetailSelection{}, "", false, ErrTeamScope } set := map[uint]bool{} byID := map[uint]*BusinessMember{} for _, m := range view.Members { byID[m.ID] = m } roots := map[uint]bool{} for _, root := range rootIDs { roots[root] = true } if len(selected) > 0 { if kind != "group" { return PerformanceDetailSelection{}, "", false, ErrPerformanceInput } limited := map[uint]bool{} for _, root := range selected { if !roots[root] { return PerformanceDetailSelection{}, "", false, ErrTeamScope } limited[root] = true } roots = limited rootIDs = nil for root := range roots { rootIDs = append(rootIDs, root) } } for _, m := range view.Members { for _, ancestor := range memberAncestors(m, byID) { if roots[ancestor] { set[m.ID] = true break } } } ids := make([]uint, 0, len(set)) for id := range set { ids = append(ids, id) } return PerformanceDetailSelection{OwnerIDs: ids, ContributorIDs: rootIDs}, name, editable, nil } func (uc *PerformanceUsecase) Details(ctx context.Context, kind string, id uint64, begin, end time.Time, metric string, page, size int, members ...uint) ([]*PerformanceChannel, int64, error) { view, err := uc.teams.View(ctx) if err != nil { return nil, 0, err } targetKind := kind if kind == "team" { targetKind = "group" } selection, _, _, err := performanceTarget(view, targetKind, id, members) if err != nil { return nil, 0, err } if kind != "team" { selection.ContributorIDs = nil } if end.Before(begin) || !(metric == "open" || metric == "effective" || metric == "pay" || metric == "all") { return nil, 0, ErrPerformanceInput } if page < 1 { page = 1 } if size < 1 || size > 500 { size = 20 } if page > int(^uint(0)>>1)/size { return nil, 0, ErrPerformanceInput } if err := uc.effective.Ensure(ctx); err != nil { return nil, 0, err } return uc.repo.Details(ctx, view.Scope, selection, nil, begin, end, metric, (page-1)*size, size) }