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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -159,11 +159,28 @@
|
||||
**测试方案**
|
||||
- 审计记录落库含 `operator_id`,并覆盖缺参错误。
|
||||
|
||||
### 10) 创作者中心 - 团队成员管理(Portal UI)(已完成)
|
||||
**需求目标**
|
||||
- 补齐创作者侧成员列表/邀请/审核等管理能力,覆盖成员生命周期。
|
||||
- 角色/状态可视化,支持管理员移除或禁用成员。
|
||||
|
||||
**技术方案(前端/后端)**
|
||||
- 前端:`frontend/portal/src/views/creator/` 新增成员管理页面(列表/邀请/审核)。
|
||||
- API(如缺失需补齐):
|
||||
- 成员列表:`GET /t/:tenantCode/v1/creator/members`
|
||||
- 邀请列表/撤销:`GET /t/:tenantCode/v1/creator/members/invites`、`DELETE /t/:tenantCode/v1/creator/members/invites/:id<int>`
|
||||
- 申请列表:`GET /t/:tenantCode/v1/creator/members/join-requests`
|
||||
- 已有:`POST /t/:tenantCode/v1/creator/members/invite`、`POST /t/:tenantCode/v1/creator/members/:id<int>/review`
|
||||
- Service:复用 `services.Tenant` 现有邀请/审核逻辑,补齐列表查询与权限校验。
|
||||
|
||||
**测试方案**
|
||||
- 列表分页/筛选;邀请创建/撤销;审核通过后成员列表可见;权限拦截。
|
||||
|
||||
---
|
||||
|
||||
## P2(中优先)
|
||||
|
||||
### 10) 运营统计报表(曝光/转化/订单/退款)(已完成)
|
||||
### 11) 运营统计报表(曝光/转化/订单/退款)(已完成)
|
||||
**需求目标**
|
||||
- 提供租户维度与时间范围的核心指标统计与导出。
|
||||
|
||||
@@ -179,7 +196,7 @@
|
||||
**测试方案**
|
||||
- 统计口径一致性;筛选组合;导出任务可用性。
|
||||
|
||||
### 11) 超管后台治理能力(健康度/异常监控/内容审核)(已完成)
|
||||
### 12) 超管后台治理能力(健康度/异常监控/内容审核)(已完成)
|
||||
**需求目标**
|
||||
- 提供超管对租户的健康指标、异常趋势、内容合规审核。
|
||||
|
||||
@@ -194,7 +211,7 @@
|
||||
**测试方案**
|
||||
- 审核状态流转有效性;异常阈值命中结果。
|
||||
|
||||
### 12) 性能优化(避免 N+1)(已完成)
|
||||
### 13) 性能优化(避免 N+1)(已完成)
|
||||
**需求目标**
|
||||
- 列表/统计场景避免逐条查询。
|
||||
|
||||
@@ -205,11 +222,34 @@
|
||||
**测试方案**
|
||||
- 对比查询次数/耗时(可选) + 数据正确性。
|
||||
|
||||
### 14) 租户公开页完善(Portal)
|
||||
**需求目标**
|
||||
- 完善租户主页信息与内容聚合体验。
|
||||
|
||||
**技术方案(前端/后端)**
|
||||
- 前端:`frontend/portal/src/views/tenant/` 增加简介/关于模块。
|
||||
- 若内容聚合维度不足,补齐筛选参数(如专辑/最新/最热)与接口字段。
|
||||
|
||||
**测试方案**
|
||||
- 不同筛选维度下内容列表正确;空内容/无简介时展示兜底。
|
||||
|
||||
### 15) 微信生态清理(若仍存在)
|
||||
**需求目标**
|
||||
- 移除微信登录/分享/支付相关逻辑,降低维护成本。
|
||||
|
||||
**技术方案(后端/前端/文档)**
|
||||
- 后端:清理 `auth` 路由与服务中的微信授权/回调逻辑(如存在)。
|
||||
- 前端:移除 Portal 登录页/分享中的 `wx.*` 依赖与 UI。
|
||||
- 配置/文档:清理 `config.toml`、`specs/*`、`docs/*` 的微信配置与说明;移除无用 SDK 依赖。
|
||||
|
||||
**测试方案**
|
||||
- 登录、分享、支付路径不再引用微信 SDK;构建与 lint 通过。
|
||||
|
||||
---
|
||||
|
||||
## P3(延后)
|
||||
|
||||
### 13) 真实存储 Provider 接入(生产)
|
||||
### 16) 真实存储 Provider 接入(生产)
|
||||
**需求目标**
|
||||
- 接入 OSS/云存储(生产环境),统一上传/访问路径策略。
|
||||
|
||||
@@ -219,7 +259,19 @@
|
||||
**测试方案**
|
||||
- 本地 FS + MinIO + 真实 Provider 三套配置可用性。
|
||||
|
||||
### 14) 支付集成
|
||||
### 17) 媒体处理管线适配对象存储(S3/MinIO)
|
||||
**需求目标**
|
||||
- 在对象存储模式下,媒体处理任务可完整执行并回传产物。
|
||||
|
||||
**技术方案(后端)**
|
||||
- Worker:从对象存储下载源文件到临时目录 → FFmpeg 处理 → 结果上传回对象存储 → 清理临时文件。
|
||||
- 产物:封面/预览片段自动生成并回写 `media_assets`。
|
||||
- 本地 FS 仍保留兼容路径(开发/测试使用)。
|
||||
|
||||
**测试方案**
|
||||
- 对象存储模式下上传视频触发处理,封面/预览可访问;任务异常可重试。
|
||||
|
||||
### 18) 支付集成
|
||||
**需求目标**
|
||||
- 最终阶段对接真实支付。
|
||||
|
||||
|
||||
@@ -31,6 +31,25 @@ export const creatorApi = {
|
||||
request(`/creator/coupons/${id}`, { method: "PUT", body: data }),
|
||||
grantCoupon: (id, data) =>
|
||||
request(`/creator/coupons/${id}/grant`, { method: "POST", body: data }),
|
||||
listMembers: (params) => {
|
||||
const qs = new URLSearchParams(params).toString();
|
||||
return request(`/creator/members?${qs}`);
|
||||
},
|
||||
removeMember: (id) => request(`/creator/members/${id}`, { method: "DELETE" }),
|
||||
listMemberInvites: (params) => {
|
||||
const qs = new URLSearchParams(params).toString();
|
||||
return request(`/creator/members/invites?${qs}`);
|
||||
},
|
||||
createMemberInvite: (data) =>
|
||||
request("/creator/members/invite", { method: "POST", body: data }),
|
||||
disableMemberInvite: (id) =>
|
||||
request(`/creator/members/invites/${id}`, { method: "DELETE" }),
|
||||
listMemberJoinRequests: (params) => {
|
||||
const qs = new URLSearchParams(params).toString();
|
||||
return request(`/creator/members/join-requests?${qs}`);
|
||||
},
|
||||
reviewMemberJoinRequest: (id, data) =>
|
||||
request(`/creator/members/${id}/review`, { method: "POST", body: data }),
|
||||
getSettings: () => request("/creator/settings"),
|
||||
updateSettings: (data) =>
|
||||
request("/creator/settings", { method: "PUT", body: data }),
|
||||
|
||||
@@ -108,6 +108,16 @@ const isFullWidth = computed(() => {
|
||||
配置
|
||||
</div>
|
||||
|
||||
<router-link
|
||||
:to="tenantRoute('/creator/members')"
|
||||
active-class="bg-primary-600 text-white shadow-md shadow-primary-900/20"
|
||||
class="flex items-center gap-3 px-4 py-3 rounded-lg hover:bg-slate-800 hover:text-white transition-all group"
|
||||
>
|
||||
<i
|
||||
class="pi pi-users text-lg group-hover:scale-110 transition-transform"
|
||||
></i>
|
||||
<span class="font-medium">成员管理</span>
|
||||
</router-link>
|
||||
<router-link
|
||||
:to="tenantRoute('/creator/settings')"
|
||||
active-class="bg-primary-600 text-white shadow-md shadow-primary-900/20"
|
||||
|
||||
@@ -149,6 +149,11 @@ const router = createRouter({
|
||||
name: "creator-orders",
|
||||
component: () => import("../views/creator/OrdersView.vue"),
|
||||
},
|
||||
{
|
||||
path: "members",
|
||||
name: "creator-members",
|
||||
component: () => import("../views/creator/MembersView.vue"),
|
||||
},
|
||||
{
|
||||
path: "coupons",
|
||||
name: "creator-coupons",
|
||||
|
||||
706
frontend/portal/src/views/creator/MembersView.vue
Normal file
706
frontend/portal/src/views/creator/MembersView.vue
Normal file
@@ -0,0 +1,706 @@
|
||||
<script setup>
|
||||
import Dialog from "primevue/dialog";
|
||||
import Toast from "primevue/toast";
|
||||
import { useToast } from "primevue/usetoast";
|
||||
import { computed, onMounted, reactive, ref } from "vue";
|
||||
import { creatorApi } from "../../api/creator";
|
||||
|
||||
const toast = useToast();
|
||||
const activeTab = ref("members");
|
||||
const loading = reactive({
|
||||
members: false,
|
||||
invites: false,
|
||||
requests: false,
|
||||
});
|
||||
|
||||
const memberKeyword = ref("");
|
||||
const memberRole = ref("");
|
||||
const memberStatus = ref("");
|
||||
const members = ref([]);
|
||||
const memberPager = ref({ page: 1, limit: 10, total: 0 });
|
||||
|
||||
const inviteStatus = ref("");
|
||||
const invites = ref([]);
|
||||
const invitePager = ref({ page: 1, limit: 10, total: 0 });
|
||||
const inviteDialog = ref(false);
|
||||
const inviteForm = reactive({
|
||||
max_uses: 1,
|
||||
expires_at: "",
|
||||
remark: "",
|
||||
});
|
||||
|
||||
const requestKeyword = ref("");
|
||||
const requestStatus = ref("pending");
|
||||
const requests = ref([]);
|
||||
const requestPager = ref({ page: 1, limit: 10, total: 0 });
|
||||
const reviewDialog = ref(false);
|
||||
const reviewAction = ref("approve");
|
||||
const reviewReason = ref("");
|
||||
const reviewTarget = ref(null);
|
||||
|
||||
const tabs = [
|
||||
{ key: "members", label: "成员列表" },
|
||||
{ key: "requests", label: "加入申请" },
|
||||
{ key: "invites", label: "邀请记录" },
|
||||
];
|
||||
|
||||
const fetchMembers = async () => {
|
||||
loading.members = true;
|
||||
try {
|
||||
const res = await creatorApi.listMembers({
|
||||
page: memberPager.value.page,
|
||||
limit: memberPager.value.limit,
|
||||
keyword: memberKeyword.value,
|
||||
role: memberRole.value,
|
||||
status: memberStatus.value,
|
||||
});
|
||||
members.value = res.items || [];
|
||||
memberPager.value.total = res.total || 0;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
loading.members = false;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchInvites = async () => {
|
||||
loading.invites = true;
|
||||
try {
|
||||
const res = await creatorApi.listMemberInvites({
|
||||
page: invitePager.value.page,
|
||||
limit: invitePager.value.limit,
|
||||
status: inviteStatus.value,
|
||||
});
|
||||
invites.value = res.items || [];
|
||||
invitePager.value.total = res.total || 0;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
loading.invites = false;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchJoinRequests = async () => {
|
||||
loading.requests = true;
|
||||
try {
|
||||
const res = await creatorApi.listMemberJoinRequests({
|
||||
page: requestPager.value.page,
|
||||
limit: requestPager.value.limit,
|
||||
status: requestStatus.value,
|
||||
keyword: requestKeyword.value,
|
||||
});
|
||||
requests.value = res.items || [];
|
||||
requestPager.value.total = res.total || 0;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
loading.requests = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchMembers();
|
||||
fetchInvites();
|
||||
fetchJoinRequests();
|
||||
});
|
||||
|
||||
const memberStatusStyle = (status) => {
|
||||
switch (status) {
|
||||
case "verified":
|
||||
return { bg: "bg-emerald-50", text: "text-emerald-600" };
|
||||
case "banned":
|
||||
return { bg: "bg-rose-50", text: "text-rose-600" };
|
||||
default:
|
||||
return { bg: "bg-slate-100", text: "text-slate-500" };
|
||||
}
|
||||
};
|
||||
|
||||
const inviteStatusStyle = (status) => {
|
||||
switch (status) {
|
||||
case "active":
|
||||
return { bg: "bg-emerald-50", text: "text-emerald-600" };
|
||||
case "disabled":
|
||||
return { bg: "bg-slate-100", text: "text-slate-500" };
|
||||
case "expired":
|
||||
return { bg: "bg-amber-50", text: "text-amber-700" };
|
||||
default:
|
||||
return { bg: "bg-slate-100", text: "text-slate-500" };
|
||||
}
|
||||
};
|
||||
|
||||
const requestStatusStyle = (status) => {
|
||||
switch (status) {
|
||||
case "pending":
|
||||
return { bg: "bg-amber-50", text: "text-amber-700" };
|
||||
case "approved":
|
||||
return { bg: "bg-emerald-50", text: "text-emerald-600" };
|
||||
case "rejected":
|
||||
return { bg: "bg-rose-50", text: "text-rose-600" };
|
||||
default:
|
||||
return { bg: "bg-slate-100", text: "text-slate-500" };
|
||||
}
|
||||
};
|
||||
|
||||
const openInviteDialog = () => {
|
||||
inviteForm.max_uses = 1;
|
||||
inviteForm.expires_at = "";
|
||||
inviteForm.remark = "";
|
||||
inviteDialog.value = true;
|
||||
};
|
||||
|
||||
const createInvite = async () => {
|
||||
try {
|
||||
const payload = {
|
||||
max_uses: Number(inviteForm.max_uses) || 1,
|
||||
remark: inviteForm.remark,
|
||||
};
|
||||
if (inviteForm.expires_at) {
|
||||
payload.expires_at = new Date(inviteForm.expires_at).toISOString();
|
||||
}
|
||||
await creatorApi.createMemberInvite(payload);
|
||||
inviteDialog.value = false;
|
||||
toast.add({ severity: "success", summary: "已创建邀请", life: 2000 });
|
||||
fetchInvites();
|
||||
} catch (e) {
|
||||
toast.add({
|
||||
severity: "error",
|
||||
summary: "创建失败",
|
||||
detail: e.message,
|
||||
life: 3000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const disableInvite = async (invite) => {
|
||||
if (!invite || !invite.id) return;
|
||||
if (!window.confirm("确认撤销该邀请?")) return;
|
||||
try {
|
||||
await creatorApi.disableMemberInvite(invite.id);
|
||||
toast.add({ severity: "success", summary: "已撤销", life: 2000 });
|
||||
fetchInvites();
|
||||
} catch (e) {
|
||||
toast.add({
|
||||
severity: "error",
|
||||
summary: "撤销失败",
|
||||
detail: e.message,
|
||||
life: 3000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const removeMember = async (member) => {
|
||||
if (!member || !member.id) return;
|
||||
if (!window.confirm("确认移除该成员?")) return;
|
||||
try {
|
||||
await creatorApi.removeMember(member.id);
|
||||
toast.add({ severity: "success", summary: "成员已移除", life: 2000 });
|
||||
fetchMembers();
|
||||
} catch (e) {
|
||||
toast.add({
|
||||
severity: "error",
|
||||
summary: "移除失败",
|
||||
detail: e.message,
|
||||
life: 3000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const openReviewDialog = (item, action) => {
|
||||
reviewTarget.value = item;
|
||||
reviewAction.value = action;
|
||||
reviewReason.value = "";
|
||||
reviewDialog.value = true;
|
||||
};
|
||||
|
||||
const submitReview = async () => {
|
||||
if (!reviewTarget.value) return;
|
||||
try {
|
||||
await creatorApi.reviewMemberJoinRequest(reviewTarget.value.id, {
|
||||
action: reviewAction.value,
|
||||
reason: reviewReason.value,
|
||||
});
|
||||
reviewDialog.value = false;
|
||||
toast.add({ severity: "success", summary: "已处理申请", life: 2000 });
|
||||
fetchJoinRequests();
|
||||
fetchMembers();
|
||||
} catch (e) {
|
||||
toast.add({
|
||||
severity: "error",
|
||||
summary: "处理失败",
|
||||
detail: e.message,
|
||||
life: 3000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const tabClass = (key) =>
|
||||
key === activeTab.value
|
||||
? "bg-slate-900 text-white"
|
||||
: "bg-white text-slate-600 border border-slate-200 hover:bg-slate-50";
|
||||
|
||||
const pendingRequests = computed(() =>
|
||||
requests.value.filter((item) => item.status === "pending"),
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Toast />
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h1 class="text-2xl font-bold text-slate-900">成员管理</h1>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="px-4 py-2 rounded-lg text-sm font-semibold transition-colors"
|
||||
:class="tabClass(tab.key)"
|
||||
@click="activeTab = tab.key"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="activeTab === 'members'" class="space-y-6">
|
||||
<div
|
||||
class="bg-white rounded-xl shadow-sm border border-slate-100 p-4 flex flex-wrap gap-4 items-center"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm font-bold text-slate-500">角色:</span>
|
||||
<select
|
||||
v-model="memberRole"
|
||||
class="h-9 px-3 rounded border border-slate-200 text-sm focus:border-primary-500 outline-none bg-white"
|
||||
>
|
||||
<option value="">全部</option>
|
||||
<option value="member">成员</option>
|
||||
<option value="tenant_admin">管理员</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm font-bold text-slate-500">状态:</span>
|
||||
<select
|
||||
v-model="memberStatus"
|
||||
class="h-9 px-3 rounded border border-slate-200 text-sm focus:border-primary-500 outline-none bg-white"
|
||||
>
|
||||
<option value="">全部</option>
|
||||
<option value="verified">已审核</option>
|
||||
<option value="banned">已封禁</option>
|
||||
<option value="active">正常</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="ml-auto relative w-72">
|
||||
<i
|
||||
class="pi pi-search absolute left-3 top-1/2 -translate-y-1/2 text-slate-400"
|
||||
></i>
|
||||
<input
|
||||
type="text"
|
||||
v-model="memberKeyword"
|
||||
@keyup.enter="fetchMembers"
|
||||
placeholder="搜索昵称/手机号..."
|
||||
class="w-full h-9 pl-9 pr-4 rounded border border-slate-200 text-sm focus:border-primary-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
class="px-4 py-2 rounded-lg bg-slate-900 text-white text-sm font-semibold"
|
||||
@click="fetchMembers"
|
||||
>
|
||||
筛选
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden"
|
||||
>
|
||||
<table class="w-full text-left text-sm">
|
||||
<thead
|
||||
class="bg-slate-50 text-slate-500 font-bold border-b border-slate-200"
|
||||
>
|
||||
<tr>
|
||||
<th class="px-6 py-4">成员</th>
|
||||
<th class="px-6 py-4">角色</th>
|
||||
<th class="px-6 py-4">状态</th>
|
||||
<th class="px-6 py-4">加入时间</th>
|
||||
<th class="px-6 py-4 text-right">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
<tr v-if="loading.members">
|
||||
<td colspan="5" class="px-6 py-6 text-center text-slate-500">
|
||||
加载中...
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-else-if="members.length === 0">
|
||||
<td colspan="5" class="px-6 py-8 text-center text-slate-400">
|
||||
暂无成员数据
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
v-for="member in members"
|
||||
:key="member.id"
|
||||
class="hover:bg-slate-50"
|
||||
>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<img
|
||||
:src="
|
||||
member.user?.avatar ||
|
||||
`https://api.dicebear.com/7.x/avataaars/svg?seed=${member.user?.id}`
|
||||
"
|
||||
class="w-10 h-10 rounded-full object-cover"
|
||||
/>
|
||||
<div>
|
||||
<div class="font-semibold text-slate-900">
|
||||
{{
|
||||
member.user?.nickname || member.user?.username || "-"
|
||||
}}
|
||||
</div>
|
||||
<div class="text-xs text-slate-400">
|
||||
{{ member.user?.phone || "--" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-slate-700">
|
||||
{{ member.role_description?.join(" / ") || "成员" }}
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span
|
||||
class="px-2 py-1 rounded-full text-xs font-semibold"
|
||||
:class="`${memberStatusStyle(member.status).bg} ${memberStatusStyle(member.status).text}`"
|
||||
>
|
||||
{{ member.status_description || member.status }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-slate-500">
|
||||
{{ member.created_at || "--" }}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<button
|
||||
class="text-rose-600 text-sm font-semibold hover:text-rose-700"
|
||||
@click="removeMember(member)"
|
||||
>
|
||||
移除
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="activeTab === 'requests'" class="space-y-6">
|
||||
<div
|
||||
class="bg-white rounded-xl shadow-sm border border-slate-100 p-4 flex flex-wrap gap-4 items-center"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm font-bold text-slate-500">状态:</span>
|
||||
<select
|
||||
v-model="requestStatus"
|
||||
class="h-9 px-3 rounded border border-slate-200 text-sm focus:border-primary-500 outline-none bg-white"
|
||||
>
|
||||
<option value="pending">待审核</option>
|
||||
<option value="approved">已通过</option>
|
||||
<option value="rejected">已拒绝</option>
|
||||
<option value="">全部</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="ml-auto relative w-72">
|
||||
<i
|
||||
class="pi pi-search absolute left-3 top-1/2 -translate-y-1/2 text-slate-400"
|
||||
></i>
|
||||
<input
|
||||
type="text"
|
||||
v-model="requestKeyword"
|
||||
@keyup.enter="fetchJoinRequests"
|
||||
placeholder="搜索昵称/手机号..."
|
||||
class="w-full h-9 pl-9 pr-4 rounded border border-slate-200 text-sm focus:border-primary-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
class="px-4 py-2 rounded-lg bg-slate-900 text-white text-sm font-semibold"
|
||||
@click="fetchJoinRequests"
|
||||
>
|
||||
筛选
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden"
|
||||
>
|
||||
<table class="w-full text-left text-sm">
|
||||
<thead
|
||||
class="bg-slate-50 text-slate-500 font-bold border-b border-slate-200"
|
||||
>
|
||||
<tr>
|
||||
<th class="px-6 py-4">申请人</th>
|
||||
<th class="px-6 py-4">申请理由</th>
|
||||
<th class="px-6 py-4">状态</th>
|
||||
<th class="px-6 py-4">申请时间</th>
|
||||
<th class="px-6 py-4 text-right">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
<tr v-if="loading.requests">
|
||||
<td colspan="5" class="px-6 py-6 text-center text-slate-500">
|
||||
加载中...
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-else-if="requests.length === 0">
|
||||
<td colspan="5" class="px-6 py-8 text-center text-slate-400">
|
||||
暂无申请记录
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-for="req in requests" :key="req.id" class="hover:bg-slate-50">
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<img
|
||||
:src="
|
||||
req.user?.avatar ||
|
||||
`https://api.dicebear.com/7.x/avataaars/svg?seed=${req.user?.id}`
|
||||
"
|
||||
class="w-10 h-10 rounded-full object-cover"
|
||||
/>
|
||||
<div>
|
||||
<div class="font-semibold text-slate-900">
|
||||
{{ req.user?.nickname || req.user?.username || "-" }}
|
||||
</div>
|
||||
<div class="text-xs text-slate-400">
|
||||
{{ req.user?.phone || "--" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-slate-600">
|
||||
{{ req.reason || "-" }}
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span
|
||||
class="px-2 py-1 rounded-full text-xs font-semibold"
|
||||
:class="`${requestStatusStyle(req.status).bg} ${requestStatusStyle(req.status).text}`"
|
||||
>
|
||||
{{ req.status_description || req.status }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-slate-500">
|
||||
{{ req.created_at || "--" }}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<div
|
||||
v-if="req.status === 'pending'"
|
||||
class="flex justify-end gap-2"
|
||||
>
|
||||
<button
|
||||
class="px-3 py-1 text-sm rounded-lg bg-emerald-500 text-white"
|
||||
@click="openReviewDialog(req, 'approve')"
|
||||
>
|
||||
通过
|
||||
</button>
|
||||
<button
|
||||
class="px-3 py-1 text-sm rounded-lg bg-rose-500 text-white"
|
||||
@click="openReviewDialog(req, 'reject')"
|
||||
>
|
||||
拒绝
|
||||
</button>
|
||||
</div>
|
||||
<span v-else class="text-slate-400 text-sm">已处理</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div v-if="pendingRequests.length === 0" class="text-sm text-slate-500">
|
||||
当前暂无待审核申请。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div
|
||||
class="bg-white rounded-xl shadow-sm border border-slate-100 p-4 flex gap-4 items-center"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm font-bold text-slate-500">状态:</span>
|
||||
<select
|
||||
v-model="inviteStatus"
|
||||
class="h-9 px-3 rounded border border-slate-200 text-sm focus:border-primary-500 outline-none bg-white"
|
||||
>
|
||||
<option value="">全部</option>
|
||||
<option value="active">可用</option>
|
||||
<option value="disabled">已撤销</option>
|
||||
<option value="expired">已过期</option>
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
class="px-4 py-2 rounded-lg bg-slate-900 text-white text-sm font-semibold"
|
||||
@click="fetchInvites"
|
||||
>
|
||||
筛选
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
class="px-4 py-2 rounded-lg bg-primary-600 text-white text-sm font-semibold shadow-sm"
|
||||
@click="openInviteDialog"
|
||||
>
|
||||
<i class="pi pi-plus mr-1"></i> 新建邀请
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden"
|
||||
>
|
||||
<table class="w-full text-left text-sm">
|
||||
<thead
|
||||
class="bg-slate-50 text-slate-500 font-bold border-b border-slate-200"
|
||||
>
|
||||
<tr>
|
||||
<th class="px-6 py-4">邀请码</th>
|
||||
<th class="px-6 py-4">状态</th>
|
||||
<th class="px-6 py-4">使用情况</th>
|
||||
<th class="px-6 py-4">过期时间</th>
|
||||
<th class="px-6 py-4">备注</th>
|
||||
<th class="px-6 py-4 text-right">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
<tr v-if="loading.invites">
|
||||
<td colspan="6" class="px-6 py-6 text-center text-slate-500">
|
||||
加载中...
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-else-if="invites.length === 0">
|
||||
<td colspan="6" class="px-6 py-8 text-center text-slate-400">
|
||||
暂无邀请记录
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
v-for="invite in invites"
|
||||
:key="invite.id"
|
||||
class="hover:bg-slate-50"
|
||||
>
|
||||
<td class="px-6 py-4 font-mono text-slate-700">
|
||||
{{ invite.code }}
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span
|
||||
class="px-2 py-1 rounded-full text-xs font-semibold"
|
||||
:class="`${inviteStatusStyle(invite.status).bg} ${inviteStatusStyle(invite.status).text}`"
|
||||
>
|
||||
{{ invite.status_description || invite.status }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-slate-600">
|
||||
{{ invite.used_count }} / {{ invite.max_uses }}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-slate-500">
|
||||
{{ invite.expires_at || "长期有效" }}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-slate-500">
|
||||
{{ invite.remark || "-" }}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<button
|
||||
v-if="invite.status === 'active'"
|
||||
class="text-rose-600 text-sm font-semibold hover:text-rose-700"
|
||||
@click="disableInvite(invite)"
|
||||
>
|
||||
撤销
|
||||
</button>
|
||||
<span v-else class="text-slate-400 text-sm">--</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog
|
||||
v-model:visible="inviteDialog"
|
||||
modal
|
||||
header="新建邀请"
|
||||
class="w-[420px]"
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm text-slate-600 mb-2">最大使用次数</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
v-model="inviteForm.max_uses"
|
||||
class="w-full h-9 px-3 rounded border border-slate-200 text-sm focus:border-primary-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-slate-600 mb-2">过期时间</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
v-model="inviteForm.expires_at"
|
||||
class="w-full h-9 px-3 rounded border border-slate-200 text-sm focus:border-primary-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-slate-600 mb-2">备注</label>
|
||||
<input
|
||||
type="text"
|
||||
v-model="inviteForm.remark"
|
||||
placeholder="可选"
|
||||
class="w-full h-9 px-3 rounded border border-slate-200 text-sm focus:border-primary-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<button
|
||||
class="px-4 py-2 rounded-lg border border-slate-200 text-sm text-slate-600 mr-2"
|
||||
@click="inviteDialog = false"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
class="px-4 py-2 rounded-lg bg-slate-900 text-white text-sm font-semibold"
|
||||
@click="createInvite"
|
||||
>
|
||||
创建
|
||||
</button>
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<Dialog
|
||||
v-model:visible="reviewDialog"
|
||||
modal
|
||||
header="审核申请"
|
||||
class="w-[420px]"
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<div class="text-sm text-slate-600">
|
||||
当前操作:
|
||||
<span class="font-semibold text-slate-900">{{
|
||||
reviewAction === "approve" ? "通过" : "拒绝"
|
||||
}}</span>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-slate-600 mb-2">备注说明</label>
|
||||
<textarea
|
||||
v-model="reviewReason"
|
||||
rows="3"
|
||||
placeholder="可选"
|
||||
class="w-full px-3 py-2 rounded border border-slate-200 text-sm focus:border-primary-500 outline-none"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<button
|
||||
class="px-4 py-2 rounded-lg border border-slate-200 text-sm text-slate-600 mr-2"
|
||||
@click="reviewDialog = false"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
class="px-4 py-2 rounded-lg bg-slate-900 text-white text-sm font-semibold"
|
||||
@click="submitReview"
|
||||
>
|
||||
确认
|
||||
</button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,76 +0,0 @@
|
||||
# QuyUn v2 Implementation Plan
|
||||
|
||||
根据当前项目状态 review,以下是针对后续开发的实施计划,涵盖清理废弃功能与补全缺失业务模块。
|
||||
|
||||
## 1. 功能清理 (Cleanup: WeChat Removal)
|
||||
|
||||
微信生态相关功能确认不再需要,需从代码库和文档中彻底移除,以降低维护成本和避免混淆。
|
||||
|
||||
### 1.1 API 接口 (Backend)
|
||||
- [ ] 移除 `backend/app/http/v1/auth` 下微信授权登录相关路由 (`/auth/wechat`, `/auth/wechat/callback`)。
|
||||
- [ ] 移除 WeChat JS-SDK 签名接口及相关 Service 逻辑。
|
||||
- [ ] 清理 `go.mod` 中可能存在的无用微信 SDK 依赖。
|
||||
|
||||
### 1.2 前端代码 (Frontend)
|
||||
- [ ] **Portal**: 修改 `frontend/portal/src/views/auth/LoginView.vue`,移除微信登录图标及相关 UI 占位。
|
||||
- [ ] **Portal**: 全局搜索并清理残留的 `wx.config`、`wx.ready` 及微信分享相关代码。
|
||||
|
||||
### 1.3 文档与配置
|
||||
- [ ] 更新 `specs/PRD.md` 和 `API.md`,删除关于微信登录、分享及支付的所有规格说明。
|
||||
- [ ] 更新 `backend/config.toml` (及模板),移除 `[wechat]` 相关配置项 (AppID, Secret 等)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 缺失功能需求 (Missing Features)
|
||||
|
||||
以下功能在后端 API 已有部分支持,但前端缺失页面,或业务逻辑需适配生产环境(特别是 S3)。
|
||||
|
||||
### 2.1 创作者中心 - 团队成员管理 (Creator Member Management)
|
||||
**优先级**: High (P0)
|
||||
**现状**: 后端已提供 `CreateInvite`, `ReviewMember`, `CreateMemberInvite` 等 API,但前端 `frontend/portal/src/views/creator/` 目录下完全缺失对应 UI。
|
||||
|
||||
**需求描述**:
|
||||
1. **成员列表页 (Member List)**
|
||||
- 展示当前租户下的所有成员。
|
||||
- 显示成员基本信息(头像、昵称、加入时间)及角色(Owner/Admin/Member)。
|
||||
- 提供移除成员的操作入口(仅 Admin/Owner 可见)。
|
||||
|
||||
2. **邀请功能 (Invite System)**
|
||||
- **生成邀请**: 提供表单生成邀请链接或邀请码(设置有效期、最大使用次数)。
|
||||
- **邀请记录**: 展示当前有效的邀请列表,支持“撤销/禁用”邀请。
|
||||
|
||||
3. **审核功能 (Join Requests)**
|
||||
- **待审核列表**: 展示用户主动发起的加入申请 (Reason, User Info)。
|
||||
- **审批操作**: 提供“通过”和“拒绝”按钮,调用后端 `ReviewMember` 接口。
|
||||
|
||||
### 2.2 媒体处理管线适配 (Media Pipeline for S3)
|
||||
**优先级**: High (P1)
|
||||
**现状**: `backend/app/jobs/media_process_job.go` 目前逻辑强依赖本地文件系统 (`Local Storage`),无法处理 S3 上的文件。
|
||||
|
||||
**需求描述**:
|
||||
1. **处理流程重构**:
|
||||
- 兼容 S3 存储模式:Worker 需先将源文件从 S3 下载到本地临时目录。
|
||||
- 执行 FFmpeg 处理(视频转码、截取封面、音频波形提取)。
|
||||
- 将处理后的产物(封面图、预览片段)上传回 S3。
|
||||
- 清理本地临时文件。
|
||||
2. **封面图自动生成**:
|
||||
- 视频上传完成后,必须自动截取第一帧或指定帧作为封面 (`cover`),避免前端展示空白。
|
||||
|
||||
### 2.3 租户公开页完善 (Tenant Public Page)
|
||||
**优先级**: Medium (P2)
|
||||
**现状**: `frontend/portal/src/views/tenant/` 目前仅有基础 `HomeView`。
|
||||
|
||||
**需求描述**:
|
||||
1. **关于/简介页**:
|
||||
- 增加展示租户详细介绍 (`Description`, `Bio`) 的区域或模态框。
|
||||
2. **内容聚合优化**:
|
||||
- 确认 `HomeView` 支持按“专辑/Topic”或“最新/最热”维度筛选展示内容。
|
||||
|
||||
---
|
||||
|
||||
## 3. 执行路线图 (Roadmap)
|
||||
|
||||
1. **Phase 1 (Cleanup)**: 优先执行 [1. 功能清理],确保代码库整洁,去除干扰项。
|
||||
2. **Phase 2 (Frontend)**: 开发 Creator Portal 的 [2.1 团队成员管理] 模块,补全多租户协作能力。
|
||||
3. **Phase 3 (Backend)**: 配合 S3 调试进度,重构 [2.2 媒体处理管线],确保线上媒体资源可用。
|
||||
4. **Phase 4 (Polish)**: 完善租户公开页细节及最终文档更新。
|
||||
Reference in New Issue
Block a user