# ============================================================================= # 企微IT智能服务台 — dev 模式 schema 漂移检测 (PowerShell 版) # ============================================================================= # 作用:对比 SQLAlchemy 模型 vs 实际 DB 字段,防止"模型加了字段忘写 migration" # 历史踩坑: # - 010: agents.otp_secret (修了一次) # - 011: conversations.impact_scope/is_blocking/emotion_state (又踩一次) # 用法:.\scripts\dev-check-schema-drift.ps1 # 前置:dev 后端跑着(docker compose -f docker-compose.dev.yml up) # 实现: # 1. 在 backend 容器里跑 Python 一行反射模型 metadata # 2. 在 db 容器里跑 psql 拿 information_schema # 3. PowerShell 做差集对比输出 # ============================================================================= $ErrorActionPreference = 'Stop' $ProjectRoot = $PSScriptRoot | Split-Path -Parent Set-Location $ProjectRoot function Write-Step($msg) { Write-Host "`n$msg" -ForegroundColor Cyan } function Write-OK($msg) { Write-Host " ✓ $msg" -ForegroundColor Green } function Write-Warn($msg) { Write-Host " ⚠ $msg" -ForegroundColor Yellow } function Write-Err($msg) { Write-Host " ✗ $msg" -ForegroundColor Red } # -------------------------------------------------------------------------- # 拿 backend 容器 ID # -------------------------------------------------------------------------- $BkId = docker compose --env-file .env.dev -f docker-compose.dev.yml ps -q backend $DbId = docker compose --env-file .env.dev -f docker-compose.dev.yml ps -q postgres if (-not $BkId -or -not $DbId) { Write-Err "dev 后端或 db 容器没跑。先跑 dev-start.ps1" exit 1 } Write-Step "═══ Schema 漂移检测 ═══" # -------------------------------------------------------------------------- # 1) 拿模型字段(在 backend 容器里跑 Python) # -------------------------------------------------------------------------- $ModelJson = docker exec $BkId python -c @' import json from app.database import Base import app.models # 触发模型注册到 Base.metadata out = {} for tname, tbl in Base.metadata.tables.items(): out[tname] = sorted([c.name for c in tbl.columns]) print(json.dumps(out, ensure_ascii=False)) '@ 2>&1 # docker exec 输出会带 status warning,过滤掉 $ModelJson = ($ModelJson | Where-Object { $_ -notmatch '^\s*$' -and $_ -notmatch 'WARN|INFO' } | Select-Object -Last 1).Trim() if (-not $ModelJson) { Write-Err "拿不到模型字段。backend 容器里 Python 跑挂了" docker logs $BkId --tail 10 exit 1 } try { $ModelTables = $ModelJson | ConvertFrom-Json } catch { Write-Err "模型 JSON 解析失败: $ModelJson" exit 1 } Write-OK "模型字段: $($ModelTables.PSObject.Properties.Name.Count) 张表" # -------------------------------------------------------------------------- # 2) 拿 DB 字段(在 db 容器里跑 psql) # -------------------------------------------------------------------------- $DbSql = "SELECT table_name, column_name FROM information_schema.columns WHERE table_schema = 'public' ORDER BY table_name, ordinal_position;" $DbRaw = @(docker exec $DbId psql -U wecom -d wecom_it_desk_dev -A -t -F "`t" -c $DbSql 2>&1 | Where-Object { $_ -is [string] }) $DbColumns = @{} foreach ($line in $DbRaw) { if ($line -isnot [string]) { continue } $line = $line.Trim() if (-not $line) { continue } $parts = $line -split "`t" if ($parts.Count -lt 2) { continue } $tname = $parts[0] $cname = $parts[1] if (-not $DbColumns.ContainsKey($tname)) { $DbColumns[$tname] = New-Object System.Collections.Generic.List[string] } $DbColumns[$tname].Add($cname) } # 拿 alembic version $VersionRaw = @(docker exec $DbId psql -U wecom -d wecom_it_desk_dev -A -t -c "SELECT version_num FROM alembic_version;" 2>&1 | Where-Object { $_ -is [string] }) $Version = ($VersionRaw | Where-Object { $_ -match '^\d|_' } | Select-Object -First 1) if ($Version) { $Version = $Version.Trim() } Write-OK "DB 字段: $($DbColumns.Keys.Count) 张表 | alembic = $Version" # -------------------------------------------------------------------------- # 3) 差集对比 # -------------------------------------------------------------------------- $Drift = $false $OnlyInModel = @() $OnlyInDb = @() foreach ($tname in $ModelTables.PSObject.Properties.Name) { $mcols = $ModelTables.$tname $dcols = if ($DbColumns.ContainsKey($tname)) { $DbColumns[$tname] } else { @() } foreach ($c in $mcols) { if ($null -eq $dcols -or -not $dcols.Contains($c)) { $OnlyInModel += [PSCustomObject]@{ Table = $tname; Column = $c } } } foreach ($c in $dcols) { if ($null -ne $c -and -not $mcols.Contains($c) -and $tname -ne 'alembic_version') { $OnlyInDb += [PSCustomObject]@{ Table = $tname; Column = $c } } } } # -------------------------------------------------------------------------- # 4) 输出报告 # -------------------------------------------------------------------------- Write-Host "" Write-Host "════════════════════════════════════════════════════════════════════" -ForegroundColor White Write-Host " alembic current = $Version" -ForegroundColor Gray Write-Host " 模型表数 = $($ModelTables.PSObject.Properties.Name.Count) DB 表数 = $($DbColumns.Keys.Count)" -ForegroundColor Gray Write-Host "════════════════════════════════════════════════════════════════════" -ForegroundColor White if ($OnlyInModel.Count -gt 0) { $Drift = $true Write-Host "" Write-Host " ❌ 模型有,DB 缺($($OnlyInModel.Count) 个):" -ForegroundColor Red foreach ($x in $OnlyInModel) { Write-Host " - $($x.Table).$($x.Column)" -ForegroundColor Red } } if ($OnlyInDb.Count -gt 0) { $Drift = $true Write-Host "" Write-Host " ⚠️ DB 有,模型没($($OnlyInDb.Count) 个,可能遗留):" -ForegroundColor Yellow foreach ($x in $OnlyInDb) { Write-Host " - $($x.Table).$($x.Column)" -ForegroundColor Yellow } } if ($Drift) { Write-Host "" Write-Host " 💡 修法:" -ForegroundColor Cyan Write-Host " 1. cd backend" -ForegroundColor Gray Write-Host " 2. alembic revision --autogenerate -m 'sync xxx fields'" -ForegroundColor Gray Write-Host " 3. 检查生成的 migration 文件(review 一下,不要直接用)" -ForegroundColor Gray Write-Host " 4. docker exec $BkId alembic upgrade head" -ForegroundColor Gray Write-Host "" exit 1 } Write-Host "" Write-Host " ✅ schema 一致,无漂移" -ForegroundColor Green Write-Host " $($ModelTables.PSObject.Properties.Name.Count) 张表全部对齐" -ForegroundColor Green Write-Host "" Write-Host " 💡 建议:加到 Git pre-commit / dev 启动检查里" -ForegroundColor Gray Write-Host "" exit 0