Files
vision-tool/internal/router/router.go
Geliebte a3bbf33358 feat: 重构为 Go Web 脚手架——在线图片识别 Web 服务 + vision-cli 独立工具
- Gin + config.yaml + SQLite 后端骨架,JWT 登录 + 管理员初始化
- web/ 原生单页:拖拽多图上传 → Agnes 免费识别 → 结果复制
- HTTP API:/api/v1/vision/analyze 免鉴权 + 令牌桶限流,支持程序直调
- cmd/vision-cli 独立 CLI 工具(复用 service 层,不依赖 Web 服务)
- build.bat / build.sh 构建脚本,移除 mcp-server
- 统一响应格式 {code, message, data},CORS/JWT/限流中间件

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-12 23:59:17 +08:00

102 lines
2.9 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 (
"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"
}
}