Files
smartmate/backend/gateway/router/router.go
Losita b08ee17893 Version: 0.9.66.dev.260504
后端:
1. 阶段 2 user/auth 服务边界落地,新增 `cmd/userauth` go-zero zrpc 服务、`services/userauth` 核心实现、gateway user API/zrpc client 与 shared contracts/ports,迁移注册、登录、刷新 token、登出、JWT、黑名单和 token 额度治理
2. gateway 与启动装配切流,`cmd/all` 只保留边缘路由、鉴权和轻量组合,通过 userauth zrpc 访问核心用户能力;拆分 MySQL/Redis 初始化与 AutoMigrate 边界,`userauth` 自迁 `users` 和 token 记账幂等表,`all` 不再迁用户表
3. 清退 Gin 单体旧 user/auth DAO、model、service、router、middleware 和 JWT handler,并同步调整 agent/schedule/cache/outbox 相关调用依赖
4. 补齐 refresh token 防并发重放、MySQL 幂等 token 记账、额度 `>=` 拦截和 RPC 错误映射,避免重复记账与内部错误透出

文档:
1. 新增《学习计划论坛与Token商店PRD》
2026-05-04 15:20:47 +08:00

163 lines
8.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package router
import (
"context"
"errors"
"log"
"net/http"
"time"
"github.com/LoveLosita/smartflow/backend/api"
"github.com/LoveLosita/smartflow/backend/dao"
gatewaymiddleware "github.com/LoveLosita/smartflow/backend/gateway/middleware"
"github.com/LoveLosita/smartflow/backend/gateway/userapi"
rootmiddleware "github.com/LoveLosita/smartflow/backend/middleware"
"github.com/LoveLosita/smartflow/backend/pkg"
"github.com/LoveLosita/smartflow/backend/shared/ports"
"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, authClient ports.UserAuthClient, cache *dao.CacheDAO, limiter *pkg.RateLimiter) *gin.Engine {
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",
})
})
userapi.RegisterRoutes(apiGroup, userapi.NewUserHandler(authClient), authClient, limiter)
taskGroup := apiGroup.Group("/task")
{
taskGroup.Use(gatewaymiddleware.JWTTokenAuth(authClient), rootmiddleware.RateLimitMiddleware(limiter, 20, 1))
taskGroup.POST("/create", rootmiddleware.IdempotencyMiddleware(cache), handlers.TaskHandler.AddTask)
taskGroup.PUT("/complete", rootmiddleware.IdempotencyMiddleware(cache), handlers.TaskHandler.CompleteTask)
taskGroup.PUT("/undo-complete", rootmiddleware.IdempotencyMiddleware(cache), handlers.TaskHandler.UndoCompleteTask)
taskGroup.PUT("/update", rootmiddleware.IdempotencyMiddleware(cache), handlers.TaskHandler.UpdateTask)
taskGroup.DELETE("/delete", rootmiddleware.IdempotencyMiddleware(cache), handlers.TaskHandler.DeleteTask)
taskGroup.GET("/get", handlers.TaskHandler.GetUserTasks)
taskGroup.POST("/batch-status", handlers.TaskHandler.BatchTaskStatus)
}
courseGroup := apiGroup.Group("/course")
{
courseGroup.Use(gatewaymiddleware.JWTTokenAuth(authClient), rootmiddleware.RateLimitMiddleware(limiter, 20, 1))
courseGroup.POST("/validate", handlers.CourseHandler.CheckUserCourse)
courseGroup.POST("/parse-image", handlers.CourseHandler.ParseCourseTableImage)
courseGroup.POST("/import", rootmiddleware.IdempotencyMiddleware(cache), handlers.CourseHandler.AddUserCourses)
}
taskClassGroup := apiGroup.Group("/task-class")
{
taskClassGroup.Use(gatewaymiddleware.JWTTokenAuth(authClient), rootmiddleware.RateLimitMiddleware(limiter, 20, 1))
taskClassGroup.POST("/add", rootmiddleware.IdempotencyMiddleware(cache), handlers.TaskClassHandler.UserAddTaskClass)
taskClassGroup.GET("/list", handlers.TaskClassHandler.UserGetTaskClassInfos)
taskClassGroup.GET("/get", handlers.TaskClassHandler.UserGetCompleteTaskClass)
taskClassGroup.PUT("/update", rootmiddleware.IdempotencyMiddleware(cache), handlers.TaskClassHandler.UserUpdateTaskClass)
taskClassGroup.POST("/insert-into-schedule", rootmiddleware.IdempotencyMiddleware(cache), handlers.TaskClassHandler.UserAddTaskClassItemIntoSchedule)
taskClassGroup.DELETE("/delete-item", rootmiddleware.IdempotencyMiddleware(cache), handlers.TaskClassHandler.DeleteTaskClassItem)
taskClassGroup.DELETE("/delete-class", rootmiddleware.IdempotencyMiddleware(cache), handlers.TaskClassHandler.DeleteTaskClass)
taskClassGroup.PUT("/apply-batch-into-schedule", rootmiddleware.IdempotencyMiddleware(cache), handlers.TaskClassHandler.UserInsertBatchTaskClassItemsIntoSchedule)
}
scheduleGroup := apiGroup.Group("/schedule")
{
scheduleGroup.Use(gatewaymiddleware.JWTTokenAuth(authClient), rootmiddleware.RateLimitMiddleware(limiter, 20, 1))
scheduleGroup.GET("/today", handlers.ScheduleHandler.GetUserTodaySchedule)
scheduleGroup.GET("/week", handlers.ScheduleHandler.GetUserWeeklySchedule)
scheduleGroup.DELETE("/delete", rootmiddleware.IdempotencyMiddleware(cache), handlers.ScheduleHandler.DeleteScheduleEvent)
scheduleGroup.GET("/recent-completed", handlers.ScheduleHandler.GetUserRecentCompletedSchedules)
scheduleGroup.GET("/current", handlers.ScheduleHandler.GetUserOngoingSchedule)
scheduleGroup.DELETE("/undo-task-item", rootmiddleware.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(gatewaymiddleware.JWTTokenAuth(authClient), rootmiddleware.RateLimitMiddleware(limiter, 20, 1))
agentGroup.POST("/chat", gatewaymiddleware.TokenQuotaGuard(authClient), 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(gatewaymiddleware.JWTTokenAuth(authClient), rootmiddleware.RateLimitMiddleware(limiter, 20, 1))
memoryGroup.GET("/items", handlers.MemoryHandler.ListItems)
memoryGroup.GET("/items/:id", handlers.MemoryHandler.GetItem)
memoryGroup.POST("/items", rootmiddleware.IdempotencyMiddleware(cache), handlers.MemoryHandler.CreateItem)
memoryGroup.PATCH("/items/:id", rootmiddleware.IdempotencyMiddleware(cache), handlers.MemoryHandler.UpdateItem)
memoryGroup.DELETE("/items/:id", rootmiddleware.IdempotencyMiddleware(cache), handlers.MemoryHandler.DeleteItem)
memoryGroup.POST("/items/:id/restore", rootmiddleware.IdempotencyMiddleware(cache), handlers.MemoryHandler.RestoreItem)
}
activeScheduleGroup := apiGroup.Group("/active-schedule")
{
activeScheduleGroup.Use(gatewaymiddleware.JWTTokenAuth(authClient), rootmiddleware.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(gatewaymiddleware.JWTTokenAuth(authClient), rootmiddleware.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)
}
}
log.Println("Routes setup completed")
return r
}