feat: 实现用户通知系统,包含通知模型、数据操作、业务逻辑及相关API接口。

This commit is contained in:
2026-01-28 08:08:14 +08:00
parent f1a390a1ef
commit 8e9c0e9024
5 changed files with 325 additions and 0 deletions

View File

@@ -0,0 +1,158 @@
package handler
import (
"accounting-app/internal/models"
"accounting-app/internal/service"
"accounting-app/pkg/api"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
type NotificationHandler struct {
service *service.NotificationService
}
func NewNotificationHandler(service *service.NotificationService) *NotificationHandler {
return &NotificationHandler{service: service}
}
// GetNotifications returns a list of notifications for the user
func (h *NotificationHandler) GetNotifications(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
api.Unauthorized(c, "User not authenticated")
return
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
var isRead *bool
if val, ok := c.GetQuery("is_read"); ok {
parsedVal, err := strconv.ParseBool(val)
if err == nil {
isRead = &parsedVal
}
}
notifications, total, err := h.service.GetNotifications(userID.(uint), page, limit, isRead)
if err != nil {
api.Error(c, http.StatusInternalServerError, "FETCH_ERROR", "Failed to fetch notifications")
return
}
api.Success(c, gin.H{
"notifications": notifications,
"total": total,
"page": page,
"limit": limit,
})
}
// GetUnreadCount returns the number of unread notifications
func (h *NotificationHandler) GetUnreadCount(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
api.Unauthorized(c, "User not authenticated")
return
}
count, err := h.service.GetUnreadCount(userID.(uint))
if err != nil {
api.Error(c, http.StatusInternalServerError, "FETCH_ERROR", "Failed to count unread notifications")
return
}
api.Success(c, gin.H{
"count": count,
})
}
// MarkAsRead marks a specific notification as read
func (h *NotificationHandler) MarkAsRead(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
api.Unauthorized(c, "User not authenticated")
return
}
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
api.Error(c, http.StatusBadRequest, "INVALID_ID", "Invalid notification ID")
return
}
if err := h.service.MarkAsRead(uint(id), userID.(uint)); err != nil {
api.Error(c, http.StatusInternalServerError, "UPDATE_ERROR", "Failed to mark as read")
return
}
api.Success(c, gin.H{"message": "Marked as read"})
}
// MarkAllAsRead marks all notifications as read
func (h *NotificationHandler) MarkAllAsRead(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
api.Unauthorized(c, "User not authenticated")
return
}
if err := h.service.MarkAllAsRead(userID.(uint)); err != nil {
api.Error(c, http.StatusInternalServerError, "UPDATE_ERROR", "Failed to mark all as read")
return
}
api.Success(c, gin.H{"message": "All marked as read"})
}
// CreateNotificationRequest represents the request body for creating a notification (internal/admin use primarily)
type CreateNotificationRequest struct {
Title string `json:"title" binding:"required"`
Content string `json:"content" binding:"required"`
Type string `json:"type" binding:"required"`
Link string `json:"link"`
}
// CreateNotification creates a new notification (For testing/admin purposes)
func (h *NotificationHandler) CreateNotification(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
api.Unauthorized(c, "User not authenticated")
return
}
var req CreateNotificationRequest
if err := c.ShouldBindJSON(&req); err != nil {
api.Error(c, http.StatusBadRequest, "INVALID_REQUEST", "Invalid request")
return
}
err := h.service.CreateNotification(
userID.(uint),
req.Title,
req.Content,
models.NotificationType(req.Type),
req.Link,
)
if err != nil {
api.Error(c, http.StatusInternalServerError, "CREATE_ERROR", "Failed to create notification")
return
}
api.Success(c, gin.H{"message": "Notification created"})
}
// RegisterRoutes registers the notification routes
func (h *NotificationHandler) RegisterRoutes(r *gin.RouterGroup) {
notifications := r.Group("/notifications")
{
notifications.GET("", h.GetNotifications)
notifications.GET("/unread-count", h.GetUnreadCount)
notifications.PUT("/:id/read", h.MarkAsRead)
notifications.PUT("/read-all", h.MarkAllAsRead)
notifications.POST("", h.CreateNotification) // Admin/Internal use
}
}

