feat: add superadmin member review and coupon ops
This commit is contained in:
@@ -2,8 +2,10 @@ package v1
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
dto "quyun/v2/app/http/super/v1/dto"
|
dto "quyun/v2/app/http/super/v1/dto"
|
||||||
|
v1_dto "quyun/v2/app/http/v1/dto"
|
||||||
"quyun/v2/app/requests"
|
"quyun/v2/app/requests"
|
||||||
"quyun/v2/app/services"
|
"quyun/v2/app/services"
|
||||||
|
"quyun/v2/database/models"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
)
|
)
|
||||||
@@ -26,3 +28,81 @@ type coupons struct{}
|
|||||||
func (c *coupons) List(ctx fiber.Ctx, filter *dto.SuperCouponListFilter) (*requests.Pager, error) {
|
func (c *coupons) List(ctx fiber.Ctx, filter *dto.SuperCouponListFilter) (*requests.Pager, error) {
|
||||||
return services.Super.ListCoupons(ctx, filter)
|
return services.Super.ListCoupons(ctx, filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create coupon
|
||||||
|
//
|
||||||
|
// @Router /super/v1/tenants/:tenantID<int>/coupons [post]
|
||||||
|
// @Summary Create coupon
|
||||||
|
// @Description Create coupon for tenant
|
||||||
|
// @Tags Coupon
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param tenantID path int64 true "Tenant ID"
|
||||||
|
// @Param form body v1_dto.CouponCreateForm true "Create form"
|
||||||
|
// @Success 200 {object} v1_dto.CouponItem
|
||||||
|
// @Bind tenantID path
|
||||||
|
// @Bind form body
|
||||||
|
// @Bind user local key(__ctx_user)
|
||||||
|
func (c *coupons) Create(ctx fiber.Ctx, user *models.User, tenantID int64, form *v1_dto.CouponCreateForm) (*v1_dto.CouponItem, error) {
|
||||||
|
return services.Coupon.Create(ctx, tenantID, user.ID, form)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get coupon
|
||||||
|
//
|
||||||
|
// @Router /super/v1/tenants/:tenantID<int>/coupons/:id<int> [get]
|
||||||
|
// @Summary Get coupon
|
||||||
|
// @Description Get coupon detail
|
||||||
|
// @Tags Coupon
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param tenantID path int64 true "Tenant ID"
|
||||||
|
// @Param id path int64 true "Coupon ID"
|
||||||
|
// @Success 200 {object} v1_dto.CouponItem
|
||||||
|
// @Bind tenantID path
|
||||||
|
// @Bind id path
|
||||||
|
func (c *coupons) Get(ctx fiber.Ctx, tenantID, id int64) (*v1_dto.CouponItem, error) {
|
||||||
|
return services.Coupon.Get(ctx, tenantID, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update coupon
|
||||||
|
//
|
||||||
|
// @Router /super/v1/tenants/:tenantID<int>/coupons/:id<int> [put]
|
||||||
|
// @Summary Update coupon
|
||||||
|
// @Description Update coupon for tenant
|
||||||
|
// @Tags Coupon
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param tenantID path int64 true "Tenant ID"
|
||||||
|
// @Param id path int64 true "Coupon ID"
|
||||||
|
// @Param form body v1_dto.CouponUpdateForm true "Update form"
|
||||||
|
// @Success 200 {object} v1_dto.CouponItem
|
||||||
|
// @Bind tenantID path
|
||||||
|
// @Bind id path
|
||||||
|
// @Bind form body
|
||||||
|
// @Bind user local key(__ctx_user)
|
||||||
|
func (c *coupons) Update(ctx fiber.Ctx, user *models.User, tenantID, id int64, form *v1_dto.CouponUpdateForm) (*v1_dto.CouponItem, error) {
|
||||||
|
return services.Coupon.Update(ctx, tenantID, user.ID, id, form)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grant coupon
|
||||||
|
//
|
||||||
|
// @Router /super/v1/tenants/:tenantID<int>/coupons/:id<int>/grant [post]
|
||||||
|
// @Summary Grant coupon
|
||||||
|
// @Description Grant coupon to users
|
||||||
|
// @Tags Coupon
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param tenantID path int64 true "Tenant ID"
|
||||||
|
// @Param id path int64 true "Coupon ID"
|
||||||
|
// @Param form body v1_dto.CouponGrantForm true "Grant form"
|
||||||
|
// @Success 200 {object} dto.SuperCouponGrantResponse
|
||||||
|
// @Bind tenantID path
|
||||||
|
// @Bind id path
|
||||||
|
// @Bind form body
|
||||||
|
func (c *coupons) Grant(ctx fiber.Ctx, tenantID, id int64, form *v1_dto.CouponGrantForm) (*dto.SuperCouponGrantResponse, error) {
|
||||||
|
granted, err := services.Coupon.Grant(ctx, tenantID, id, form.UserIDs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &dto.SuperCouponGrantResponse{Granted: granted}, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -454,6 +454,59 @@ type TenantUser struct {
|
|||||||
UpdatedAt string `json:"updated_at"`
|
UpdatedAt string `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SuperTenantJoinRequestListFilter 超管成员申请列表过滤条件。
|
||||||
|
type SuperTenantJoinRequestListFilter struct {
|
||||||
|
requests.Pagination
|
||||||
|
// TenantID 租户ID,精确匹配。
|
||||||
|
TenantID *int64 `query:"tenant_id"`
|
||||||
|
// TenantCode 租户编码,模糊匹配。
|
||||||
|
TenantCode *string `query:"tenant_code"`
|
||||||
|
// TenantName 租户名称,模糊匹配。
|
||||||
|
TenantName *string `query:"tenant_name"`
|
||||||
|
// UserID 申请用户ID,精确匹配。
|
||||||
|
UserID *int64 `query:"user_id"`
|
||||||
|
// Username 申请用户用户名/昵称,模糊匹配。
|
||||||
|
Username *string `query:"username"`
|
||||||
|
// Status 申请状态(pending/approved/rejected)。
|
||||||
|
Status *consts.TenantJoinRequestStatus `query:"status"`
|
||||||
|
// CreatedAtFrom 申请时间起始(RFC3339)。
|
||||||
|
CreatedAtFrom *string `query:"created_at_from"`
|
||||||
|
// CreatedAtTo 申请时间结束(RFC3339)。
|
||||||
|
CreatedAtTo *string `query:"created_at_to"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SuperTenantJoinRequestItem 超管成员申请列表项。
|
||||||
|
type SuperTenantJoinRequestItem struct {
|
||||||
|
// ID 申请记录ID。
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
// TenantID 租户ID。
|
||||||
|
TenantID int64 `json:"tenant_id"`
|
||||||
|
// TenantCode 租户编码。
|
||||||
|
TenantCode string `json:"tenant_code"`
|
||||||
|
// TenantName 租户名称。
|
||||||
|
TenantName string `json:"tenant_name"`
|
||||||
|
// UserID 申请用户ID。
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
// Username 申请用户名称。
|
||||||
|
Username string `json:"username"`
|
||||||
|
// 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"`
|
||||||
|
}
|
||||||
|
|
||||||
// Order Related
|
// Order Related
|
||||||
type SuperOrderItem struct {
|
type SuperOrderItem struct {
|
||||||
// ID 订单ID。
|
// ID 订单ID。
|
||||||
|
|||||||
@@ -73,3 +73,9 @@ type SuperCouponItem struct {
|
|||||||
// UpdatedAt 更新时间(RFC3339)。
|
// UpdatedAt 更新时间(RFC3339)。
|
||||||
UpdatedAt string `json:"updated_at"`
|
UpdatedAt string `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SuperCouponGrantResponse 超管优惠券发放结果。
|
||||||
|
type SuperCouponGrantResponse struct {
|
||||||
|
// Granted 实际发放数量。
|
||||||
|
Granted int `json:"granted"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ package v1
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
dto "quyun/v2/app/http/super/v1/dto"
|
dto "quyun/v2/app/http/super/v1/dto"
|
||||||
|
v1_dto "quyun/v2/app/http/v1/dto"
|
||||||
"quyun/v2/app/middlewares"
|
"quyun/v2/app/middlewares"
|
||||||
"quyun/v2/database/models"
|
"quyun/v2/database/models"
|
||||||
|
|
||||||
@@ -87,6 +88,34 @@ func (r *Routes) Register(router fiber.Router) {
|
|||||||
r.coupons.List,
|
r.coupons.List,
|
||||||
Query[dto.SuperCouponListFilter]("filter"),
|
Query[dto.SuperCouponListFilter]("filter"),
|
||||||
))
|
))
|
||||||
|
r.log.Debugf("Registering route: Get /super/v1/tenants/:tenantID<int>/coupons/:id<int> -> coupons.Get")
|
||||||
|
router.Get("/super/v1/tenants/:tenantID<int>/coupons/:id<int>"[len(r.Path()):], DataFunc2(
|
||||||
|
r.coupons.Get,
|
||||||
|
PathParam[int64]("tenantID"),
|
||||||
|
PathParam[int64]("id"),
|
||||||
|
))
|
||||||
|
r.log.Debugf("Registering route: Post /super/v1/tenants/:tenantID<int>/coupons -> coupons.Create")
|
||||||
|
router.Post("/super/v1/tenants/:tenantID<int>/coupons"[len(r.Path()):], DataFunc3(
|
||||||
|
r.coupons.Create,
|
||||||
|
Local[*models.User]("__ctx_user"),
|
||||||
|
PathParam[int64]("tenantID"),
|
||||||
|
Body[v1_dto.CouponCreateForm]("form"),
|
||||||
|
))
|
||||||
|
r.log.Debugf("Registering route: Post /super/v1/tenants/:tenantID<int>/coupons/:id<int>/grant -> coupons.Grant")
|
||||||
|
router.Post("/super/v1/tenants/:tenantID<int>/coupons/:id<int>/grant"[len(r.Path()):], DataFunc3(
|
||||||
|
r.coupons.Grant,
|
||||||
|
PathParam[int64]("tenantID"),
|
||||||
|
PathParam[int64]("id"),
|
||||||
|
Body[v1_dto.CouponGrantForm]("form"),
|
||||||
|
))
|
||||||
|
r.log.Debugf("Registering route: Put /super/v1/tenants/:tenantID<int>/coupons/:id<int> -> coupons.Update")
|
||||||
|
router.Put("/super/v1/tenants/:tenantID<int>/coupons/:id<int>"[len(r.Path()):], DataFunc4(
|
||||||
|
r.coupons.Update,
|
||||||
|
Local[*models.User]("__ctx_user"),
|
||||||
|
PathParam[int64]("tenantID"),
|
||||||
|
PathParam[int64]("id"),
|
||||||
|
Body[v1_dto.CouponUpdateForm]("form"),
|
||||||
|
))
|
||||||
// Register routes for controller: creators
|
// Register routes for controller: creators
|
||||||
r.log.Debugf("Registering route: Get /super/v1/creators -> creators.List")
|
r.log.Debugf("Registering route: Get /super/v1/creators -> creators.List")
|
||||||
router.Get("/super/v1/creators"[len(r.Path()):], DataFunc1(
|
router.Get("/super/v1/creators"[len(r.Path()):], DataFunc1(
|
||||||
@@ -126,6 +155,11 @@ func (r *Routes) Register(router fiber.Router) {
|
|||||||
Body[dto.SuperReportExportForm]("form"),
|
Body[dto.SuperReportExportForm]("form"),
|
||||||
))
|
))
|
||||||
// Register routes for controller: tenants
|
// Register routes for controller: tenants
|
||||||
|
r.log.Debugf("Registering route: Get /super/v1/tenant-join-requests -> tenants.ListJoinRequests")
|
||||||
|
router.Get("/super/v1/tenant-join-requests"[len(r.Path()):], DataFunc1(
|
||||||
|
r.tenants.ListJoinRequests,
|
||||||
|
Query[dto.SuperTenantJoinRequestListFilter]("filter"),
|
||||||
|
))
|
||||||
r.log.Debugf("Registering route: Get /super/v1/tenants -> tenants.List")
|
r.log.Debugf("Registering route: Get /super/v1/tenants -> tenants.List")
|
||||||
router.Get("/super/v1/tenants"[len(r.Path()):], DataFunc1(
|
router.Get("/super/v1/tenants"[len(r.Path()):], DataFunc1(
|
||||||
r.tenants.List,
|
r.tenants.List,
|
||||||
@@ -163,11 +197,24 @@ func (r *Routes) Register(router fiber.Router) {
|
|||||||
PathParam[int64]("id"),
|
PathParam[int64]("id"),
|
||||||
Body[dto.TenantStatusUpdateForm]("form"),
|
Body[dto.TenantStatusUpdateForm]("form"),
|
||||||
))
|
))
|
||||||
|
r.log.Debugf("Registering route: Post /super/v1/tenant-join-requests/:id<int>/review -> tenants.ReviewJoinRequest")
|
||||||
|
router.Post("/super/v1/tenant-join-requests/:id<int>/review"[len(r.Path()):], Func3(
|
||||||
|
r.tenants.ReviewJoinRequest,
|
||||||
|
Local[*models.User]("__ctx_user"),
|
||||||
|
PathParam[int64]("id"),
|
||||||
|
Body[v1_dto.TenantJoinReviewForm]("form"),
|
||||||
|
))
|
||||||
r.log.Debugf("Registering route: Post /super/v1/tenants -> tenants.Create")
|
r.log.Debugf("Registering route: Post /super/v1/tenants -> tenants.Create")
|
||||||
router.Post("/super/v1/tenants"[len(r.Path()):], Func1(
|
router.Post("/super/v1/tenants"[len(r.Path()):], Func1(
|
||||||
r.tenants.Create,
|
r.tenants.Create,
|
||||||
Body[dto.TenantCreateForm]("form"),
|
Body[dto.TenantCreateForm]("form"),
|
||||||
))
|
))
|
||||||
|
r.log.Debugf("Registering route: Post /super/v1/tenants/:tenantID<int>/invites -> tenants.CreateInvite")
|
||||||
|
router.Post("/super/v1/tenants/:tenantID<int>/invites"[len(r.Path()):], DataFunc2(
|
||||||
|
r.tenants.CreateInvite,
|
||||||
|
PathParam[int64]("tenantID"),
|
||||||
|
Body[v1_dto.TenantInviteCreateForm]("form"),
|
||||||
|
))
|
||||||
// Register routes for controller: users
|
// Register routes for controller: users
|
||||||
r.log.Debugf("Registering route: Get /super/v1/users -> users.List")
|
r.log.Debugf("Registering route: Get /super/v1/users -> users.List")
|
||||||
router.Get("/super/v1/users"[len(r.Path()):], DataFunc1(
|
router.Get("/super/v1/users"[len(r.Path()):], DataFunc1(
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ package v1
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
dto "quyun/v2/app/http/super/v1/dto"
|
dto "quyun/v2/app/http/super/v1/dto"
|
||||||
|
v1_dto "quyun/v2/app/http/v1/dto"
|
||||||
"quyun/v2/app/requests"
|
"quyun/v2/app/requests"
|
||||||
"quyun/v2/app/services"
|
"quyun/v2/app/services"
|
||||||
|
"quyun/v2/database/models"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
)
|
)
|
||||||
@@ -62,6 +64,57 @@ func (c *tenants) ListUsers(ctx fiber.Ctx, tenantID int64, filter *dto.SuperTena
|
|||||||
return services.Super.ListTenantUsers(ctx, tenantID, filter)
|
return services.Super.ListTenantUsers(ctx, tenantID, filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// List tenant join requests
|
||||||
|
//
|
||||||
|
// @Router /super/v1/tenant-join-requests [get]
|
||||||
|
// @Summary List tenant join requests
|
||||||
|
// @Description List tenant join requests across tenants
|
||||||
|
// @Tags Tenant
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param page query int false "Page number"
|
||||||
|
// @Param limit query int false "Page size"
|
||||||
|
// @Success 200 {object} requests.Pager{items=[]dto.SuperTenantJoinRequestItem}
|
||||||
|
// @Bind filter query
|
||||||
|
func (c *tenants) ListJoinRequests(ctx fiber.Ctx, filter *dto.SuperTenantJoinRequestListFilter) (*requests.Pager, error) {
|
||||||
|
return services.Super.ListTenantJoinRequests(ctx, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Review tenant join request
|
||||||
|
//
|
||||||
|
// @Router /super/v1/tenant-join-requests/:id<int>/review [post]
|
||||||
|
// @Summary Review tenant join request
|
||||||
|
// @Description Approve or reject a tenant join request
|
||||||
|
// @Tags Tenant
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path int64 true "Join request ID"
|
||||||
|
// @Param form body v1_dto.TenantJoinReviewForm true "Review form"
|
||||||
|
// @Success 200 {string} string "Reviewed"
|
||||||
|
// @Bind user local key(__ctx_user)
|
||||||
|
// @Bind id path
|
||||||
|
// @Bind form body
|
||||||
|
func (c *tenants) ReviewJoinRequest(ctx fiber.Ctx, user *models.User, id int64, form *v1_dto.TenantJoinReviewForm) error {
|
||||||
|
return services.Super.ReviewTenantJoinRequest(ctx, user.ID, id, form)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create tenant invite
|
||||||
|
//
|
||||||
|
// @Router /super/v1/tenants/:tenantID<int>/invites [post]
|
||||||
|
// @Summary Create tenant invite
|
||||||
|
// @Description Create tenant invite code
|
||||||
|
// @Tags Tenant
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param tenantID path int64 true "Tenant ID"
|
||||||
|
// @Param form body v1_dto.TenantInviteCreateForm true "Invite form"
|
||||||
|
// @Success 200 {object} v1_dto.TenantInviteItem
|
||||||
|
// @Bind tenantID path
|
||||||
|
// @Bind form body
|
||||||
|
func (c *tenants) CreateInvite(ctx fiber.Ctx, tenantID int64, form *v1_dto.TenantInviteCreateForm) (*v1_dto.TenantInviteItem, error) {
|
||||||
|
return services.Super.CreateTenantInvite(ctx, tenantID, form)
|
||||||
|
}
|
||||||
|
|
||||||
// Create tenant
|
// Create tenant
|
||||||
//
|
//
|
||||||
// @Router /super/v1/tenants [post]
|
// @Router /super/v1/tenants [post]
|
||||||
|
|||||||
@@ -768,6 +768,273 @@ func (s *super) ListTenantUsers(ctx context.Context, tenantID int64, filter *sup
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *super) ListTenantJoinRequests(ctx context.Context, filter *super_dto.SuperTenantJoinRequestListFilter) (*requests.Pager, error) {
|
||||||
|
if filter == nil {
|
||||||
|
filter = &super_dto.SuperTenantJoinRequestListFilter{}
|
||||||
|
}
|
||||||
|
|
||||||
|
tbl, q := models.TenantJoinRequestQuery.QueryContext(ctx)
|
||||||
|
if filter.TenantID != nil && *filter.TenantID > 0 {
|
||||||
|
q = q.Where(tbl.TenantID.Eq(*filter.TenantID))
|
||||||
|
}
|
||||||
|
if filter.UserID != nil && *filter.UserID > 0 {
|
||||||
|
q = q.Where(tbl.UserID.Eq(*filter.UserID))
|
||||||
|
}
|
||||||
|
if filter.Status != nil && *filter.Status != "" {
|
||||||
|
q = q.Where(tbl.Status.Eq(string(*filter.Status)))
|
||||||
|
}
|
||||||
|
|
||||||
|
tenantIDs, tenantFilter, err := s.lookupTenantIDs(ctx, filter.TenantCode, filter.TenantName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if tenantFilter {
|
||||||
|
if len(tenantIDs) == 0 {
|
||||||
|
q = q.Where(tbl.ID.Eq(-1))
|
||||||
|
} else {
|
||||||
|
q = q.Where(tbl.TenantID.In(tenantIDs...))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
userIDs, userFilter, err := s.lookupUserIDs(ctx, filter.Username)
|
||||||
|
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...))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.CreatedAtFrom != nil {
|
||||||
|
from, err := s.parseFilterTime(filter.CreatedAtFrom)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if from != nil {
|
||||||
|
q = q.Where(tbl.CreatedAt.Gte(*from))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if filter.CreatedAtTo != nil {
|
||||||
|
to, err := s.parseFilterTime(filter.CreatedAtTo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if to != nil {
|
||||||
|
q = q.Where(tbl.CreatedAt.Lte(*to))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 补齐租户与用户信息,便于前端展示。
|
||||||
|
tenantMap := make(map[int64]*models.Tenant)
|
||||||
|
userMap := make(map[int64]*models.User)
|
||||||
|
if len(list) > 0 {
|
||||||
|
tenantIDSet := make(map[int64]struct{})
|
||||||
|
userIDSet := make(map[int64]struct{})
|
||||||
|
for _, req := range list {
|
||||||
|
if req.TenantID > 0 {
|
||||||
|
tenantIDSet[req.TenantID] = struct{}{}
|
||||||
|
}
|
||||||
|
if req.UserID > 0 {
|
||||||
|
userIDSet[req.UserID] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tenantIDs := make([]int64, 0, len(tenantIDSet))
|
||||||
|
for id := range tenantIDSet {
|
||||||
|
tenantIDs = append(tenantIDs, id)
|
||||||
|
}
|
||||||
|
if len(tenantIDs) > 0 {
|
||||||
|
tenantTbl, tenantQuery := models.TenantQuery.QueryContext(ctx)
|
||||||
|
tenants, err := tenantQuery.Where(tenantTbl.ID.In(tenantIDs...)).Find()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||||
|
}
|
||||||
|
for _, tenant := range tenants {
|
||||||
|
tenantMap[tenant.ID] = tenant
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
userIDs := make([]int64, 0, len(userIDSet))
|
||||||
|
for id := range userIDSet {
|
||||||
|
userIDs = append(userIDs, id)
|
||||||
|
}
|
||||||
|
if len(userIDs) > 0 {
|
||||||
|
userTbl, userQuery := models.UserQuery.QueryContext(ctx)
|
||||||
|
users, err := userQuery.Where(userTbl.ID.In(userIDs...)).Find()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||||
|
}
|
||||||
|
for _, user := range users {
|
||||||
|
userMap[user.ID] = user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items := make([]super_dto.SuperTenantJoinRequestItem, 0, len(list))
|
||||||
|
for _, req := range list {
|
||||||
|
tenant := tenantMap[req.TenantID]
|
||||||
|
user := userMap[req.UserID]
|
||||||
|
status := consts.TenantJoinRequestStatus(req.Status)
|
||||||
|
statusDescription := status.Description()
|
||||||
|
if statusDescription == "" {
|
||||||
|
statusDescription = req.Status
|
||||||
|
}
|
||||||
|
|
||||||
|
tenantCode := ""
|
||||||
|
tenantName := ""
|
||||||
|
if tenant != nil {
|
||||||
|
tenantCode = tenant.Code
|
||||||
|
tenantName = tenant.Name
|
||||||
|
}
|
||||||
|
username := ""
|
||||||
|
if user != nil {
|
||||||
|
username = user.Username
|
||||||
|
}
|
||||||
|
if username == "" && req.UserID > 0 {
|
||||||
|
username = "ID:" + strconv.FormatInt(req.UserID, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
items = append(items, super_dto.SuperTenantJoinRequestItem{
|
||||||
|
ID: req.ID,
|
||||||
|
TenantID: req.TenantID,
|
||||||
|
TenantCode: tenantCode,
|
||||||
|
TenantName: tenantName,
|
||||||
|
UserID: req.UserID,
|
||||||
|
Username: username,
|
||||||
|
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 *super) ReviewTenantJoinRequest(ctx context.Context, operatorID, requestID int64, form *v1_dto.TenantJoinReviewForm) error {
|
||||||
|
if operatorID == 0 {
|
||||||
|
return errorx.ErrUnauthorized.WithMsg("缺少操作者信息")
|
||||||
|
}
|
||||||
|
if form == nil {
|
||||||
|
return errorx.ErrBadRequest.WithMsg("审核参数不能为空")
|
||||||
|
}
|
||||||
|
action := strings.ToLower(strings.TrimSpace(form.Action))
|
||||||
|
if action != "approve" && action != "reject" {
|
||||||
|
return errorx.ErrBadRequest.WithMsg("审核动作无效")
|
||||||
|
}
|
||||||
|
|
||||||
|
tblReq, qReq := models.TenantJoinRequestQuery.QueryContext(ctx)
|
||||||
|
req, err := qReq.Where(tblReq.ID.Eq(requestID)).First()
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return errorx.ErrRecordNotFound.WithMsg("申请不存在")
|
||||||
|
}
|
||||||
|
return errorx.ErrDatabaseError.WithCause(err)
|
||||||
|
}
|
||||||
|
if req.Status != string(consts.TenantJoinRequestStatusPending) {
|
||||||
|
return errorx.ErrBadRequest.WithMsg("申请已处理")
|
||||||
|
}
|
||||||
|
|
||||||
|
reason := strings.TrimSpace(form.Reason)
|
||||||
|
now := time.Now()
|
||||||
|
if action == "reject" {
|
||||||
|
_, err = qReq.Where(
|
||||||
|
tblReq.ID.Eq(req.ID),
|
||||||
|
tblReq.Status.Eq(string(consts.TenantJoinRequestStatusPending)),
|
||||||
|
).UpdateSimple(
|
||||||
|
tblReq.Status.Value(string(consts.TenantJoinRequestStatusRejected)),
|
||||||
|
tblReq.DecidedAt.Value(now),
|
||||||
|
tblReq.DecidedOperatorUserID.Value(operatorID),
|
||||||
|
tblReq.DecidedReason.Value(reason),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return errorx.ErrDatabaseError.WithCause(err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 审核通过需在事务内写入成员并更新申请状态。
|
||||||
|
return models.Q.Transaction(func(tx *models.Query) error {
|
||||||
|
tblMember, qMember := tx.TenantUser.QueryContext(ctx)
|
||||||
|
exists, err := qMember.Where(
|
||||||
|
tblMember.TenantID.Eq(req.TenantID),
|
||||||
|
tblMember.UserID.Eq(req.UserID),
|
||||||
|
).Exists()
|
||||||
|
if err != nil {
|
||||||
|
return errorx.ErrDatabaseError.WithCause(err)
|
||||||
|
}
|
||||||
|
if exists {
|
||||||
|
return errorx.ErrBadRequest.WithMsg("用户已是成员")
|
||||||
|
}
|
||||||
|
|
||||||
|
member := &models.TenantUser{
|
||||||
|
TenantID: req.TenantID,
|
||||||
|
UserID: req.UserID,
|
||||||
|
Role: types.Array[consts.TenantUserRole]{consts.TenantUserRoleMember},
|
||||||
|
Status: consts.UserStatusVerified,
|
||||||
|
}
|
||||||
|
if err := qMember.Create(member); err != nil {
|
||||||
|
return errorx.ErrDatabaseError.WithCause(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tblReqTx, qReqTx := tx.TenantJoinRequest.QueryContext(ctx)
|
||||||
|
_, err = qReqTx.Where(
|
||||||
|
tblReqTx.ID.Eq(req.ID),
|
||||||
|
tblReqTx.Status.Eq(string(consts.TenantJoinRequestStatusPending)),
|
||||||
|
).UpdateSimple(
|
||||||
|
tblReqTx.Status.Value(string(consts.TenantJoinRequestStatusApproved)),
|
||||||
|
tblReqTx.DecidedAt.Value(now),
|
||||||
|
tblReqTx.DecidedOperatorUserID.Value(operatorID),
|
||||||
|
tblReqTx.DecidedReason.Value(reason),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return errorx.ErrDatabaseError.WithCause(err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *super) CreateTenantInvite(ctx context.Context, tenantID int64, form *v1_dto.TenantInviteCreateForm) (*v1_dto.TenantInviteItem, error) {
|
||||||
|
if tenantID == 0 {
|
||||||
|
return nil, errorx.ErrRecordNotFound.WithMsg("租户不存在")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用租户主账号执行创建邀请码逻辑,复用既有校验流程。
|
||||||
|
tbl, q := models.TenantQuery.QueryContext(ctx)
|
||||||
|
tenant, err := q.Where(tbl.ID.Eq(tenantID)).First()
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, errorx.ErrRecordNotFound.WithMsg("租户不存在")
|
||||||
|
}
|
||||||
|
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||||
|
}
|
||||||
|
return Tenant.CreateInvite(ctx, tenantID, tenant.UserID, form)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *super) ListUserTenants(ctx context.Context, userID int64, filter *super_dto.SuperUserTenantListFilter) (*requests.Pager, error) {
|
func (s *super) ListUserTenants(ctx context.Context, userID int64, filter *super_dto.SuperUserTenantListFilter) (*requests.Pager, error) {
|
||||||
tbl, q := models.TenantUserQuery.QueryContext(ctx)
|
tbl, q := models.TenantUserQuery.QueryContext(ctx)
|
||||||
q = q.Where(tbl.UserID.Eq(userID))
|
q = q.Where(tbl.UserID.Eq(userID))
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
|
|
||||||
## 1) 总体结论
|
## 1) 总体结论
|
||||||
|
|
||||||
- **已落地**:登录、租户/用户/订单/内容的基础管理、提现审核、报表概览与导出、优惠券与创作者基础列表。
|
- **已落地**:登录、租户/用户/订单/内容的基础管理、内容审核(含批量)、提现审核、报表概览与导出、用户钱包查看、创作者成员审核/邀请、优惠券创建/编辑/发放。
|
||||||
- **部分落地**:平台概览(缺少内容统计与运营趋势)、内容治理(缺审核流)、创作者/优惠券/财务(缺跨租户深度操作与明细)。
|
- **部分落地**:平台概览(缺少内容统计与运营趋势)、租户详情(缺财务/报表聚合)、用户管理/详情(缺通知/优惠券/实名/充值/收藏等明细)、创作者/优惠券(缺结算账户审核/发放记录/冻结等深度治理)。
|
||||||
- **未落地**:资产治理、通知中心、审计与系统配置类能力。
|
- **未落地**:资产治理、通知中心、审计与系统配置类能力。
|
||||||
|
|
||||||
## 2) 按页面完成度(对照 2.x)
|
## 2) 按页面完成度(对照 2.x)
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
### 2.4 租户详情 `/superadmin/tenants/:tenantID`
|
### 2.4 租户详情 `/superadmin/tenants/:tenantID`
|
||||||
- 状态:**部分完成**
|
- 状态:**部分完成**
|
||||||
- 已有:租户信息、状态/续期、成员列表、内容与订单查询。
|
- 已有:租户信息、状态/续期、成员列表、内容与订单查询。
|
||||||
- 缺口:成员审核/邀请、租户级财务与报表聚合入口。
|
- 缺口:租户级财务与报表聚合入口(成员审核/邀请由超管入口完成)。
|
||||||
|
|
||||||
### 2.5 用户管理 `/superadmin/users`
|
### 2.5 用户管理 `/superadmin/users`
|
||||||
- 状态:**部分完成**
|
- 状态:**部分完成**
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
### 2.6 用户详情 `/superadmin/users/:userID`
|
### 2.6 用户详情 `/superadmin/users/:userID`
|
||||||
- 状态:**部分完成**
|
- 状态:**部分完成**
|
||||||
- 已有:用户资料、租户关系、订单查询、钱包余额与流水。
|
- 已有:用户资料、租户关系、订单查询、钱包余额与流水。
|
||||||
- 缺口:通知、优惠券、收藏/点赞等详情。
|
- 缺口:通知、优惠券、收藏/点赞、实名认证详情等明细。
|
||||||
|
|
||||||
### 2.7 内容治理 `/superadmin/contents`
|
### 2.7 内容治理 `/superadmin/contents`
|
||||||
- 状态:**部分完成**
|
- 状态:**部分完成**
|
||||||
@@ -52,13 +52,13 @@
|
|||||||
|
|
||||||
### 2.9 创作者与成员审核 `/superadmin/creators`
|
### 2.9 创作者与成员审核 `/superadmin/creators`
|
||||||
- 状态:**部分完成**
|
- 状态:**部分完成**
|
||||||
- 已有:创作者(租户)列表、状态更新。
|
- 已有:创作者(租户)列表、状态更新、成员申请列表/审核、成员邀请创建。
|
||||||
- 缺口:成员审核/邀请、结算账户审核、跨租户创作者审核流。
|
- 缺口:跨租户创作者申请审核、结算账户审核、提现审核入口(与财务联动)。
|
||||||
|
|
||||||
### 2.10 优惠券 `/superadmin/coupons`
|
### 2.10 优惠券 `/superadmin/coupons`
|
||||||
- 状态:**部分完成**
|
- 状态:**部分完成**
|
||||||
- 已有:跨租户优惠券列表。
|
- 已有:跨租户优惠券列表、创建/编辑、发放。
|
||||||
- 缺口:创建/编辑/冻结、发放记录与异常核查。
|
- 缺口:冻结/归档、发放记录与异常核查。
|
||||||
|
|
||||||
### 2.11 财务与钱包 `/superadmin/finance`
|
### 2.11 财务与钱包 `/superadmin/finance`
|
||||||
- 状态:**部分完成**
|
- 状态:**部分完成**
|
||||||
@@ -82,12 +82,12 @@
|
|||||||
|
|
||||||
## 3) `/super/v1` 接口覆盖度概览
|
## 3) `/super/v1` 接口覆盖度概览
|
||||||
|
|
||||||
- **已具备**:Auth、Tenants、Users(含钱包)、Contents、Orders、Withdrawals、Reports、Coupons(列表)、Creators(列表)。
|
- **已具备**:Auth、Tenants(含成员审核/邀请)、Users(含钱包)、Contents、Orders、Withdrawals、Reports、Coupons(列表/创建/编辑/发放)、Creators(列表)。
|
||||||
- **缺失/待补**:资产治理、通知中心、用户优惠券/通知明细、创作者成员审核、优惠券发放与冻结。
|
- **缺失/待补**:资产治理、通知中心、用户优惠券/通知明细、创作者申请/结算账户审核、优惠券冻结与发放记录。
|
||||||
|
|
||||||
## 4) 建议的下一步(按优先级)
|
## 4) 建议的下一步(按优先级)
|
||||||
|
|
||||||
1. **补齐内容审核与违规治理**:落地 `/super/v1/contents/:id/review` 前端入口与批量操作。
|
1. **完善用户与通知/优惠券明细**:补齐用户通知、优惠券、实名详情、充值记录等超管视图接口与页面。
|
||||||
2. **完善用户与钱包视图**:增加钱包流水、充值/退款/通知/优惠券详情接口及页面。
|
2. **创作者/优惠券深度治理**:补齐创作者申请/结算账户审核、优惠券冻结/发放记录。
|
||||||
3. **创作者与优惠券深度操作**:新增成员审核/邀请、优惠券冻结/发放记录。
|
3. **平台概览增强**:补齐内容总量与趋势、退款率、订单漏斗等核心指标。
|
||||||
4. **资产与通知中心**:补齐接口与页面,形成治理闭环。
|
4. **资产与通知中心**:补齐资产治理与通知中心接口/页面,形成治理闭环。
|
||||||
|
|||||||
@@ -40,5 +40,55 @@ export const CouponService = {
|
|||||||
total: data?.total ?? 0,
|
total: data?.total ?? 0,
|
||||||
items: normalizeItems(data?.items)
|
items: normalizeItems(data?.items)
|
||||||
};
|
};
|
||||||
|
},
|
||||||
|
async createCoupon(tenantID, form = {}) {
|
||||||
|
if (!tenantID) throw new Error('tenantID is required');
|
||||||
|
return requestJson(`/super/v1/tenants/${tenantID}/coupons`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: normalizeCouponForm(form)
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async getCoupon(tenantID, couponID) {
|
||||||
|
if (!tenantID) throw new Error('tenantID is required');
|
||||||
|
if (!couponID) throw new Error('couponID is required');
|
||||||
|
return requestJson(`/super/v1/tenants/${tenantID}/coupons/${couponID}`);
|
||||||
|
},
|
||||||
|
async updateCoupon(tenantID, couponID, form = {}) {
|
||||||
|
if (!tenantID) throw new Error('tenantID is required');
|
||||||
|
if (!couponID) throw new Error('couponID is required');
|
||||||
|
return requestJson(`/super/v1/tenants/${tenantID}/coupons/${couponID}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: normalizeCouponForm(form)
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async grantCoupon(tenantID, couponID, userIDs = []) {
|
||||||
|
if (!tenantID) throw new Error('tenantID is required');
|
||||||
|
if (!couponID) throw new Error('couponID is required');
|
||||||
|
if (!Array.isArray(userIDs) || userIDs.length === 0) throw new Error('userIDs is required');
|
||||||
|
return requestJson(`/super/v1/tenants/${tenantID}/coupons/${couponID}/grant`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: { user_ids: userIDs }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function normalizeCouponForm(form) {
|
||||||
|
const iso = (d) => {
|
||||||
|
if (!d) return undefined;
|
||||||
|
const date = d instanceof Date ? d : new Date(d);
|
||||||
|
if (Number.isNaN(date.getTime())) return undefined;
|
||||||
|
return date.toISOString();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: form.title,
|
||||||
|
description: form.description,
|
||||||
|
type: form.type,
|
||||||
|
value: form.value,
|
||||||
|
min_order_amount: form.min_order_amount,
|
||||||
|
max_discount: form.max_discount,
|
||||||
|
total_quantity: form.total_quantity,
|
||||||
|
start_at: iso(form.start_at),
|
||||||
|
end_at: iso(form.end_at)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -38,5 +38,52 @@ export const CreatorService = {
|
|||||||
total: data?.total ?? 0,
|
total: data?.total ?? 0,
|
||||||
items: normalizeItems(data?.items)
|
items: normalizeItems(data?.items)
|
||||||
};
|
};
|
||||||
|
},
|
||||||
|
async listJoinRequests({ page, limit, tenant_id, tenant_code, tenant_name, user_id, username, status, created_at_from, created_at_to } = {}) {
|
||||||
|
const iso = (d) => {
|
||||||
|
if (!d) return undefined;
|
||||||
|
const date = d instanceof Date ? d : new Date(d);
|
||||||
|
if (Number.isNaN(date.getTime())) return undefined;
|
||||||
|
return date.toISOString();
|
||||||
|
};
|
||||||
|
|
||||||
|
const query = {
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
tenant_id,
|
||||||
|
tenant_code,
|
||||||
|
tenant_name,
|
||||||
|
user_id,
|
||||||
|
username,
|
||||||
|
status,
|
||||||
|
created_at_from: iso(created_at_from),
|
||||||
|
created_at_to: iso(created_at_to)
|
||||||
|
};
|
||||||
|
|
||||||
|
const data = await requestJson('/super/v1/tenant-join-requests', { query });
|
||||||
|
return {
|
||||||
|
page: data?.page ?? page ?? 1,
|
||||||
|
limit: data?.limit ?? limit ?? 10,
|
||||||
|
total: data?.total ?? 0,
|
||||||
|
items: normalizeItems(data?.items)
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async reviewJoinRequest(requestID, { action, reason } = {}) {
|
||||||
|
if (!requestID) throw new Error('requestID is required');
|
||||||
|
return requestJson(`/super/v1/tenant-join-requests/${requestID}/review`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: { action, reason }
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async createInvite(tenantID, { max_uses, expires_at, remark } = {}) {
|
||||||
|
if (!tenantID) throw new Error('tenantID is required');
|
||||||
|
return requestJson(`/super/v1/tenants/${tenantID}/invites`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
max_uses,
|
||||||
|
expires_at,
|
||||||
|
remark
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -39,6 +39,25 @@ const statusOptions = [
|
|||||||
{ label: '已过期', value: 'expired' }
|
{ label: '已过期', value: 'expired' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const editDialogVisible = ref(false);
|
||||||
|
const couponSubmitting = ref(false);
|
||||||
|
const editingCoupon = ref(null);
|
||||||
|
const formTenantID = ref(null);
|
||||||
|
const formTitle = ref('');
|
||||||
|
const formDescription = ref('');
|
||||||
|
const formType = ref('fix_amount');
|
||||||
|
const formValue = ref(0);
|
||||||
|
const formMinOrderAmount = ref(0);
|
||||||
|
const formMaxDiscount = ref(0);
|
||||||
|
const formTotalQuantity = ref(0);
|
||||||
|
const formStartAt = ref(null);
|
||||||
|
const formEndAt = ref(null);
|
||||||
|
|
||||||
|
const grantDialogVisible = ref(false);
|
||||||
|
const grantSubmitting = ref(false);
|
||||||
|
const grantCoupon = ref(null);
|
||||||
|
const grantUserIDsText = ref('');
|
||||||
|
|
||||||
function formatDate(value) {
|
function formatDate(value) {
|
||||||
if (!value) return '-';
|
if (!value) return '-';
|
||||||
if (String(value).startsWith('0001-01-01')) return '-';
|
if (String(value).startsWith('0001-01-01')) return '-';
|
||||||
@@ -47,6 +66,13 @@ function formatDate(value) {
|
|||||||
return date.toLocaleString();
|
return date.toLocaleString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseDateValue(value) {
|
||||||
|
if (!value) return null;
|
||||||
|
const date = new Date(value);
|
||||||
|
if (Number.isNaN(date.getTime())) return null;
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
|
||||||
function formatCny(amountInCents) {
|
function formatCny(amountInCents) {
|
||||||
const amount = Number(amountInCents) / 100;
|
const amount = Number(amountInCents) / 100;
|
||||||
if (!Number.isFinite(amount)) return '-';
|
if (!Number.isFinite(amount)) return '-';
|
||||||
@@ -66,6 +92,111 @@ function getStatusSeverity(value) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resetCouponForm() {
|
||||||
|
formTenantID.value = null;
|
||||||
|
formTitle.value = '';
|
||||||
|
formDescription.value = '';
|
||||||
|
formType.value = 'fix_amount';
|
||||||
|
formValue.value = 0;
|
||||||
|
formMinOrderAmount.value = 0;
|
||||||
|
formMaxDiscount.value = 0;
|
||||||
|
formTotalQuantity.value = 0;
|
||||||
|
formStartAt.value = null;
|
||||||
|
formEndAt.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCreateDialog() {
|
||||||
|
editingCoupon.value = null;
|
||||||
|
resetCouponForm();
|
||||||
|
editDialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditDialog(row) {
|
||||||
|
editingCoupon.value = row;
|
||||||
|
formTenantID.value = row?.tenant_id ?? null;
|
||||||
|
formTitle.value = row?.title ?? '';
|
||||||
|
formDescription.value = row?.description ?? '';
|
||||||
|
formType.value = row?.type ?? 'fix_amount';
|
||||||
|
formValue.value = row?.value ?? 0;
|
||||||
|
formMinOrderAmount.value = row?.min_order_amount ?? 0;
|
||||||
|
formMaxDiscount.value = row?.max_discount ?? 0;
|
||||||
|
formTotalQuantity.value = row?.total_quantity ?? 0;
|
||||||
|
formStartAt.value = parseDateValue(row?.start_at);
|
||||||
|
formEndAt.value = parseDateValue(row?.end_at);
|
||||||
|
editDialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmSaveCoupon() {
|
||||||
|
const tenantValue = formTenantID.value;
|
||||||
|
if (!tenantValue) {
|
||||||
|
toast.add({ severity: 'warn', summary: '缺少租户', detail: '请填写 TenantID', life: 3000 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!formTitle.value.trim()) {
|
||||||
|
toast.add({ severity: 'warn', summary: '缺少标题', detail: '优惠券标题不能为空', life: 3000 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
couponSubmitting.value = true;
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
title: formTitle.value,
|
||||||
|
description: formDescription.value,
|
||||||
|
type: formType.value,
|
||||||
|
value: formValue.value,
|
||||||
|
min_order_amount: formMinOrderAmount.value,
|
||||||
|
max_discount: formMaxDiscount.value,
|
||||||
|
total_quantity: formTotalQuantity.value,
|
||||||
|
start_at: formStartAt.value,
|
||||||
|
end_at: formEndAt.value
|
||||||
|
};
|
||||||
|
if (editingCoupon.value?.id) {
|
||||||
|
await CouponService.updateCoupon(tenantValue, editingCoupon.value.id, payload);
|
||||||
|
toast.add({ severity: 'success', summary: '更新成功', detail: `CouponID: ${editingCoupon.value.id}`, life: 3000 });
|
||||||
|
} else {
|
||||||
|
await CouponService.createCoupon(tenantValue, payload);
|
||||||
|
toast.add({ severity: 'success', summary: '创建成功', detail: `TenantID: ${tenantValue}`, life: 3000 });
|
||||||
|
}
|
||||||
|
editDialogVisible.value = false;
|
||||||
|
await loadCoupons();
|
||||||
|
} catch (error) {
|
||||||
|
toast.add({ severity: 'error', summary: '保存失败', detail: error?.message || '无法保存优惠券', life: 4000 });
|
||||||
|
} finally {
|
||||||
|
couponSubmitting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openGrantDialog(row) {
|
||||||
|
grantCoupon.value = row;
|
||||||
|
grantUserIDsText.value = '';
|
||||||
|
grantDialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmGrant() {
|
||||||
|
const coupon = grantCoupon.value;
|
||||||
|
if (!coupon?.id || !coupon?.tenant_id) return;
|
||||||
|
const ids = grantUserIDsText.value
|
||||||
|
.split(/[\s,]+/g)
|
||||||
|
.map((item) => Number(item))
|
||||||
|
.filter((value) => Number.isFinite(value) && value > 0);
|
||||||
|
if (ids.length === 0) {
|
||||||
|
toast.add({ severity: 'warn', summary: '请输入用户ID', detail: '至少填写 1 个用户ID', life: 3000 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
grantSubmitting.value = true;
|
||||||
|
try {
|
||||||
|
const res = await CouponService.grantCoupon(coupon.tenant_id, coupon.id, ids);
|
||||||
|
toast.add({ severity: 'success', summary: '发放成功', detail: `已发放 ${res?.granted ?? ids.length} 张`, life: 3000 });
|
||||||
|
grantDialogVisible.value = false;
|
||||||
|
await loadCoupons();
|
||||||
|
} catch (error) {
|
||||||
|
toast.add({ severity: 'error', summary: '发放失败', detail: error?.message || '无法发放优惠券', life: 4000 });
|
||||||
|
} finally {
|
||||||
|
grantSubmitting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadCoupons() {
|
async function loadCoupons() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
@@ -136,6 +267,7 @@ onMounted(() => {
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<h4 class="m-0">优惠券</h4>
|
<h4 class="m-0">优惠券</h4>
|
||||||
|
<Button label="新建优惠券" icon="pi pi-plus" @click="openCreateDialog" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SearchPanel :loading="loading" @search="onSearch" @reset="onReset">
|
<SearchPanel :loading="loading" @search="onSearch" @reset="onReset">
|
||||||
@@ -246,6 +378,96 @@ onMounted(() => {
|
|||||||
{{ formatDate(data.created_at) }}
|
{{ formatDate(data.created_at) }}
|
||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
|
<Column header="操作" style="min-width: 12rem">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<Button label="编辑" icon="pi pi-pencil" text size="small" class="p-0 mr-3" @click="openEditDialog(data)" />
|
||||||
|
<Button label="发放" icon="pi pi-send" text size="small" class="p-0" @click="openGrantDialog(data)" />
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
</DataTable>
|
</DataTable>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Dialog v-model:visible="editDialogVisible" :modal="true" :style="{ width: '600px' }">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="font-medium">{{ editingCoupon ? '编辑优惠券' : '新建优惠券' }}</span>
|
||||||
|
<span v-if="editingCoupon?.id" class="text-muted-color">ID: {{ editingCoupon.id }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block font-medium mb-2">TenantID</label>
|
||||||
|
<InputNumber v-model="formTenantID" :min="1" placeholder="必填" class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block font-medium mb-2">标题</label>
|
||||||
|
<InputText v-model="formTitle" placeholder="请输入标题" class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block font-medium mb-2">描述</label>
|
||||||
|
<InputText v-model="formDescription" placeholder="可选" class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block font-medium mb-2">类型</label>
|
||||||
|
<Select v-model="formType" :options="typeOptions" optionLabel="label" optionValue="value" class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block font-medium mb-2">面额/折扣</label>
|
||||||
|
<InputNumber v-model="formValue" :min="0" class="w-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block font-medium mb-2">最低门槛(分)</label>
|
||||||
|
<InputNumber v-model="formMinOrderAmount" :min="0" class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block font-medium mb-2">最高折扣(分)</label>
|
||||||
|
<InputNumber v-model="formMaxDiscount" :min="0" class="w-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block font-medium mb-2">发行量</label>
|
||||||
|
<InputNumber v-model="formTotalQuantity" :min="0" class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-muted-color flex items-center">0 表示不限量</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block font-medium mb-2">开始时间</label>
|
||||||
|
<DatePicker v-model="formStartAt" showIcon showButtonBar placeholder="可选" class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block font-medium mb-2">结束时间</label>
|
||||||
|
<DatePicker v-model="formEndAt" showIcon showButtonBar placeholder="可选" class="w-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<Button label="取消" icon="pi pi-times" text @click="editDialogVisible = false" :disabled="couponSubmitting" />
|
||||||
|
<Button label="确认保存" icon="pi pi-check" @click="confirmSaveCoupon" :loading="couponSubmitting" />
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog v-model:visible="grantDialogVisible" :modal="true" :style="{ width: '520px' }">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="font-medium">发放优惠券</span>
|
||||||
|
<span class="text-muted-color">ID: {{ grantCoupon?.id ?? '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div class="text-sm text-muted-color">输入用户ID,使用逗号或空格分隔。</div>
|
||||||
|
<div>
|
||||||
|
<label class="block font-medium mb-2">用户ID列表</label>
|
||||||
|
<InputText v-model="grantUserIDsText" placeholder="例如:1,2,3" class="w-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<Button label="取消" icon="pi pi-times" text @click="grantDialogVisible = false" :disabled="grantSubmitting" />
|
||||||
|
<Button label="确认发放" icon="pi pi-send" @click="confirmGrant" :loading="grantSubmitting" />
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { onMounted, ref } from 'vue';
|
|||||||
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
|
const tabValue = ref('creators');
|
||||||
|
|
||||||
const creators = ref([]);
|
const creators = ref([]);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
@@ -32,6 +34,39 @@ const statusUpdating = ref(false);
|
|||||||
const statusTenant = ref(null);
|
const statusTenant = ref(null);
|
||||||
const statusValue = ref(null);
|
const statusValue = ref(null);
|
||||||
|
|
||||||
|
const joinRequests = ref([]);
|
||||||
|
const joinRequestsLoading = ref(false);
|
||||||
|
const joinRequestsTotal = ref(0);
|
||||||
|
const joinRequestsPage = ref(1);
|
||||||
|
const joinRequestsRows = ref(10);
|
||||||
|
const joinTenantID = ref(null);
|
||||||
|
const joinTenantCode = ref('');
|
||||||
|
const joinTenantName = ref('');
|
||||||
|
const joinUserID = ref(null);
|
||||||
|
const joinUsername = ref('');
|
||||||
|
const joinStatus = ref('pending');
|
||||||
|
const joinCreatedAtFrom = ref(null);
|
||||||
|
const joinCreatedAtTo = ref(null);
|
||||||
|
|
||||||
|
const joinStatusOptions = [
|
||||||
|
{ label: '全部', value: '' },
|
||||||
|
{ label: '待审核', value: 'pending' },
|
||||||
|
{ label: '已通过', value: 'approved' },
|
||||||
|
{ label: '已驳回', value: 'rejected' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const reviewDialogVisible = ref(false);
|
||||||
|
const reviewSubmitting = ref(false);
|
||||||
|
const reviewAction = ref('approve');
|
||||||
|
const reviewReason = ref('');
|
||||||
|
const reviewTarget = ref(null);
|
||||||
|
|
||||||
|
const inviteDialogVisible = ref(false);
|
||||||
|
const inviteSubmitting = ref(false);
|
||||||
|
const inviteTenantID = ref(null);
|
||||||
|
const inviteMaxUses = ref(1);
|
||||||
|
const inviteExpiresAt = ref(null);
|
||||||
|
const inviteRemark = ref('');
|
||||||
function formatDate(value) {
|
function formatDate(value) {
|
||||||
if (!value) return '-';
|
if (!value) return '-';
|
||||||
if (String(value).startsWith('0001-01-01')) return '-';
|
if (String(value).startsWith('0001-01-01')) return '-';
|
||||||
@@ -53,6 +88,19 @@ function getStatusSeverity(value) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getJoinStatusSeverity(value) {
|
||||||
|
switch (value) {
|
||||||
|
case 'pending':
|
||||||
|
return 'warn';
|
||||||
|
case 'approved':
|
||||||
|
return 'success';
|
||||||
|
case 'rejected':
|
||||||
|
return 'danger';
|
||||||
|
default:
|
||||||
|
return 'secondary';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function ensureStatusOptionsLoaded() {
|
async function ensureStatusOptionsLoaded() {
|
||||||
if (statusOptions.value.length > 0) return;
|
if (statusOptions.value.length > 0) return;
|
||||||
statusOptionsLoading.value = true;
|
statusOptionsLoading.value = true;
|
||||||
@@ -94,6 +142,30 @@ async function loadCreators() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadJoinRequests() {
|
||||||
|
joinRequestsLoading.value = true;
|
||||||
|
try {
|
||||||
|
const result = await CreatorService.listJoinRequests({
|
||||||
|
page: joinRequestsPage.value,
|
||||||
|
limit: joinRequestsRows.value,
|
||||||
|
tenant_id: joinTenantID.value || undefined,
|
||||||
|
tenant_code: joinTenantCode.value,
|
||||||
|
tenant_name: joinTenantName.value,
|
||||||
|
user_id: joinUserID.value || undefined,
|
||||||
|
username: joinUsername.value,
|
||||||
|
status: joinStatus.value || undefined,
|
||||||
|
created_at_from: joinCreatedAtFrom.value || undefined,
|
||||||
|
created_at_to: joinCreatedAtTo.value || undefined
|
||||||
|
});
|
||||||
|
joinRequests.value = result.items;
|
||||||
|
joinRequestsTotal.value = result.total;
|
||||||
|
} catch (error) {
|
||||||
|
toast.add({ severity: 'error', summary: '加载失败', detail: error?.message || '无法加载成员申请', life: 4000 });
|
||||||
|
} finally {
|
||||||
|
joinRequestsLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function onSearch() {
|
function onSearch() {
|
||||||
page.value = 1;
|
page.value = 1;
|
||||||
loadCreators();
|
loadCreators();
|
||||||
@@ -126,6 +198,31 @@ function onSort(event) {
|
|||||||
loadCreators();
|
loadCreators();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onJoinSearch() {
|
||||||
|
joinRequestsPage.value = 1;
|
||||||
|
loadJoinRequests();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onJoinReset() {
|
||||||
|
joinTenantID.value = null;
|
||||||
|
joinTenantCode.value = '';
|
||||||
|
joinTenantName.value = '';
|
||||||
|
joinUserID.value = null;
|
||||||
|
joinUsername.value = '';
|
||||||
|
joinStatus.value = 'pending';
|
||||||
|
joinCreatedAtFrom.value = null;
|
||||||
|
joinCreatedAtTo.value = null;
|
||||||
|
joinRequestsPage.value = 1;
|
||||||
|
joinRequestsRows.value = 10;
|
||||||
|
loadJoinRequests();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onJoinPage(event) {
|
||||||
|
joinRequestsPage.value = (event.page ?? 0) + 1;
|
||||||
|
joinRequestsRows.value = event.rows ?? joinRequestsRows.value;
|
||||||
|
loadJoinRequests();
|
||||||
|
}
|
||||||
|
|
||||||
async function openStatusDialog(tenant) {
|
async function openStatusDialog(tenant) {
|
||||||
statusTenant.value = tenant;
|
statusTenant.value = tenant;
|
||||||
statusValue.value = tenant?.status ?? null;
|
statusValue.value = tenant?.status ?? null;
|
||||||
@@ -154,14 +251,82 @@ async function confirmUpdateStatus() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openReviewDialog(row, action) {
|
||||||
|
reviewTarget.value = row;
|
||||||
|
reviewAction.value = action || 'approve';
|
||||||
|
reviewReason.value = '';
|
||||||
|
reviewDialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmReview() {
|
||||||
|
const targetID = reviewTarget.value?.id;
|
||||||
|
if (!targetID) return;
|
||||||
|
const reason = reviewReason.value.trim();
|
||||||
|
if (reviewAction.value === 'reject' && !reason) {
|
||||||
|
toast.add({ severity: 'warn', summary: '请输入原因', detail: '驳回时需填写原因', life: 3000 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
reviewSubmitting.value = true;
|
||||||
|
try {
|
||||||
|
await CreatorService.reviewJoinRequest(targetID, { action: reviewAction.value, reason });
|
||||||
|
toast.add({ severity: 'success', summary: '审核完成', detail: `申请ID: ${targetID}`, life: 3000 });
|
||||||
|
reviewDialogVisible.value = false;
|
||||||
|
await loadJoinRequests();
|
||||||
|
} catch (error) {
|
||||||
|
toast.add({ severity: 'error', summary: '审核失败', detail: error?.message || '无法审核申请', life: 4000 });
|
||||||
|
} finally {
|
||||||
|
reviewSubmitting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openInviteDialog(row) {
|
||||||
|
inviteTenantID.value = Number(row?.tenant_id ?? row?.tenant?.id ?? 0) || null;
|
||||||
|
inviteMaxUses.value = 1;
|
||||||
|
inviteExpiresAt.value = null;
|
||||||
|
inviteRemark.value = '';
|
||||||
|
inviteDialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmInvite() {
|
||||||
|
const tenantIDValue = inviteTenantID.value;
|
||||||
|
if (!tenantIDValue) {
|
||||||
|
toast.add({ severity: 'warn', summary: '请输入租户ID', detail: '租户ID不能为空', life: 3000 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
inviteSubmitting.value = true;
|
||||||
|
try {
|
||||||
|
const result = await CreatorService.createInvite(tenantIDValue, {
|
||||||
|
max_uses: inviteMaxUses.value,
|
||||||
|
expires_at: inviteExpiresAt.value instanceof Date ? inviteExpiresAt.value.toISOString() : inviteExpiresAt.value || undefined,
|
||||||
|
remark: inviteRemark.value
|
||||||
|
});
|
||||||
|
toast.add({ severity: 'success', summary: '邀请已创建', detail: `邀请码: ${result?.code || '-'}`, life: 4000 });
|
||||||
|
inviteDialogVisible.value = false;
|
||||||
|
} catch (error) {
|
||||||
|
toast.add({ severity: 'error', summary: '创建失败', detail: error?.message || '无法创建邀请', life: 4000 });
|
||||||
|
} finally {
|
||||||
|
inviteSubmitting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadCreators();
|
loadCreators();
|
||||||
ensureStatusOptionsLoaded().catch(() => {});
|
ensureStatusOptionsLoaded().catch(() => {});
|
||||||
|
loadJoinRequests();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
<Tabs v-model:value="tabValue" value="creators">
|
||||||
|
<TabList>
|
||||||
|
<Tab value="creators">创作者列表</Tab>
|
||||||
|
<Tab value="members">成员审核</Tab>
|
||||||
|
</TabList>
|
||||||
|
<TabPanels>
|
||||||
|
<TabPanel value="creators">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<h4 class="m-0">创作者列表</h4>
|
<h4 class="m-0">创作者列表</h4>
|
||||||
</div>
|
</div>
|
||||||
@@ -246,6 +411,111 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
</DataTable>
|
</DataTable>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel value="members">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<h4 class="m-0">成员申请</h4>
|
||||||
|
<span class="text-muted-color">跨租户审核与邀请</span>
|
||||||
|
</div>
|
||||||
|
<Button label="创建邀请" icon="pi pi-link" severity="secondary" @click="openInviteDialog()" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SearchPanel :loading="joinRequestsLoading" @search="onJoinSearch" @reset="onJoinReset">
|
||||||
|
<SearchField label="TenantID">
|
||||||
|
<InputNumber v-model="joinTenantID" :min="1" placeholder="精确匹配" class="w-full" />
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="TenantCode">
|
||||||
|
<InputText v-model="joinTenantCode" placeholder="模糊匹配" class="w-full" @keyup.enter="onJoinSearch" />
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="TenantName">
|
||||||
|
<InputText v-model="joinTenantName" placeholder="模糊匹配" class="w-full" @keyup.enter="onJoinSearch" />
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="UserID">
|
||||||
|
<InputNumber v-model="joinUserID" :min="1" placeholder="精确匹配" class="w-full" />
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="Username">
|
||||||
|
<IconField>
|
||||||
|
<InputIcon>
|
||||||
|
<i class="pi pi-search" />
|
||||||
|
</InputIcon>
|
||||||
|
<InputText v-model="joinUsername" placeholder="模糊匹配" class="w-full" @keyup.enter="onJoinSearch" />
|
||||||
|
</IconField>
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="状态">
|
||||||
|
<Select v-model="joinStatus" :options="joinStatusOptions" optionLabel="label" optionValue="value" placeholder="请选择" class="w-full" />
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="申请时间 From">
|
||||||
|
<DatePicker v-model="joinCreatedAtFrom" showIcon showButtonBar placeholder="开始时间" class="w-full" />
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="申请时间 To">
|
||||||
|
<DatePicker v-model="joinCreatedAtTo" showIcon showButtonBar placeholder="结束时间" class="w-full" />
|
||||||
|
</SearchField>
|
||||||
|
</SearchPanel>
|
||||||
|
|
||||||
|
<DataTable
|
||||||
|
:value="joinRequests"
|
||||||
|
dataKey="id"
|
||||||
|
:loading="joinRequestsLoading"
|
||||||
|
lazy
|
||||||
|
:paginator="true"
|
||||||
|
:rows="joinRequestsRows"
|
||||||
|
:totalRecords="joinRequestsTotal"
|
||||||
|
:first="(joinRequestsPage - 1) * joinRequestsRows"
|
||||||
|
:rowsPerPageOptions="[10, 20, 50, 100]"
|
||||||
|
@page="onJoinPage"
|
||||||
|
currentPageReportTemplate="显示第 {first} - {last} 条,共 {totalRecords} 条"
|
||||||
|
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
|
||||||
|
scrollable
|
||||||
|
scrollHeight="420px"
|
||||||
|
responsiveLayout="scroll"
|
||||||
|
>
|
||||||
|
<Column field="id" header="申请ID" style="min-width: 8rem" />
|
||||||
|
<Column header="租户" style="min-width: 16rem">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<router-link v-if="data.tenant_id" class="inline-flex items-center gap-1 font-medium text-primary hover:underline" :to="`/superadmin/tenants/${data.tenant_id}`">
|
||||||
|
<span class="truncate max-w-[200px]">{{ data.tenant_name || data.tenant_code || '-' }}</span>
|
||||||
|
<i class="pi pi-external-link text-xs" />
|
||||||
|
</router-link>
|
||||||
|
<div class="text-xs text-muted-color">Code: {{ data.tenant_code || '-' }} / ID: {{ data.tenant_id ?? '-' }}</div>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column header="申请用户" style="min-width: 14rem">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<router-link v-if="data.user_id" class="inline-flex items-center gap-1 font-medium text-primary hover:underline" :to="`/superadmin/users/${data.user_id}`">
|
||||||
|
<span class="truncate max-w-[200px]">{{ data.username || `ID:${data.user_id}` }}</span>
|
||||||
|
<i class="pi pi-external-link text-xs" />
|
||||||
|
</router-link>
|
||||||
|
<div class="text-xs text-muted-color">ID: {{ data.user_id ?? '-' }}</div>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="status" header="状态" style="min-width: 10rem">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<Tag :value="data.status_description || data.status || '-'" :severity="getJoinStatusSeverity(data.status)" />
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="reason" header="申请说明" style="min-width: 16rem" />
|
||||||
|
<Column field="created_at" header="申请时间" style="min-width: 14rem">
|
||||||
|
<template #body="{ data }">
|
||||||
|
{{ formatDate(data.created_at) }}
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="decided_at" header="处理时间" style="min-width: 14rem">
|
||||||
|
<template #body="{ data }">
|
||||||
|
{{ formatDate(data.decided_at) }}
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column header="操作" style="min-width: 12rem">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<Button v-if="data.status === 'pending'" label="通过" icon="pi pi-check" text size="small" class="p-0 mr-3" @click="openReviewDialog(data, 'approve')" />
|
||||||
|
<Button v-if="data.status === 'pending'" label="驳回" icon="pi pi-times" severity="danger" text size="small" class="p-0 mr-3" @click="openReviewDialog(data, 'reject')" />
|
||||||
|
<Button label="邀请" icon="pi pi-link" text size="small" class="p-0" @click="openInviteDialog(data)" />
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
</DataTable>
|
||||||
|
</TabPanel>
|
||||||
|
</TabPanels>
|
||||||
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Dialog v-model:visible="statusDialogVisible" :modal="true" :style="{ width: '420px' }">
|
<Dialog v-model:visible="statusDialogVisible" :modal="true" :style="{ width: '420px' }">
|
||||||
@@ -266,4 +536,67 @@ onMounted(() => {
|
|||||||
<Button label="确认" icon="pi pi-check" @click="confirmUpdateStatus" :loading="statusUpdating" :disabled="!statusValue" />
|
<Button label="确认" icon="pi pi-check" @click="confirmUpdateStatus" :loading="statusUpdating" :disabled="!statusValue" />
|
||||||
</template>
|
</template>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog v-model:visible="reviewDialogVisible" :modal="true" :style="{ width: '520px' }">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="font-medium">成员申请审核</span>
|
||||||
|
<span class="text-muted-color">申请ID: {{ reviewTarget?.id ?? '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div class="text-sm text-muted-color">审核租户成员申请,请确认处理动作与备注。</div>
|
||||||
|
<div>
|
||||||
|
<label class="block font-medium mb-2">审核动作</label>
|
||||||
|
<Select
|
||||||
|
v-model="reviewAction"
|
||||||
|
:options="[
|
||||||
|
{ label: '通过', value: 'approve' },
|
||||||
|
{ label: '驳回', value: 'reject' }
|
||||||
|
]"
|
||||||
|
optionLabel="label"
|
||||||
|
optionValue="value"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block font-medium mb-2">审核说明</label>
|
||||||
|
<InputText v-model="reviewReason" placeholder="驳回时建议填写原因" class="w-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<Button label="取消" icon="pi pi-times" text @click="reviewDialogVisible = false" :disabled="reviewSubmitting" />
|
||||||
|
<Button label="确认审核" icon="pi pi-check" severity="success" @click="confirmReview" :loading="reviewSubmitting" :disabled="reviewSubmitting || (reviewAction === 'reject' && !reviewReason.trim())" />
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog v-model:visible="inviteDialogVisible" :modal="true" :style="{ width: '520px' }">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="font-medium">创建成员邀请</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block font-medium mb-2">TenantID</label>
|
||||||
|
<InputNumber v-model="inviteTenantID" :min="1" placeholder="请输入租户ID" class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block font-medium mb-2">最大使用次数</label>
|
||||||
|
<InputNumber v-model="inviteMaxUses" :min="1" placeholder="默认 1 次" class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block font-medium mb-2">过期时间</label>
|
||||||
|
<DatePicker v-model="inviteExpiresAt" showIcon showButtonBar placeholder="默认 7 天" class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block font-medium mb-2">备注</label>
|
||||||
|
<InputText v-model="inviteRemark" placeholder="可选备注" class="w-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<Button label="取消" icon="pi pi-times" text @click="inviteDialogVisible = false" :disabled="inviteSubmitting" />
|
||||||
|
<Button label="确认创建" icon="pi pi-check" @click="confirmInvite" :loading="inviteSubmitting" />
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user