feat: add creator member management

This commit is contained in:
2026-01-17 20:42:43 +08:00
parent 984a404b5f
commit 7fca7a40e7
14 changed files with 2915 additions and 81 deletions

View File

@@ -70,6 +70,91 @@ func (c *Creator) CreateMemberInvite(
return services.Tenant.CreateInvite(ctx, tenantID, user.ID, form)
}
// List tenant members
//
// @Router /t/:tenantCode/v1/creator/members [get]
// @Summary List tenant members
// @Description List tenant members with filters
// @Tags CreatorCenter
// @Accept json
// @Produce json
// @Param filter query dto.TenantMemberListFilter false "Member list filter"
// @Success 200 {object} requests.Pager{items=[]dto.TenantMemberItem}
// @Bind user local key(__ctx_user)
// @Bind filter query
func (c *Creator) ListMembers(ctx fiber.Ctx, user *models.User, filter *dto.TenantMemberListFilter) (*requests.Pager, error) {
tenantID := getTenantID(ctx)
return services.Tenant.ListMembers(ctx, tenantID, user.ID, filter)
}
// List member invites
//
// @Router /t/:tenantCode/v1/creator/members/invites [get]
// @Summary List member invites
// @Description List member invites with filters
// @Tags CreatorCenter
// @Accept json
// @Produce json
// @Param filter query dto.TenantInviteListFilter false "Invite list filter"
// @Success 200 {object} requests.Pager{items=[]dto.TenantInviteListItem}
// @Bind user local key(__ctx_user)
// @Bind filter query
func (c *Creator) ListMemberInvites(ctx fiber.Ctx, user *models.User, filter *dto.TenantInviteListFilter) (*requests.Pager, error) {
tenantID := getTenantID(ctx)
return services.Tenant.ListInvites(ctx, tenantID, user.ID, filter)
}
// Disable member invite
//
// @Router /t/:tenantCode/v1/creator/members/invites/:id<int> [delete]
// @Summary Disable member invite
// @Description Disable a member invite by ID
// @Tags CreatorCenter
// @Accept json
// @Produce json
// @Param id path int64 true "Invite ID"
// @Success 200 {string} string "Disabled"
// @Bind user local key(__ctx_user)
// @Bind id path
func (c *Creator) DisableMemberInvite(ctx fiber.Ctx, user *models.User, id int64) error {
tenantID := getTenantID(ctx)
return services.Tenant.DisableInvite(ctx, tenantID, user.ID, id)
}
// List member join requests
//
// @Router /t/:tenantCode/v1/creator/members/join-requests [get]
// @Summary List member join requests
// @Description List tenant join requests
// @Tags CreatorCenter
// @Accept json
// @Produce json
// @Param filter query dto.TenantJoinRequestListFilter false "Join request list filter"
// @Success 200 {object} requests.Pager{items=[]dto.TenantJoinRequestItem}
// @Bind user local key(__ctx_user)
// @Bind filter query
func (c *Creator) ListMemberJoinRequests(ctx fiber.Ctx, user *models.User, filter *dto.TenantJoinRequestListFilter) (*requests.Pager, error) {
tenantID := getTenantID(ctx)
return services.Tenant.ListJoinRequests(ctx, tenantID, user.ID, filter)
}
// Remove tenant member
//
// @Router /t/:tenantCode/v1/creator/members/:id<int> [delete]
// @Summary Remove tenant member
// @Description Remove a tenant member by relation ID
// @Tags CreatorCenter
// @Accept json
// @Produce json
// @Param id path int64 true "Member ID"
// @Success 200 {string} string "Removed"
// @Bind user local key(__ctx_user)
// @Bind id path
func (c *Creator) RemoveMember(ctx fiber.Ctx, user *models.User, id int64) error {
tenantID := getTenantID(ctx)
return services.Tenant.RemoveMember(ctx, tenantID, user.ID, id)
}
// Get report overview
//
// @Router /t/:tenantCode/v1/creator/reports/overview [get]

View File

