package business import ( "context" "strconv" "strings" "time" "github.com/go-kratos/kratos/v3/errors" "golang.org/x/crypto/bcrypt" "kra/internal/biz/system" ) var ( ErrAccountNotFound = errors.NotFound("SW_ACCOUNT_NOT_FOUND", "商务账号不存在") ErrAccountScope = errors.Forbidden("SW_ACCOUNT_SCOPE", "没有权限访问用户数据") ErrChildLevel = errors.BadRequest("SW_CHILD_LEVEL", "三级商务不可再创建下级,请使用绑定游戏商务") ErrAccountInput = errors.BadRequest("SW_ACCOUNT_INPUT", "商务账号参数不正确") ErrAccountExists = errors.BadRequest("SW_ACCOUNT_EXISTS", "登录账号已存在") ErrPhoneExists = errors.BadRequest("SW_PHONE_EXISTS", "手机号码已存在") ErrEmailExists = errors.BadRequest("SW_EMAIL_EXISTS", "邮箱账号已存在") ErrBusinessRole = errors.BadRequest("SW_BUSINESS_ROLE", "未找到对应层级的商务角色,请核对角色迁移映射") ) type Account struct { HireDate, Employment string ID uint Username, NickName, Password, RealName, Phone, Email, QQNumber string Level int ParentID uint IDPath, ParentUserName string Status int RejectReason string AuthorityID uint CreatedAt, UpdatedAt time.Time PromoteCount *int64 BindCount int64 } type AccountCounts struct { // 无渠道表时为 nil,避免将未接入数据误报为真实零渠道。 PromoteCount *int64 BindCount int64 } // UserScope 表示商务树可见范围,与框架部门权限相互独立。 type UserScope struct { All, Empty, IncludeSelf, OnlySelf bool SelfID uint DescendantPath string } type AccountFilter struct { UserID uint Username, NickName, RealName, Phone string Level *int Status *int BeginTime, EndTime *time.Time } type AccountUpdate struct { ID uint Username, NickName, RealName, Phone, Email, QQNumber, RejectReason *string Status *int } type ListOptions struct { Filter AccountFilter Offset, Limit int OrderBy string Desc bool } type ListOption func(*ListOptions) func ListFilter(filter AccountFilter) ListOption { return func(o *ListOptions) { o.Filter = filter } } func ListOffset(n int) ListOption { return func(o *ListOptions) { o.Offset = n } } func ListLimit(n int) ListOption { return func(o *ListOptions) { o.Limit = n } } func ListOrderBy(key string, desc bool) ListOption { return func(o *ListOptions) { o.OrderBy, o.Desc = key, desc } } func NewListOptions(options ...ListOption) ListOptions { o := ListOptions{Limit: 10, OrderBy: "userId"} for _, option := range options { option(&o) } return o } type AccountRepo interface { Find(context.Context, uint) (*Account, error) List(context.Context, UserScope, ...ListOption) ([]*Account, int64, error) Create(context.Context, *Account) error Update(context.Context, *Account) error AuthorityForLevel(context.Context, int) (uint, error) DirectCounts(context.Context, []uint) (map[uint]AccountCounts, error) } type AccountUsecase struct{ repo AccountRepo } func NewAccountUsecase(repo AccountRepo) *AccountUsecase { return &AccountUsecase{repo: repo} } func (uc *AccountUsecase) actor(ctx context.Context) (system.Actor, *Account, error) { actor, ok := system.ActorFromContext(ctx) if !ok || actor.UserID == 0 { return actor, nil, ErrUnauthenticated } account, err := uc.repo.Find(ctx, actor.UserID) return actor, account, err } func descendantPath(account *Account) string { path := strings.TrimSpace(account.IDPath) if path == "" { path = "0" } return path + "," + strconv.FormatUint(uint64(account.ID), 10) } func descendantOf(owner, target *Account) bool { if owner == nil || target == nil || owner.ID == target.ID { return false } path := descendantPath(owner) return target.ParentID == owner.ID || target.IDPath == path || strings.HasPrefix(target.IDPath, path+",") } func (uc *AccountUsecase) List(ctx context.Context, filter AccountFilter, options ...ListOption) ([]*Account, int64, error) { actor, current, err := uc.actor(ctx) if err != nil { return nil, 0, err } scope := UserScope{All: actor.AuthorityID == 888} if !scope.All { if current.Level >= 3 { return []*Account{}, 0, nil } scope.SelfID, scope.DescendantPath = current.ID, descendantPath(current) } rows, total, err := uc.repo.List(ctx, scope, append(options, ListFilter(filter))...) if err != nil || len(rows) == 0 { return rows, total, err } ids := make([]uint, 0, len(rows)) for _, row := range rows { ids = append(ids, row.ID) } counts, err := uc.repo.DirectCounts(ctx, ids) if err != nil { return nil, 0, err } for _, row := range rows { row.PromoteCount, row.BindCount = counts[row.ID].PromoteCount, counts[row.ID].BindCount } return rows, total, nil } func (uc *AccountUsecase) Find(ctx context.Context, id uint) (*Account, error) { actor, current, err := uc.actor(ctx) if err != nil { return nil, err } account, err := uc.repo.Find(ctx, id) if err != nil { return nil, err } if actor.AuthorityID != 888 && id != current.ID && !descendantOf(current, account) { return nil, ErrAccountScope } return account, nil } func (uc *AccountUsecase) CreateChild(ctx context.Context, input *Account) (*Account, error) { if input == nil { return nil, ErrAccountInput } actor, parent, err := uc.actor(ctx) if err != nil { return nil, err } account := *input account.ID, account.ParentID, account.IDPath, account.Level = 0, 0, "0", 1 if actor.AuthorityID != 888 { if parent.Level >= 3 { return nil, ErrChildLevel } level := parent.Level if level < 1 { level = 1 } account.Level, account.ParentID, account.IDPath = level+1, parent.ID, descendantPath(parent) } account.AuthorityID, err = uc.repo.AuthorityForLevel(ctx, account.Level) if err != nil { return nil, err } if account.NickName == "" { account.NickName = account.RealName } hash, err := bcrypt.GenerateFromPassword([]byte(account.Password), bcrypt.DefaultCost) if err != nil { return nil, ErrAccountInput } account.Password = string(hash) if err = uc.repo.Create(ctx, &account); err != nil { return nil, err } account.Password = "" return &account, nil } func (uc *AccountUsecase) Update(ctx context.Context, input *AccountUpdate) error { if input == nil || input.ID == 0 { return ErrAccountInput } account, err := uc.Find(ctx, input.ID) if err != nil { return err } if account.AuthorityID == 888 { return ErrAccountScope } if input.Username != nil && *input.Username != account.Username || input.NickName != nil && *input.NickName != account.NickName { return ErrAccountInput } // 编辑资料和审核不会改变商务层级、归属、角色或密码。 for _, field := range []struct { dst *string src *string }{ {&account.Username, input.Username}, {&account.NickName, input.NickName}, {&account.RealName, input.RealName}, {&account.Phone, input.Phone}, {&account.Email, input.Email}, {&account.QQNumber, input.QQNumber}, {&account.RejectReason, input.RejectReason}, } { if field.src != nil { *field.dst = *field.src } } if input.Status != nil { account.Status = *input.Status } return uc.repo.Update(ctx, account) }