记一次 Oracle 测试库自动刷新脚本的踩坑实录

从 SFTP 拉 DMP、清库、impdp 导入、补授权、改配置,最后挂到计划任务------看似简单的自动化,踩了不下十个坑。本文完整记录问题与解决过程,附最终可用脚本。


一、需求背景

公司有一套测试环境需要定期从生产备份刷新两个 Oracle 用户:

  • CCRCPQU33:主应用 schema
  • CCRCPQ:关联 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 在备份脚本里能用。

排查过程:

  1. 确认 ppk 格式正确:Get-Content D:\id_rsa_for_winscp.ppk -TotalCount 1 输出 PuTTY-User-Key-File-3: ssh-rsa,格式没问题。

  2. 对比服务器端 ~/.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
  3. 服务器上:

    bash 复制代码
    cat ~/.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 的中文错误信息变成 ????,什么都看不到。

最终方案:

  1. 不要在脚本里改代码页 (删掉 chcp 65001 和 [Console]::OutputEncoding)
  2. 用系统默认(中文 Windows = GBK/936),WinSCP 的中文报错正常显示
  3. 把 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 调用方式的问题。

根因有两个:

  1. $Args 是 PowerShell 的保留变量 ,即使函数参数里定义了 [string[]]$Args,某些 PowerShell 5.1 版本下 @Args 展开仍会出问题。
  2. 参数含 @ 符号 (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

  1. authorized_keys 一行一把公钥,粘错换行整行全废
  2. WinSCP 只认 PuTTY 格式 ppk ,OpenSSH 私钥要先 puttygen 转换
  3. 命令行调 WinSCP 用 /ini=NUL /nointeractiveinput,防止弹交互卡住
  4. 首次连接用 -hostkey="*" 跳过校验,但生产环境建议写死指纹

关于 PowerShell

  1. $Args、$Input、$Error 是保留变量,别做参数名
  2. 外部命令含 @ 参数,走 cmd /c,避开 PowerShell 参数解析
  3. 大输出的外部命令别用 $out = & ... 2>&1 捕获,会死锁
  4. 老式 Win32 工具在 PowerShell 里调不通,直接用 cmd
  5. Write-Host 在字体不支持 UTF-8 的控制台会触发 0x1F,别硬改代码页
  6. 异常信息为空时,打印 GetType() / Message / InnerException / ScriptStackTrace

关于 Oracle Data Pump

  1. 退出码 5 = completed with warnings,不是失败
  2. transform=oid:n 对业务无影响,干净环境可用可不用
  3. impdp 自带 logfile=,日志分析看它,别依赖 stdout
  4. 导 A 用户时若 dump 里有 GRANT ... TO B 会报 ORA-01917,正常现象,后续补授权即可

关于计划任务

  1. 用有权限的账户运行,别用 SYSTEM
  2. /rl HIGHEST 保证 OS 认证可用
  3. schtasks 在 cmd 里创建,PowerShell 里容易参数丢失
  4. 创建后清理命令历史,避免密码泄漏

七、结语

这套脚本从开始到跑通,踩了:

  • 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 会话日志
相关推荐
2501_931803751 小时前
MySQL 索引核心原理
数据库·mysql
浪潮IT馆1 小时前
Windows 10 安装 PostgreSQL 9.6.24 完整教程
数据库·windows·postgresql
Elastic 中国社区官方博客2 小时前
14 个 alerts,1 个 incident:使用 Elasticsearch 中的 ES|QL 衡量 alerting rule 噪声
大数据·运维·数据库·elasticsearch·搜索引擎·全文检索
资深技术分享员2 小时前
遗留系统——把“改不动的老系统“接过来
java·服务器·数据库
AllData公司负责人3 小时前
AllData数据中台物联网实时平台|集成 Apache StreamPipes,MQTT工业物联网数据实时预警实践案例
大数据·数据库·人工智能·物联网·apache·工业物联网·streampipes
在放️3 小时前
技术支持岗 · 笔试填空、问答题
数据库
oradh3 小时前
Oracle固定执行计划的方法----SQL Profie(二)
数据库·sql·oracle
Mortalbreeze3 小时前
MySQL 基础篇(二):数据库和数据表的基本操作
linux·服务器·数据库·mysql