- 移除管理员初始化/登录/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>
93 lines
2.6 KiB
Go
93 lines
2.6 KiB
Go
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"
|
||
}
|
||
}
|