feat: add tenant membership flow
This commit is contained in:
@@ -29,6 +29,46 @@ func (c *Creator) Apply(ctx fiber.Ctx, user *models.User, form *dto.ApplyForm) e
|
||||
return services.Creator.Apply(ctx, tenantID, user.ID, form)
|
||||
}
|
||||
|
||||
// Review join request
|
||||
//
|
||||
// @Router /t/:tenantCode/v1/creator/members/:id<int>/review [post]
|
||||
// @Summary Review join request
|
||||
// @Description Approve or reject a tenant join request
|
||||
// @Tags CreatorCenter
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int64 true "Join request ID"
|
||||
// @Param form body dto.TenantJoinReviewForm true "Review form"
|
||||
// @Success 200 {string} string "Reviewed"
|
||||
// @Bind user local key(__ctx_user)
|
||||
// @Bind id path
|
||||
// @Bind form body
|
||||
func (c *Creator) ReviewMember(ctx fiber.Ctx, user *models.User, id int64, form *dto.TenantJoinReviewForm) error {
|
||||
tenantID := getTenantID(ctx)
|
||||
return services.Tenant.ReviewJoin(ctx, tenantID, user.ID, id, form)
|
||||
}
|
||||
|
||||
// Create member invite
|
||||
//
|
||||
// @Router /t/:tenantCode/v1/creator/members/invite [post]
|
||||
// @Summary Create member invite
|
||||
// @Description Create an invite for tenant members
|
||||
// @Tags CreatorCenter
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param form body dto.TenantInviteCreateForm true "Invite form"
|
||||
// @Success 200 {object} dto.TenantInviteItem
|
||||
// @Bind user local key(__ctx_user)
|
||||
// @Bind form body
|
||||
func (c *Creator) CreateMemberInvite(
|
||||
ctx fiber.Ctx,
|
||||
user *models.User,
|
||||
form *dto.TenantInviteCreateForm,
|
||||
) (*dto.TenantInviteItem, error) {
|
||||
tenantID := getTenantID(ctx)
|
||||
return services.Tenant.CreateInvite(ctx, tenantID, user.ID, form)
|
||||
}
|
||||
|
||||
// Get creator dashboard stats
|
||||
//
|
||||
// @Router /t/:tenantCode/v1/creator/dashboard [get]
|
||||
|
||||
46
backend/app/http/v1/dto/tenant_member.go
Normal file
46
backend/app/http/v1/dto/tenant_member.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package dto
|
||||
|
||||
type TenantJoinApplyForm struct {
|
||||
// Reason 申请加入原因(可选,空值会使用默认文案)。
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
type TenantJoinReviewForm struct {
|
||||
// Action 审核动作(approve/reject)。
|
||||
Action string `json:"action"`
|
||||
// Reason 审核说明(可选,用于展示驳回原因或备注)。
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
type TenantInviteCreateForm struct {
|
||||
// MaxUses 最大可使用次数(<=0 默认 1)。
|
||||
MaxUses int32 `json:"max_uses"`
|
||||
// ExpiresAt 过期时间(RFC3339,可选,空值使用默认过期时间)。
|
||||
ExpiresAt *string `json:"expires_at"`
|
||||
// Remark 备注说明(可选)。
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
type TenantInviteAcceptForm struct {
|
||||
// Code 邀请码(必填)。
|
||||
Code string `json:"code"`
|
||||
}
|
||||
|
||||
type TenantInviteItem struct {
|
||||
// ID 邀请记录ID。
|
||||
ID int64 `json:"id"`
|
||||
// Code 邀请码。
|
||||
Code string `json:"code"`
|
||||
// Status 邀请状态(active/disabled/expired)。
|
||||
Status string `json:"status"`
|
||||
// MaxUses 最大可使用次数。
|
||||
MaxUses int32 `json:"max_uses"`
|
||||
// UsedCount 已使用次数。
|
||||
UsedCount int32 `json:"used_count"`
|
||||
// ExpiresAt 过期时间(RFC3339,空字符串表示不限制)。
|
||||
ExpiresAt string `json:"expires_at"`
|
||||
// CreatedAt 创建时间(RFC3339)。
|
||||
CreatedAt string `json:"created_at"`
|
||||
// Remark 备注说明。
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
@@ -208,6 +208,19 @@ func (r *Routes) Register(router fiber.Router) {
|
||||
Local[*models.User]("__ctx_user"),
|
||||
Body[dto.ContentCreateForm]("form"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /t/:tenantCode/v1/creator/members/:id<int>/review -> creator.ReviewMember")
|
||||
router.Post("/t/:tenantCode/v1/creator/members/:id<int>/review"[len(r.Path()):], Func3(
|
||||
r.creator.ReviewMember,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
PathParam[int64]("id"),
|
||||
Body[dto.TenantJoinReviewForm]("form"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /t/:tenantCode/v1/creator/members/invite -> creator.CreateMemberInvite")
|
||||
router.Post("/t/:tenantCode/v1/creator/members/invite"[len(r.Path()):], DataFunc2(
|
||||
r.creator.CreateMemberInvite,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
Body[dto.TenantInviteCreateForm]("form"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /t/:tenantCode/v1/creator/orders/:id<int>/refund -> creator.Refund")
|
||||
router.Post("/t/:tenantCode/v1/creator/orders/:id<int>/refund"[len(r.Path()):], Func3(
|
||||
r.creator.Refund,
|
||||
@@ -260,6 +273,11 @@ func (r *Routes) Register(router fiber.Router) {
|
||||
Local[*models.User]("__ctx_user"),
|
||||
PathParam[int64]("id"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Delete /t/:tenantCode/v1/tenants/:id<int>/join -> tenant.CancelJoin")
|
||||
router.Delete("/t/:tenantCode/v1/tenants/:id<int>/join"[len(r.Path()):], Func1(
|
||||
r.tenant.CancelJoin,
|
||||
PathParam[int64]("id"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /t/:tenantCode/v1/creators/:id<int>/contents -> tenant.ListContents")
|
||||
router.Get("/t/:tenantCode/v1/creators/:id<int>/contents"[len(r.Path()):], DataFunc2(
|
||||
r.tenant.ListContents,
|
||||
@@ -283,6 +301,18 @@ func (r *Routes) Register(router fiber.Router) {
|
||||
Local[*models.User]("__ctx_user"),
|
||||
PathParam[int64]("id"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /t/:tenantCode/v1/tenants/:id<int>/invites/accept -> tenant.AcceptInvite")
|
||||
router.Post("/t/:tenantCode/v1/tenants/:id<int>/invites/accept"[len(r.Path()):], Func2(
|
||||
r.tenant.AcceptInvite,
|
||||
PathParam[int64]("id"),
|
||||
Body[dto.TenantInviteAcceptForm]("form"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /t/:tenantCode/v1/tenants/:id<int>/join -> tenant.ApplyJoin")
|
||||
router.Post("/t/:tenantCode/v1/tenants/:id<int>/join"[len(r.Path()):], Func2(
|
||||
r.tenant.ApplyJoin,
|
||||
PathParam[int64]("id"),
|
||||
Body[dto.TenantJoinApplyForm]("form"),
|
||||
))
|
||||
// Register routes for controller: Transaction
|
||||
r.log.Debugf("Registering route: Get /t/:tenantCode/v1/orders/:id<int>/status -> transaction.Status")
|
||||
router.Get("/t/:tenantCode/v1/orders/:id<int>/status"[len(r.Path()):], DataFunc2(
|
||||
|
||||
@@ -120,3 +120,67 @@ func (t *Tenant) Unfollow(ctx fiber.Ctx, user *models.User, id int64) error {
|
||||
}
|
||||
return services.Tenant.Unfollow(ctx, tenantID, user.ID)
|
||||
}
|
||||
|
||||
// Apply to join a tenant
|
||||
//
|
||||
// @Router /t/:tenantCode/v1/tenants/:id<int>/join [post]
|
||||
// @Summary Apply to join tenant
|
||||
// @Description Submit join request for a tenant
|
||||
// @Tags TenantPublic
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int64 true "Tenant ID"
|
||||
// @Param form body dto.TenantJoinApplyForm true "Join form"
|
||||
// @Success 200 {string} string "Applied"
|
||||
// @Bind id path
|
||||
// @Bind form body
|
||||
func (t *Tenant) ApplyJoin(ctx fiber.Ctx, id int64, form *dto.TenantJoinApplyForm) error {
|
||||
tenantID := getTenantID(ctx)
|
||||
if tenantID > 0 && id != tenantID {
|
||||
return errorx.ErrForbidden.WithMsg("租户不匹配")
|
||||
}
|
||||
userID := getUserID(ctx)
|
||||
return services.Tenant.ApplyJoin(ctx, id, userID, form)
|
||||
}
|
||||
|
||||
// Cancel join request
|
||||
//
|
||||
// @Router /t/:tenantCode/v1/tenants/:id<int>/join [delete]
|
||||
// @Summary Cancel join request
|
||||
// @Description Cancel join request for a tenant
|
||||
// @Tags TenantPublic
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int64 true "Tenant ID"
|
||||
// @Success 200 {string} string "Canceled"
|
||||
// @Bind id path
|
||||
func (t *Tenant) CancelJoin(ctx fiber.Ctx, id int64) error {
|
||||
tenantID := getTenantID(ctx)
|
||||
if tenantID > 0 && id != tenantID {
|
||||
return errorx.ErrForbidden.WithMsg("租户不匹配")
|
||||
}
|
||||
userID := getUserID(ctx)
|
||||
return services.Tenant.CancelJoin(ctx, id, userID)
|
||||
}
|
||||
|
||||
// Accept tenant invite
|
||||
//
|
||||
// @Router /t/:tenantCode/v1/tenants/:id<int>/invites/accept [post]
|
||||
// @Summary Accept tenant invite
|
||||
// @Description Accept a tenant invite by code
|
||||
// @Tags TenantPublic
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int64 true "Tenant ID"
|
||||
// @Param form body dto.TenantInviteAcceptForm true "Invite form"
|
||||
// @Success 200 {string} string "Accepted"
|
||||
// @Bind id path
|
||||
// @Bind form body
|
||||
func (t *Tenant) AcceptInvite(ctx fiber.Ctx, id int64, form *dto.TenantInviteAcceptForm) error {
|
||||
tenantID := getTenantID(ctx)
|
||||
if tenantID > 0 && id != tenantID {
|
||||
return errorx.ErrForbidden.WithMsg("租户不匹配")
|
||||
}
|
||||
userID := getUserID(ctx)
|
||||
return services.Tenant.AcceptInvite(ctx, id, userID, form)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
427
backend/app/services/tenant_member.go
Normal file
427
backend/app/services/tenant_member.go
Normal 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,
|
||||
}
|
||||
}
|
||||
304
backend/app/services/tenant_member_test.go
Normal file
304
backend/app/services/tenant_member_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
@@ -1637,6 +1637,82 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/t/{tenantCode}/v1/creator/members/invite": {
|
||||
"post": {
|
||||
"description": "Create an invite for tenant members",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"CreatorCenter"
|
||||
],
|
||||
"summary": "Create member invite",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Invite form",
|
||||
"name": "form",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.TenantInviteCreateForm"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.TenantInviteItem"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/t/{tenantCode}/v1/creator/members/{id}/review": {
|
||||
"post": {
|
||||
"description": "Approve or reject a tenant join request",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"CreatorCenter"
|
||||
],
|
||||
"summary": "Review join request",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "Join request ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Review form",
|
||||
"name": "form",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.TenantJoinReviewForm"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Reviewed",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/t/{tenantCode}/v1/creator/orders": {
|
||||
"get": {
|
||||
"description": "List sales orders",
|
||||
@@ -2895,6 +2971,121 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/t/{tenantCode}/v1/tenants/{id}/invites/accept": {
|
||||
"post": {
|
||||
"description": "Accept a tenant invite by code",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"TenantPublic"
|
||||
],
|
||||
"summary": "Accept tenant invite",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "Tenant ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Invite form",
|
||||
"name": "form",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.TenantInviteAcceptForm"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Accepted",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/t/{tenantCode}/v1/tenants/{id}/join": {
|
||||
"post": {
|
||||
"description": "Submit join request for a tenant",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"TenantPublic"
|
||||
],
|
||||
"summary": "Apply to join tenant",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "Tenant ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Join form",
|
||||
"name": "form",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.TenantJoinApplyForm"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Applied",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"description": "Cancel join request for a tenant",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"TenantPublic"
|
||||
],
|
||||
"summary": "Cancel join request",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "Tenant ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Canceled",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/t/{tenantCode}/v1/topics": {
|
||||
"get": {
|
||||
"description": "List curated topics",
|
||||
@@ -4624,6 +4815,69 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.TenantInviteAcceptForm": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"description": "Code 邀请码(必填)。",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.TenantInviteCreateForm": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"expires_at": {
|
||||
"description": "ExpiresAt 过期时间(RFC3339,可选,空值使用默认过期时间)。",
|
||||
"type": "string"
|
||||
},
|
||||
"max_uses": {
|
||||
"description": "MaxUses 最大可使用次数(\u003c=0 默认 1)。",
|
||||
"type": "integer"
|
||||
},
|
||||
"remark": {
|
||||
"description": "Remark 备注说明(可选)。",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.TenantInviteItem": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"description": "Code 邀请码。",
|
||||
"type": "string"
|
||||
},
|
||||
"created_at": {
|
||||
"description": "CreatedAt 创建时间(RFC3339)。",
|
||||
"type": "string"
|
||||
},
|
||||
"expires_at": {
|
||||
"description": "ExpiresAt 过期时间(RFC3339,空字符串表示不限制)。",
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"description": "ID 邀请记录ID。",
|
||||
"type": "integer"
|
||||
},
|
||||
"max_uses": {
|
||||
"description": "MaxUses 最大可使用次数。",
|
||||
"type": "integer"
|
||||
},
|
||||
"remark": {
|
||||
"description": "Remark 备注说明。",
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"description": "Status 邀请状态(active/disabled/expired)。",
|
||||
"type": "string"
|
||||
},
|
||||
"used_count": {
|
||||
"description": "UsedCount 已使用次数。",
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.TenantItem": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -4710,6 +4964,28 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.TenantJoinApplyForm": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"reason": {
|
||||
"description": "Reason 申请加入原因(可选,空值会使用默认文案)。",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.TenantJoinReviewForm": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {
|
||||
"description": "Action 审核动作(approve/reject)。",
|
||||
"type": "string"
|
||||
},
|
||||
"reason": {
|
||||
"description": "Reason 审核说明(可选,用于展示驳回原因或备注)。",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.TenantOwnerUserLite": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -1631,6 +1631,82 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/t/{tenantCode}/v1/creator/members/invite": {
|
||||
"post": {
|
||||
"description": "Create an invite for tenant members",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"CreatorCenter"
|
||||
],
|
||||
"summary": "Create member invite",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Invite form",
|
||||
"name": "form",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.TenantInviteCreateForm"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.TenantInviteItem"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/t/{tenantCode}/v1/creator/members/{id}/review": {
|
||||
"post": {
|
||||
"description": "Approve or reject a tenant join request",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"CreatorCenter"
|
||||
],
|
||||
"summary": "Review join request",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "Join request ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Review form",
|
||||
"name": "form",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.TenantJoinReviewForm"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Reviewed",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/t/{tenantCode}/v1/creator/orders": {
|
||||
"get": {
|
||||
"description": "List sales orders",
|
||||
@@ -2889,6 +2965,121 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/t/{tenantCode}/v1/tenants/{id}/invites/accept": {
|
||||
"post": {
|
||||
"description": "Accept a tenant invite by code",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"TenantPublic"
|
||||
],
|
||||
"summary": "Accept tenant invite",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "Tenant ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Invite form",
|
||||
"name": "form",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.TenantInviteAcceptForm"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Accepted",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/t/{tenantCode}/v1/tenants/{id}/join": {
|
||||
"post": {
|
||||
"description": "Submit join request for a tenant",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"TenantPublic"
|
||||
],
|
||||
"summary": "Apply to join tenant",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "Tenant ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Join form",
|
||||
"name": "form",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dto.TenantJoinApplyForm"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Applied",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"description": "Cancel join request for a tenant",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"TenantPublic"
|
||||
],
|
||||
"summary": "Cancel join request",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "Tenant ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Canceled",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/t/{tenantCode}/v1/topics": {
|
||||
"get": {
|
||||
"description": "List curated topics",
|
||||
@@ -4618,6 +4809,69 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.TenantInviteAcceptForm": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"description": "Code 邀请码(必填)。",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.TenantInviteCreateForm": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"expires_at": {
|
||||
"description": "ExpiresAt 过期时间(RFC3339,可选,空值使用默认过期时间)。",
|
||||
"type": "string"
|
||||
},
|
||||
"max_uses": {
|
||||
"description": "MaxUses 最大可使用次数(\u003c=0 默认 1)。",
|
||||
"type": "integer"
|
||||
},
|
||||
"remark": {
|
||||
"description": "Remark 备注说明(可选)。",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.TenantInviteItem": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"description": "Code 邀请码。",
|
||||
"type": "string"
|
||||
},
|
||||
"created_at": {
|
||||
"description": "CreatedAt 创建时间(RFC3339)。",
|
||||
"type": "string"
|
||||
},
|
||||
"expires_at": {
|
||||
"description": "ExpiresAt 过期时间(RFC3339,空字符串表示不限制)。",
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"description": "ID 邀请记录ID。",
|
||||
"type": "integer"
|
||||
},
|
||||
"max_uses": {
|
||||
"description": "MaxUses 最大可使用次数。",
|
||||
"type": "integer"
|
||||
},
|
||||
"remark": {
|
||||
"description": "Remark 备注说明。",
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"description": "Status 邀请状态(active/disabled/expired)。",
|
||||
"type": "string"
|
||||
},
|
||||
"used_count": {
|
||||
"description": "UsedCount 已使用次数。",
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.TenantItem": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -4704,6 +4958,28 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.TenantJoinApplyForm": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"reason": {
|
||||
"description": "Reason 申请加入原因(可选,空值会使用默认文案)。",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.TenantJoinReviewForm": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {
|
||||
"description": "Action 审核动作(approve/reject)。",
|
||||
"type": "string"
|
||||
},
|
||||
"reason": {
|
||||
"description": "Reason 审核说明(可选,用于展示驳回原因或备注)。",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.TenantOwnerUserLite": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -1015,6 +1015,51 @@ definitions:
|
||||
required:
|
||||
- duration
|
||||
type: object
|
||||
dto.TenantInviteAcceptForm:
|
||||
properties:
|
||||
code:
|
||||
description: Code 邀请码(必填)。
|
||||
type: string
|
||||
type: object
|
||||
dto.TenantInviteCreateForm:
|
||||
properties:
|
||||
expires_at:
|
||||
description: ExpiresAt 过期时间(RFC3339,可选,空值使用默认过期时间)。
|
||||
type: string
|
||||
max_uses:
|
||||
description: MaxUses 最大可使用次数(<=0 默认 1)。
|
||||
type: integer
|
||||
remark:
|
||||
description: Remark 备注说明(可选)。
|
||||
type: string
|
||||
type: object
|
||||
dto.TenantInviteItem:
|
||||
properties:
|
||||
code:
|
||||
description: Code 邀请码。
|
||||
type: string
|
||||
created_at:
|
||||
description: CreatedAt 创建时间(RFC3339)。
|
||||
type: string
|
||||
expires_at:
|
||||
description: ExpiresAt 过期时间(RFC3339,空字符串表示不限制)。
|
||||
type: string
|
||||
id:
|
||||
description: ID 邀请记录ID。
|
||||
type: integer
|
||||
max_uses:
|
||||
description: MaxUses 最大可使用次数。
|
||||
type: integer
|
||||
remark:
|
||||
description: Remark 备注说明。
|
||||
type: string
|
||||
status:
|
||||
description: Status 邀请状态(active/disabled/expired)。
|
||||
type: string
|
||||
used_count:
|
||||
description: UsedCount 已使用次数。
|
||||
type: integer
|
||||
type: object
|
||||
dto.TenantItem:
|
||||
properties:
|
||||
admin_users:
|
||||
@@ -1074,6 +1119,21 @@ definitions:
|
||||
description: UUID 租户UUID。
|
||||
type: string
|
||||
type: object
|
||||
dto.TenantJoinApplyForm:
|
||||
properties:
|
||||
reason:
|
||||
description: Reason 申请加入原因(可选,空值会使用默认文案)。
|
||||
type: string
|
||||
type: object
|
||||
dto.TenantJoinReviewForm:
|
||||
properties:
|
||||
action:
|
||||
description: Action 审核动作(approve/reject)。
|
||||
type: string
|
||||
reason:
|
||||
description: Reason 审核说明(可选,用于展示驳回原因或备注)。
|
||||
type: string
|
||||
type: object
|
||||
dto.TenantOwnerUserLite:
|
||||
properties:
|
||||
id:
|
||||
@@ -2662,6 +2722,56 @@ paths:
|
||||
summary: Dashboard stats
|
||||
tags:
|
||||
- CreatorCenter
|
||||
/t/{tenantCode}/v1/creator/members/{id}/review:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Approve or reject a tenant join request
|
||||
parameters:
|
||||
- description: Join request ID
|
||||
format: int64
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: integer
|
||||
- description: Review form
|
||||
in: body
|
||||
name: form
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/dto.TenantJoinReviewForm'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Reviewed
|
||||
schema:
|
||||
type: string
|
||||
summary: Review join request
|
||||
tags:
|
||||
- CreatorCenter
|
||||
/t/{tenantCode}/v1/creator/members/invite:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Create an invite for tenant members
|
||||
parameters:
|
||||
- description: Invite form
|
||||
in: body
|
||||
name: form
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/dto.TenantInviteCreateForm'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/dto.TenantInviteItem'
|
||||
summary: Create member invite
|
||||
tags:
|
||||
- CreatorCenter
|
||||
/t/{tenantCode}/v1/creator/orders:
|
||||
get:
|
||||
consumes:
|
||||
@@ -3488,6 +3598,83 @@ paths:
|
||||
summary: Follow tenant
|
||||
tags:
|
||||
- TenantPublic
|
||||
/t/{tenantCode}/v1/tenants/{id}/invites/accept:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Accept a tenant invite by code
|
||||
parameters:
|
||||
- description: Tenant ID
|
||||
format: int64
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: integer
|
||||
- description: Invite form
|
||||
in: body
|
||||
name: form
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/dto.TenantInviteAcceptForm'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Accepted
|
||||
schema:
|
||||
type: string
|
||||
summary: Accept tenant invite
|
||||
tags:
|
||||
- TenantPublic
|
||||
/t/{tenantCode}/v1/tenants/{id}/join:
|
||||
delete:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Cancel join request for a tenant
|
||||
parameters:
|
||||
- description: Tenant ID
|
||||
format: int64
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Canceled
|
||||
schema:
|
||||
type: string
|
||||
summary: Cancel join request
|
||||
tags:
|
||||
- TenantPublic
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Submit join request for a tenant
|
||||
parameters:
|
||||
- description: Tenant ID
|
||||
format: int64
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: integer
|
||||
- description: Join form
|
||||
in: body
|
||||
name: form
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/dto.TenantJoinApplyForm'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Applied
|
||||
schema:
|
||||
type: string
|
||||
summary: Apply to join tenant
|
||||
tags:
|
||||
- TenantPublic
|
||||
/t/{tenantCode}/v1/topics:
|
||||
get:
|
||||
consumes:
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
- API
|
||||
- 申请加入:`POST /t/:tenantCode/v1/tenants/:id<int>/join`
|
||||
- 取消申请:`DELETE /t/:tenantCode/v1/tenants/:id<int>/join`
|
||||
- 接受邀请:`POST /t/:tenantCode/v1/tenants/:id<int>/invites/accept`
|
||||
- 管理员审核:`POST /t/:tenantCode/v1/creator/members/:id<int>/review`
|
||||
- 邀请成员:`POST /t/:tenantCode/v1/creator/members/invite`
|
||||
- DB
|
||||
|
||||
Reference in New Issue
Block a user