feat: adjust openapi page laod

This commit is contained in:
engigu
2026-03-06 08:53:22 +08:00
parent 40c3c58312
commit 36d1569f83
11 changed files with 293 additions and 256 deletions
+1 -1
View File
@@ -62,7 +62,7 @@ func (ec *EnvController) CreateEnvVar(c *gin.Context) {
// @Param name query string false "按名称模糊查询" // @Param name query string false "按名称模糊查询"
// @Param page query int false "页码" // @Param page query int false "页码"
// @Param page_size query int false "每页数量" // @Param page_size query int false "每页数量"
// @Success 200 {object} utils.Response{data=utils.PaginationData{list=[]vo.EnvVO}} // @Success 200 {object} utils.Response{data=utils.PaginationData{data=[]vo.EnvVO}}
// @Router /env [get] // @Router /env [get]
func (ec *EnvController) GetEnvVars(c *gin.Context) { func (ec *EnvController) GetEnvVars(c *gin.Context) {
userID := c.GetString("userID") userID := c.GetString("userID")
+1 -1
View File
@@ -28,7 +28,7 @@ func NewLogController() *LogController {
// @Param status query string false "状态" // @Param status query string false "状态"
// @Param page query int false "页码" // @Param page query int false "页码"
// @Param page_size query int false "每页数量" // @Param page_size query int false "每页数量"
// @Success 200 {object} utils.Response{data=utils.PaginationData{list=[]vo.TaskLogVO}} // @Success 200 {object} utils.Response{data=utils.PaginationData{data=[]vo.TaskLogVO}}
// @Router /logs [get] // @Router /logs [get]
func (lc *LogController) GetLogs(c *gin.Context) { func (lc *LogController) GetLogs(c *gin.Context) {
p := utils.ParsePagination(c) p := utils.ParsePagination(c)
+5 -5
View File
@@ -330,11 +330,11 @@ func (sc *SettingsController) GetLoginLogs(c *gin.Context) {
return return
} }
utils.Success(c, gin.H{ utils.Success(c, utils.PaginationData{
"data": vo.ToLoginLogVOListFromModels(logs), Data: vo.ToLoginLogVOListFromModels(logs),
"total": total, Total: total,
"page": page, Page: page,
"page_size": pageSize, PageSize: pageSize,
}) })
} }
+1 -1
View File
@@ -118,7 +118,7 @@ func (tc *TaskController) CreateTask(c *gin.Context) {
// @Param type query string false "任务类型" // @Param type query string false "任务类型"
// @Param page query int false "页码" // @Param page query int false "页码"
// @Param page_size query int false "每页数量" // @Param page_size query int false "每页数量"
// @Success 200 {object} utils.Response{data=utils.PaginationData{list=[]vo.TaskVO}} // @Success 200 {object} utils.Response{data=utils.PaginationData{data=[]vo.TaskVO}}
// @Router /tasks [get] // @Router /tasks [get]
func (tc *TaskController) GetTasks(c *gin.Context) { func (tc *TaskController) GetTasks(c *gin.Context) {
p := utils.ParsePagination(c) p := utils.ParsePagination(c)
+114 -77
View File
@@ -69,8 +69,50 @@ func Setup(c *Controllers) *gin.Engine {
root = router.Group("") root = router.Group("")
} }
initStaticRoutes(root)
initOpenAPIRoutes(root, urlPrefix)
// API 路由组
apiV1 := root.Group("/api/v1")
initPublicAPIRoutes(apiV1, c)
initAuthorizedAPIRoutes(apiV1, c)
initAgentAPIRoutes(root, c)
// SPA 兜底路由 - 返回 index.html(HTML禁用缓存以保证实时同步)
// 必须在最后注册,作为兜底路由
router.NoRoute(func(ctx *gin.Context) {
path := ctx.Request.URL.Path
// 如果配置了前缀,只处理带前缀的路径
if urlPrefix != "" && !strings.HasPrefix(path, urlPrefix) {
ctx.Status(404)
return
}
// 解析实际的相对路径
relPath := strings.TrimPrefix(path, urlPrefix)
if !strings.HasPrefix(relPath, "/") {
relPath = "/" + relPath
}
// 拦截属于 API 或静态资源目录下不存在的请求,绝不能返回 index.html 造成前端 MIME 错误
if strings.HasPrefix(relPath, "/api/") || strings.HasPrefix(relPath, "/assets/") {
ctx.String(404, "Not Found")
return
}
serveSPA(ctx, urlPrefix, 200)
})
return router
}
func initStaticRoutes(root *gin.RouterGroup) {
staticFS := static.GetFS() staticFS := static.GetFS()
if staticFS != nil { if staticFS == nil {
return
}
// 静态资源服务(Vue SPA),带缓存头部 // 静态资源服务(Vue SPA),带缓存头部
assetsGroup := root.Group("/assets") assetsGroup := root.Group("/assets")
assetsGroup.Use(cacheControl("public, max-age=31536000, immutable")) // 带哈希的资源缓存 assetsGroup.Use(cacheControl("public, max-age=31536000, immutable")) // 带哈希的资源缓存
@@ -86,8 +128,9 @@ func Setup(c *Controllers) *gin.Engine {
ctx.Header("Cache-Control", "public, max-age=86400") // 缓存1天 ctx.Header("Cache-Control", "public, max-age=86400") // 缓存1天
ctx.Data(200, "image/svg+xml", data) ctx.Data(200, "image/svg+xml", data)
}) })
} }
func initOpenAPIRoutes(root *gin.RouterGroup, urlPrefix string) {
// OpenAPI documentation using Scalar UI (带 Basic Auth 认证) // OpenAPI documentation using Scalar UI (带 Basic Auth 认证)
root.GET("/openapi/*any", func(c *gin.Context) { root.GET("/openapi/*any", func(c *gin.Context) {
settingsSvc := services.NewSettingsService() settingsSvc := services.NewSettingsService()
@@ -112,14 +155,13 @@ func Setup(c *Controllers) *gin.Engine {
middleware.SwaggerAuth()(c) middleware.SwaggerAuth()(c)
if c.IsAborted() { if c.IsAborted() {
// 如果认证失败(且被中间件置为 404,如密码错误且我们想要隐藏它) // 如果认证失败(且被中间件置为 404,如密码错误且我们想要隐藏它)
if c.Writer.Status() == 404 { if c.Writer.Status() == http.StatusNotFound {
serveSPA(c, urlPrefix, 404) serveSPA(c, urlPrefix, 404)
} }
return return
} }
// 获取内部路径并标准化(移除前后的所有斜杠) // 获取内部路径并标准化(移除前后的所有斜杠)
// c.Param("any") 对于 *any 匹配通常包含领先斜杠,如 "/index.html"
path := strings.Trim(c.Param("any"), "/") path := strings.Trim(c.Param("any"), "/")
// 1. 根路径或空路径 -> 重定向到 index.html // 1. 根路径或空路径 -> 重定向到 index.html
@@ -163,10 +205,9 @@ func Setup(c *Controllers) *gin.Engine {
// 其他未匹配路径 -> 返回 404 SPA 页面 // 其他未匹配路径 -> 返回 404 SPA 页面
serveSPA(c, urlPrefix, 404) serveSPA(c, urlPrefix, 404)
}) })
}
// API 路由组 func initPublicAPIRoutes(api *gin.RouterGroup, c *Controllers) {
api := root.Group("/api/v1")
{
// Health check (无需认证) // Health check (无需认证)
api.GET("/ping", func(ctx *gin.Context) { api.GET("/ping", func(ctx *gin.Context) {
ctx.JSON(200, gin.H{"message": "pong"}) ctx.JSON(200, gin.H{"message": "pong"})
@@ -182,8 +223,9 @@ func Setup(c *Controllers) *gin.Engine {
// 公开的站点设置(无需认证) // 公开的站点设置(无需认证)
api.GET("/settings/public", c.Settings.GetPublicSiteSettings) api.GET("/settings/public", c.Settings.GetPublicSiteSettings)
}
// 需要认证的路由 func initAuthorizedAPIRoutes(api *gin.RouterGroup, c *Controllers) {
authorized := api.Group("") authorized := api.Group("")
authorized.Use(middleware.AuthRequired()) authorized.Use(middleware.AuthRequired())
{ {
@@ -196,8 +238,29 @@ func Setup(c *Controllers) *gin.Engine {
authorized.GET("/sendstats", c.Dashboard.GetSendStats) authorized.GET("/sendstats", c.Dashboard.GetSendStats)
authorized.GET("/taskstats", c.Dashboard.GetTaskStats) authorized.GET("/taskstats", c.Dashboard.GetTaskStats)
// 任务模块 registerTaskRoutes(authorized, c)
tasks := authorized.Group("/tasks") registerEnvRoutes(authorized, c)
registerScriptRoutes(authorized, c)
registerFileRoutes(authorized, c)
registerLogRoutes(authorized, c)
registerTerminalRoutes(authorized, c)
registerSettingsRoutes(authorized, c)
registerDependencyRoutes(authorized, c)
registerAgentRoutes(authorized, c)
registerMiseRoutes(authorized, c)
registerNotificationRoutes(authorized, c)
}
// 通知发送 API(使用通知 Token 认证,供脚本调用)
notifyAPI := api.Group("/notify")
notifyAPI.Use(middleware.NotifyTokenAuth())
{
notifyAPI.POST("/send", c.Notification.SendNotification)
}
}
func registerTaskRoutes(g *gin.RouterGroup, c *Controllers) {
tasks := g.Group("/tasks")
{ {
tasks.POST("", c.Task.CreateTask) tasks.POST("", c.Task.CreateTask)
tasks.GET("", c.Task.GetTasks) tasks.GET("", c.Task.GetTasks)
@@ -207,16 +270,16 @@ func Setup(c *Controllers) *gin.Engine {
tasks.POST("/stop/:logID", c.Task.StopTask) tasks.POST("/stop/:logID", c.Task.StopTask)
} }
// 任务执行模块 execution := g.Group("/execute")
execution := authorized.Group("/execute")
{ {
execution.POST("/task/:id", c.Executor.ExecuteTask) execution.POST("/task/:id", c.Executor.ExecuteTask)
execution.POST("/command", c.Executor.ExecuteCommand) execution.POST("/command", c.Executor.ExecuteCommand)
execution.GET("/results", c.Executor.GetLastResults) execution.GET("/results", c.Executor.GetLastResults)
} }
}
// 环境变量模块 func registerEnvRoutes(g *gin.RouterGroup, c *Controllers) {
env := authorized.Group("/env") env := g.Group("/env")
{ {
env.POST("", c.Env.CreateEnvVar) env.POST("", c.Env.CreateEnvVar)
env.GET("", c.Env.GetEnvVars) env.GET("", c.Env.GetEnvVars)
@@ -226,9 +289,10 @@ func Setup(c *Controllers) *gin.Engine {
env.PUT("/:id", c.Env.UpdateEnvVar) env.PUT("/:id", c.Env.UpdateEnvVar)
env.DELETE("/:id", c.Env.DeleteEnvVar) env.DELETE("/:id", c.Env.DeleteEnvVar)
} }
}
// 脚本模块 func registerScriptRoutes(g *gin.RouterGroup, c *Controllers) {
scripts := authorized.Group("/scripts") scripts := g.Group("/scripts")
{ {
scripts.POST("", c.Script.CreateScript) scripts.POST("", c.Script.CreateScript)
scripts.GET("", c.Script.GetScripts) scripts.GET("", c.Script.GetScripts)
@@ -236,9 +300,10 @@ func Setup(c *Controllers) *gin.Engine {
scripts.PUT("/:id", c.Script.UpdateScript) scripts.PUT("/:id", c.Script.UpdateScript)
scripts.DELETE("/:id", c.Script.DeleteScript) scripts.DELETE("/:id", c.Script.DeleteScript)
} }
}
// 文件管理模块 func registerFileRoutes(g *gin.RouterGroup, c *Controllers) {
files := authorized.Group("/files") files := g.Group("/files")
{ {
files.GET("/tree", c.File.GetFileTree) files.GET("/tree", c.File.GetFileTree)
files.GET("/content", c.File.GetFileContent) files.GET("/content", c.File.GetFileContent)
@@ -252,9 +317,10 @@ func Setup(c *Controllers) *gin.Engine {
files.POST("/upload", c.File.UploadArchive) files.POST("/upload", c.File.UploadArchive)
files.POST("/uploadfiles", c.File.UploadFiles) files.POST("/uploadfiles", c.File.UploadFiles)
} }
}
// 日志查看模块 func registerLogRoutes(g *gin.RouterGroup, c *Controllers) {
logs := authorized.Group("/logs") logs := g.Group("/logs")
{ {
logs.GET("", c.Log.GetLogs) logs.GET("", c.Log.GetLogs)
logs.POST("/clear", c.Log.ClearLogs) logs.POST("/clear", c.Log.ClearLogs)
@@ -262,14 +328,16 @@ func Setup(c *Controllers) *gin.Engine {
logs.GET("/:id", c.Log.GetLogDetail) logs.GET("/:id", c.Log.GetLogDetail)
logs.DELETE("/:id", c.Log.DeleteLog) logs.DELETE("/:id", c.Log.DeleteLog)
} }
}
// 终端模块 func registerTerminalRoutes(g *gin.RouterGroup, c *Controllers) {
authorized.GET("/terminal/ws", c.Terminal.HandleWebSocket) g.GET("/terminal/ws", c.Terminal.HandleWebSocket)
authorized.POST("/terminal/exec", c.Terminal.ExecuteShellCommand) g.POST("/terminal/exec", c.Terminal.ExecuteShellCommand)
authorized.GET("/terminal/cmds", c.Terminal.GetCommands) g.GET("/terminal/cmds", c.Terminal.GetCommands)
}
// 设置中心模块 func registerSettingsRoutes(g *gin.RouterGroup, c *Controllers) {
settings := authorized.Group("/settings") settings := g.Group("/settings")
{ {
settings.POST("/password", c.Settings.ChangePassword) settings.POST("/password", c.Settings.ChangePassword)
settings.GET("/site", c.Settings.GetSiteSettings) settings.GET("/site", c.Settings.GetSiteSettings)
@@ -289,9 +357,10 @@ func Setup(c *Controllers) *gin.Engine {
settings.GET("/:section/:key", c.Settings.GetSetting) settings.GET("/:section/:key", c.Settings.GetSetting)
settings.POST("/:section/:key/generate", c.Settings.GenerateSettingToken) settings.POST("/:section/:key/generate", c.Settings.GenerateSettingToken)
} }
}
// Dependency routes (依赖管理) func registerDependencyRoutes(g *gin.RouterGroup, c *Controllers) {
deps := authorized.Group("/deps") deps := g.Group("/deps")
{ {
deps.GET("", c.Dependency.List) deps.GET("", c.Dependency.List)
deps.POST("", c.Dependency.Create) deps.POST("", c.Dependency.Create)
@@ -304,9 +373,10 @@ func Setup(c *Controllers) *gin.Engine {
deps.POST("/reinstall-all-cmd", c.Dependency.GetReinstallAllCommand) deps.POST("/reinstall-all-cmd", c.Dependency.GetReinstallAllCommand)
deps.GET("/installed", c.Dependency.GetInstalled) deps.GET("/installed", c.Dependency.GetInstalled)
} }
}
// Agent routes (Agent 管理) func registerAgentRoutes(g *gin.RouterGroup, c *Controllers) {
agents := authorized.Group("/agents") agents := g.Group("/agents")
{ {
agents.GET("", c.Agent.List) agents.GET("", c.Agent.List)
agents.GET("/version", c.Agent.GetVersion) agents.GET("/version", c.Agent.GetVersion)
@@ -320,8 +390,15 @@ func Setup(c *Controllers) *gin.Engine {
agents.DELETE("/tokens/:id", c.Agent.DeleteToken) agents.DELETE("/tokens/:id", c.Agent.DeleteToken)
} }
// Mise routes (Mise 管理) // Agent API(供前端调用,保持在 v1 下)
mise := authorized.Group("/mise") agentAPIv1 := g.Group("/agent")
{
agentAPIv1.GET("/download", c.Agent.Download)
}
}
func registerMiseRoutes(g *gin.RouterGroup, c *Controllers) {
mise := g.Group("/mise")
{ {
mise.GET("/ls", c.Mise.List) mise.GET("/ls", c.Mise.List)
mise.POST("/sync", c.Mise.Sync) mise.POST("/sync", c.Mise.Sync)
@@ -329,15 +406,10 @@ func Setup(c *Controllers) *gin.Engine {
mise.GET("/versions", c.Mise.Versions) mise.GET("/versions", c.Mise.Versions)
mise.GET("/verify-cmd", c.Mise.VerifyCommand) mise.GET("/verify-cmd", c.Mise.VerifyCommand)
} }
}
// Agent API(供前端调用,保持在 v1 下) func registerNotificationRoutes(g *gin.RouterGroup, c *Controllers) {
agentAPIv1 := authorized.Group("/agent") notify := g.Group("/notify")
{
agentAPIv1.GET("/download", c.Agent.Download)
}
// 通知推送模块
notify := authorized.Group("/notify")
{ {
notify.GET("/types", c.Notification.GetChannelTypes) notify.GET("/types", c.Notification.GetChannelTypes)
notify.GET("/channels", c.Notification.GetChannels) notify.GET("/channels", c.Notification.GetChannels)
@@ -348,16 +420,9 @@ func Setup(c *Controllers) *gin.Engine {
notify.POST("/bindings", c.Notification.SaveBinding) notify.POST("/bindings", c.Notification.SaveBinding)
notify.DELETE("/bindings/:id", c.Notification.DeleteBinding) notify.DELETE("/bindings/:id", c.Notification.DeleteBinding)
} }
} }
// 通知发送 API(使用通知 Token 认证,供脚本调用)
notifyAPI := api.Group("/notify")
notifyAPI.Use(middleware.NotifyTokenAuth())
{
notifyAPI.POST("/send", c.Notification.SendNotification)
}
}
func initAgentAPIRoutes(root *gin.RouterGroup, c *Controllers) {
// Agent API(供远程 Agent 调用,不使用 /v1 版本号) // Agent API(供远程 Agent 调用,不使用 /v1 版本号)
agentAPI := root.Group("/api/agent") agentAPI := root.Group("/api/agent")
{ {
@@ -367,34 +432,6 @@ func Setup(c *Controllers) *gin.Engine {
agentAPI.GET("/download", c.Agent.Download) // 也在这里注册,兼容 Agent 调用 agentAPI.GET("/download", c.Agent.Download) // 也在这里注册,兼容 Agent 调用
agentAPI.GET("/ws", c.Agent.WSConnect) // WebSocket 连接 agentAPI.GET("/ws", c.Agent.WSConnect) // WebSocket 连接
} }
// SPA 兜底路由 - 返回 index.html(HTML禁用缓存以保证实时同步)
// 必须在最后注册,作为兜底路由
router.NoRoute(func(ctx *gin.Context) {
path := ctx.Request.URL.Path
// 如果配置了前缀,只处理带前缀的路径
if urlPrefix != "" && !strings.HasPrefix(path, urlPrefix) {
ctx.Status(404)
return
}
// 解析实际的相对路径
relPath := strings.TrimPrefix(path, urlPrefix)
if !strings.HasPrefix(relPath, "/") {
relPath = "/" + relPath
}
// 拦截属于 API 或静态资源目录下不存在的请求,绝不能返回 index.html 造成前端 MIME 错误
if strings.HasPrefix(relPath, "/api/") || strings.HasPrefix(relPath, "/assets/") {
ctx.String(404, "Not Found")
return
}
serveSPA(ctx, urlPrefix, 200)
})
return router
} }
// serveSPA 注入配置并返回 index.html 给前端渲染 // serveSPA 注入配置并返回 index.html 给前端渲染
+2 -2
View File
@@ -53,7 +53,7 @@ func (p Pagination) Offset() int {
// PaginationData 分页数据 // PaginationData 分页数据
type PaginationData struct { type PaginationData struct {
List interface{} `json:"list"` Data interface{} `json:"data"`
Total int64 `json:"total"` Total int64 `json:"total"`
Page int `json:"page"` Page int `json:"page"`
PageSize int `json:"page_size"` PageSize int `json:"page_size"`
@@ -62,7 +62,7 @@ type PaginationData struct {
// PaginatedResponse 分页响应 // PaginatedResponse 分页响应
func PaginatedResponse(c *gin.Context, data interface{}, total int64, p Pagination) { func PaginatedResponse(c *gin.Context, data interface{}, total int64, p Pagination) {
Success(c, PaginationData{ Success(c, PaginationData{
List: data, Data: data,
Total: total, Total: total,
Page: p.Page, Page: p.Page,
PageSize: p.PageSize, PageSize: p.PageSize,
+4 -4
View File
@@ -344,7 +344,7 @@ export interface ExecutionResult {
} }
export interface TaskListResponse { export interface TaskListResponse {
list: Task[] data: Task[]
total: number total: number
page: number page: number
page_size: number page_size: number
@@ -365,7 +365,7 @@ export interface EnvVar {
} }
export interface EnvListResponse { export interface EnvListResponse {
list: EnvVar[] data: EnvVar[]
total: number total: number
page: number page: number
page_size: number page_size: number
@@ -396,7 +396,7 @@ export interface TaskLog {
} }
export interface LogListResponse { export interface LogListResponse {
list: TaskLog[] data: TaskLog[]
total: number total: number
page: number page: number
page_size: number page_size: number
@@ -457,7 +457,7 @@ export interface LoginLog {
} }
export interface LoginLogListResponse { export interface LoginLogListResponse {
list: LoginLog[] data: LoginLog[]
total: number total: number
page: number page: number
page_size: number page_size: number
+2 -2
View File
@@ -34,10 +34,10 @@ let searchTimer: ReturnType<typeof setTimeout> | null = null
async function loadEnvVars() { async function loadEnvVars() {
try { try {
const res = await api.env.list({ page: currentPage.value, page_size: pageSize.value, name: filterName.value || undefined }) const res = await api.env.list({ page: currentPage.value, page_size: pageSize.value, name: filterName.value || undefined })
envVars.value = res.list envVars.value = res.data
total.value = res.total total.value = res.total
// 初始化显示状态,根据数据库的 hidden 状态同步显示 // 初始化显示状态,根据数据库的 hidden 状态同步显示
res.list.forEach(env => { res.data.forEach(env => {
showValues.value[env.id] = !env.hidden showValues.value[env.id] = !env.hidden
}) })
} catch { toast.error('加载环境变量失败') } } catch { toast.error('加载环境变量失败') }
+1 -1
View File
@@ -76,7 +76,7 @@ async function loadLogs() {
params.status = filterStatus.value params.status = filterStatus.value
} }
const response = await api.logs.list(params) const response = await api.logs.list(params)
logs.value = response.list logs.value = response.data
total.value = response.total total.value = response.total
} catch { } catch {
toast.error('加载日志失败') toast.error('加载日志失败')
+1 -1
View File
@@ -81,7 +81,7 @@ async function loadLogs() {
page_size: pageSize.value, page_size: pageSize.value,
username: filterUsername.value || undefined username: filterUsername.value || undefined
}) })
logs.value = res.list logs.value = res.data
total.value = res.total total.value = res.total
} catch { } catch {
toast.error('加载登录日志失败') toast.error('加载登录日志失败')
+1 -1
View File
@@ -74,7 +74,7 @@ async function loadTasks() {
type: filterType.value === 'all' ? undefined : filterType.value, type: filterType.value === 'all' ? undefined : filterType.value,
agent_id: filterAgentId.value || undefined agent_id: filterAgentId.value || undefined
}) })
tasks.value = res.list tasks.value = res.data
total.value = res.total total.value = res.total
} catch { toast.error('加载任务失败') } } catch { toast.error('加载任务失败') }
} }