first commit

This commit is contained in:
2026-09-11 01:41:52 +08:00
commit 9f22e6ccae
263 changed files with 28935 additions and 0 deletions
+158
View File
@@ -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)
}
}