View File

@@ -0,0 +1,34 @@
package models
import (
"time"
)
// NotificationType represents the type of notification
type NotificationType string
const (
NotificationTypeSystem NotificationType = "system"
NotificationTypeAlert NotificationType = "alert"
NotificationTypeInfo NotificationType = "info"
)
// Notification represents a system notification for a user
type Notification struct {
BaseModel
UserID uint `gorm:"not null;index" json:"user_id"`
Title string `gorm:"size:200;not null" json:"title"`
Content string `gorm:"type:text;not null" json:"content"`
Type NotificationType `gorm:"size:20;not null;default:'system'" json:"type"`
IsRead bool `gorm:"default:false" json:"is_read"`
Link string `gorm:"size:255" json:"link,omitempty"` // Optional link to redirect
ReadAt *time.Time `json:"read_at,omitempty"`
// Relationships
User User `gorm:"foreignKey:UserID" json:"-"`
}
// TableName specifies the table name for Notification
func (Notification) TableName() string {
return "notifications"
}

View File

@@ -0,0 +1,74 @@
package repository
import (
"accounting-app/internal/models"
"time"
"gorm.io/gorm"
)
type NotificationRepository struct {
db *gorm.DB
}
func NewNotificationRepository(db *gorm.DB) *NotificationRepository {
return &NotificationRepository{db: db}
}
// Create creates a new notification
func (r *NotificationRepository) Create(notification *models.Notification) error {
return r.db.Create(notification).Error
}
// FindAll retrieves notifications for a user with pagination and filtering
func (r *NotificationRepository) FindAll(userID uint, page, pageSize int, isRead *bool) ([]models.Notification, int64, error) {
var notifications []models.Notification
var total int64
query := r.db.Model(&models.Notification{}).Where("user_id = ?", userID)
if isRead != nil {
query = query.Where("is_read = ?", *isRead)
}
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
offset := (page - 1) * pageSize
err := query.Order("created_at desc").
Offset(offset).
Limit(pageSize).
Find(&notifications).Error
return notifications, total, err
}
// CountUnread counts unread notifications for a user
func (r *NotificationRepository) CountUnread(userID uint) (int64, error) {
var count int64
err := r.db.Model(&models.Notification{}).
Where("user_id = ? AND is_read = ?", userID, false).
Count(&count).Error
return count, err
}
// MarkAsRead marks a single notification as read
func (r *NotificationRepository) MarkAsRead(id uint, userID uint) error {
return r.db.Model(&models.Notification{}).
Where("id = ? AND user_id = ?", id, userID).
Updates(map[string]interface{}{
"is_read": true,
"read_at": time.Now(),
}).Error
}
// MarkAllAsRead marks all notifications as read for a user
func (r *NotificationRepository) MarkAllAsRead(userID uint) error {
return r.db.Model(&models.Notification{}).
Where("user_id = ? AND is_read = ?", userID, false).
Updates(map[string]interface{}{
"is_read": true,
"read_at": time.Now(),
}).Error
}

View File

