feat: 实现用户通知系统,包含通知模型、数据操作、业务逻辑及相关API接口。
This commit is contained in:
158
internal/handler/notification_handler.go
Normal file
158
internal/handler/notification_handler.go
Normal 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
|
||||
}
|
||||
}
|
||||
34
internal/models/notification.go
Normal file
34
internal/models/notification.go
Normal 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"
|
||||
}
|
||||
74
internal/repository/notification_repository.go
Normal file
74
internal/repository/notification_repository.go
Normal 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(¬ifications).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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
47
internal/service/notification_service.go
Normal file
47
internal/service/notification_service.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user