diff --git a/backend/internal/api/api.go b/backend/internal/api/api.go index 753c2b5..075fa71 100644 --- a/backend/internal/api/api.go +++ b/backend/internal/api/api.go @@ -192,6 +192,9 @@ func (a *API) getPost(w http.ResponseWriter, r *http.Request) { httpx.NotFound(w) return } + // 浏览数:详情页每被拉取一次 +1(草稿不走这里;不区分访客,博客够用)。 + // 自增失败静默——计数器丢一次无关紧要,不能让文章因此打不开。 + a.Store.BumpViewCount(p.ID) // 邻居给详情页侧栏的时间线用;取不到不该让详情页挂掉。 nb, err := a.Store.Neighbors(slug) if err != nil { diff --git a/backend/internal/model/model.go b/backend/internal/model/model.go index 52a3a4e..7df603d 100644 --- a/backend/internal/model/model.go +++ b/backend/internal/model/model.go @@ -35,6 +35,10 @@ type Post struct { // LinkCard 是正文里第一个外链的预览卡片(发布时抓一次,存 posts.link_card)。 // nil = 没有链接或抓取失败,前台不渲染卡片。 LinkCard *LinkCard `json:"link_card,omitempty"` + // CommentCount 是非删除评论数(列表接口子查询带出,操作行「回复」旁展示)。 + CommentCount int64 `json:"comment_count"` + // ViewCount 是浏览数(公开文章详情每次拉取 +1)。 + ViewCount int64 `json:"view_count"` } // LinkCard 是链接预览卡片:由 internal/linkmeta 抓取,以 JSON 存在 posts 表里。 diff --git a/backend/internal/store/store.go b/backend/internal/store/store.go index 5337502..b5118a6 100644 --- a/backend/internal/store/store.go +++ b/backend/internal/store/store.go @@ -164,6 +164,8 @@ func (s *Store) migrate() error { `ALTER TABLE posts ADD COLUMN images TEXT NOT NULL DEFAULT '[]'`, // 正文首个外链的预览卡片(og 标题/描述/封面),JSON 对象或空串 `ALTER TABLE posts ADD COLUMN link_card TEXT NOT NULL DEFAULT ''`, + // 浏览数:公开文章详情每被拉取一次 +1(不区分访客,博客场景够用) + `ALTER TABLE posts ADD COLUMN view_count INTEGER NOT NULL DEFAULT 0`, // 账号角色:owner(站主,全库唯一)/ reader(评论区访客) `ALTER TABLE users ADD COLUMN role TEXT NOT NULL DEFAULT 'reader'`, // 后台账号密码(provider=admin 的行才有值):多用户改造后密码上库, @@ -580,7 +582,9 @@ const postCols = `id, kind, title, slug, summary, cover_url, content_md, content const listCols = `id, kind, title, slug, summary, cover_url, '' AS content_md, CASE WHEN kind = 'short' THEN content_html ELSE substr(content_html, 1, 600) END AS content_html, - status, published_at, created_at, updated_at, reading_minutes, LENGTH(content_md), images, link_card` + status, published_at, created_at, updated_at, reading_minutes, LENGTH(content_md), images, link_card, + (SELECT COUNT(*) FROM comments c WHERE c.post_id = posts.id AND c.is_deleted = 0) AS comment_count, + view_count` // images 列的 JSON 编解码(列存 '[]',Go 侧 []string;坏数据静默为空)。 func decodeImages(s string) []string { @@ -634,7 +638,7 @@ func scanPost(rows interface{ Scan(...any) error }) (model.Post, error) { var imgs, card string err := rows.Scan(&p.ID, &p.Kind, &p.Title, &p.Slug, &p.Summary, &p.CoverURL, &p.ContentMd, &p.ContentHTML, &p.Status, &p.PublishedAt, &p.CreatedAt, &p.UpdatedAt, - &p.ReadingMinutes, &p.ContentLen, &imgs, &card) + &p.ReadingMinutes, &p.ContentLen, &imgs, &card, &p.CommentCount, &p.ViewCount) p.Tags = []string{} p.Images = decodeImages(imgs) p.LinkCard = decodeLinkCard(card) @@ -1473,6 +1477,11 @@ func (s *Store) Archive() ([]model.ArchiveYear, error) { return years, nil } +// BumpViewCount 浏览数自增。静默失败:计数器不该影响文章可读性。 +func (s *Store) BumpViewCount(id int64) { + _, _ = s.db.Exec(s.db.Q(`UPDATE posts SET view_count = view_count + 1 WHERE id = ?`), id) +} + func splitDate(rfc3339 string) (string, string) { if len(rfc3339) < 7 { return "", "" diff --git a/frontend/src/components/PostCard.vue b/frontend/src/components/PostCard.vue index 875ff66..fb7c021 100644 --- a/frontend/src/components/PostCard.vue +++ b/frontend/src/components/PostCard.vue @@ -1,14 +1,33 @@