[CmdletBinding()] param( [Parameter(Mandatory = $true)] [ValidatePattern('^https?://')] [string]$GatewayBaseUrl, [string[]]$AccessTokens = @(), [ValidateRange(1, 4)] [int]$TaskCount = 1, [string[]]$TaskIds = @(), [ValidateRange(1, 7200)] [int]$TimeoutSeconds = 3600, [ValidateRange(1, 60)] [int]$PollSeconds = 2, [ValidateRange(0, 200000)] [int]$MinimumRows = 0, [string]$Keyword = '', [ValidateSet('', 'PROFILE_REQUIRED', 'PENDING', 'ACTIVE', 'DISABLED', 'REJECTED')] [string]$Status = '', [string]$OutputDirectory = '', [switch]$CreateOnly, [switch]$SkipDownload ) $ErrorActionPreference = 'Stop' $baseUrl = $GatewayBaseUrl.TrimEnd('/') $apiPath = "$baseUrl/api/platform/v1/files/export-tasks" if ($AccessTokens.Count -eq 0 -and -not [string]::IsNullOrWhiteSpace($env:ADMIN_ACCESS_TOKEN)) { $AccessTokens = @($env:ADMIN_ACCESS_TOKEN) } if ($AccessTokens.Count -eq 0) { throw '至少提供一个 -AccessTokens,或通过 ADMIN_ACCESS_TOKEN 环境变量注入。' } if ($TaskIds.Count -eq 0 -and $AccessTokens.Count -lt $TaskCount) { throw "创建 $TaskCount 个并发任务需要至少 $TaskCount 个不同用户的 Access Token,避免 60 秒重复提交合并。" } if ($TaskIds.Count -gt 0 -and $AccessTokens.Count -lt $TaskIds.Count) { throw '恢复验收时每个 TaskId 都需要对应申请人的 Access Token。' } function Get-Headers([string]$Token) { return @{ Authorization = "Bearer $Token" } } function Invoke-ApiJson([string]$Method, [string]$Uri, [string]$Token, [object]$Body = $null) { $arguments = @{ Method = $Method Uri = $Uri Headers = Get-Headers $Token TimeoutSec = 60 } if ($null -ne $Body) { $arguments.ContentType = 'application/json' $arguments.Body = $Body | ConvertTo-Json -Depth 10 -Compress } $response = Invoke-RestMethod @arguments if ($null -eq $response -or [string]$response.code -ne '200') { $requestId = if ($response.requestId) { " requestId=$($response.requestId)" } else { '' } $traceId = if ($response.traceId) { " traceId=$($response.traceId)" } else { '' } throw "API 失败 method=$Method uri=$Uri code=$($response.code) message=$($response.message)$requestId$traceId" } return $response.data } function New-ExportTask([string]$Token, [int]$Index) { $parameters = [ordered]@{} if (-not [string]::IsNullOrWhiteSpace($Keyword)) { $parameters.keyword = $Keyword.Trim() } if (-not [string]::IsNullOrWhiteSpace($Status)) { $parameters.status = $Status } $payload = @{ exportType = 'CUSTOMER_LIST' format = 'XLSX' taskName = "客户列表验收-$Index" parameters = $parameters } return Invoke-ApiJson POST $apiPath $Token $payload } function Wait-ExportTask([string]$TaskId, [string]$Token) { $deadline = [DateTimeOffset]::UtcNow.AddSeconds($TimeoutSeconds) $lastStatus = '' while ([DateTimeOffset]::UtcNow -lt $deadline) { $task = Invoke-ApiJson GET "$apiPath/$TaskId" $Token if ($task.status -ne $lastStatus) { Write-Host "taskId=$TaskId status=$($task.status) processed=$($task.processedRows) total=$($task.totalRows)" $lastStatus = $task.status } if ($task.status -in @('SUCCEEDED', 'FAILED', 'EXPIRED')) { return $task } Start-Sleep -Seconds $PollSeconds } throw "等待导出任务超时 taskId=$TaskId timeoutSeconds=$TimeoutSeconds" } function Save-And-VerifyXlsx([object]$Task, [string]$Token, [string]$Directory) { $target = Join-Path $Directory "$($Task.id).xlsx" $response = Invoke-WebRequest -UseBasicParsing -Uri "$apiPath/$($Task.id)/download" ` -Headers (Get-Headers $Token) -TimeoutSec 600 -OutFile $target -PassThru $contentType = [string]$response.Headers['Content-Type'] if ($contentType.ToLowerInvariant().Contains('json')) { $payload = Get-Content -Raw -LiteralPath $target | ConvertFrom-Json throw "下载返回业务错误 taskId=$($Task.id) code=$($payload.code) message=$($payload.message) requestId=$($payload.requestId) traceId=$($payload.traceId)" } $stream = [IO.File]::OpenRead($target) try { $signature = New-Object byte[] 4 if ($stream.Read($signature, 0, 4) -ne 4 -or $signature[0] -ne 0x50 -or $signature[1] -ne 0x4B ` -or $signature[2] -ne 0x03 -or $signature[3] -ne 0x04) { throw "下载结果不是有效 XLSX/ZIP taskId=$($Task.id) contentType=$contentType" } } finally { $stream.Dispose() } return [pscustomobject]@{ Path = $target; Bytes = (Get-Item -LiteralPath $target).Length; ContentType = $contentType } } $createdTasks = [System.Collections.Generic.List[object]]::new() if ($TaskIds.Count -eq 0) { for ($index = 0; $index -lt $TaskCount; $index++) { $task = New-ExportTask $AccessTokens[$index] ($index + 1) $createdTasks.Add([pscustomobject]@{ Task = $task; Token = $AccessTokens[$index] }) Write-Host "created taskId=$($task.id) owner=$($task.requestedBy) status=$($task.status)" } $uniqueIds = @($createdTasks | ForEach-Object { [string]$_.Task.id } | Sort-Object -Unique) if ($uniqueIds.Count -ne $createdTasks.Count) { throw '多个创建请求被重复提交规则合并;容量验收需使用不同申请用户,或等待 60 秒后重试。' } } else { for ($index = 0; $index -lt $TaskIds.Count; $index++) { $createdTasks.Add([pscustomobject]@{ Task = [pscustomobject]@{ id = $TaskIds[$index] }; Token = $AccessTokens[$index] }) } } if ($CreateOnly) { $createdTasks | ForEach-Object { [pscustomobject]@{ taskId = [string]$_.Task.id } } | ConvertTo-Json exit 0 } $temporaryOutput = [string]::IsNullOrWhiteSpace($OutputDirectory) if ($temporaryOutput) { $OutputDirectory = Join-Path ([IO.Path]::GetTempPath()) "baiyue-export-acceptance-$([Guid]::NewGuid())" } New-Item -ItemType Directory -Path $OutputDirectory -Force | Out-Null $results = [System.Collections.Generic.List[object]]::new() try { foreach ($entry in $createdTasks) { $task = Wait-ExportTask ([string]$entry.Task.id) $entry.Token if ($task.status -ne 'SUCCEEDED') { throw "导出任务未成功 taskId=$($task.id) status=$($task.status) failure=$($task.failureMessage)" } if ([long]$task.processedRows -lt $MinimumRows) { throw "导出行数未达到门槛 taskId=$($task.id) processed=$($task.processedRows) minimum=$MinimumRows" } $artifact = $null if (-not $SkipDownload) { $artifact = Save-And-VerifyXlsx $task $entry.Token $OutputDirectory } $results.Add([pscustomobject]@{ taskId = [string]$task.id status = $task.status processedRows = [long]$task.processedRows sizeBytes = [long]$task.sizeBytes downloadedBytes = if ($artifact) { [long]$artifact.Bytes } else { 0 } file = if ($artifact) { $artifact.Path } else { $null } }) } $results | ConvertTo-Json -Depth 5 Write-Host "EXPORT_CENTER_ACCEPTANCE_PASSED=$($results.Count)" } finally { if ($temporaryOutput -and (Test-Path -LiteralPath $OutputDirectory)) { Remove-Item -LiteralPath $OutputDirectory -Recurse -Force } }