Przeglądaj źródła

fix: resolve frontend build hash mismatch across servers

- Include public/build in Git to ensure consistent deploy across 4 servers
- Remove unstable /app/public/build anonymous volume from all mount yml files
- Add unified deploy.sh script for all server types (main/api/pdf)
- Build frontend locally before mount, so .:/app carries correct assets

Co-authored-by: Cursor <cursoragent@cursor.com>
yemeishu 4 dni temu
rodzic
commit
0cae40cda5

+ 1 - 1
.gitignore

@@ -14,7 +14,7 @@
 /.zed
 /auth.json
 /node_modules
-/public/build
+# /public/build — 不再忽略,构建产物纳入 Git 管理以确保部署一致性
 /public/hot
 /public/storage
 /bootstrap/cache/*

+ 192 - 43
deploy.sh

@@ -1,56 +1,205 @@
 #!/bin/bash
+#
+# math_cms 统一部署脚本
+#
+# 用法:
+#   ./deploy.sh                    # 部署第一台服务器(主站)
+#   ./deploy.sh api                # 部署第二台服务器(API)
+#   ./deploy.sh pdf                # 部署第三/四台服务器(PDF Worker)
+#   ./deploy.sh all                # 部署当前服务器所有服务
+#
+# 流程:git pull → 前端构建 → 清 Laravel 缓存 → 重建并重启容器
+#
 
 set -e
 
-echo "🚀 快速部署 Filament Admin"
+# ============ 配置 ============
+COMPOSE_FILES_MAIN="-f docker-compose.yml -f docker-compose.mount.yml"
+COMPOSE_FILES_API="-f docker-compose.api.yml -f docker-compose.api.mount.yml"
+COMPOSE_FILES_PDF="-f docker-compose.pdf.yml -f docker-compose.pdf.mount.yml"
 
-# 颜色定义
+# ============ 颜色 ============
 RED='\033[0;31m'
 GREEN='\033[0;32m'
 YELLOW='\033[1;33m'
 BLUE='\033[0;34m'
 NC='\033[0m'
 
-# 检查镜像是否存在
-if ! docker images filamentadmin:latest | grep -q filamentadmin; then
-    echo -e "${RED}❌ 错误:未找到镜像 filamentadmin:latest${NC}"
-    echo -e "${YELLOW}💡 请先运行 ./build.sh 构建镜像${NC}"
-    exit 1
-fi
-
-echo -e "${BLUE}ℹ️  部署信息:${NC}"
-echo "   镜像:filamentadmin:latest"
-echo "   端口:5019:8000"
-echo "   容器名:filament_admin"
-echo ""
-
-# 停止并删除现有容器
-if docker ps -a | grep -q filament_admin; then
-    echo -e "${YELLOW}🛑 停止现有容器...${NC}"
-    docker compose down
-fi
-
-# 启动新容器
-echo -e "${GREEN}▶️  启动容器...${NC}"
-docker compose up -d
-
-# 等待服务启动
-echo -e "${YELLOW}⏳ 等待服务启动...${NC}"
-sleep 5
-
-# 检查状态
-if docker compose ps | grep -q "Up"; then
-    echo -e "${GREEN}✅ 部署成功!${NC}"
+info()  { echo -e "${BLUE}[INFO]${NC} $1"; }
+ok()    { echo -e "${GREEN}[OK]${NC} $1"; }
+warn()  { echo -e "${YELLOW}[WARN]${NC} $1"; }
+error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }
+
+# ============ 步骤 1:拉取代码 ============
+git_pull() {
+    info "拉取最新代码..."
+    git fetch origin main
+    LOCAL=$(git rev-parse HEAD)
+    REMOTE=$(git rev-parse origin/main)
+
+    if [ "$LOCAL" = "$REMOTE" ]; then
+        ok "代码已是最新 ($LOCAL)"
+    else
+        git pull origin main
+        ok "代码已更新"
+    fi
+}
+
+# ============ 步骤 2:前端构建 ============
+build_frontend() {
+    info "构建前端资源..."
+
+    # 检测宿主机是否有 bun/npm
+    if command -v bun &>/dev/null; then
+        info "使用 bun 构建..."
+        bun install
+        bun run build
+    elif command -v npm &>/dev/null; then
+        info "使用 npm 构建..."
+        npm install --prefer-offline
+        npm run build
+    else
+        # 宿主机没有 Node.js,用 Docker 容器构建
+        info "宿主机无 Node.js,使用 Docker 容器构建..."
+        docker run --rm \
+            -v "$(pwd):/app" \
+            -w /app \
+            node:20-alpine \
+            sh -c "npm config set registry https://registry.npmmirror.com && npm install && npm run build"
+    fi
+
+    # 验证构建产物
+    if [ ! -f "public/build/manifest.json" ]; then
+        error "前端构建失败:public/build/manifest.json 不存在"
+    fi
+
+    ok "前端构建完成"
+    info "构建产物:"
+    ls -la public/build/assets/
+}
+
+# ============ 步骤 3:清 Laravel 缓存 ==========
+clear_cache() {
+    info "清除 Laravel 缓存..."
+
+    # 如果容器正在运行,通过容器执行
+    if docker ps --format '{{.Names}}' 2>/dev/null | grep -q 'math_cms_app'; then
+        local container=$(docker ps --format '{{.Names}}' | grep 'math_cms_app' | head -1)
+        docker exec "$container" php artisan view:clear 2>/dev/null || true
+        docker exec "$container" php artisan cache:clear 2>/dev/null || true
+        ok "通过容器清除缓存"
+    else
+        # 容器未运行时本地执行(需要 PHP)
+        php artisan view:clear 2>/dev/null || true
+        php artisan cache:clear 2>/dev/null || true
+        ok "本地清除缓存"
+    fi
+}
+
+# ============ 步骤 4:构建镜像并重启 ============
+deploy_services() {
+    local compose_files="$1"
+    shift
+    local services="$@"
+
+    info "构建 Docker 镜像..."
+    docker compose $compose_files build --no-cache app
+
+    if [ -z "$services" ]; then
+        services="app"
+    fi
+
+    info "重启服务: $services"
+    docker compose $compose_files up -d --no-deps --force-recreate $services
+
+    ok "服务已重启: $services"
+}
+
+# ============ 步骤 5:验证 ============
+verify() {
+    local compose_files="$1"
+
+    info "等待服务启动..."
+    sleep 5
+
+    # 检查容器状态
+    local status=$(docker compose $compose_files ps --format '{{.Status}}' 2>/dev/null | head -1)
+    if echo "$status" | grep -q 'Up'; then
+        ok "容器运行正常 ($status)"
+    else
+        warn "容器状态: $status"
+        warn "请检查日志: docker compose $compose_files logs --tail=50"
+    fi
+
+    # 检查前端资源是否可访问
+    local port=$(docker compose $compose_files port app 8000 2>/dev/null | cut -d: -f2)
+    if [ -n "$port" ]; then
+        local manifest_status=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:$port/build/manifest.json" 2>/dev/null || echo "000")
+        if [ "$manifest_status" = "200" ]; then
+            ok "前端资源验证通过 (manifest.json HTTP $manifest_status)"
+        else
+            warn "前端资源验证失败 (manifest.json HTTP $manifest_status)"
+        fi
+    fi
+}
+
+# ============ 主流程 ============
+main() {
+    local mode="${1:-main}"
+
     echo ""
-    echo -e "${BLUE}🌐 访问地址:${NC}"
-    echo -e "   http://localhost:5019/admin"
+    echo -e "${BLUE}========================================${NC}"
+    echo -e "${BLUE}  math_cms 部署脚本${NC}"
+    echo -e "${BLUE}========================================${NC}"
     echo ""
-    echo -e "${BLUE}📋 常用命令:${NC}"
-    echo -e "   查看日志:${GREEN}docker compose logs -f${NC}"
-    echo -e "   停止服务:${GREEN}docker compose down${NC}"
-    echo -e "   重启服务:${GREEN}docker compose restart${NC}"
-else
-    echo -e "${RED}❌ 部署失败,请检查日志:${NC}"
-    docker compose logs
-    exit 1
-fi
+
+    git_pull
+    build_frontend
+    clear_cache
+
+    case "$mode" in
+        main|1)
+            info "部署模式:主站(第一台服务器)"
+            deploy_services "$COMPOSE_FILES_MAIN" app queue pdf-worker gotenberg
+            verify "$COMPOSE_FILES_MAIN"
+            ;;
+        api|2)
+            info "部署模式:API(第二台服务器)"
+            deploy_services "$COMPOSE_FILES_API" app
+            verify "$COMPOSE_FILES_API"
+            ;;
+        pdf|3)
+            info "部署模式:PDF Worker(第三/四台服务器)"
+            deploy_services "$COMPOSE_FILES_PDF" app pdf-worker-1 pdf-worker-2 logic-worker-1 logic-worker-2 gotenberg
+            verify "$COMPOSE_FILES_PDF"
+            ;;
+        all)
+            info "部署模式:所有服务"
+            # 自动检测当前服务器使用的 compose 文件
+            if [ -f "docker-compose.pdf.yml" ] && docker ps --format '{{.Names}}' 2>/dev/null | grep -q 'pdf_worker'; then
+                deploy_services "$COMPOSE_FILES_PDF" app pdf-worker-1 pdf-worker-2 logic-worker-1 logic-worker-2 gotenberg
+                verify "$COMPOSE_FILES_PDF"
+            elif docker ps --format '{{.Names}}' 2>/dev/null | grep -q 'math_cms_queue'; then
+                deploy_services "$COMPOSE_FILES_MAIN" app queue pdf-worker gotenberg
+                verify "$COMPOSE_FILES_MAIN"
+            else
+                deploy_services "$COMPOSE_FILES_API" app
+                verify "$COMPOSE_FILES_API"
+            fi
+            ;;
+        *)
+            echo "用法: $0 [main|api|pdf|all]"
+            echo "  main  - 第一台服务器(主站,默认)"
+            echo "  api   - 第二台服务器(API)"
+            echo "  pdf   - 第三/四台服务器(PDF Worker)"
+            echo "  all   - 自动检测并部署当前服务器所有服务"
+            exit 1
+            ;;
+    esac
+
+    echo ""
+    ok "部署完成!"
+    echo ""
+}
+
+main "$@"

+ 6 - 7
docker-compose.api.mount.yml

@@ -1,17 +1,16 @@
 # API 服务器的代码卷映射配置
-# 用于快速部署/联调:首次 build 后,日常只需 git pull + 重启容器
+# 用于快速部署:只需 ./deploy.sh api
 #
-# 使用方式:docker compose -f docker-compose.api.yml -f docker-compose.api.mount.yml up -d
+# 使用方式:./deploy.sh api
+#   或手动:docker compose -f docker-compose.api.yml -f docker-compose.api.mount.yml up -d
+#
+# 前端构建产物由 deploy.sh 在宿主机上构建后通过 .:/app 挂载进入容器
 
 services:
   app:
-    build:
-      context: .
-      target: app-runtime-api-hot
     volumes:
-      - .:/app                          # 代码目录映射
+      - .:/app                          # 代码目录映射(含 public/build)
       - /app/vendor                     # 排除 vendor(用镜像里的)
       - /app/node_modules               # 排除 node_modules
-      - /app/public/build               # 排除前端构建产物(用镜像里的)
       - ./storage:/app/storage          # 保留:日志 + 临时文件
       - ./.env:/app/.env                # 保留:环境配置

+ 7 - 6
docker-compose.mount.yml

@@ -1,15 +1,18 @@
 # 代码卷映射模式的 docker-compose 配置
-# 用于快速部署:只需 git pull + 清缓存,无需 build
+# 用于快速部署:只需 ./deploy.sh main,无需手动 build
 #
-# 使用方式:docker compose -f docker-compose.yml -f docker-compose.mount.yml up -d
+# 使用方式:./deploy.sh main
+#   或手动:docker compose -f docker-compose.yml -f docker-compose.mount.yml up -d
+#
+# 前端构建产物由 deploy.sh 在宿主机上构建后通过 .:/app 挂载进入容器
+# 不再使用 /app/public/build anonymous volume(避免哈希不同步导致 404)
 
 services:
   app:
     volumes:
-      - .:/app                          # 代码目录映射
+      - .:/app                          # 代码目录映射(含 public/build)
       - /app/vendor                     # 排除 vendor(用镜像里的)
       - /app/node_modules               # 排除 node_modules
-      - /app/public/build               # 排除前端构建产物(用镜像里的)
       - ./storage:/app/storage          # 保留:日志 + 临时文件
       - ./.env:/app/.env                # 保留:环境配置
 
@@ -18,7 +21,6 @@ services:
       - .:/app
       - /app/vendor
       - /app/node_modules
-      - /app/public/build
       - ./storage:/app/storage
       - ./.env:/app/.env
 
@@ -27,6 +29,5 @@ services:
       - .:/app
       - /app/vendor
       - /app/node_modules
-      - /app/public/build
       - ./storage:/app/storage
       - ./.env:/app/.env

+ 6 - 8
docker-compose.pdf.mount.yml

@@ -1,15 +1,17 @@
 # PDF 服务器的代码卷映射配置
-# 用于快速部署:只需 git pull + 重启容器,无需 build
+# 用于快速部署:只需 ./deploy.sh pdf
 #
-# 使用方式:docker compose -f docker-compose.pdf.yml -f docker-compose.pdf.mount.yml up -d
+# 使用方式:./deploy.sh pdf
+#   或手动:docker compose -f docker-compose.pdf.yml -f docker-compose.pdf.mount.yml up -d
+#
+# 前端构建产物由 deploy.sh 在宿主机上构建后通过 .:/app 挂载进入容器
 
 services:
   app:
     volumes:
-      - .:/app                          # 代码目录映射
+      - .:/app                          # 代码目录映射(含 public/build)
       - /app/vendor                     # 排除 vendor(用镜像里的)
       - /app/node_modules               # 排除 node_modules
-      - /app/public/build               # 排除前端构建产物(用镜像里的)
       - ./storage:/app/storage          # 保留:日志 + 临时文件
       - ./.env:/app/.env                # 保留:环境配置
 
@@ -18,7 +20,6 @@ services:
       - .:/app
       - /app/vendor
       - /app/node_modules
-      - /app/public/build
       - ./storage:/app/storage
       - ./.env:/app/.env
 
@@ -27,7 +28,6 @@ services:
       - .:/app
       - /app/vendor
       - /app/node_modules
-      - /app/public/build
       - ./storage:/app/storage
       - ./.env:/app/.env
 
@@ -36,7 +36,6 @@ services:
       - .:/app
       - /app/vendor
       - /app/node_modules
-      - /app/public/build
       - ./storage:/app/storage
       - ./.env:/app/.env
 
@@ -45,6 +44,5 @@ services:
       - .:/app
       - /app/vendor
       - /app/node_modules
-      - /app/public/build
       - ./storage:/app/storage
       - ./.env:/app/.env

Plik diff jest za duży
+ 0 - 0
public/build/assets/app-BnXFdCbE.css


Plik diff jest za duży
+ 0 - 0
public/build/assets/app-CuPzC284.js


+ 1 - 0
public/build/assets/chunk-utils-Cj8bMZRZ.js

@@ -0,0 +1 @@
+import{i as n}from"./chunk-vendor-B7aWpGE5.js";var r=function(n,r,t){return n>=r&&n<=t};function t(n){for(var r=[],t=n.length,u=0;u<t-1;u++){var a=n[u],i=n[u+1];r.push({from:{x:a[0],y:a[1]},to:{x:i[0],y:i[1]}})}if(r.length>1){var f=n[0],o=n[t-1];r.push({from:{x:o[0],y:o[1]},to:{x:f[0],y:f[1]}})}return r}function u(n){var r=n.map(function(n){return n[0]}),t=n.map(function(n){return n[1]});return{minX:Math.min.apply(null,r),maxX:Math.max.apply(null,r),minY:Math.min.apply(null,t),maxY:Math.max.apply(null,t)}}function a(a,i){if(a.length<2||i.length<2)return!1;var f,o;if(f=u(a),(o=u(i)).minX>f.maxX||o.maxX<f.minX||o.minY>f.maxY||o.maxY<f.minY)return!1;var m=!1;if(i.forEach(function(r){if(n(a,r[0],r[1]))return m=!0,!1}),m)return!0;if(a.forEach(function(r){if(n(i,r[0],r[1]))return m=!0,!1}),m)return!0;var e=t(a),x=t(i),l=!1;return x.forEach(function(n){if(function(n,t){var u=!1;return n.forEach(function(n){if(function(n,t,u,a){var i=u.x-n.x,f=u.y-n.y,o=t.x-n.x,m=t.y-n.y,e=a.x-u.x,x=a.y-u.y,l=o*x-m*e,c=null;if(l*l>.001*(o*o+m*m)*(e*e+x*x)){var y=(i*x-f*e)/l,h=(i*m-f*o)/l;r(y,0,1)&&r(h,0,1)&&(c={x:n.x+y*o,y:n.y+y*m})}return c}(n.from,n.to,t.from,t.to))return u=!0,!1}),u}(e,n))return l=!0,!1}),l}export{a as i};

Plik diff jest za duży
+ 0 - 0
public/build/assets/chunk-vendor-B7aWpGE5.js


+ 38 - 0
public/build/manifest.json

@@ -0,0 +1,38 @@
+{
+  "_chunk-utils-Cj8bMZRZ.js": {
+    "file": "assets/chunk-utils-Cj8bMZRZ.js",
+    "name": "chunk-utils",
+    "imports": [
+      "_chunk-vendor-B7aWpGE5.js"
+    ]
+  },
+  "_chunk-vendor-B7aWpGE5.js": {
+    "file": "assets/chunk-vendor-B7aWpGE5.js",
+    "name": "chunk-vendor",
+    "imports": [
+      "_chunk-utils-Cj8bMZRZ.js"
+    ]
+  },
+  "resources/css/app.css": {
+    "file": "assets/app-BnXFdCbE.css",
+    "src": "resources/css/app.css",
+    "isEntry": true,
+    "name": "app",
+    "names": [
+      "app.css"
+    ]
+  },
+  "resources/js/app.js": {
+    "file": "assets/app-CuPzC284.js",
+    "name": "app",
+    "src": "resources/js/app.js",
+    "isEntry": true,
+    "imports": [
+      "_chunk-vendor-B7aWpGE5.js",
+      "_chunk-utils-Cj8bMZRZ.js"
+    ],
+    "css": [
+      "assets/app-BnXFdCbE.css"
+    ]
+  }
+}

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików