feat: 新增通知系统,支持通知列表、未读计数、标记已读、删除、创建和广播功能,并引入管理员中间件和用户仓库。
This commit is contained in:
@@ -13,6 +13,12 @@ ENVIRONMENT=production
|
||||
# ============================================
|
||||
DATA_DIR=./data
|
||||
|
||||
# ============================================
|
||||
# 管理员密钥 (必填)
|
||||
# 用于保护敏感的管理接口,如系统公告
|
||||
# ============================================
|
||||
ADMIN_SECRET_KEY=admin_secret_key_change_me_in_prod
|
||||
|
||||
# ============================================
|
||||
# MySQL 数据库配置(必填)
|
||||
# ============================================
|
||||
|
||||
@@ -11,6 +11,7 @@ type Config struct {
|
||||
// Server configuration
|
||||
ServerPort string
|
||||
Environment string
|
||||
AdminSecretKey string
|
||||
|
||||
// MySQL Database configuration
|
||||
DBHost string
|
||||
@@ -75,6 +76,7 @@ func Load() *Config {
|
||||
// Server
|
||||
ServerPort: getEnv("SERVER_PORT", "2612"),
|
||||
Environment: getEnv("ENVIRONMENT", "development"),
|
||||
AdminSecretKey: getEnv("ADMIN_SECRET_KEY", "admin_secret"),
|
||||
|
||||
// Data directory
|
||||
DataDir: getEnv("DATA_DIR", "./data"),
|
||||
|
||||
@@ -205,6 +205,34 @@ func (h *NotificationHandler) CreateNotification(c *gin.Context) {
|
||||
api.Created(c, notification)
|
||||
}
|
||||
|
||||
// BroadcastNotification handles POST /api/v1/notifications/broadcast
|
||||
// Sends a notification to all active users
|
||||
func (h *NotificationHandler) BroadcastNotification(c *gin.Context) {
|
||||
// TODO: Add strict admin authorization check
|
||||
|
||||
var input service.BroadcastNotificationInput
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
api.BadRequest(c, "Invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Validate input
|
||||
if input.Title == "" || input.Content == "" {
|
||||
api.BadRequest(c, "Title and content are required")
|
||||
return
|
||||
}
|
||||
if input.Type == "" {
|
||||
input.Type = models.NotificationTypeSystem
|
||||
}
|
||||
|
||||
if err := h.notificationService.BroadcastNotification(input); err != nil {
|
||||
api.InternalError(c, "Failed to broadcast notification: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
api.Success(c, gin.H{"message": "Notification broadcast started"})
|
||||
}
|
||||
|
||||
// RegisterUserRoutes registers notification routes for regular users
|
||||
func (h *NotificationHandler) RegisterUserRoutes(rg *gin.RouterGroup) {
|
||||
notifications := rg.Group("/notifications")
|
||||
@@ -222,5 +250,6 @@ func (h *NotificationHandler) RegisterAdminRoutes(rg *gin.RouterGroup) {
|
||||
notifications := rg.Group("/notifications")
|
||||
{
|
||||
notifications.POST("", h.CreateNotification)
|
||||
notifications.POST("/broadcast", h.BroadcastNotification)
|
||||
}
|
||||
}
|
||||
|
||||
38
internal/middleware/admin_middleware.go
Normal file
38
internal/middleware/admin_middleware.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"accounting-app/internal/config"
|
||||
"accounting-app/pkg/api"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// AdminMiddleware provides admin authentication middleware
|
||||
type AdminMiddleware struct {
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
// NewAdminMiddleware creates a new AdminMiddleware
|
||||
func NewAdminMiddleware(cfg *config.Config) *AdminMiddleware {
|
||||
return &AdminMiddleware{
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
// RequireAdminKey checks for the Admin-Secret-Key header
|
||||
func (m *AdminMiddleware) RequireAdminKey() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
key := c.GetHeader("X-Admin-Secret-Key")
|
||||
if key == "" {
|
||||
// Fallback to query param for easier testing
|
||||
key = c.Query("admin_secret")
|
||||
}
|
||||
|
||||
if key != m.cfg.AdminSecretKey {
|
||||
api.Unauthorized(c, "Invalid or missing admin secret key")
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -66,7 +66,6 @@ func (r *UserRepository) GetByEmail(email string) (*models.User, error) {
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
|
||||
// Update updates a user in the database
|
||||
func (r *UserRepository) Update(user *models.User) error {
|
||||
return r.db.Save(user).Error
|
||||
@@ -163,3 +162,12 @@ func (r *UserRepository) EmailExists(email string) (bool, error) {
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
// GetAllActiveUserIDs retrieves IDs of all active users
|
||||
func (r *UserRepository) GetAllActiveUserIDs() ([]uint, error) {
|
||||
var ids []uint
|
||||
if err := r.db.Model(&models.User{}).Where("is_active = ?", true).Pluck("id", &ids).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ func Setup(db *gorm.DB, yunAPIClient *service.YunAPIClient, cfg *config.Config)
|
||||
}
|
||||
authHandler := handler.NewAuthHandlerWithConfig(authService, gitHubOAuthService, giteeOAuthService, cfg)
|
||||
authMiddleware := middleware.NewAuthMiddleware(authService)
|
||||
adminMiddleware := middleware.NewAdminMiddleware(cfg)
|
||||
|
||||
// Initialize services
|
||||
accountService := service.NewAccountService(accountRepo, db)
|
||||
@@ -89,7 +90,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)
|
||||
notificationService := service.NewNotificationService(notificationRepo, userRepo)
|
||||
streakService := service.NewStreakService(streakRepo)
|
||||
|
||||
// Feature: financial-core-upgrade - Initialize new services
|
||||
@@ -237,7 +238,13 @@ func Setup(db *gorm.DB, yunAPIClient *service.YunAPIClient, cfg *config.Config)
|
||||
|
||||
// Feature: notification-center - Register notification routes
|
||||
notificationHandler.RegisterUserRoutes(protected)
|
||||
notificationHandler.RegisterAdminRoutes(v1)
|
||||
|
||||
// Admin routes
|
||||
admin := v1.Group("")
|
||||
admin.Use(adminMiddleware.RequireAdminKey())
|
||||
{
|
||||
notificationHandler.RegisterAdminRoutes(admin)
|
||||
}
|
||||
|
||||
// Register report routes
|
||||
protected.GET("/reports/summary", reportHandler.GetTransactionSummary)
|
||||
@@ -341,6 +348,7 @@ func SetupWithRedis(db *gorm.DB, yunAPIClient *service.YunAPIClient, redisClient
|
||||
}
|
||||
authHandler := handler.NewAuthHandlerWithConfig(authService, gitHubOAuthService, giteeOAuthService, cfg)
|
||||
authMiddleware := middleware.NewAuthMiddleware(authService)
|
||||
adminMiddleware := middleware.NewAdminMiddleware(cfg)
|
||||
|
||||
// Initialize services
|
||||
accountService := service.NewAccountService(accountRepo, db)
|
||||
@@ -365,7 +373,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)
|
||||
notificationService := service.NewNotificationService(notificationRepo, userRepo)
|
||||
streakService := service.NewStreakService(streakRepo)
|
||||
|
||||
// Feature: financial-core-upgrade - Initialize new services
|
||||
@@ -544,7 +552,13 @@ func SetupWithRedis(db *gorm.DB, yunAPIClient *service.YunAPIClient, redisClient
|
||||
|
||||
// Register notification routes
|
||||
notificationHandler.RegisterUserRoutes(protected)
|
||||
notificationHandler.RegisterAdminRoutes(v1)
|
||||
|
||||
// Admin routes
|
||||
admin := v1.Group("")
|
||||
admin.Use(adminMiddleware.RequireAdminKey())
|
||||
{
|
||||
notificationHandler.RegisterAdminRoutes(admin)
|
||||
}
|
||||
|
||||
// Feature: streak - Register streak routes
|
||||
streakHandler.RegisterRoutes(v1)
|
||||
|
||||
@@ -16,11 +16,12 @@ var (
|
||||
// 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) *NotificationService {
|
||||
return &NotificationService{repo: repo}
|
||||
func NewNotificationService(repo *repository.NotificationRepository, userRepo *repository.UserRepository) *NotificationService {
|
||||
return &NotificationService{repo: repo, userRepo: userRepo}
|
||||
}
|
||||
|
||||
// CreateNotificationInput represents input for creating a notification
|
||||
@@ -158,3 +159,30 @@ func (s *NotificationService) SendSystemNotification(userID uint, title, content
|
||||
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
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user