feat(portal-shell): v2.0 P0 shadcn standardization + security + streaming + error handling
- 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 路由生成成功
This commit is contained in:
@@ -1,103 +1,169 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Edu start all application services script
|
||||
Edu v2.1 一键启动脚本
|
||||
.DESCRIPTION
|
||||
Starts 10 application services + 1 frontend (teacher-portal)
|
||||
Each service runs in a separate terminal window for log visibility
|
||||
Automatically injects Python service env vars (CLICKHOUSE/KAFKA/OTEL)
|
||||
Prerequisite: infrastructure containers (MySQL/Redis/Kafka/ClickHouse etc) must be running
|
||||
.PARAMETER SkipInfraCheck
|
||||
Skip infrastructure health check (use when infra is known to be running)
|
||||
启动顺序:
|
||||
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 -SkipInfraCheck
|
||||
.\scripts\start-all.ps1 # 启动全部
|
||||
.\scripts\start-all.ps1 -SkipInfra # 仅启动应用服务
|
||||
.\scripts\start-all.ps1 -SkipApps # 仅启动基础设施
|
||||
.\scripts\start-all.ps1 -Force # 自动杀端口占用
|
||||
#>
|
||||
param(
|
||||
[switch]$SkipInfraCheck,
|
||||
[switch]$Force
|
||||
[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 Start All Services" -ForegroundColor Cyan
|
||||
Write-Host " Edu v2.1 Start All Services" -ForegroundColor Cyan
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
# ===== 1. Infrastructure health check =====
|
||||
if (-not $SkipInfraCheck) {
|
||||
Write-Host "[1/6] Checking infrastructure health..." -ForegroundColor Yellow
|
||||
$infraServices = @(
|
||||
@{Name="MySQL"; Container="edu-mysql"},
|
||||
@{Name="Redis"; Container="edu-redis"},
|
||||
@{Name="Kafka"; Container="edu-kafka"},
|
||||
@{Name="ClickHouse"; Container="edu-clickhouse"},
|
||||
@{Name="Debezium"; Container="edu-debezium"},
|
||||
@{Name="Jaeger"; Container="edu-jaeger"}
|
||||
)
|
||||
$allHealthy = $true
|
||||
$prevEAP = $ErrorActionPreference
|
||||
$ErrorActionPreference = "Continue"
|
||||
foreach ($svc in $infraServices) {
|
||||
$running = docker inspect -f '{{.State.Running}}' $svc.Container 2>$null
|
||||
if ($running -ne "true") {
|
||||
Write-Host " [FAIL] $($svc.Name) not running" -ForegroundColor Red
|
||||
$allHealthy = $false
|
||||
continue
|
||||
}
|
||||
# 条件模板:Health 不存在时返回空字符串,不报错
|
||||
$health = docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{end}}' $svc.Container 2>$null
|
||||
if ($health -eq "healthy") {
|
||||
Write-Host " [OK] $($svc.Name) ($($svc.Container))" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host " [WARN] $($svc.Name) running (no healthcheck)" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
$ErrorActionPreference = $prevEAP
|
||||
if (-not $allHealthy) {
|
||||
Write-Host ""
|
||||
Write-Host "Infrastructure not ready. Start it first:" -ForegroundColor Red
|
||||
Write-Host " docker compose -f infra/docker-compose.yml --profile p6 --profile observability up -d" -ForegroundColor White
|
||||
exit 1
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
# ===== 2. Environment variables =====
|
||||
Write-Host "[2/6] Preparing environment variables..." -ForegroundColor Yellow
|
||||
|
||||
$env:DEV_MODE = "true"
|
||||
$env:OTEL_EXPORTER_OTLP_ENDPOINT = "http://localhost:4318"
|
||||
$env:DATABASE_URL = "mysql://edu:changeme@localhost:3306/next_edu_cloud"
|
||||
$env:REDIS_URL = "redis://localhost:6379"
|
||||
$env:JWT_SECRET = "p1-dev-secret-change-in-production"
|
||||
$env:KAFKA_BROKERS = "localhost:9092"
|
||||
|
||||
$pyEnv = @{
|
||||
CLICKHOUSE_HOST = "localhost"
|
||||
CLICKHOUSE_PORT = "8123"
|
||||
CLICKHOUSE_USER = "default"
|
||||
CLICKHOUSE_PASSWORD = "clickhouse"
|
||||
CLICKHOUSE_DATABASE = "edu_analytics"
|
||||
KAFKA_BROKERS = "localhost:9092"
|
||||
OTEL_ENDPOINT = "http://localhost:4318"
|
||||
DEV_MODE = "true"
|
||||
$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++
|
||||
}
|
||||
|
||||
Write-Host " DEV_MODE=true (dev-token bypass)" -ForegroundColor Green
|
||||
Write-Host " OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318" -ForegroundColor Green
|
||||
# 补充 .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 ""
|
||||
|
||||
# ===== 3. Port conflict check =====
|
||||
Write-Host "[3/6] Checking port conflicts..." -ForegroundColor Yellow
|
||||
# ===== 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 ""
|
||||
|
||||
$portMap = @{
|
||||
3001="classes"; 3002="iam"; 3003="teacher-bff"; 3004="core-edu"
|
||||
3005="content"; 3006="data-ana"; 3007="msg"; 3008="ai"
|
||||
8080="api-gateway"; 8081="push-gateway"; 3000="teacher-portal"
|
||||
# ===== 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
|
||||
@@ -110,31 +176,20 @@ foreach ($port in $portMap.Keys | Sort-Object) {
|
||||
$conflicts += [PSCustomObject]@{Port=$port; Service=$svcName; PID=$procId; Process=$procName}
|
||||
}
|
||||
}
|
||||
|
||||
if ($conflicts.Count -gt 0) {
|
||||
Write-Host ""
|
||||
Write-Host " $($conflicts.Count) port(s) already in use." -ForegroundColor Yellow
|
||||
|
||||
Write-Host " $($conflicts.Count) port(s) in use." -ForegroundColor Yellow
|
||||
$shouldKill = $false
|
||||
if ($Force) {
|
||||
Write-Host " -Force specified, killing automatically..." -ForegroundColor White
|
||||
$shouldKill = $true
|
||||
} else {
|
||||
Write-Host " These services may already be running. Options:" -ForegroundColor White
|
||||
Write-Host " 1. Run .\scripts\stop-all.ps1 -KillByPort first, then re-run start-all" -ForegroundColor White
|
||||
Write-Host " 2. Re-run with -Force to auto-kill and continue" -ForegroundColor White
|
||||
Write-Host ""
|
||||
$answer = Read-Host " Kill existing processes and continue? (y/N)"
|
||||
if ($answer -eq "y" -or $answer -eq "Y") {
|
||||
$shouldKill = $true
|
||||
}
|
||||
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) ($($c.Process)) on port $($c.Port)" -ForegroundColor Green
|
||||
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
|
||||
@@ -142,7 +197,7 @@ if ($conflicts.Count -gt 0) {
|
||||
}
|
||||
Start-Sleep -Seconds 2
|
||||
} else {
|
||||
Write-Host " Aborting. Please stop existing services first." -ForegroundColor Red
|
||||
Write-Host " Aborting." -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
} else {
|
||||
@@ -150,20 +205,26 @@ if ($conflicts.Count -gt 0) {
|
||||
}
|
||||
Write-Host ""
|
||||
|
||||
# ===== 4. Build NestJS services (required for nest start --watch) =====
|
||||
Write-Host "[4/6] Building NestJS services (first time required)..." -ForegroundColor Yellow
|
||||
# ===== 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 ""
|
||||
|
||||
$nestjsServices = @(
|
||||
"@edu/classes-service",
|
||||
"@edu/iam-service",
|
||||
"@edu/teacher-bff",
|
||||
"@edu/core-edu-service",
|
||||
"@edu/content-service",
|
||||
"@edu/msg-service"
|
||||
)
|
||||
|
||||
# Clean tsbuildinfo cache (incremental mode leftover causes tsc to skip emit)
|
||||
$nestjsDirs = @("classes", "iam", "teacher-bff", "core-edu", "content", "msg")
|
||||
# ===== 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) {
|
||||
@@ -171,131 +232,250 @@ foreach ($dir in $nestjsDirs) {
|
||||
Remove-Item -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($svc in $nestjsServices) {
|
||||
Write-Host " Building $svc..." -ForegroundColor Gray -NoNewline
|
||||
$buildResult = pnpm --filter $svc build 2>&1
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host " [OK]" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host " [FAIL]" -ForegroundColor Red
|
||||
Write-Host " $buildResult" -ForegroundColor DarkGray
|
||||
}
|
||||
}
|
||||
Write-Host " [OK] Cache cleaned" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
# ===== 5. Start application services =====
|
||||
Write-Host "[5/6] Starting application services (11 windows)..." -ForegroundColor Yellow
|
||||
# ===== 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-classes"; Cmd="pnpm"; Args=@("--filter","@edu/classes-service","dev"); Dir="$ProjectRoot"},
|
||||
@{Title="edu-app-iam"; Cmd="pnpm"; Args=@("--filter","@edu/iam-service","dev"); Dir="$ProjectRoot"},
|
||||
@{Title="edu-app-teacher-bff"; Cmd="pnpm"; Args=@("--filter","@edu/teacher-bff","dev"); Dir="$ProjectRoot"},
|
||||
@{Title="edu-app-core-edu"; Cmd="pnpm"; Args=@("--filter","@edu/core-edu-service","dev"); Dir="$ProjectRoot"},
|
||||
@{Title="edu-app-content"; Cmd="pnpm"; Args=@("--filter","@edu/content-service","dev"); Dir="$ProjectRoot"},
|
||||
@{Title="edu-app-msg"; Cmd="pnpm"; Args=@("--filter","@edu/msg-service","dev"); 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-teacher-portal";Cmd="pnpm";Args=@("--filter","teacher-portal","dev"); Dir="$ProjectRoot"}
|
||||
@{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) {
|
||||
$cmdStr = "$($svc.Cmd) $($svc.Args -join ' ')"
|
||||
|
||||
$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
|
||||
Start-Sleep -Milliseconds 500
|
||||
Write-Host " [START] $($svc.Title)" -ForegroundColor Green
|
||||
$startedCount++
|
||||
Start-Sleep -Milliseconds 800
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host " All services started in new windows. Waiting 40s for init..." -ForegroundColor Yellow
|
||||
Start-Sleep -Seconds 40
|
||||
Write-Host " $startedCount services launched. Waiting 60s for init..." -ForegroundColor Yellow
|
||||
Start-Sleep -Seconds 60
|
||||
|
||||
# ===== 6. Health check =====
|
||||
Write-Host "[6/6] Health check..." -ForegroundColor Yellow
|
||||
# ===== 7. 健康检查 =====
|
||||
Write-Host "[7/7] Health check..." -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
|
||||
$healthServices = @(
|
||||
@{Name="classes"; Url="http://localhost:3001/healthz"},
|
||||
@{Name="iam"; Url="http://localhost:3002/healthz"},
|
||||
@{Name="teacher-bff"; Url="http://localhost:3003/healthz"},
|
||||
@{Name="core-edu"; Url="http://localhost:3004/healthz"},
|
||||
@{Name="content"; Url="http://localhost:3005/healthz"},
|
||||
@{Name="data-ana"; Url="http://localhost:3006/healthz"},
|
||||
@{Name="msg"; Url="http://localhost:3007/healthz"},
|
||||
@{Name="ai"; Url="http://localhost:3008/healthz"},
|
||||
@{Name="api-gateway"; Url="http://localhost:8080/healthz"},
|
||||
@{Name="push-gateway"; Url="http://localhost:8081/healthz"},
|
||||
@{Name="teacher-portal";Url="http://localhost:3000/"}
|
||||
@{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 = 3
|
||||
$maxRetries = 5
|
||||
$success = $false
|
||||
$lastError = ""
|
||||
while ($retries -lt $maxRetries -and -not $success) {
|
||||
try {
|
||||
$null = Invoke-RestMethod -Uri $svc.Url -Method Get -TimeoutSec 5 -ErrorAction Stop
|
||||
Write-Host " [OK] $($svc.Name)" -ForegroundColor Green
|
||||
$tag = if ($svc.Critical) { "CRITICAL" } else { "optional" }
|
||||
Write-Host " [OK] $($svc.Name) ($tag)" -ForegroundColor Green
|
||||
$success = $true
|
||||
$okCount++
|
||||
} catch {
|
||||
$lastError = $_.Exception.Message
|
||||
$retries++
|
||||
if ($retries -lt $maxRetries) {
|
||||
Start-Sleep -Seconds 5
|
||||
}
|
||||
if ($retries -lt $maxRetries) { Start-Sleep -Seconds 4 }
|
||||
}
|
||||
}
|
||||
if (-not $success) {
|
||||
Write-Host " [FAIL] $($svc.Name) (after $maxRetries retries: $lastError)" -ForegroundColor Red
|
||||
$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 " Started: [OK] $okCount ready / [FAIL] $failCount failed" -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 services: $($failedServices -join ', ')" -ForegroundColor Red
|
||||
Write-Host "Failed: $($failedServices -join ', ')" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
Write-Host "Troubleshooting:" -ForegroundColor Yellow
|
||||
Write-Host " 1. Check the service window for error output" -ForegroundColor White
|
||||
Write-Host " 2. Verify dependencies: pnpm install / uv sync / go mod tidy" -ForegroundColor White
|
||||
Write-Host " 3. Re-run health check: .\scripts\health-check.ps1" -ForegroundColor White
|
||||
Write-Host " 4. Stop and retry: .\scripts\stop-all.ps1 -KillByPort then .\scripts\start-all.ps1" -ForegroundColor White
|
||||
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 ""
|
||||
exit 1
|
||||
}
|
||||
|
||||
# ===== 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 "Next steps:" -ForegroundColor Yellow
|
||||
Write-Host " Health check: .\scripts\health-check.ps1" -ForegroundColor White
|
||||
Write-Host " CDC test: .\scripts\test-cdc.ps1" -ForegroundColor White
|
||||
Write-Host " Stop all: .\scripts\stop-all.ps1" -ForegroundColor White
|
||||
|
||||
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 ""
|
||||
|
||||
Reference in New Issue
Block a user