From 0a6c9d7d430c75e413f3eb59db4b4c1d16323b8b Mon Sep 17 00:00:00 2001 From: admin <1297598740@qq.com> Date: Thu, 29 Jan 2026 13:41:53 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E9=80=9A=E7=9F=A5?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=EF=BC=8C=E6=94=AF=E6=8C=81=E9=80=9A=E7=9F=A5?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E3=80=81=E6=9C=AA=E8=AF=BB=E8=AE=A1=E6=95=B0?= =?UTF-8?q?=E3=80=81=E6=A0=87=E8=AE=B0=E5=B7=B2=E8=AF=BB=E3=80=81=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E3=80=81=E5=88=9B=E5=BB=BA=E5=92=8C=E5=B9=BF=E6=92=AD?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=8C=E5=B9=B6=E5=BC=95=E5=85=A5=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=91=98=E4=B8=AD=E9=97=B4=E4=BB=B6=E5=92=8C=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E4=BB=93=E5=BA=93=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.prod | 6 ++++ internal/config/config.go | 10 ++++--- internal/handler/notification_handler.go | 29 ++++++++++++++++++ internal/middleware/admin_middleware.go | 38 ++++++++++++++++++++++++ internal/repository/user_repository.go | 10 ++++++- internal/router/router.go | 22 +++++++++++--- internal/service/notification_service.go | 34 +++++++++++++++++++-- 7 files changed, 137 insertions(+), 12 deletions(-) create mode 100644 internal/middleware/admin_middleware.go diff --git a/.env.prod b/.env.prod index ddb7265..d482253 100644 --- a/.env.prod +++ b/.env.prod @@ -13,6 +13,12 @@ ENVIRONMENT=production # ============================================ DATA_DIR=./data +# ============================================ +# 管理员密钥 (必填) +# 用于保护敏感的管理接口,如系统公告 +# ============================================ +ADMIN_SECRET_KEY=admin_secret_key_change_me_in_prod + # ============================================ # MySQL 数据库配置(必填) # ============================================ diff --git a/internal/config/config.go b/internal/config/config.go index 6115470..fa3ff1a 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -9,8 +9,9 @@ import ( // Config holds all configuration for the application type Config struct { // Server configuration - ServerPort string - Environment string + ServerPort string + Environment string + AdminSecretKey string // MySQL Database configuration DBHost string @@ -73,8 +74,9 @@ type Config struct { func Load() *Config { cfg := &Config{ // Server - ServerPort: getEnv("SERVER_PORT", "2612"), - Environment: getEnv("ENVIRONMENT", "development"), + ServerPort: getEnv("SERVER_PORT", "2612"), + Environment: getEnv("ENVIRONMENT", "development"), + AdminSecretKey: getEnv("ADMIN_SECRET_KEY", "admin_secret"), // Data directory DataDir: getEnv("DATA_DIR", "./data"), diff --git a/internal/handler/notification_handler.go b/internal/handler/notification_handler.go index 5dbd68a..938146f 100644 --- a/internal/handler/notification_handler.go +++ b/internal/handler/notification_handler.go @@ -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) } } diff --git a/internal/middleware/admin_middleware.go b/internal/middleware/admin_middleware.go new file mode 100644 index 0000000..3bba51c --- /dev/null +++ b/internal/middleware/admin_middleware.go @@ -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() + } +} diff --git a/internal/repository/user_repository.go b/internal/repository/user_repository.go index 9ec5b04..f89335c 100644 --- a/internal/repository/user_repository.go +++ b/internal/repository/user_repository.go @@ -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 +} diff --git a/internal/router/router.go b/internal/router/router.go index f4fdaa8..216c784 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -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) diff --git a/internal/service/notification_service.go b/internal/service/notification_service.go index a75125f..2b463e2 100644 --- a/internal/service/notification_service.go +++ b/internal/service/notification_service.go @@ -15,12 +15,13 @@ var ( // NotificationService handles business logic for notifications type NotificationService struct { - repo *repository.NotificationRepository + 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 +}