Files
vision-tool/internal/router/router.go
Geliebte f9cdeb5d64 feat: 认证简化为固定进入密钥 + 页面增强(API 文档/主题切换/版权署名)
- 移除管理员初始化/登录/JWT 流程,改为固定进入密钥(19735500),
  首次启动自动初始化到数据库 settings 表(可改库更新)
- analyze 接口支持可选密钥鉴权:X-Access-Key 请求头或 access_key 字段,
  带密钥强制校验(错误 401),不带密钥保持免鉴权
- 解锁后顶栏"复制 API 文档"按钮:以当前访问 URL 为基础地址动态生成
  Markdown 文档(免鉴权/带密钥两种 curl 示例,供智能体直接调用)
- 白昼/黑夜主题切换:CSS 变量驱动,localStorage 持久化,默认跟随系统
- 修复 hidden 属性被 display:flex 覆盖导致解锁层无法隐藏的问题
- 页面去除"免费/Agnes"文案,页脚署名青梧映星软件开发工作室
- build.bat/build.sh 简化为 Linux 交叉编译(CGO_ENABLED=0)
- 依赖精简:移除 golang-jwt/bcrypt,直接依赖仅 gin/sqlite/gorm/yaml

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-13 01:21:18 +08:00

93 lines
2.6 KiB
Go
Raw Permalink 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"
"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"
}
}