diff --git a/cmd/api/main.go b/cmd/api/main.go index ea61fc9..c6c8d5d 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -121,6 +121,10 @@ func main() { expiryRepo := expiry.NewRepository(database.DB) expiryService := expiry.NewService(expiryRepo) + if redisClient != nil { + expirySummaryCache := expiry.NewSummaryCache(redisClient.Redis(), redisClient.KeyPrefix(), 5*time.Minute) + expiryService.BindSummaryCache(expirySummaryCache) + } expiryHandler := expiry.NewHandler(expiryService) // 6) 注册路由:把 URL 映射到 handler diff --git a/docs/expiry/reports/performance_optimization_2026-03-04.md b/docs/expiry/reports/performance_optimization_2026-03-04.md new file mode 100644 index 0000000..0b85447 --- /dev/null +++ b/docs/expiry/reports/performance_optimization_2026-03-04.md @@ -0,0 +1,36 @@ +# 性能优化说明(2026-03-04) + +## 后端优化 + +### 1. summary Redis 缓存(5分钟) +- 缓存 key: `expiry_summary:{user_id}` +- 缓存时机:`GetSummary` +- 失效时机: + - 创建物品 + - 更新物品 + - 删除物品 + - 标记状态(used/discarded) + +### 2. 查询路径优化 +- 继续使用以下索引支撑核心查询: + - `idx_user_expiry (user_id, expiry_date)` + - `idx_user_category (user_id, category)` + - `idx_user_status (user_id, status)` +- 修复汇总统计中基查询复用导致的统计偏差风险。 + +## 前端优化 + +### 1. 列表滚动优化 +- 列表采用分页加载(`page/page_size`)与上拉触底加载,避免一次性渲染大量节点。 +- 筛选与排序切换时采用增量刷新,降低页面抖动。 + +### 2. 首页加载优化 +- 首页仅请求 `summary` + `expiring` 前 10 条,减少首屏数据量。 + +## 验证建议 +1. 在压测环境执行: +```bash +hey -n 100 -c 20 -H "Authorization: Bearer " http:///api/expiry/summary +``` +2. 观察均值响应时间与 P95。 +3. 观察 Redis 命中率与数据库 QPS 变化。 diff --git a/internal/expiry/cache.go b/internal/expiry/cache.go new file mode 100644 index 0000000..5d119b0 --- /dev/null +++ b/internal/expiry/cache.go @@ -0,0 +1,84 @@ +package expiry + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "strings" + "time" + + "github.com/redis/go-redis/v9" +) + +// SummaryCache 封装 summary 接口的 Redis 缓存。 +type SummaryCache struct { + rc *redis.Client + keyPrefix string + ttl time.Duration +} + +func NewSummaryCache(rc *redis.Client, keyPrefix string, ttl time.Duration) *SummaryCache { + if ttl <= 0 { + ttl = 5 * time.Minute + } + return &SummaryCache{ + rc: rc, + keyPrefix: keyPrefix, + ttl: ttl, + } +} + +func (c *SummaryCache) Get(ctx context.Context, userID uint) (*SummaryResponse, bool, error) { + if c == nil || c.rc == nil || userID == 0 { + return nil, false, nil + } + + val, err := c.rc.Get(ctx, c.summaryKey(userID)).Bytes() + if err != nil { + if errors.Is(err, redis.Nil) { + return nil, false, nil + } + return nil, false, fmt.Errorf("redis get summary: %w", err) + } + + var summary SummaryResponse + if err := json.Unmarshal(val, &summary); err != nil { + _ = c.rc.Del(ctx, c.summaryKey(userID)).Err() + return nil, false, nil + } + + return &summary, true, nil +} + +func (c *SummaryCache) Set(ctx context.Context, userID uint, summary *SummaryResponse) error { + if c == nil || c.rc == nil || userID == 0 || summary == nil { + return nil + } + body, err := json.Marshal(summary) + if err != nil { + return fmt.Errorf("marshal summary: %w", err) + } + if err := c.rc.Set(ctx, c.summaryKey(userID), body, c.ttl).Err(); err != nil { + return fmt.Errorf("redis set summary: %w", err) + } + return nil +} + +func (c *SummaryCache) Delete(ctx context.Context, userID uint) error { + if c == nil || c.rc == nil || userID == 0 { + return nil + } + if err := c.rc.Del(ctx, c.summaryKey(userID)).Err(); err != nil { + return fmt.Errorf("redis del summary: %w", err) + } + return nil +} + +func (c *SummaryCache) summaryKey(userID uint) string { + prefix := c.keyPrefix + if prefix != "" && !strings.HasSuffix(prefix, ":") { + prefix += ":" + } + return fmt.Sprintf("%sexpiry_summary:%d", prefix, userID) +} diff --git a/internal/expiry/service.go b/internal/expiry/service.go index a1d31d2..645e6d4 100644 --- a/internal/expiry/service.go +++ b/internal/expiry/service.go @@ -1,6 +1,7 @@ package expiry import ( + "context" "errors" "strings" "time" @@ -24,7 +25,8 @@ var ( // Service 封装保质期模块业务逻辑。 type Service struct { - repo *Repository + repo *Repository + summaryCache *SummaryCache } // CreateItemRequest 创建物品请求。 @@ -96,6 +98,11 @@ func NewService(repo *Repository) *Service { return &Service{repo: repo} } +// BindSummaryCache 绑定 summary 的 Redis 缓存(可选)。 +func (s *Service) BindSummaryCache(cache *SummaryCache) { + s.summaryCache = cache +} + // CreateItem 创建物品,并处理过期日期自动计算。 func (s *Service) CreateItem(userID uint, req CreateItemRequest) (*ExpiryItem, error) { if req.MiniProgramID == 0 { @@ -135,6 +142,7 @@ func (s *Service) CreateItem(userID uint, req CreateItemRequest) (*ExpiryItem, e if err := s.repo.Create(item); err != nil { return nil, err } + s.invalidateSummaryCache(userID) item.Status = item.CalculateStatus() return item, nil @@ -186,6 +194,7 @@ func (s *Service) UpdateItem(id, userID uint, req UpdateItemRequest) (*ExpiryIte if err := s.repo.Update(item); err != nil { return nil, err } + s.invalidateSummaryCache(userID) item.Status = item.CalculateStatus() return item, nil @@ -193,7 +202,11 @@ func (s *Service) UpdateItem(id, userID uint, req UpdateItemRequest) (*ExpiryIte // DeleteItem 删除物品。 func (s *Service) DeleteItem(id, userID uint) error { - return s.repo.Delete(id, userID) + if err := s.repo.Delete(id, userID); err != nil { + return err + } + s.invalidateSummaryCache(userID) + return nil } // GetItem 获取单个物品。 @@ -237,19 +250,26 @@ func (s *Service) GetItems(userID uint, filters ItemFilters) (*ItemListResponse, // GetSummary 获取首页汇总。 func (s *Service) GetSummary(userID uint) (*SummaryResponse, error) { + if cached, ok := s.getSummaryFromCache(userID); ok { + return cached, nil + } + data, err := s.repo.GetSummary(userID) if err != nil { return nil, err } - return &SummaryResponse{ + summary := &SummaryResponse{ TotalItems: data["total_items"], ExpiringSoon: data["expiring_soon"], Expired: data["expired"], Normal: data["normal"], Used: data["used"], Discarded: data["discarded"], - }, nil + } + + s.setSummaryToCache(userID, summary) + return summary, nil } // UpdateItemStatus 标记物品状态。 @@ -258,7 +278,11 @@ func (s *Service) UpdateItemStatus(id, userID uint, status string) error { if status != StatusUsed && status != StatusDiscarded { return ErrExpiryStatusInvalid } - return s.repo.UpdateStatus(id, userID, status) + if err := s.repo.UpdateStatus(id, userID, status); err != nil { + return err + } + s.invalidateSummaryCache(userID) + return nil } // GetSettings 获取用户提醒设置;若未配置则返回默认值。 @@ -433,3 +457,28 @@ func validateRemindDays(days []int) ([]int, error) { return result, nil } + +func (s *Service) getSummaryFromCache(userID uint) (*SummaryResponse, bool) { + if s.summaryCache == nil { + return nil, false + } + summary, ok, err := s.summaryCache.Get(context.Background(), userID) + if err != nil || !ok || summary == nil { + return nil, false + } + return summary, true +} + +func (s *Service) setSummaryToCache(userID uint, summary *SummaryResponse) { + if s.summaryCache == nil || summary == nil { + return + } + _ = s.summaryCache.Set(context.Background(), userID, summary) +} + +func (s *Service) invalidateSummaryCache(userID uint) { + if s.summaryCache == nil { + return + } + _ = s.summaryCache.Delete(context.Background(), userID) +}