package service import ( "context" "log/slog" "git.lzcwy.cn/zhengchiliu/zt-closet-backend/global" "git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/dto" "git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/mysqldao" "git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/mysqlmodel" "git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/redisdao" "git.lzcwy.cn/zhengchiliu/zt-closet-backend/pkg/errcode" ) // NotificationService 通知业务服务。 type NotificationService struct { notificationDao *mysqldao.NotificationDao } // NewNotificationService 创建通知服务。 func NewNotificationService() *NotificationService { return &NotificationService{ notificationDao: mysqldao.NewNotificationDao(), } } // List 分页查询当前用户通知。 func (s *NotificationService) List(ctx context.Context, userID int64, request *dto.NotificationListRequest) (*dto.NotificationListResponse, *errcode.Error) { ctx, span := global.Tracer.Start(ctx, "svc.NotificationList") defer span.End() page, pageSize := normalizePage(request.Page, request.PageSize) list, total, err := s.notificationDao.NotificationListByUserID(ctx, userID, request.IsRead, request.MsgType, page, pageSize) if err != nil { return nil, errcode.DatabaseError } items := make([]dto.NotificationGetByUUIDResponse, 0, len(list)) for i := range list { items = append(items, buildNotificationResponse(&list[i])) } return &dto.NotificationListResponse{ PageResponse: dto.PageResponse{Total: total, Page: page, PageSize: pageSize}, List: items, }, nil } // GetByUUID 获取通知详情(校验归属)。 func (s *NotificationService) GetByUUID(ctx context.Context, userID int64, request *dto.NotificationGetByUUIDRequest) (*dto.NotificationGetByUUIDResponse, *errcode.Error) { ctx, span := global.Tracer.Start(ctx, "svc.NotificationGetByUUID") defer span.End() n, err := s.notificationDao.NotificationGetByUUID(ctx, request.NotificationUUID) if err != nil { if isNotFound(err) { return nil, errcode.NotificationNotFound } return nil, errcode.DatabaseError } if n.UserID != userID { return nil, errcode.Forbidden } response := buildNotificationResponse(n) return &response, nil } // Read 标记通知已读(校验归属),并同步扣减未读数。 func (s *NotificationService) Read(ctx context.Context, userID int64, request *dto.NotificationReadRequest) (*dto.NotificationUnreadResponse, *errcode.Error) { ctx, span := global.Tracer.Start(ctx, "svc.NotificationRead") defer span.End() n, err := s.notificationDao.NotificationGetByUUID(ctx, request.NotificationUUID) if err != nil { if isNotFound(err) { return nil, errcode.NotificationNotFound } return nil, errcode.DatabaseError } if n.UserID != userID { return nil, errcode.Forbidden } // 已读则直接返回当前未读数。 if n.IsRead == NotificationRead { cnt, _ := redisdao.NotificationUnreadGet(ctx, userID) return &dto.NotificationUnreadResponse{UnreadCount: cnt}, nil } // 条件标记已读(仅未读 -> 已读),防并发重复扣减未读数。 ok, err := s.notificationDao.NotificationUpdateReadByUUIDIfUnread(ctx, request.NotificationUUID) if err != nil { return nil, errcode.DatabaseError } if !ok { // 已被并发标记已读,直接返回当前未读数。 cnt, _ := redisdao.NotificationUnreadGet(ctx, userID) return &dto.NotificationUnreadResponse{UnreadCount: cnt}, nil } cnt, _ := redisdao.NotificationUnreadDecr(ctx, userID) return &dto.NotificationUnreadResponse{UnreadCount: cnt}, nil } // Unread 获取当前用户未读数,Redis 优先、回落 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 } // Redis 为 0 或异常,回落 MySQL 统计。 dbCnt, derr := s.notificationDao.NotificationCountUnread(ctx, userID) if derr != nil { return nil, errcode.DatabaseError } // 回填 Redis 未读数缓存,避免 key 过期导致后续标记已读时扣减被吞。 _ = redisdao.NotificationUnreadSet(ctx, userID, dbCnt) return &dto.NotificationUnreadResponse{UnreadCount: dbCnt}, nil } // buildNotificationResponse 将通知模型映射为响应。 func buildNotificationResponse(n *mysqlmodel.UserNotification) dto.NotificationGetByUUIDResponse { return dto.NotificationGetByUUIDResponse{ NotificationUUID: n.NotificationUUID, MsgType: n.MsgType, Title: n.Title, Content: n.Content, IsRead: n.IsRead, CreatedAt: n.CreatedAt, } } // sendNotification 发送一条通知并同步递增接收方未读数(供 like/order 等复用,best-effort)。 func sendNotification(ctx context.Context, recipientUserID, senderID int64, msgType int8, title, content string) { dao := mysqldao.NewNotificationDao() n := &mysqlmodel.UserNotification{ NotificationUUID: genUUID(), UserID: recipientUserID, MsgType: msgType, SenderID: senderID, Title: title, Content: content, IsRead: NotificationUnread, } if err := dao.NotificationCreate(ctx, n); err != nil { slog.WarnContext(ctx, "create notification failed", "user_id", recipientUserID, "msg_type", msgType, "error", err) return } if _, err := redisdao.NotificationUnreadIncr(ctx, recipientUserID); err != nil { slog.ErrorContext(ctx, "notification unread incr failed", "user_id", recipientUserID, "error", err) } }