feat: 完善小组绩效与有效端统计并修复审查问题
This commit is contained in:
@@ -98,6 +98,17 @@ func validateAccount(req *dto.BusinessAccountRequest, create bool) (*int, error)
|
||||
if req == nil {
|
||||
return nil, business.ErrAccountInput
|
||||
}
|
||||
if create && req.HireDate == nil {
|
||||
return nil, business.ErrMemberInput
|
||||
}
|
||||
if req.Phone != nil && !validMemberPhone(*req.Phone) {
|
||||
return nil, business.ErrMemberInput
|
||||
}
|
||||
if req.HireDate != nil {
|
||||
if _, err := time.Parse("2006-01-02", *req.HireDate); err != nil {
|
||||
return nil, business.ErrMemberInput
|
||||
}
|
||||
}
|
||||
if create && (req.Username == nil || req.RealName == nil || strings.TrimSpace(*req.RealName) == "" || req.Password == "") {
|
||||
return nil, business.ErrAccountInput
|
||||
}
|
||||
@@ -107,7 +118,7 @@ func validateAccount(req *dto.BusinessAccountRequest, create bool) (*int, error)
|
||||
for _, field := range []struct {
|
||||
s *string
|
||||
max int
|
||||
}{{req.NickName, 30}, {req.RealName, 30}, {req.Phone, 11}, {req.Email, 50}, {req.QQNumber, 20}, {req.RejectReason, 200}} {
|
||||
}{{req.NickName, 30}, {req.RealName, 30}, {req.Phone, 20}, {req.Email, 50}, {req.QQNumber, 20}, {req.RejectReason, 200}} {
|
||||
if field.s != nil && (utf8.RuneCountInString(*field.s) > field.max || strings.ContainsAny(*field.s, "<>")) {
|
||||
return nil, business.ErrAccountInput
|
||||
}
|
||||
@@ -147,6 +158,7 @@ func (s *AccountService) Create(ctx context.Context, req *dto.BusinessAccountReq
|
||||
}
|
||||
a := &business.Account{Username: stringValue(req.Username), NickName: stringValue(req.NickName), RealName: stringValue(req.RealName), Phone: stringValue(req.Phone),
|
||||
Email: stringValue(req.Email), QQNumber: stringValue(req.QQNumber), Password: req.Password, RejectReason: stringValue(req.RejectReason)}
|
||||
a.HireDate = stringValue(req.HireDate)
|
||||
if status != nil {
|
||||
a.Status = *status
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ func (f *accountUsecaseFake) List(_ context.Context, filter business.AccountFilt
|
||||
|
||||
func TestAccountTransportIgnoresForgedOwnershipAndPreservesDefaultStatus(t *testing.T) {
|
||||
var req dto.BusinessAccountRequest
|
||||
if err := json.Unmarshal([]byte(`{"userId":999,"userName":"child","realName":"姓名","password":"abc123","level":1,"parentId":999,"roleIds":[888]}`), &req); err != nil {
|
||||
if err := json.Unmarshal([]byte(`{"userId":999,"userName":"child","realName":"姓名","password":"abc123","hireDate":"2026-11-01","level":1,"parentId":999,"roleIds":[888]}`), &req); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f := &accountUsecaseFake{}
|
||||
@@ -67,3 +67,21 @@ func TestAccountQueryEmptyFiltersAndPageBounds(t *testing.T) {
|
||||
t.Fatalf("溢出分页应拒绝: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateBusinessRequiresHireDateAndValidPhone(t *testing.T) {
|
||||
f := &accountUsecaseFake{}
|
||||
s := &AccountService{uc: f}
|
||||
name, owner, phone, hire := "member", "负责人", "abc", "2026-11-01"
|
||||
req := &dto.BusinessAccountRequest{Username: &name, RealName: &owner, Password: "test-password"}
|
||||
if _, err := s.Create(context.Background(), req); err != business.ErrMemberInput || f.created != nil {
|
||||
t.Fatal("missing hire date must not reach usecase")
|
||||
}
|
||||
req.HireDate, req.Phone = &hire, &phone
|
||||
if _, err := s.Create(context.Background(), req); err != business.ErrMemberInput || f.created != nil {
|
||||
t.Fatal("invalid phone must not reach usecase")
|
||||
}
|
||||
phone = "138 0013 8000"
|
||||
if _, err := s.Create(context.Background(), req); err != nil || f.created.HireDate != hire {
|
||||
t.Fatalf("valid profile rejected: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ func (s *AnalyticsService) Channels(ctx context.Context, req *dto.AnalyticsQuery
|
||||
result.Rows = append(result.Rows, channelAnalyticsRow(row, false))
|
||||
}
|
||||
if page.Summary != nil {
|
||||
result.EffectiveCount = &page.EffectiveCount
|
||||
result.Summary = channelAnalyticsRow(page.Summary, true)
|
||||
result.ChannelCount = &page.Total
|
||||
result.TotalRegisterCount = &page.Summary.Counts.Register
|
||||
|
||||
@@ -41,14 +41,14 @@ func (s *ChannelService) List(ctx context.Context, req *dto.ChannelQuery) (*dto.
|
||||
if page > int(^uint(0)>>1)/size {
|
||||
return nil, business.ErrChannelInput
|
||||
}
|
||||
filter := business.ChannelFilter{Sources: sources, BusierUserID: req.BusierUserID, BusierAccount: strings.TrimSpace(req.BusierAccount), Keyword: strings.TrimSpace(req.Keyword), Level: req.Level, RegisterType: req.RegisterType, Status: req.Status, LockStatus: req.LockStatus}
|
||||
filter := business.ChannelFilter{Sources: sources, BusierUserID: req.BusierUserID, BusierAccount: strings.TrimSpace(req.BusierAccount), Keyword: strings.TrimSpace(req.Keyword), Level: req.Level, RegisterType: req.RegisterType, Status: req.Status, LockStatus: req.LockStatus, Effective: req.Effective}
|
||||
result, err := s.uc.List(ctx, filter, business.ListOffset((page-1)*size), business.ListLimit(size))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := &dto.ChannelPage{Rows: []*dto.ChannelResponse{}, Total: result.Total, Available: result.Available}
|
||||
for _, c := range result.Rows {
|
||||
out.Rows = append(out.Rows, &dto.ChannelResponse{ID: c.ID, SourceDB: c.SourceDB, PlatformName: business.PlatformName(c.SourceDB), Account: c.Account, Nickname: c.Nickname, Level: c.Level, ParentID: c.ParentID, ParentName: c.ParentName, RealName: c.RealName, RegisterType: c.RegisterType, BusierID: c.BusierID, BusierUserID: c.BusierUserID, BusierDisplayName: c.BusinessDisplayName(), Status: c.Status, LockStatus: c.LockStatus, GameCount: c.GameCount, PlayerCount: c.PlayerCount, TotalPayAmount: c.TotalPayAmount})
|
||||
out.Rows = append(out.Rows, &dto.ChannelResponse{ID: c.ID, SourceDB: c.SourceDB, PlatformName: business.PlatformName(c.SourceDB), Account: c.Account, Nickname: c.Nickname, Level: c.Level, ParentID: c.ParentID, ParentName: c.ParentName, RealName: c.RealName, RegisterType: c.RegisterType, BusierID: c.BusierID, BusierUserID: c.BusierUserID, BusierDisplayName: c.BusinessDisplayName(), Status: c.Status, LockStatus: c.LockStatus, GameCount: c.GameCount, PlayerCount: c.PlayerCount, TotalPayAmount: c.TotalPayAmount, Effective: c.QualifiedAt != nil, QualifiedAt: c.QualifiedAt, CreatedAt: c.CreatedUnix})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
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})
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"kra/internal/biz/business"
|
||||
"kra/internal/service/dto"
|
||||
)
|
||||
|
||||
func (s *BusinessPerformanceService) Export(ctx context.Context, req *dto.PerformanceQuery) ([]byte, error) {
|
||||
f, err := performanceRequest(req, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := s.performance.Query(ctx, f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !data.Available {
|
||||
return nil, business.ErrWorkbenchUnavailable
|
||||
}
|
||||
if len(data.Rows) > 10000 {
|
||||
return nil, business.ErrLedgerExportLimit
|
||||
}
|
||||
if req.View == "groups" {
|
||||
return exportGroupPerformance(data)
|
||||
}
|
||||
if req.View == "trend" {
|
||||
return exportPerformanceTrend(data)
|
||||
}
|
||||
headers := []string{"小组", "商务", "入职日期", "在职状态", "区间开端", "区间有效端", "区间充值流水", "有效率"}
|
||||
for i := len(data.Periods) - 1; i >= 0; i-- {
|
||||
p := data.Periods[i]
|
||||
headers = append(headers, p.Key+" 开端", p.Key+" 有效端", p.Key+" 充值流水")
|
||||
}
|
||||
rows := [][]ledgerCell{}
|
||||
appendRow := func(group, name, hire, employment string, total business.PerformanceMetrics, rate *string, periods map[string]business.PerformanceMetrics) {
|
||||
r := []ledgerCell{textCell(group), textCell(name), textCell(hire), textCell(employment), numericCell(strconv.FormatInt(total.Open, 10)), numericCell(strconv.FormatInt(total.Effective, 10)), numericCell(total.Pay), textCell("")}
|
||||
if rate != nil {
|
||||
r[7] = textCell(*rate + "%")
|
||||
}
|
||||
for i := len(data.Periods) - 1; i >= 0; i-- {
|
||||
m := periods[data.Periods[i].Key]
|
||||
r = append(r, numericCell(strconv.FormatInt(m.Open, 10)), numericCell(strconv.FormatInt(m.Effective, 10)), numericCell(m.Pay))
|
||||
}
|
||||
rows = append(rows, r)
|
||||
}
|
||||
{
|
||||
groups := map[uint64]*business.PerformanceGroupRow{}
|
||||
for _, g := range data.Groups {
|
||||
groups[g.Group.ID] = g
|
||||
}
|
||||
for i, r := range data.Rows {
|
||||
name := r.Member.NickName
|
||||
if name == "" {
|
||||
name = r.Member.Username
|
||||
}
|
||||
appendRow(r.Member.GroupName, name, r.Member.HireDate, map[string]string{"employed": "在职", "probation": "试用", "departed": "离职"}[r.Member.Employment], r.Total, r.Rate, r.Periods)
|
||||
if g := groups[r.Member.GroupID]; g != nil && (i == len(data.Rows)-1 || data.Rows[i+1].Member.GroupID != r.Member.GroupID) {
|
||||
appendRow(g.Group.Name, g.Group.Name+" 小计", "", "", g.UniqueTotal, business.PerformanceRate(g.UniqueTotal), g.UniquePeriods)
|
||||
}
|
||||
}
|
||||
}
|
||||
appendRow("筛选结果去重合计", "上下级不重复计算", "", "", data.MatrixTotal, data.MatrixRate, data.MatrixTrend)
|
||||
return ledgerWorkbook("商务绩效统计", headers, rows)
|
||||
}
|
||||
|
||||
func exportGroupPerformance(data *business.PerformancePage) ([]byte, error) {
|
||||
headers := []string{"小组", "人数", "区间开端", "区间有效端", "区间流水", "开端完成率", "有效端完成率", "流水完成率"}
|
||||
for i := len(data.Periods) - 1; i >= 0; i-- {
|
||||
p := data.Periods[i]
|
||||
for _, metric := range []string{"开端", "有效端", "流水"} {
|
||||
headers = append(headers, p.Key+" "+metric+"目标", p.Key+" "+metric+"实际")
|
||||
}
|
||||
}
|
||||
rows := [][]ledgerCell{}
|
||||
for _, g := range data.Groups {
|
||||
row := []ledgerCell{textCell(g.Group.Name), numericCell(strconv.Itoa(len(g.Group.MemberIDs))), numericCell(strconv.FormatInt(g.Total.Open, 10)), numericCell(strconv.FormatInt(g.Total.Effective, 10)), numericCell(g.Total.Pay)}
|
||||
for _, key := range []string{"open", "effective", "pay"} {
|
||||
rate := business.GoalCompletion(g.Periods, g.Goals, key)
|
||||
if rate == nil {
|
||||
row = append(row, textCell("—"))
|
||||
} else {
|
||||
row = append(row, textCell(*rate+"%"))
|
||||
}
|
||||
}
|
||||
for i := len(data.Periods) - 1; i >= 0; i-- {
|
||||
p := data.Periods[i]
|
||||
goal := g.Goals[p.Key[:7]]
|
||||
value := g.Periods[p.Key]
|
||||
for _, key := range []string{"open", "effective", "pay"} {
|
||||
target := business.GoalPeriodTarget(goal, key, p.Key, data.Granularity)
|
||||
if target == nil {
|
||||
row = append(row, textCell("—"))
|
||||
} else {
|
||||
row = append(row, numericCell(*target))
|
||||
}
|
||||
if goal == nil {
|
||||
row = append(row, textCell("—"))
|
||||
continue
|
||||
}
|
||||
actual := value.Pay
|
||||
if key == "open" {
|
||||
actual = strconv.FormatInt(value.Open, 10)
|
||||
} else if key == "effective" {
|
||||
actual = strconv.FormatInt(value.Effective, 10)
|
||||
}
|
||||
row = append(row, numericCell(actual))
|
||||
}
|
||||
}
|
||||
rows = append(rows, row)
|
||||
}
|
||||
return ledgerWorkbook("小组绩效", headers, rows)
|
||||
}
|
||||
|
||||
func exportPerformanceTrend(data *business.PerformancePage) ([]byte, error) {
|
||||
headers := []string{"日期/月份", "开端数", "有效端数", "充值流水", "有效率", "开端环比", "有效端环比", "流水环比"}
|
||||
rows := [][]ledgerCell{}
|
||||
for i := len(data.Periods) - 1; i >= 0; i-- {
|
||||
p := data.Periods[i]
|
||||
value := data.Trend[p.Key]
|
||||
previous := data.Prior
|
||||
if i > 0 {
|
||||
previous = data.Trend[data.Periods[i-1].Key]
|
||||
}
|
||||
row := []ledgerCell{textCell(p.Key), numericCell(strconv.FormatInt(value.Open, 10)), numericCell(strconv.FormatInt(value.Effective, 10)), numericCell(value.Pay)}
|
||||
rate := business.PerformanceRate(value)
|
||||
if rate == nil {
|
||||
row = append(row, textCell("—"))
|
||||
} else {
|
||||
row = append(row, textCell(*rate+"%"))
|
||||
}
|
||||
for _, key := range []string{"open", "effective", "pay"} {
|
||||
change := business.PerformanceChange(value, previous, key)
|
||||
if change == nil {
|
||||
row = append(row, textCell("—"))
|
||||
} else {
|
||||
row = append(row, textCell(*change+"%"))
|
||||
}
|
||||
}
|
||||
rows = append(rows, row)
|
||||
}
|
||||
return ledgerWorkbook("趋势图", headers, rows)
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/xuri/excelize/v2"
|
||||
"kra/internal/biz/business"
|
||||
"kra/internal/service/dto"
|
||||
)
|
||||
|
||||
type performanceServiceFake struct {
|
||||
data *business.PerformancePage
|
||||
calls int
|
||||
filter business.PerformanceFilter
|
||||
}
|
||||
|
||||
func (f *performanceServiceFake) Query(_ context.Context, filter business.PerformanceFilter) (*business.PerformancePage, error) {
|
||||
f.calls++
|
||||
f.filter = filter
|
||||
return f.data, nil
|
||||
}
|
||||
func (f *performanceServiceFake) Details(context.Context, string, uint64, time.Time, time.Time, string, int, int, ...uint) ([]*business.PerformanceChannel, int64, error) {
|
||||
f.calls++
|
||||
return nil, 0, nil
|
||||
}
|
||||
|
||||
type goalServiceFake struct {
|
||||
goal *business.PerformanceGoal
|
||||
calls int
|
||||
}
|
||||
|
||||
type memberProfileFake struct {
|
||||
TeamUsecase
|
||||
profile *business.MemberProfile
|
||||
}
|
||||
|
||||
func (f *memberProfileFake) UpdateProfile(_ context.Context, value business.MemberProfile) error {
|
||||
f.profile = &value
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestMemberProfileValidatesPhoneBeforeDelegating(t *testing.T) {
|
||||
f := &memberProfileFake{}
|
||||
s := &BusinessPerformanceService{teams: f}
|
||||
req := &dto.MemberProfileRequest{UserID: 9, RealName: "成员", Phone: "abc", HireDate: "2026-11-01", Employment: "employed"}
|
||||
if err := s.UpdateProfile(context.Background(), req); err != business.ErrMemberInput || f.profile != nil {
|
||||
t.Fatal("invalid phone reached usecase")
|
||||
}
|
||||
for _, phone := range []string{"", "138 0013 8000", "010-12345678"} {
|
||||
req.Phone = phone
|
||||
if err := s.UpdateProfile(context.Background(), req); err != nil || f.profile.Phone != phone {
|
||||
t.Fatalf("valid phone rejected: %q %v", phone, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (f *goalServiceFake) Save(_ context.Context, g *business.PerformanceGoal) error {
|
||||
f.calls++
|
||||
f.goal = g
|
||||
return nil
|
||||
}
|
||||
func (f *goalServiceFake) History(context.Context, string, uint64) (*business.GoalPage, error) {
|
||||
return &business.GoalPage{}, nil
|
||||
}
|
||||
func (f *goalServiceFake) Mine(context.Context) (*business.GoalMonth, error) { return nil, nil }
|
||||
|
||||
func TestPerformanceServiceValidatesDatesAndPreservesNullableGoals(t *testing.T) {
|
||||
f := &performanceServiceFake{data: &business.PerformancePage{Available: true}}
|
||||
g := &goalServiceFake{}
|
||||
s := &BusinessPerformanceService{performance: f, goals: g}
|
||||
one, bad := 1, 13
|
||||
for _, req := range []*dto.PerformanceQuery{nil, {BeginMonth: &one}, {BeginMonth: &one, EndMonth: &bad}, {Level: &bad}} {
|
||||
if _, err := s.Query(context.Background(), req, false); err != business.ErrPerformanceInput {
|
||||
t.Fatalf("无效查询应在进入用例前失败: %v", err)
|
||||
}
|
||||
}
|
||||
if f.calls != 0 {
|
||||
t.Fatal("无效查询访问了用例")
|
||||
}
|
||||
zero := int64(0)
|
||||
negative := int64(-1)
|
||||
if err := s.SaveGoal(context.Background(), &dto.GoalRequest{Kind: "person", TargetID: 9, Month: "2026-11", Open: &negative}); err != business.ErrGoalInput || g.calls != 0 {
|
||||
t.Fatal("负数目标不应进入用例")
|
||||
}
|
||||
if err := s.SaveGoal(context.Background(), &dto.GoalRequest{Kind: "person", TargetID: 9, Month: "2026-11", Open: &zero}); err != nil || g.goal.Open == nil || *g.goal.Open != 0 || g.goal.Amount != nil {
|
||||
t.Fatal("保存时应保留零和留空的差异")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPerformanceExportsMatchViewsAndKeepPreciseAmounts(t *testing.T) {
|
||||
open, amount := int64(10), int64(3000)
|
||||
periods := []business.PerformancePeriod{{Key: "2026-10", Label: "10月"}, {Key: "2026-11", Label: "11月"}}
|
||||
values := map[string]business.PerformanceMetrics{"2026-10": {Open: 1, Effective: 0, Pay: "10.00"}, "2026-11": {Open: 12, Effective: 2, Pay: "9007199254740993.01"}}
|
||||
member := &business.PerformanceRow{Member: &business.BusinessMember{Account: business.Account{ID: 9, Username: "=1+1"}, GroupID: 1, GroupName: "甲组"}, Total: values["2026-11"], Periods: values}
|
||||
group := &business.PerformanceGroupRow{Group: &business.BusinessGroup{ID: 1, Name: "甲组", MemberIDs: []uint{9}}, Total: values["2026-11"], UniqueTotal: values["2026-11"], Periods: values, UniquePeriods: values, Members: []*business.PerformanceRow{member}, Goals: map[string]*business.PerformanceGoal{"2026-11": {Open: &open, Amount: &amount}}}
|
||||
data := &business.PerformancePage{Available: true, Periods: periods, Rows: []*business.PerformanceRow{member}, Groups: []*business.PerformanceGroupRow{group}, Trend: values, Total: values["2026-11"], MatrixTotal: values["2026-11"], MatrixTrend: values, Granularity: "month", Prior: business.PerformanceMetrics{Pay: "0.00"}}
|
||||
s := &BusinessPerformanceService{performance: &performanceServiceFake{data: data}}
|
||||
for _, sample := range []struct {
|
||||
view, sheet string
|
||||
rows int
|
||||
}{{"groups", "小组绩效", 2}, {"trend", "趋势图", 3}, {"business", "商务绩效统计", 4}} {
|
||||
blob, err := s.Export(context.Background(), &dto.PerformanceQuery{View: sample.view})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
file, err := excelize.OpenReader(bytes.NewReader(blob))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rows, err := file.GetRows(sample.sheet)
|
||||
if err != nil || len(rows) != sample.rows {
|
||||
t.Fatalf("视图导出行数不符或包含了展开成员: %s rows=%d %v", sample.view, len(rows), err)
|
||||
}
|
||||
if sample.view == "groups" {
|
||||
value, _ := file.GetCellValue(sample.sheet, "N2", excelize.Options{RawCellValue: true})
|
||||
if value != "9007199254740993.01" {
|
||||
t.Fatalf("金额不能经过浮点转换: %s", value)
|
||||
}
|
||||
if rows[1][14] != "—" || rows[1][15] != "—" {
|
||||
t.Fatal("未设目标月份必须目标/实际分格占位")
|
||||
}
|
||||
}
|
||||
if sample.view == "business" {
|
||||
formula, _ := file.GetCellFormula(sample.sheet, "B2")
|
||||
if formula != "" || rows[1][1] != "=1+1" {
|
||||
t.Fatal("用户名称必须写成文本")
|
||||
}
|
||||
}
|
||||
if sample.view == "trend" && rows[1][0] != "2026-11" {
|
||||
t.Fatal("趋势导出应与页面倒序一致")
|
||||
}
|
||||
file.Close()
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,7 @@ type GameAnalyticsRow struct {
|
||||
ARPPU string `json:"arppu"`
|
||||
}
|
||||
type ChannelAnalyticsPage struct {
|
||||
EffectiveCount *int64 `json:"effectiveCount"`
|
||||
Rows []*ChannelAnalyticsRow `json:"rows"`
|
||||
Total int64 `json:"total"`
|
||||
ChannelCount *int64 `json:"channelCount"`
|
||||
|
||||
@@ -17,6 +17,7 @@ type BusinessAccountQuery struct {
|
||||
}
|
||||
|
||||
type BusinessAccountRequest struct {
|
||||
HireDate *string `json:"hireDate"`
|
||||
UserID uint `json:"userId"`
|
||||
Username *string `json:"userName"`
|
||||
NickName *string `json:"nickName"`
|
||||
|
||||
@@ -3,6 +3,7 @@ package dto
|
||||
import "encoding/json"
|
||||
|
||||
type ChannelQuery struct {
|
||||
Effective *bool `form:"effective"`
|
||||
Page int `form:"pageNum"`
|
||||
PageSize int `form:"pageSize"`
|
||||
Sources []string `form:"sourceDb"`
|
||||
@@ -15,6 +16,9 @@ type ChannelQuery struct {
|
||||
LockStatus *int `form:"lockStatus"`
|
||||
}
|
||||
type ChannelResponse struct {
|
||||
Effective bool `json:"effective"`
|
||||
QualifiedAt *int64 `json:"qualifiedAt"`
|
||||
CreatedAt *int64 `json:"createdAt"`
|
||||
ID uint64 `json:"id"`
|
||||
SourceDB string `json:"sourceDb"`
|
||||
PlatformName string `json:"platformName"`
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
package dto
|
||||
|
||||
type DashboardQuery struct {
|
||||
Sources []string `form:"sourceDbList"`
|
||||
Period string `form:"period"`
|
||||
Begin string `form:"begin"`
|
||||
End string `form:"end"`
|
||||
}
|
||||
type DashboardOverview struct {
|
||||
Cards map[string]*WorkbenchCard `json:"cards"`
|
||||
Begin string `json:"begin"`
|
||||
End string `json:"end"`
|
||||
Settings OtherSettingsValues `json:"settings"`
|
||||
Available bool `json:"available"`
|
||||
}
|
||||
|
||||
type BusinessMemberResponse struct {
|
||||
*BusinessAccountResponse
|
||||
HireDate string `json:"hireDate"`
|
||||
Employment string `json:"employment"`
|
||||
GroupID uint64 `json:"groupId"`
|
||||
GroupName string `json:"groupName"`
|
||||
CanEdit bool `json:"canEdit"`
|
||||
CanSetGoal bool `json:"canSetGoal"`
|
||||
}
|
||||
type BusinessGroupResponse struct {
|
||||
ID uint64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
MemberIDs []uint `json:"memberIds"`
|
||||
CanEdit bool `json:"canEdit"`
|
||||
CanSetGoal bool `json:"canSetGoal"`
|
||||
Mine bool `json:"mine"`
|
||||
Partial bool `json:"partial"`
|
||||
Overlapping bool `json:"overlapping"`
|
||||
Relation string `json:"relation"`
|
||||
ChannelCount int64 `json:"channelCount"`
|
||||
}
|
||||
type TeamResponse struct {
|
||||
Revision uint64 `json:"revision"`
|
||||
Groups []*BusinessGroupResponse `json:"groups"`
|
||||
Members []*BusinessMemberResponse `json:"members"`
|
||||
CanManage bool `json:"canManage"`
|
||||
ViewerID uint `json:"viewerId"`
|
||||
}
|
||||
type GroupInput struct {
|
||||
ID uint64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
MemberIDs []uint `json:"memberIds"`
|
||||
}
|
||||
type GroupSaveRequest struct {
|
||||
Revision *uint64 `json:"revision"`
|
||||
Groups []*GroupInput `json:"groups"`
|
||||
}
|
||||
type MemberProfileRequest struct {
|
||||
UserID uint `json:"userId"`
|
||||
RealName string `json:"realName"`
|
||||
Phone string `json:"phoneNumber"`
|
||||
HireDate string `json:"hireDate"`
|
||||
Employment string `json:"employment"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
type PerformanceQuery struct {
|
||||
Year *int `form:"year"`
|
||||
BeginMonth *int `form:"beginMonth"`
|
||||
EndMonth *int `form:"endMonth"`
|
||||
Name string `form:"name"`
|
||||
Level *int `form:"level"`
|
||||
GroupID *uint64 `form:"groupId"`
|
||||
Sort string `form:"sort"`
|
||||
Order string `form:"order"`
|
||||
View string `form:"view"`
|
||||
}
|
||||
type PerformanceMetrics struct {
|
||||
Open int64 `json:"open"`
|
||||
Effective int64 `json:"effective"`
|
||||
Pay string `json:"pay"`
|
||||
}
|
||||
type PerformancePeriod struct {
|
||||
Key string `json:"key"`
|
||||
Label string `json:"label"`
|
||||
Begin string `json:"begin"`
|
||||
End string `json:"end"`
|
||||
}
|
||||
type GoalResponse struct {
|
||||
Open *int64 `json:"open"`
|
||||
Effective *int64 `json:"effective"`
|
||||
Amount *int64 `json:"amount"`
|
||||
UpdatedBy uint `json:"updatedBy"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
}
|
||||
type PerformanceRow struct {
|
||||
Member *BusinessMemberResponse `json:"member"`
|
||||
Total PerformanceMetrics `json:"total"`
|
||||
Rate *string `json:"rate"`
|
||||
Periods map[string]PerformanceMetrics `json:"periods"`
|
||||
Goals map[string]*GoalResponse `json:"goals"`
|
||||
DescendantCount int `json:"descendantCount"`
|
||||
}
|
||||
type PerformanceGroupRow struct {
|
||||
Group *BusinessGroupResponse `json:"group"`
|
||||
Total PerformanceMetrics `json:"total"`
|
||||
UniqueTotal PerformanceMetrics `json:"uniqueTotal"`
|
||||
Periods map[string]PerformanceMetrics `json:"periods"`
|
||||
UniquePeriods map[string]PerformanceMetrics `json:"uniquePeriods"`
|
||||
Members []*PerformanceRow `json:"members"`
|
||||
Goals map[string]*GoalResponse `json:"goals"`
|
||||
}
|
||||
type PerformancePage struct {
|
||||
MatrixTotal PerformanceMetrics `json:"matrixTotal"`
|
||||
MatrixRate *string `json:"matrixRate"`
|
||||
MatrixTrend map[string]PerformanceMetrics `json:"matrixTrend"`
|
||||
Prior PerformanceMetrics `json:"prior"`
|
||||
Rows []*PerformanceRow `json:"rows"`
|
||||
Groups []*PerformanceGroupRow `json:"groups"`
|
||||
Total PerformanceMetrics `json:"total"`
|
||||
Rate *string `json:"rate"`
|
||||
ProductiveMembers int64 `json:"productiveMembers"`
|
||||
Periods []PerformancePeriod `json:"periods"`
|
||||
Trend map[string]PerformanceMetrics `json:"trend"`
|
||||
Begin string `json:"begin"`
|
||||
End string `json:"end"`
|
||||
Granularity string `json:"granularity"`
|
||||
Settings OtherSettingsValues `json:"settings"`
|
||||
Available bool `json:"available"`
|
||||
}
|
||||
type PerformanceDetailQuery struct {
|
||||
MemberIDs string `form:"memberIds"`
|
||||
Kind string `form:"kind"`
|
||||
TargetID uint64 `form:"targetId"`
|
||||
Begin string `form:"begin"`
|
||||
End string `form:"end"`
|
||||
Metric string `form:"metric"`
|
||||
Page int `form:"page"`
|
||||
Size int `form:"size"`
|
||||
}
|
||||
type PerformanceChannel struct {
|
||||
ContributorID uint `json:"contributorId"`
|
||||
ContributorName string `json:"contributorName"`
|
||||
SourceDB string `json:"sourceDb"`
|
||||
SourceDBLabel string `json:"sourceDbLabel"`
|
||||
ID uint64 `json:"id"`
|
||||
Account string `json:"account"`
|
||||
Name string `json:"name"`
|
||||
OwnerName string `json:"ownerName"`
|
||||
CreatedAt *int64 `json:"createdAt"`
|
||||
QualifiedAt *int64 `json:"qualifiedAt"`
|
||||
WindowPay string `json:"windowPay"`
|
||||
PeriodPay string `json:"periodPay"`
|
||||
}
|
||||
type PerformanceDetailPage struct {
|
||||
Rows []*PerformanceChannel `json:"rows"`
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
type GoalQuery struct {
|
||||
Kind string `form:"kind"`
|
||||
TargetID uint64 `form:"targetId"`
|
||||
}
|
||||
type GoalRequest struct {
|
||||
Kind string `json:"kind"`
|
||||
TargetID uint64 `json:"targetId"`
|
||||
Month string `json:"month"`
|
||||
Open *int64 `json:"open"`
|
||||
Effective *int64 `json:"effective"`
|
||||
Amount *int64 `json:"amount"`
|
||||
}
|
||||
type GoalMonthResponse struct {
|
||||
Available bool `json:"available"`
|
||||
Month string `json:"month"`
|
||||
Goal *GoalResponse `json:"goal"`
|
||||
Actual PerformanceMetrics `json:"actual"`
|
||||
Status string `json:"status"`
|
||||
Missing int `json:"missing"`
|
||||
CanEdit bool `json:"canEdit"`
|
||||
}
|
||||
type GoalPage struct {
|
||||
Kind string `json:"kind"`
|
||||
TargetID uint64 `json:"targetId"`
|
||||
Name string `json:"name"`
|
||||
CanEdit bool `json:"canEdit"`
|
||||
Months []GoalMonthResponse `json:"months"`
|
||||
MemberGoals map[string]PerformanceMetrics `json:"memberGoals"`
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
var ProviderSet = wire.NewSet(
|
||||
businessservice.NewUserPreferenceService,
|
||||
businessservice.NewOtherSettingsService,
|
||||
businessservice.NewBusinessPerformanceService,
|
||||
businessservice.NewAccountService,
|
||||
businessservice.NewBindingService,
|
||||
businessservice.NewPasswordResetService,
|
||||
|
||||
Reference in New Issue
Block a user