feat: add creator member management
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user