feat: 首页增加内存缓存(30s TTL)

- 新增 internal/cache/home_cache.go,实现带 TTL 的首页文章列表内存缓存
- frontend.go 首页 handler 优先读取缓存,未命中/过期时查询 DB 并回填
- PostService 注入失效回调,在 Create/Update/Delete/Approve 后自动失效缓存
- 减少首页每次请求都查 DB 的开销
This commit is contained in:
2026-06-02 17:20:59 +08:00
parent 3e0fed80ce
commit 593b52096f
6 changed files with 110 additions and 11 deletions

55
internal/cache/home_cache.go vendored Normal file
View File

@ -0,0 +1,55 @@
package cache
import (
"sync"
"time"
"metazone.cc/metalab/internal/model"
)
// HomePageCache 首页内存缓存30 秒 TTL创建/更新/删除/审核通过时失效
type HomePageCache struct {
mu sync.RWMutex
posts []model.Post
total int64
expires time.Time
ttl time.Duration
}
// NewHomePageCache 创建首页缓存ttl 为缓存有效期
func NewHomePageCache(ttl time.Duration) *HomePageCache {
return &HomePageCache{ttl: ttl}
}
// Get 返回缓存数据ok=false 表示缓存未命中或已过期
func (c *HomePageCache) Get() (posts []model.Post, total int64, ok bool) {
c.mu.RLock()
defer c.mu.RUnlock()
if time.Now().After(c.expires) {
return nil, 0, false
}
// 返回浅拷贝以防止调用方修改缓存内部数据
copied := make([]model.Post, len(c.posts))
copy(copied, c.posts)
return copied, c.total, true
}
// Set 写入缓存数据
func (c *HomePageCache) Set(posts []model.Post, total int64) {
c.mu.Lock()
defer c.mu.Unlock()
// 深拷贝一份避免外部修改影响缓存
c.posts = make([]model.Post, len(posts))
copy(c.posts, posts)
c.total = total
c.expires = time.Now().Add(c.ttl)
}
// Invalidate 主动失效缓存
func (c *HomePageCache) Invalidate() {
c.mu.Lock()
defer c.mu.Unlock()
c.posts = nil
c.total = 0
c.expires = time.Time{}
}

View File

