159 lines
5.6 KiB
Go
159 lines
5.6 KiB
Go
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)
|
|
}
|
|
}
|