Merge remote-tracking branch 'origin/dev' into dev
This commit is contained in:
commit
44ef533aa0
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -11,3 +11,5 @@ warehouse-mms.exe
|
||||||
frontend/dist/
|
frontend/dist/
|
||||||
|
|
||||||
.history/
|
.history/
|
||||||
|
deploy/
|
||||||
|
internal/web/dist/
|
||||||
|
|
|
||||||
18
Dockerfile.deploy
Normal file
18
Dockerfile.deploy
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# 部署镜像:只打包本机已编译好的二进制(含嵌入前端),镜像内不做任何编译。
|
||||||
|
# 构建前置:先执行 scripts/deploy-build.sh 产出 deploy/server。
|
||||||
|
FROM alpine:3.20
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 运行时依赖:证书、时区;nc 由 busybox 自带(entrypoint 探活用)
|
||||||
|
RUN apk add --no-cache ca-certificates tzdata && mkdir -p /app/uploads
|
||||||
|
|
||||||
|
COPY deploy/server /app/server
|
||||||
|
COPY deploy/entrypoint.sh /app/entrypoint.sh
|
||||||
|
RUN chmod +x /app/entrypoint.sh
|
||||||
|
|
||||||
|
EXPOSE 8082
|
||||||
|
ENV GIN_MODE=release
|
||||||
|
|
||||||
|
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||||
|
CMD ["/app/server"]
|
||||||
|
|
@ -1,15 +1,17 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/wmms/wmms/internal/handler"
|
"github.com/wmms/wmms/internal/handler"
|
||||||
"github.com/wmms/wmms/internal/repository"
|
"github.com/wmms/wmms/internal/repository"
|
||||||
"github.com/wmms/wmms/internal/service"
|
"github.com/wmms/wmms/internal/service"
|
||||||
|
"github.com/wmms/wmms/internal/web"
|
||||||
"github.com/wmms/wmms/pkg/database"
|
"github.com/wmms/wmms/pkg/database"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -45,7 +47,7 @@ func main() {
|
||||||
|
|
||||||
r := gin.Default()
|
r := gin.Default()
|
||||||
|
|
||||||
registerFrontendRoutes(r, "./frontend/dist")
|
registerFrontendRoutes(r)
|
||||||
|
|
||||||
// 提供上传的文件
|
// 提供上传的文件
|
||||||
r.Static("/uploads", "./uploads")
|
r.Static("/uploads", "./uploads")
|
||||||
|
|
@ -71,19 +73,64 @@ func main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func registerFrontendRoutes(r *gin.Engine, distDir string) {
|
func registerFrontendRoutes(r *gin.Engine) {
|
||||||
r.Static("/web", distDir)
|
dist, err := web.DistFS()
|
||||||
r.Static("/assets", filepath.Join(distDir, "assets"))
|
if err != nil {
|
||||||
r.GET("/", func(c *gin.Context) {
|
log.Fatalf("embedded frontend unavailable: %v", err)
|
||||||
c.File(filepath.Join(distDir, "index.html"))
|
}
|
||||||
|
|
||||||
|
// 静态资源(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)
|
||||||
})
|
})
|
||||||
r.NoRoute(func(c *gin.Context) {
|
|
||||||
path := c.Request.URL.Path
|
indexHandler := func(c *gin.Context) {
|
||||||
if c.Request.Method != "GET" || path == "/api" || strings.HasPrefix(path, "/api/") ||
|
data, err := web.IndexHTML()
|
||||||
path == "/ws" || strings.HasPrefix(path, "/uploads/") || filepath.Ext(path) != "" {
|
if err != nil {
|
||||||
c.Status(404)
|
c.Status(http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.File(filepath.Join(distDir, "index.html"))
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
31
docker-compose.deploy.yml
Normal file
31
docker-compose.deploy.yml
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# WMS 生产部署(单容器,前端已嵌入 Go 二进制)
|
||||||
|
# 使用:
|
||||||
|
# ./scripts/deploy-build.sh # 本机编译前后端
|
||||||
|
# docker compose -f docker-compose.deploy.yml up -d --build
|
||||||
|
# 公共 MySQL/网络由仓库公共层提供,不自带数据库、不 publish 端口。
|
||||||
|
|
||||||
|
services:
|
||||||
|
wms:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.deploy
|
||||||
|
image: wms:local
|
||||||
|
container_name: wms
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
DB_DSN: "${WMS_DB_USER}:${WMS_DB_PASSWORD}@tcp(mysql:3306)/wms_default?charset=utf8mb4&parseTime=true"
|
||||||
|
GIN_MODE: release
|
||||||
|
volumes:
|
||||||
|
- ${VOL_DIR}/wms/uploads:/app/uploads
|
||||||
|
networks:
|
||||||
|
- services-net
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "-q", "--spider", "http://localhost:8082/api/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
|
|
||||||
|
networks:
|
||||||
|
services-net:
|
||||||
|
external: true
|
||||||
20
entrypoint-deploy.sh
Executable file
20
entrypoint-deploy.sh
Executable file
|
|
@ -0,0 +1,20 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# 等待公共 MySQL(services-net 内容器名 mysql)。
|
||||||
|
# alpine busybox 自带 nc,无需额外安装客户端。
|
||||||
|
echo "Waiting for MySQL (mysql:3306)..."
|
||||||
|
i=0
|
||||||
|
until nc -z mysql 3306 2>/dev/null; do
|
||||||
|
i=$((i+1))
|
||||||
|
if [ "$i" -ge 60 ]; then
|
||||||
|
echo "MySQL not reachable after ~120s, exiting." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
echo "MySQL is reachable."
|
||||||
|
|
||||||
|
# Schema 首次由人工灌入 wms_default;应用启动时自动 seed 默认 admin。
|
||||||
|
echo "Starting application..."
|
||||||
|
exec "$@"
|
||||||
22
internal/web/web.go
Normal file
22
internal/web/web.go
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
// Package web 通过 go:embed 持有前端构建产物(Vue3 dist)。
|
||||||
|
// dist 目录由 Dockerfile 的 node 构建阶段生成到本目录后再编译 Go,
|
||||||
|
// 因此最终二进制自带前端,无需独立 Nginx 容器。
|
||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"io/fs"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed all:dist
|
||||||
|
var distFS embed.FS
|
||||||
|
|
||||||
|
// DistFS 返回以 dist 内容为根的只读文件系统。
|
||||||
|
func DistFS() (fs.FS, error) {
|
||||||
|
return fs.Sub(distFS, "dist")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IndexHTML 返回前端入口页字节。
|
||||||
|
func IndexHTML() ([]byte, error) {
|
||||||
|
return distFS.ReadFile("dist/index.html")
|
||||||
|
}
|
||||||
27
scripts/deploy-build.sh
Executable file
27
scripts/deploy-build.sh
Executable file
|
|
@ -0,0 +1,27 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# 在本机编译前后端,产出供 Dockerfile.deploy 打包的文件。
|
||||||
|
# 产物:deploy/server(已 embed 前端 dist)、deploy/entrypoint.sh
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# 解析到服务根目录(脚本位于 scripts/)
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
cd "${ROOT}"
|
||||||
|
|
||||||
|
echo "==> 1/3 构建前端 (npm run build)"
|
||||||
|
cd frontend
|
||||||
|
[ -d node_modules ] || npm install
|
||||||
|
npm run build
|
||||||
|
cd "${ROOT}"
|
||||||
|
|
||||||
|
echo "==> 2/3 将 dist 放入 embed 目录 internal/web/dist"
|
||||||
|
rm -rf internal/web/dist
|
||||||
|
mkdir -p internal/web/dist
|
||||||
|
cp -r frontend/dist/. internal/web/dist/
|
||||||
|
|
||||||
|
echo "==> 3/3 本机编译 Go(前端嵌入二进制)"
|
||||||
|
mkdir -p deploy
|
||||||
|
CGO_ENABLED=0 GOOS=linux go build -o deploy/server ./cmd/server
|
||||||
|
cp entrypoint-deploy.sh deploy/entrypoint.sh
|
||||||
|
chmod +x deploy/entrypoint.sh
|
||||||
|
|
||||||
|
echo "done -> deploy/server (含前端)"
|
||||||
Loading…
Reference in New Issue
Block a user