package router import ( "io/fs" "net/http" "path/filepath" "strings" "time" "github.com/gin-gonic/gin" "gorm.io/gorm" "vision-tool/internal/config" "vision-tool/internal/handler" "vision-tool/internal/middleware" "vision-tool/internal/model" "vision-tool/internal/repository" "vision-tool/internal/service" ) // Setup 构建路由,并完成分层依赖注入: // db → repository → service → handler → router。 // webFS 为 //go:embed web 的嵌入文件系统,用于内嵌前端。 func Setup(cfg *config.Config, db *gorm.DB, webFS fs.FS) *gin.Engine { r := gin.New() r.Use(gin.Logger(), gin.Recovery(), middleware.CORS()) // ---------- 分层依赖注入 ---------- userRepo := repository.NewUserRepository(db) authSvc := service.NewAuthService(userRepo, cfg.JWT.Secret, time.Duration(cfg.JWT.ExpireHours)*time.Hour) visionSvc := service.NewVisionService(&cfg.AI) authH := handler.NewAuthHandler(authSvc) visionH := handler.NewVisionHandler(visionSvc, cfg.Upload) // 识别接口免鉴权,用令牌桶限流防刷 analyzeLimiter := middleware.NewRateLimiter(10, 20) // ---------- API 路由 ---------- api := r.Group("/api") api.GET("/health", func(c *gin.Context) { c.JSON(http.StatusOK, model.OK(gin.H{"status": "ok"})) }) v1 := api.Group("/v1") { // 管理员初始化(方式 A:check + init) v1.GET("/admin/check", authH.Check) v1.POST("/admin/init", authH.Init) // 认证 v1.POST("/auth/login", authH.Login) v1.GET("/user/profile", middleware.JWTAuth(cfg.JWT.Secret), authH.Profile) // 在线图片识别(免鉴权 + 限流) v1.POST("/vision/analyze", analyzeLimiter.Middleware(), visionH.Analyze) } // ---------- 静态资源 ---------- // 上传图片 r.StaticFS("/uploads", http.Dir(cfg.Upload.Path)) // 内嵌前端:未匹配路由统一走 NoRoute // /api/* /uploads/* → 404;embed 内文件 → 直接返回;其余 → index.html(SPA 回退) assets, err := fs.Sub(webFS, "web") if err != nil { panic("web 目录嵌入失败: " + err.Error()) } r.NoRoute(func(c *gin.Context) { path := c.Request.URL.Path if strings.HasPrefix(path, "/api/") || strings.HasPrefix(path, "/uploads/") { c.JSON(http.StatusNotFound, model.Err(model.CodeNotFound, "接口不存在")) return } if data, err := fs.ReadFile(assets, strings.TrimPrefix(path, "/")); err == nil { c.Data(http.StatusOK, mimeByExt(path), data) return } index, _ := fs.ReadFile(assets, "index.html") c.Data(http.StatusOK, "text/html; charset=utf-8", index) }) return r } // mimeByExt 按扩展名返回静态资源 Content-Type。 func mimeByExt(path string) string { switch filepath.Ext(path) { case ".css": return "text/css; charset=utf-8" case ".js": return "application/javascript; charset=utf-8" case ".svg": return "image/svg+xml" case ".png": return "image/png" case ".jpg", ".jpeg": return "image/jpeg" default: return "text/html; charset=utf-8" } }