Files
panel/internal/route/route.go
T
耗子andClaude Opus 4.8 443516a2cf refactor!: 迁移至 samber/do 依赖注入与三层架构
依赖注入:
- 移除 google/wire(含 wire.go/wire_gen.go 与全部 ProviderSet),
  改用 samber/do v2 单注入器 + 双入口惰性构建
- 贡献模型替代命令式注册:路由 routes:、命令 commands:、
  任务 jobs: 前缀经 internal/registry 收集与校验
- 构造函数统一 func NewXxx(i do.Injector) (T, error)

三层架构:
- 补全用例层:每个 biz.XxxRepo 配 XxxUsecase,
  service/command/job 表现层只依赖用例,不再直接引用仓储

路由与文档:
- route/http.go 按域拆为声明式 Endpoint 贡献,
  Endpoint 承载登录白名单与端点限流语义
- 调试模式下提供 OpenAPI 3.1 文档:/openapi.json 与 /docs(Scalar),
  从 validate 标签生成

目录与依赖:
- internal/http/{middleware,request,rule} 拍平至 internal/*
- CLI 命令拆至 internal/command
- 日志轮转 timberjack 换为 libtnb/logrotate
- 升级 validator/cron/sessions/gormstore/sqlite/securecookie
- 数据库迁移保持 gormigrate 不变

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 21:54:22 +08:00

150 lines
5.2 KiB
Go

package route
import (
"net/http"
"runtime/debug"
"time"
"github.com/go-chi/chi/v5"
"github.com/libtnb/validator/contrib/openapi"
"github.com/samber/do/v2"
"github.com/acepanel/panel/v3/internal/middleware"
"github.com/acepanel/panel/v3/internal/registry"
"github.com/acepanel/panel/v3/pkg/config"
)
const RoutePrefix = "routes:"
var Package = do.Package(
do.LazyNamed(RoutePrefix+"user", UserRoutes), do.LazyNamed(RoutePrefix+"user_passkey", UserPasskeyRoutes),
do.LazyNamed(RoutePrefix+"user_token", UserTokenRoutes), do.LazyNamed(RoutePrefix+"safe", SafeRoutes),
do.LazyNamed(RoutePrefix+"task", TaskRoutes), do.LazyNamed(RoutePrefix+"home", HomeRoutes),
do.LazyNamed(RoutePrefix+"website", WebsiteRoutes), do.LazyNamed(RoutePrefix+"website_stat", WebsiteStatRoutes),
do.LazyNamed(RoutePrefix+"project", ProjectRoutes), do.LazyNamed(RoutePrefix+"database", DatabaseRoutes),
do.LazyNamed(RoutePrefix+"database_server", DatabaseServerRoutes), do.LazyNamed(RoutePrefix+"database_user", DatabaseUserRoutes),
do.LazyNamed(RoutePrefix+"database_redis", DatabaseRedisRoutes), do.LazyNamed(RoutePrefix+"database_elasticsearch", DatabaseElasticsearchRoutes),
do.LazyNamed(RoutePrefix+"cert", CertRoutes), do.LazyNamed(RoutePrefix+"backup", BackupRoutes),
do.LazyNamed(RoutePrefix+"backup_storage", BackupStorageRoutes), do.LazyNamed(RoutePrefix+"app", AppRoutes),
do.LazyNamed(RoutePrefix+"environment", EnvironmentRoutes), do.LazyNamed(RoutePrefix+"container", ContainerRoutes),
do.LazyNamed(RoutePrefix+"file", FileRoutes), do.LazyNamed(RoutePrefix+"cron", CronRoutes),
do.LazyNamed(RoutePrefix+"process", ProcessRoutes), do.LazyNamed(RoutePrefix+"firewall", FirewallRoutes),
do.LazyNamed(RoutePrefix+"ssh", SSHRoutes), do.LazyNamed(RoutePrefix+"systemctl", SystemctlRoutes),
do.LazyNamed(RoutePrefix+"setting", SettingRoutes), do.LazyNamed(RoutePrefix+"log", LogRoutes),
do.LazyNamed(RoutePrefix+"monitor", MonitorRoutes), do.LazyNamed(RoutePrefix+"webhook", WebHookRoutes),
do.LazyNamed(RoutePrefix+"template", TemplateRoutes), do.LazyNamed(RoutePrefix+"toolbox_network", ToolboxNetworkRoutes),
do.LazyNamed(RoutePrefix+"toolbox_system", ToolboxSystemRoutes), do.LazyNamed(RoutePrefix+"toolbox_benchmark", ToolboxBenchmarkRoutes),
do.LazyNamed(RoutePrefix+"toolbox_ssh", ToolboxSSHRoutes), do.LazyNamed(RoutePrefix+"toolbox_disk", ToolboxDiskRoutes),
do.LazyNamed(RoutePrefix+"toolbox_log", ToolboxLogRoutes), do.LazyNamed(RoutePrefix+"toolbox_migration", ToolboxMigrationRoutes),
do.LazyNamed(RoutePrefix+"ws", WsRoutes),
)
// ThrottleRule 端点级限流规则。
type ThrottleRule struct {
Tokens uint64
Interval time.Duration
}
// Endpoint 声明一个 HTTP 端点:如何服务,以及(经 Request/Response 样本)如何生成文档。
// 无 Request/Response 的端点(探针、WebSocket)不进 OpenAPI 文档。
type Endpoint struct {
Method string
Path string // 绝对路径,如 "/api/users"
Handler http.HandlerFunc
Summary string
Tags []string
Request any // request.* 样本
Response any // service.Envelope[...] 样本
Status int
Public bool // 登录白名单(MustLogin 放行)
Throttle *ThrottleRule // 非 nil 时端点级限流
}
// Endpoints 是一个模块对 HTTP 路由的贡献。
type Endpoints []Endpoint
// HTTP 将全部 "routes:*" 贡献注册到 r。
func HTTP(i do.Injector, r chi.Router) error {
conf := do.MustInvoke[*config.Config](i)
groups, err := registry.Collect[Endpoints](i, RoutePrefix)
if err != nil {
return err
}
for _, endpoints := range groups {
for _, e := range endpoints {
if e.Throttle != nil {
r.With(middleware.Throttle(conf.HTTP.IPHeader, e.Throttle.Tokens, e.Throttle.Interval)).Method(e.Method, e.Path, e.Handler)
} else {
r.Method(e.Method, e.Path, e.Handler)
}
}
}
return nil
}
// PublicPaths 收集去重后的登录白名单路径,供 MustLogin 中间件放行。
func PublicPaths(i do.Injector) ([]string, error) {
groups, err := registry.Collect[Endpoints](i, RoutePrefix)
if err != nil {
return nil, err
}
seen := make(map[string]struct{})
paths := make([]string, 0)
for _, endpoints := range groups {
for _, e := range endpoints {
if !e.Public {
continue
}
if _, ok := seen[e.Path]; ok {
continue
}
seen[e.Path] = struct{}{}
paths = append(paths, e.Path)
}
}
return paths, nil
}
// SpecJSON 从每个带文档样本的端点组装 OpenAPI 3 文档。
func SpecJSON(i do.Injector, title string) ([]byte, error) {
g := openapi.New(title, buildVersion(),
openapi.WithType(time.Time{}, &openapi.Schema{Type: "string", Format: "date-time"}),
)
groups, err := registry.Collect[Endpoints](i, RoutePrefix)
if err != nil {
return nil, err
}
for _, endpoints := range groups {
for _, e := range endpoints {
if e.Request == nil && e.Response == nil {
continue
}
if err := g.Add(e.Method, e.Path, openapi.Op{
Summary: e.Summary,
Tags: e.Tags,
Request: e.Request,
Response: e.Response,
Status: e.Status,
}); err != nil {
return nil, err
}
}
}
return g.JSON()
}
func buildVersion() string {
if info, ok := debug.ReadBuildInfo(); ok {
if v := info.Main.Version; v != "" && v != "(devel)" {
return v
}
}
return "dev"
}