Files
kra-new/internal/biz/business/team.go
T

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, &copy)
}
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
}