commit 010f0b30be0cba3d684aaeedb57f32c7f0bcf6ca Author: leiyun Date: Tue May 26 22:19:27 2026 +0800 feat: 提交 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1882caf --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +# 打包产物 +isolated/dist/ +*.tar +*.tar.gz +*.tgz + +# 真实环境变量文件,只提交 .env.example +.env +*.env +!.env.example +!**/.env.example + +# 临时文件 +*.log +*.tmp +.DS_Store +Thumbs.db + diff --git a/isolated/.env.example b/isolated/.env.example new file mode 100644 index 0000000..8bbd4bf --- /dev/null +++ b/isolated/.env.example @@ -0,0 +1,154 @@ +# EMP 隔离测试环境变量模板 +# 使用方式:复制为 .env 后按目标服务器实际情况修改。 + +# ----------------------------------------------------------------------------- +# 镜像与项目名称 +# ----------------------------------------------------------------------------- +COMPOSE_PROJECT_NAME=emp-test +CONTAINER_PREFIX=emp-test +IMAGE_NAMESPACE=emp-test +IMAGE_TAG=latest + +# 服务器外网 IP 或域名。 +# Kafka 对外访问会把这个地址写入 advertised.listeners,外部客户端必须能访问它。 +PUBLIC_HOST=127.0.0.1 + +# ----------------------------------------------------------------------------- +# 对外端口 +# ----------------------------------------------------------------------------- +# 前端访问:http://PUBLIC_HOST:ADMIN_HOST_PORT +ADMIN_HOST_PORT=4081 +GATEWAY_HOST_PORT=9000 +WS_HOST_PORT=3000 +PDF_HOST_PORT=3100 +NACOS_HOST_PORT=9008 +NACOS_GRPC_HOST_PORT=10008 + +# MySQL / Kafka / TDengine 需要开放到宿主机,便于 Navicat、Kafka 客户端、TDengine Web/REST 调试。 +# 如果宿主机端口已被其他项目占用,改这里即可。 +MYSQL_HOST_PORT=13306 +KAFKA_HOST_PORT=19094 +TDENGINE_HOST_PORT=6030 +TDENGINE_REST_HOST_PORT=6041 +TDENGINE_RPC_HOST_PORT=6043 +TDENGINE_RPC_UDP_HOST_PORT=6044 +TDENGINE_KEEPER_HOST_PORT=6060 + +# Redis 默认只绑定本机,避免直接暴露公网;确实需要外部访问再改成 0.0.0.0。 +REDIS_BIND_HOST=127.0.0.1 +REDIS_HOST_PORT=16379 + +# ----------------------------------------------------------------------------- +# 中间件镜像 +# ----------------------------------------------------------------------------- +MYSQL_IMAGE=mysql:8.0 +REDIS_IMAGE=redis:7-alpine +KAFKA_IMAGE=bitnami/kafka:3.7.0 +TDENGINE_IMAGE=tdengine/tdengine:3.3.6.0 +NACOS_IMAGE=nacos/nacos-server:v2.3.2-slim + +# ----------------------------------------------------------------------------- +# MySQL 8.0 +# ----------------------------------------------------------------------------- +MYSQL_DATABASE=emp +MYSQL_ROOT_PASSWORD=change-me-mysql-root +DB_URL=jdbc:mysql://mysql:3306/emp?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai +DB_USER=root +DB_PWD=change-me-mysql-root + +# 给 emp_ws 模拟器读取车辆档案使用。 +SIMULATOR_DB_HOST=mysql +SIMULATOR_DB_PORT=3306 +SIMULATOR_DB_USER=root +SIMULATOR_DB_PASSWORD=change-me-mysql-root +SIMULATOR_DB_DATABASE=emp +SIMULATOR_DB_LIMIT=0 + +# ----------------------------------------------------------------------------- +# Redis +# ----------------------------------------------------------------------------- +REDIS_HOST=redis +REDIS_PORT=6379 +REDIS_PASSWORD=change-me-redis +REDIS_DB=0 + +# ----------------------------------------------------------------------------- +# Kafka +# ----------------------------------------------------------------------------- +KAFKA_BROKERS=kafka:9092 +KAFKA_GROUP_ID=ecmp-data-group-v2 +KAFKA_TOPIC=vehicle-data +KAFKA_USER= +KAFKA_PWD= + +SIMULATOR_KAFKA_BROKERS=kafka:9092 +SIMULATOR_KAFKA_TOPIC=vehicle-data +SIMULATOR_KAFKA_USER= +SIMULATOR_KAFKA_PASSWORD= +SIMULATOR_KAFKA_CLIENT_ID=emp-simulator +SIMULATOR_KAFKA_BATCH_SIZE=500 + +# ----------------------------------------------------------------------------- +# TDengine +# ----------------------------------------------------------------------------- +TDENGINE_DATABASE=emp +TDENGINE_USER=root +TDENGINE_PWD=taosdata +TDENGINE_URL=jdbc:TAOS-RS://tdengine:6041/emp + +# ----------------------------------------------------------------------------- +# Nacos +# ----------------------------------------------------------------------------- +NACOS_ADDR=nacos:8848 +NACOS_USER=nacos +NACOS_PWD=nacos +NACOS_AUTH_ENABLE=true +NACOS_AUTH_IDENTITY_KEY=emp +NACOS_AUTH_IDENTITY_VALUE=emp2026 +NACOS_AUTH_TOKEN=ZW1wLXBsYXRmb3JtLW5hY29zLXNlY3JldC1rZXktMjAyNg== + +# ----------------------------------------------------------------------------- +# 后端通用配置 +# ----------------------------------------------------------------------------- +SPRING_PROFILES_ACTIVE=prod +JWT_SECRET=emp-platform-secret-key-2026-yjfs +JWT_EXPIRATION=86400000 +SCHEDULER_ENABLED=true + +# ----------------------------------------------------------------------------- +# WebSocket / 模拟器 +# ----------------------------------------------------------------------------- +EMP_WS_ENV=production +NODE_ENV=production +PORT=3000 +WS_INSTANCES=1 +WS_HOST=emp-ws +SERVER_API_BASE_URL=http://emp-gateway:9000/api +SIMULATOR_ADMIN_USERNAME=admin +SIMULATOR_LOGIN_AUTH=88871fe697e860463cd062cf3705b16f +SIMULATOR_JWT_SECRET=emp-platform-secret-key-2026-yjfs + +# ----------------------------------------------------------------------------- +# PDF 与前端地址 +# ----------------------------------------------------------------------------- +PDF_SERVICE_URL=http://emp-pdf:3100 +PDF_FRONTEND_BASE_URL=http://127.0.0.1:4081 + +# ----------------------------------------------------------------------------- +# 第三方配置,按需填写 +# ----------------------------------------------------------------------------- +AMAP_KEY= +COS_SECRET_ID=change-me +COS_SECRET_KEY=change-me +COS_REGION=ap-chengdu +COS_BUCKET=emp-example-bucket + +SYNC_BASE_URL=https://example.com +SYNC_TK=change-me +SYNC_TENANT_ID=change-me +SYNC_REPORT_CRON=0 30 2 * * ? +SYNC_REPORT_SYNC_ENABLED=false +SYNC_REPORT_SYNC_CONCURRENCY=3 +SYNC_REPORT_SYNC_GROUP_NAMES= +SYNC_REPORT_CACHE_MISS_FETCH_ENABLED=false +GROUP_REPORT_CRON=0 30 4 ? * THU,SUN diff --git a/isolated/README.md b/isolated/README.md new file mode 100644 index 0000000..1decc77 --- /dev/null +++ b/isolated/README.md @@ -0,0 +1,188 @@ +# EMP 隔离测试环境部署说明 + +这套部署方案用于“服务器不放源码,只运行本地打好的 Docker 镜像包”的场景。 + +## 目录说明 + +本地项目中: + +```text +deploy/isolated/ + build-package.ps1 # Windows 本地打包 + build-package.sh # Linux / macOS / WSL 本地打包 + docker-compose.runtime.yml # 服务器运行用 compose 模板 + .env.example # 服务器运行配置模板 + install.sh # 服务器安装脚本 + dockerfiles/ # 本地构建镜像用 Dockerfile + nginx/admin.conf # 前端 nginx 代理配置 +``` + +打包后产物在: + +```text +deploy/isolated/dist/emp-test-runtime-时间戳.tar.gz +``` + +服务器解压后只需要: + +```text +docker-compose.yml +.env +install.sh +images.tar +README.md +``` + +## 本地打包 + +Windows PowerShell: + +```powershell +cd E:\emp\deploy\isolated +.\build-package.ps1 +``` + +Linux / WSL: + +```bash +cd /path/to/emp/deploy/isolated +sh build-package.sh +``` + +如果本机已经构建好所有业务镜像,只想重新生成安装包: + +```bash +SKIP_BUILD=1 sh build-package.sh +``` + +国内 npm 慢时可以指定源: + +```bash +NPM_REGISTRY=https://registry.npmmirror.com sh build-package.sh +``` + +默认会把 MySQL、Redis、Kafka、TDengine、Nacos 的中间件镜像一起打进 `images.tar`,服务器不需要访问 Docker Hub。 + +## 上传服务器 + +```bash +scp deploy/isolated/dist/emp-test-runtime-*.tar.gz root@服务器IP:/opt/ +``` + +服务器执行: + +```bash +mkdir -p /opt/emp-test +tar -xzf /opt/emp-test-runtime-*.tar.gz -C /opt/emp-test --strip-components=1 +cd /opt/emp-test +cp .env.example .env +vi .env +sh install.sh +``` + +## 必改配置 + +`.env` 中至少要改这些: + +```env +PUBLIC_HOST=服务器外网IP或域名 +MYSQL_ROOT_PASSWORD=强密码 +DB_PWD=同 MYSQL_ROOT_PASSWORD +SIMULATOR_DB_PASSWORD=同 MYSQL_ROOT_PASSWORD +REDIS_PASSWORD=强密码 +PDF_FRONTEND_BASE_URL=http://服务器外网IP:4081 +``` + +如果端口被宿主机其他项目占用,改这些端口: + +```env +ADMIN_HOST_PORT=4081 +GATEWAY_HOST_PORT=9000 +MYSQL_HOST_PORT=13306 +KAFKA_HOST_PORT=19094 +TDENGINE_REST_HOST_PORT=6041 +``` + +## 对外连接 + +MySQL 8.0: + +```text +host: 服务器外网IP +port: MYSQL_HOST_PORT,默认 13306 +user: root +password: MYSQL_ROOT_PASSWORD +database: emp +``` + +Kafka: + +```text +bootstrap.servers=PUBLIC_HOST:KAFKA_HOST_PORT +默认端口:19094 +``` + +TDengine REST: + +```text +http://PUBLIC_HOST:TDENGINE_REST_HOST_PORT +默认端口:6041 +``` + +Nacos: + +```text +http://PUBLIC_HOST:NACOS_HOST_PORT/nacos +默认账号:nacos +默认密码:nacos +``` + +前端: + +```text +http://PUBLIC_HOST:ADMIN_HOST_PORT +``` + +## 数据导入 + +MySQL 导入示例: + +```bash +docker compose --env-file .env -f docker-compose.yml -p emp-test exec -T mysql \ + mysql -uroot -p"$MYSQL_ROOT_PASSWORD" emp < emp.sql +``` + +TDengine 导入建议仍使用 `taosdump`: + +```bash +docker cp td_dump_dir emp-test-tdengine-1:/tmp/td_dump +docker compose --env-file .env -f docker-compose.yml -p emp-test exec tdengine \ + taosdump -u root -p"$TDENGINE_PWD" -i /tmp/td_dump +``` + +## 常用命令 + +查看状态: + +```bash +docker compose --env-file .env -f docker-compose.yml -p emp-test ps +``` + +看日志: + +```bash +docker compose --env-file .env -f docker-compose.yml -p emp-test logs -f emp-gateway +``` + +停止: + +```bash +docker compose --env-file .env -f docker-compose.yml -p emp-test down +``` + +停止并删除数据卷: + +```bash +docker compose --env-file .env -f docker-compose.yml -p emp-test down -v +``` + diff --git a/isolated/build-package.ps1 b/isolated/build-package.ps1 new file mode 100644 index 0000000..d353430 --- /dev/null +++ b/isolated/build-package.ps1 @@ -0,0 +1,205 @@ +param( + [string]$ImageNamespace = $(if ($env:IMAGE_NAMESPACE) { $env:IMAGE_NAMESPACE } else { "emp-test" }), + [string]$ImageTag = $(if ($env:IMAGE_TAG) { $env:IMAGE_TAG } else { Get-Date -Format "yyyyMMddHHmmss" }), + [string]$NpmRegistry = $(if ($env:NPM_REGISTRY) { $env:NPM_REGISTRY } else { "https://registry.npmjs.org" }), + [switch]$SkipBuild, + [switch]$NoMiddlewareImages +) + +$ErrorActionPreference = "Stop" + +# Local package script. Build images locally, export them, then run on server only with docker load + compose up. +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$RootDir = Resolve-Path (Join-Path $ScriptDir "..\..") +$DistDir = Join-Path $ScriptDir "dist" +$PackageName = "emp-test-runtime-$ImageTag" +$PackageDir = Join-Path $DistDir $PackageName +$PackageArchive = Join-Path $DistDir "$PackageName.tar.gz" + +$JavaModules = @("emp_gateway", "emp_auth", "emp_monitor", "emp_data") +$AppImages = @( + "$ImageNamespace/emp-gateway:$ImageTag", + "$ImageNamespace/emp-gateway:latest", + "$ImageNamespace/emp-auth:$ImageTag", + "$ImageNamespace/emp-auth:latest", + "$ImageNamespace/emp-monitor:$ImageTag", + "$ImageNamespace/emp-monitor:latest", + "$ImageNamespace/emp-data:$ImageTag", + "$ImageNamespace/emp-data:latest", + "$ImageNamespace/emp-pdf:$ImageTag", + "$ImageNamespace/emp-pdf:latest", + "$ImageNamespace/emp-ws:$ImageTag", + "$ImageNamespace/emp-ws:latest", + "$ImageNamespace/emp-admin:$ImageTag", + "$ImageNamespace/emp-admin:latest" +) + +$MiddlewareImages = @( + $(if ($env:MYSQL_IMAGE) { $env:MYSQL_IMAGE } else { "mysql:8.0" }), + $(if ($env:REDIS_IMAGE) { $env:REDIS_IMAGE } else { "redis:7-alpine" }), + $(if ($env:KAFKA_IMAGE) { $env:KAFKA_IMAGE } else { "bitnami/kafka:3.7.0" }), + $(if ($env:TDENGINE_IMAGE) { $env:TDENGINE_IMAGE } else { "tdengine/tdengine:3.3.6.0" }), + $(if ($env:NACOS_IMAGE) { $env:NACOS_IMAGE } else { "nacos/nacos-server:v2.3.2-slim" }) +) + +function Write-Log { + param([string]$Message) + Write-Host "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] $Message" +} + +function Add-KnownToolPaths { + $KnownDirs = @( + "C:\Program Files\Docker\Docker\resources\bin", + "C:\Program Files\Docker\Docker", + "$env:ProgramFiles\Docker\Docker\resources\bin" + ) + + foreach ($Dir in $KnownDirs) { + if ($Dir -and (Test-Path $Dir) -and (($env:Path -split ';') -notcontains $Dir)) { + $env:Path = "$Dir;$env:Path" + } + } +} + +function Assert-Command { + param([string]$Name) + if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) { + throw "Missing command: $Name" + } +} + +function Assert-DockerDaemon { + & docker info *> $null + if ($LASTEXITCODE -ne 0) { + throw "Docker daemon is not running. Start Docker Desktop and wait until it is ready, then rerun this script." + } +} + +function Invoke-Step { + param( + [string]$WorkingDirectory, + [string]$FilePath, + [string[]]$Arguments + ) + Push-Location $WorkingDirectory + try { + & $FilePath @Arguments + if ($LASTEXITCODE -ne 0) { + throw "$FilePath failed, exit code: $LASTEXITCODE" + } + } finally { + Pop-Location + } +} + +function Build-JavaImages { + $ServerDir = Join-Path $RootDir "emp_server" + Write-Log "Build backend jars" + Invoke-Step $ServerDir "mvn" @("package", "-DskipTests", "-B", "-pl", ($JavaModules -join ","), "-am") + + foreach ($Module in $JavaModules) { + $TargetDir = Join-Path $ServerDir "$Module\target" + $Jar = Get-ChildItem $TargetDir -Filter "*.jar" | + Where-Object { $_.Name -ne "app.jar" -and $_.Name -notlike "*original*" } | + Select-Object -First 1 + if (-not $Jar) { + throw "Jar not found for $Module" + } + Copy-Item $Jar.FullName (Join-Path $TargetDir "app.jar") -Force + } + + Write-Log "Build backend images" + Invoke-Step $ServerDir "docker" @("build", "-f", "Dockerfile.service", "--build-arg", "MODULE=emp_gateway", "-t", "$ImageNamespace/emp-gateway:$ImageTag", "-t", "$ImageNamespace/emp-gateway:latest", ".") + Invoke-Step $ServerDir "docker" @("build", "-f", "Dockerfile.service", "--build-arg", "MODULE=emp_auth", "-t", "$ImageNamespace/emp-auth:$ImageTag", "-t", "$ImageNamespace/emp-auth:latest", ".") + Invoke-Step $ServerDir "docker" @("build", "-f", "Dockerfile.service", "--build-arg", "MODULE=emp_monitor", "-t", "$ImageNamespace/emp-monitor:$ImageTag", "-t", "$ImageNamespace/emp-monitor:latest", ".") + Invoke-Step $ServerDir "docker" @("build", "-f", "Dockerfile.service", "--build-arg", "MODULE=emp_data", "-t", "$ImageNamespace/emp-data:$ImageTag", "-t", "$ImageNamespace/emp-data:latest", ".") + + Write-Log "Build PDF image" + Invoke-Step $ServerDir "docker" @("build", "-f", "emp_pdf/Dockerfile", "-t", "$ImageNamespace/emp-pdf:$ImageTag", "-t", "$ImageNamespace/emp-pdf:latest", "emp_pdf") +} + +function Build-WsImage { + Write-Log "Build WS/simulator image" + Invoke-Step $RootDir "docker" @( + "build", + "-f", "deploy/isolated/dockerfiles/emp-ws.Dockerfile", + "--build-arg", "NPM_REGISTRY=$NpmRegistry", + "-t", "$ImageNamespace/emp-ws:$ImageTag", + "-t", "$ImageNamespace/emp-ws:latest", + "." + ) +} + +function Build-AdminImage { + $AdminDir = Join-Path $RootDir "emp_admin" + Write-Log "Build frontend dist" + Invoke-Step $AdminDir "pnpm" @("install", "--frozen-lockfile") + Invoke-Step $AdminDir "pnpm" @("run", "build:shunfeng") + + Write-Log "Build frontend image" + Invoke-Step $RootDir "docker" @( + "build", + "-f", "deploy/isolated/dockerfiles/emp-admin.Dockerfile", + "-t", "$ImageNamespace/emp-admin:$ImageTag", + "-t", "$ImageNamespace/emp-admin:latest", + "." + ) +} + +function Prepare-Package { + if (Test-Path $PackageDir) { + Remove-Item $PackageDir -Recurse -Force + } + New-Item -ItemType Directory -Force $PackageDir | Out-Null + + Copy-Item (Join-Path $ScriptDir "docker-compose.runtime.yml") (Join-Path $PackageDir "docker-compose.yml") -Force + Copy-Item (Join-Path $ScriptDir ".env.example") (Join-Path $PackageDir ".env.example") -Force + Copy-Item (Join-Path $ScriptDir "install.sh") (Join-Path $PackageDir "install.sh") -Force + Copy-Item (Join-Path $ScriptDir "README.md") (Join-Path $PackageDir "README.md") -Force +} + +function Save-Images { + $Images = New-Object System.Collections.Generic.List[string] + foreach ($Image in $AppImages) { + $Images.Add($Image) + } + + if (-not $NoMiddlewareImages) { + Write-Log "Pull middleware images" + foreach ($Image in $MiddlewareImages) { + Invoke-Step $RootDir "docker" @("pull", $Image) + $Images.Add($Image) + } + } + + Write-Log "Save images" + Invoke-Step $RootDir "docker" @(@("save", "-o", (Join-Path $PackageDir "images.tar")) + $Images.ToArray()) +} + +function Archive-Package { + New-Item -ItemType Directory -Force $DistDir | Out-Null + if (Test-Path $PackageArchive) { + Remove-Item $PackageArchive -Force + } + Invoke-Step $DistDir "tar" @("-czf", $PackageArchive, $PackageName) + Write-Log "Package created: $PackageArchive" +} + +Add-KnownToolPaths +Assert-Command "docker" +Assert-DockerDaemon +Assert-Command "tar" + +if (-not $SkipBuild) { + Assert-Command "mvn" + Assert-Command "pnpm" + Build-JavaImages + Build-WsImage + Build-AdminImage +} else { + Write-Log "Skip build, package existing local images only" +} + +Prepare-Package +Save-Images +Archive-Package diff --git a/isolated/build-package.sh b/isolated/build-package.sh new file mode 100644 index 0000000..d5203e1 --- /dev/null +++ b/isolated/build-package.sh @@ -0,0 +1,154 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +# 本地打包脚本:本机完成构建和 docker save,服务器只需要 docker load + compose up。 +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" +DIST_DIR="$SCRIPT_DIR/dist" + +IMAGE_NAMESPACE="${IMAGE_NAMESPACE:-emp-test}" +IMAGE_TAG="${IMAGE_TAG:-$(date '+%Y%m%d%H%M%S')}" +PACKAGE_NAME="${PACKAGE_NAME:-emp-test-runtime-${IMAGE_TAG}}" +PACKAGE_DIR="$DIST_DIR/$PACKAGE_NAME" +PACKAGE_ARCHIVE="$DIST_DIR/${PACKAGE_NAME}.tar.gz" +NPM_REGISTRY="${NPM_REGISTRY:-https://registry.npmjs.org}" +SKIP_BUILD="${SKIP_BUILD:-0}" +INCLUDE_MIDDLEWARE_IMAGES="${INCLUDE_MIDDLEWARE_IMAGES:-1}" + +JAVA_MODULES=(emp_gateway emp_auth emp_monitor emp_data) +APP_IMAGES=( + "$IMAGE_NAMESPACE/emp-gateway:$IMAGE_TAG" + "$IMAGE_NAMESPACE/emp-gateway:latest" + "$IMAGE_NAMESPACE/emp-auth:$IMAGE_TAG" + "$IMAGE_NAMESPACE/emp-auth:latest" + "$IMAGE_NAMESPACE/emp-monitor:$IMAGE_TAG" + "$IMAGE_NAMESPACE/emp-monitor:latest" + "$IMAGE_NAMESPACE/emp-data:$IMAGE_TAG" + "$IMAGE_NAMESPACE/emp-data:latest" + "$IMAGE_NAMESPACE/emp-pdf:$IMAGE_TAG" + "$IMAGE_NAMESPACE/emp-pdf:latest" + "$IMAGE_NAMESPACE/emp-ws:$IMAGE_TAG" + "$IMAGE_NAMESPACE/emp-ws:latest" + "$IMAGE_NAMESPACE/emp-admin:$IMAGE_TAG" + "$IMAGE_NAMESPACE/emp-admin:latest" +) + +MIDDLEWARE_IMAGES=( + "${MYSQL_IMAGE:-mysql:8.0}" + "${REDIS_IMAGE:-redis:7-alpine}" + "${KAFKA_IMAGE:-bitnami/kafka:3.7.0}" + "${TDENGINE_IMAGE:-tdengine/tdengine:3.3.6.0}" + "${NACOS_IMAGE:-nacos/nacos-server:v2.3.2-slim}" +) + +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" +} + +die() { + echo "ERROR: $*" >&2 + exit 1 +} + +need_cmd() { + command -v "$1" >/dev/null 2>&1 || die "缺少命令:$1" +} + +build_java_images() { + cd "$ROOT_DIR/emp_server" + log "构建后端 jar" + mvn package -DskipTests -B -pl "$(IFS=,; echo "${JAVA_MODULES[*]}")" -am + + for module in "${JAVA_MODULES[@]}"; do + local jar + jar="$(ls "$module"/target/*.jar | grep -v original | grep -v app.jar | head -1)" + cp "$jar" "$module/target/app.jar" + done + + log "构建后端镜像" + docker build -f Dockerfile.service --build-arg MODULE=emp_gateway -t "$IMAGE_NAMESPACE/emp-gateway:$IMAGE_TAG" -t "$IMAGE_NAMESPACE/emp-gateway:latest" . + docker build -f Dockerfile.service --build-arg MODULE=emp_auth -t "$IMAGE_NAMESPACE/emp-auth:$IMAGE_TAG" -t "$IMAGE_NAMESPACE/emp-auth:latest" . + docker build -f Dockerfile.service --build-arg MODULE=emp_monitor -t "$IMAGE_NAMESPACE/emp-monitor:$IMAGE_TAG" -t "$IMAGE_NAMESPACE/emp-monitor:latest" . + docker build -f Dockerfile.service --build-arg MODULE=emp_data -t "$IMAGE_NAMESPACE/emp-data:$IMAGE_TAG" -t "$IMAGE_NAMESPACE/emp-data:latest" . + + log "构建 PDF 镜像" + docker build -f emp_pdf/Dockerfile -t "$IMAGE_NAMESPACE/emp-pdf:$IMAGE_TAG" -t "$IMAGE_NAMESPACE/emp-pdf:latest" emp_pdf +} + +build_ws_image() { + cd "$ROOT_DIR" + log "构建 WS/模拟器镜像" + docker build \ + -f deploy/isolated/dockerfiles/emp-ws.Dockerfile \ + --build-arg "NPM_REGISTRY=$NPM_REGISTRY" \ + -t "$IMAGE_NAMESPACE/emp-ws:$IMAGE_TAG" \ + -t "$IMAGE_NAMESPACE/emp-ws:latest" \ + . +} + +build_admin_image() { + cd "$ROOT_DIR/emp_admin" + log "构建前端 dist" + pnpm install --frozen-lockfile + pnpm run build:shunfeng + + cd "$ROOT_DIR" + log "构建前端镜像" + docker build \ + -f deploy/isolated/dockerfiles/emp-admin.Dockerfile \ + -t "$IMAGE_NAMESPACE/emp-admin:$IMAGE_TAG" \ + -t "$IMAGE_NAMESPACE/emp-admin:latest" \ + . +} + +prepare_package() { + rm -rf "$PACKAGE_DIR" + mkdir -p "$PACKAGE_DIR" + + cp "$SCRIPT_DIR/docker-compose.runtime.yml" "$PACKAGE_DIR/docker-compose.yml" + cp "$SCRIPT_DIR/.env.example" "$PACKAGE_DIR/.env.example" + cp "$SCRIPT_DIR/install.sh" "$PACKAGE_DIR/install.sh" + cp "$SCRIPT_DIR/README.md" "$PACKAGE_DIR/README.md" + + chmod +x "$PACKAGE_DIR/install.sh" +} + +save_images() { + local images=("${APP_IMAGES[@]}") + + if [[ "$INCLUDE_MIDDLEWARE_IMAGES" == "1" ]]; then + log "拉取中间件镜像" + for image in "${MIDDLEWARE_IMAGES[@]}"; do + docker pull "$image" + images+=("$image") + done + fi + + log "导出镜像包" + docker save -o "$PACKAGE_DIR/images.tar" "${images[@]}" +} + +archive_package() { + mkdir -p "$DIST_DIR" + rm -f "$PACKAGE_ARCHIVE" + tar -czf "$PACKAGE_ARCHIVE" -C "$DIST_DIR" "$PACKAGE_NAME" + log "打包完成:$PACKAGE_ARCHIVE" +} + +need_cmd docker +need_cmd tar + +if [[ "$SKIP_BUILD" != "1" ]]; then + need_cmd mvn + need_cmd pnpm + build_java_images + build_ws_image + build_admin_image +else + log "跳过构建,仅打包当前本机已有镜像" +fi + +prepare_package +save_images +archive_package + diff --git a/isolated/docker-compose.runtime.yml b/isolated/docker-compose.runtime.yml new file mode 100644 index 0000000..5dd2d5e --- /dev/null +++ b/isolated/docker-compose.runtime.yml @@ -0,0 +1,247 @@ +x-app-env: &app-env + env_file: + - .env + restart: unless-stopped + networks: + - emp-net + +x-java-depends: &java-depends + nacos: + condition: service_healthy + mysql: + condition: service_healthy + redis: + condition: service_healthy + +services: + mysql: + image: ${MYSQL_IMAGE:-mysql:8.0} + restart: unless-stopped + ports: + - "0.0.0.0:${MYSQL_HOST_PORT:-13306}:3306" + environment: + TZ: Asia/Shanghai + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} + MYSQL_DATABASE: ${MYSQL_DATABASE:-emp} + command: + - --character-set-server=utf8mb4 + - --collation-server=utf8mb4_0900_ai_ci + - --default-time-zone=+08:00 + - --max-connections=1000 + volumes: + - mysql_data:/var/lib/mysql + healthcheck: + test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p\"$${MYSQL_ROOT_PASSWORD}\" --silent"] + interval: 10s + timeout: 5s + retries: 30 + networks: + - emp-net + + redis: + image: ${REDIS_IMAGE:-redis:7-alpine} + restart: unless-stopped + ports: + - "${REDIS_BIND_HOST:-127.0.0.1}:${REDIS_HOST_PORT:-16379}:6379" + command: ["redis-server", "--appendonly", "yes", "--requirepass", "${REDIS_PASSWORD}"] + volumes: + - redis_data:/data + healthcheck: + test: ["CMD-SHELL", "redis-cli -a \"$${REDIS_PASSWORD}\" ping | grep -q PONG"] + interval: 10s + timeout: 5s + retries: 30 + networks: + - emp-net + + kafka: + image: ${KAFKA_IMAGE:-bitnami/kafka:3.7.0} + restart: unless-stopped + ports: + - "0.0.0.0:${KAFKA_HOST_PORT:-19094}:9094" + environment: + ALLOW_PLAINTEXT_LISTENER: "yes" + KAFKA_CFG_NODE_ID: 1 + KAFKA_CFG_PROCESS_ROLES: controller,broker + KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093 + KAFKA_CFG_LISTENERS: INTERNAL://:9092,CONTROLLER://:9093,EXTERNAL://:9094 + KAFKA_CFG_ADVERTISED_LISTENERS: INTERNAL://kafka:9092,EXTERNAL://${PUBLIC_HOST}:${KAFKA_HOST_PORT:-19094} + KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP: INTERNAL:PLAINTEXT,CONTROLLER:PLAINTEXT,EXTERNAL:PLAINTEXT + KAFKA_CFG_INTER_BROKER_LISTENER_NAME: INTERNAL + KAFKA_CFG_CONTROLLER_LISTENER_NAMES: CONTROLLER + KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE: "true" + KAFKA_CFG_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_CFG_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 + KAFKA_CFG_TRANSACTION_STATE_LOG_MIN_ISR: 1 + volumes: + - kafka_data:/bitnami/kafka + healthcheck: + test: ["CMD-SHELL", "/opt/bitnami/kafka/bin/kafka-topics.sh --bootstrap-server 127.0.0.1:9092 --list >/dev/null 2>&1"] + interval: 10s + timeout: 5s + retries: 30 + networks: + - emp-net + + kafka-init: + image: ${KAFKA_IMAGE:-bitnami/kafka:3.7.0} + restart: "no" + depends_on: + kafka: + condition: service_healthy + entrypoint: ["/bin/bash", "-lc"] + command: > + /opt/bitnami/kafka/bin/kafka-topics.sh + --bootstrap-server kafka:9092 + --create + --if-not-exists + --topic ${KAFKA_TOPIC:-vehicle-data} + --partitions 3 + --replication-factor 1 + networks: + - emp-net + + tdengine: + image: ${TDENGINE_IMAGE:-tdengine/tdengine:3.3.6.0} + hostname: tdengine + privileged: true + restart: unless-stopped + ports: + - "0.0.0.0:${TDENGINE_HOST_PORT:-6030}:6030" + - "0.0.0.0:${TDENGINE_REST_HOST_PORT:-6041}:6041" + - "0.0.0.0:${TDENGINE_RPC_HOST_PORT:-6043}:6043" + - "0.0.0.0:${TDENGINE_RPC_UDP_HOST_PORT:-6044}:6044/udp" + - "0.0.0.0:${TDENGINE_KEEPER_HOST_PORT:-6060}:6060" + environment: + TZ: Asia/Shanghai + TAOS_FQDN: ${PUBLIC_HOST} + volumes: + - tdengine_data:/var/lib/taos + - tdengine_log:/var/log/taos + healthcheck: + test: ["CMD-SHELL", "taos -s 'show databases;' >/dev/null 2>&1"] + interval: 10s + timeout: 5s + retries: 30 + networks: + - emp-net + + tdengine-init: + image: ${TDENGINE_IMAGE:-tdengine/tdengine:3.3.6.0} + restart: "no" + depends_on: + tdengine: + condition: service_healthy + entrypoint: ["/bin/sh", "-lc"] + command: > + taos -h tdengine -u ${TDENGINE_USER:-root} -p"${TDENGINE_PWD:-taosdata}" + -s "create database if not exists ${TDENGINE_DATABASE:-emp};" + networks: + - emp-net + + nacos: + image: ${NACOS_IMAGE:-nacos/nacos-server:v2.3.2-slim} + restart: unless-stopped + ports: + - "0.0.0.0:${NACOS_HOST_PORT:-9008}:8848" + - "0.0.0.0:${NACOS_GRPC_HOST_PORT:-10008}:9848" + environment: + MODE: standalone + JVM_XMS: 256m + JVM_XMX: 512m + SPRING_DATASOURCE_PLATFORM: "" + NACOS_AUTH_ENABLE: ${NACOS_AUTH_ENABLE:-true} + NACOS_AUTH_IDENTITY_KEY: ${NACOS_AUTH_IDENTITY_KEY:-emp} + NACOS_AUTH_IDENTITY_VALUE: ${NACOS_AUTH_IDENTITY_VALUE:-emp2026} + NACOS_AUTH_TOKEN: ${NACOS_AUTH_TOKEN} + volumes: + - nacos_data:/home/nacos/data + - nacos_logs:/home/nacos/logs + healthcheck: + test: ["CMD-SHELL", "curl -sf http://127.0.0.1:8848/nacos/actuator/health || exit 1"] + interval: 10s + timeout: 5s + retries: 30 + networks: + - emp-net + + emp-gateway: + <<: *app-env + image: ${IMAGE_NAMESPACE:-emp-test}/emp-gateway:${IMAGE_TAG:-latest} + ports: + - "0.0.0.0:${GATEWAY_HOST_PORT:-9000}:9000" + depends_on: + <<: *java-depends + + emp-auth: + <<: *app-env + image: ${IMAGE_NAMESPACE:-emp-test}/emp-auth:${IMAGE_TAG:-latest} + depends_on: + <<: *java-depends + + emp-monitor: + <<: *app-env + image: ${IMAGE_NAMESPACE:-emp-test}/emp-monitor:${IMAGE_TAG:-latest} + depends_on: + <<: *java-depends + tdengine: + condition: service_healthy + emp-pdf: + condition: service_started + + emp-data: + <<: *app-env + image: ${IMAGE_NAMESPACE:-emp-test}/emp-data:${IMAGE_TAG:-latest} + depends_on: + <<: *java-depends + tdengine: + condition: service_healthy + kafka-init: + condition: service_completed_successfully + emp-ws: + condition: service_started + + emp-pdf: + <<: *app-env + image: ${IMAGE_NAMESPACE:-emp-test}/emp-pdf:${IMAGE_TAG:-latest} + ports: + - "127.0.0.1:${PDF_HOST_PORT:-3100}:3100" + + emp-ws: + <<: *app-env + image: ${IMAGE_NAMESPACE:-emp-test}/emp-ws:${IMAGE_TAG:-latest} + ports: + - "0.0.0.0:${WS_HOST_PORT:-3000}:3000" + depends_on: + mysql: + condition: service_healthy + redis: + condition: service_healthy + kafka-init: + condition: service_completed_successfully + + emp-admin: + image: ${IMAGE_NAMESPACE:-emp-test}/emp-admin:${IMAGE_TAG:-latest} + restart: unless-stopped + ports: + - "0.0.0.0:${ADMIN_HOST_PORT:-4081}:80" + depends_on: + emp-gateway: + condition: service_started + emp-ws: + condition: service_started + networks: + - emp-net + +networks: + emp-net: + driver: bridge + +volumes: + mysql_data: + redis_data: + kafka_data: + tdengine_data: + tdengine_log: + nacos_data: + nacos_logs: diff --git a/isolated/dockerfiles/emp-admin.Dockerfile b/isolated/dockerfiles/emp-admin.Dockerfile new file mode 100644 index 0000000..7d5e536 --- /dev/null +++ b/isolated/dockerfiles/emp-admin.Dockerfile @@ -0,0 +1,7 @@ +FROM nginx:alpine + +COPY emp_admin/dist /usr/share/nginx/html +COPY deploy/isolated/nginx/admin.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 + diff --git a/isolated/dockerfiles/emp-ws.Dockerfile b/isolated/dockerfiles/emp-ws.Dockerfile new file mode 100644 index 0000000..98743e5 --- /dev/null +++ b/isolated/dockerfiles/emp-ws.Dockerfile @@ -0,0 +1,17 @@ +FROM node:20-alpine + +ARG NPM_REGISTRY=https://registry.npmjs.org + +WORKDIR /app +ENV NODE_ENV=production + +COPY emp_ws/package*.json ./ +RUN npm config set registry "$NPM_REGISTRY" \ + && npm install --omit=dev + +COPY emp_ws ./ +RUN mkdir -p runtime + +EXPOSE 3000 +CMD ["node", "server.js"] + diff --git a/isolated/install.sh b/isolated/install.sh new file mode 100644 index 0000000..a7652f6 --- /dev/null +++ b/isolated/install.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +# 服务器运行脚本:只加载本地镜像包并启动 compose,不做源码构建。 +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +PROJECT_NAME="${PROJECT_NAME:-emp-test}" +ENV_FILE="${ENV_FILE:-.env}" +COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.yml}" +IMAGE_TAR="${IMAGE_TAR:-images.tar}" + +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" +} + +die() { + echo "ERROR: $*" >&2 + exit 1 +} + +need_cmd() { + command -v "$1" >/dev/null 2>&1 || die "缺少命令:$1" +} + +need_cmd docker + +if docker compose version >/dev/null 2>&1; then + DC=(docker compose) +elif command -v docker-compose >/dev/null 2>&1; then + DC=(docker-compose) +else + die "未安装 docker compose" +fi + +if [[ ! -f "$ENV_FILE" ]]; then + cp .env.example "$ENV_FILE" + die "已生成 $ENV_FILE,请先修改密码、PUBLIC_HOST 和端口后重新执行:sh install.sh" +fi + +if [[ ! -f "$COMPOSE_FILE" ]]; then + die "未找到 $COMPOSE_FILE" +fi + +if [[ -f "$IMAGE_TAR" ]]; then + log "加载镜像包:$IMAGE_TAR" + docker load -i "$IMAGE_TAR" +else + log "未找到 $IMAGE_TAR,将尝试使用本机已有镜像或在线拉取镜像" +fi + +log "启动隔离测试环境:$PROJECT_NAME" +"${DC[@]}" \ + --env-file "$ENV_FILE" \ + -f "$COMPOSE_FILE" \ + -p "$PROJECT_NAME" \ + up -d + +log "当前容器状态" +"${DC[@]}" \ + --env-file "$ENV_FILE" \ + -f "$COMPOSE_FILE" \ + -p "$PROJECT_NAME" \ + ps + +log "完成。前端地址请访问 .env 中的 PUBLIC_HOST:ADMIN_HOST_PORT。" + diff --git a/isolated/nginx/admin.conf b/isolated/nginx/admin.conf new file mode 100644 index 0000000..e383e71 --- /dev/null +++ b/isolated/nginx/admin.conf @@ -0,0 +1,53 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + client_max_body_size 105m; + + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml image/svg+xml; + gzip_min_length 1024; + + location = /index.html { + add_header Cache-Control "no-store"; + } + + location /api/ { + proxy_pass http://emp-gateway:9000/api/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 300s; + } + + location /socket.io/ { + proxy_pass http://emp-ws:3000/socket.io/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_read_timeout 300s; + } + + location / { + add_header Cache-Control "no-store"; + try_files $uri $uri/ /index.html; + } + + location ~* \.(js|css)$ { + expires -1; + add_header Cache-Control "no-cache, no-store, must-revalidate"; + try_files $uri =404; + } + + location ~* \.(png|jpg|jpeg|gif|ico|svg|woff2?|ttf|glb)$ { + expires 30d; + add_header Cache-Control "public, immutable"; + try_files $uri =404; + } +} +