<# .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 ""