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} }