feat: add superadmin user library detail
This commit is contained in:
@@ -151,3 +151,68 @@ type SuperUserContentActionItem struct {
|
||||
// Content 互动对应内容详情(含租户与作者信息)。
|
||||
Content *AdminContentItem `json:"content"`
|
||||
}
|
||||
|
||||
// SuperUserLibraryListFilter 超管用户内容消费列表过滤条件。
|
||||
type SuperUserLibraryListFilter struct {
|
||||
requests.Pagination
|
||||
// TenantID 内容所属租户ID,精确匹配。
|
||||
TenantID *int64 `query:"tenant_id"`
|
||||
// TenantCode 租户编码,模糊匹配。
|
||||
TenantCode *string `query:"tenant_code"`
|
||||
// TenantName 租户名称,模糊匹配。
|
||||
TenantName *string `query:"tenant_name"`
|
||||
// ContentID 内容ID,精确匹配。
|
||||
ContentID *int64 `query:"content_id"`
|
||||
// Keyword 内容标题/摘要/描述关键字,模糊匹配。
|
||||
Keyword *string `query:"keyword"`
|
||||
// Status 内容访问状态过滤(active/revoked/expired)。
|
||||
Status *consts.ContentAccessStatus `query:"status"`
|
||||
// OrderID 订单ID,精确匹配。
|
||||
OrderID *int64 `query:"order_id"`
|
||||
// OrderStatus 订单状态过滤。
|
||||
OrderStatus *consts.OrderStatus `query:"order_status"`
|
||||
// PaidAtFrom 支付时间起始(RFC3339)。
|
||||
PaidAtFrom *string `query:"paid_at_from"`
|
||||
// PaidAtTo 支付时间结束(RFC3339)。
|
||||
PaidAtTo *string `query:"paid_at_to"`
|
||||
// AccessedAtFrom 获取访问权限时间起始(RFC3339)。
|
||||
AccessedAtFrom *string `query:"accessed_at_from"`
|
||||
// AccessedAtTo 获取访问权限时间结束(RFC3339)。
|
||||
AccessedAtTo *string `query:"accessed_at_to"`
|
||||
// Asc 升序字段(id/created_at)。
|
||||
Asc *string `query:"asc"`
|
||||
// Desc 降序字段(id/created_at)。
|
||||
Desc *string `query:"desc"`
|
||||
}
|
||||
|
||||
// SuperUserLibraryItem 超管用户内容消费条目。
|
||||
type SuperUserLibraryItem struct {
|
||||
// AccessID 访问记录ID。
|
||||
AccessID int64 `json:"access_id"`
|
||||
// TenantID 内容所属租户ID。
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
// ContentID 内容ID。
|
||||
ContentID int64 `json:"content_id"`
|
||||
// OrderID 订单ID。
|
||||
OrderID int64 `json:"order_id"`
|
||||
// OrderType 订单类型。
|
||||
OrderType consts.OrderType `json:"order_type"`
|
||||
// OrderStatus 订单状态。
|
||||
OrderStatus consts.OrderStatus `json:"order_status"`
|
||||
// OrderStatusDescription 订单状态描述(用于展示)。
|
||||
OrderStatusDescription string `json:"order_status_description"`
|
||||
// AmountPaid 该内容实付金额(分)。
|
||||
AmountPaid int64 `json:"amount_paid"`
|
||||
// PaidAt 支付时间(RFC3339)。
|
||||
PaidAt string `json:"paid_at"`
|
||||
// AccessStatus 访问状态。
|
||||
AccessStatus consts.ContentAccessStatus `json:"access_status"`
|
||||
// AccessStatusDescription 访问状态描述(用于展示)。
|
||||
AccessStatusDescription string `json:"access_status_description"`
|
||||
// AccessedAt 获取访问权限时间(RFC3339)。
|
||||
AccessedAt string `json:"accessed_at"`
|
||||
// Content 内容详情(含租户/作者/价格)。
|
||||
Content *AdminContentItem `json:"content"`
|
||||
// Snapshot 下单快照(内容标题/金额等)。
|
||||
Snapshot any `json:"snapshot"`
|
||||
}
|
||||
|
||||
@@ -328,6 +328,12 @@ func (r *Routes) Register(router fiber.Router) {
|
||||
PathParam[int64]("id"),
|
||||
Query[dto.SuperUserTenantListFilter]("filter"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /super/v1/users/:id<int>/library -> users.ListLibrary")
|
||||
router.Get("/super/v1/users/:id<int>/library"[len(r.Path()):], DataFunc2(
|
||||
r.users.ListLibrary,
|
||||
PathParam[int64]("id"),
|
||||
Query[dto.SuperUserLibraryListFilter]("filter"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /super/v1/users/:id<int>/likes -> users.ListLikes")
|
||||
router.Get("/super/v1/users/:id<int>/likes"[len(r.Path()):], DataFunc2(
|
||||
r.users.ListLikes,
|
||||
|
||||
@@ -127,6 +127,24 @@ func (c *users) ListTenants(ctx fiber.Ctx, id int64, filter *dto.SuperUserTenant
|
||||
return services.Super.ListUserTenants(ctx, id, filter)
|
||||
}
|
||||
|
||||
// List user library
|
||||
//
|
||||
// @Router /super/v1/users/:id<int>/library [get]
|
||||
// @Summary List user library
|
||||
// @Description List purchased contents of a user
|
||||
// @Tags User
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int64 true "User ID"
|
||||
// @Param page query int false "Page number"
|
||||
// @Param limit query int false "Page size"
|
||||
// @Success 200 {object} requests.Pager{items=[]dto.SuperUserLibraryItem}
|
||||
// @Bind id path
|
||||
// @Bind filter query
|
||||
func (c *users) ListLibrary(ctx fiber.Ctx, id int64, filter *dto.SuperUserLibraryListFilter) (*requests.Pager, error) {
|
||||
return services.Super.ListUserLibrary(ctx, id, filter)
|
||||
}
|
||||
|
||||
// List user favorites
|
||||
//
|
||||
// @Router /super/v1/users/:id<int>/favorites [get]
|
||||
|
||||
@@ -1704,6 +1704,247 @@ func (s *super) ListUserTenants(ctx context.Context, userID int64, filter *super
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *super) ListUserLibrary(ctx context.Context, userID int64, filter *super_dto.SuperUserLibraryListFilter) (*requests.Pager, error) {
|
||||
if userID == 0 {
|
||||
return nil, errorx.ErrBadRequest.WithMsg("用户ID不能为空")
|
||||
}
|
||||
if filter == nil {
|
||||
filter = &super_dto.SuperUserLibraryListFilter{}
|
||||
}
|
||||
|
||||
tbl, q := models.ContentAccessQuery.QueryContext(ctx)
|
||||
q = q.Where(tbl.UserID.Eq(userID))
|
||||
|
||||
if filter.Status != nil && *filter.Status != "" {
|
||||
q = q.Where(tbl.Status.Eq(*filter.Status))
|
||||
}
|
||||
if filter.TenantID != nil && *filter.TenantID > 0 {
|
||||
q = q.Where(tbl.TenantID.Eq(*filter.TenantID))
|
||||
}
|
||||
if filter.OrderID != nil && *filter.OrderID > 0 {
|
||||
q = q.Where(tbl.OrderID.Eq(*filter.OrderID))
|
||||
}
|
||||
|
||||
contentIDs, contentFilter, err := s.filterContentIDsForUserLibrary(ctx, filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if contentFilter {
|
||||
if len(contentIDs) == 0 {
|
||||
filter.Pagination.Format()
|
||||
return &requests.Pager{
|
||||
Pagination: filter.Pagination,
|
||||
Total: 0,
|
||||
Items: []super_dto.SuperUserLibraryItem{},
|
||||
}, nil
|
||||
}
|
||||
q = q.Where(tbl.ContentID.In(contentIDs...))
|
||||
}
|
||||
|
||||
orderIDs, orderFilter, err := s.filterOrderIDsForUserLibrary(ctx, userID, filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if orderFilter {
|
||||
if len(orderIDs) == 0 {
|
||||
filter.Pagination.Format()
|
||||
return &requests.Pager{
|
||||
Pagination: filter.Pagination,
|
||||
Total: 0,
|
||||
Items: []super_dto.SuperUserLibraryItem{},
|
||||
}, nil
|
||||
}
|
||||
q = q.Where(tbl.OrderID.In(orderIDs...))
|
||||
}
|
||||
|
||||
if filter.AccessedAtFrom != nil {
|
||||
from, err := s.parseFilterTime(filter.AccessedAtFrom)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if from != nil {
|
||||
q = q.Where(tbl.CreatedAt.Gte(*from))
|
||||
}
|
||||
}
|
||||
if filter.AccessedAtTo != nil {
|
||||
to, err := s.parseFilterTime(filter.AccessedAtTo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if to != nil {
|
||||
q = q.Where(tbl.CreatedAt.Lte(*to))
|
||||
}
|
||||
}
|
||||
|
||||
orderApplied := false
|
||||
if filter.Desc != nil && strings.TrimSpace(*filter.Desc) != "" {
|
||||
switch strings.TrimSpace(*filter.Desc) {
|
||||
case "id":
|
||||
q = q.Order(tbl.ID.Desc())
|
||||
case "created_at":
|
||||
q = q.Order(tbl.CreatedAt.Desc())
|
||||
}
|
||||
orderApplied = true
|
||||
} else if filter.Asc != nil && strings.TrimSpace(*filter.Asc) != "" {
|
||||
switch strings.TrimSpace(*filter.Asc) {
|
||||
case "id":
|
||||
q = q.Order(tbl.ID)
|
||||
case "created_at":
|
||||
q = q.Order(tbl.CreatedAt)
|
||||
}
|
||||
orderApplied = true
|
||||
}
|
||||
if !orderApplied {
|
||||
q = q.Order(tbl.CreatedAt.Desc())
|
||||
}
|
||||
|
||||
filter.Pagination.Format()
|
||||
total, err := q.Count()
|
||||
if err != nil {
|
||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
list, err := q.Offset(int(filter.Pagination.Offset())).Limit(int(filter.Pagination.Limit)).Find()
|
||||
if err != nil {
|
||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
if len(list) == 0 {
|
||||
return &requests.Pager{
|
||||
Pagination: filter.Pagination,
|
||||
Total: total,
|
||||
Items: []super_dto.SuperUserLibraryItem{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
contentIDSet := make(map[int64]struct{}, len(list))
|
||||
contentIDs = make([]int64, 0, len(list))
|
||||
orderIDSet := make(map[int64]struct{}, len(list))
|
||||
orderIDs = make([]int64, 0, len(list))
|
||||
for _, access := range list {
|
||||
if access.ContentID > 0 {
|
||||
if _, ok := contentIDSet[access.ContentID]; !ok {
|
||||
contentIDSet[access.ContentID] = struct{}{}
|
||||
contentIDs = append(contentIDs, access.ContentID)
|
||||
}
|
||||
}
|
||||
if access.OrderID > 0 {
|
||||
if _, ok := orderIDSet[access.OrderID]; !ok {
|
||||
orderIDSet[access.OrderID] = struct{}{}
|
||||
orderIDs = append(orderIDs, access.OrderID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var contents []*models.Content
|
||||
if len(contentIDs) > 0 {
|
||||
contentTbl, contentQuery := models.ContentQuery.QueryContext(ctx)
|
||||
err := contentQuery.Where(contentTbl.ID.In(contentIDs...)).
|
||||
UnderlyingDB().
|
||||
Preload("Author").
|
||||
Preload("ContentAssets.Asset").
|
||||
Find(&contents).Error
|
||||
if err != nil {
|
||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
}
|
||||
|
||||
priceMap, err := s.contentPriceMap(ctx, contents)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tenantMap, err := s.contentTenantMap(ctx, contents)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
contentMap := make(map[int64]*models.Content, len(contents))
|
||||
for _, content := range contents {
|
||||
contentMap[content.ID] = content
|
||||
}
|
||||
|
||||
orderMap := make(map[int64]*models.Order, len(orderIDs))
|
||||
if len(orderIDs) > 0 {
|
||||
orderTbl, orderQuery := models.OrderQuery.QueryContext(ctx)
|
||||
orders, err := orderQuery.Where(orderTbl.ID.In(orderIDs...)).Find()
|
||||
if err != nil {
|
||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
for _, order := range orders {
|
||||
orderMap[order.ID] = order
|
||||
}
|
||||
}
|
||||
|
||||
type orderItemKey struct {
|
||||
orderID int64
|
||||
contentID int64
|
||||
}
|
||||
orderItemMap := make(map[orderItemKey]*models.OrderItem, len(list))
|
||||
if len(orderIDs) > 0 {
|
||||
itemTbl, itemQuery := models.OrderItemQuery.QueryContext(ctx)
|
||||
itemQuery = itemQuery.Where(itemTbl.OrderID.In(orderIDs...), itemTbl.UserID.Eq(userID))
|
||||
if len(contentIDs) > 0 {
|
||||
itemQuery = itemQuery.Where(itemTbl.ContentID.In(contentIDs...))
|
||||
}
|
||||
items, err := itemQuery.Find()
|
||||
if err != nil {
|
||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
for _, item := range items {
|
||||
orderItemMap[orderItemKey{orderID: item.OrderID, contentID: item.ContentID}] = item
|
||||
}
|
||||
}
|
||||
|
||||
items := make([]super_dto.SuperUserLibraryItem, 0, len(list))
|
||||
for _, access := range list {
|
||||
var contentItem *super_dto.AdminContentItem
|
||||
if content := contentMap[access.ContentID]; content != nil {
|
||||
item := s.toSuperContentItem(content, priceMap[content.ID], tenantMap[content.TenantID])
|
||||
contentItem = &item
|
||||
}
|
||||
|
||||
order := orderMap[access.OrderID]
|
||||
orderStatusDesc := ""
|
||||
orderType := consts.OrderType("")
|
||||
orderStatus := consts.OrderStatus("")
|
||||
paidAt := ""
|
||||
if order != nil {
|
||||
orderType = order.Type
|
||||
orderStatus = order.Status
|
||||
orderStatusDesc = order.Status.Description()
|
||||
paidAt = s.formatTime(order.PaidAt)
|
||||
}
|
||||
|
||||
amountPaid := int64(0)
|
||||
var snapshot any
|
||||
if orderItem := orderItemMap[orderItemKey{orderID: access.OrderID, contentID: access.ContentID}]; orderItem != nil {
|
||||
amountPaid = orderItem.AmountPaid
|
||||
snapshot = orderItem.Snapshot.Data()
|
||||
}
|
||||
|
||||
items = append(items, super_dto.SuperUserLibraryItem{
|
||||
AccessID: access.ID,
|
||||
TenantID: access.TenantID,
|
||||
ContentID: access.ContentID,
|
||||
OrderID: access.OrderID,
|
||||
OrderType: orderType,
|
||||
OrderStatus: orderStatus,
|
||||
OrderStatusDescription: orderStatusDesc,
|
||||
AmountPaid: amountPaid,
|
||||
PaidAt: paidAt,
|
||||
AccessStatus: access.Status,
|
||||
AccessStatusDescription: access.Status.Description(),
|
||||
AccessedAt: s.formatTime(access.CreatedAt),
|
||||
Content: contentItem,
|
||||
Snapshot: snapshot,
|
||||
})
|
||||
}
|
||||
|
||||
return &requests.Pager{
|
||||
Pagination: filter.Pagination,
|
||||
Total: total,
|
||||
Items: items,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *super) ListUserFavorites(ctx context.Context, userID int64, filter *super_dto.SuperUserContentActionListFilter) (*requests.Pager, error) {
|
||||
return s.listUserContentActions(ctx, userID, consts.UserContentActionTypeFavorite, filter)
|
||||
}
|
||||
@@ -3667,6 +3908,117 @@ func (s *super) filterContentIDsForUserActions(
|
||||
return ids, true, nil
|
||||
}
|
||||
|
||||
func (s *super) filterContentIDsForUserLibrary(
|
||||
ctx context.Context,
|
||||
filter *super_dto.SuperUserLibraryListFilter,
|
||||
) ([]int64, bool, error) {
|
||||
if filter == nil {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
tbl, q := models.ContentQuery.QueryContext(ctx)
|
||||
filterApplied := false
|
||||
|
||||
if filter.ContentID != nil && *filter.ContentID > 0 {
|
||||
q = q.Where(tbl.ID.Eq(*filter.ContentID))
|
||||
filterApplied = true
|
||||
}
|
||||
if filter.TenantID != nil && *filter.TenantID > 0 {
|
||||
q = q.Where(tbl.TenantID.Eq(*filter.TenantID))
|
||||
filterApplied = true
|
||||
}
|
||||
|
||||
tenantIDs, tenantFilter, err := s.lookupTenantIDs(ctx, filter.TenantCode, filter.TenantName)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if tenantFilter {
|
||||
filterApplied = true
|
||||
if len(tenantIDs) == 0 {
|
||||
return []int64{}, true, nil
|
||||
}
|
||||
q = q.Where(tbl.TenantID.In(tenantIDs...))
|
||||
}
|
||||
|
||||
if filter.Keyword != nil && strings.TrimSpace(*filter.Keyword) != "" {
|
||||
keyword := "%" + strings.TrimSpace(*filter.Keyword) + "%"
|
||||
q = q.Where(field.Or(
|
||||
tbl.Title.Like(keyword),
|
||||
tbl.Description.Like(keyword),
|
||||
tbl.Summary.Like(keyword),
|
||||
))
|
||||
filterApplied = true
|
||||
}
|
||||
|
||||
if !filterApplied {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
ids, err := q.PluckIDs()
|
||||
if err != nil {
|
||||
return nil, true, errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
return ids, true, nil
|
||||
}
|
||||
|
||||
func (s *super) filterOrderIDsForUserLibrary(
|
||||
ctx context.Context,
|
||||
userID int64,
|
||||
filter *super_dto.SuperUserLibraryListFilter,
|
||||
) ([]int64, bool, error) {
|
||||
if filter == nil {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
tbl, q := models.OrderQuery.QueryContext(ctx)
|
||||
q = q.Where(tbl.UserID.Eq(userID))
|
||||
filterApplied := false
|
||||
|
||||
if filter.OrderID != nil && *filter.OrderID > 0 {
|
||||
q = q.Where(tbl.ID.Eq(*filter.OrderID))
|
||||
filterApplied = true
|
||||
}
|
||||
if filter.OrderStatus != nil && *filter.OrderStatus != "" {
|
||||
q = q.Where(tbl.Status.Eq(*filter.OrderStatus))
|
||||
filterApplied = true
|
||||
}
|
||||
if filter.PaidAtFrom != nil {
|
||||
from, err := s.parseFilterTime(filter.PaidAtFrom)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if from != nil {
|
||||
q = q.Where(tbl.PaidAt.Gte(*from))
|
||||
filterApplied = true
|
||||
}
|
||||
}
|
||||
if filter.PaidAtTo != nil {
|
||||
to, err := s.parseFilterTime(filter.PaidAtTo)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if to != nil {
|
||||
q = q.Where(tbl.PaidAt.Lte(*to))
|
||||
filterApplied = true
|
||||
}
|
||||
}
|
||||
|
||||
if !filterApplied {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
orders, err := q.Select(tbl.ID).Find()
|
||||
if err != nil {
|
||||
return nil, true, errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
|
||||
ids := make([]int64, 0, len(orders))
|
||||
for _, order := range orders {
|
||||
ids = append(ids, order.ID)
|
||||
}
|
||||
return ids, true, nil
|
||||
}
|
||||
|
||||
func (s *super) lookupOrderIDsByContent(ctx context.Context, contentID *int64, contentTitle *string) ([]int64, bool, error) {
|
||||
var id int64
|
||||
if contentID != nil {
|
||||
|
||||
Reference in New Issue
Block a user