param( [string]$RepositoryRoot = (Split-Path -Parent $PSScriptRoot), [string]$GatewayBaseUrl = "http://127.0.0.1:9000", [string]$MerchantAdminUsername = "merchant_admin", [Parameter(Mandatory = $true)][string]$MerchantAdminPassword, [string]$PlatformAdminUsername = "platform_admin", [string]$PlatformAdminPassword = "", [string]$TemporaryAdminPassword = "Phase1Temp456!", [string]$MemberInitialPassword = "Phase1Member123!", [string]$MemberResetPassword = "Phase1Member456!", [string]$OutputPath = "" ) $ErrorActionPreference = "Stop" $startedAt = Get-Date $stamp = Get-Date -Format "yyyyMMddHHmmssfff" $prefix = "p1m_$stamp" $results = [System.Collections.Generic.List[object]]::new() $testData = [ordered]@{ Prefix = $prefix } $adminPasswordChanged = $false $memberId = $null $memberUsername = "${prefix}_member" $tenantUsername = "${prefix}_tenant" $roleId = $null $latestAdminAccess = $null $latestAdminRefresh = $null $cleanupResidual = [ordered]@{ retainedTestMemberIds = [System.Collections.Generic.List[long]]::new() undeletedRoleIds = [System.Collections.Generic.List[long]]::new() crossTenantIds = [System.Collections.Generic.List[long]]::new() } if ([string]::IsNullOrWhiteSpace($PlatformAdminPassword)) { $PlatformAdminPassword = $MerchantAdminPassword } if ([string]::IsNullOrWhiteSpace($OutputPath)) { $OutputPath = Join-Path $RepositoryRoot "reports\phase1-business-maturity-regression-$stamp.json" } if (-not (Test-Path -LiteralPath $RepositoryRoot -PathType Container)) { throw "仓库根目录不存在:$RepositoryRoot" } function Add-Check([string]$Name, [bool]$Passed, [string]$Evidence, [string]$Category = "业务") { $script:results.Add([pscustomobject]@{ name = $Name category = $Category passed = $Passed evidence = $Evidence checkedAt = (Get-Date).ToString("o") }) Write-Host "[$(if ($Passed) { '通过' } else { '失败' })] $Name - $Evidence" } function Require-Check([string]$Name, [bool]$Passed, [string]$Evidence, [string]$Category = "业务") { Add-Check $Name $Passed $Evidence $Category if (-not $Passed) { throw "验收失败:$Name;$Evidence" } } function Invoke-Api([string]$Method, [string]$Path, [object]$Body = $null, [string]$AccessToken = $null) { $headers = @{} if (-not [string]::IsNullOrWhiteSpace($AccessToken)) { $headers.Authorization = "Bearer $AccessToken" } $arguments = @{ Method = $Method Uri = "$($GatewayBaseUrl.TrimEnd('/'))$Path" Headers = $headers TimeoutSec = 20 UseBasicParsing = $true } if ($null -ne $Body) { $arguments.ContentType = "application/json; charset=utf-8" $jsonText = $Body | ConvertTo-Json -Depth 20 -Compress $arguments.Body = [Text.Encoding]::UTF8.GetBytes($jsonText) } try { $response = Invoke-WebRequest @arguments $contentText = $response.Content if ($null -ne $response.RawContentStream) { try { $contentText = [Text.Encoding]::UTF8.GetString($response.RawContentStream.ToArray()) } catch { } } $json = if ([string]::IsNullOrWhiteSpace($contentText)) { $null } else { $contentText | ConvertFrom-Json } return [pscustomobject]@{ HttpStatus = [int]$response.StatusCode; Body = $json } } catch { $status = 0 if ($null -ne $_.Exception.Response) { try { $status = [int]$_.Exception.Response.StatusCode } catch { $status = 0 } } $json = $null if (-not [string]::IsNullOrWhiteSpace($_.ErrorDetails.Message)) { try { $json = $_.ErrorDetails.Message | ConvertFrom-Json } catch { $json = $null } } if ($null -eq $json -and $null -ne $_.Exception.Response) { try { $stream = $_.Exception.Response.GetResponseStream() $reader = New-Object System.IO.StreamReader($stream, [Text.Encoding]::UTF8) $content = $reader.ReadToEnd() $reader.Dispose() if (-not [string]::IsNullOrWhiteSpace($content)) { $json = $content | ConvertFrom-Json } } catch { $json = $null } } return [pscustomobject]@{ HttpStatus = $status; Body = $json } } } function Require-Success([string]$Name, [object]$Response, [string]$Category = "业务") { $ok = $Response.HttpStatus -eq 200 -and $null -ne $Response.Body -and $Response.Body.code -eq "0" $code = if ($null -eq $Response.Body) { "无响应码" } else { $Response.Body.code } Require-Check $Name $ok "HTTP=$($Response.HttpStatus), code=$code" $Category return $Response.Body.data } function Require-Failure([string]$Name, [object]$Response, [int]$HttpStatus, [string[]]$Codes, [string]$Category = "异常") { $code = if ($null -eq $Response.Body) { $null } else { [string]$Response.Body.code } $ok = $Response.HttpStatus -eq $HttpStatus -and ($Codes.Count -eq 0 -or $code -in $Codes) Require-Check $Name $ok "HTTP=$($Response.HttpStatus), code=$(if ($null -eq $code) { '无' } else { $code })" $Category } function Login([string]$Username, [string]$Password, [string]$Name) { $response = Invoke-Api "POST" "/api/auth/login" @{ username = $Username; password = $Password } return Require-Success $Name $response "认证" } function Restore-AdminPassword { if (-not $script:adminPasswordChanged) { return } try { $login = Login $MerchantAdminUsername $TemporaryAdminPassword "恢复前重新登录管理员" $restore = Invoke-Api "PUT" "/api/auth/password" @{ oldPassword = $TemporaryAdminPassword newPassword = $MerchantAdminPassword } $login.accessToken if ($restore.HttpStatus -eq 200 -and $restore.Body.code -eq "0") { $script:adminPasswordChanged = $false Add-Check "恢复管理员原密码" $true "恢复成功" "清理" } else { Add-Check "恢复管理员原密码" $false "HTTP=$($restore.HttpStatus)" "清理" } } catch { Add-Check "恢复管理员原密码" $false "恢复过程异常,需人工检查测试账号" "清理" } } function Invoke-BestEffortCleanup { # 清理范围必须同时满足一期脚本的成员编号和显示名特征,禁止按单一模糊条件处理正常成员。 $cleanupLogin = $null foreach ($candidatePassword in @($MerchantAdminPassword, $TemporaryAdminPassword)) { if ($null -ne $cleanupLogin) { break } $attempt = Invoke-Api "POST" "/api/auth/login" @{ username = $MerchantAdminUsername password = $candidatePassword } if ($attempt.HttpStatus -eq 200 -and $null -ne $attempt.Body -and $attempt.Body.code -eq "0") { $cleanupLogin = $attempt.Body.data } } if ($null -eq $cleanupLogin) { Add-Check "清理阶段登录商户管理员" $false "原密码和临时密码均无法登录" "清理" return } Add-Check "清理阶段登录商户管理员" $true "登录成功" "清理" $memberResponse = Invoke-Api "GET" "/api/merchant/v1/iam/members" $null $cleanupLogin.accessToken if ($memberResponse.HttpStatus -ne 200 -or $null -eq $memberResponse.Body -or $memberResponse.Body.code -ne "0") { Add-Check "扫描历史一期测试成员" $false "HTTP=$($memberResponse.HttpStatus)" "清理" return } $testMembers = @($memberResponse.Body.data | Where-Object { $_.memberNo -like "P1-*" -and $_.displayName -like "一期验收成员*" }) foreach ($testMember in $testMembers) { $id = [long]$testMember.membershipId if ($testMember.status -ne "DISABLED") { $disable = Invoke-Api "PUT" "/api/merchant/v1/iam/members/$id/status" @{ status = "DISABLED" } $cleanupLogin.accessToken if ($disable.HttpStatus -ne 200 -or $disable.Body.code -ne "0") { Add-Check "停用历史测试成员 $id" $false "HTTP=$($disable.HttpStatus)" "清理" if ($id -notin $cleanupResidual.retainedTestMemberIds) { $cleanupResidual.retainedTestMemberIds.Add($id) } continue } } $clearRoles = Invoke-Api "PUT" "/api/merchant/v1/iam/members/roles" @{ membershipId = $id roleIds = @() } $cleanupLogin.accessToken $cleared = $clearRoles.HttpStatus -eq 200 -and $null -ne $clearRoles.Body -and $clearRoles.Body.code -eq "0" Add-Check "清理历史测试成员角色 $id" $cleared "HTTP=$($clearRoles.HttpStatus)" "清理" if ($id -notin $cleanupResidual.retainedTestMemberIds) { $cleanupResidual.retainedTestMemberIds.Add($id) } } $roleResponse = Invoke-Api "GET" "/api/merchant/v1/iam/roles" $null $cleanupLogin.accessToken if ($roleResponse.HttpStatus -ne 200 -or $null -eq $roleResponse.Body -or $roleResponse.Body.code -ne "0") { Add-Check "扫描历史一期测试角色" $false "HTTP=$($roleResponse.HttpStatus)" "清理" } else { $testRoles = @($roleResponse.Body.data | Where-Object { $_.roleCode -like "P1_SELF_*" -and $_.roleType -eq "CUSTOM" }) foreach ($testRole in $testRoles) { $id = [long]$testRole.id $delete = Invoke-Api "DELETE" "/api/merchant/v1/iam/roles/$id" $null $cleanupLogin.accessToken $deleted = $delete.HttpStatus -eq 200 -and $null -ne $delete.Body -and $delete.Body.code -eq "0" Add-Check "删除历史测试角色 $id" $deleted "HTTP=$($delete.HttpStatus)" "清理" if (-not $deleted) { $cleanupResidual.undeletedRoleIds.Add($id) } } } $logout = Invoke-Api "POST" "/api/auth/logout" @{ refreshToken = $cleanupLogin.refreshToken } $cleanupLogin.accessToken Add-Check "退出清理会话" ($logout.HttpStatus -eq 200 -and $logout.Body.code -eq "0") "HTTP=$($logout.HttpStatus)" "清理" } $fatalMessage = $null try { # 登录、刷新及刷新令牌轮换。 $initialLogin = Login $MerchantAdminUsername $MerchantAdminPassword "商户管理员登录" $initialAccess = $initialLogin.accessToken $initialRefresh = $initialLogin.refreshToken $refreshResponse = Invoke-Api "POST" "/api/auth/refresh" @{ refreshToken = $initialRefresh } $rotated = Require-Success "刷新令牌并轮换会话" $refreshResponse "认证" $latestAdminAccess = $rotated.accessToken $latestAdminRefresh = $rotated.refreshToken Require-Failure "旧刷新令牌不可重复使用" (Invoke-Api "POST" "/api/auth/refresh" @{ refreshToken = $initialRefresh }) 401 @("A0401") "认证" Require-Failure "刷新后旧访问令牌失效" (Invoke-Api "GET" "/api/merchant/v1/iam/profile" $null $initialAccess) 401 @("A0401") "认证" # 个人资料查询、修改、回显和恢复。 $profile = Require-Success "查询本人个人资料" (Invoke-Api "GET" "/api/merchant/v1/iam/profile" $null $latestAdminAccess) "个人资料" $updatedName = "一期验收-$stamp" $updated = Require-Success "编辑本人个人资料" (Invoke-Api "PUT" "/api/merchant/v1/iam/profile" @{ displayName = $updatedName phone = $profile.phone email = $profile.email } $latestAdminAccess) "个人资料" Require-Check "个人资料修改后正确回显" ($updated.displayName -eq $updatedName) "displayName 已更新" "个人资料" $null = Require-Success "恢复本人个人资料" (Invoke-Api "PUT" "/api/merchant/v1/iam/profile" @{ displayName = $profile.displayName phone = $profile.phone email = $profile.email } $latestAdminAccess) "清理" # 修改密码失败和成功场景,随后恢复测试管理员密码。 Require-Failure "错误旧密码禁止修改密码" (Invoke-Api "PUT" "/api/auth/password" @{ oldPassword = "WrongOldPassword999!" newPassword = $TemporaryAdminPassword } $latestAdminAccess) 400 @("A0400") "密码" $null = Require-Success "正确旧密码修改成功" (Invoke-Api "PUT" "/api/auth/password" @{ oldPassword = $MerchantAdminPassword newPassword = $TemporaryAdminPassword } $latestAdminAccess) "密码" $adminPasswordChanged = $true Require-Failure "修改密码后旧访问令牌失效" (Invoke-Api "GET" "/api/merchant/v1/iam/profile" $null $latestAdminAccess) 401 @("A0401") "密码" Require-Failure "修改密码后旧刷新令牌失效" (Invoke-Api "POST" "/api/auth/refresh" @{ refreshToken = $latestAdminRefresh }) 401 @("A0401") "密码" Restore-AdminPassword $adminLogin = Login $MerchantAdminUsername $MerchantAdminPassword "恢复密码后重新登录管理员" $latestAdminAccess = $adminLogin.accessToken $latestAdminRefresh = $adminLogin.refreshToken # 平台身份不能访问商户端接口。 $platformLogin = Login $PlatformAdminUsername $PlatformAdminPassword "平台管理员登录" Require-Failure "平台令牌访问商户接口返回403" (Invoke-Api "GET" "/api/merchant/v1/iam/members" $null $platformLogin.accessToken) 403 @("A0403") "权限" # 创建仅用于本次验收的自助保护角色,并授予最小成员权限。 $role = Require-Success "创建验收角色" (Invoke-Api "POST" "/api/merchant/v1/iam/roles" @{ roleCode = "P1_SELF_$stamp" roleName = "一期自助保护验收角色" description = "一期自动化验收数据,禁止用于生产授权" } $latestAdminAccess) "成员" $roleId = [long]$role.id $testData.RoleId = $roleId $editedRole = Require-Success "编辑验收角色" (Invoke-Api "PUT" "/api/merchant/v1/iam/roles/$roleId" @{ roleName = "一期自助保护验收角色-已编辑" description = "一期自动化验收编辑回显" } $latestAdminAccess) "角色" Require-Check "角色编辑结果正确回显" ($editedRole.roleName -eq "一期自助保护验收角色-已编辑") "roleName 已更新" "角色" $disabledRole = Require-Success "停用自定义角色" (Invoke-Api "PUT" "/api/merchant/v1/iam/roles/$roleId/status" @{ status = "DISABLED" } $latestAdminAccess) "角色" Require-Check "角色停用状态正确回显" ($disabledRole.status -eq "DISABLED") "status=DISABLED" "角色" $enabledRole = Require-Success "重新启用自定义角色" (Invoke-Api "PUT" "/api/merchant/v1/iam/roles/$roleId/status" @{ status = "ACTIVE" } $latestAdminAccess) "角色" Require-Check "角色启用状态正确回显" ($enabledRole.status -eq "ACTIVE") "status=ACTIVE" "角色" $permissions = Require-Success "查询成员按钮权限" (Invoke-Api "GET" "/api/merchant/v1/iam/permissions" $null $latestAdminAccess) "权限" $neededCodes = @("merchant:member:list", "merchant:member:status") $permissionIds = @($permissions | Where-Object { $_.permissionCode -in $neededCodes } | ForEach-Object { [long]$_.id }) Require-Check "自助保护所需权限已初始化" ($permissionIds.Count -eq $neededCodes.Count) "权限数量=$($permissionIds.Count)" "权限" $null = Require-Success "授予验收角色最小权限" (Invoke-Api "PUT" "/api/merchant/v1/iam/roles/permissions" @{ roleId = $roleId permissionIds = $permissionIds } $latestAdminAccess) "权限" $roleDetail = Require-Success "查询角色当前权限回显" (Invoke-Api "GET" "/api/merchant/v1/iam/roles/$roleId" $null $latestAdminAccess) "权限" Require-Check "角色 permissionIds 完整回显" (@($roleDetail.permissionIds).Count -eq $permissionIds.Count -and @($permissionIds | Where-Object { $_ -notin @($roleDetail.permissionIds) }).Count -eq 0) "permissionIds 数量=$(@($roleDetail.permissionIds).Count)" "权限" # 新增成员、列表/详情角色回显及编辑。 $member = Require-Success "新增企业成员及登录账号" (Invoke-Api "POST" "/api/merchant/v1/iam/members" @{ username = $memberUsername password = $MemberInitialPassword displayName = "一期验收成员" phone = "13800000001" email = "$memberUsername@example.com" memberType = "EMPLOYEE" memberNo = "P1-$stamp" roleIds = @($roleId) } $latestAdminAccess) "成员" $memberId = [long]$member.membershipId $testData.MemberUsername = $memberUsername $testData.MembershipId = $memberId Require-Check "新增响应包含当前角色" ($roleId -in @($member.roleIds)) "roleIds 已回显" "成员" Require-Failure "已分配成员的角色禁止删除" (Invoke-Api "DELETE" "/api/merchant/v1/iam/roles/$roleId" $null $latestAdminAccess) 409 @("A0409") "角色" $members = Require-Success "查询企业成员列表" (Invoke-Api "GET" "/api/merchant/v1/iam/members" $null $latestAdminAccess) "成员" $listed = @($members | Where-Object { [long]$_.membershipId -eq $memberId } | Select-Object -First 1) Require-Check "列表返回姓名联系方式及角色" ($listed.Count -eq 1 -and $listed[0].displayName -eq "一期验收成员" -and $listed[0].phone -eq "13800000001" -and $roleId -in @($listed[0].roleIds)) "成员列表字段完整" "成员" $detail = Require-Success "查询成员详情" (Invoke-Api "GET" "/api/merchant/v1/iam/members/$memberId" $null $latestAdminAccess) "成员" Require-Check "成员详情角色正确回显" ($roleId -in @($detail.roleIds)) "roleIds 已回显" "成员" $edited = Require-Success "编辑企业成员" (Invoke-Api "PUT" "/api/merchant/v1/iam/members/$memberId" @{ displayName = "一期验收成员-已编辑" phone = "13800000002" email = "${memberUsername}_edited@example.com" memberType = "EMPLOYEE" memberNo = "P1-$stamp" roleIds = @($roleId) } $latestAdminAccess) "成员" Require-Check "成员编辑结果正确" ($edited.displayName -eq "一期验收成员-已编辑" -and $edited.phone -eq "13800000002") "姓名和手机已更新" "成员" # 重置密码、启停、本人保护和会话撤销。 $memberLogin = Login $memberUsername $MemberInitialPassword "新增成员可登录" $null = Require-Success "管理员重置成员密码" (Invoke-Api "PUT" "/api/merchant/v1/iam/members/$memberId/password" @{ newPassword = $MemberResetPassword } $latestAdminAccess) "成员" Require-Failure "重置密码后成员旧访问令牌失效" (Invoke-Api "GET" "/api/merchant/v1/iam/profile" $null $memberLogin.accessToken) 401 @("A0401") "成员" Require-Failure "成员原密码登录失败" (Invoke-Api "POST" "/api/auth/login" @{ username = $memberUsername; password = $MemberInitialPassword }) 401 @("A0401") "成员" $memberLogin = Login $memberUsername $MemberResetPassword "成员使用重置密码登录" Require-Failure "普通成员不能停用本人" (Invoke-Api "PUT" "/api/merchant/v1/iam/members/$memberId/status" @{ status = "DISABLED" } $memberLogin.accessToken) 400 @("A0400") "权限" $ownerMembershipId = [long](Require-Success "重新读取管理员个人资料" (Invoke-Api "GET" "/api/merchant/v1/iam/profile" $null $latestAdminAccess) "成员").userId # 个人资料返回的是 IAM 用户 ID,不是成员 ID;从成员列表按 authUserId 定位所有者成员。 $adminClaimsPayload = $latestAdminAccess.Split('.')[1].Replace('-', '+').Replace('_', '/') switch ($adminClaimsPayload.Length % 4) { 2 { $adminClaimsPayload += '==' } 3 { $adminClaimsPayload += '=' } } $adminClaims = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($adminClaimsPayload)) | ConvertFrom-Json $ownerMembershipId = [long]$adminClaims.membership_id Require-Failure "企业所有者不可被停用" (Invoke-Api "PUT" "/api/merchant/v1/iam/members/$ownerMembershipId/status" @{ status = "DISABLED" } $latestAdminAccess) 403 @("A0403") "权限" $null = Require-Success "停用普通成员" (Invoke-Api "PUT" "/api/merchant/v1/iam/members/$memberId/status" @{ status = "DISABLED" } $latestAdminAccess) "成员" Require-Failure "停用成员旧访问令牌失效" (Invoke-Api "GET" "/api/merchant/v1/iam/profile" $null $memberLogin.accessToken) 401 @("A0401") "成员" Require-Failure "停用成员禁止登录" (Invoke-Api "POST" "/api/auth/login" @{ username = $memberUsername; password = $MemberResetPassword }) 403 @("A0403") "成员" $null = Require-Success "重新启用普通成员" (Invoke-Api "PUT" "/api/merchant/v1/iam/members/$memberId/status" @{ status = "ACTIVE" } $latestAdminAccess) "成员" $memberLogin = Login $memberUsername $MemberResetPassword "启用后成员恢复登录" # 第二租户只能看到自己的数据,不能读取第一租户成员。 $tenantPassword = $MemberInitialPassword $registration = Require-Success "注册跨租户验收企业" (Invoke-Api "POST" "/api/auth/register" @{ username = $tenantUsername password = $tenantPassword displayName = "跨租户验收所有者" companyName = "跨租户验收企业-$stamp" legalPerson = "验收人员" legalPhone = "13800000003" contactEmail = "$tenantUsername@example.com" countryCode = "CN" address = "一期自动化验收地址" }) "租户隔离" $testData.CrossTenantUsername = $tenantUsername $testData.CrossTenantId = [long]$registration.tenantId $crossTenantLogin = Login $tenantUsername $tenantPassword "跨租户账号登录" Require-Failure "跨租户读取成员被拒绝" (Invoke-Api "GET" "/api/merchant/v1/iam/members/$memberId" $null $crossTenantLogin.accessToken) 404 @("A0404") "租户隔离" # 退出必须同时使访问和刷新会话失效。 $null = Require-Success "成员退出登录" (Invoke-Api "POST" "/api/auth/logout" @{ refreshToken = $memberLogin.refreshToken } $memberLogin.accessToken) "认证" Require-Failure "退出后访问令牌失效" (Invoke-Api "GET" "/api/merchant/v1/iam/profile" $null $memberLogin.accessToken) 401 @("A0401") "认证" Require-Failure "退出后刷新令牌失效" (Invoke-Api "POST" "/api/auth/refresh" @{ refreshToken = $memberLogin.refreshToken }) 401 @("A0401") "认证" # 无删除成员接口,最终停用并保留带前缀的可审计测试数据。 $null = Require-Success "清理时停用验收成员" (Invoke-Api "PUT" "/api/merchant/v1/iam/members/$memberId/status" @{ status = "DISABLED" } $latestAdminAccess) "清理" $null = Require-Success "清理成员角色关系" (Invoke-Api "PUT" "/api/merchant/v1/iam/members/roles" @{ membershipId = $memberId roleIds = @() } $latestAdminAccess) "清理" $null = Require-Success "删除临时验收角色" (Invoke-Api "DELETE" "/api/merchant/v1/iam/roles/$roleId" $null $latestAdminAccess) "清理" $null = Require-Success "退出跨租户验收账号" (Invoke-Api "POST" "/api/auth/logout" @{ refreshToken = $crossTenantLogin.refreshToken } $crossTenantLogin.accessToken) "清理" $null = Require-Success "退出商户管理员" (Invoke-Api "POST" "/api/auth/logout" @{ refreshToken = $latestAdminRefresh } $latestAdminAccess) "清理" } catch { $fatalMessage = $_.Exception.Message Add-Check "脚本主流程" $false $fatalMessage "执行" } finally { Restore-AdminPassword if ($testData.Contains("CrossTenantId")) { $cleanupResidual.crossTenantIds.Add([long]$testData.CrossTenantId) } Invoke-BestEffortCleanup $finishedAt = Get-Date $passed = @($results | Where-Object { $_.passed }).Count $failed = @($results | Where-Object { -not $_.passed }).Count $report = [ordered]@{ reportName = "一期业务成熟度真实 API 回归报告" startedAt = $startedAt.ToString("o") finishedAt = $finishedAt.ToString("o") gatewayBaseUrl = $GatewayBaseUrl repositoryRoot = (Resolve-Path -LiteralPath $RepositoryRoot).Path success = ($failed -eq 0 -and [string]::IsNullOrWhiteSpace($fatalMessage)) summary = [ordered]@{ total = $results.Count; passed = $passed; failed = $failed } testData = $testData residualData = $cleanupResidual results = $results fatalMessage = $fatalMessage securityNote = "报告不包含密码、访问令牌或刷新令牌" } $directory = Split-Path -Parent $OutputPath if (-not (Test-Path -LiteralPath $directory)) { New-Item -ItemType Directory -Path $directory -Force | Out-Null } $report | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $OutputPath -Encoding UTF8 Write-Host "报告已生成:$OutputPath" Write-Output ($report | ConvertTo-Json -Depth 12) if (-not $report.success) { exit 1 } }