从 SFTP 拉 DMP、清库、impdp 导入、补授权、改配置,最后挂到计划任务------看似简单的自动化,踩了不下十个坑。本文完整记录问题与解决过程,附最终可用脚本。
一、需求背景
公司有一套测试环境需要定期从生产备份刷新两个 Oracle 用户:
CCRCPQU33:主应用 schemaCCRCPQ:关联 schema,需要 SELECT 主 schema 的部分表/视图
生产库每天凌晨 0:40 自动 expdp 备份到一台 SFTP 服务器(10.0.216.80),测试环境需要每天凌晨 1:40 自动从 SFTP 拉最新 DMP,清掉旧用户,重新导入,补授权,最后更新数据源地址和登录密码。
环境:
- Windows Server 2012 R2(6.3.9600)
- Oracle 11g(11.2.0.1)客户端
- WinSCP(公钥认证)
- PowerShell 5.1
二、整体架构
┌─────────────────┐ SFTP (公钥认证) ┌──────────────────┐
│ 生产 Oracle │ ──────────────────────> │ SFTP 服务器 │
│ expdp 0:40 │ │ 10.0.216.80 │
└─────────────────┘ └──────────────────┘
│
│ WinSCP.com
│ -privatekey
▼
┌──────────────────┐
│ 测试 Windows │
│ 1:40 计划任务 │
│ ↓ │
│ sqlplus 杀会话 │
│ DROP USER │
│ impdp 导入 │
│ GRANT 补授权 │
│ UPDATE 改配置 │
└──────────────────┘
三、踩坑与解决
坑 1:SFTP 公钥认证一直"服务器拒绝接受我们的密钥"
现象 :WinSCP 报错 服务器拒绝接受我们的密钥,但同一把 ppk 在备份脚本里能用。
排查过程:
-
确认 ppk 格式正确:
Get-Content D:\id_rsa_for_winscp.ppk -TotalCount 1输出PuTTY-User-Key-File-3: ssh-rsa,格式没问题。 -
对比服务器端
~/.ssh/authorized_keys和本地 ppk 导出的公钥:powershell& "C:\Program Files (x86)\WinSCP\puttygen.exe" D:\id_rsa_for_winscp.ppk -O public-openssh -o D:\mykey.pub Get-Content D:\mykey.pub -
服务器上:
bashcat ~/.ssh/authorized_keys
根因 :authorized_keys 里两把公钥粘到了同一行:
ssh-rsa AAAAB3...rsa-key-20260828ssh-rsa AAAAB3...rsa-key-20260827
↑
这里本该换行
SSH 按行解析公钥,整行畸形 = 两把全废。
解决:在服务器上重写,确保每把公钥独占一行:
bash
sudo su - vod
cp ~/.ssh/authorized_keys ~/.ssh/authorized_keys.bak_$(date +%Y%m%d)
cat > ~/.ssh/authorized_keys << 'EOF'
ssh-rsa AAAAB3...rsa-key-20260828
ssh-rsa AAAAB3...rsa-key-20260827
EOF
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
# 验证:应该输出 2
wc -l ~/.ssh/authorized_keys
注意:
- 是
<<(两个小于号,heredoc),不是< - 是
>>(追加)而不是>(覆盖),除非确定要重写 - 每把公钥后必须有换行,Windows 里粘贴到 Linux 常常丢换行
坑 2:PowerShell 控制台 0x1F 崩溃
现象:
FATAL: The Win32 internal error "A device attached to the system is not functioning"
0x1F occurred while writing to the console output buffer at the current cursor position.
根因 :脚本里用了 chcp 65001 切到 UTF-8 代码页,但运行脚本的控制台字体不支持 UTF-8。一输出中文字符,底层 Win32 就崩。
尝试过的错误方案 :把所有输出改成 ASCII → WinSCP 的中文错误信息变成 ????,什么都看不到。
最终方案:
- 不要在脚本里改代码页 (删掉
chcp 65001和[Console]::OutputEncoding) - 用系统默认(中文 Windows = GBK/936),WinSCP 的中文报错正常显示
- 把 WinSCP 的原始输出写成 UTF-8 文件作为备份:
powershell
$rawLog = Join-Path $ScriptLogDir "winscp_raw_$(Get-Date -Format 'yyyyMMdd_HHmmss').log"
$raw | Out-File -FilePath $rawLog -Encoding UTF8
教训 :控制台编码是最容易踩的坑,别在脚本里硬改,让系统默认。需要留证的场景,写到文件里最稳。
坑 3:PowerShell 里调 impdp 报"空异常"
现象:
[3/6] Importing CCRCPQU33 ...
----- impdp ... -----
FATAL:
Location: D:\download_dmp.ps1:94 字符: 12
+ $out = & $Tool @Args 2>&1
FATAL: 后面什么都没有。
排查 :手动在 PowerShell 里跑 impdp 完全正常,数据导入了。说明不是 impdp 的问题,是 PowerShell 调用方式的问题。
根因有两个:
$Args是 PowerShell 的保留变量 ,即使函数参数里定义了[string[]]$Args,某些 PowerShell 5.1 版本下@Args展开仍会出问题。- 参数含
@符号 (system/abcd1234@orcl),PowerShell 传参给外部 exe 时解析异常。
解决 :改用 cmd.exe /c 包一层,避开 PowerShell 的解析:
powershell
function Invoke-OracleTool {
param(
[string]$Tool,
[string[]]$ToolArgs # 改名,避开 $Args 保留变量
)
$quoted = $ToolArgs | ForEach-Object {
if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '""') + '"' } else { $_ }
}
$cmdLine = "$Tool " + ($quoted -join ' ')
& cmd.exe /c $cmdLine
$ec = $LASTEXITCODE
# ...
}
教训:
$Args、$Input、$Error等是 PowerShell 自动变量,别拿来做参数名- 调用含
@参数的 Oracle 工具,一律走cmd /c - 异常信息空的时候,一定要打印
Exception.GetType()、Exception.Message、InnerException、ScriptStackTrace,才能定位
坑 4:impdp 退出码 5 被误判为失败
现象:
Job "SYSTEM"."SYS_IMPORT_SCHEMA_01" completed with 8 error(s)
impdp exit code: 5
FATAL: impdp exit code 5
但数据其实全部导入成功。
根因:Oracle Data Pump 的退出码语义:
| 退出码 | 含义 |
|---|---|
| 0 | 完全成功 |
| 5 | 完成但有 warning(对象级失败) |
| 1 | 致命错误 |
这里的 8 个 warning 是 ORA-01917: user or role 'CCRCPQ' does not exist------因为导 CCRCPQU33 时 CCRCPQ 用户还没建,dump 里的 GRANT ... TO CCRCPQ 就失败了。这属于预期行为,脚本第 5 步会重新授权。
解决 :Invoke-OracleTool 把退出码 5 当成功:
powershell
if ($ec -eq 0) {
Write-Log " $Tool exit code: 0 (success)" "Green"
} elseif ($ec -eq 5) {
Write-Log " $Tool exit code: 5 (completed with warnings - OK for Data Pump)" "Yellow"
} else {
throw "$Tool exit code $ec"
}
教训 :Oracle 工具的退出码语义跟 Unix 惯例不同,别用 != 0 判断失败。
坑 5:impdp 大文件导入时 PowerShell 卡死
现象 :CCRCPQU33 只有 13 MB,秒过;CCRCPQ 有 912 MB,脚本卡在 [4/6] 一动不动。
根因 :PowerShell 里 $out = & cmd.exe /c $cmdLine 2>&1 捕获输出时,外部命令输出量超过 PowerShell 内部缓冲区会死锁------cmd 等 PowerShell 读,PowerShell 等 cmd 写。
解决 :不捕获输出,让 impdp 直接写到控制台:
powershell
# 旧(会卡)
$out = & cmd.exe /c $cmdLine 2>&1
# 新(不卡)
& cmd.exe /c $cmdLine
$ec = $LASTEXITCODE
impdp 自带 logfile= 参数,日志写到 $LocalDumpDir\imp_CCRCPQ.log,不依赖 PowerShell 捕获。
教训 :大输出的外部命令,永远别用 PowerShell 的 $out = & ... 2>&1 捕获,让它直接输出到控制台或写到文件。
坑 6:transform=oid:n 是什么
答:控制导入时对象 OID(Object Identifier)的处理。
- 默认(
oid:y):保留源库对象的原始 OID oid:n:让目标库重新分配新 OID
用不用?
| 场景 | 建议 |
|---|---|
| 目标库干净(先 DROP 再导) | 可用可不用 |
| 目标库有同名对象类型 | 必须用,否则 ORA-02304 |
| 生产库导出的 dump | 建议保留 |
对业务无影响------OID 只是 Oracle 内部标识符,业务代码用表名、列名访问数据,跟 OID 无关。
结论:留着,零风险。
坑 7:schtasks 在 PowerShell 里报"缺少 sc"
现象:
powershell
PS D:\> schtasks /create /tn "Refresh_CCRCPQ_DB" /tr "..." /sc DAILY /st 01:40 /ru "Administrator" /rp "abcd@1234" /rl HIGHEST /f
无效语法。缺少强制选项 'sc'。
参数明明都给了,就是报缺 sc。
根因 :PowerShell 对 / 开头的参数有特殊解析 ,会当成命令开关,传给 schtasks 时参数丢失。schtasks 是传统 Win32 程序,用 /参数 风格,跟 PowerShell 的解析规则冲突。
解决 :用 cmd.exe 跑:
cmd
schtasks /create /tn "Refresh_CCRCPQ_DB" /tr "powershell.exe -ExecutionPolicy Bypass -NoProfile -File \"D:\download_dmp.ps1\"" /sc DAILY /st 01:40 /ru "Administrator" /rp "abcd@1234" /rl HIGHEST /f
其他方案:
- PowerShell 里
cmd /c 'schtasks /create ...'包一层 - 用图形界面
taskschd.msc(最稳,就是步骤多)
教训 :老式 Win32 命令行工具,在 PowerShell 里调不通,就用 cmd。
四、最终脚本
powershell
<#
.SYNOPSIS
Test server CCRCPQU33 / CCRCPQ database refresh script
.DESCRIPTION
1. Download latest DMP files from SFTP using WinSCP.com (public key auth)
2. Kill residual sessions and DROP USER ... CASCADE for both users
3. Import CCRCPQU33 then CCRCPQ using impdp
4. Grant SELECT privileges (CCRCPQ -> CCRCPQU33 objects)
5. Update datasource URL and login password
#>
[CmdletBinding()]
param(
[string]$SftpHost = "10.0.216.80",
[int] $SftpPort = 22,
[string]$SftpUser = "vod",
[string]$SftpPrivateKey = "D:\id_rsa_for_winscp.ppk",
[string]$RemoteDir = "/home/vod/dingpan/dmp",
[string]$WinSCP = "C:\Program Files (x86)\WinSCP\WinSCP.com",
[string]$LocalDumpDir = "E:\app\Administrator\admin\orcl\dpdump",
[string]$SysConn = "system/abcd1234@orcl",
[string]$SysDbaConn = "/ as sysdba",
[string]$Directory = "DATA_PUMP_DIR",
[string]$ScriptLogDir = "E:\db_refresh\logs"
)
$ErrorActionPreference = "Stop"
$startTime = Get-Date
# ---------- Log setup ----------
if (!(Test-Path $ScriptLogDir)) {
New-Item -ItemType Directory -Path $ScriptLogDir -Force | Out-Null
}
$ScriptLog = Join-Path $ScriptLogDir "refresh_$(Get-Date -Format 'yyyyMMdd_HHmmss').log"
function Write-Log {
param([string]$Message, [string]$Color = "Gray")
$Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$LogEntry = "[$Timestamp] $Message"
Write-Host $LogEntry -ForegroundColor $Color
Add-Content -Path $ScriptLog -Value $LogEntry -Encoding UTF8
}
# ============================================================
# SQL*Plus 调用
# ============================================================
function Invoke-SqlPlus {
param(
[Parameter(Mandatory=$true)][string]$Sql,
[Parameter(Mandatory=$true)][string]$Conn,
[switch]$AllowError
)
$tmp = [System.IO.Path]::Combine($env:TEMP, "dbrefresh_$([guid]::NewGuid().ToString('N')).sql")
$Sql | Out-File -FilePath $tmp -Encoding ASCII
Write-Log "----- SQL*Plus ($Conn) -----" "DarkGray"
Write-Log $Sql "DarkGray"
try {
$out = & sqlplus -S $Conn "@$tmp" 2>&1
$out | ForEach-Object { Write-Host $_; Add-Content -Path $ScriptLog -Value $_ -Encoding UTF8 }
if ($LASTEXITCODE -ne 0 -and -not $AllowError) {
throw "sqlplus exit code $LASTEXITCODE"
}
return $out
}
finally {
Remove-Item $tmp -Force -ErrorAction SilentlyContinue
}
}
# ============================================================
# impdp / expdp 调用(通过 cmd.exe,避开 PowerShell 参数解析坑)
# 退出码语义:0=成功,5=有warning(视为成功),其他=失败
# ============================================================
function Invoke-OracleTool {
param(
[string]$Tool,
[string[]]$ToolArgs
)
Write-Log "----- $Tool $($ToolArgs -join ' ') -----" "DarkGray"
$cmd = Get-Command $Tool -ErrorAction SilentlyContinue
if (-not $cmd) {
throw "$Tool not found in PATH."
}
Write-Log " Using: $($cmd.Path)" "DarkGray"
$quoted = $ToolArgs | ForEach-Object {
if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '""') + '"' } else { $_ }
}
$cmdLine = "$Tool " + ($quoted -join ' ')
Write-Log " CMD: $cmdLine" "DarkGray"
# 关键:不抓输出(大文件导入时会死锁),让 cmd 直接输出到控制台
& cmd.exe /c $cmdLine
$ec = $LASTEXITCODE
if ($ec -eq 0) {
Write-Log " $Tool exit code: 0 (success)" "Green"
} elseif ($ec -eq 5) {
Write-Log " $Tool exit code: 5 (completed with warnings - OK for Data Pump)" "Yellow"
} else {
Write-Log " $Tool exit code: $ec" "Red"
throw "$Tool exit code $ec"
}
}
# ============================================================
# 远端目录列表
# ============================================================
function Get-RemoteFileList {
param([string]$Pattern = "*")
$lsScript = @"
open sftp://$SftpUser@$($SftpHost):$SftpPort/ -hostkey="*" -privatekey="$SftpPrivateKey"
cd "$RemoteDir"
ls
exit
"@
$tmpScript = [System.IO.Path]::GetTempFileName()
$lsScript | Out-File -FilePath $tmpScript -Encoding ASCII
$prevEAP = $ErrorActionPreference
$ErrorActionPreference = "Continue"
try {
$raw = & $WinSCP /ini=NUL /nointeractiveinput /script=$tmpScript 2>&1
$exitCode = $LASTEXITCODE
} finally {
$ErrorActionPreference = $prevEAP
Remove-Item $tmpScript -Force -ErrorAction SilentlyContinue
}
# 原始输出写 UTF-8 文件,便于排查
$rawLog = Join-Path $ScriptLogDir "winscp_raw_$(Get-Date -Format 'yyyyMMdd_HHmmss').log"
$raw | Out-File -FilePath $rawLog -Encoding UTF8
Write-Host "----- WinSCP raw output saved to: $rawLog -----" -ForegroundColor Yellow
Write-Host "----- WinSCP raw output begin -----" -ForegroundColor DarkGray
$raw | ForEach-Object { Write-Host $_ }
Write-Host "----- WinSCP raw output end (exit=$exitCode) -----" -ForegroundColor DarkGray
if ($exitCode -ne 0) {
throw "WinSCP ls failed, exit code $exitCode. See $rawLog"
}
$files = @()
foreach ($line in $raw) {
if ($line -match '\s+(\S+\.(?:DMP|dmp))\s*$') {
$files += $matches[1]
}
}
if ($Pattern -and $Pattern -ne "*") {
$regex = '^' + ($Pattern -replace '\.', '\.' -replace '\*', '.*') + '$'
$files = $files | Where-Object { $_ -match $regex }
}
Write-Host " Parsed $($files.Count) DMP files" -ForegroundColor Gray
return $files
}
function Get-LatestDumpFile {
param(
[string]$SchemaName,
[string[]]$Files
)
$matched = $Files | Where-Object {
$_ -like "${SchemaName}_*.DMP" -or $_ -like "${SchemaName}_*.dmp"
}
if (-not $matched -or $matched.Count -eq 0) {
throw "No ${SchemaName}_*.DMP found in remote dir $RemoteDir"
}
$sorted = $matched | Sort-Object {
if ($_ -match '_(\d{8}_\d{6})\.(?:dmp|DMP)$') {
[datetime]::ParseExact($matches[1], 'yyyyMMdd_HHmmss', $null)
} else {
[datetime]::MinValue
}
} -Descending
return $sorted[0]
}
# ============================================================
# 主流程
# ============================================================
try {
# ============================================================
# 1) 从 SFTP 下载最新 DMP
# ============================================================
Write-Log "`n[1/6] Downloading latest DMP files from SFTP..." "Cyan"
if (-not (Test-Path $WinSCP)) {
throw "WinSCP.com not found: $WinSCP"
}
if (-not (Test-Path $SftpPrivateKey)) {
throw "Private key not found: $SftpPrivateKey"
}
if (-not (Test-Path $LocalDumpDir)) {
throw "Local DMP directory not found: $LocalDumpDir"
}
Write-Log " Listing remote dir $RemoteDir ..."
$allDumps = Get-RemoteFileList
Write-Log " Found $($allDumps.Count) DMP files on remote"
$DumpFile1 = Get-LatestDumpFile -SchemaName "CCRCPQU33" -Files $allDumps
$DumpFile2 = Get-LatestDumpFile -SchemaName "CCRCPQ" -Files $allDumps
Write-Log " Latest CCRCPQU33 dump: $DumpFile1" "Green"
Write-Log " Latest CCRCPQ dump: $DumpFile2" "Green"
$local1 = Join-Path $LocalDumpDir $DumpFile1
$local2 = Join-Path $LocalDumpDir $DumpFile2
foreach ($f in @($local1, $local2)) {
if (Test-Path $f) { Remove-Item $f -Force }
}
$WinscpScript = @"
open sftp://$SftpUser@$($SftpHost):$SftpPort/ -hostkey="*" -privatekey="$SftpPrivateKey"
lcd "$LocalDumpDir"
cd "$RemoteDir"
get "$DumpFile1"
get "$DumpFile2"
exit
"@
$ScriptFile = [System.IO.Path]::GetTempFileName()
$WinscpScript | Out-File -FilePath $ScriptFile -Encoding ASCII
$WinscpLog = Join-Path $ScriptLogDir "winscp_download_$(Get-Date -Format 'yyyyMMdd_HHmmss').log"
$WinscpOutput = & $WinSCP /ini=NUL /nointeractiveinput /script=$ScriptFile /log="$WinscpLog" 2>&1
$WinscpOutput | ForEach-Object { Write-Host $_; Add-Content -Path $ScriptLog -Value $_ -Encoding UTF8 }
Remove-Item $ScriptFile -Force -ErrorAction SilentlyContinue
if ($LASTEXITCODE -ne 0) {
throw "WinSCP download failed, exit code $LASTEXITCODE"
}
foreach ($f in @($local1, $local2)) {
if (-not (Test-Path $f)) {
throw "File not found after download: $f"
}
$size = [math]::Round((Get-Item $f).Length / 1MB, 2)
Write-Log " Downloaded: $(Split-Path $f -Leaf) ($size MB)" "Green"
}
Write-Log " Download complete" "Green"
# ============================================================
# 2) 杀会话 + DROP USER
# ============================================================
Write-Log "`n[2/6] Cleaning existing users CCRCPQU33 / CCRCPQ ..." "Cyan"
$killSql = @"
SET HEADING OFF FEEDBACK OFF ECHO OFF VERIFY OFF
WHENEVER SQLERROR CONTINUE
DECLARE
v_cnt PLS_INTEGER := 0;
BEGIN
FOR r IN (SELECT sid, serial#, username FROM v`$session
WHERE username IN ('CCRCPQU33','CCRCPQ')) LOOP
BEGIN
EXECUTE IMMEDIATE 'ALTER SYSTEM KILL SESSION ''' || r.sid || ',' || r.serial# || ''' IMMEDIATE';
v_cnt := v_cnt + 1;
EXCEPTION WHEN OTHERS THEN NULL;
END;
END LOOP;
DBMS_OUTPUT.PUT_LINE('killed sessions: ' || v_cnt);
END;
/
EXIT
"@
Invoke-SqlPlus -Sql $killSql -Conn $SysDbaConn -AllowError
Start-Sleep -Seconds 5
$dropSql = @"
WHENEVER SQLERROR CONTINUE
DECLARE
v_cnt PLS_INTEGER := 0;
BEGIN
FOR r IN (SELECT sid, serial#, username FROM v`$session
WHERE username IN ('CCRCPQU33','CCRCPQ')) LOOP
BEGIN
EXECUTE IMMEDIATE 'ALTER SYSTEM KILL SESSION ''' || r.sid || ',' || r.serial# || ''' IMMEDIATE';
v_cnt := v_cnt + 1;
EXCEPTION WHEN OTHERS THEN NULL;
END;
END LOOP;
END;
/
DROP USER CCRCPQU33 CASCADE;
DROP USER CCRCPQ CASCADE;
SELECT username FROM dba_users WHERE username IN ('CCRCPQU33','CCRCPQ');
EXIT
"@
$dropOut = Invoke-SqlPlus -Sql $dropSql -Conn $SysDbaConn -AllowError
$stillThere = $dropOut | Select-String -Pattern "^(CCRCPQU33|CCRCPQ)\s*$"
if ($stillThere) {
Write-Log "WARNING: user still exists after DROP USER:" "Yellow"
$stillThere | ForEach-Object { Write-Log " $_" "Yellow" }
} else {
Write-Log " User cleanup complete" "Green"
}
# ============================================================
# 3) 导入 CCRCPQU33
# ============================================================
Write-Log "`n[3/6] Importing CCRCPQU33 ..." "Cyan"
Invoke-OracleTool -Tool "impdp" -ToolArgs @(
"$SysConn",
"directory=$Directory",
"dumpfile=$DumpFile1",
"schemas=CCRCPQU33",
"logfile=imp_CCRCPQU33.log",
"transform=oid:n"
)
Write-Log " CCRCPQU33 import complete" "Green"
# ============================================================
# 4) 导入 CCRCPQ
# ============================================================
Write-Log "`n[4/6] Importing CCRCPQ ..." "Cyan"
Invoke-OracleTool -Tool "impdp" -ToolArgs @(
"$SysConn",
"directory=$Directory",
"dumpfile=$DumpFile2",
"schemas=CCRCPQ",
"logfile=imp_CCRCPQ.log",
"transform=oid:n"
)
Write-Log " CCRCPQ import complete" "Green"
# ============================================================
# 5) 补授权
# ============================================================
Write-Log "`n[5/6] Granting SELECT privileges ..." "Cyan"
$grantSql = @"
WHENEVER SQLERROR CONTINUE
GRANT SELECT ON "CCRCPQU33"."LEMIS_LOGIN_USERINFO" TO "CCRCPQ";
GRANT SELECT ON "CCRCPQU33"."UP_LEMIS_MENU" TO "CCRCPQ";
GRANT SELECT ON "CCRCPQU33"."UP_ORG_UNIT_EXT_LEMIS" TO "CCRCPQ";
GRANT SELECT ON "CCRCPQU33"."UP_ORG_USER" TO "CCRCPQ";
GRANT SELECT ON "CCRCPQU33"."UP_ORG_USER_BUSIROLE" TO "CCRCPQ";
GRANT SELECT ON "CCRCPQU33"."UP_ORG_USER_EXT_LEMIS" TO "CCRCPQ";
GRANT SELECT ON "CCRCPQU33"."V_ORG_USER_UNIT" TO "CCRCPQ";
GRANT SELECT ON "CCRCPQU33"."V_USER_MENU_AUTHORITY" TO "CCRCPQ";
EXIT
"@
Invoke-SqlPlus -Sql $grantSql -Conn $SysConn -AllowError
Write-Log " Grant complete" "Green"
# ============================================================
# 6) 更新数据源地址 + 登录密码
# ============================================================
Write-Log "`n[6/6] Updating datasource URL and login password ..." "Cyan"
$updateSql = @"
WHENEVER SQLERROR CONTINUE
UPDATE CCRCPQU33.DRM_SYS_DATASOURCE
SET DBURL = 'jdbc:oracle:thin:@172.16.0.60:1521:orcl';
UPDATE CCRCPQU33.UP_ORG_USER
SET USER_PASSWORD = 'ISGMyneATSuhkiwz4BURBQ==';
COMMIT;
EXIT
"@
Invoke-SqlPlus -Sql $updateSql -Conn $SysConn -AllowError
Write-Log " Update complete" "Green"
$elapsed = (Get-Date) - $startTime
Write-Log "`n============================================" "Green"
Write-Log " All steps completed. Elapsed: $($elapsed.ToString('hh\:mm\:ss'))" "Green"
Write-Log "============================================" "Green"
}
catch {
Write-Log "FATAL: $($_.Exception.Message)" "Red"
Write-Log "Location: $($_.InvocationInfo.PositionMessage)" "Red"
Write-Log "Stack: $($_.ScriptStackTrace)" "Red"
Write-Host "`nPress any key to exit..." -ForegroundColor Yellow
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
exit 1
}
五、计划任务配置
必须用 cmd.exe 创建(PowerShell 里 schtasks 会报"缺少 sc"):
cmd
schtasks /create /tn "Refresh_CCRCPQ_DB" /tr "powershell.exe -ExecutionPolicy Bypass -NoProfile -File \"D:\download_dmp.ps1\"" /sc DAILY /st 01:40 /ru "Administrator" /rp "你的密码" /rl HIGHEST /f
参数说明:
| 参数 | 作用 |
|---|---|
/tr |
执行的命令,-NoProfile 避免 profile 干扰 |
/sc DAILY /st 01:40 |
每天 01:40 执行 |
/ru Administrator |
关键:必须用有 sysdba OS 认证权限的账户 |
/rp |
账户密码,不加会弹窗询问 |
/rl HIGHEST |
最高权限,sqlplus / as sysdba 需要 |
/f |
覆盖同名任务 |
验证:
powershell
schtasks /query /tn "Refresh_CCRCPQ_DB" /v /fo LIST
schtasks /run /tn "Refresh_CCRCPQ_DB"
创建后清理命令历史(避免密码留痕):
powershell
Remove-Item "$env:APPDATA\Microsoft\Windows\PowerShell\PSReadLine\ConsoleHost_history.txt" -Force -ErrorAction SilentlyContinue
六、经验总结
关于 SSH / SFTP
authorized_keys一行一把公钥,粘错换行整行全废- WinSCP 只认 PuTTY 格式 ppk ,OpenSSH 私钥要先
puttygen转换 - 命令行调 WinSCP 用
/ini=NUL /nointeractiveinput,防止弹交互卡住 - 首次连接用
-hostkey="*"跳过校验,但生产环境建议写死指纹
关于 PowerShell
$Args、$Input、$Error是保留变量,别做参数名- 外部命令含
@参数,走cmd /c,避开 PowerShell 参数解析 - 大输出的外部命令别用
$out = & ... 2>&1捕获,会死锁 - 老式 Win32 工具在 PowerShell 里调不通,直接用 cmd
Write-Host在字体不支持 UTF-8 的控制台会触发 0x1F,别硬改代码页- 异常信息为空时,打印
GetType()/Message/InnerException/ScriptStackTrace
关于 Oracle Data Pump
- 退出码 5 = completed with warnings,不是失败
transform=oid:n对业务无影响,干净环境可用可不用- impdp 自带
logfile=,日志分析看它,别依赖 stdout - 导 A 用户时若 dump 里有
GRANT ... TO B会报 ORA-01917,正常现象,后续补授权即可
关于计划任务
- 用有权限的账户运行,别用 SYSTEM
/rl HIGHEST保证 OS 认证可用schtasks在 cmd 里创建,PowerShell 里容易参数丢失- 创建后清理命令历史,避免密码泄漏
七、结语
这套脚本从开始到跑通,踩了:
- SSH 公钥格式坑
- Windows 控制台编码坑
- PowerShell 参数解析坑
- Oracle 退出码语义坑
- PowerShell 管道死锁坑
- schtasks 在 PowerShell 里的坑
每一个坑单独看都不复杂,叠在一起就是三天工作量。 希望本文能帮到遇到类似问题的同学,少走弯路。
附:脚本文件清单
| 文件 | 说明 |
|---|---|
D:\download_dmp.ps1 |
主脚本 |
D:\id_rsa_for_winscp.ppk |
SFTP 私钥 |
E:\app\Administrator\admin\orcl\dpdump\ |
DMP 落地目录 |
E:\db_refresh\logs\refresh_*.log |
脚本运行日志 |
E:\db_refresh\logs\winscp_raw_*.log |
WinSCP 原始输出 |
E:\db_refresh\logs\winscp_download_*.log |
WinSCP 会话日志 |