feat: add creator member management

This commit is contained in:
2026-01-17 20:42:43 +08:00
parent 984a404b5f
commit 7fca7a40e7
14 changed files with 2915 additions and 81 deletions

View File

@@ -8,10 +8,12 @@ import (
"quyun/v2/app/errorx"
tenant_dto "quyun/v2/app/http/v1/dto"
"quyun/v2/app/requests"
"quyun/v2/database/models"
"quyun/v2/pkg/consts"
"github.com/google/uuid"
"go.ipao.vip/gen/field"
"go.ipao.vip/gen/types"
"gorm.io/gorm"
)
@@ -337,6 +339,346 @@ func (s *tenant) AcceptInvite(ctx context.Context, tenantID, userID int64, form
})
}
func (s *tenant) ListMembers(ctx context.Context, tenantID, operatorID int64, filter *tenant_dto.TenantMemberListFilter) (*requests.Pager, error) {
if tenantID == 0 {
return nil, errorx.ErrRecordNotFound.WithMsg("租户不存在")
}
if filter == nil {
filter = &tenant_dto.TenantMemberListFilter{}
}
// 校验操作者权限,避免非管理员查看成员详情。
if _, err := s.ensureTenantAdmin(ctx, tenantID, operatorID); err != nil {
return nil, err
}
tbl, q := models.TenantUserQuery.QueryContext(ctx)
q = q.Where(tbl.TenantID.Eq(tenantID))
if filter.Role != nil && *filter.Role != "" {
q = q.Where(tbl.Role.Contains(types.Array[consts.TenantUserRole]{*filter.Role}))
}
if filter.Status != nil && *filter.Status != "" {
q = q.Where(tbl.Status.Eq(*filter.Status))
}
userIDs, userFilter, err := s.lookupUserIDs(ctx, filter.Keyword)
if err != nil {
return nil, err
}
if userFilter {
if len(userIDs) == 0 {
q = q.Where(tbl.ID.Eq(-1))
} else {
q = q.Where(tbl.UserID.In(userIDs...))
}
}
filter.Pagination.Format()
total, err := q.Count()
if err != nil {
return nil, errorx.ErrDatabaseError.WithCause(err)
}
list, err := q.Order(tbl.CreatedAt.Desc()).
Offset(int(filter.Pagination.Offset())).
Limit(int(filter.Pagination.Limit)).
Find()
if err != nil {
return nil, errorx.ErrDatabaseError.WithCause(err)
}
// 批量补齐成员用户信息,避免逐条查询。
userIDSet := make(map[int64]struct{})
for _, item := range list {
userIDSet[item.UserID] = struct{}{}
}
userIDs = make([]int64, 0, len(userIDSet))
for id := range userIDSet {
userIDs = append(userIDs, id)
}
userMap, err := s.loadUserMap(ctx, userIDs)
if err != nil {
return nil, err
}
items := make([]tenant_dto.TenantMemberItem, 0, len(list))
for _, member := range list {
roles := make([]consts.TenantUserRole, 0, len(member.Role))
roleDescriptions := make([]string, 0, len(member.Role))
for _, role := range member.Role {
roles = append(roles, role)
roleDescriptions = append(roleDescriptions, role.Description())
}
statusDescription := member.Status.Description()
if statusDescription == "" {
statusDescription = string(member.Status)
}
items = append(items, tenant_dto.TenantMemberItem{
ID: member.ID,
TenantID: member.TenantID,
User: s.toTenantMemberUserLite(userMap[member.UserID]),
Role: roles,
RoleDescription: roleDescriptions,
Status: member.Status,
StatusDescription: statusDescription,
CreatedAt: s.formatTime(member.CreatedAt),
UpdatedAt: s.formatTime(member.UpdatedAt),
})
}
return &requests.Pager{
Pagination: filter.Pagination,
Total: total,
Items: items,
}, nil
}
func (s *tenant) ListInvites(ctx context.Context, tenantID, operatorID int64, filter *tenant_dto.TenantInviteListFilter) (*requests.Pager, error) {
if tenantID == 0 {
return nil, errorx.ErrRecordNotFound.WithMsg("租户不存在")
}
if filter == nil {
filter = &tenant_dto.TenantInviteListFilter{}
}
// 校验操作者权限,避免越权读取邀请信息。
if _, err := s.ensureTenantAdmin(ctx, tenantID, operatorID); err != nil {
return nil, err
}
tbl, q := models.TenantInviteQuery.QueryContext(ctx)
q = q.Where(tbl.TenantID.Eq(tenantID))
if filter.Status != nil && *filter.Status != "" {
q = q.Where(tbl.Status.Eq(string(*filter.Status)))
}
filter.Pagination.Format()
total, err := q.Count()
if err != nil {
return nil, errorx.ErrDatabaseError.WithCause(err)
}
list, err := q.Order(tbl.CreatedAt.Desc()).
Offset(int(filter.Pagination.Offset())).
Limit(int(filter.Pagination.Limit)).
Find()
if err != nil {
return nil, errorx.ErrDatabaseError.WithCause(err)
}
// 批量补齐邀请创建者信息,便于前端展示。
userIDSet := make(map[int64]struct{})
for _, invite := range list {
userIDSet[invite.UserID] = struct{}{}
}
userIDs := make([]int64, 0, len(userIDSet))
for id := range userIDSet {
userIDs = append(userIDs, id)
}
userMap, err := s.loadUserMap(ctx, userIDs)
if err != nil {
return nil, err
}
items := make([]tenant_dto.TenantInviteListItem, 0, len(list))
for _, invite := range list {
status := consts.TenantInviteStatus(invite.Status)
statusDescription := status.Description()
if statusDescription == "" {
statusDescription = invite.Status
}
expiresAt := ""
if !invite.ExpiresAt.IsZero() {
expiresAt = invite.ExpiresAt.Format(time.RFC3339)
}
items = append(items, tenant_dto.TenantInviteListItem{
ID: invite.ID,
Code: invite.Code,
Status: invite.Status,
StatusDescription: statusDescription,
MaxUses: invite.MaxUses,
UsedCount: invite.UsedCount,
ExpiresAt: expiresAt,
CreatedAt: s.formatTime(invite.CreatedAt),
Remark: invite.Remark,
Creator: s.toTenantMemberUserLite(userMap[invite.UserID]),
})
}
return &requests.Pager{
Pagination: filter.Pagination,
Total: total,
Items: items,
}, nil
}
func (s *tenant) ListJoinRequests(ctx context.Context, tenantID, operatorID int64, filter *tenant_dto.TenantJoinRequestListFilter) (*requests.Pager, error) {
if tenantID == 0 {
return nil, errorx.ErrRecordNotFound.WithMsg("租户不存在")
}
if filter == nil {
filter = &tenant_dto.TenantJoinRequestListFilter{}
}
// 校验操作者权限,避免越权读取审核信息。
if _, err := s.ensureTenantAdmin(ctx, tenantID, operatorID); err != nil {
return nil, err
}
tbl, q := models.TenantJoinRequestQuery.QueryContext(ctx)
q = q.Where(tbl.TenantID.Eq(tenantID))
if filter.Status != nil && *filter.Status != "" {
q = q.Where(tbl.Status.Eq(string(*filter.Status)))
}
userIDs, userFilter, err := s.lookupUserIDs(ctx, filter.Keyword)
if err != nil {
return nil, err
}
if userFilter {
if len(userIDs) == 0 {
q = q.Where(tbl.ID.Eq(-1))
} else {
q = q.Where(tbl.UserID.In(userIDs...))
}
}
filter.Pagination.Format()
total, err := q.Count()
if err != nil {
return nil, errorx.ErrDatabaseError.WithCause(err)
}
list, err := q.Order(tbl.CreatedAt.Desc()).
Offset(int(filter.Pagination.Offset())).
Limit(int(filter.Pagination.Limit)).
Find()
if err != nil {
return nil, errorx.ErrDatabaseError.WithCause(err)
}
// 批量补齐申请用户信息。
userIDSet := make(map[int64]struct{})
for _, req := range list {
userIDSet[req.UserID] = struct{}{}
}
userIDs = make([]int64, 0, len(userIDSet))
for id := range userIDSet {
userIDs = append(userIDs, id)
}
userMap, err := s.loadUserMap(ctx, userIDs)
if err != nil {
return nil, err
}
items := make([]tenant_dto.TenantJoinRequestItem, 0, len(list))
for _, req := range list {
status := consts.TenantJoinRequestStatus(req.Status)
statusDescription := status.Description()
if statusDescription == "" {
statusDescription = req.Status
}
items = append(items, tenant_dto.TenantJoinRequestItem{
ID: req.ID,
User: s.toTenantMemberUserLite(userMap[req.UserID]),
Status: req.Status,
StatusDescription: statusDescription,
Reason: req.Reason,
DecidedAt: s.formatTime(req.DecidedAt),
DecidedOperatorUserID: req.DecidedOperatorUserID,
DecidedReason: req.DecidedReason,
CreatedAt: s.formatTime(req.CreatedAt),
UpdatedAt: s.formatTime(req.UpdatedAt),
})
}
return &requests.Pager{
Pagination: filter.Pagination,
Total: total,
Items: items,
}, nil
}
func (s *tenant) DisableInvite(ctx context.Context, tenantID, operatorID, inviteID int64) error {
if tenantID == 0 {
return errorx.ErrRecordNotFound.WithMsg("租户不存在")
}
if inviteID == 0 {
return errorx.ErrInvalidParameter.WithMsg("邀请记录不存在")
}
// 校验操作者权限,避免越权撤销邀请。
if _, err := s.ensureTenantAdmin(ctx, tenantID, operatorID); err != nil {
return err
}
tbl, q := models.TenantInviteQuery.QueryContext(ctx)
invite, err := q.Where(tbl.ID.Eq(inviteID)).First()
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errorx.ErrRecordNotFound.WithMsg("邀请记录不存在")
}
return errorx.ErrDatabaseError.WithCause(err)
}
if invite.TenantID != tenantID {
return errorx.ErrForbidden.WithMsg("租户不匹配")
}
if invite.Status != string(consts.TenantInviteStatusActive) {
return errorx.ErrBadRequest.WithMsg("邀请码不可撤销")
}
now := time.Now()
_, err = q.Where(
tbl.ID.Eq(invite.ID),
tbl.Status.Eq(string(consts.TenantInviteStatusActive)),
).UpdateSimple(
tbl.Status.Value(string(consts.TenantInviteStatusDisabled)),
tbl.DisabledAt.Value(now),
tbl.DisabledOperatorUserID.Value(operatorID),
)
if err != nil {
return errorx.ErrDatabaseError.WithCause(err)
}
return nil
}
func (s *tenant) RemoveMember(ctx context.Context, tenantID, operatorID, memberID int64) error {
if tenantID == 0 {
return errorx.ErrRecordNotFound.WithMsg("租户不存在")
}
if memberID == 0 {
return errorx.ErrInvalidParameter.WithMsg("成员不存在")
}
// 校验操作者权限,并拿到租户信息用于保护主账号。
tenant, err := s.ensureTenantAdmin(ctx, tenantID, operatorID)
if err != nil {
return err
}
tbl, q := models.TenantUserQuery.QueryContext(ctx)
member, err := q.Where(tbl.ID.Eq(memberID)).First()
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errorx.ErrRecordNotFound.WithMsg("成员不存在")
}
return errorx.ErrDatabaseError.WithCause(err)
}
if member.TenantID != tenantID {
return errorx.ErrForbidden.WithMsg("租户不匹配")
}
if tenant != nil && member.UserID == tenant.UserID {
return errorx.ErrBadRequest.WithMsg("无法移除租户主账号")
}
if _, err := q.Where(tbl.ID.Eq(member.ID)).Delete(); err != nil {
return errorx.ErrDatabaseError.WithCause(err)
}
return nil
}
func (s *tenant) ensureTenantAdmin(ctx context.Context, tenantID, userID int64) (*models.Tenant, error) {
if userID == 0 {
return nil, errorx.ErrUnauthorized
@@ -425,3 +767,64 @@ func (s *tenant) toTenantInviteItem(invite *models.TenantInvite) *tenant_dto.Ten
Remark: invite.Remark,
}
}
func (s *tenant) toTenantMemberUserLite(user *models.User) *tenant_dto.TenantMemberUserLite {
if user == nil {
return nil
}
return &tenant_dto.TenantMemberUserLite{
ID: user.ID,
Username: user.Username,
Phone: user.Phone,
Nickname: user.Nickname,
Avatar: user.Avatar,
}
}
func (s *tenant) loadUserMap(ctx context.Context, userIDs []int64) (map[int64]*models.User, error) {
userMap := make(map[int64]*models.User)
if len(userIDs) == 0 {
return userMap, nil
}
tbl, q := models.UserQuery.QueryContext(ctx)
users, err := q.Where(tbl.ID.In(userIDs...)).Find()
if err != nil {
return nil, errorx.ErrDatabaseError.WithCause(err)
}
for _, user := range users {
userMap[user.ID] = user
}
return userMap, nil
}
func (s *tenant) lookupUserIDs(ctx context.Context, keyword *string) ([]int64, bool, error) {
text := ""
if keyword != nil {
text = strings.TrimSpace(*keyword)
}
if text == "" {
return nil, false, nil
}
tbl, q := models.UserQuery.QueryContext(ctx)
like := "%" + text + "%"
q = q.Where(field.Or(tbl.Username.Like(like), tbl.Nickname.Like(like), tbl.Phone.Like(like)))
users, err := q.Select(tbl.ID).Find()
if err != nil {
return nil, true, errorx.ErrDatabaseError.WithCause(err)
}
ids := make([]int64, 0, len(users))
for _, user := range users {
ids = append(ids, user.ID)
}
return ids, true, nil
}
func (s *tenant) formatTime(t time.Time) string {
if t.IsZero() {
return ""
}
return t.Format(time.RFC3339)
}