first commit
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user