- shadcn/ui 标准化:废弃纸感令牌,统一 bg-background/text-foreground 等 - Tailwind v4 + @theme inline,移除 tailwind.config.js - React 19 use() + Suspense 流式渲染,首屏骨架秒出 - 三级错误边界:Route → Section → Widget 层层兜底 - 错误上报:useErrorReport → sendBeacon → /api/log mock 端点 - 三层安全边界:L1 角色门禁 / L2 权限点门禁 / L3 数据范围 - 权限位图 base36 压缩:67 权限点 → ~14 字符,JWT 体积减少 ≥ 99% - notify 统一 Toast 封装,禁止业务直接 import sonner - PluginBoundary 替代 PluginLoader(错误边界 + Suspense + Skeleton 三件套) 验证:typecheck 0 错误 / lint 0 错误 / build 6 路由生成成功
482 lines
22 KiB
PowerShell
482 lines
22 KiB
PowerShell
<#
|
||
.SYNOPSIS
|
||
Edu v2.1 一键启动脚本
|
||
.DESCRIPTION
|
||
启动顺序:
|
||
1. Docker 基础设施(MySQL/Redis/Kafka/ClickHouse/Neo4j/ES/observability)
|
||
2. Apollo Router(Docker 容器,GraphQL 联邦网关)
|
||
3. 应用服务(本地 pnpm dev / uvicorn / go run)
|
||
应用服务列表(v2.1):
|
||
- NestJS: iam, config-service, classes, core-edu, content, msg
|
||
- Python: data-ana, ai
|
||
- Go: api-gateway, push-gateway
|
||
- Next.js: portal-shell (前端)
|
||
.PARAMETER SkipInfra
|
||
跳过 Docker 基础设施启动(已确认 infra 在运行时使用)
|
||
.PARAMETER SkipApps
|
||
仅启动基础设施,不启动应用服务
|
||
.PARAMETER Force
|
||
自动杀掉占用端口的进程(不交互确认)
|
||
.PARAMETER Profile
|
||
Docker Compose profile 层级:default | p3 | p4 | p5 | p6(默认 p6=全部)
|
||
.EXAMPLE
|
||
.\scripts\start-all.ps1 # 启动全部
|
||
.\scripts\start-all.ps1 -SkipInfra # 仅启动应用服务
|
||
.\scripts\start-all.ps1 -SkipApps # 仅启动基础设施
|
||
.\scripts\start-all.ps1 -Force # 自动杀端口占用
|
||
#>
|
||
param(
|
||
[switch]$SkipInfra,
|
||
[switch]$SkipApps,
|
||
[switch]$SkipRouter,
|
||
[switch]$Force,
|
||
[string]$Profile = "p6"
|
||
)
|
||
|
||
$ErrorActionPreference = "Stop"
|
||
$ProjectRoot = Split-Path -Parent $PSScriptRoot
|
||
|
||
Write-Host "========================================" -ForegroundColor Cyan
|
||
Write-Host " Edu v2.1 Start All Services" -ForegroundColor Cyan
|
||
Write-Host "========================================" -ForegroundColor Cyan
|
||
Write-Host ""
|
||
|
||
# ===== 0. 加载 .env 文件 =====
|
||
Write-Host "[0/7] Loading .env..." -ForegroundColor Yellow
|
||
$envFile = Join-Path $ProjectRoot ".env"
|
||
if (-not (Test-Path $envFile)) {
|
||
Write-Host " [FAIL] .env not found at $envFile" -ForegroundColor Red
|
||
exit 1
|
||
}
|
||
$envCount = 0
|
||
$envLines = Get-Content $envFile
|
||
foreach ($line in $envLines) {
|
||
$line = $line.Trim()
|
||
if (-not $line -or $line.StartsWith("#") -or -not $line.Contains("=")) { continue }
|
||
$idx = $line.IndexOf("=")
|
||
$key = $line.Substring(0, $idx).Trim()
|
||
$val = $line.Substring($idx + 1).Trim()
|
||
# 移除可能的引号
|
||
if ($val.StartsWith('"') -and $val.EndsWith('"')) { $val = $val.Substring(1, $val.Length - 2) }
|
||
if ($val.StartsWith("'") -and $val.EndsWith("'")) { $val = $val.Substring(1, $val.Length - 2) }
|
||
[Environment]::SetEnvironmentVariable($key, $val, "Process")
|
||
$envCount++
|
||
}
|
||
# 补充 .env 中未定义但 dev 模式需要的变量
|
||
if (-not $env:DEV_MODE) { $env:DEV_MODE = "true" }
|
||
if (-not $env:OTEL_EXPORTER_OTLP_ENDPOINT) { $env:OTEL_EXPORTER_OTLP_ENDPOINT = "http://localhost:4318" }
|
||
if (-not $env:NEO4J_URI) { $env:NEO4J_URI = "bolt://localhost:7687" }
|
||
if (-not $env:NEO4J_USER) { $env:NEO4J_USER = "neo4j" }
|
||
if (-not $env:CLICKHOUSE_URL) { $env:CLICKHOUSE_URL = "http://localhost:8123" }
|
||
if (-not $env:ES_URL) { $env:ES_URL = "http://localhost:9200" }
|
||
if (-not $env:ROUTER_AUTH_SECRET) { $env:ROUTER_AUTH_SECRET = "dev-router-secret" }
|
||
Write-Host " [OK] Loaded $envCount env vars from .env (DEV_MODE=$($env:DEV_MODE))" -ForegroundColor Green
|
||
Write-Host ""
|
||
|
||
# ===== 1. 启动 Docker 基础设施 =====
|
||
# 混合部署:仅启动基础设施容器(MySQL/Redis/Kafka/ClickHouse/Neo4j/ES/Debezium/observability)
|
||
# Temporal 暂不启动(auto-setup 镜像需调整配置,非关键路径)
|
||
# 应用服务(config-service/apollo-router/portal-shell)本地运行,避免 Docker 构建失败
|
||
if (-not $SkipInfra) {
|
||
Write-Host "[1/7] Starting Docker infrastructure..." -ForegroundColor Yellow
|
||
Push-Location "$ProjectRoot\infra"
|
||
# docker compose 把进度输出到 stderr,临时放宽 ErrorActionPreference 避免误判
|
||
$prevEAP = $ErrorActionPreference
|
||
$ErrorActionPreference = "Continue"
|
||
try {
|
||
# 启用 p3+p5 profile,显式启动基础设施服务(跳过 temporal 和需构建镜像的 app)
|
||
$composeCmd = "docker compose -f docker-compose.yml --profile p3 --profile p5 up -d mysql redis kafka zookeeper clickhouse neo4j elasticsearch debezium-connect"
|
||
Write-Host " $composeCmd" -ForegroundColor DarkGray
|
||
Invoke-Expression $composeCmd 2>&1 | Out-Null
|
||
if ($LASTEXITCODE -ne 0) {
|
||
Write-Host " [WARN] Bulk start had issues, retrying individually..." -ForegroundColor Yellow
|
||
docker compose -f docker-compose.yml up -d mysql redis 2>&1 | Out-Null
|
||
docker compose -f docker-compose.yml --profile p3 up -d kafka zookeeper clickhouse neo4j debezium-connect 2>&1 | Out-Null
|
||
docker compose -f docker-compose.yml --profile p5 up -d elasticsearch 2>&1 | Out-Null
|
||
}
|
||
# 可观测性栈(独立 profile)
|
||
docker compose -f docker-compose.yml --profile observability up -d 2>&1 | Out-Null
|
||
Pop-Location
|
||
$ErrorActionPreference = $prevEAP
|
||
Write-Host " Waiting 20s for containers to initialize..." -ForegroundColor Gray
|
||
Start-Sleep -Seconds 20
|
||
Write-Host " [OK] Docker infrastructure started" -ForegroundColor Green
|
||
} catch {
|
||
Pop-Location
|
||
$ErrorActionPreference = $prevEAP
|
||
Write-Host " [FAIL] $_" -ForegroundColor Red
|
||
exit 1
|
||
}
|
||
} else {
|
||
Write-Host "[1/7] Skipping Docker infrastructure (-SkipInfra)" -ForegroundColor Gray
|
||
}
|
||
Write-Host ""
|
||
|
||
# ===== 2. 基础设施健康检查 =====
|
||
Write-Host "[2/7] Checking infrastructure health..." -ForegroundColor Yellow
|
||
$infraServices = @(
|
||
@{Name="MySQL"; Container="edu-mysql"},
|
||
@{Name="Redis"; Container="edu-redis"},
|
||
@{Name="Kafka"; Container="edu-kafka"},
|
||
@{Name="Zookeeper"; Container="edu-zookeeper"},
|
||
@{Name="ClickHouse"; Container="edu-clickhouse"},
|
||
@{Name="Neo4j"; Container="edu-neo4j"},
|
||
@{Name="Elasticsearch"; Container="edu-es"},
|
||
@{Name="Debezium"; Container="edu-debezium"},
|
||
@{Name="Jaeger"; Container="edu-jaeger"},
|
||
@{Name="Prometheus"; Container="edu-prometheus"},
|
||
@{Name="Grafana"; Container="edu-grafana"}
|
||
)
|
||
$prevEAP = $ErrorActionPreference
|
||
$ErrorActionPreference = "Continue"
|
||
$infraOkCount = 0
|
||
foreach ($svc in $infraServices) {
|
||
$running = docker inspect -f '{{.State.Running}}' $svc.Container 2>$null
|
||
if ($running -ne "true") {
|
||
Write-Host " [SKIP] $($svc.Name) not running (may be optional)" -ForegroundColor Gray
|
||
continue
|
||
}
|
||
$health = docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{end}}' $svc.Container 2>$null
|
||
if ($health -eq "healthy" -or $health -eq "") {
|
||
Write-Host " [OK] $($svc.Name)" -ForegroundColor Green
|
||
$infraOkCount++
|
||
} elseif ($health -eq "starting") {
|
||
Write-Host " [WAIT] $($svc.Name) still starting..." -ForegroundColor Yellow
|
||
} else {
|
||
Write-Host " [WARN] $($svc.Name) health=$health" -ForegroundColor Yellow
|
||
}
|
||
}
|
||
$ErrorActionPreference = $prevEAP
|
||
Write-Host " $infraOkCount/$($infraServices.Count) infrastructure services OK" -ForegroundColor Green
|
||
Write-Host ""
|
||
|
||
if ($SkipApps) {
|
||
Write-Host "========================================" -ForegroundColor Cyan
|
||
Write-Host " Infrastructure only (-SkipApps). Done." -ForegroundColor Cyan
|
||
Write-Host "========================================" -ForegroundColor Cyan
|
||
exit 0
|
||
}
|
||
|
||
# ===== 3. 端口冲突检查 =====
|
||
Write-Host "[3/7] Checking port conflicts..." -ForegroundColor Yellow
|
||
$portMap = @{
|
||
3001="classes"; 3002="iam"; 3004="core-edu"; 3005="content"
|
||
3006="data-ana"; 3007="msg"; 3008="ai"; 3011="config-service"
|
||
8080="api-gateway"; 8081="push-gateway"; 4010="portal-shell"
|
||
}
|
||
$conflicts = @()
|
||
foreach ($port in $portMap.Keys | Sort-Object) {
|
||
$conn = Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue
|
||
if ($conn) {
|
||
$svcName = $portMap[$port]
|
||
$procId = $conn[0].OwningProcess
|
||
$procName = ""
|
||
try { $procName = (Get-Process -Id $procId -ErrorAction Stop).ProcessName } catch {}
|
||
Write-Host " [WARN] Port $port ($svcName) occupied by PID $procId ($procName)" -ForegroundColor Yellow
|
||
$conflicts += [PSCustomObject]@{Port=$port; Service=$svcName; PID=$procId; Process=$procName}
|
||
}
|
||
}
|
||
if ($conflicts.Count -gt 0) {
|
||
Write-Host " $($conflicts.Count) port(s) in use." -ForegroundColor Yellow
|
||
$shouldKill = $false
|
||
if ($Force) {
|
||
$shouldKill = $true
|
||
} else {
|
||
$answer = Read-Host " Kill existing processes and continue? (y/N)"
|
||
if ($answer -eq "y" -or $answer -eq "Y") { $shouldKill = $true }
|
||
}
|
||
if ($shouldKill) {
|
||
foreach ($c in $conflicts) {
|
||
try {
|
||
Stop-Process -Id $c.PID -Force -ErrorAction Stop
|
||
Write-Host " [OK] Killed PID $($c.PID) on port $($c.Port)" -ForegroundColor Green
|
||
Start-Sleep -Milliseconds 500
|
||
} catch {
|
||
Write-Host " [WARN] Cannot kill PID $($c.PID): $($_.Exception.Message)" -ForegroundColor Yellow
|
||
}
|
||
}
|
||
Start-Sleep -Seconds 2
|
||
} else {
|
||
Write-Host " Aborting." -ForegroundColor Red
|
||
exit 1
|
||
}
|
||
} else {
|
||
Write-Host " [OK] All app ports are free" -ForegroundColor Green
|
||
}
|
||
Write-Host ""
|
||
|
||
# ===== 4. 确保 shared-ts 已编译 =====
|
||
Write-Host "[4/7] Ensuring shared-ts is built..." -ForegroundColor Yellow
|
||
$sharedTsPath = Join-Path $ProjectRoot "packages\shared-ts\dist"
|
||
if (-not (Test-Path $sharedTsPath)) {
|
||
Write-Host " Building @edu/shared-ts..." -ForegroundColor Gray
|
||
pnpm --filter @edu/shared-ts build 2>&1 | Out-Null
|
||
if ($LASTEXITCODE -eq 0) {
|
||
Write-Host " [OK] shared-ts built" -ForegroundColor Green
|
||
} else {
|
||
Write-Host " [FAIL] shared-ts build failed" -ForegroundColor Red
|
||
exit 1
|
||
}
|
||
} else {
|
||
Write-Host " [OK] shared-ts already built" -ForegroundColor Green
|
||
}
|
||
Write-Host ""
|
||
|
||
# ===== 5. 清理 NestJS 增量缓存 =====
|
||
Write-Host "[5/7] Cleaning NestJS tsbuildinfo cache..." -ForegroundColor Yellow
|
||
$nestjsDirs = @("iam", "config-service", "classes", "core-edu", "content", "msg")
|
||
foreach ($dir in $nestjsDirs) {
|
||
$svcPath = Join-Path $ProjectRoot "services\$dir"
|
||
if (Test-Path $svcPath) {
|
||
Get-ChildItem -Path $svcPath -Filter "*.tsbuildinfo" -Recurse -ErrorAction SilentlyContinue |
|
||
Remove-Item -Force -ErrorAction SilentlyContinue
|
||
}
|
||
}
|
||
Write-Host " [OK] Cache cleaned" -ForegroundColor Green
|
||
Write-Host ""
|
||
|
||
# ===== 6. 启动应用服务 =====
|
||
Write-Host "[6/7] Starting application services (v2.1)..." -ForegroundColor Yellow
|
||
|
||
# Python 服务环境变量
|
||
$pyEnv = @{
|
||
CLICKHOUSE_HOST = "localhost"
|
||
CLICKHOUSE_PORT = "8123"
|
||
CLICKHOUSE_USER = "default"
|
||
CLICKHOUSE_PASSWORD = "clickhouse"
|
||
CLICKHOUSE_DATABASE = "edu_analytics"
|
||
CLICKHOUSE_URL = "http://localhost:8123"
|
||
KAFKA_BROKERS = "localhost:9092"
|
||
OTEL_ENDPOINT = "http://localhost:4318"
|
||
OTEL_EXPORTER_OTLP_ENDPOINT = "http://localhost:4318"
|
||
DEV_MODE = "true"
|
||
NEO4J_URI = "bolt://localhost:7687"
|
||
NEO4J_USER = "neo4j"
|
||
NEO4J_PASSWORD = "changeme"
|
||
ES_URL = "http://localhost:9200"
|
||
}
|
||
|
||
# Go 服务环境变量默认值(在当前进程设置,子进程会继承)
|
||
if (-not $env:CLASSES_SERVICE_URL) { $env:CLASSES_SERVICE_URL = "http://localhost:3001" }
|
||
if (-not $env:IAM_SERVICE_URL) { $env:IAM_SERVICE_URL = "http://localhost:3002" }
|
||
if (-not $env:APOLLO_ROUTER_URL) { $env:APOLLO_ROUTER_URL = "http://localhost:3000" }
|
||
if (-not $env:CORE_EDU_SERVICE_URL) { $env:CORE_EDU_SERVICE_URL = "http://localhost:3004" }
|
||
if (-not $env:CONTENT_SERVICE_URL) { $env:CONTENT_SERVICE_URL = "http://localhost:3005" }
|
||
if (-not $env:DATA_ANA_SERVICE_URL) { $env:DATA_ANA_SERVICE_URL = "http://localhost:3006" }
|
||
if (-not $env:MSG_SERVICE_URL) { $env:MSG_SERVICE_URL = "http://localhost:3007" }
|
||
if (-not $env:AI_SERVICE_URL) { $env:AI_SERVICE_URL = "http://localhost:3008" }
|
||
if (-not $env:CORS_ORIGINS) { $env:CORS_ORIGINS = "http://localhost:4000,http://localhost:4010" }
|
||
if (-not $env:JWKS_URL) { $env:JWKS_URL = "http://localhost:3002/v1/iam/.well-known/jwks.json" }
|
||
if (-not $env:PUSH_INTERNAL_TOKEN) { $env:PUSH_INTERNAL_TOKEN = "edu-internal-token" }
|
||
|
||
$services = @(
|
||
@{Title="edu-app-iam"; Cmd="pnpm"; Args=@("--filter","@edu/iam-service","exec","nest","start"); Dir="$ProjectRoot"},
|
||
@{Title="edu-app-config-service"; Cmd="pnpm"; Args=@("--filter","@edu/config-service","exec","nest","start"); Dir="$ProjectRoot"},
|
||
@{Title="edu-app-classes"; Cmd="pnpm"; Args=@("--filter","@edu/classes-service","exec","nest","start"); Dir="$ProjectRoot"},
|
||
@{Title="edu-app-core-edu"; Cmd="pnpm"; Args=@("--filter","@edu/core-edu-service","exec","nest","start"); Dir="$ProjectRoot"},
|
||
@{Title="edu-app-content"; Cmd="pnpm"; Args=@("--filter","@edu/content-service","exec","nest","start"); Dir="$ProjectRoot"},
|
||
@{Title="edu-app-msg"; Cmd="pnpm"; Args=@("--filter","@edu/msg-service","exec","nest","start"); Dir="$ProjectRoot"},
|
||
@{Title="edu-app-data-ana"; Cmd="uv"; Args=@("run","uvicorn","data_ana.main:app","--app-dir","src","--host","0.0.0.0","--port","3006","--reload"); Dir="$ProjectRoot\services\data-ana"; PyEnv=$true},
|
||
@{Title="edu-app-ai"; Cmd="uv"; Args=@("run","uvicorn","ai.main:app","--app-dir","src","--host","0.0.0.0","--port","3008","--reload"); Dir="$ProjectRoot\services\ai"; PyEnv=$true},
|
||
@{Title="edu-app-api-gateway"; Cmd="go"; Args=@("run","."); Dir="$ProjectRoot\services\api-gateway"; GoEnv=$true},
|
||
@{Title="edu-app-push-gateway"; Cmd="go"; Args=@("run","."); Dir="$ProjectRoot\services\push-gateway"; GoEnv=$true},
|
||
@{Title="edu-app-portal-shell"; Cmd="pnpm"; Args=@("--filter","@edu/portal-shell","dev"); Dir="$ProjectRoot"}
|
||
)
|
||
|
||
$startedCount = 0
|
||
foreach ($svc in $services) {
|
||
$psCmd = "Set-Location '$($svc.Dir)'; "
|
||
|
||
# Python 服务额外设置环境变量
|
||
if ($svc.PyEnv) {
|
||
foreach ($kv in $pyEnv.GetEnumerator()) {
|
||
$psCmd += "`$env:$($kv.Key)='$($kv.Value)'; "
|
||
}
|
||
}
|
||
# Go 服务确保 PATH 包含 Go bin
|
||
if ($svc.GoEnv) {
|
||
$psCmd += "`$env:Path = 'C:\Program Files\Go\bin;' + `$env:Path; "
|
||
}
|
||
|
||
$cmdStr = "$($svc.Cmd) $($svc.Args -join ' ')"
|
||
$psCmd += "$cmdStr; Write-Host ''; Write-Host 'Service stopped. Press any key to close...' -ForegroundColor Yellow; `$null = `$Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')"
|
||
|
||
Start-Process -FilePath "powershell" -ArgumentList "-NoExit","-Command",$psCmd -WindowStyle Normal | Out-Null
|
||
Write-Host " [START] $($svc.Title)" -ForegroundColor Green
|
||
$startedCount++
|
||
Start-Sleep -Milliseconds 800
|
||
}
|
||
|
||
Write-Host ""
|
||
Write-Host " $startedCount services launched. Waiting 60s for init..." -ForegroundColor Yellow
|
||
Start-Sleep -Seconds 60
|
||
|
||
# ===== 7. 健康检查 =====
|
||
Write-Host "[7/7] Health check..." -ForegroundColor Yellow
|
||
Write-Host ""
|
||
|
||
$healthServices = @(
|
||
@{Name="iam"; Url="http://localhost:3002/healthz"; Critical=$true},
|
||
@{Name="config-service"; Url="http://localhost:3011/healthz"; Critical=$true},
|
||
@{Name="classes"; Url="http://localhost:3001/healthz"; Critical=$false},
|
||
@{Name="core-edu"; Url="http://localhost:3004/healthz"; Critical=$false},
|
||
@{Name="content"; Url="http://localhost:3005/healthz"; Critical=$false},
|
||
@{Name="data-ana"; Url="http://localhost:3006/healthz"; Critical=$false},
|
||
@{Name="msg"; Url="http://localhost:3007/healthz"; Critical=$false},
|
||
@{Name="ai"; Url="http://localhost:3008/healthz"; Critical=$false},
|
||
@{Name="api-gateway"; Url="http://localhost:8080/healthz"; Critical=$false},
|
||
@{Name="push-gateway"; Url="http://localhost:8081/healthz"; Critical=$false},
|
||
@{Name="portal-shell"; Url="http://localhost:4010/api/health"; Critical=$true}
|
||
)
|
||
|
||
$okCount = 0
|
||
$failCount = 0
|
||
$criticalFail = 0
|
||
$failedServices = @()
|
||
foreach ($svc in $healthServices) {
|
||
$retries = 0
|
||
$maxRetries = 5
|
||
$success = $false
|
||
while ($retries -lt $maxRetries -and -not $success) {
|
||
try {
|
||
$null = Invoke-RestMethod -Uri $svc.Url -Method Get -TimeoutSec 5 -ErrorAction Stop
|
||
$tag = if ($svc.Critical) { "CRITICAL" } else { "optional" }
|
||
Write-Host " [OK] $($svc.Name) ($tag)" -ForegroundColor Green
|
||
$success = $true
|
||
$okCount++
|
||
} catch {
|
||
$retries++
|
||
if ($retries -lt $maxRetries) { Start-Sleep -Seconds 4 }
|
||
}
|
||
}
|
||
if (-not $success) {
|
||
$tag = if ($svc.Critical) { "CRITICAL" } else { "optional" }
|
||
Write-Host " [FAIL] $($svc.Name) ($tag)" -ForegroundColor Red
|
||
$failCount++
|
||
$failedServices += $svc.Name
|
||
if ($svc.Critical) { $criticalFail++ }
|
||
}
|
||
}
|
||
|
||
Write-Host ""
|
||
Write-Host "========================================" -ForegroundColor Cyan
|
||
Write-Host " Result: [OK] $okCount / [FAIL] $failCount" -ForegroundColor Cyan
|
||
if ($criticalFail -gt 0) {
|
||
Write-Host " CRITICAL failures: $criticalFail" -ForegroundColor Red
|
||
}
|
||
Write-Host "========================================" -ForegroundColor Cyan
|
||
|
||
if ($failCount -gt 0) {
|
||
Write-Host ""
|
||
Write-Host "Failed: $($failedServices -join ', ')" -ForegroundColor Yellow
|
||
Write-Host ""
|
||
Write-Host "Troubleshooting:" -ForegroundColor Yellow
|
||
Write-Host " 1. Check service window for errors" -ForegroundColor White
|
||
Write-Host " 2. Health check: .\scripts\health-check.ps1" -ForegroundColor White
|
||
Write-Host " 3. Stop & retry: .\scripts\stop-all.ps1 -KillByPort then .\scripts\start-all.ps1 -SkipInfra -Force" -ForegroundColor White
|
||
Write-Host ""
|
||
}
|
||
|
||
# ===== 8. 启动 Apollo Router(Docker 容器) =====
|
||
# Apollo Router 需要所有 Federation 2 子图(iam/core-edu/content/msg/config)就绪后才能 compose supergraph
|
||
# 子图在 host 上运行,router 容器通过 host.docker.internal 访问
|
||
if (-not $SkipRouter -and -not $SkipApps) {
|
||
Write-Host "[8/8] Starting Apollo Router (Docker)..." -ForegroundColor Yellow
|
||
|
||
$routerImage = "edu/apollo-router:dev"
|
||
$routerDir = Join-Path $ProjectRoot "infra\apollo-router"
|
||
|
||
# 检查镜像是否存在,不存在则提示
|
||
$imageExists = docker image inspect $routerImage 2>$null
|
||
if (-not $imageExists) {
|
||
Write-Host " [WARN] Image $routerImage not found. Building..." -ForegroundColor Yellow
|
||
Push-Location $routerDir
|
||
try {
|
||
docker build -t $routerImage . 2>&1 | Out-Host
|
||
if ($LASTEXITCODE -ne 0) {
|
||
Write-Host " [FAIL] Apollo Router image build failed" -ForegroundColor Red
|
||
Write-Host " Skip with -SkipRouter. Portal-shell will fall back to config-service direct connection." -ForegroundColor Yellow
|
||
Pop-Location
|
||
$SkipRouter = $true
|
||
} else {
|
||
Write-Host " [OK] Image built" -ForegroundColor Green
|
||
}
|
||
} catch {
|
||
Pop-Location
|
||
Write-Host " [FAIL] Build error: $_" -ForegroundColor Red
|
||
$SkipRouter = $true
|
||
}
|
||
if (-not $SkipRouter) { Pop-Location }
|
||
}
|
||
|
||
if (-not $SkipRouter) {
|
||
# 移除旧容器
|
||
docker rm -f edu-apollo-router 2>&1 | Out-Null
|
||
|
||
# 检查 3000 端口是否被占用
|
||
$port3000 = Get-NetTCPConnection -LocalPort 3000 -State Listen -ErrorAction SilentlyContinue
|
||
if ($port3000) {
|
||
Write-Host " [WARN] Port 3000 already in use, skipping router startup" -ForegroundColor Yellow
|
||
Write-Host " (use docker rm -f edu-apollo-router to clean up old container)" -ForegroundColor Gray
|
||
} else {
|
||
$proxyVar = $env:HTTP_PROXY
|
||
if (-not $proxyVar) { $proxyVar = "http://host.docker.internal:7897" }
|
||
|
||
$dockerArgs = @(
|
||
"run","-d","--name","edu-apollo-router",
|
||
"--add-host=host.docker.internal:host-gateway",
|
||
"-p","3000:3000","-p","8088:8088",
|
||
"-v","$routerDir\dev-supergraph.yaml:/dist/supergraph.yaml",
|
||
"-v","$routerDir\dev-entrypoint.sh:/dist/entrypoint.sh",
|
||
"-v","$routerDir\router.yaml:/dist/configuration.yaml",
|
||
"-e","ROUTER_AUTH_SECRET=dev-router-secret",
|
||
"-e","APOLLO_ELV2_LICENSE=accept",
|
||
"-e","HTTP_PROXY=$proxyVar",
|
||
"-e","HTTPS_PROXY=$proxyVar",
|
||
"-e","http_proxy=$proxyVar",
|
||
"-e","https_proxy=$proxyVar",
|
||
"-e","NO_PROXY=localhost,127.0.0.1,host.docker.internal",
|
||
$routerImage,
|
||
"/dist/entrypoint.sh"
|
||
)
|
||
|
||
Write-Host " Starting container..." -ForegroundColor Gray
|
||
$prevEAP = $ErrorActionPreference
|
||
$ErrorActionPreference = "Continue"
|
||
& docker @dockerArgs 2>&1 | Out-Null
|
||
$ErrorActionPreference = $prevEAP
|
||
|
||
if ($LASTEXITCODE -eq 0) {
|
||
Write-Host " [START] edu-apollo-router (waiting 25s for supergraph compose)..." -ForegroundColor Green
|
||
Start-Sleep -Seconds 25
|
||
# 验证健康
|
||
try {
|
||
$null = Invoke-RestMethod -Uri "http://localhost:8088/health" -Method Get -TimeoutSec 5 -ErrorAction Stop
|
||
Write-Host " [OK] Apollo Router healthy (GraphQL: http://localhost:3000/graphql)" -ForegroundColor Green
|
||
} catch {
|
||
Write-Host " [WARN] Router not yet healthy, check: docker logs edu-apollo-router" -ForegroundColor Yellow
|
||
}
|
||
} else {
|
||
Write-Host " [FAIL] docker run failed" -ForegroundColor Red
|
||
Write-Host " Portal-shell will fall back to config-service direct connection (http://localhost:3011)" -ForegroundColor Yellow
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
if ($SkipRouter) {
|
||
Write-Host "[8/8] Skipping Apollo Router (-SkipRouter)" -ForegroundColor Gray
|
||
} else {
|
||
Write-Host "[8/8] Skipping Apollo Router (-SkipApps: no subgraphs running)" -ForegroundColor Gray
|
||
}
|
||
}
|
||
Write-Host ""
|
||
|
||
Write-Host "Access points:" -ForegroundColor Yellow
|
||
Write-Host " Portal Shell (前端): http://localhost:4010" -ForegroundColor White
|
||
Write-Host " Apollo Router (GraphQL): http://localhost:3000" -ForegroundColor White
|
||
Write-Host " API Gateway: http://localhost:8080" -ForegroundColor White
|
||
Write-Host " IAM Health: http://localhost:3002/healthz" -ForegroundColor White
|
||
Write-Host " Grafana: http://localhost:3030" -ForegroundColor White
|
||
Write-Host " Jaeger: http://localhost:16686" -ForegroundColor White
|
||
Write-Host ""
|