352 lines
9.3 KiB
Go
352 lines
9.3 KiB
Go
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
|
|
}
|