@@ -49,6 +49,7 @@ func Setup(db *gorm.DB, yunAPIClient *service.YunAPIClient, cfg *config.Config)
ledgerRepo := repository.NewLedgerRepository(db)
userSettingsRepo := repository.NewUserSettingsRepository(db)
userRepo := repository.NewUserRepository(db)
notificationRepo := repository.NewNotificationRepository(db)
// Initialize auth services
authService := service.NewAuthService(userRepo, cfg)
@@ -83,6 +84,7 @@ func Setup(db *gorm.DB, yunAPIClient *service.YunAPIClient, cfg *config.Config)
reimbursementService := service.NewReimbursementService(db, transactionRepo, accountRepo)
refundService := service.NewRefundService(db, transactionRepo, accountRepo)
userSettingsService := service.NewUserSettingsService(userSettingsRepo)
notificationService := service.NewNotificationService(notificationRepo)
// Feature: financial-core-upgrade - Initialize new services
subAccountService := service.NewSubAccountService(accountRepo, db)
@@ -112,6 +114,7 @@ func Setup(db *gorm.DB, yunAPIClient *service.YunAPIClient, cfg *config.Config)
reimbursementHandler := handler.NewReimbursementHandler(reimbursementService)
refundHandler := handler.NewRefundHandler(refundService)
settingsHandler := handler.NewSettingsHandler(userSettingsService)
notificationHandler := handler.NewNotificationHandler(notificationService)
// Feature: financial-core-upgrade - Initialize new handlers
subAccountHandler := handler.NewSubAccountHandler(subAccountService)
@@ -237,6 +240,9 @@ func Setup(db *gorm.DB, yunAPIClient *service.YunAPIClient, cfg *config.Config)
protected.POST("/app-lock/verify", appLockHandler.VerifyPassword)
protected.POST("/app-lock/disable", appLockHandler.DisableLock)
protected.POST("/app-lock/password/change", appLockHandler.ChangePassword)
// Register notification routes
notificationHandler.RegisterRoutes(protected)
}
}
@@ -297,6 +303,7 @@ func SetupWithRedis(db *gorm.DB, yunAPIClient *service.YunAPIClient, redisClient
ledgerRepo := repository.NewLedgerRepository(db)
userSettingsRepo := repository.NewUserSettingsRepository(db)
userRepo := repository.NewUserRepository(db)
notificationRepo := repository.NewNotificationRepository(db)
// Initialize auth services
authService := service.NewAuthService(userRepo, cfg)
@@ -330,6 +337,7 @@ func SetupWithRedis(db *gorm.DB, yunAPIClient *service.YunAPIClient, redisClient
reimbursementService := service.NewReimbursementService(db, transactionRepo, accountRepo)
refundService := service.NewRefundService(db, transactionRepo, accountRepo)
userSettingsService := service.NewUserSettingsService(userSettingsRepo)
notificationService := service.NewNotificationService(notificationRepo)
// Feature: financial-core-upgrade - Initialize new services
subAccountService := service.NewSubAccountService(accountRepo, db)
@@ -365,6 +373,7 @@ func SetupWithRedis(db *gorm.DB, yunAPIClient *service.YunAPIClient, redisClient
reimbursementHandler := handler.NewReimbursementHandler(reimbursementService)
refundHandler := handler.NewRefundHandler(refundService)
settingsHandler := handler.NewSettingsHandler(userSettingsService)
notificationHandler := handler.NewNotificationHandler(notificationService)
// Feature: financial-core-upgrade - Initialize new handlers
subAccountHandler := handler.NewSubAccountHandler(subAccountService)
@@ -500,6 +509,9 @@ func SetupWithRedis(db *gorm.DB, yunAPIClient *service.YunAPIClient, redisClient
v1.POST("/app-lock/verify", appLockHandler.VerifyPassword)
v1.POST("/app-lock/disable", appLockHandler.DisableLock)
v1.POST("/app-lock/password/change", appLockHandler.ChangePassword)
// Register notification routes
notificationHandler.RegisterRoutes(protected)
}
return r, syncScheduler

View File

@@ -0,0 +1,47 @@
package service
import (
"accounting-app/internal/models"
"accounting-app/internal/repository"
)
type NotificationService struct {
repo *repository.NotificationRepository
}
func NewNotificationService(repo *repository.NotificationRepository) *NotificationService {
return &NotificationService{repo: repo}
}
func (s *NotificationService) CreateNotification(userID uint, title, content string, notifType models.NotificationType, link string) error {
notification := &models.Notification{
UserID: userID,
Title: title,
Content: content,
Type: notifType,
Link: link,
}
return s.repo.Create(notification)
}
func (s *NotificationService) GetNotifications(userID uint, page, limit int, isRead *bool) ([]models.Notification, int64, error) {
if page < 1 {
page = 1
}
if limit < 1 {
limit = 10
}
return s.repo.FindAll(userID, page, limit, isRead)
}
func (s *NotificationService) GetUnreadCount(userID uint) (int64, error) {
return s.repo.CountUnread(userID)
}
func (s *NotificationService) MarkAsRead(id uint, userID uint) error {
return s.repo.MarkAsRead(id, userID)
}
func (s *NotificationService) MarkAllAsRead(userID uint) error {
return s.repo.MarkAllAsRead(userID)
}