Files
Novault-backend/internal/service/notification_service.go

229 lines
7.5 KiB
Go

package service
import (
"errors"
"fmt"
"accounting-app/internal/models"
"accounting-app/internal/repository"
)
// Notification service errors
var (
ErrNotificationNotFound = errors.New("notification not found")
)
// NotificationService handles business logic for notifications
type NotificationService struct {
repo *repository.NotificationRepository
}
// NewNotificationService creates a new NotificationService instance
func NewNotificationService(repo *repository.NotificationRepository) *NotificationService {
return &NotificationService{repo: repo}
}
// CreateNotificationInput represents input for creating a notification
type CreateNotificationInput struct {
UserID uint `json:"user_id" binding:"required"`
Type models.NotificationType `json:"type" binding:"required"`
Title string `json:"title" binding:"required"`
Content string `json:"content" binding:"required"`
RelatedID *uint `json:"related_id"`
}
// CreateNotification creates a new notification
func (s *NotificationService) CreateNotification(input CreateNotificationInput) (*models.Notification, error) {
notification := &models.Notification{
UserID: input.UserID,
Type: input.Type,
Title: input.Title,
Content: input.Content,
RelatedID: input.RelatedID,
IsRead: false,
}
if err := s.repo.Create(notification); err != nil {
return nil, fmt.Errorf("failed to create notification: %w", err)
}
return notification, nil
}
// GetNotification retrieves a notification by ID
func (s *NotificationService) GetNotification(userID, id uint) (*models.Notification, error) {
notification, err := s.repo.GetByID(userID, id)
if err != nil {
if errors.Is(err, repository.ErrNotificationNotFound) {
return nil, ErrNotificationNotFound
}
return nil, err
}
return notification, nil
}
// NotificationListInput represents input for listing notifications
type NotificationListInput struct {
Type *models.NotificationType
IsRead *bool
Offset int
Limit int
}
// ListNotifications retrieves notifications with pagination
func (s *NotificationService) ListNotifications(userID uint, input NotificationListInput) (*repository.NotificationListResult, error) {
limit := input.Limit
if limit <= 0 {
limit = 20
}
options := repository.NotificationListOptions{
Type: input.Type,
IsRead: input.IsRead,
Offset: input.Offset,
Limit: limit,
}
return s.repo.List(userID, options)
}
// MarkAsRead marks a notification as read
func (s *NotificationService) MarkAsRead(userID, id uint) error {
err := s.repo.MarkAsRead(userID, id)
if err != nil {
if errors.Is(err, repository.ErrNotificationNotFound) {
return ErrNotificationNotFound
}
return err
}
return nil
}
// MarkAllAsRead marks all notifications as read
func (s *NotificationService) MarkAllAsRead(userID uint) error {
return s.repo.MarkAllAsRead(userID)
}
// DeleteNotification deletes a notification
func (s *NotificationService) DeleteNotification(userID, id uint) error {
err := s.repo.Delete(userID, id)
if err != nil {
if errors.Is(err, repository.ErrNotificationNotFound) {
return ErrNotificationNotFound
}
return err
}
return nil
}
// GetUnreadCount returns the count of unread notifications
func (s *NotificationService) GetUnreadCount(userID uint) (int64, error) {
return s.repo.GetUnreadCount(userID)
}
// SendBudgetAlert creates a budget alert notification
func (s *NotificationService) SendBudgetAlert(userID uint, budgetName string, usedPercentage float64, budgetID uint) (*models.Notification, error) {
title := fmt.Sprintf("预算提醒: %s", budgetName)
content := fmt.Sprintf("您的「%s」预算已使用 %.1f%%,请注意控制支出。", budgetName, usedPercentage)
return s.CreateNotification(CreateNotificationInput{
UserID: userID,
Type: models.NotificationTypeBudgetAlert,
Title: title,
Content: content,
RelatedID: &budgetID,
})
}
// SendRecurringReminder creates a recurring transaction reminder
func (s *NotificationService) SendRecurringReminder(userID uint, recurringName string, amount float64, recurringID uint) (*models.Notification, error) {
title := fmt.Sprintf("周期交易提醒: %s", recurringName)
content := fmt.Sprintf("您的周期交易「%s」(¥%.2f) 已自动执行。", recurringName, amount)
return s.CreateNotification(CreateNotificationInput{
UserID: userID,
Type: models.NotificationTypeRecurring,
Title: title,
Content: content,
RelatedID: &recurringID,
})
}
// SendSystemNotification creates a system notification
func (s *NotificationService) SendSystemNotification(userID uint, title, content string) (*models.Notification, error) {
return s.CreateNotification(CreateNotificationInput{
UserID: userID,
Type: models.NotificationTypeSystem,
Title: title,
Content: content,
})
}
// BroadcastInput represents input for broadcasting a notification
type BroadcastInput struct {
Title string `json:"title" binding:"required"`
Content string `json:"content" binding:"required"`
Type models.NotificationType `json:"type" binding:"required"`
AdminID uint `json:"-"` // Set from context
}
// Broadcast sends a notification to all users and records it in history
func (s *NotificationService) Broadcast(input BroadcastInput, userRepo *repository.UserRepository, announcementRepo *repository.AnnouncementRepository) error {
// 1. Create Announcement record for history
announcement := &models.Announcement{
Title: input.Title,
Content: input.Content,
Type: input.Type,
CreatedBy: input.AdminID,
}
if err := announcementRepo.Create(announcement); err != nil {
return fmt.Errorf("failed to create announcement history: %w", err)
}
// 2. Fetch all active users (ID only)
// Note: For very large user bases, this should be done in batches or via background worker.
// For standard enterprise use, fetching IDs is acceptable.
var users []models.User
// optimize: select only ID
if err := userRepo.DB().Select("id").Find(&users).Error; err != nil {
return fmt.Errorf("failed to fetch users for broadcast: %w", err)
}
if len(users) == 0 {
return nil
}
// 3. Create Notification records for each user (Write-Diffusion)
notifications := make([]models.Notification, len(users))
for i, user := range users {
notifications[i] = models.Notification{
UserID: user.ID,
Title: input.Title,
Content: input.Content,
Type: input.Type,
IsRead: false,
}
}
// 4. Batch insert notifications
if err := s.repo.CreateBatch(notifications); err != nil {
return fmt.Errorf("failed to broadcast notifications: %w", err)
}
// 5. Update sent count (optional, but good for history)
announcement.SentCount = len(users)
// We can update the record if needed, but for now we skip re-saving to keep it simple
// or we could add an Update method to AnnouncementRepository.
// Since GORM Create updates the struct with ID, we can just save it again if we want accurate count immediately,
// but strictly speaking the simple Create is enough for history log if we don't strictly need accurate real-time count in the history object immediately after creation.
// Let's assume the Create call was enough for the log.
return nil
}
// GetAnnouncementHistory retrieves the history of system broadcasts
func (s *NotificationService) GetAnnouncementHistory(limit, offset int, announcementRepo *repository.AnnouncementRepository) (*repository.AnnouncementListResult, error) {
return announcementRepo.List(limit, offset)
}