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>
This commit is contained in:
101
internal/router/router.go
Normal file
101
internal/router/router.go
Normal file
@@ -0,0 +1,101 @@
|
||||
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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user