package services import ( "context" "errors" "strings" "time" "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" ) func (s *tenant) ApplyJoin(ctx context.Context, tenantID, userID int64, form *tenant_dto.TenantJoinApplyForm) error { if userID == 0 { return errorx.ErrUnauthorized } if tenantID == 0 { return errorx.ErrRecordNotFound.WithMsg("租户不存在") } // 校验租户可加入状态。 tblTenant, qTenant := models.TenantQuery.QueryContext(ctx) tenant, err := qTenant.Where(tblTenant.ID.Eq(tenantID)).First() if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return errorx.ErrRecordNotFound.WithMsg("租户不存在") } return errorx.ErrDatabaseError.WithCause(err) } if tenant.Status != consts.TenantStatusVerified { return errorx.ErrBadRequest.WithMsg("租户暂不可加入") } if tenant.UserID == userID { return errorx.ErrBadRequest.WithMsg("您已是租户管理员") } // 已是成员则不允许重复申请。 tblMember, qMember := models.TenantUserQuery.QueryContext(ctx) exists, err := qMember.Where(tblMember.TenantID.Eq(tenantID), tblMember.UserID.Eq(userID)).Exists() if err != nil { return errorx.ErrDatabaseError.WithCause(err) } if exists { return errorx.ErrBadRequest.WithMsg("您已是租户成员") } // 防止重复提交同一租户的待审核申请。 tblReq, qReq := models.TenantJoinRequestQuery.QueryContext(ctx) pendingExists, err := qReq.Where( tblReq.TenantID.Eq(tenantID), tblReq.UserID.Eq(userID), tblReq.Status.Eq(string(consts.TenantJoinRequestStatusPending)), ).Exists() if err != nil { return errorx.ErrDatabaseError.WithCause(err) } if pendingExists { return errorx.ErrBadRequest.WithMsg("已提交申请") } reason := "" if form != nil { reason = strings.TrimSpace(form.Reason) } if reason == "" { reason = "申请加入" } req := &models.TenantJoinRequest{ TenantID: tenantID, UserID: userID, Status: string(consts.TenantJoinRequestStatusPending), Reason: reason, } if err := qReq.Create(req); err != nil { return errorx.ErrDatabaseError.WithCause(err) } return nil } func (s *tenant) CancelJoin(ctx context.Context, tenantID, userID int64) error { if userID == 0 { return errorx.ErrUnauthorized } if tenantID == 0 { return errorx.ErrRecordNotFound.WithMsg("租户不存在") } tblReq, qReq := models.TenantJoinRequestQuery.QueryContext(ctx) req, err := qReq.Where( tblReq.TenantID.Eq(tenantID), tblReq.UserID.Eq(userID), tblReq.Status.Eq(string(consts.TenantJoinRequestStatusPending)), ).First() if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return errorx.ErrRecordNotFound.WithMsg("申请不存在") } return errorx.ErrDatabaseError.WithCause(err) } if _, err := qReq.Where(tblReq.ID.Eq(req.ID)).Delete(); err != nil { return errorx.ErrDatabaseError.WithCause(err) } return nil } func (s *tenant) ReviewJoin( ctx context.Context, tenantID, operatorID, requestID int64, form *tenant_dto.TenantJoinReviewForm, ) error { if tenantID == 0 { return errorx.ErrRecordNotFound.WithMsg("租户不存在") } if form == nil { return errorx.ErrBadRequest.WithMsg("审核参数不能为空") } action := strings.ToLower(strings.TrimSpace(form.Action)) if action != "approve" && action != "reject" { return errorx.ErrBadRequest.WithMsg("审核动作无效") } // 校验操作者为租户管理员或租户主账号。 if _, err := s.ensureTenantAdmin(ctx, tenantID, operatorID); err != nil { return err } tblReq, qReq := models.TenantJoinRequestQuery.QueryContext(ctx) req, err := qReq.Where(tblReq.ID.Eq(requestID)).First() if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return errorx.ErrRecordNotFound.WithMsg("申请不存在") } return errorx.ErrDatabaseError.WithCause(err) } if req.TenantID != tenantID { return errorx.ErrForbidden.WithMsg("租户不匹配") } if req.Status != string(consts.TenantJoinRequestStatusPending) { return errorx.ErrBadRequest.WithMsg("申请已处理") } reason := strings.TrimSpace(form.Reason) now := time.Now() if action == "reject" { _, err = qReq.Where( tblReq.ID.Eq(req.ID), tblReq.Status.Eq(string(consts.TenantJoinRequestStatusPending)), ).UpdateSimple( tblReq.Status.Value(string(consts.TenantJoinRequestStatusRejected)), tblReq.DecidedAt.Value(now), tblReq.DecidedOperatorUserID.Value(operatorID), tblReq.DecidedReason.Value(reason), ) if err != nil { return errorx.ErrDatabaseError.WithCause(err) } return nil } // 审核通过需在事务内写入成员并更新申请状态。 return models.Q.Transaction(func(tx *models.Query) error { tblMember, qMember := tx.TenantUser.QueryContext(ctx) exists, err := qMember.Where( tblMember.TenantID.Eq(tenantID), tblMember.UserID.Eq(req.UserID), ).Exists() if err != nil { return errorx.ErrDatabaseError.WithCause(err) } if exists { return errorx.ErrBadRequest.WithMsg("用户已是成员") } member := &models.TenantUser{ TenantID: tenantID, UserID: req.UserID, Role: types.Array[consts.TenantUserRole]{consts.TenantUserRoleMember}, Status: consts.UserStatusVerified, } if err := qMember.Create(member); err != nil { return errorx.ErrDatabaseError.WithCause(err) } tblReqTx, qReqTx := tx.TenantJoinRequest.QueryContext(ctx) _, err = qReqTx.Where( tblReqTx.ID.Eq(req.ID), tblReqTx.Status.Eq(string(consts.TenantJoinRequestStatusPending)), ).UpdateSimple( tblReqTx.Status.Value(string(consts.TenantJoinRequestStatusApproved)), tblReqTx.DecidedAt.Value(now), tblReqTx.DecidedOperatorUserID.Value(operatorID), tblReqTx.DecidedReason.Value(reason), ) if err != nil { return errorx.ErrDatabaseError.WithCause(err) } return nil }) } func (s *tenant) CreateInvite( ctx context.Context, tenantID, operatorID int64, form *tenant_dto.TenantInviteCreateForm, ) (*tenant_dto.TenantInviteItem, error) { if tenantID == 0 { return nil, errorx.ErrRecordNotFound.WithMsg("租户不存在") } if form == nil { return nil, errorx.ErrBadRequest.WithMsg("邀请参数不能为空") } // 校验操作者权限。 if _, err := s.ensureTenantAdmin(ctx, tenantID, operatorID); err != nil { return nil, err } maxUses := form.MaxUses if maxUses <= 0 { maxUses = 1 } expiresAt, err := s.normalizeInviteExpiry(form) if err != nil { return nil, err } remark := strings.TrimSpace(form.Remark) if remark == "" { remark = "成员邀请" } code, err := s.newInviteCode(ctx) if err != nil { return nil, err } invite := &models.TenantInvite{ TenantID: tenantID, UserID: operatorID, Code: code, Status: string(consts.TenantInviteStatusActive), MaxUses: maxUses, UsedCount: 0, ExpiresAt: expiresAt, Remark: remark, } if err := models.TenantInviteQuery.WithContext(ctx).Create(invite); err != nil { return nil, errorx.ErrDatabaseError.WithCause(err) } return s.toTenantInviteItem(invite), nil } func (s *tenant) AcceptInvite(ctx context.Context, tenantID, userID int64, form *tenant_dto.TenantInviteAcceptForm) error { if userID == 0 { return errorx.ErrUnauthorized } if tenantID == 0 { return errorx.ErrRecordNotFound.WithMsg("租户不存在") } if form == nil || strings.TrimSpace(form.Code) == "" { return errorx.ErrBadRequest.WithMsg("邀请码不能为空") } code := strings.TrimSpace(form.Code) now := time.Now() // 邀请校验 + 成员入库需要事务保证一致性。 return models.Q.Transaction(func(tx *models.Query) error { tblInvite, qInvite := tx.TenantInvite.QueryContext(ctx) invite, err := qInvite.Where( tblInvite.TenantID.Eq(tenantID), tblInvite.Code.Eq(code), ).First() if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return errorx.ErrRecordNotFound.WithMsg("邀请码无效") } return errorx.ErrDatabaseError.WithCause(err) } if invite.Status != string(consts.TenantInviteStatusActive) { return errorx.ErrBadRequest.WithMsg("邀请码不可用") } if !invite.ExpiresAt.IsZero() && invite.ExpiresAt.Before(now) { _, err = qInvite.Where(tblInvite.ID.Eq(invite.ID)).UpdateSimple( tblInvite.Status.Value(string(consts.TenantInviteStatusExpired)), ) if err != nil { return errorx.ErrDatabaseError.WithCause(err) } return errorx.ErrBadRequest.WithMsg("邀请码已过期") } if invite.UsedCount >= invite.MaxUses { return errorx.ErrBadRequest.WithMsg("邀请码已用尽") } tblMember, qMember := tx.TenantUser.QueryContext(ctx) exists, err := qMember.Where( tblMember.TenantID.Eq(tenantID), tblMember.UserID.Eq(userID), ).Exists() if err != nil { return errorx.ErrDatabaseError.WithCause(err) } if exists { return errorx.ErrBadRequest.WithMsg("您已是租户成员") } member := &models.TenantUser{ TenantID: tenantID, UserID: userID, Role: types.Array[consts.TenantUserRole]{consts.TenantUserRoleMember}, Status: consts.UserStatusVerified, } if err := qMember.Create(member); err != nil { return errorx.ErrDatabaseError.WithCause(err) } _, err = qInvite.Where(tblInvite.ID.Eq(invite.ID)).UpdateSimple( tblInvite.UsedCount.Value(invite.UsedCount + 1), ) if err != nil { return errorx.ErrDatabaseError.WithCause(err) } return nil }) } 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 } tblTenant, qTenant := models.TenantQuery.QueryContext(ctx) tenant, err := qTenant.Where(tblTenant.ID.Eq(tenantID)).First() if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errorx.ErrRecordNotFound.WithMsg("租户不存在") } return nil, errorx.ErrDatabaseError.WithCause(err) } if tenant.UserID == userID { return tenant, nil } // 非主账号需校验租户管理员角色。 tblMember, qMember := models.TenantUserQuery.QueryContext(ctx) exists, err := qMember.Where( tblMember.TenantID.Eq(tenantID), tblMember.UserID.Eq(userID), tblMember.Status.Eq(consts.UserStatusVerified), tblMember.Role.Contains(types.Array[consts.TenantUserRole]{consts.TenantUserRoleTenantAdmin}), ).Exists() if err != nil { return nil, errorx.ErrDatabaseError.WithCause(err) } if !exists { return nil, errorx.ErrPermissionDenied.WithMsg("无权限操作该租户") } return tenant, nil } func (s *tenant) normalizeInviteExpiry(form *tenant_dto.TenantInviteCreateForm) (time.Time, error) { if form == nil || form.ExpiresAt == nil || strings.TrimSpace(*form.ExpiresAt) == "" { return time.Now().Add(7 * 24 * time.Hour), nil } raw := strings.TrimSpace(*form.ExpiresAt) expireAt, err := time.Parse(time.RFC3339, raw) if err != nil { return time.Time{}, errorx.ErrBadRequest.WithMsg("过期时间格式错误") } if expireAt.Before(time.Now()) { return time.Time{}, errorx.ErrBadRequest.WithMsg("过期时间不能早于当前时间") } return expireAt, nil } func (s *tenant) newInviteCode(ctx context.Context) (string, error) { for i := 0; i < 5; i++ { code := strings.ReplaceAll(uuid.NewString(), "-", "") exists, err := models.TenantInviteQuery.WithContext(ctx). Where(models.TenantInviteQuery.Code.Eq(code)). Exists() if err != nil { return "", errorx.ErrDatabaseError.WithCause(err) } if !exists { return code, nil } } return "", errorx.ErrInternalError.WithMsg("生成邀请码失败") } func (s *tenant) toTenantInviteItem(invite *models.TenantInvite) *tenant_dto.TenantInviteItem { if invite == nil { return nil } expiresAt := "" if !invite.ExpiresAt.IsZero() { expiresAt = invite.ExpiresAt.Format(time.RFC3339) } createdAt := "" if !invite.CreatedAt.IsZero() { createdAt = invite.CreatedAt.Format(time.RFC3339) } return &tenant_dto.TenantInviteItem{ ID: invite.ID, Code: invite.Code, Status: invite.Status, MaxUses: invite.MaxUses, UsedCount: invite.UsedCount, ExpiresAt: expiresAt, CreatedAt: createdAt, 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) }