Files
usernode/internal/router/router.go
T
cao.wangrenbo ae45aba607 feat(M0): 工程骨架 — Go 后端 + Vue3 前端脚手架
- Go 工程:cmd/usernode CLI(serve/migrate/admin create/reset-password/user otp)
  + internal/{config,model,service,system,auth,cron,api,router,server,pkg,webui}
- 配置:TOML + 环境变量覆盖(USERNODE_<SEC>_<FIELD>),config.example.toml
- 数据:GORM 双驱动(SQLite/MySQL)8 表模型,migrate 子命令可跑通
- 系统层:system.Manager 接口(useradd/userdel/passwd 白名单,dev dry-run)
- HTTP:Gin 路由骨架(/api/v1 + 501 占位),healthz,slog 结构化日志,优雅启停
- 前端:Vite + Vue3 + TS + Element Plus 最小可运行(web/),go:embed 打通
- 构建:deploy/Containerfile 多阶段单二进制镜像,web/Containerfile.dev 前端 dev
  镜像,Makefile(build/test/dev/web-build/image);go.mod 锁定 go 1.22
- 验证:go build/vet/test 通过;podman 镜像构建运行 healthz+embed 通过
2026-08-29 22:43:27 +08:00

82 lines
2.7 KiB
Go
Raw 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 负责路由注册与中间件装配。
package router
import (
"log/slog"
"net/http"
"time"
"github.com/gin-gonic/gin"
"ws_usernode/internal/api"
"ws_usernode/internal/config"
"ws_usernode/internal/webui"
)
// New 构建根 routerAPI v1 + 前端静态资源(go:embed)。
// production 模式启用 gin.ReleaseMode;否则启用调试模式与开发日志。
func New(cfg *config.Config, h *api.Handler, log *slog.Logger) *gin.Engine {
if cfg.App.Env == "production" {
gin.SetMode(gin.ReleaseMode)
}
r := gin.New()
r.Use(gin.Recovery(), requestLogger(log))
// 健康检查(不进 /api/v1 前缀,便于负载均衡/探针)
r.GET("/healthz", h.Health.Healthz)
// RESTful API v1
v1 := r.Group("/api/v1")
{
auth := v1.Group("/auth")
{
// M1captcha / otp/send / otp/login / admin/login / logout / me
auth.GET("/captcha", notImplemented("图形验证码(M1"))
auth.POST("/otp/send", notImplemented("OTP 发送(M1"))
auth.POST("/otp/login", notImplemented("OTP 登录(M1"))
auth.POST("/admin/login", notImplemented("管理员登录(M1"))
auth.POST("/logout", notImplemented("登出(M1"))
auth.GET("/me", notImplemented("当前会话(M1"))
}
v1.GET("/users", h.User.List)
v1.POST("/users", h.User.Create)
v1.POST("/users/:id/disable", notImplemented("禁用用户(M1"))
v1.POST("/users/:id/enable", notImplemented("启用用户(M1"))
v1.POST("/users/:id/extend", notImplemented("延期(M1"))
v1.POST("/approvals", notImplemented("提交申请(M3"))
v1.GET("/approvals", notImplemented("申请列表(M3"))
v1.POST("/approvals/:id/review", notImplemented("审批(M3"))
v1.GET("/audit", notImplemented("审计查询(M4"))
v1.GET("/audit/export", notImplemented("审计导出(M4"))
v1.GET("/settings", notImplemented("设置(M4"))
v1.PUT("/settings", notImplemented("设置(M4"))
}
// 前端静态资源(go:embeddev 阶段由 Vite dev server 代理,见 Makefile dev
r.NoRoute(gin.WrapH(webui.NewHandler()))
return r
}
// notImplemented 返回 501 占位 handler,标注里程碑。
func notImplemented(what string) gin.HandlerFunc {
return func(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"error": "接口 " + what + " 尚未实现"})
}
}
// requestLogger 以 slog 输出结构化请求日志。
func requestLogger(log *slog.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next()
log.Info("http",
"method", c.Request.Method,
"path", c.Request.URL.Path,
"status", c.Writer.Status(),
"ip", c.ClientIP(),
"latency_ms", time.Since(start).Milliseconds(),
)
}
}