feat: add superadmin user interaction views

This commit is contained in:
2026-01-15 15:51:26 +08:00
parent b896d0fa00
commit 339fd4fb1d
10 changed files with 1463 additions and 4 deletions

View File

@@ -116,3 +116,38 @@ type SuperUserRealNameResponse struct {
// IDCardMasked 身份证号脱敏展示。
IDCardMasked string `json:"id_card_masked"`
}
// SuperUserContentActionListFilter 超管用户互动内容列表过滤条件。
type SuperUserContentActionListFilter 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"`
// CreatedAtFrom 互动时间起始RFC3339
CreatedAtFrom *string `query:"created_at_from"`
// CreatedAtTo 互动时间结束RFC3339
CreatedAtTo *string `query:"created_at_to"`
// Asc 升序字段id/created_at
Asc *string `query:"asc"`
// Desc 降序字段id/created_at
Desc *string `query:"desc"`
}
// SuperUserContentActionItem 超管用户互动内容条目。
type SuperUserContentActionItem struct {
// ActionID 互动记录ID。
ActionID int64 `json:"action_id"`
// ActionType 互动类型like/favorite
ActionType consts.UserContentActionType `json:"action_type"`
// ActionAt 互动发生时间RFC3339
ActionAt string `json:"action_at"`
// Content 互动对应内容详情(含租户与作者信息)。
Content *AdminContentItem `json:"content"`
}

View File

@@ -316,6 +316,24 @@ func (r *Routes) Register(router fiber.Router) {
PathParam[int64]("id"),
Query[dto.SuperUserCouponListFilter]("filter"),
))
r.log.Debugf("Registering route: Get /super/v1/users/:id<int>/favorites -> users.ListFavorites")
router.Get("/super/v1/users/:id<int>/favorites"[len(r.Path()):], DataFunc2(
r.users.ListFavorites,
PathParam[int64]("id"),
Query[dto.SuperUserContentActionListFilter]("filter"),
))
r.log.Debugf("Registering route: Get /super/v1/users/:id<int>/following -> users.ListFollowing")
router.Get("/super/v1/users/:id<int>/following"[len(r.Path()):], DataFunc2(
r.users.ListFollowing,
PathParam[int64]("id"),
Query[dto.SuperUserTenantListFilter]("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,
PathParam[int64]("id"),
Query[dto.SuperUserContentActionListFilter]("filter"),
))
r.log.Debugf("Registering route: Get /super/v1/users/:id<int>/notifications -> users.ListNotifications")
router.Get("/super/v1/users/:id<int>/notifications"[len(r.Path()):], DataFunc2(
r.users.ListNotifications,

View File

@@ -127,6 +127,60 @@ func (c *users) ListTenants(ctx fiber.Ctx, id int64, filter *dto.SuperUserTenant
return services.Super.ListUserTenants(ctx, id, filter)
}
// List user favorites
//
// @Router /super/v1/users/:id<int>/favorites [get]
// @Summary List user favorites
// @Description List user's favorited contents
// @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.SuperUserContentActionItem}
// @Bind id path
// @Bind filter query
func (c *users) ListFavorites(ctx fiber.Ctx, id int64, filter *dto.SuperUserContentActionListFilter) (*requests.Pager, error) {
return services.Super.ListUserFavorites(ctx, id, filter)
}
// List user likes
//
// @Router /super/v1/users/:id<int>/likes [get]
// @Summary List user likes
// @Description List user's liked contents
// @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.SuperUserContentActionItem}
// @Bind id path
// @Bind filter query
func (c *users) ListLikes(ctx fiber.Ctx, id int64, filter *dto.SuperUserContentActionListFilter) (*requests.Pager, error) {
return services.Super.ListUserLikes(ctx, id, filter)
}
// List user following tenants
//
// @Router /super/v1/users/:id<int>/following [get]
// @Summary List user following tenants
// @Description List tenants followed by 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.UserTenantItem}
// @Bind id path
// @Bind filter query
func (c *users) ListFollowing(ctx fiber.Ctx, id int64, filter *dto.SuperUserTenantListFilter) (*requests.Pager, error) {
return services.Super.ListUserFollowing(ctx, id, filter)
}
// Update user status
//
// @Router /super/v1/users/:id<int>/status [patch]

View File

@@ -1704,6 +1704,176 @@ func (s *super) ListUserTenants(ctx context.Context, userID int64, filter *super
}, 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)
}
func (s *super) ListUserLikes(ctx context.Context, userID int64, filter *super_dto.SuperUserContentActionListFilter) (*requests.Pager, error) {
return s.listUserContentActions(ctx, userID, consts.UserContentActionTypeLike, filter)
}
func (s *super) ListUserFollowing(ctx context.Context, userID int64, filter *super_dto.SuperUserTenantListFilter) (*requests.Pager, error) {
if filter == nil {
filter = &super_dto.SuperUserTenantListFilter{}
}
// 关注列表默认只展示普通成员关注关系。
role := consts.TenantUserRoleMember
filter.Role = &role
return s.ListUserTenants(ctx, userID, filter)
}
func (s *super) listUserContentActions(
ctx context.Context,
userID int64,
actionType consts.UserContentActionType,
filter *super_dto.SuperUserContentActionListFilter,
) (*requests.Pager, error) {
if userID == 0 {
return nil, errorx.ErrBadRequest.WithMsg("用户ID不能为空")
}
if filter == nil {
filter = &super_dto.SuperUserContentActionListFilter{}
}
tbl, q := models.UserContentActionQuery.QueryContext(ctx)
q = q.Where(tbl.UserID.Eq(userID), tbl.Type.Eq(string(actionType)))
contentIDs, contentFilter, err := s.filterContentIDsForUserActions(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.SuperUserContentActionItem{},
}, nil
}
q = q.Where(tbl.ContentID.In(contentIDs...))
}
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))
}
}
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.SuperUserContentActionItem{},
}, nil
}
contentIDSet := make(map[int64]struct{}, len(list))
contentIDs = make([]int64, 0, len(list))
for _, action := range list {
if action.ContentID == 0 {
continue
}
if _, ok := contentIDSet[action.ContentID]; ok {
continue
}
contentIDSet[action.ContentID] = struct{}{}
contentIDs = append(contentIDs, action.ContentID)
}
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
}
items := make([]super_dto.SuperUserContentActionItem, 0, len(list))
for _, action := range list {
var contentItem *super_dto.AdminContentItem
if content := contentMap[action.ContentID]; content != nil {
item := s.toSuperContentItem(content, priceMap[content.ID], tenantMap[content.TenantID])
contentItem = &item
}
items = append(items, super_dto.SuperUserContentActionItem{
ActionID: action.ID,
ActionType: actionType,
ActionAt: s.formatTime(action.CreatedAt),
Content: contentItem,
})
}
return &requests.Pager{
Pagination: filter.Pagination,
Total: total,
Items: items,
}, nil
}
func (s *super) ListContents(ctx context.Context, filter *super_dto.SuperContentListFilter) (*requests.Pager, error) {
tbl, q := models.ContentQuery.QueryContext(ctx)
@@ -3444,6 +3614,59 @@ func (s *super) lookupUserIDs(ctx context.Context, username *string) ([]int64, b
return ids, true, nil
}
func (s *super) filterContentIDsForUserActions(
ctx context.Context,
filter *super_dto.SuperUserContentActionListFilter,
) ([]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) lookupOrderIDsByContent(ctx context.Context, contentID *int64, contentTitle *string) ([]int64, bool, error) {
var id int64
if contentID != nil {