227 lines
7.0 KiB
Go
227 lines
7.0 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
|
|
userRepo *repository.UserRepository
|
|
}
|
|
|
|
// NewNotificationService creates a new NotificationService instance
|
|
func NewNotificationService(repo *repository.NotificationRepository, userRepo *repository.UserRepository) *NotificationService {
|
|
return &NotificationService{repo: repo, userRepo: userRepo}
|
|
}
|
|
|
|
// 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,
|
|
})
|
|
}
|
|
|
|
// BroadcastNotificationInput represents input for broadcasting a notification
|
|
type BroadcastNotificationInput struct {
|
|
Type models.NotificationType `json:"type" binding:"required"`
|
|
Title string `json:"title" binding:"required"`
|
|
Content string `json:"content" binding:"required"`
|
|
}
|
|
|
|
// BroadcastNotification sends a notification to all active users and saves to history
|
|
func (s *NotificationService) BroadcastNotification(input BroadcastNotificationInput) error {
|
|
userIDs, err := s.userRepo.GetAllActiveUserIDs()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get active user IDs: %w", err)
|
|
}
|
|
|
|
// Create announcement record
|
|
announcement := &models.SystemAnnouncement{
|
|
Title: input.Title,
|
|
Content: input.Content,
|
|
Type: input.Type,
|
|
SentCount: len(userIDs),
|
|
}
|
|
if err := s.repo.CreateAnnouncement(announcement); err != nil {
|
|
// Log error but continue with broadcasting if possible?
|
|
// Actually, better to store the history first.
|
|
return fmt.Errorf("failed to save announcement history: %w", err)
|
|
}
|
|
|
|
for _, userID := range userIDs {
|
|
// Ignore errors for individual users to ensure best-effort delivery
|
|
_, _ = s.CreateNotification(CreateNotificationInput{
|
|
UserID: userID,
|
|
Type: input.Type,
|
|
Title: input.Title,
|
|
Content: input.Content,
|
|
})
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// AnnouncementListResult represents the result of a paginated announcement list query
|
|
type AnnouncementListResult struct {
|
|
Announcements []models.SystemAnnouncement `json:"announcements"`
|
|
Total int64 `json:"total"`
|
|
Limit int `json:"limit"`
|
|
Offset int `json:"offset"`
|
|
}
|
|
|
|
// ListAnnouncements retrieves system announcements with pagination
|
|
func (s *NotificationService) ListAnnouncements(limit, offset int) (*AnnouncementListResult, error) {
|
|
if limit <= 0 {
|
|
limit = 10
|
|
}
|
|
announcements, total, err := s.repo.ListAnnouncements(limit, offset)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &AnnouncementListResult{
|
|
Announcements: announcements,
|
|
Total: total,
|
|
Limit: limit,
|
|
Offset: offset,
|
|
}, nil
|
|
}
|