feat: add tenant membership flow

This commit is contained in:
2026-01-13 10:26:11 +08:00
parent fa43c0355f
commit 2a670b3a78
11 changed files with 1719 additions and 0 deletions

View File

@@ -1,8 +1,12 @@
package services
import (
"bytes"
"database/sql"
"errors"
"mime/multipart"
"net/http"
"net/http/httptest"
"testing"
"quyun/v2/app/commands/testx"
@@ -40,6 +44,9 @@ func (s *CommonTestSuite) Test_AbortUpload() {
ctx := s.T().Context()
tenantID := int64(1)
ownerID := int64(1001)
tempDir := s.T().TempDir()
So(Common.storage, ShouldNotBeNil)
Common.storage.Config.LocalPath = tempDir
newUpload := func() string {
form := &common_dto.UploadInitForm{
@@ -84,3 +91,64 @@ func (s *CommonTestSuite) Test_AbortUpload() {
})
})
}
func (s *CommonTestSuite) Test_UploadPart() {
Convey("UploadPart", s.T(), func() {
ctx := s.T().Context()
tenantID := int64(1)
ownerID := int64(1001)
otherID := int64(2002)
tempDir := s.T().TempDir()
So(Common.storage, ShouldNotBeNil)
Common.storage.Config.LocalPath = tempDir
newUpload := func() string {
form := &common_dto.UploadInitForm{
Filename: "sample.mp4",
Type: "video",
MimeType: "video/mp4",
}
resp, err := Common.InitUpload(ctx, tenantID, ownerID, form)
So(err, ShouldBeNil)
So(resp.UploadID, ShouldNotBeBlank)
return resp.UploadID
}
newFileHeader := func() *multipart.FileHeader {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("file", "sample.txt")
So(err, ShouldBeNil)
_, err = part.Write([]byte("hello world"))
So(err, ShouldBeNil)
So(writer.Close(), ShouldBeNil)
req := httptest.NewRequest(http.MethodPost, "/upload", body)
req.Header.Set("Content-Type", writer.FormDataContentType())
So(req.ParseMultipartForm(32<<20), ShouldBeNil)
So(req.MultipartForm, ShouldNotBeNil)
So(req.MultipartForm.File["file"], ShouldNotBeEmpty)
return req.MultipartForm.File["file"][0]
}
Convey("should allow owner to upload part", func() {
uploadID := newUpload()
form := &common_dto.UploadPartForm{UploadID: uploadID, PartNumber: 1}
err := Common.UploadPart(ctx, tenantID, ownerID, newFileHeader(), form)
So(err, ShouldBeNil)
})
Convey("should reject other user", func() {
uploadID := newUpload()
form := &common_dto.UploadPartForm{UploadID: uploadID, PartNumber: 1}
err := Common.UploadPart(ctx, tenantID, otherID, newFileHeader(), form)
So(err, ShouldNotBeNil)
var appErr *errorx.AppError
So(errors.As(err, &appErr), ShouldBeTrue)
So(appErr.Code, ShouldEqual, errorx.ErrForbidden.Code)
})
})
}

View File

@@ -0,0 +1,427 @@
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,
}
}

View File

