// Package routers 路由配置 // 定义所有 HTTP 路由和路由组 package routers import ( "context" "errors" "log" "net/http" "time" "github.com/LoveLosita/smartflow/backend/api" "github.com/LoveLosita/smartflow/backend/dao" "github.com/LoveLosita/smartflow/backend/middleware" "github.com/LoveLosita/smartflow/backend/pkg" "github.com/gin-gonic/gin" "github.com/spf13/viper" ) // StartEngine 启动 HTTP 服务,并在上下文取消时尽量优雅退出。 func StartEngine(ctx context.Context, r *gin.Engine) { // 1. 先解析端口,保持和历史行为一致。 // 2. 再用 http.Server 托管 gin engine,方便在取消信号到来时执行 Shutdown。 port := viper.GetString("server.port") if port == "" { port = "8080" } srv := &http.Server{ Addr: ":" + port, Handler: r, } errCh := make(chan error, 1) go func() { log.Printf("Server starting on port %s...", port) errCh <- srv.ListenAndServe() }() select { case <-ctx.Done(): shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := srv.Shutdown(shutdownCtx); err != nil && !errors.Is(err, context.Canceled) { log.Printf("Failed to shutdown server gracefully: %v", err) } if err := <-errCh; err != nil && !errors.Is(err, http.ErrServerClosed) { log.Fatalf("Failed to start server: %v", err) } case err := <-errCh: if err != nil && !errors.Is(err, http.ErrServerClosed) { log.Fatalf("Failed to start server: %v", err) } } } func RegisterRouters(handlers *api.ApiHandlers, cache *dao.CacheDAO, userRepo *dao.UserDAO, limiter *pkg.RateLimiter) *gin.Engine { // 初始化 Gin 引擎 r := gin.Default() // 在这里注册所有的路由和路由组 apiGroup := r.Group("/api/v1") { // 健康检查路由 apiGroup.GET("/health", func(c *gin.Context) { c.JSON(200, gin.H{ "status": "ok", "version": "0.4.0.dev", }) }) userGroup := apiGroup.Group("/user") { userGroup.POST("/register", handlers.UserHandler.UserRegister) userGroup.POST("/login", handlers.UserHandler.UserLogin) userGroup.POST("/refresh-token", handlers.UserHandler.RefreshTokenHandler) userGroup.POST("/logout", middleware.JWTTokenAuth(cache), middleware.RateLimitMiddleware(limiter, 20, 1), handlers.UserHandler.UserLogout) } taskGroup := apiGroup.Group("/task") { taskGroup.Use(middleware.JWTTokenAuth(cache), middleware.RateLimitMiddleware(limiter, 20, 1)) taskGroup.POST("/create", middleware.IdempotencyMiddleware(cache), handlers.TaskHandler.AddTask) taskGroup.PUT("/complete", middleware.IdempotencyMiddleware(cache), handlers.TaskHandler.CompleteTask) taskGroup.PUT("/undo-complete", middleware.IdempotencyMiddleware(cache), handlers.TaskHandler.UndoCompleteTask) taskGroup.PUT("/update", middleware.IdempotencyMiddleware(cache), handlers.TaskHandler.UpdateTask) taskGroup.DELETE("/delete", middleware.IdempotencyMiddleware(cache), handlers.TaskHandler.DeleteTask) taskGroup.GET("/get", handlers.TaskHandler.GetUserTasks) taskGroup.POST("/batch-status", handlers.TaskHandler.BatchTaskStatus) } courseGroup := apiGroup.Group("/course") { courseGroup.Use(middleware.JWTTokenAuth(cache), middleware.RateLimitMiddleware(limiter, 20, 1)) courseGroup.POST("/validate", handlers.CourseHandler.CheckUserCourse) courseGroup.POST("/parse-image", handlers.CourseHandler.ParseCourseTableImage) courseGroup.POST("/import", middleware.IdempotencyMiddleware(cache), handlers.CourseHandler.AddUserCourses) } taskClassGroup := apiGroup.Group("/task-class") { taskClassGroup.Use(middleware.JWTTokenAuth(cache), middleware.RateLimitMiddleware(limiter, 20, 1)) taskClassGroup.POST("/add", middleware.IdempotencyMiddleware(cache), handlers.TaskClassHandler.UserAddTaskClass) taskClassGroup.GET("/list", handlers.TaskClassHandler.UserGetTaskClassInfos) taskClassGroup.GET("/get", handlers.TaskClassHandler.UserGetCompleteTaskClass) taskClassGroup.PUT("/update", middleware.IdempotencyMiddleware(cache), handlers.TaskClassHandler.UserUpdateTaskClass) taskClassGroup.POST("/insert-into-schedule", middleware.IdempotencyMiddleware(cache), handlers.TaskClassHandler.UserAddTaskClassItemIntoSchedule) taskClassGroup.DELETE("/delete-item", middleware.IdempotencyMiddleware(cache), handlers.TaskClassHandler.DeleteTaskClassItem) taskClassGroup.DELETE("/delete-class", middleware.IdempotencyMiddleware(cache), handlers.TaskClassHandler.DeleteTaskClass) taskClassGroup.PUT("/apply-batch-into-schedule", middleware.IdempotencyMiddleware(cache), handlers.TaskClassHandler.UserInsertBatchTaskClassItemsIntoSchedule) } scheduleGroup := apiGroup.Group("/schedule") { scheduleGroup.Use(middleware.JWTTokenAuth(cache), middleware.RateLimitMiddleware(limiter, 20, 1)) scheduleGroup.GET("/today", handlers.ScheduleHandler.GetUserTodaySchedule) scheduleGroup.GET("/week", handlers.ScheduleHandler.GetUserWeeklySchedule) scheduleGroup.DELETE("/delete", middleware.IdempotencyMiddleware(cache), handlers.ScheduleHandler.DeleteScheduleEvent) scheduleGroup.GET("/recent-completed", handlers.ScheduleHandler.GetUserRecentCompletedSchedules) scheduleGroup.GET("/current", handlers.ScheduleHandler.GetUserOngoingSchedule) scheduleGroup.DELETE("/undo-task-item", middleware.IdempotencyMiddleware(cache), handlers.ScheduleHandler.UserRevocateTaskItemFromSchedule) scheduleGroup.GET("/smart-planning", handlers.ScheduleHandler.SmartPlanning) scheduleGroup.POST("/smart-planning-multi", handlers.ScheduleHandler.SmartPlanningMulti) } agentGroup := apiGroup.Group("/agent") { agentGroup.Use(middleware.JWTTokenAuth(cache), middleware.RateLimitMiddleware(limiter, 20, 1)) agentGroup.POST("/chat", middleware.TokenQuotaGuard(cache, userRepo), handlers.AgentHandler.ChatAgent) agentGroup.GET("/conversation-meta", handlers.AgentHandler.GetConversationMeta) agentGroup.GET("/conversation-list", handlers.AgentHandler.GetConversationList) agentGroup.GET("/conversation-timeline", handlers.AgentHandler.GetConversationTimeline) agentGroup.GET("/schedule-preview", handlers.AgentHandler.GetSchedulePlanPreview) agentGroup.GET("/context-stats", handlers.AgentHandler.GetContextStats) agentGroup.POST("/schedule-state", handlers.AgentHandler.SaveScheduleState) } memoryGroup := apiGroup.Group("/memory") { memoryGroup.Use(middleware.JWTTokenAuth(cache), middleware.RateLimitMiddleware(limiter, 20, 1)) memoryGroup.GET("/items", handlers.MemoryHandler.ListItems) memoryGroup.GET("/items/:id", handlers.MemoryHandler.GetItem) memoryGroup.POST("/items", middleware.IdempotencyMiddleware(cache), handlers.MemoryHandler.CreateItem) memoryGroup.PATCH("/items/:id", middleware.IdempotencyMiddleware(cache), handlers.MemoryHandler.UpdateItem) memoryGroup.DELETE("/items/:id", middleware.IdempotencyMiddleware(cache), handlers.MemoryHandler.DeleteItem) memoryGroup.POST("/items/:id/restore", middleware.IdempotencyMiddleware(cache), handlers.MemoryHandler.RestoreItem) } activeScheduleGroup := apiGroup.Group("/active-schedule") { activeScheduleGroup.Use(middleware.JWTTokenAuth(cache), middleware.RateLimitMiddleware(limiter, 20, 1)) activeScheduleGroup.POST("/dry-run", handlers.ActiveSchedule.DryRun) activeScheduleGroup.POST("/trigger", handlers.ActiveSchedule.Trigger) activeScheduleGroup.POST("/preview", handlers.ActiveSchedule.CreatePreview) activeScheduleGroup.GET("/preview/:preview_id", handlers.ActiveSchedule.GetPreview) activeScheduleGroup.POST("/preview/:preview_id/confirm", handlers.ActiveSchedule.ConfirmPreview) } notificationGroup := apiGroup.Group("/notification") { notificationGroup.Use(middleware.JWTTokenAuth(cache), middleware.RateLimitMiddleware(limiter, 20, 1)) notificationGroup.GET("/channels/feishu", handlers.Notification.GetFeishuWebhook) notificationGroup.PUT("/channels/feishu", handlers.Notification.SaveFeishuWebhook) notificationGroup.DELETE("/channels/feishu", handlers.Notification.DeleteFeishuWebhook) notificationGroup.POST("/channels/feishu/test", handlers.Notification.TestFeishuWebhook) } } // 初始化 Gin 引擎 log.Println("Routes setup completed") return r }