@@ -1,5 +1,10 @@
package dto
import (
"quyun/v2/app/requests"
"quyun/v2/pkg/consts"
)
type TenantJoinApplyForm struct {
// Reason 申请加入原因(可选,空值会使用默认文案)。
Reason string `json:"reason"`
@@ -44,3 +49,110 @@ type TenantInviteItem struct {
// Remark 备注说明。
Remark string `json:"remark"`
}
type TenantMemberListFilter struct {
// Pagination 分页参数page/limit
requests.Pagination
// Keyword 关键词搜索(匹配用户名/昵称/手机号)。
Keyword *string `query:"keyword"`
// Role 成员角色筛选member/tenant_admin
Role *consts.TenantUserRole `query:"role"`
// Status 成员状态筛选active/verified/banned 等)。
Status *consts.UserStatus `query:"status"`
}
type TenantMemberUserLite struct {
// ID 用户ID。
ID int64 `json:"id"`
// Username 用户名。
Username string `json:"username"`
// Phone 手机号。
Phone string `json:"phone"`
// Nickname 昵称。
Nickname string `json:"nickname"`
// Avatar 头像URL。
Avatar string `json:"avatar"`
}
type TenantMemberItem struct {
// ID 成员关系记录ID。
ID int64 `json:"id"`
// TenantID 租户ID。
TenantID int64 `json:"tenant_id"`
// User 成员用户信息。
User *TenantMemberUserLite `json:"user"`
// Role 成员角色列表。
Role []consts.TenantUserRole `json:"role"`
// RoleDescription 角色描述列表。
RoleDescription []string `json:"role_description"`
// Status 成员状态。
Status consts.UserStatus `json:"status"`
// StatusDescription 成员状态描述。
StatusDescription string `json:"status_description"`
// CreatedAt 加入时间RFC3339
CreatedAt string `json:"created_at"`
// UpdatedAt 更新时间RFC3339
UpdatedAt string `json:"updated_at"`
}
type TenantInviteListFilter struct {
// Pagination 分页参数page/limit
requests.Pagination
// Status 邀请状态筛选active/disabled/expired
Status *consts.TenantInviteStatus `query:"status"`
}
type TenantInviteListItem struct {
// ID 邀请记录ID。
ID int64 `json:"id"`
// Code 邀请码。
Code string `json:"code"`
// Status 邀请状态active/disabled/expired
Status string `json:"status"`
// StatusDescription 状态描述。
StatusDescription string `json:"status_description"`
// 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"`
// Creator 创建者信息(可选)。
Creator *TenantMemberUserLite `json:"creator"`
}
type TenantJoinRequestListFilter struct {
// Pagination 分页参数page/limit
requests.Pagination
// Status 申请状态筛选pending/approved/rejected
Status *consts.TenantJoinRequestStatus `query:"status"`
// Keyword 关键词搜索(匹配用户名/昵称/手机号)。
Keyword *string `query:"keyword"`
}
type TenantJoinRequestItem struct {
// ID 申请记录ID。
ID int64 `json:"id"`
// User 申请用户信息。
User *TenantMemberUserLite `json:"user"`
// Status 申请状态。
Status string `json:"status"`
// StatusDescription 状态描述。
StatusDescription string `json:"status_description"`
// Reason 申请说明。
Reason string `json:"reason"`
// DecidedAt 审核时间RFC3339
DecidedAt string `json:"decided_at"`
// DecidedOperatorUserID 审核操作者ID。
DecidedOperatorUserID int64 `json:"decided_operator_user_id"`
// DecidedReason 审核备注/原因。
DecidedReason string `json:"decided_reason"`
// CreatedAt 申请时间RFC3339
CreatedAt string `json:"created_at"`
// UpdatedAt 更新时间RFC3339
UpdatedAt string `json:"updated_at"`
}

View File

@@ -157,6 +157,18 @@ func (r *Routes) Register(router fiber.Router) {
Local[*models.User]("__ctx_user"),
PathParam[int64]("id"),
))
r.log.Debugf("Registering route: Delete /t/:tenantCode/v1/creator/members/:id<int> -> creator.RemoveMember")
router.Delete("/t/:tenantCode/v1/creator/members/:id<int>"[len(r.Path()):], Func2(
r.creator.RemoveMember,
Local[*models.User]("__ctx_user"),
PathParam[int64]("id"),
))
r.log.Debugf("Registering route: Delete /t/:tenantCode/v1/creator/members/invites/:id<int> -> creator.DisableMemberInvite")
router.Delete("/t/:tenantCode/v1/creator/members/invites/:id<int>"[len(r.Path()):], Func2(
r.creator.DisableMemberInvite,
Local[*models.User]("__ctx_user"),
PathParam[int64]("id"),
))
r.log.Debugf("Registering route: Delete /t/:tenantCode/v1/creator/payout-accounts -> creator.RemovePayoutAccount")
router.Delete("/t/:tenantCode/v1/creator/payout-accounts"[len(r.Path()):], Func2(
r.creator.RemovePayoutAccount,
@@ -192,6 +204,24 @@ func (r *Routes) Register(router fiber.Router) {
r.creator.Dashboard,
Local[*models.User]("__ctx_user"),
))
r.log.Debugf("Registering route: Get /t/:tenantCode/v1/creator/members -> creator.ListMembers")
router.Get("/t/:tenantCode/v1/creator/members"[len(r.Path()):], DataFunc2(
r.creator.ListMembers,
Local[*models.User]("__ctx_user"),
Query[dto.TenantMemberListFilter]("filter"),
))
r.log.Debugf("Registering route: Get /t/:tenantCode/v1/creator/members/invites -> creator.ListMemberInvites")
router.Get("/t/:tenantCode/v1/creator/members/invites"[len(r.Path()):], DataFunc2(
r.creator.ListMemberInvites,
Local[*models.User]("__ctx_user"),
Query[dto.TenantInviteListFilter]("filter"),
))
r.log.Debugf("Registering route: Get /t/:tenantCode/v1/creator/members/join-requests -> creator.ListMemberJoinRequests")
router.Get("/t/:tenantCode/v1/creator/members/join-requests"[len(r.Path()):], DataFunc2(
r.creator.ListMemberJoinRequests,
Local[*models.User]("__ctx_user"),
Query[dto.TenantJoinRequestListFilter]("filter"),
))
r.log.Debugf("Registering route: Get /t/:tenantCode/v1/creator/orders -> creator.ListOrders")
router.Get("/t/:tenantCode/v1/creator/orders"[len(r.Path()):], DataFunc2(
r.creator.ListOrders,

View File

@@ -8,10 +8,12 @@ import (
"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"
)
@@ -337,6 +339,346 @@ func (s *tenant) AcceptInvite(ctx context.Context, tenantID, userID int64, form
})
}
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
@@ -425,3 +767,64 @@ func (s *tenant) toTenantInviteItem(invite *models.TenantInvite) *tenant_dto.Ten
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)
}

