material_manage_system/cmd/server/main.go
copper-xw 9c78dcb6db feat: 支持前端 go:embed 单容器部署
新增部署专用 Dockerfile.deploy / docker-compose.deploy.yml 与本机构建脚本:
前端 dist 经 go:embed 编入 Go 二进制,alpine 运行时只打包不编译,
旧 Dockerfile 与双容器编排保持不变。
2026-10-10 16:14:49 +08:00

137 lines
4.0 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 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"
}
}