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

529 lines
15 KiB
Go

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
inMatrix bool
}
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)
a.inMatrix = a.inMatrix || matrixVisible(byID[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)
if a.inMatrix {
matrix.add(key, metric, pay)
}
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)
}
goals, err := uc.goals.List(ctx, begin.Format("2006-01"), end.Format("2006-01"))
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)
}