View File

@@ -6,6 +6,7 @@ import (
"quyun/v2/app/errorx"
tenant_dto "quyun/v2/app/http/v1/dto"
"quyun/v2/app/requests"
"quyun/v2/database"
"quyun/v2/database/models"
"quyun/v2/pkg/consts"
@@ -302,3 +303,142 @@ func (s *TenantTestSuite) Test_AcceptInvite() {
So(invite.UsedCount, ShouldEqual, 1)
})
}
func (s *TenantTestSuite) Test_ListMembersAndRemove() {
Convey("ListMembers and RemoveMember", s.T(), func() {
ctx := s.T().Context()
database.Truncate(ctx, s.DB,
models.TableNameTenantUser,
models.TableNameTenant,
models.TableNameUser,
)
owner := &models.User{Username: "owner_member", Phone: "13900002001"}
member := &models.User{Username: "member_user", Phone: "13900002002"}
_ = models.UserQuery.WithContext(ctx).Create(owner)
_ = models.UserQuery.WithContext(ctx).Create(member)
tenant := &models.Tenant{
Name: "Tenant Member",
UserID: owner.ID,
Status: consts.TenantStatusVerified,
}
_ = models.TenantQuery.WithContext(ctx).Create(tenant)
link := &models.TenantUser{
TenantID: tenant.ID,
UserID: member.ID,
Role: types.Array[consts.TenantUserRole]{consts.TenantUserRoleMember},
Status: consts.UserStatusVerified,
}
_ = models.TenantUserQuery.WithContext(ctx).Create(link)
res, err := Tenant.ListMembers(ctx, tenant.ID, owner.ID, &tenant_dto.TenantMemberListFilter{
Pagination: requests.Pagination{Page: 1, Limit: 10},
})
So(err, ShouldBeNil)
So(res.Total, ShouldEqual, 1)
items := res.Items.([]tenant_dto.TenantMemberItem)
So(items[0].User.ID, ShouldEqual, member.ID)
err = Tenant.RemoveMember(ctx, tenant.ID, owner.ID, items[0].ID)
So(err, ShouldBeNil)
exists, err := models.TenantUserQuery.WithContext(ctx).
Where(models.TenantUserQuery.ID.Eq(items[0].ID)).
Exists()
So(err, ShouldBeNil)
So(exists, ShouldBeFalse)
})
}
func (s *TenantTestSuite) Test_ListInvitesAndDisable() {
Convey("ListInvites and DisableInvite", s.T(), func() {
ctx := s.T().Context()
database.Truncate(ctx, s.DB,
models.TableNameTenantInvite,
models.TableNameTenant,
models.TableNameUser,
)
owner := &models.User{Username: "owner_invite", Phone: "13900002003"}
_ = models.UserQuery.WithContext(ctx).Create(owner)
tenant := &models.Tenant{
Name: "Tenant Invite",
UserID: owner.ID,
Status: consts.TenantStatusVerified,
}
_ = models.TenantQuery.WithContext(ctx).Create(tenant)
invite := &models.TenantInvite{
TenantID: tenant.ID,
UserID: owner.ID,
Code: "invite_list",
Status: string(consts.TenantInviteStatusActive),
MaxUses: 2,
UsedCount: 0,
ExpiresAt: time.Now().Add(24 * time.Hour),
Remark: "测试邀请",
}
_ = models.TenantInviteQuery.WithContext(ctx).Create(invite)
res, err := Tenant.ListInvites(ctx, tenant.ID, owner.ID, &tenant_dto.TenantInviteListFilter{
Pagination: requests.Pagination{Page: 1, Limit: 10},
})
So(err, ShouldBeNil)
So(res.Total, ShouldEqual, 1)
err = Tenant.DisableInvite(ctx, tenant.ID, owner.ID, invite.ID)
So(err, ShouldBeNil)
updated, err := models.TenantInviteQuery.WithContext(ctx).
Where(models.TenantInviteQuery.ID.Eq(invite.ID)).
First()
So(err, ShouldBeNil)
So(updated.Status, ShouldEqual, string(consts.TenantInviteStatusDisabled))
})
}
func (s *TenantTestSuite) Test_ListJoinRequests() {
Convey("ListJoinRequests", s.T(), func() {
ctx := s.T().Context()
database.Truncate(ctx, s.DB,
models.TableNameTenantJoinRequest,
models.TableNameTenant,
models.TableNameUser,
)
owner := &models.User{Username: "owner_request", Phone: "13900002004"}
user := &models.User{Username: "request_user", Phone: "13900002005"}
_ = models.UserQuery.WithContext(ctx).Create(owner)
_ = models.UserQuery.WithContext(ctx).Create(user)
tenant := &models.Tenant{
Name: "Tenant Request",
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)
status := consts.TenantJoinRequestStatusPending
res, err := Tenant.ListJoinRequests(ctx, tenant.ID, owner.ID, &tenant_dto.TenantJoinRequestListFilter{
Pagination: requests.Pagination{Page: 1, Limit: 10},
Status: &status,
})
So(err, ShouldBeNil)
So(res.Total, ShouldEqual, 1)
})
}

