Files
baiyue-firstleg/scripts/phase1-business-maturity-regression.ps1
T

411 lines
24 KiB
PowerShell
Raw Normal View History

2026-07-30 18:01:36 +08:00
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 }
}