Files
2026-09-11 01:47:20 +08:00

119 lines
4.1 KiB
Go

package service
import (
"context"
"log/slog"
"strings"
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/global"
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/internal/dto"
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/internal/mysqldao"
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/internal/mysqlmodel"
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/internal/redisdao"
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/pkg/errcode"
)
// broadcastBatchSize 全量广播每批处理的用户数(分批遍历用户 + 分批插入,避免大事务与内存暴涨)。
const broadcastBatchSize = 500
// NotificationService 通知推送服务。
type NotificationService struct {
notificationDao *mysqldao.NotificationDao
userDao *mysqldao.UserDao
}
// NewNotificationService 创建通知服务。
func NewNotificationService() *NotificationService {
return &NotificationService{
notificationDao: mysqldao.NewNotificationDao(),
userDao: mysqldao.NewUserDao(),
}
}
// Send 向单个用户发送系统通知(写 user_notifications + Redis 未读计数自增)。
func (s *NotificationService) Send(ctx context.Context, adminID int64, request *dto.NotificationSendRequest) *errcode.Error {
ctx, span := global.Tracer.Start(ctx, "svc.AdminNotificationSend")
defer span.End()
// 校验接收用户存在。
if _, err := s.userDao.UserGetByID(ctx, request.UserID); err != nil {
if isNotFound(err) {
return errcode.UserNotFound
}
return errcode.DatabaseError
}
notification := buildSystemNotification(request.UserID, request.Title, request.Content)
if err := s.notificationDao.NotificationBatchCreate(ctx, []mysqlmodel.UserNotification{notification}); err != nil {
return errcode.DatabaseError
}
s.incrUnread(ctx, request.UserID)
return nil
}
// Broadcast 全量广播系统通知:分批遍历用户写入,返回发送人数。
func (s *NotificationService) Broadcast(ctx context.Context, adminID int64, request *dto.NotificationBroadcastRequest) (*dto.NotificationBroadcastResponse, *errcode.Error) {
ctx, span := global.Tracer.Start(ctx, "svc.AdminNotificationBroadcast")
defer span.End()
title := strings.TrimSpace(request.Title)
content := strings.TrimSpace(request.Content)
var sent int64
var lastID int64
for {
ids, err := s.userDao.UserIDListAll(ctx, lastID, broadcastBatchSize)
if err != nil {
slog.ErrorContext(ctx, "broadcast fetch user ids failed",
"sent", sent, "error", err)
return &dto.NotificationBroadcastResponse{SentCount: sent}, errcode.DatabaseError
}
if len(ids) == 0 {
break
}
batch := make([]mysqlmodel.UserNotification, 0, len(ids))
for _, uid := range ids {
batch = append(batch, buildSystemNotification(uid, title, content))
}
if err := s.notificationDao.NotificationBatchCreate(ctx, batch); err != nil {
slog.ErrorContext(ctx, "broadcast insert batch failed",
"sent", sent, "error", err)
return &dto.NotificationBroadcastResponse{SentCount: sent}, errcode.DatabaseError
}
sent += int64(len(ids))
// best-effort 未读计数自增。
for _, uid := range ids {
s.incrUnread(ctx, uid)
}
lastID = ids[len(ids)-1]
if len(ids) < broadcastBatchSize {
break
}
}
return &dto.NotificationBroadcastResponse{SentCount: sent}, nil
}
// incrUnread 未读计数自增(best-effort,失败仅告警,不影响主流程)。
func (s *NotificationService) incrUnread(ctx context.Context, userID int64) {
if _, err := redisdao.NotificationUnreadIncr(ctx, userID); err != nil {
slog.WarnContext(ctx, "incr notification unread failed", "user_id", userID, "error", err)
}
}
// buildSystemNotification 构造系统通知记录(msg_type=3、sender_id=0、未读)。
func buildSystemNotification(userID int64, title, content string) mysqlmodel.UserNotification {
return mysqlmodel.UserNotification{
NotificationUUID: genUUID(),
UserID: userID,
MsgType: NotificationTypeSystem,
SenderID: NotificationSenderSystem,
Title: strings.TrimSpace(title),
Content: strings.TrimSpace(content),
IsRead: NotificationUnread,
}
}