新增部署专用 Dockerfile.deploy / docker-compose.deploy.yml 与本机构建脚本: 前端 dist 经 go:embed 编入 Go 二进制,alpine 运行时只打包不编译, 旧 Dockerfile 与双容器编排保持不变。
137 lines
4.0 KiB
Go
137 lines
4.0 KiB
Go
package main
|
||
|
||
import (
|
||
"io/fs"
|
||
"log"
|
||
"net/http"
|
||
"os"
|
||
"strings"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"github.com/wmms/wmms/internal/handler"
|
||
"github.com/wmms/wmms/internal/repository"
|
||
"github.com/wmms/wmms/internal/service"
|
||
"github.com/wmms/wmms/internal/web"
|
||
"github.com/wmms/wmms/pkg/database"
|
||
)
|
||
|
||
func main() {
|
||
dsn := os.Getenv("DB_DSN")
|
||
if dsn == "" {
|
||
dsn = "root:1234@tcp(localhost:3306)/wmms?charset=utf8mb4&parseTime=true"
|
||
}
|
||
|
||
err := database.Init(dsn)
|
||
if err != nil {
|
||
log.Fatalf("Failed to initialize database: %v", err)
|
||
}
|
||
defer database.Close()
|
||
if err := service.LoadRevokedSessions(); err != nil {
|
||
log.Fatalf("Failed to load revoked sessions; verify database upgrade: %v", err)
|
||
}
|
||
|
||
// 初始化用户表并创建默认用户
|
||
if err := repository.InitUserTable(); err != nil {
|
||
log.Fatalf("Failed to initialize users table: %v", err)
|
||
}
|
||
if err := repository.CreateDefaultUsers(); err != nil {
|
||
log.Fatalf("Failed to create default users: %v", err)
|
||
}
|
||
|
||
// 申请表的数据库结构统一由 scripts/init.sql(空库)或增量 SQL(升级)管理;
|
||
// 服务启动只校验运行所需的组织列是否已到位,缺列时报明确的升级错误,不再建表。
|
||
appRepo := repository.NewApplicationRepository()
|
||
if err := appRepo.CheckApplicationSchema(); err != nil {
|
||
log.Fatalf("Failed to verify applications schema: %v", err)
|
||
}
|
||
|
||
r := gin.Default()
|
||
|
||
registerFrontendRoutes(r)
|
||
|
||
// 提供上传的文件
|
||
r.Static("/uploads", "./uploads")
|
||
|
||
h := handler.NewHandler()
|
||
h.RegisterRoutes(r)
|
||
|
||
// WebSocket route
|
||
// 建连不走中间件:浏览器先凭 JWT 与公司头调用 /api/auth/websocket-ticket 换票,
|
||
// 再以 /ws?ticket=... 握手;HandleWebSocket 消费一次性票据并实时复核授权。
|
||
r.GET("/ws", handler.HandleWebSocket)
|
||
|
||
// 端口可通过 APP_PORT 覆盖,默认 8082,便于试用脚本探测空闲端口。
|
||
port := os.Getenv("APP_PORT")
|
||
if port == "" {
|
||
port = "8082"
|
||
}
|
||
addr := ":" + port
|
||
log.Printf("Server starting on %s", addr)
|
||
log.Println("Default users: admin/admin123, user/user123")
|
||
if err := r.Run(addr); err != nil {
|
||
log.Fatalf("Failed to start server: %v", err)
|
||
}
|
||
}
|
||
|
||
func registerFrontendRoutes(r *gin.Engine) {
|
||
dist, err := web.DistFS()
|
||
if err != nil {
|
||
log.Fatalf("embedded frontend unavailable: %v", err)
|
||
}
|
||
|
||
// 静态资源(js/css 等)直接由 embed FS 提供;带一年 immutable 缓存。
|
||
fileServer := http.FileServer(http.FS(dist))
|
||
r.GET("/assets/*filepath", func(c *gin.Context) {
|
||
c.Header("Cache-Control", "public, max-age=31536000, immutable")
|
||
c.Request.URL.Path = strings.TrimPrefix(c.Request.URL.Path, "/")
|
||
fileServer.ServeHTTP(c.Writer, c.Request)
|
||
})
|
||
|
||
indexHandler := func(c *gin.Context) {
|
||
data, err := web.IndexHTML()
|
||
if err != nil {
|
||
c.Status(http.StatusInternalServerError)
|
||
return
|
||
}
|
||
c.Data(http.StatusOK, "text/html; charset=utf-8", data)
|
||
}
|
||
r.GET("/", indexHandler)
|
||
|
||
r.NoRoute(func(c *gin.Context) {
|
||
path := c.Request.URL.Path
|
||
// API / WebSocket / uploads / 带扩展名的真实文件不做 SPA fallback
|
||
if c.Request.Method != "GET" || path == "/api" || strings.HasPrefix(path, "/api/") ||
|
||
path == "/ws" || strings.HasPrefix(path, "/uploads/") {
|
||
c.Status(http.StatusNotFound)
|
||
return
|
||
}
|
||
if ext := strings.LastIndex(path, "."); ext != -1 {
|
||
// 尝试从 embed 提供真实静态文件(favicon、图片等)
|
||
rel := strings.TrimPrefix(path, "/")
|
||
if b, err := fs.ReadFile(dist, rel); err == nil {
|
||
c.Data(http.StatusOK, mimeFor(rel), b)
|
||
return
|
||
}
|
||
c.Status(http.StatusNotFound)
|
||
return
|
||
}
|
||
// 其余路径交给 Vue Router(SPA)
|
||
indexHandler(c)
|
||
})
|
||
}
|
||
|
||
func mimeFor(name string) string {
|
||
switch {
|
||
case strings.HasSuffix(name, ".js"):
|
||
return "application/javascript; charset=utf-8"
|
||
case strings.HasSuffix(name, ".css"):
|
||
return "text/css; charset=utf-8"
|
||
case strings.HasSuffix(name, ".svg"):
|
||
return "image/svg+xml"
|
||
case strings.HasSuffix(name, ".html"):
|
||
return "text/html; charset=utf-8"
|
||
default:
|
||
return "application/octet-stream"
|
||
}
|
||
}
|