From fcff28ab60e208dd63e1fd4f80c56a4453a341b5 Mon Sep 17 00:00:00 2001 From: Victor_Jay Date: Tue, 2 Jun 2026 23:26:48 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20Studio=20=E6=A6=82=E8=A7=88/=E5=88=86?= =?UTF-8?q?=E6=9E=90=E8=A1=A5=E4=B8=8A=E9=98=85=E8=AF=BB=E9=87=8F=20+=20?= =?UTF-8?q?=E8=AF=A6=E6=83=85=E9=A1=B5=20meta=20=E8=A1=8C=E6=94=B9?= =?UTF-8?q?=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - StudioOverview 新增 TotalViews 字段,GetOverviewByUserID 查询含 SUM(views_count) - 新增 ViewTrendStore 接口和 AggregateViewsByAuthor,UNION ALL 合并已登录+访客阅读日志按日期聚合 - 数据分析页面新增阅读量趋势折线图(绿色,Chart.js) - 概览和数据分析页面新增总阅读量统计卡片 - 详情页 meta 行改为标签格式:作者/阅读/点赞/收藏/评论/发布时间 - 移除详情页公开状态徽章,已锁定+理由仅作者可见 --- internal/repository/post_stats_repo.go | 34 ++++++++++++++++--- internal/router/deps_extra.go | 2 +- internal/service/post_service.go | 4 ++- internal/service/repository.go | 7 +++- internal/service/trends_service.go | 14 +++++++- templates/MetaLab-2026/html/posts/show.html | 15 ++++---- .../MetaLab-2026/html/studio/analytics.html | 23 +++++++++++-- .../MetaLab-2026/html/studio/overview.html | 7 ++++ 8 files changed, 88 insertions(+), 18 deletions(-) diff --git a/internal/repository/post_stats_repo.go b/internal/repository/post_stats_repo.go index f6fda64..0e2d91d 100644 --- a/internal/repository/post_stats_repo.go +++ b/internal/repository/post_stats_repo.go @@ -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 +} diff --git a/internal/router/deps_extra.go b/internal/router/deps_extra.go index 561709a..4a7899d 100644 --- a/internal/router/deps_extra.go +++ b/internal/router/deps_extra.go @@ -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) diff --git a/internal/service/post_service.go b/internal/service/post_service.go index 60f138a..4e90703 100644 --- a/internal/service/post_service.go +++ b/internal/service/post_service.go @@ -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 } diff --git a/internal/service/repository.go b/internal/service/repository.go index 4bc2ae4..90e98c8 100644 --- a/internal/service/repository.go +++ b/internal/service/repository.go @@ -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) +} diff --git a/internal/service/trends_service.go b/internal/service/trends_service.go index b314de5..845bf8f 100644 --- a/internal/service/trends_service.go +++ b/internal/service/trends_service.go @@ -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 } diff --git a/templates/MetaLab-2026/html/posts/show.html b/templates/MetaLab-2026/html/posts/show.html index 55e1bd7..c780817 100644 --- a/templates/MetaLab-2026/html/posts/show.html +++ b/templates/MetaLab-2026/html/posts/show.html @@ -8,13 +8,14 @@

{{.Post.Title}}

- - {{.Post.CreatedAt.Format "2006-01-02 15:04"}} - {{if ne .Post.Status "approved"}} - {{index $.StatusNames .Post.Status}} - {{end}} - {{if .Post.IsLocked}} - 已锁定 + 作者:{{if .Post.AuthorName}}{{.Post.AuthorName}}{{else}}该用户已注销{{end}} + 阅读:{{.Post.ViewsCount}} + 点赞:{{.Post.LikesCount}} + 收藏:{{.Post.FavoritesCount}} + 评论:{{.Post.CommentsCount}} + 发布时间:{{.Post.CreatedAt.Format "2006-01-02 15:04"}} + {{if and .Post.IsLocked (eq $.Post.UserID $.UID)}} + 已锁定{{if .Post.LockReason}}:{{.Post.LockReason}}{{end}} {{end}} {{if and .Post.PendingBody (eq $.Post.UserID $.UID)}} 修订审核中 diff --git a/templates/MetaLab-2026/html/studio/analytics.html b/templates/MetaLab-2026/html/studio/analytics.html index 4036c2a..3f80d92 100644 --- a/templates/MetaLab-2026/html/studio/analytics.html +++ b/templates/MetaLab-2026/html/studio/analytics.html @@ -37,6 +37,13 @@ 草稿
+
+
👁
+
+ {{.Overview.TotalViews}} + 总阅读量 +
+
@@ -78,6 +85,13 @@
+ {{end}} @@ -98,7 +112,8 @@ energy: { border: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, comments: { border: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, likes: { border: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, - favorites: { border: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.1)' } + favorites: { border: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.1)' }, + views: { border: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' } }; function fillDates(points, days) { @@ -193,8 +208,9 @@ showLoading('commentsLoading'); showLoading('likesLoading'); showLoading('favoritesLoading'); + showLoading('viewsLoading'); - var canvasIds = ['energyChart', 'commentsChart', 'likesChart', 'favoritesChart']; + var canvasIds = ['energyChart', 'commentsChart', 'likesChart', 'favoritesChart', 'viewsChart']; canvasIds.forEach(function (id) { var canvas = document.getElementById(id); canvas.style.display = 'none'; @@ -217,7 +233,8 @@ { id: 'energyChart', key: 'energy', color: chartColors.energy, label: '赋能值', loading: 'energyLoading' }, { id: 'commentsChart', key: 'comments', color: chartColors.comments, label: '评论', loading: 'commentsLoading' }, { id: 'likesChart', key: 'likes', color: chartColors.likes, label: '点赞', loading: 'likesLoading' }, - { id: 'favoritesChart', key: 'favorites', color: chartColors.favorites, label: '收藏', loading: 'favoritesLoading' } + { id: 'favoritesChart', key: 'favorites', color: chartColors.favorites, label: '收藏', loading: 'favoritesLoading' }, + { id: 'viewsChart', key: 'views', color: chartColors.views, label: '阅读', loading: 'viewsLoading' } ]; maps.forEach(function (m) { diff --git a/templates/MetaLab-2026/html/studio/overview.html b/templates/MetaLab-2026/html/studio/overview.html index c9507de..056b241 100644 --- a/templates/MetaLab-2026/html/studio/overview.html +++ b/templates/MetaLab-2026/html/studio/overview.html @@ -52,6 +52,13 @@ 已退回 +
+
👁
+
+ {{.Overview.TotalViews}} + 总阅读量 +
+