831 lines
23 KiB
Go
831 lines
23 KiB
Go
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)
|
|
}
|