View File

@@ -4617,6 +4617,98 @@ const docTemplate = `{
}
}
},
"/t/{tenantCode}/v1/creator/members": {
"get": {
"description": "List tenant members with filters",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"CreatorCenter"
],
"summary": "List tenant members",
"parameters": [
{
"type": "string",
"description": "Keyword 关键词搜索(匹配用户名/昵称/手机号)。",
"name": "keyword",
"in": "query"
},
{
"type": "integer",
"description": "Limit is page size; only values in {10,20,50,100} are accepted (otherwise defaults to 10).",
"name": "limit",
"in": "query"
},
{
"type": "integer",
"description": "Page is 1-based page index; values \u003c= 0 are normalized to 1.",
"name": "page",
"in": "query"
},
{
"enum": [
"member",
"tenant_admin"
],
"type": "string",
"x-enum-varnames": [
"TenantUserRoleMember",
"TenantUserRoleTenantAdmin"
],
"description": "Role 成员角色筛选member/tenant_admin。",
"name": "role",
"in": "query"
},
{
"enum": [
"active",
"inactive",
"pending_verify",
"verified",
"banned"
],
"type": "string",
"x-enum-varnames": [
"UserStatusActive",
"UserStatusInactive",
"UserStatusPendingVerify",
"UserStatusVerified",
"UserStatusBanned"
],
"description": "Status 成员状态筛选active/verified/banned 等)。",
"name": "status",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/requests.Pager"
},
{
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/dto.TenantMemberItem"
}
}
}
}
]
}
}
}
}
},
"/t/{tenantCode}/v1/creator/members/invite": {
"post": {
"description": "Create an invite for tenant members",
@@ -4651,6 +4743,214 @@ const docTemplate = `{
}
}
},
"/t/{tenantCode}/v1/creator/members/invites": {
"get": {
"description": "List member invites with filters",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"CreatorCenter"
],
"summary": "List member invites",
"parameters": [
{
"type": "integer",
"description": "Limit is page size; only values in {10,20,50,100} are accepted (otherwise defaults to 10).",
"name": "limit",
"in": "query"
},
{
"type": "integer",
"description": "Page is 1-based page index; values \u003c= 0 are normalized to 1.",
"name": "page",
"in": "query"
},
{
"enum": [
"active",
"disabled",
"expired"
],
"type": "string",
"x-enum-varnames": [
"TenantInviteStatusActive",
"TenantInviteStatusDisabled",
"TenantInviteStatusExpired"
],
"description": "Status 邀请状态筛选active/disabled/expired。",
"name": "status",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/requests.Pager"
},
{
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/dto.TenantInviteListItem"
}
}
}
}
]
}
}
}
}
},
"/t/{tenantCode}/v1/creator/members/invites/{id}": {
"delete": {
"description": "Disable a member invite by ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"CreatorCenter"
],
"summary": "Disable member invite",
"parameters": [
{
"type": "integer",
"format": "int64",
"description": "Invite ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "Disabled",
"schema": {
"type": "string"
}
}
}
}
},
"/t/{tenantCode}/v1/creator/members/join-requests": {
"get": {
"description": "List tenant join requests",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"CreatorCenter"
],
"summary": "List member join requests",
"parameters": [
{
"type": "string",
"description": "Keyword 关键词搜索(匹配用户名/昵称/手机号)。",
"name": "keyword",
"in": "query"
},
{
"type": "integer",
"description": "Limit is page size; only values in {10,20,50,100} are accepted (otherwise defaults to 10).",
"name": "limit",
"in": "query"
},
{
"type": "integer",
"description": "Page is 1-based page index; values \u003c= 0 are normalized to 1.",
"name": "page",
"in": "query"
},
{
"enum": [
"pending",
"approved",
"rejected"
],
"type": "string",
"x-enum-varnames": [
"TenantJoinRequestStatusPending",
"TenantJoinRequestStatusApproved",
"TenantJoinRequestStatusRejected"
],
"description": "Status 申请状态筛选pending/approved/rejected。",
"name": "status",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/requests.Pager"
},
{
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/dto.TenantJoinRequestItem"
}
}
}
}
]
}
}
}
}
},
"/t/{tenantCode}/v1/creator/members/{id}": {
"delete": {
"description": "Remove a tenant member by relation ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"CreatorCenter"
],
"summary": "Remove tenant member",
"parameters": [
{
"type": "integer",
"format": "int64",
"description": "Member ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "Removed",
"schema": {
"type": "string"
}
}
}
}
},
"/t/{tenantCode}/v1/creator/members/{id}/review": {
"post": {
"description": "Approve or reject a tenant join request",
@@ -6666,6 +6966,32 @@ const docTemplate = `{
"RoleCreator"
]
},
"consts.TenantInviteStatus": {
"type": "string",
"enum": [
"active",
"disabled",
"expired"
],
"x-enum-varnames": [
"TenantInviteStatusActive",
"TenantInviteStatusDisabled",
"TenantInviteStatusExpired"
]
},
"consts.TenantJoinRequestStatus": {
"type": "string",
"enum": [
"pending",
"approved",
"rejected"
],
"x-enum-varnames": [
"TenantJoinRequestStatusPending",
"TenantJoinRequestStatusApproved",
"TenantJoinRequestStatusRejected"
]
},
"consts.TenantLedgerType": {
"type": "string",
"enum": [
@@ -10641,6 +10967,55 @@ const docTemplate = `{
}
}
},
"dto.TenantInviteListItem": {
"type": "object",
"properties": {
"code": {
"description": "Code 邀请码。",
"type": "string"
},
"created_at": {
"description": "CreatedAt 创建时间RFC3339。",
"type": "string"
},
"creator": {
"description": "Creator 创建者信息(可选)。",
"allOf": [
{
"$ref": "#/definitions/dto.TenantMemberUserLite"
}
]
},
"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"
},
"status_description": {
"description": "StatusDescription 状态描述。",
"type": "string"
},
"used_count": {
"description": "UsedCount 已使用次数。",
"type": "integer"
}
}
},
"dto.TenantItem": {
"type": "object",
"properties": {
@@ -10736,6 +11111,55 @@ const docTemplate = `{
}
}
},
"dto.TenantJoinRequestItem": {
"type": "object",
"properties": {
"created_at": {
"description": "CreatedAt 申请时间RFC3339。",
"type": "string"
},
"decided_at": {
"description": "DecidedAt 审核时间RFC3339。",
"type": "string"
},
"decided_operator_user_id": {
"description": "DecidedOperatorUserID 审核操作者ID。",
"type": "integer"
},
"decided_reason": {
"description": "DecidedReason 审核备注/原因。",
"type": "string"
},
"id": {
"description": "ID 申请记录ID。",
"type": "integer"
},
"reason": {
"description": "Reason 申请说明。",
"type": "string"
},
"status": {
"description": "Status 申请状态。",
"type": "string"
},
"status_description": {
"description": "StatusDescription 状态描述。",
"type": "string"
},
"updated_at": {
"description": "UpdatedAt 更新时间RFC3339。",
"type": "string"
},
"user": {
"description": "User 申请用户信息。",
"allOf": [
{
"$ref": "#/definitions/dto.TenantMemberUserLite"
}
]
}
}
},
"dto.TenantJoinReviewForm": {
"type": "object",
"properties": {
@@ -10749,6 +11173,86 @@ const docTemplate = `{
}
}
},
"dto.TenantMemberItem": {
"type": "object",
"properties": {
"created_at": {
"description": "CreatedAt 加入时间RFC3339。",
"type": "string"
},
"id": {
"description": "ID 成员关系记录ID。",
"type": "integer"
},
"role": {
"description": "Role 成员角色列表。",
"type": "array",
"items": {
"$ref": "#/definitions/consts.TenantUserRole"
}
},
"role_description": {
"description": "RoleDescription 角色描述列表。",
"type": "array",
"items": {
"type": "string"
}
},
"status": {
"description": "Status 成员状态。",
"allOf": [
{
"$ref": "#/definitions/consts.UserStatus"
}
]
},
"status_description": {
"description": "StatusDescription 成员状态描述。",
"type": "string"
},
"tenant_id": {
"description": "TenantID 租户ID。",
"type": "integer"
},
"updated_at": {
"description": "UpdatedAt 更新时间RFC3339。",
"type": "string"
},
"user": {
"description": "User 成员用户信息。",
"allOf": [
{
"$ref": "#/definitions/dto.TenantMemberUserLite"
}
]
}
}
},
"dto.TenantMemberUserLite": {
"type": "object",
"properties": {
"avatar": {
"description": "Avatar 头像URL。",
"type": "string"
},
"id": {
"description": "ID 用户ID。",
"type": "integer"
},
"nickname": {
"description": "Nickname 昵称。",
"type": "string"
},
"phone": {
"description": "Phone 手机号。",
"type": "string"
},
"username": {
"description": "Username 用户名。",
"type": "string"
}
}
},
"dto.TenantOwnerUserLite": {
"type": "object",
"properties": {

View File

@@ -4611,6 +4611,98 @@
}
}
},
"/t/{tenantCode}/v1/creator/members": {
"get": {
"description": "List tenant members with filters",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"CreatorCenter"
],
"summary": "List tenant members",
"parameters": [
{
"type": "string",
"description": "Keyword 关键词搜索(匹配用户名/昵称/手机号)。",
"name": "keyword",
"in": "query"
},
{
"type": "integer",
"description": "Limit is page size; only values in {10,20,50,100} are accepted (otherwise defaults to 10).",
"name": "limit",
"in": "query"
},
{
"type": "integer",
"description": "Page is 1-based page index; values \u003c= 0 are normalized to 1.",
"name": "page",
"in": "query"
},
{
"enum": [
"member",
"tenant_admin"
],
"type": "string",
"x-enum-varnames": [
"TenantUserRoleMember",
"TenantUserRoleTenantAdmin"
],
"description": "Role 成员角色筛选member/tenant_admin。",
"name": "role",
"in": "query"
},
{
"enum": [
"active",
"inactive",
"pending_verify",
"verified",
"banned"
],
"type": "string",
"x-enum-varnames": [
"UserStatusActive",
"UserStatusInactive",
"UserStatusPendingVerify",
"UserStatusVerified",
"UserStatusBanned"
],
"description": "Status 成员状态筛选active/verified/banned 等)。",
"name": "status",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/requests.Pager"
},
{
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/dto.TenantMemberItem"
}
}
}
}
]
}
}
}
}
},
"/t/{tenantCode}/v1/creator/members/invite": {
"post": {
"description": "Create an invite for tenant members",
@@ -4645,6 +4737,214 @@
}
}
},
"/t/{tenantCode}/v1/creator/members/invites": {
"get": {
"description": "List member invites with filters",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"CreatorCenter"
],
"summary": "List member invites",
"parameters": [
{
"type": "integer",
"description": "Limit is page size; only values in {10,20,50,100} are accepted (otherwise defaults to 10).",
"name": "limit",
"in": "query"
},
{
"type": "integer",
"description": "Page is 1-based page index; values \u003c= 0 are normalized to 1.",
"name": "page",
"in": "query"
},
{
"enum": [
"active",
"disabled",
"expired"
],
"type": "string",
"x-enum-varnames": [
"TenantInviteStatusActive",
"TenantInviteStatusDisabled",
"TenantInviteStatusExpired"
],
"description": "Status 邀请状态筛选active/disabled/expired。",
"name": "status",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/requests.Pager"
},
{
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/dto.TenantInviteListItem"
}
}
}
}
]
}
}
}
}
},
"/t/{tenantCode}/v1/creator/members/invites/{id}": {
"delete": {
"description": "Disable a member invite by ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"CreatorCenter"
],
"summary": "Disable member invite",
"parameters": [
{
"type": "integer",
"format": "int64",
"description": "Invite ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "Disabled",
"schema": {
"type": "string"
}
}
}
}
},
"/t/{tenantCode}/v1/creator/members/join-requests": {
"get": {
"description": "List tenant join requests",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"CreatorCenter"
],
"summary": "List member join requests",
"parameters": [
{
"type": "string",
"description": "Keyword 关键词搜索(匹配用户名/昵称/手机号)。",
"name": "keyword",
"in": "query"
},
{
"type": "integer",
"description": "Limit is page size; only values in {10,20,50,100} are accepted (otherwise defaults to 10).",
"name": "limit",
"in": "query"
},
{
"type": "integer",
"description": "Page is 1-based page index; values \u003c= 0 are normalized to 1.",
"name": "page",
"in": "query"
},
{
"enum": [
"pending",
"approved",
"rejected"
],
"type": "string",
"x-enum-varnames": [
"TenantJoinRequestStatusPending",
"TenantJoinRequestStatusApproved",
"TenantJoinRequestStatusRejected"
],
"description": "Status 申请状态筛选pending/approved/rejected。",
"name": "status",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/requests.Pager"
},
{
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/dto.TenantJoinRequestItem"
}
}
}
}
]
}
}
}
}
},
"/t/{tenantCode}/v1/creator/members/{id}": {
"delete": {
"description": "Remove a tenant member by relation ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"CreatorCenter"
],
"summary": "Remove tenant member",
"parameters": [
{
"type": "integer",
"format": "int64",
"description": "Member ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "Removed",
"schema": {
"type": "string"
}
}
}
}
},
"/t/{tenantCode}/v1/creator/members/{id}/review": {
"post": {
"description": "Approve or reject a tenant join request",
@@ -6660,6 +6960,32 @@
"RoleCreator"
]
},
"consts.TenantInviteStatus": {
"type": "string",
"enum": [
"active",
"disabled",
"expired"
],
"x-enum-varnames": [
"TenantInviteStatusActive",
"TenantInviteStatusDisabled",
"TenantInviteStatusExpired"
]
},
"consts.TenantJoinRequestStatus": {
"type": "string",
"enum": [
"pending",
"approved",
"rejected"
],
"x-enum-varnames": [
"TenantJoinRequestStatusPending",
"TenantJoinRequestStatusApproved",
"TenantJoinRequestStatusRejected"
]
},
"consts.TenantLedgerType": {
"type": "string",
"enum": [
@@ -10635,6 +10961,55 @@
}
}
},
"dto.TenantInviteListItem": {
"type": "object",
"properties": {
"code": {
"description": "Code 邀请码。",
"type": "string"
},
"created_at": {
"description": "CreatedAt 创建时间RFC3339。",
"type": "string"
},
"creator": {
"description": "Creator 创建者信息(可选)。",
"allOf": [
{
"$ref": "#/definitions/dto.TenantMemberUserLite"
}
]
},
"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"
},
"status_description": {
"description": "StatusDescription 状态描述。",
"type": "string"
},
"used_count": {
"description": "UsedCount 已使用次数。",
"type": "integer"
}
}
},
"dto.TenantItem": {
"type": "object",
"properties": {
@@ -10730,6 +11105,55 @@
}
}
},
"dto.TenantJoinRequestItem": {
"type": "object",
"properties": {
"created_at": {
"description": "CreatedAt 申请时间RFC3339。",
"type": "string"
},
"decided_at": {
"description": "DecidedAt 审核时间RFC3339。",
"type": "string"
},
"decided_operator_user_id": {
"description": "DecidedOperatorUserID 审核操作者ID。",
"type": "integer"
},
"decided_reason": {
"description": "DecidedReason 审核备注/原因。",
"type": "string"
},
"id": {
"description": "ID 申请记录ID。",
"type": "integer"
},
"reason": {
"description": "Reason 申请说明。",
"type": "string"
},
"status": {
"description": "Status 申请状态。",
"type": "string"
},
"status_description": {
"description": "StatusDescription 状态描述。",
"type": "string"
},
"updated_at": {
"description": "UpdatedAt 更新时间RFC3339。",
"type": "string"
},
"user": {
"description": "User 申请用户信息。",
"allOf": [
{
"$ref": "#/definitions/dto.TenantMemberUserLite"
}
]
}
}
},
"dto.TenantJoinReviewForm": {
"type": "object",
"properties": {
@@ -10743,6 +11167,86 @@
}
}
},
"dto.TenantMemberItem": {
"type": "object",
"properties": {
"created_at": {
"description": "CreatedAt 加入时间RFC3339。",
"type": "string"
},
"id": {
"description": "ID 成员关系记录ID。",
"type": "integer"
},
"role": {
"description": "Role 成员角色列表。",
"type": "array",
"items": {
"$ref": "#/definitions/consts.TenantUserRole"
}
},
"role_description": {
"description": "RoleDescription 角色描述列表。",
"type": "array",
"items": {
"type": "string"
}
},
"status": {
"description": "Status 成员状态。",
"allOf": [
{
"$ref": "#/definitions/consts.UserStatus"
}
]
},
"status_description": {
"description": "StatusDescription 成员状态描述。",
"type": "string"
},
"tenant_id": {
"description": "TenantID 租户ID。",
"type": "integer"
},
"updated_at": {
"description": "UpdatedAt 更新时间RFC3339。",
"type": "string"
},
"user": {
"description": "User 成员用户信息。",
"allOf": [
{
"$ref": "#/definitions/dto.TenantMemberUserLite"
}
]
}
}
},
"dto.TenantMemberUserLite": {
"type": "object",
"properties": {
"avatar": {
"description": "Avatar 头像URL。",
"type": "string"
},
"id": {
"description": "ID 用户ID。",
"type": "integer"
},
"nickname": {
"description": "Nickname 昵称。",
"type": "string"
},
"phone": {
"description": "Phone 手机号。",
"type": "string"
},
"username": {
"description": "Username 用户名。",
"type": "string"
}
}
},
"dto.TenantOwnerUserLite": {
"type": "object",
"properties": {

View File

@@ -138,6 +138,26 @@ definitions:
- RoleUser
- RoleSuperAdmin
- RoleCreator
consts.TenantInviteStatus:
enum:
- active
- disabled
- expired
type: string
x-enum-varnames:
- TenantInviteStatusActive
- TenantInviteStatusDisabled
- TenantInviteStatusExpired
consts.TenantJoinRequestStatus:
enum:
- pending
- approved
- rejected
type: string
x-enum-varnames:
- TenantJoinRequestStatusPending
- TenantJoinRequestStatusApproved
- TenantJoinRequestStatusRejected
consts.TenantLedgerType:
enum:
- debit_purchase
@@ -2929,6 +2949,40 @@ definitions:
description: UsedCount 已使用次数。
type: integer
type: object
dto.TenantInviteListItem:
properties:
code:
description: Code 邀请码。
type: string
created_at:
description: CreatedAt 创建时间RFC3339
type: string
creator:
allOf:
- $ref: '#/definitions/dto.TenantMemberUserLite'
description: Creator 创建者信息(可选)。
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
status_description:
description: StatusDescription 状态描述。
type: string
used_count:
description: UsedCount 已使用次数。
type: integer
type: object
dto.TenantItem:
properties:
admin_users:
@@ -2994,6 +3048,40 @@ definitions:
description: Reason 申请加入原因(可选,空值会使用默认文案)。
type: string
type: object
dto.TenantJoinRequestItem:
properties:
created_at:
description: CreatedAt 申请时间RFC3339
type: string
decided_at:
description: DecidedAt 审核时间RFC3339
type: string
decided_operator_user_id:
description: DecidedOperatorUserID 审核操作者ID。
type: integer
decided_reason:
description: DecidedReason 审核备注/原因。
type: string
id:
description: ID 申请记录ID。
type: integer
reason:
description: Reason 申请说明。
type: string
status:
description: Status 申请状态。
type: string
status_description:
description: StatusDescription 状态描述。
type: string
updated_at:
description: UpdatedAt 更新时间RFC3339
type: string
user:
allOf:
- $ref: '#/definitions/dto.TenantMemberUserLite'
description: User 申请用户信息。
type: object
dto.TenantJoinReviewForm:
properties:
action:
@@ -3003,6 +3091,60 @@ definitions:
description: Reason 审核说明(可选,用于展示驳回原因或备注)。
type: string
type: object
dto.TenantMemberItem:
properties:
created_at:
description: CreatedAt 加入时间RFC3339
type: string
id:
description: ID 成员关系记录ID。
type: integer
role:
description: Role 成员角色列表。
items:
$ref: '#/definitions/consts.TenantUserRole'
type: array
role_description:
description: RoleDescription 角色描述列表。
items:
type: string
type: array
status:
allOf:
- $ref: '#/definitions/consts.UserStatus'
description: Status 成员状态。
status_description:
description: StatusDescription 成员状态描述。
type: string
tenant_id:
description: TenantID 租户ID。
type: integer
updated_at:
description: UpdatedAt 更新时间RFC3339
type: string
user:
allOf:
- $ref: '#/definitions/dto.TenantMemberUserLite'
description: User 成员用户信息。
type: object
dto.TenantMemberUserLite:
properties:
avatar:
description: Avatar 头像URL。
type: string
id:
description: ID 用户ID。
type: integer
nickname:
description: Nickname 昵称。
type: string
phone:
description: Phone 手机号。
type: string
username:
description: Username 用户名。
type: string
type: object
dto.TenantOwnerUserLite:
properties:
id:
@@ -6503,6 +6645,90 @@ paths:
summary: Dashboard stats
tags:
- CreatorCenter
/t/{tenantCode}/v1/creator/members:
get:
consumes:
- application/json
description: List tenant members with filters
parameters:
- description: Keyword 关键词搜索(匹配用户名/昵称/手机号)。
in: query
name: keyword
type: string
- description: Limit is page size; only values in {10,20,50,100} are accepted
(otherwise defaults to 10).
in: query
name: limit
type: integer
- description: Page is 1-based page index; values <= 0 are normalized to 1.
in: query
name: page
type: integer
- description: Role 成员角色筛选member/tenant_admin
enum:
- member
- tenant_admin
in: query
name: role
type: string
x-enum-varnames:
- TenantUserRoleMember
- TenantUserRoleTenantAdmin
- description: Status 成员状态筛选active/verified/banned 等)。
enum:
- active
- inactive
- pending_verify
- verified
- banned
in: query
name: status
type: string
x-enum-varnames:
- UserStatusActive
- UserStatusInactive
- UserStatusPendingVerify
- UserStatusVerified
- UserStatusBanned
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/requests.Pager'
- properties:
items:
items:
$ref: '#/definitions/dto.TenantMemberItem'
type: array
type: object
summary: List tenant members
tags:
- CreatorCenter
/t/{tenantCode}/v1/creator/members/{id}:
delete:
consumes:
- application/json
description: Remove a tenant member by relation ID
parameters:
- description: Member ID
format: int64
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: Removed
schema:
type: string
summary: Remove tenant member
tags:
- CreatorCenter
/t/{tenantCode}/v1/creator/members/{id}/review:
post:
consumes:
@@ -6553,6 +6779,120 @@ paths:
summary: Create member invite
tags:
- CreatorCenter
/t/{tenantCode}/v1/creator/members/invites:
get:
consumes:
- application/json
description: List member invites with filters
parameters:
- description: Limit is page size; only values in {10,20,50,100} are accepted
(otherwise defaults to 10).
in: query
name: limit
type: integer
- description: Page is 1-based page index; values <= 0 are normalized to 1.
in: query
name: page
type: integer
- description: Status 邀请状态筛选active/disabled/expired
enum:
- active
- disabled
- expired
in: query
name: status
type: string
x-enum-varnames:
- TenantInviteStatusActive
- TenantInviteStatusDisabled
- TenantInviteStatusExpired
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/requests.Pager'
- properties:
items:
items:
$ref: '#/definitions/dto.TenantInviteListItem'
type: array
type: object
summary: List member invites
tags:
- CreatorCenter
/t/{tenantCode}/v1/creator/members/invites/{id}:
delete:
consumes:
- application/json
description: Disable a member invite by ID
parameters:
- description: Invite ID
format: int64
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: Disabled
schema:
type: string
summary: Disable member invite
tags:
- CreatorCenter
/t/{tenantCode}/v1/creator/members/join-requests:
get:
consumes:
- application/json
description: List tenant join requests
parameters:
- description: Keyword 关键词搜索(匹配用户名/昵称/手机号)。
in: query
name: keyword
type: string
- description: Limit is page size; only values in {10,20,50,100} are accepted
(otherwise defaults to 10).
in: query
name: limit
type: integer
- description: Page is 1-based page index; values <= 0 are normalized to 1.
in: query
name: page
type: integer
- description: Status 申请状态筛选pending/approved/rejected
enum:
- pending
- approved
- rejected
in: query
name: status
type: string
x-enum-varnames:
- TenantJoinRequestStatusPending
- TenantJoinRequestStatusApproved
- TenantJoinRequestStatusRejected
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/requests.Pager'
- properties:
items:
items:
$ref: '#/definitions/dto.TenantJoinRequestItem'
type: array
type: object
summary: List member join requests
tags:
- CreatorCenter
/t/{tenantCode}/v1/creator/orders:
get:
consumes: