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