@ -4,6 +4,7 @@ import (
"log" "log"
"time" "time"
"metazone.cc/metalab/internal/cache"
"metazone.cc/metalab/internal/config" "metazone.cc/metalab/internal/config"
"metazone.cc/metalab/internal/controller" "metazone.cc/metalab/internal/controller"
adminCtrl "metazone.cc/metalab/internal/controller/admin" adminCtrl "metazone.cc/metalab/internal/controller/admin"
@ -42,6 +43,7 @@ type dependencies struct {
favoriteCtrl *controller.FavoriteController favoriteCtrl *controller.FavoriteController
favoriteService *service.FavoriteService favoriteService *service.FavoriteService
trendsService *service.TrendsService trendsService *service.TrendsService
homeCache *cache.HomePageCache
} }
func buildDepsCore(db *gorm.DB, cfg *config.Config, siteSettings *config.SiteSettings, redisClient *redis.Client) ( func buildDepsCore(db *gorm.DB, cfg *config.Config, siteSettings *config.SiteSettings, redisClient *redis.Client) (

View File

@ -1,6 +1,9 @@
package router package router
import ( import (
"time"
"metazone.cc/metalab/internal/cache"
"metazone.cc/metalab/internal/config" "metazone.cc/metalab/internal/config"
"metazone.cc/metalab/internal/controller" "metazone.cc/metalab/internal/controller"
adminCtrl "metazone.cc/metalab/internal/controller/admin" adminCtrl "metazone.cc/metalab/internal/controller/admin"
@ -99,6 +102,10 @@ func buildDeps(db *gorm.DB, cfg *config.Config, siteSettings *config.SiteSetting
maintenanceMdw := middleware.NewMaintenanceMiddleware(cfg, siteSettings, sessionMgr) maintenanceMdw := middleware.NewMaintenanceMiddleware(cfg, siteSettings, sessionMgr)
// 首页内存缓存30s TTL文章变更时失效
homeCache := cache.NewHomePageCache(30 * time.Second)
postService.WithHomePageInvalidator(func() { homeCache.Invalidate() })
return &dependencies{ return &dependencies{
authCtrl: authCtrl, authMdw: authMdw, maintenanceMdw: maintenanceMdw, authCtrl: authCtrl, authMdw: authMdw, maintenanceMdw: maintenanceMdw,
settingsCtrl: settingsCtrl, settingsCtrl: settingsCtrl,
@ -119,5 +126,6 @@ func buildDeps(db *gorm.DB, cfg *config.Config, siteSettings *config.SiteSetting
favoriteCtrl: favoriteCtrl, favoriteCtrl: favoriteCtrl,
favoriteService: favoriteService, favoriteService: favoriteService,
trendsService: trendsService, trendsService: trendsService,
homeCache: homeCache,
} }
} }

View File

@ -22,10 +22,16 @@ func setupFrontendRoutes(r *gin.Engine, cfg *config.Config, d *dependencies) {
}) })
{ {
pages.GET("/", func(c *gin.Context) { pages.GET("/", func(c *gin.Context) {
posts, total, err := d.postService.List("", 1, 6) posts, total, ok := d.homeCache.Get()
if err != nil { if !ok {
posts = nil var err error
total = 0 posts, total, err = d.postService.List("", 1, 6)
if err != nil {
posts = nil
total = 0
} else {
d.homeCache.Set(posts, total)
}
} }
c.HTML(http.StatusOK, "home/index.html", common.BuildPageData(c, gin.H{ c.HTML(http.StatusOK, "home/index.html", common.BuildPageData(c, gin.H{
"Title": "首页", "Title": "首页",

View File

@ -21,7 +21,11 @@ func (s *PostService) Update(postID uint, title, body string) error {
post.PendingTitle = title post.PendingTitle = title
post.PendingBody = body post.PendingBody = body
post.RejectReason = "" // 清空旧退回理由 post.RejectReason = "" // 清空旧退回理由
return s.repo.Update(post) err := s.repo.Update(post)
if err == nil {
s.invalidateHomeCache()
}
return err
} }
// 草稿/已退回:直接修改原文(现有逻辑) // 草稿/已退回:直接修改原文(现有逻辑)
@ -34,12 +38,20 @@ func (s *PostService) Update(postID uint, title, body string) error {
post.RejectReason = "" post.RejectReason = ""
} }
return s.repo.Update(post) err = s.repo.Update(post)
if err == nil {
s.invalidateHomeCache()
}
return err
} }
// Delete 软删除(权限在 controller 层检查) // Delete 软删除(权限在 controller 层检查)
func (s *PostService) Delete(postID uint) error { func (s *PostService) Delete(postID uint) error {
return s.repo.SoftDelete(postID) if err := s.repo.SoftDelete(postID); err != nil {
return err
}
s.invalidateHomeCache()
return nil
} }
// SubmitForAudit 提交审核draft → pending // SubmitForAudit 提交审核draft → pending
@ -79,6 +91,7 @@ func (s *PostService) Approve(postID uint) error {
if err := s.repo.Update(post); err != nil { if err := s.repo.Update(post); err != nil {
return err return err
} }
s.invalidateHomeCache()
// 通知作者修订审核通过 // 通知作者修订审核通过
if s.notifier != nil { if s.notifier != nil {
s.notifier.NotifyPostApproved(post.UserID, post.Title, post.ID) s.notifier.NotifyPostApproved(post.UserID, post.Title, post.ID)
@ -95,6 +108,7 @@ func (s *PostService) Approve(postID uint) error {
if err := s.repo.Update(post); err != nil { if err := s.repo.Update(post); err != nil {
return err return err
} }
s.invalidateHomeCache()
// 通知作者审核通过 // 通知作者审核通过
if s.notifier != nil { if s.notifier != nil {
s.notifier.NotifyPostApproved(post.UserID, post.Title, post.ID) s.notifier.NotifyPostApproved(post.UserID, post.Title, post.ID)

View File

@ -13,10 +13,11 @@ import (
// PostService 帖子业务逻辑 // PostService 帖子业务逻辑
type PostService struct { type PostService struct {
repo postStore repo postStore
ss *config.SiteSettings ss *config.SiteSettings
notifier postNotifier notifier postNotifier
userRepo userExpReader userRepo userExpReader
onHomePageChanged func()
} }
// NewPostService 构造函数 // NewPostService 构造函数
@ -29,6 +30,11 @@ func NewPostService(repo postStore, ss *config.SiteSettings, notifier postNotifi
} }
} }
// WithHomePageInvalidator 注入首页缓存失效回调(文章变更时触发)
func (s *PostService) WithHomePageInvalidator(fn func()) {
s.onHomePageChanged = fn
}
// auditEnabled 审核是否开启 // auditEnabled 审核是否开启
func (s *PostService) auditEnabled() bool { func (s *PostService) auditEnabled() bool {
return s.ss.IsAuditEnabled() return s.ss.IsAuditEnabled()
@ -78,6 +84,7 @@ func (s *PostService) Create(userID uint, title, body string) (*model.Post, erro
if err := s.repo.Create(post); err != nil { if err := s.repo.Create(post); err != nil {
return nil, err return nil, err
} }
s.invalidateHomeCache()
return post, nil return post, nil
} }
@ -156,6 +163,13 @@ func (s *PostService) CanCreatePost(userID uint) bool {
return user.Exp >= 1 return user.Exp >= 1
} }
// invalidateHomeCache 失效首页缓存(异步安全调用)
func (s *PostService) invalidateHomeCache() {
if s.onHomePageChanged != nil {
s.onHomePageChanged()
}
}
// RecordRead 记录已登录用户阅读文章(已登录去重 + 防刷冷却) // RecordRead 记录已登录用户阅读文章(已登录去重 + 防刷冷却)
func (s *PostService) RecordRead(userID, postID uint) { func (s *PostService) RecordRead(userID, postID uint) {
firstRead, err := s.repo.RecordRead(userID, postID) firstRead, err := s.repo.RecordRead(userID, postID)