package router import ( "io/fs" "net/http" "path/filepath" "strings" "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/service" ) // Setup 构建路由,并完成分层依赖注入。 // 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()) // ---------- 分层依赖注入 ---------- accessSvc := service.NewAccessService(db) visionSvc := service.NewVisionService(&cfg.AI) authH := handler.NewAuthHandler(accessSvc) visionH := handler.NewVisionHandler(visionSvc, accessSvc, 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") { // 进入密钥校验(固定密钥,初始化在数据库) v1.POST("/auth/verify", authH.Verify) // 在线图片识别(免鉴权 + 限流) 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" } }