暂存提交

This commit is contained in:
2026-07-30 18:01:36 +08:00
commit 2f4e159356
368 changed files with 20370 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
$env:NACOS_SERVER_ADDR = '127.0.0.1:8848'
$env:NACOS_NAMESPACE = 'public'
$env:NACOS_GROUP = 'BAIYUE'
$env:REDIS_HOST = '127.0.0.1'
$env:REDIS_PORT = '6379'
$env:SPRING_DATA_REDIS_HOST = '127.0.0.1'
$env:SPRING_DATA_REDIS_PORT = '6379'
$env:AUTH_DB_PASSWORD = '123456'
$env:IAM_DB_PASSWORD = '123456'
$env:ORDER_DB_PASSWORD = '123456'
$env:FIRSTLEG_DB_PASSWORD = '123456'
$env:WAREHOUSE_DB_PASSWORD = '123456'
$env:FILE_DB_PASSWORD = '123456'
$env:PRINT_DB_PASSWORD = '123456'
$env:IAM_SERVICE_URL = 'http://127.0.0.1:9201'
$env:JWT_SECRET = 'phase1-local-test-jwt-secret-at-least-32-bytes'
$env:INTERNAL_SERVICE_TOKEN = 'phase1-local-internal-token-at-least-32-bytes'
$env:SWAGGER_ENABLED = 'true'
$env:CORS_ALLOWED_ORIGINS = 'http://localhost:5173,http://localhost:5174,http://127.0.0.1:5173,http://127.0.0.1:5174,http://192.168.110.149:5173,http://192.168.110.149:5174'
$env:FILE_STORAGE_ROOT = 'D:/baiyue-firstleg-runtime/uploads'
Write-Host '头程本地环境已加载:Nacos 8848、Redis 6379、MySQL 密码、JWT、Swagger 和本地跨域白名单'
@@ -0,0 +1,230 @@
param(
[string]$GatewayBaseUrl = 'http://127.0.0.1:19002',
[string]$OrderBaseUrl = 'http://127.0.0.1:19204',
[string]$WarehouseBaseUrl = 'http://127.0.0.1:19205',
[string]$MerchantUsername = 'merchant_admin',
[string]$PlatformUsername = 'platform_admin',
[string]$AdminPassword = 'ChangeMe123!',
[string]$MysqlPassword = '123456',
[string]$OutputPath = ''
)
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
if ([string]::IsNullOrWhiteSpace($OutputPath)) {
$OutputPath = Join-Path $root ("reports\订单仓库往来单位真实API回归-{0}.json" -f (Get-Date -Format 'yyyyMMdd-HHmmss'))
}
$mysql = 'D:\baiyue-runtime\mysql\server\bin\mysql.exe'
$results = [System.Collections.Generic.List[object]]::new()
$startedAt = Get-Date
$testData = [ordered]@{}
$previousMysqlPassword = $env:MYSQL_PWD
$env:MYSQL_PWD = $MysqlPassword
function Add-Result([string]$Name,[bool]$Passed,[string]$Evidence) {
$results.Add([pscustomobject]@{ name=$Name; passed=$Passed; evidence=$Evidence })
Write-Host "[$(if($Passed){'通过'}else{'失败'})] $Name - $Evidence"
if (-not $Passed) { throw "$Name:$Evidence" }
}
function Invoke-Api([string]$Method,[string]$Url,[object]$Body=$null,[string]$Token=$null) {
$headers=@{}; if($Token){$headers.Authorization="Bearer $Token"}
$args=@{Method=$Method;Uri=$Url;Headers=$headers;TimeoutSec=15;UseBasicParsing=$true}
if($null -ne $Body){
$args.ContentType='application/json; charset=utf-8'
$jsonText=$Body|ConvertTo-Json -Depth 15 -Compress
$args.Body=[Text.Encoding]::UTF8.GetBytes($jsonText)
}
try {
$response=Invoke-WebRequest @args
$content=$response.Content
if($null -ne $response.RawContentStream){try{$content=[Text.Encoding]::UTF8.GetString($response.RawContentStream.ToArray())}catch{}}
$json=$null; if($content){try{$json=$content|ConvertFrom-Json}catch{}}
return [pscustomobject]@{status=[int]$response.StatusCode;json=$json;body=$content}
} catch {
$status=0; if($_.Exception.Response){$status=[int]$_.Exception.Response.StatusCode}
$content=$_.ErrorDetails.Message; $json=$null; if($content){try{$json=$content|ConvertFrom-Json}catch{}}
if($null -eq $json -and $null -ne $_.Exception.Response){
try {
$reader=New-Object IO.StreamReader($_.Exception.Response.GetResponseStream(),[Text.Encoding]::UTF8)
$content=$reader.ReadToEnd();$reader.Dispose()
if($content){$json=$content|ConvertFrom-Json}
} catch {$json=$null}
}
return [pscustomobject]@{status=$status;json=$json;body=$content}
}
}
function Require-Success([string]$Name,$Response) {
$ok=$Response.status -eq 200 -and $Response.json.code -eq '0'
Add-Result $Name $ok "HTTP=$($Response.status),code=$($Response.json.code)"
return $Response.json.data
}
function Require-Http([string]$Name,$Response,[int]$Status,[string]$Code='') {
$ok=$Response.status -eq $Status -and ([string]::IsNullOrWhiteSpace($Code) -or $Response.json.code -eq $Code)
Add-Result $Name $ok "HTTP=$($Response.status),code=$($Response.json.code)"
}
function Require-Check([string]$Name,[bool]$Condition,[string]$Evidence) { Add-Result $Name $Condition $Evidence }
function Invoke-Sql([string]$Database,[string]$Sql) {
# MySQL 会在标准错误流输出命令行密码提示,但成功退出码仍为 0;避免将提示误判为 SQL 失败。
$previousErrorAction=$ErrorActionPreference
$ErrorActionPreference='Continue'
try {
$output=& $mysql '--default-character-set=utf8mb4' '--host=127.0.0.1' '--port=3306' '--user=root' "--database=$Database" "--execute=$Sql" 2>$null
$exitCode=$LASTEXITCODE
} finally {
$ErrorActionPreference=$previousErrorAction
}
if($exitCode -ne 0){throw "SQL执行失败:$output"}
}
function Invoke-SqlScalar([string]$Database,[string]$Sql) {
$previousErrorAction=$ErrorActionPreference
$ErrorActionPreference='Continue'
try {
$output=& $mysql '--batch' '--skip-column-names' '--default-character-set=utf8mb4' '--host=127.0.0.1' '--port=3306' '--user=root' "--database=$Database" "--execute=$Sql" 2>$null
$exitCode=$LASTEXITCODE
} finally {
$ErrorActionPreference=$previousErrorAction
}
if($exitCode -ne 0){throw 'SQL标量查询失败'}
return [string](@($output)[0])
}
function Login([string]$Username) {
return Require-Success "$Username 登录" (Invoke-Api POST "$GatewayBaseUrl/api/auth/login" @{username=$Username;password=$AdminPassword})
}
$activeWarehouse=190000000000000001L
$inactiveWarehouse=190000000000000002L
$crossWarehouse=190000000000000003L
$activePartner=191000000000000001L
$inactivePartner=191000000000000002L
$crossPartner=191000000000000003L
$orderId=$null
$fatal=$null
$permissionPreexisting=$false
try {
if(-not(Test-Path -LiteralPath $mysql)){throw "找不到 MySQL 客户端:$mysql"}
foreach($url in @("$GatewayBaseUrl/actuator/health","$OrderBaseUrl/actuator/health","$WarehouseBaseUrl/actuator/health")) {
$health=Invoke-Api GET $url
Require-Check "服务健康:$url" ($health.status -eq 200) "HTTP=$($health.status)"
}
# 回归数据只使用固定测试 ID,重复执行会恢复到启用/停用基线。
$permissionPreexisting=(Invoke-SqlScalar baiyue_iam "SELECT COUNT(*) FROM iam_permission WHERE id=5102018;") -eq '1'
Invoke-Sql baiyue_iam @"
INSERT INTO iam_permission(id,tenant_id,app_code,permission_type,parent_id,permission_code,permission_name,sort_no,status,deleted)
VALUES(5102018,0,'PLATFORM','BUTTON',5102010,'platform:order:fulfillment','登记履约快递',80,'ACTIVE',0)
ON DUPLICATE KEY UPDATE permission_name=VALUES(permission_name),status='ACTIVE',deleted=0;
INSERT INTO iam_role_permission(role_id,permission_id,tenant_id) VALUES(4000001,5102018,0)
ON DUPLICATE KEY UPDATE tenant_id=VALUES(tenant_id);
"@
Invoke-Sql baiyue_warehouse @"
INSERT INTO wh_warehouse(id,tenant_id,warehouse_code,warehouse_name,warehouse_type,country_code,timezone,status,remark)
VALUES
($inactiveWarehouse,10001,'REG-INACTIVE','回归停用仓','DOMESTIC','CN','Asia/Shanghai','INACTIVE','自动化回归'),
($crossWarehouse,10002,'REG-CROSS','其他企业仓库','DOMESTIC','CN','Asia/Shanghai','ACTIVE','自动化回归')
ON DUPLICATE KEY UPDATE status=VALUES(status),warehouse_name=VALUES(warehouse_name);
INSERT INTO wh_warehouse_address(id,tenant_id,warehouse_id,address_type,country_code,state_province,city,district,postal_code,address_line1)
VALUES
(190000000000000012,10001,$inactiveWarehouse,'OPERATING','CN','广东省','深圳市','龙岗区','518000','停用仓测试地址'),
(190000000000000013,10002,$crossWarehouse,'OPERATING','CN','广东省','广州市','白云区','510000','跨租户仓测试地址')
ON DUPLICATE KEY UPDATE address_line1=VALUES(address_line1);
INSERT INTO wh_service_capability(id,tenant_id,warehouse_id,capability_code,capability_name,enabled,cutoff_time,description)
VALUES
(190000000000000022,10001,$inactiveWarehouse,'RECEIVING','头程收货',1,'15:00:00','自动化回归'),
(190000000000000023,10002,$crossWarehouse,'RECEIVING','头程收货',1,'15:30:00','自动化回归')
ON DUPLICATE KEY UPDATE enabled=1,cutoff_time=VALUES(cutoff_time);
INSERT INTO wh_partner(id,tenant_id,partner_code,partner_name,partner_type,contact_name,contact_phone,contact_email,country_code,province,city,district,address_line,postal_code,status)
VALUES
($inactivePartner,10001,'REG-INACTIVE','回归停用往来单位','CUSTOMER','停用联系人','13800000011','inactive@example.com','CN','广东省','深圳市','龙岗区','停用往来单位地址','518000','INACTIVE'),
($crossPartner,10002,'REG-CROSS','其他企业往来单位','CUSTOMER','跨租户联系人','13800000012','cross@example.com','CN','广东省','广州市','白云区','跨租户往来单位地址','510000','ACTIVE')
ON DUPLICATE KEY UPDATE status=VALUES(status),partner_name=VALUES(partner_name);
UPDATE wh_warehouse SET status='ACTIVE' WHERE id=$activeWarehouse AND tenant_id=10001;
UPDATE wh_partner SET status='ACTIVE' WHERE id=$activePartner AND tenant_id=10001;
"@
$merchant=Login $MerchantUsername; $merchantToken=$merchant.accessToken
$platform=Login $PlatformUsername; $platformToken=$platform.accessToken
$warehouses=Require-Success '商户查询启用仓库' (Invoke-Api GET "$GatewayBaseUrl/api/merchant/v1/warehouses?page=1&size=50" $null $merchantToken)
$warehouseIds=@($warehouses.records|ForEach-Object{[long]$_.id})
Require-Check '仓库列表强制启用过滤' ($activeWarehouse -in $warehouseIds -and $inactiveWarehouse -notin $warehouseIds) "返回ID=$($warehouseIds -join ',')"
$warehouseDetail=Require-Success '仓库详情回显地址和收货能力' (Invoke-Api GET "$GatewayBaseUrl/api/merchant/v1/warehouses/$activeWarehouse" $null $merchantToken)
Require-Check '仓库地址完整' (@($warehouseDetail.addresses).Count -gt 0 -and $warehouseDetail.addresses[0].addressLine1) "地址=$($warehouseDetail.addresses[0].addressLine1)"
$receiving=@($warehouseDetail.capabilities|Where-Object{$_.capabilityCode -eq 'RECEIVING' -and $_.enabled})|Select-Object -First 1
Require-Check '收货截单时间回显' ($null -ne $receiving -and $receiving.cutoffTime) "截单时间=$($receiving.cutoffTime)"
Require-Http '停用仓库详情不可见' (Invoke-Api GET "$GatewayBaseUrl/api/merchant/v1/warehouses/$inactiveWarehouse" $null $merchantToken) 404 'A0404'
Require-Http '跨租户仓库详情不可见' (Invoke-Api GET "$GatewayBaseUrl/api/merchant/v1/warehouses/$crossWarehouse" $null $merchantToken) 404 'A0404'
$partners=Require-Success '商户查询启用往来单位' (Invoke-Api GET "$GatewayBaseUrl/api/merchant/v1/partners/candidates" $null $merchantToken)
$partnerIds=@($partners|ForEach-Object{[long]$_.id})
Require-Check '往来单位强制启用过滤' ($activePartner -in $partnerIds -and $inactivePartner -notin $partnerIds) "返回ID=$($partnerIds -join ',')"
Require-Http '停用往来单位不可见' (Invoke-Api GET "$GatewayBaseUrl/api/merchant/v1/partners/$inactivePartner" $null $merchantToken) 404 'A0404'
Require-Http '跨租户往来单位不可见' (Invoke-Api GET "$GatewayBaseUrl/api/merchant/v1/partners/$crossPartner" $null $merchantToken) 404 'A0404'
$stamp=Get-Date -Format 'MMddHHmmss'
$party=@{companyName='客户端可调整公司';contactName='联系人';phone='13800138000';email='order@example.com';countryCode='CN';province='广东省';city='深圳市';district='南山区';addressLine='订单地址快照1号';postalCode='518000'}
$cargo=@{cargoName='回归货物';packageType='CARTON';packageCount=1;grossWeightKg=10;volumeCbm=1;dangerousGoods=$false}
$payload=@{customerId=$activePartner;customerName='伪造客户名';transportMode='SEA';tradeTerm='FOB';destinationCountryCode='US';destinationWarehouseName='海外目的仓';expectedShipDate=(Get-Date).AddDays(10).ToString('yyyy-MM-dd');customerRemark='真实API回归';sender=$party;receiver=$party;cargoes=@($cargo);pickupMethod='PICKUP';receivingWarehouseId=$activeWarehouse;receivingWarehouseName='伪造收货仓名';channelCode='REG';channelName='回归渠道';taxMode='BUY_DECLARATION';containerMode='LCL';customsInspection=$false;destinationType='OVERSEAS'}
$created=Require-Success '创建订单草稿' (Invoke-Api POST "$GatewayBaseUrl/api/merchant/v1/orders/first-leg" $payload $merchantToken)
$orderId=[long]$created; $testData.orderId=$orderId
$detail=Require-Success '查询新建草稿' (Invoke-Api GET "$GatewayBaseUrl/api/merchant/v1/orders/first-leg/$orderId" $null $merchantToken)
Require-Check '订单使用服务端可信名称快照' ($detail.order.customerName -eq '深圳示例贸易有限公司' -and $detail.order.receivingWarehouseName -eq '深圳头程收货仓') "客户=$($detail.order.customerName),仓库=$($detail.order.receivingWarehouseName)"
Require-Check '订单保存仓库地址和收货时间快照' ($detail.order.receivingWarehouseAddress -match '示例物流园' -and $detail.order.receivingTime -match '16:00') "地址=$($detail.order.receivingWarehouseAddress),收货=$($detail.order.receivingTime)"
$payload.sender.addressLine='订单编辑后地址2号'; $payload.customerName='再次伪造名称'; $payload.receivingWarehouseName='再次伪造仓名'
$null=Require-Success '编辑并重新保存草稿' (Invoke-Api PUT "$GatewayBaseUrl/api/merchant/v1/orders/first-leg/$orderId" $payload $merchantToken)
$detail=Require-Success '读取编辑后的草稿' (Invoke-Api GET "$GatewayBaseUrl/api/merchant/v1/orders/first-leg/$orderId" $null $merchantToken)
$sender=@($detail.parties|Where-Object{$_.partyType -eq 'SENDER'})|Select-Object -First 1
Require-Check '订单参与方地址保存为历史快照' ($sender.addressLine -eq '订单编辑后地址2号') "地址=$($sender.addressLine)"
Require-Check '编辑仍不信任客户端主数据名称' ($detail.order.customerName -eq '深圳示例贸易有限公司' -and $detail.order.receivingWarehouseName -eq '深圳头程收货仓') '可信名称未被伪造值覆盖'
Invoke-Sql baiyue_warehouse "UPDATE wh_warehouse SET status='INACTIVE' WHERE id=$activeWarehouse AND tenant_id=10001;"
Require-Http '仓库停用后禁止保存草稿' (Invoke-Api PUT "$GatewayBaseUrl/api/merchant/v1/orders/first-leg/$orderId" $payload $merchantToken) 404 'A0404'
Invoke-Sql baiyue_warehouse "UPDATE wh_warehouse SET status='ACTIVE' WHERE id=$activeWarehouse AND tenant_id=10001;"
Invoke-Sql baiyue_warehouse "UPDATE wh_partner SET status='INACTIVE' WHERE id=$activePartner AND tenant_id=10001;"
Require-Http '往来单位停用后禁止保存草稿' (Invoke-Api PUT "$GatewayBaseUrl/api/merchant/v1/orders/first-leg/$orderId" $payload $merchantToken) 404 'A0404'
Invoke-Sql baiyue_warehouse "UPDATE wh_partner SET status='ACTIVE' WHERE id=$activePartner AND tenant_id=10001;"
Invoke-Sql baiyue_iam "UPDATE iam_tenant SET status='PENDING' WHERE id=10001;"
Require-Http '企业审核未通过禁止创建订单' (Invoke-Api POST "$GatewayBaseUrl/api/merchant/v1/orders/first-leg" $payload $merchantToken) 403 'A0403'
Invoke-Sql baiyue_iam "UPDATE iam_tenant SET status='ACTIVE' WHERE id=10001;"
$null=Require-Success '提交草稿' (Invoke-Api POST "$GatewayBaseUrl/api/merchant/v1/orders/first-leg/$orderId/submit" $null $merchantToken)
Require-Http '重复提交返回409' (Invoke-Api POST "$GatewayBaseUrl/api/merchant/v1/orders/first-leg/$orderId/submit" $null $merchantToken) 409 'A0409'
$null=Require-Success '平台履约写入快递信息' (Invoke-Api PUT "$GatewayBaseUrl/api/platform/v1/orders/first-leg/$orderId/express?tenantId=10001" @{expressCompany='顺丰速运';expressTrackingNo="SF$stamp"} $platformToken)
Require-Http '商户身份不能写平台快递接口' (Invoke-Api PUT "$GatewayBaseUrl/api/platform/v1/orders/first-leg/$orderId/express?tenantId=10001" @{expressCompany='顺丰速运';expressTrackingNo="SF$stamp-X"} $merchantToken) 403 'A0403'
$detail=Require-Success '商户订单详情只读展示快递信息' (Invoke-Api GET "$GatewayBaseUrl/api/merchant/v1/orders/first-leg/$orderId" $null $merchantToken)
Require-Check '快递公司和单号真实回显' ($detail.order.expressCompany -eq '顺丰速运' -and $detail.order.expressTrackingNo -eq "SF$stamp") "快递=$($detail.order.expressCompany),单号=$($detail.order.expressTrackingNo)"
foreach($item in @(
@{name='Order merchant api-docs';url="$OrderBaseUrl/v3/api-docs/merchant"},
@{name='Warehouse merchant api-docs';url="$WarehouseBaseUrl/v3/api-docs/merchant"}
)) {
$doc=Invoke-Api GET $item.url
Require-Check $item.name ($doc.status -eq 200 -and $doc.body -match '"openapi"') "HTTP=$($doc.status)"
}
$gatewayConfig=Get-Content -LiteralPath (Join-Path $root 'gateway\src\main\resources\application-cn.yml') -Raw -Encoding UTF8
Require-Check 'Gateway Order api-docs 路由源配置' ($gatewayConfig -match 'order-service/v3/api-docs') '已静态审查源配置'
Require-Check 'Gateway Warehouse api-docs 路由源配置' ($gatewayConfig -match 'warehouse-service/v3/api-docs') '已静态审查源配置'
} catch {
$fatal=$_.Exception.Message
$results.Add([pscustomobject]@{name='脚本主流程';passed=$false;evidence=$fatal})
} finally {
try { Invoke-Sql baiyue_iam "UPDATE iam_tenant SET status='ACTIVE' WHERE id=10001;" } catch {}
try {
if($orderId) {
Invoke-Sql baiyue_order "DELETE FROM ord_order_internal_note WHERE tenant_id=10001 AND order_id=$orderId; DELETE FROM ord_order_status_history WHERE tenant_id=10001 AND order_id=$orderId; DELETE FROM ord_order_cargo WHERE tenant_id=10001 AND order_id=$orderId; DELETE FROM ord_order_party WHERE tenant_id=10001 AND order_id=$orderId; DELETE FROM ord_outbox_event WHERE tenant_id=10001 AND aggregate_id=$orderId; DELETE FROM ord_order WHERE tenant_id=10001 AND id=$orderId;"
}
} catch {}
try { Invoke-Sql baiyue_warehouse "UPDATE wh_warehouse SET status='ACTIVE' WHERE id=$activeWarehouse AND tenant_id=10001; UPDATE wh_partner SET status='ACTIVE' WHERE id=$activePartner AND tenant_id=10001; DELETE FROM wh_service_capability WHERE id IN (190000000000000022,190000000000000023); DELETE FROM wh_warehouse_address WHERE id IN (190000000000000012,190000000000000013); DELETE FROM wh_partner WHERE id IN ($inactivePartner,$crossPartner); DELETE FROM wh_warehouse WHERE id IN ($inactiveWarehouse,$crossWarehouse);" } catch {}
try { if(-not $permissionPreexisting){Invoke-Sql baiyue_iam "DELETE FROM iam_role_permission WHERE permission_id=5102018; DELETE FROM iam_permission WHERE id=5102018;"} } catch {}
$passed=@($results|Where-Object{$_.passed}).Count; $failed=@($results|Where-Object{-not $_.passed}).Count
$report=[ordered]@{reportName='订单仓库往来单位真实API回归报告';startedAt=$startedAt.ToString('o');finishedAt=(Get-Date).ToString('o');stack=[ordered]@{gateway=$GatewayBaseUrl;order=$OrderBaseUrl;warehouse=$WarehouseBaseUrl};success=($failed -eq 0);summary=[ordered]@{total=$results.Count;passed=$passed;failed=$failed};testData=$testData;results=$results;fatalMessage=$fatal;securityNote='报告不保存密码或令牌'}
$dir=Split-Path -Parent $OutputPath; if(-not(Test-Path $dir)){New-Item -ItemType Directory -Path $dir -Force|Out-Null}
$report|ConvertTo-Json -Depth 12|Set-Content -LiteralPath $OutputPath -Encoding UTF8
if ($null -eq $previousMysqlPassword) { Remove-Item Env:MYSQL_PWD -ErrorAction SilentlyContinue } else { $env:MYSQL_PWD=$previousMysqlPassword }
Write-Host "报告已生成:$OutputPath"
if(-not $report.success){exit 1}
}
+100
View File
@@ -0,0 +1,100 @@
param(
[string]$MysqlPassword = "123456",
[string]$AdminPassword = "ChangeMe123!",
[string]$GatewayBaseUrl = "http://127.0.0.1:9000",
[string]$IamBaseUrl = ""
)
$ErrorActionPreference = "Stop"
$results = [System.Collections.Generic.List[object]]::new()
if ([string]::IsNullOrWhiteSpace($IamBaseUrl)) { $IamBaseUrl = $GatewayBaseUrl }
function Add-Result([string]$Name, [bool]$Passed, [string]$Evidence) {
$results.Add([pscustomobject]@{ Test = $Name; Passed = $Passed; Evidence = $Evidence })
Write-Host "[$(if ($Passed) { 'PASS' } else { 'FAIL' })] $Name - $Evidence"
if (-not $Passed) { throw "FAILED: $Name - $Evidence" }
}
function Invoke-Json([string]$Method, [string]$Uri, [hashtable]$Headers = @{}, [object]$Body = $null) {
$args = @{ Method = $Method; Uri = $Uri; Headers = $Headers; TimeoutSec = 10 }
if ($null -ne $Body) {
$args.ContentType = "application/json"
$args.Body = $Body | ConvertTo-Json -Depth 10
}
Invoke-RestMethod @args
}
function Decode-JwtPayload([string]$Token) {
$part = $Token.Split('.')[1].Replace('-', '+').Replace('_', '/')
switch ($part.Length % 4) { 2 { $part += '==' } 3 { $part += '=' } }
[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($part)) | ConvertFrom-Json
}
function Assert-ApiSuccess([string]$Name, [string]$Uri, [hashtable]$Headers) {
$response = Invoke-Json GET $Uri $Headers
Add-Result $Name ($response.code -eq '0') "code=$($response.code)"
}
$mysql = "D:\baiyue-runtime\mysql\server\bin\mysql.exe"
$redisCli = "D:\baiyue-runtime\redis\redis-cli.exe"
$verifySql = "D:/baiyue/iam/src/test/resources/db/verify-phase1-admin-rbac.sql"
$env:MYSQL_PWD = $MysqlPassword
$dbFailures = & $mysql '--host=127.0.0.1' '--port=3306' '--user=root' '--batch' '--raw' '--skip-column-names' "--execute=source $verifySql"
Add-Result "database seed assertions" ([string]::IsNullOrWhiteSpace(($dbFailures -join ''))) "no assertion rows"
$pong = & $redisCli -h 127.0.0.1 -p 6379 PING
Add-Result "redis connectivity" ($pong -eq 'PONG') "PING=$pong"
$nacosReady = (Invoke-WebRequest -UseBasicParsing -Uri 'http://127.0.0.1:8848/nacos/v1/console/health/readiness' -TimeoutSec 5).Content
Add-Result "nacos readiness" ($nacosReady -eq 'OK') "readiness=$nacosReady"
$nacosServices = Invoke-Json GET 'http://127.0.0.1:8848/nacos/v1/ns/service/list?pageNo=1&pageSize=50&groupName=BAIYUE&namespaceId=public'
$expectedServices = @('gateway', 'auth-service', 'iam-service', 'order-service', 'warehouse-service', 'firstleg-service', 'lastleg-service', 'audit-service')
$missingServices = @($expectedServices | Where-Object { $_ -notin $nacosServices.doms })
Add-Result "phase-one service registration" ($missingServices.Count -eq 0) "registered=$($nacosServices.count), missing=$($missingServices -join ',')"
$platformLogin = Invoke-Json POST "$GatewayBaseUrl/api/auth/login" @{} @{ username = 'platform_admin'; password = $AdminPassword }
$merchantLogin = Invoke-Json POST "$GatewayBaseUrl/api/auth/login" @{} @{ username = 'merchant_admin'; password = $AdminPassword }
Add-Result "platform administrator login" ($platformLogin.code -eq '0') "login success"
Add-Result "merchant administrator login" ($merchantLogin.code -eq '0') "login success"
$platformClaims = Decode-JwtPayload $platformLogin.data.accessToken
$merchantClaims = Decode-JwtPayload $merchantLogin.data.accessToken
Add-Result "platform JWT identity" (
$platformClaims.app_code -eq 'PLATFORM' -and $platformClaims.tenant_id -eq 0 -and
$platformClaims.membership_id -eq 3000001 -and 'PLATFORM_ADMIN' -in $platformClaims.roles -and
@($platformClaims.permissions).Count -eq 31
) "tenant=$($platformClaims.tenant_id), roles=$($platformClaims.roles -join ','), permissions=$(@($platformClaims.permissions).Count)"
Add-Result "merchant JWT identity" (
$merchantClaims.app_code -eq 'MERCHANT' -and $merchantClaims.tenant_id -eq 10001 -and
$merchantClaims.membership_id -eq 3000002 -and 'MERCHANT_OWNER' -in $merchantClaims.roles -and
@($merchantClaims.permissions).Count -eq 21
) "tenant=$($merchantClaims.tenant_id), roles=$($merchantClaims.roles -join ','), permissions=$(@($merchantClaims.permissions).Count)"
$platformHeaders = @{ Authorization = "Bearer $($platformLogin.data.accessToken)" }
$merchantHeaders = @{ Authorization = "Bearer $($merchantLogin.data.accessToken)" }
Assert-ApiSuccess "platform role list" "$IamBaseUrl/api/platform/v1/iam/roles" $platformHeaders
Assert-ApiSuccess "platform menu/button list" "$IamBaseUrl/api/platform/v1/iam/permissions" $platformHeaders
Assert-ApiSuccess "platform enterprise review list" "$IamBaseUrl/api/platform/v1/iam/reviews" $platformHeaders
Assert-ApiSuccess "platform first-leg order list" "$GatewayBaseUrl/api/platform/v1/orders/first-leg?tenantId=10001&page=1&size=10" $platformHeaders
Assert-ApiSuccess "platform warehouse list" "$GatewayBaseUrl/api/platform/v1/warehouse-admin?tenantId=10001&page=1&size=10" $platformHeaders
Assert-ApiSuccess "merchant enterprise profile" "$IamBaseUrl/api/merchant/v1/iam/tenant/profile" $merchantHeaders
Assert-ApiSuccess "merchant role list" "$IamBaseUrl/api/merchant/v1/iam/roles" $merchantHeaders
Assert-ApiSuccess "merchant menu/button list" "$IamBaseUrl/api/merchant/v1/iam/permissions" $merchantHeaders
Assert-ApiSuccess "merchant warehouse list" "$GatewayBaseUrl/api/merchant/v1/warehouses?page=1&size=10" $merchantHeaders
Assert-ApiSuccess "merchant first-leg order list" "$GatewayBaseUrl/api/merchant/v1/orders/first-leg?page=1&size=10" $merchantHeaders
$crossScopeStatus = 0
try {
Invoke-WebRequest -UseBasicParsing -Uri "$IamBaseUrl/api/merchant/v1/iam/tenant/profile" -Headers $platformHeaders -TimeoutSec 5 | Out-Null
$crossScopeStatus = 200
} catch {
$crossScopeStatus = [int]$_.Exception.Response.StatusCode
}
Add-Result "platform token denied from merchant APIs" ($crossScopeStatus -eq 403) "HTTP $crossScopeStatus"
$results | Format-Table -AutoSize
"PHASE1_ACCEPTANCE_PASSED=$($results.Count)"
@@ -0,0 +1,410 @@
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 }
}
+77
View File
@@ -0,0 +1,77 @@
param(
[string]$AuthUrl = 'http://127.0.0.1:9502',
[string]$IamUrl = 'http://127.0.0.1:9501',
[string]$FileUrl = 'http://127.0.0.1:9506',
[string]$OrderUrl = 'http://127.0.0.1:9504',
[string]$ImagePath,
[string]$PlatformPassword = 'ChangeMe123!'
)
$ErrorActionPreference = 'Stop'
function Invoke-Json($method, $url, $body = $null, $token = $null) {
$headers = @{}; if ($token) { $headers.Authorization = "Bearer $token" }
$args = @{ Method=$method; Uri=$url; Headers=$headers; TimeoutSec=20 }
if ($null -ne $body) { $args.ContentType='application/json'; $args.Body=$body | ConvertTo-Json -Depth 12 }
Invoke-RestMethod @args
}
function Invoke-Status($method, $url, $body, $token) {
try { Invoke-Json $method $url $body $token | Out-Null; return 200 }
catch { return [int]$_.Exception.Response.StatusCode }
}
function Assert-Equal($name, $actual, $expected) {
if ($actual -ne $expected) { throw "$name expected=$expected actual=$actual" }
}
$stamp = Get-Date -Format 'MMddHHmmss'
$username = "phase1_$stamp"; $password = 'Phase1Test123!'
$registration = Invoke-Json POST "$AuthUrl/api/auth/register" @{
username=$username; password=$password; displayName='Phase One Customer'; companyName="Phase One Enterprise $stamp"
legalPerson='Test Legal Person'; legalPhone='13800138000'; contactEmail="phase1_$stamp@example.com"
countryCode='CN'; address='No. 1 Test Road, Nanshan, Shenzhen'
}
Assert-Equal 'registration status' $registration.data.tenantStatus 'PROFILE_REQUIRED'
$tenantId = [long]$registration.data.tenantId
$merchantLogin = Invoke-Json POST "$AuthUrl/api/auth/login" @{username=$username;password=$password}
$merchantToken = $merchantLogin.data.accessToken
$order = @{
customerId=$tenantId; customerName="Phase One Enterprise $stamp"; transportMode='SEA'; tradeTerm='FOB'
destinationCountryCode='US'; destinationWarehouseId=1001; destinationWarehouseName='ONT8'
expectedShipDate='2026-08-15'; customerRemark='Phase one E2E'; pickupMethod='PICKUP'
receivingWarehouseId=2001; receivingWarehouseName='Shenzhen Receiving Warehouse'; channelCode='SEA-US-FBA'
channelName='US Sea Channel'; taxMode='BUY_DECLARATION'; containerMode='LCL'; customsInspection=$false
destinationType='FBA'; amazonWarehouseCode='ONT8'; shipmentId="FBA$stamp"; referenceId="REF$stamp"
sender=@{companyName="Phase One Enterprise $stamp";contactName='Test Sender';phone='13800138000';email="p$stamp@example.com";countryCode='CN';province='Guangdong';city='Shenzhen';district='Nanshan';addressLine='No. 1 Test Road';postalCode='518000'}
receiver=@{companyName='Amazon';contactName='ONT8';phone='1234567890';email='ont8@example.com';countryCode='US';province='CA';city='Moreno Valley';district='Riverside';addressLine='24300 Nandina Ave';postalCode='92551'}
cargoes=@(@{cargoName='Bluetooth Speaker';hsCode='8518210000';packageType='CARTON';packageCount=2;grossWeightKg=20.5;volumeCbm=0.2;lengthCm=50;widthCm=40;heightCm=30;declaredValue=200;currency='USD';dangerousGoods=$false;cargoRemark='Phase one test';skuNo="SKU$stamp";upcNo='123456789012';cargoNameEn='Bluetooth Speaker';customsHsCode='8518210000';unitsPerPackage=10;totalQuantity=20;netWeightKg=18;unitDeclaredValue=10;material='Plastic';usage='Audio';model='P1';brand='Demo'})
}
$preApprovalStatus = Invoke-Status POST "$OrderUrl/api/merchant/v1/orders/first-leg" $order $merchantToken
Assert-Equal 'pre-approval order status' $preApprovalStatus 403
$uploadJson = & curl.exe -sS -X POST "$FileUrl/api/merchant/v1/files/qualifications" `
-H "Authorization: Bearer $merchantToken" -F "file=@$ImagePath;type=image/png" `
-F "certificateNo=91310101$stamp"
if ($LASTEXITCODE -ne 0) { throw 'qualification upload transport failed' }
$upload = $uploadJson | ConvertFrom-Json
Assert-Equal 'upload API code' $upload.code '0'
Assert-Equal 'upload tenant status' $upload.data.tenantStatus 'PENDING'
$platformLogin = Invoke-Json POST "$AuthUrl/api/auth/login" @{username='platform_admin';password=$PlatformPassword}
$platformToken = $platformLogin.data.accessToken
$reviews = Invoke-Json GET "$IamUrl/api/platform/v1/iam/reviews" $null $platformToken
$review = @($reviews.data) | Where-Object { [long]$_.tenantId -eq $tenantId } | Select-Object -First 1
if ($null -eq $review) { throw 'pending review not found' }
$qualifications = Invoke-Json GET "$IamUrl/api/platform/v1/iam/tenants/$tenantId/qualifications" $null $platformToken
Assert-Equal 'qualification count' @($qualifications.data).Count 1
$fileName = Split-Path $upload.data.documentUrl -Leaf
$preview = Invoke-WebRequest -UseBasicParsing -Uri "$FileUrl/api/platform/v1/files/qualifications/$tenantId/$fileName" `
-Headers @{Authorization="Bearer $platformToken"} -TimeoutSec 20
Assert-Equal 'platform preview status' $preview.StatusCode 200
Invoke-Json POST "$IamUrl/api/platform/v1/iam/reviews/$($review.id)/approve" @{comment='Phase one E2E approved'} $platformToken | Out-Null
$created = Invoke-Json POST "$OrderUrl/api/merchant/v1/orders/first-leg" $order $merchantToken
$orderId = [long]$created.data
Invoke-Json POST "$OrderUrl/api/merchant/v1/orders/first-leg/$orderId/submit" $null $merchantToken | Out-Null
$detail = Invoke-Json GET "$OrderUrl/api/merchant/v1/orders/first-leg/$orderId" $null $merchantToken
Assert-Equal 'submitted order status' $detail.data.order.status 'SUBMITTED'
[pscustomobject]@{username=$username;tenantId=$tenantId;registerStatus='PROFILE_REQUIRED';preApprovalOrderHttp=$preApprovalStatus;uploadStatus='PENDING';reviewId=$review.id;previewHttp=200;orderId=$orderId;finalOrderStatus='SUBMITTED'} | ConvertTo-Json
+45
View File
@@ -0,0 +1,45 @@
param(
[string]$ServerAddr = '127.0.0.1:8848',
[string]$Group = 'BAIYUE',
[string]$Namespace = 'public'
)
$ErrorActionPreference = 'Stop'
$projectRoot = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
$configRoot = Join-Path $projectRoot 'config\nacos\local'
$healthUrl = "http://$ServerAddr/nacos/v1/console/health/readiness"
$publishUrl = "http://$ServerAddr/nacos/v1/cs/configs"
$health = Invoke-WebRequest -UseBasicParsing -Uri $healthUrl -TimeoutSec 5
if ($health.StatusCode -ne 200 -or $health.Content.Trim() -ne 'OK') {
throw "Local Nacos is not ready: $healthUrl"
}
$files = Get-ChildItem -LiteralPath $configRoot -File -Filter '*.yml' | Sort-Object Name
if ($files.Count -eq 0) {
throw "No local Nacos configuration found: $configRoot"
}
$published = 0
foreach ($file in $files) {
$body = @{
dataId = $file.Name
group = $Group
content = Get-Content -LiteralPath $file.FullName -Raw -Encoding UTF8
type = 'yaml'
}
if ($Namespace -and $Namespace -ne 'public') {
$body.tenant = $Namespace
}
$result = Invoke-RestMethod -Method Post -Uri $publishUrl `
-ContentType 'application/x-www-form-urlencoded; charset=UTF-8' `
-Body $body -TimeoutSec 10
if ([string]$result -ne 'true') {
throw "Publish failed: $($file.Name), Nacos response: $result"
}
$published++
Write-Host "Published $($file.Name) -> $Namespace / $Group"
}
Write-Host "Local first-leg Nacos configuration published: $published items."