Explorar el Código

feat: 提交

master
leiyun hace 3 semanas
commit
010f0b30be
Se han modificado 10 ficheros con 1110 adiciones y 0 borrados
  1. +18
    -0
      .gitignore
  2. +154
    -0
      isolated/.env.example
  3. +188
    -0
      isolated/README.md
  4. +205
    -0
      isolated/build-package.ps1
  5. +154
    -0
      isolated/build-package.sh
  6. +247
    -0
      isolated/docker-compose.runtime.yml
  7. +7
    -0
      isolated/dockerfiles/emp-admin.Dockerfile
  8. +17
    -0
      isolated/dockerfiles/emp-ws.Dockerfile
  9. +67
    -0
      isolated/install.sh
  10. +53
    -0
      isolated/nginx/admin.conf

+ 18
- 0
.gitignore Ver fichero

@@ -0,0 +1,18 @@
# 打包产物
isolated/dist/
*.tar
*.tar.gz
*.tgz

# 真实环境变量文件,只提交 .env.example
.env
*.env
!.env.example
!**/.env.example

# 临时文件
*.log
*.tmp
.DS_Store
Thumbs.db


+ 154
- 0
isolated/.env.example Ver fichero

@@ -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

+ 188
- 0
isolated/README.md Ver fichero

@@ -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
```


+ 205
- 0
isolated/build-package.ps1 Ver fichero

@@ -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

+ 154
- 0
isolated/build-package.sh Ver fichero

@@ -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


+ 247
- 0
isolated/docker-compose.runtime.yml Ver fichero

@@ -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:

+ 7
- 0
isolated/dockerfiles/emp-admin.Dockerfile Ver fichero

@@ -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


+ 17
- 0
isolated/dockerfiles/emp-ws.Dockerfile Ver fichero

@@ -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"]


+ 67
- 0
isolated/install.sh Ver fichero

@@ -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。"


+ 53
- 0
isolated/nginx/admin.conf Ver fichero

@@ -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;
}
}


Cargando…
Cancelar
Guardar