package services import ( "context" "errors" "strings" "time" "quyun/v2/app/errorx" tenant_dto "quyun/v2/app/http/v1/dto" "quyun/v2/database/models" "quyun/v2/pkg/consts" "github.com/google/uuid" "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) 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, } }