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

297 lines
12 KiB
Go

package business
import (
"context"
"strconv"
"strings"
"time"
"unicode/utf8"
"kra/internal/biz/business"
"kra/internal/service/dto"
)
type TeamUsecase interface {
View(context.Context) (*business.TeamView, error)
Save(context.Context, uint64, []*business.BusinessGroup) (*business.TeamView, error)
UpdateProfile(context.Context, business.MemberProfile) error
}
type PerformanceUsecase interface {
Query(context.Context, business.PerformanceFilter) (*business.PerformancePage, error)
Details(context.Context, string, uint64, time.Time, time.Time, string, int, int, ...uint) ([]*business.PerformanceChannel, int64, error)
}
type GoalUsecase interface {
Mine(context.Context) (*business.GoalMonth, error)
Save(context.Context, *business.PerformanceGoal) error
History(context.Context, string, uint64) (*business.GoalPage, error)
}
type DashboardUsecase interface {
Overview(context.Context, []string, string, *time.Time, *time.Time) (*business.DashboardOverview, error)
}
type BusinessPerformanceService struct {
teams TeamUsecase
performance PerformanceUsecase
goals GoalUsecase
dashboard DashboardUsecase
}
func NewBusinessPerformanceService(teams *business.TeamUsecase, performance *business.PerformanceUsecase, goals *business.GoalUsecase, dashboard *business.DashboardUsecase) *BusinessPerformanceService {
return &BusinessPerformanceService{teams: teams, performance: performance, goals: goals, dashboard: dashboard}
}
func (s *BusinessPerformanceService) Overview(ctx context.Context, req *dto.DashboardQuery) (*dto.DashboardOverview, error) {
if req == nil {
return nil, business.ErrPerformanceInput
}
sources, err := normalizePlatforms(req.Sources)
if err != nil {
return nil, err
}
var begin, end *time.Time
if req.Period == "custom" {
location := time.FixedZone("Asia/Shanghai", 8*3600)
b, err := time.ParseInLocation("2006-01-02", req.Begin, location)
if err != nil {
return nil, business.ErrPerformanceInput
}
e, err := time.ParseInLocation("2006-01-02", req.End, location)
if err != nil || e.Before(b) {
return nil, business.ErrPerformanceInput
}
begin, end = &b, &e
}
v, err := s.dashboard.Overview(ctx, sources, req.Period, begin, end)
if err != nil {
return nil, err
}
r := &dto.DashboardOverview{Cards: map[string]*dto.WorkbenchCard{}, Begin: v.Begin, End: v.End, Settings: dto.OtherSettingsValues{Amount: v.Settings.Amount, Days: v.Settings.Days}, Available: v.Available}
for key, card := range v.Cards {
r.Cards[key] = workbenchCardResponse(card)
}
return r, nil
}
func (s *BusinessPerformanceService) MyGoal(ctx context.Context) (*dto.GoalMonthResponse, error) {
v, err := s.goals.Mine(ctx)
if err != nil {
return nil, err
}
if v == nil {
return nil, nil
}
return &dto.GoalMonthResponse{Month: v.Month, Goal: goalResponse(v.Goal), Actual: metricResponse(v.Actual), Available: v.Available}, nil
}
func memberResponse(m *business.BusinessMember) *dto.BusinessMemberResponse {
return &dto.BusinessMemberResponse{BusinessAccountResponse: accountResponse(&m.Account), HireDate: m.HireDate, Employment: m.Employment, GroupID: m.GroupID, GroupName: m.GroupName, CanEdit: m.CanEdit, CanSetGoal: m.CanSetGoal}
}
func groupResponse(g *business.BusinessGroup) *dto.BusinessGroupResponse {
return &dto.BusinessGroupResponse{ID: g.ID, Name: g.Name, MemberIDs: g.MemberIDs, CanEdit: g.CanEdit, CanSetGoal: g.CanSetGoal, Mine: g.Mine, Partial: g.Partial, Overlapping: g.Overlapping, Relation: g.Relation, ChannelCount: g.ChannelCount}
}
func teamResponse(v *business.TeamView) *dto.TeamResponse {
r := &dto.TeamResponse{Revision: v.Revision, Groups: []*dto.BusinessGroupResponse{}, Members: []*dto.BusinessMemberResponse{}, CanManage: v.CanManage, ViewerID: v.ViewerID}
for _, g := range v.Groups {
r.Groups = append(r.Groups, groupResponse(g))
}
for _, m := range v.Members {
r.Members = append(r.Members, memberResponse(m))
}
return r
}
func (s *BusinessPerformanceService) Teams(ctx context.Context) (*dto.TeamResponse, error) {
v, err := s.teams.View(ctx)
if err != nil {
return nil, err
}
return teamResponse(v), nil
}
func (s *BusinessPerformanceService) SaveTeams(ctx context.Context, req *dto.GroupSaveRequest) (*dto.TeamResponse, error) {
if req == nil || req.Revision == nil || req.Groups == nil {
return nil, business.ErrTeamInput
}
groups := []*business.BusinessGroup{}
for _, g := range req.Groups {
if g == nil || utf8.RuneCountInString(strings.TrimSpace(g.Name)) < 1 || utf8.RuneCountInString(strings.TrimSpace(g.Name)) > 10 {
return nil, business.ErrTeamInput
}
groups = append(groups, &business.BusinessGroup{ID: g.ID, Name: g.Name, MemberIDs: g.MemberIDs})
}
v, err := s.teams.Save(ctx, *req.Revision, groups)
if err != nil {
return nil, err
}
return teamResponse(v), nil
}
func (s *BusinessPerformanceService) UpdateProfile(ctx context.Context, req *dto.MemberProfileRequest) error {
if req == nil || req.UserID == 0 || utf8.RuneCountInString(strings.TrimSpace(req.RealName)) < 1 || utf8.RuneCountInString(req.RealName) > 30 || !validMemberPhone(req.Phone) {
return business.ErrMemberInput
}
if _, err := time.Parse("2006-01-02", req.HireDate); err != nil {
return business.ErrMemberInput
}
if req.Employment != "employed" && req.Employment != "probation" && req.Employment != "departed" {
return business.ErrMemberInput
}
if req.Password != "" && (len(req.Password) > 72 || utf8.RuneCountInString(req.Password) < 5 || utf8.RuneCountInString(req.Password) > 50) {
return business.ErrMemberInput
}
return s.teams.UpdateProfile(ctx, business.MemberProfile{ID: req.UserID, RealName: strings.TrimSpace(req.RealName), Phone: strings.TrimSpace(req.Phone), HireDate: req.HireDate, Employment: req.Employment, Password: req.Password})
}
func validMemberPhone(value string) bool {
if len(value) > 20 {
return false
}
for _, r := range value {
if (r < '0' || r > '9') && r != ' ' && r != '-' {
return false
}
}
return true
}
func performanceRequest(req *dto.PerformanceQuery, management bool) (business.PerformanceFilter, error) {
if req == nil {
return business.PerformanceFilter{}, business.ErrPerformanceInput
}
if (req.BeginMonth == nil) != (req.EndMonth == nil) {
return business.PerformanceFilter{}, business.ErrPerformanceInput
}
f := business.PerformanceFilter{Name: strings.TrimSpace(req.Name), Level: req.Level, GroupID: req.GroupID, SortField: req.Sort, Ascending: req.Order == "asc", ForManagement: management}
if req.Year != nil {
if *req.Year < 1970 || *req.Year > 9999 {
return f, business.ErrPerformanceInput
}
f.Year = *req.Year
}
for _, pair := range []struct {
value *int
dst *int
}{{req.BeginMonth, &f.BeginMonth}, {req.EndMonth, &f.EndMonth}} {
if pair.value != nil {
if *pair.value < 1 || *pair.value > 12 {
return f, business.ErrPerformanceInput
}
*pair.dst = *pair.value
}
}
if req.Level != nil && (*req.Level < 1 || *req.Level > 3) {
return f, business.ErrPerformanceInput
}
return f, nil
}
func metricResponse(m business.PerformanceMetrics) dto.PerformanceMetrics {
return dto.PerformanceMetrics{Open: m.Open, Effective: m.Effective, Pay: m.Pay}
}
func metricsResponse(values map[string]business.PerformanceMetrics) map[string]dto.PerformanceMetrics {
r := map[string]dto.PerformanceMetrics{}
for key, m := range values {
r[key] = metricResponse(m)
}
return r
}
func goalResponse(g *business.PerformanceGoal) *dto.GoalResponse {
if g == nil {
return nil
}
return &dto.GoalResponse{Open: g.Open, Effective: g.Effective, Amount: g.Amount, UpdatedBy: g.UpdatedBy, UpdatedAt: g.UpdatedAt.Format(time.RFC3339)}
}
func goalsResponse(goals map[string]*business.PerformanceGoal) map[string]*dto.GoalResponse {
r := map[string]*dto.GoalResponse{}
for month, g := range goals {
r[month] = goalResponse(g)
}
return r
}
func performanceRowResponse(r *business.PerformanceRow) *dto.PerformanceRow {
return &dto.PerformanceRow{Member: memberResponse(r.Member), Total: metricResponse(r.Total), Rate: r.Rate, Periods: metricsResponse(r.Periods), Goals: goalsResponse(r.Goals), DescendantCount: r.DescendantCount}
}
func performanceResponse(v *business.PerformancePage) *dto.PerformancePage {
r := &dto.PerformancePage{Rows: []*dto.PerformanceRow{}, Groups: []*dto.PerformanceGroupRow{}, Total: metricResponse(v.Total), Rate: v.Rate, ProductiveMembers: v.ProductiveMembers, Prior: metricResponse(v.Prior), Periods: []dto.PerformancePeriod{}, Trend: metricsResponse(v.Trend), Begin: v.Begin, End: v.End, Granularity: v.Granularity, Settings: dto.OtherSettingsValues{Amount: v.Settings.Amount, Days: v.Settings.Days}, Available: v.Available}
r.MatrixTotal, r.MatrixRate, r.MatrixTrend = metricResponse(v.MatrixTotal), v.MatrixRate, metricsResponse(v.MatrixTrend)
for _, row := range v.Rows {
r.Rows = append(r.Rows, performanceRowResponse(row))
}
for _, group := range v.Groups {
g := &dto.PerformanceGroupRow{Group: groupResponse(group.Group), Total: metricResponse(group.Total), UniqueTotal: metricResponse(group.UniqueTotal), Periods: metricsResponse(group.Periods), UniquePeriods: metricsResponse(group.UniquePeriods), Members: []*dto.PerformanceRow{}, Goals: goalsResponse(group.Goals)}
for _, member := range group.Members {
g.Members = append(g.Members, performanceRowResponse(member))
}
r.Groups = append(r.Groups, g)
}
for _, p := range v.Periods {
r.Periods = append(r.Periods, dto.PerformancePeriod{Key: p.Key, Label: p.Label, Begin: p.Begin, End: p.End})
}
return r
}
func (s *BusinessPerformanceService) Query(ctx context.Context, req *dto.PerformanceQuery, management bool) (*dto.PerformancePage, error) {
f, err := performanceRequest(req, management)
if err != nil {
return nil, err
}
v, err := s.performance.Query(ctx, f)
if err != nil {
return nil, err
}
return performanceResponse(v), nil
}
func (s *BusinessPerformanceService) Details(ctx context.Context, req *dto.PerformanceDetailQuery) (*dto.PerformanceDetailPage, error) {
if req == nil {
return nil, business.ErrPerformanceInput
}
location := time.FixedZone("Asia/Shanghai", 8*3600)
begin, err := time.ParseInLocation("2006-01-02", req.Begin, location)
if err != nil {
return nil, business.ErrPerformanceInput
}
end, err := time.ParseInLocation("2006-01-02", req.End, location)
if err != nil || end.Before(begin) {
return nil, business.ErrPerformanceInput
}
var members []uint
if req.MemberIDs != "" {
parts := strings.Split(req.MemberIDs, ",")
if len(parts) > 10000 {
return nil, business.ErrPerformanceInput
}
for _, part := range parts {
id, err := strconv.ParseUint(part, 10, strconv.IntSize)
if err != nil || id == 0 {
return nil, business.ErrPerformanceInput
}
members = append(members, uint(id))
}
}
rows, total, err := s.performance.Details(ctx, req.Kind, req.TargetID, begin, end, req.Metric, req.Page, req.Size, members...)
if err != nil {
return nil, err
}
out := &dto.PerformanceDetailPage{Rows: []*dto.PerformanceChannel{}, Total: total}
for _, r := range rows {
out.Rows = append(out.Rows, &dto.PerformanceChannel{ContributorID: r.ContributorID, ContributorName: r.ContributorName, SourceDB: r.SourceDB, SourceDBLabel: business.PlatformName(r.SourceDB), ID: r.ID, Account: r.Account, Name: r.Name, OwnerName: r.OwnerName, CreatedAt: r.CreatedAt, QualifiedAt: r.QualifiedAt, WindowPay: r.WindowPay, PeriodPay: r.PeriodPay})
}
return out, nil
}
func (s *BusinessPerformanceService) GoalHistory(ctx context.Context, req *dto.GoalQuery) (*dto.GoalPage, error) {
if req == nil {
return nil, business.ErrGoalInput
}
v, err := s.goals.History(ctx, req.Kind, req.TargetID)
if err != nil {
return nil, err
}
r := &dto.GoalPage{Kind: v.Kind, TargetID: v.TargetID, Name: v.Name, CanEdit: v.CanEdit, Months: []dto.GoalMonthResponse{}, MemberGoals: metricsResponse(v.MemberGoals)}
for _, m := range v.Months {
r.Months = append(r.Months, dto.GoalMonthResponse{Month: m.Month, Goal: goalResponse(m.Goal), Actual: metricResponse(m.Actual), Status: m.Status, Missing: m.Missing, CanEdit: m.CanEdit, Available: m.Available})
}
return r, nil
}
func (s *BusinessPerformanceService) SaveGoal(ctx context.Context, req *dto.GoalRequest) error {
if req == nil {
return business.ErrGoalInput
}
for _, value := range []*int64{req.Open, req.Effective, req.Amount} {
if value != nil && (*value < 0 || *value > business.MaxSettingsInteger) {
return business.ErrGoalInput
}
}
return s.goals.Save(ctx, &business.PerformanceGoal{Kind: req.Kind, TargetID: req.TargetID, Month: req.Month, Open: req.Open, Effective: req.Effective, Amount: req.Amount})
}