feat: Studio 概览/分析补上阅读量 + 详情页 meta 行改版

- StudioOverview 新增 TotalViews 字段,GetOverviewByUserID 查询含 SUM(views_count)
- 新增 ViewTrendStore 接口和 AggregateViewsByAuthor,UNION ALL 合并已登录+访客阅读日志按日期聚合
- 数据分析页面新增阅读量趋势折线图(绿色,Chart.js)
- 概览和数据分析页面新增总阅读量统计卡片
- 详情页 meta 行改为标签格式:作者/阅读/点赞/收藏/评论/发布时间
- 移除详情页公开状态徽章,已锁定+理由仅作者可见
This commit is contained in:
2026-06-02 23:26:48 +08:00
parent ea2f196ff7
commit fcff28ab60
8 changed files with 88 additions and 18 deletions

View File

@ -4,6 +4,7 @@ import (
"time"
"metazone.cc/metalab/internal/model"
"metazone.cc/metalab/internal/service"
"gorm.io/gorm"
"gorm.io/gorm/clause"
@ -40,10 +41,11 @@ type overviewRow struct {
PendingCount int64
ApprovedCount int64
RejectedCount int64
TotalViews int64
}
// GetOverviewByUserID 用单次 GROUP BY 查询聚合用户各状态文章数
func (r *PostRepo) GetOverviewByUserID(userID uint) (totalPosts, draftCount, pendingCount, approvedCount, rejectedCount int64, err error) {
// GetOverviewByUserID 用单次 GROUP BY 查询聚合用户各状态文章数 + 总阅读量
func (r *PostRepo) GetOverviewByUserID(userID uint) (totalPosts, draftCount, pendingCount, approvedCount, rejectedCount, totalViews int64, err error) {
var row overviewRow
err = r.db.Model(&model.Post{}).
Select(`
@ -51,11 +53,12 @@ func (r *PostRepo) GetOverviewByUserID(userID uint) (totalPosts, draftCount, pen
COALESCE(SUM(CASE WHEN status = 'draft' THEN 1 ELSE 0 END), 0) AS draft_count,
COALESCE(SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END), 0) AS pending_count,
COALESCE(SUM(CASE WHEN status = 'approved' THEN 1 ELSE 0 END), 0) AS approved_count,
COALESCE(SUM(CASE WHEN status = 'rejected' THEN 1 ELSE 0 END), 0) AS rejected_count
COALESCE(SUM(CASE WHEN status = 'rejected' THEN 1 ELSE 0 END), 0) AS rejected_count,
COALESCE(SUM(views_count), 0) AS total_views
`).
Where("user_id = ? AND deleted_at IS NULL", userID).
Scan(&row).Error
return row.TotalPosts, row.DraftCount, row.PendingCount, row.ApprovedCount, row.RejectedCount, err
return row.TotalPosts, row.DraftCount, row.PendingCount, row.ApprovedCount, row.RejectedCount, row.TotalViews, err
}
// readCooldown 两次阅读记录最小间隔
@ -106,3 +109,26 @@ func (r *PostRepo) IncrementViewsCount(postID uint) error {
return r.db.Model(&model.Post{}).Where("id = ?", postID).
Update("views_count", gorm.Expr("views_count + 1")).Error
}
// AggregateViewsByAuthor 按日期聚合阅读量(作者所有文章在时间段内的阅读记录,含已登录+访客)
func (r *PostRepo) AggregateViewsByAuthor(userID uint, since string) ([]service.TrendPoint, error) {
var results []service.TrendPoint
err := r.db.Raw(`
SELECT TO_CHAR(t.read_at, 'YYYY-MM-DD') AS date, COUNT(*) AS value
FROM (
SELECT prl.read_at FROM post_read_logs prl
JOIN posts p ON p.id = prl.post_id
WHERE p.user_id = ? AND prl.read_at >= ?
UNION ALL
SELECT pgrl.read_at FROM post_guest_read_logs pgrl
JOIN posts p ON p.id = pgrl.post_id
WHERE p.user_id = ? AND pgrl.read_at >= ?
) t
GROUP BY TO_CHAR(t.read_at, 'YYYY-MM-DD')
ORDER BY date ASC
`, userID, since, userID, since).Scan(&results).Error
if results == nil {
results = []service.TrendPoint{}
}
return results, err
}

View File

@ -98,7 +98,7 @@ func buildDeps(db *gorm.DB, cfg *config.Config, siteSettings *config.SiteSetting
spaceCtrl.WithFavoriteService(favoriteService)
// Studio 趋势图
trendsService := service.NewTrendsService(energyRepo, commentRepo, reactionRepo, favoriteRepo)
trendsService := service.NewTrendsService(energyRepo, commentRepo, reactionRepo, favoriteRepo, postRepo)
studioCtrl.WithTrendsService(trendsService)
maintenanceMdw := middleware.NewMaintenanceMiddleware(cfg, siteSettings, sessionMgr)

View File

@ -132,11 +132,12 @@ type StudioOverview struct {
PendingCount int64 `json:"pending_count"`
ApprovedCount int64 `json:"approved_count"`
RejectedCount int64 `json:"rejected_count"`
TotalViews int64 `json:"total_views"`
}
// GetOverview 获取创作中心数据概览(单次 GROUP BY 查询)
func (s *PostService) GetOverview(userID uint) (*StudioOverview, error) {
totalPosts, draftCount, pendingCount, approvedCount, rejectedCount, err := s.repo.GetOverviewByUserID(userID)
totalPosts, draftCount, pendingCount, approvedCount, rejectedCount, totalViews, err := s.repo.GetOverviewByUserID(userID)
if err != nil {
return nil, err
}
@ -146,6 +147,7 @@ func (s *PostService) GetOverview(userID uint) (*StudioOverview, error) {
PendingCount: pendingCount,
ApprovedCount: approvedCount,
RejectedCount: rejectedCount,
TotalViews: totalViews,
}, nil
}

View File

@ -37,7 +37,7 @@ type postStore interface {
FindPageable(keyword string, offset, limit int) ([]model.Post, int64, error)
FindAdminPageable(keyword, status string, offset, limit int) ([]model.Post, int64, error)
FindByUserIDAndStatus(userID uint, status string, offset, limit int) ([]model.Post, int64, error)
GetOverviewByUserID(userID uint) (totalPosts, draftCount, pendingCount, approvedCount, rejectedCount int64, err error)
GetOverviewByUserID(userID uint) (totalPosts, draftCount, pendingCount, approvedCount, rejectedCount, totalViews int64, err error)
FindPendingRevisions(keyword string, offset, limit int) ([]model.Post, int64, error)
Update(post *model.Post) error
SoftDelete(id uint) error
@ -231,3 +231,8 @@ type LikeTrendStore interface {
type FavoriteTrendStore interface {
AggregateFavoritesByAuthor(userID uint, since string) ([]TrendPoint, error)
}
// ViewTrendStore 阅读量趋势聚合ISP
type ViewTrendStore interface {
AggregateViewsByAuthor(userID uint, since string) ([]TrendPoint, error)
}

View File

@ -12,6 +12,7 @@ type TrendResult struct {
Comments []TrendPoint `json:"comments"`
Likes []TrendPoint `json:"likes"`
Favorites []TrendPoint `json:"favorites"`
Views []TrendPoint `json:"views"`
}
// TrendsService Studio 趋势分析服务
@ -20,15 +21,17 @@ type TrendsService struct {
commentRepo CommentTrendStore
likeRepo LikeTrendStore
favoriteRepo FavoriteTrendStore
viewRepo ViewTrendStore
}
// NewTrendsService 构造函数
func NewTrendsService(energyRepo EnergyTrendStore, commentRepo CommentTrendStore, likeRepo LikeTrendStore, favoriteRepo FavoriteTrendStore) *TrendsService {
func NewTrendsService(energyRepo EnergyTrendStore, commentRepo CommentTrendStore, likeRepo LikeTrendStore, favoriteRepo FavoriteTrendStore, viewRepo ViewTrendStore) *TrendsService {
return &TrendsService{
energyRepo: energyRepo,
commentRepo: commentRepo,
likeRepo: likeRepo,
favoriteRepo: favoriteRepo,
viewRepo: viewRepo,
}
}
@ -73,6 +76,15 @@ func (s *TrendsService) GetTrends(userID uint, since string) (*TrendResult, erro
return nil
})
g.Go(func() error {
var err error
result.Views, err = s.viewRepo.AggregateViewsByAuthor(userID, since)
if err != nil {
return fmt.Errorf("查询阅读趋势: %w", err)
}
return nil
})
if err := g.Wait(); err != nil {
return nil, err
}