package business import ( "context" "sort" "strconv" "strings" "time" "unicode/utf8" "github.com/go-kratos/kratos/v3/errors" "golang.org/x/crypto/bcrypt" ) var ( ErrTeamInput = errors.BadRequest("SW_TEAM_INPUT", "小组名称须为1至10个字,每位商务只能加入一个小组") ErrTeamScope = errors.Forbidden("SW_TEAM_SCOPE", "无权管理该小组或成员") ErrTeamConflict = errors.Conflict("SW_TEAM_CONFLICT", "小组配置已被其他人修改,请刷新后重试") ErrMemberInput = errors.BadRequest("SW_MEMBER_INPUT", "请填写有效的姓名、联系电话、入职日期和在职状态") ) type BusinessMember struct { Account GroupID uint64 GroupName string CanEdit, CanSetGoal bool } type BusinessGroup struct { ID uint64 Name string OwnerID uint Sort int MemberIDs []uint CanEdit, CanSetGoal, Mine, Partial, Overlapping bool Relation string ChannelCount int64 } type TeamCatalog struct { Revision uint64 Groups []*BusinessGroup } type TeamView struct { Scope UserScope Revision uint64 Groups []*BusinessGroup Members []*BusinessMember CanManage bool ViewerID uint } type MemberProfile struct { ID uint RealName, Phone, HireDate, Employment, Password string } type TeamRepo interface { Members(context.Context, UserScope) ([]*BusinessMember, error) Catalog(context.Context) (*TeamCatalog, error) SaveCatalog(context.Context, uint64, []*BusinessGroup) (*TeamCatalog, error) UpdateProfile(context.Context, MemberProfile) error } type TeamUsecase struct { repo TeamRepo accounts *AccountUsecase } func NewTeamUsecase(repo TeamRepo, accounts *AccountUsecase) *TeamUsecase { return &TeamUsecase{repo: repo, accounts: accounts} } func memberName(member *BusinessMember) string { if strings.TrimSpace(member.NickName) != "" { return member.NickName } return member.Username } // Business depth is bounded; derive ancestors from IDs instead of comparing // every member with every other member on each statistics request. func memberAncestors(member *BusinessMember, byID map[uint]*BusinessMember) []uint { ids := []uint{member.ID} seen := map[uint]bool{member.ID: true} add := func(id uint) { if !seen[id] && byID[id] != nil && descendantOf(&byID[id].Account, &member.Account) { seen[id] = true ids = append(ids, id) } } add(member.ParentID) for _, part := range strings.Split(member.IDPath, ",") { id, err := strconv.ParseUint(part, 10, 64) if err == nil { add(uint(id)) } } return ids } func organizeMembers(members []*BusinessMember) { byID := map[uint]*BusinessMember{} children := map[uint][]*BusinessMember{} for _, m := range members { byID[m.ID] = m children[m.ParentID] = append(children[m.ParentID], m) } for _, list := range children { sort.SliceStable(list, func(i, j int) bool { return list[i].ID < list[j].ID }) } roots := []*BusinessMember{} for _, m := range members { if byID[m.ParentID] == nil { roots = append(roots, m) } } sort.SliceStable(roots, func(i, j int) bool { return roots[i].ID < roots[j].ID }) ordered := make([]*BusinessMember, 0, len(members)) seen := map[uint]bool{} var visit func(*BusinessMember) visit = func(m *BusinessMember) { if seen[m.ID] { return } seen[m.ID] = true ordered = append(ordered, m) for _, c := range children[m.ID] { visit(c) } } for _, m := range roots { visit(m) } for _, m := range members { visit(m) } copy(members, ordered) } func (uc *TeamUsecase) View(ctx context.Context) (*TeamView, error) { actor, account, err := uc.accounts.actor(ctx) if err != nil { return nil, err } scope := portraitUserScope(account) scope.All = actor.AuthorityID == 888 members, err := uc.repo.Members(ctx, scope) if err != nil { return nil, err } organizeMembers(members) catalog, err := uc.repo.Catalog(ctx) if err != nil { return nil, err } view := &TeamView{Revision: catalog.Revision, Groups: []*BusinessGroup{}, Members: members, CanManage: scope.All || account.Level == 1, ViewerID: account.ID, Scope: scope} visible := map[uint]*BusinessMember{} order := map[uint]int{} for i, m := range members { m.GroupID, m.GroupName = 0, "" visible[m.ID] = m order[m.ID] = i m.CanEdit = scope.All || m.ID == account.ID || descendantOf(account, &m.Account) m.CanSetGoal = scope.All || (account.Level == 1 && descendantOf(account, &m.Account)) } for _, g := range catalog.Groups { group := *g group.MemberIDs = []uint{} for _, id := range g.MemberIDs { if m := visible[id]; m != nil { group.MemberIDs = append(group.MemberIDs, id) m.GroupID = g.ID m.GroupName = g.Name if id == account.ID { group.Mine = true } if m.PromoteCount != nil { group.ChannelCount += *m.PromoteCount } } } if len(group.MemberIDs) == 0 && !scope.All && visible[g.OwnerID] == nil { continue } group.Partial = len(group.MemberIDs) != len(g.MemberIDs) group.CanEdit = scope.All || view.CanManage && !group.Partial group.CanSetGoal = view.CanManage && len(group.MemberIDs) > 0 if group.Mine { group.Relation = "本组" } else if len(group.MemberIDs) > 0 { nearest := visible[group.MemberIDs[0]] for _, id := range group.MemberIDs { m := visible[id] if m.Level < nearest.Level || (m.Level == nearest.Level && order[m.ID] < order[nearest.ID]) { nearest = m } } group.Relation = "下级 · " + memberName(nearest) } memberIDs := map[uint]bool{} for _, id := range group.MemberIDs { memberIDs[id] = true } for _, id := range group.MemberIDs { for _, ancestor := range memberAncestors(visible[id], visible) { if ancestor != id && memberIDs[ancestor] { group.Overlapping = true } } } view.Groups = append(view.Groups, &group) } return view, nil } func (uc *TeamUsecase) Save(ctx context.Context, revision uint64, input []*BusinessGroup) (*TeamView, error) { view, err := uc.View(ctx) if err != nil { return nil, err } if !view.CanManage { return nil, ErrTeamScope } if revision != view.Revision { return nil, ErrTeamConflict } all, err := uc.repo.Catalog(ctx) if err != nil { return nil, err } if all.Revision != revision { return nil, ErrTeamConflict } maxSort := 0 for _, g := range all.Groups { if g.Sort >= maxSort { maxSort = g.Sort + 1 } } visible := map[uint]bool{} editable := map[uint64]*BusinessGroup{} previous := map[uint64]*BusinessGroup{} for _, m := range view.Members { visible[m.ID] = true } for _, g := range view.Groups { editable[g.ID] = g } for _, g := range all.Groups { previous[g.ID] = g } if len(input) > 500 { return nil, ErrTeamInput } seenGroups := map[uint64]bool{} seenMembers := map[uint]bool{} next := []*BusinessGroup{} for index, g := range input { if g == nil { return nil, ErrTeamInput } name := strings.TrimSpace(g.Name) if utf8.RuneCountInString(name) < 1 || utf8.RuneCountInString(name) > 10 { return nil, ErrTeamInput } copy := &BusinessGroup{ID: g.ID, Name: name, OwnerID: view.ViewerID, Sort: index, MemberIDs: []uint{}} if !view.Scope.All { copy.Sort = maxSort maxSort++ } if g.ID != 0 { old := previous[g.ID] permission := editable[g.ID] if old == nil || permission == nil || seenGroups[g.ID] { return nil, ErrTeamScope } if !permission.CanEdit && name != old.Name { return nil, ErrTeamScope } seenGroups[g.ID] = true copy.OwnerID = old.OwnerID if !view.Scope.All { copy.Sort = old.Sort } for _, id := range old.MemberIDs { if !visible[id] { copy.MemberIDs = append(copy.MemberIDs, id) } } } for _, id := range g.MemberIDs { if !visible[id] { return nil, ErrTeamScope } if seenMembers[id] { return nil, ErrTeamInput } seenMembers[id] = true copy.MemberIDs = append(copy.MemberIDs, id) } next = append(next, copy) } for _, old := range all.Groups { if seenGroups[old.ID] { continue } if permission := editable[old.ID]; permission != nil { if !permission.CanEdit { return nil, ErrTeamScope } continue } copy := *old copy.MemberIDs = append([]uint{}, old.MemberIDs...) next = append(next, ©) } if _, err := uc.repo.SaveCatalog(ctx, revision, next); err != nil { return nil, err } return uc.View(ctx) } func (uc *TeamUsecase) UpdateProfile(ctx context.Context, profile MemberProfile) error { view, err := uc.View(ctx) if err != nil { return err } var member *BusinessMember for _, m := range view.Members { if m.ID == profile.ID { member = m break } } if member == nil || !member.CanEdit { return ErrTeamScope } if profile.Password != "" { hash, err := bcrypt.GenerateFromPassword([]byte(profile.Password), bcrypt.DefaultCost) if err != nil { return ErrMemberInput } profile.Password = string(hash) } return uc.repo.UpdateProfile(ctx, profile) } func DefaultPerformanceRange(now time.Time) (time.Time, time.Time) { now = now.In(portraitLocation) begin := time.Date(now.Year(), 1, 1, 0, 0, 0, 0, portraitLocation) end := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, portraitLocation).AddDate(0, 0, -1) return begin, end }