This commit is contained in:
2026-09-15 16:02:45 +08:00
parent 112c5f8e46
commit ade2453083
70 changed files with 1441 additions and 707 deletions
+35 -21
View File
@@ -84,43 +84,57 @@ func (s *NotificationService) Read(ctx context.Context, userID int64, request *d
// 已读则直接返回当前未读数。
if n.IsRead == NotificationRead {
cnt, _ := redisdao.NotificationUnreadGet(ctx, userID)
cnt, cerr := s.unreadCount(ctx, userID)
if cerr != nil {
return nil, cerr
}
return &dto.NotificationUnreadResponse{UnreadCount: cnt}, nil
}
// 条件标记已读(仅未读 -> 已读),防并发重复扣减未读数。
ok, err := s.notificationDao.NotificationUpdateReadByUUIDIfUnread(ctx, request.NotificationUUID)
if err != nil {
// 条件标记已读(仅未读 -> 已读),防并发重复标记。
if _, err := s.notificationDao.NotificationUpdateReadByUUIDIfUnread(ctx, request.NotificationUUID); err != nil {
return nil, errcode.DatabaseError
}
if !ok {
// 已被并发标记已读,直接返回当前未读数。
cnt, _ := redisdao.NotificationUnreadGet(ctx, userID)
return &dto.NotificationUnreadResponse{UnreadCount: cnt}, nil
// 无论本次是否真的标记成功,都按 DB 重新统计并回填缓存:
// Redis 计数在 key 过期后会被 INCR 从 1 重建,直接返回 Redis 的 decr 结果会长期少报。
cnt, cerr := s.unreadCount(ctx, userID)
if cerr != nil {
return nil, cerr
}
cnt, _ := redisdao.NotificationUnreadDecr(ctx, userID)
return &dto.NotificationUnreadResponse{UnreadCount: cnt}, nil
}
// Unread 获取当前用户未读数,Redis 优先、回落 MySQL。
// Unread 获取当前用户未读数(以 MySQL 为权威)。
func (s *NotificationService) Unread(ctx context.Context, userID int64) (*dto.NotificationUnreadResponse, *errcode.Error) {
ctx, span := global.Tracer.Start(ctx, "svc.NotificationUnread")
defer span.End()
cnt, err := redisdao.NotificationUnreadGet(ctx, userID)
if err == nil && cnt > 0 {
return &dto.NotificationUnreadResponse{UnreadCount: cnt}, nil
cnt, cerr := s.unreadCount(ctx, userID)
if cerr != nil {
return nil, cerr
}
return &dto.NotificationUnreadResponse{UnreadCount: cnt}, nil
}
// Redis 为 0 或异常,回落 MySQL 统计。
dbCnt, derr := s.notificationDao.NotificationCountUnread(ctx, userID)
if derr != nil {
return nil, errcode.DatabaseError
// unreadCount 以 MySQL 为权威统计未读数并回填 Redis 缓存。
// 此前「Redis 优先、只有 0 或异常才回源」的策略在 key 过期后会长期少报
//(key 过期 → 新通知 INCR 从 1 开始 → Unread 读到正数就不再回源),改为 DB 权威。
// DB 异常时才用 Redis 兜底,保证降级可用性。
func (s *NotificationService) unreadCount(ctx context.Context, userID int64) (int64, *errcode.Error) {
cnt, err := s.notificationDao.NotificationCountUnread(ctx, userID)
if err != nil {
if rcnt, rerr := redisdao.NotificationUnreadGet(ctx, userID); rerr == nil {
slog.WarnContext(ctx, "count unread from db failed, fallback to redis",
"user_id", userID, "error", err)
return rcnt, nil
}
return 0, errcode.DatabaseError
}
// 回填 Redis 未读数缓存,避免 key 过期导致后续标记已读时扣减被吞。
_ = redisdao.NotificationUnreadSet(ctx, userID, dbCnt)
return &dto.NotificationUnreadResponse{UnreadCount: dbCnt}, nil
if serr := redisdao.NotificationUnreadSet(ctx, userID, cnt); serr != nil {
slog.WarnContext(ctx, "backfill notification unread cache failed",
"user_id", userID, "error", serr)
}
return cnt, nil
}
// buildNotificationResponse 将通知模型映射为响应。