@@ -0,0 +1,304 @@
package services
import (
"errors"
"time"
"quyun/v2/app/errorx"
tenant_dto "quyun/v2/app/http/v1/dto"
"quyun/v2/database"
"quyun/v2/database/models"
"quyun/v2/pkg/consts"
. "github.com/smartystreets/goconvey/convey"
"go.ipao.vip/gen/types"
)
func (s *TenantTestSuite) Test_ApplyJoin() {
Convey("ApplyJoin", s.T(), func() {
ctx := s.T().Context()
setup := func() (*models.Tenant, *models.User, *models.User) {
database.Truncate(ctx, s.DB,
models.TableNameTenantJoinRequest,
models.TableNameTenantUser,
models.TableNameTenant,
models.TableNameUser,
)
owner := &models.User{Username: "owner_apply", Phone: "13900001001"}
user := &models.User{Username: "user_apply", Phone: "13900001002"}
_ = models.UserQuery.WithContext(ctx).Create(owner)
_ = models.UserQuery.WithContext(ctx).Create(user)
tenant := &models.Tenant{
Name: "Tenant Apply",
UserID: owner.ID,
Status: consts.TenantStatusVerified,
}
_ = models.TenantQuery.WithContext(ctx).Create(tenant)
return tenant, owner, user
}
Convey("should create pending join request", func() {
tenant, _, user := setup()
err := Tenant.ApplyJoin(ctx, tenant.ID, user.ID, &tenant_dto.TenantJoinApplyForm{Reason: "想加入"})
So(err, ShouldBeNil)
req, err := models.TenantJoinRequestQuery.WithContext(ctx).
Where(models.TenantJoinRequestQuery.TenantID.Eq(tenant.ID),
models.TenantJoinRequestQuery.UserID.Eq(user.ID)).
First()
So(err, ShouldBeNil)
So(req.Status, ShouldEqual, string(consts.TenantJoinRequestStatusPending))
})
Convey("should reject duplicate apply", func() {
tenant, _, user := setup()
_ = Tenant.ApplyJoin(ctx, tenant.ID, user.ID, &tenant_dto.TenantJoinApplyForm{})
err := Tenant.ApplyJoin(ctx, tenant.ID, user.ID, &tenant_dto.TenantJoinApplyForm{})
So(err, ShouldNotBeNil)
var appErr *errorx.AppError
So(errors.As(err, &appErr), ShouldBeTrue)
So(appErr.Code, ShouldEqual, errorx.ErrBadRequest.Code)
})
Convey("should reject when already member", func() {
tenant, _, user := setup()
_ = models.TenantUserQuery.WithContext(ctx).Create(&models.TenantUser{
TenantID: tenant.ID,
UserID: user.ID,
Role: types.Array[consts.TenantUserRole]{consts.TenantUserRoleMember},
Status: consts.UserStatusVerified,
})
err := Tenant.ApplyJoin(ctx, tenant.ID, user.ID, &tenant_dto.TenantJoinApplyForm{})
So(err, ShouldNotBeNil)
var appErr *errorx.AppError
So(errors.As(err, &appErr), ShouldBeTrue)
So(appErr.Code, ShouldEqual, errorx.ErrBadRequest.Code)
})
})
}
func (s *TenantTestSuite) Test_CancelJoin() {
Convey("CancelJoin", s.T(), func() {
ctx := s.T().Context()
setup := func() (*models.Tenant, *models.User, *models.User, *models.TenantJoinRequest) {
database.Truncate(ctx, s.DB,
models.TableNameTenantJoinRequest,
models.TableNameTenantUser,
models.TableNameTenant,
models.TableNameUser,
)
owner := &models.User{Username: "owner_cancel", Phone: "13900001003"}
user := &models.User{Username: "user_cancel", Phone: "13900001004"}
_ = models.UserQuery.WithContext(ctx).Create(owner)
_ = models.UserQuery.WithContext(ctx).Create(user)
tenant := &models.Tenant{
Name: "Tenant Cancel",
UserID: owner.ID,
Status: consts.TenantStatusVerified,
}
_ = models.TenantQuery.WithContext(ctx).Create(tenant)
req := &models.TenantJoinRequest{
TenantID: tenant.ID,
UserID: user.ID,
Status: string(consts.TenantJoinRequestStatusPending),
Reason: "测试取消",
}
_ = models.TenantJoinRequestQuery.WithContext(ctx).Create(req)
return tenant, owner, user, req
}
Convey("should cancel pending request", func() {
tenant, _, user, _ := setup()
err := Tenant.CancelJoin(ctx, tenant.ID, user.ID)
So(err, ShouldBeNil)
exists, err := models.TenantJoinRequestQuery.WithContext(ctx).
Where(models.TenantJoinRequestQuery.TenantID.Eq(tenant.ID),
models.TenantJoinRequestQuery.UserID.Eq(user.ID)).
Exists()
So(err, ShouldBeNil)
So(exists, ShouldBeFalse)
})
Convey("should return not found when request missing", func() {
tenant, _, user, _ := setup()
_ = Tenant.CancelJoin(ctx, tenant.ID, user.ID)
err := Tenant.CancelJoin(ctx, tenant.ID, user.ID)
So(err, ShouldNotBeNil)
var appErr *errorx.AppError
So(errors.As(err, &appErr), ShouldBeTrue)
So(appErr.Code, ShouldEqual, errorx.ErrRecordNotFound.Code)
})
})
}
func (s *TenantTestSuite) Test_ReviewJoin() {
Convey("ReviewJoin", s.T(), func() {
ctx := s.T().Context()
setup := func() (*models.Tenant, *models.User, *models.User, *models.TenantJoinRequest) {
database.Truncate(ctx, s.DB,
models.TableNameTenantJoinRequest,
models.TableNameTenantUser,
models.TableNameTenant,
models.TableNameUser,
)
owner := &models.User{Username: "owner_review", Phone: "13900001005"}
user := &models.User{Username: "user_review", Phone: "13900001006"}
_ = models.UserQuery.WithContext(ctx).Create(owner)
_ = models.UserQuery.WithContext(ctx).Create(user)
tenant := &models.Tenant{
Name: "Tenant Review",
UserID: owner.ID,
Status: consts.TenantStatusVerified,
}
_ = models.TenantQuery.WithContext(ctx).Create(tenant)
req := &models.TenantJoinRequest{
TenantID: tenant.ID,
UserID: user.ID,
Status: string(consts.TenantJoinRequestStatusPending),
Reason: "测试审核",
}
_ = models.TenantJoinRequestQuery.WithContext(ctx).Create(req)
return tenant, owner, user, req
}
Convey("should approve join request", func() {
tenant, owner, user, req := setup()
err := Tenant.ReviewJoin(ctx, tenant.ID, owner.ID, req.ID, &tenant_dto.TenantJoinReviewForm{
Action: "approve",
Reason: "通过",
})
So(err, ShouldBeNil)
req, err = models.TenantJoinRequestQuery.WithContext(ctx).Where(models.TenantJoinRequestQuery.ID.Eq(req.ID)).First()
So(err, ShouldBeNil)
So(req.Status, ShouldEqual, string(consts.TenantJoinRequestStatusApproved))
exists, err := models.TenantUserQuery.WithContext(ctx).
Where(models.TenantUserQuery.TenantID.Eq(tenant.ID),
models.TenantUserQuery.UserID.Eq(user.ID)).
Exists()
So(err, ShouldBeNil)
So(exists, ShouldBeTrue)
})
Convey("should reject join request", func() {
tenant, owner, _, req := setup()
err := Tenant.ReviewJoin(ctx, tenant.ID, owner.ID, req.ID, &tenant_dto.TenantJoinReviewForm{
Action: "reject",
Reason: "不符合条件",
})
So(err, ShouldBeNil)
req, err = models.TenantJoinRequestQuery.WithContext(ctx).Where(models.TenantJoinRequestQuery.ID.Eq(req.ID)).First()
So(err, ShouldBeNil)
So(req.Status, ShouldEqual, string(consts.TenantJoinRequestStatusRejected))
})
})
}
func (s *TenantTestSuite) Test_CreateInvite() {
Convey("CreateInvite", s.T(), func() {
ctx := s.T().Context()
database.Truncate(ctx, s.DB,
models.TableNameTenantInvite,
models.TableNameTenantUser,
models.TableNameTenant,
models.TableNameUser,
)
owner := &models.User{Username: "owner_invite", Phone: "13900001007"}
_ = models.UserQuery.WithContext(ctx).Create(owner)
tenant := &models.Tenant{
Name: "Tenant Invite",
UserID: owner.ID,
Status: consts.TenantStatusVerified,
}
_ = models.TenantQuery.WithContext(ctx).Create(tenant)
expiresAt := time.Now().Add(24 * time.Hour).Format(time.RFC3339)
item, err := Tenant.CreateInvite(ctx, tenant.ID, owner.ID, &tenant_dto.TenantInviteCreateForm{
MaxUses: 2,
ExpiresAt: &expiresAt,
Remark: "测试邀请",
})
So(err, ShouldBeNil)
So(item.Code, ShouldNotBeBlank)
invite, err := models.TenantInviteQuery.WithContext(ctx).Where(models.TenantInviteQuery.ID.Eq(item.ID)).First()
So(err, ShouldBeNil)
So(invite.MaxUses, ShouldEqual, 2)
So(invite.Status, ShouldEqual, string(consts.TenantInviteStatusActive))
})
}
func (s *TenantTestSuite) Test_AcceptInvite() {
Convey("AcceptInvite", s.T(), func() {
ctx := s.T().Context()
database.Truncate(ctx, s.DB,
models.TableNameTenantInvite,
models.TableNameTenantUser,
models.TableNameTenant,
models.TableNameUser,
)
owner := &models.User{Username: "owner_accept", Phone: "13900001008"}
user := &models.User{Username: "user_accept", Phone: "13900001009"}
_ = models.UserQuery.WithContext(ctx).Create(owner)
_ = models.UserQuery.WithContext(ctx).Create(user)
tenant := &models.Tenant{
Name: "Tenant Accept",
UserID: owner.ID,
Status: consts.TenantStatusVerified,
}
_ = models.TenantQuery.WithContext(ctx).Create(tenant)
invite := &models.TenantInvite{
TenantID: tenant.ID,
UserID: owner.ID,
Code: "invite_accept",
Status: string(consts.TenantInviteStatusActive),
MaxUses: 1,
UsedCount: 0,
ExpiresAt: time.Now().Add(24 * time.Hour),
Remark: "测试使用",
}
_ = models.TenantInviteQuery.WithContext(ctx).Create(invite)
err := Tenant.AcceptInvite(ctx, tenant.ID, user.ID, &tenant_dto.TenantInviteAcceptForm{Code: "invite_accept"})
So(err, ShouldBeNil)
exists, err := models.TenantUserQuery.WithContext(ctx).
Where(models.TenantUserQuery.TenantID.Eq(tenant.ID),
models.TenantUserQuery.UserID.Eq(user.ID)).
Exists()
So(err, ShouldBeNil)
So(exists, ShouldBeTrue)
invite, err = models.TenantInviteQuery.WithContext(ctx).Where(models.TenantInviteQuery.ID.Eq(invite.ID)).First()
So(err, ShouldBeNil)
So(invite.UsedCount, ShouldEqual, 1)
})
}