优化
This commit is contained in:
+1
-1
@@ -43,7 +43,7 @@ class AdminPrototypeMenuPermissionSqlContractTest {
|
||||
.contains("'platform:overseas-storage-fee:create','platform:overseas-storage-fee:delete'");
|
||||
|
||||
String unified = Files.readString(resolveUnifiedSql(), StandardCharsets.UTF_8);
|
||||
assertThat(unified).contains("<> 2")
|
||||
assertThat(unified).contains("<> 4")
|
||||
.contains("platform:overseas-storage-fee:list', 'platform:overseas-storage-fee:create'");
|
||||
}
|
||||
|
||||
|
||||
+4
-3
@@ -24,14 +24,15 @@
|
||||
脚本固定运行:
|
||||
|
||||
```text
|
||||
mvn -f <仓库根>/pom.xml -pl order,firstleg -am test
|
||||
mvn -f <仓库根>/pom.xml -pl order,firstleg,iam,gateway,file,warehouse -am test
|
||||
```
|
||||
|
||||
这会先构建公共依赖,再运行 `order` 与 `firstleg` 的全部 Maven 测试。覆盖包括:
|
||||
这会先构建公共依赖,再运行 `order`、`firstleg`、`iam`、`gateway`、`file` 与 `warehouse` 的全部 Maven 测试。覆盖包括:
|
||||
|
||||
- 三期 Ocean Booking Controller/Service/Mapper/SQL 契约;
|
||||
- 三期 Truck Appointment Controller/Service/Mapper 契约、状态机、乐观锁和幂等操作;
|
||||
- 二期 `WAIT_SPLIT` 的 firstleg 查询、order 统一查询及筛选语义;
|
||||
- Warehouse 海外仓阶梯费用 SQL 与 Controller 权限合同;
|
||||
- 拣货/预装/装柜模型映射与既有 Smart Loading/Split 契约。
|
||||
|
||||
## Dry-run
|
||||
@@ -61,7 +62,7 @@ Dry-run 只检查根 `pom.xml` 和关键回归测试文件并打印 Maven 命令
|
||||
|
||||
### 本地 MySQL 全量验收
|
||||
|
||||
正式入口是 `local-db-full-acceptance.ps1`,只允许连接本机 `127.0.0.1` 或 `localhost`,默认端口 `3306`;测试库 `8.163.30.128` 会立即以退出码 2 拒绝。脚本固定校验统一 SQL SHA-256 `7F61C3BAB0D4D49525A30C1E5030DEA4B798F38F6A0287901E6D1D676967DFFA` 和 13712 行,并静态确认中文注释升级采用校验入队、每表一次 ALTER、结构签名复核、九库覆盖断言及唯一最终 `PASS`。
|
||||
正式入口是 `local-db-full-acceptance.ps1`,只允许连接本机 `127.0.0.1` 或 `localhost`,默认端口 `3306`;测试库 `8.163.30.128` 会立即以退出码 2 拒绝。脚本固定校验统一 SQL SHA-256 `BE7C5C98712D5202F5D187EA9A37C64EAB9F6AA9AE772C89A51F186507B1AE53` 和 14706 行,并静态确认中文注释升级采用校验入队、每表一次 ALTER、结构签名复核、九库覆盖断言及唯一最终 `PASS`。
|
||||
|
||||
```powershell
|
||||
.\local-db-full-acceptance.ps1 -DryRun
|
||||
|
||||
@@ -12,7 +12,7 @@ import process from 'node:process'
|
||||
const DEFAULT_GATEWAY = 'http://8.163.30.128:9000'
|
||||
const DEFAULT_USERNAME = 'platform_admin'
|
||||
const DEFAULT_PASSWORD = 'ChangeMe123!'
|
||||
const DEFAULT_MODULES = ['customers', 'overseas-storage-fees', 'system', 'master-data', 'suppliers', 'domestic-container', 'orders', 'fee-items', 'pricing', 'downloads', 'inbound-weight-ocr', 'inbound-measurement', 'phase3-container-loading', 'phase3-loading', 'phase3-loading-model-bridge', 'phase3-pda-convergence', 'phase3-evidence-gate', 'supplemental', 'supplemental-boundaries', 'ocean-bookings', 'truck-appointments', 'service-center-flow', 'phase3-customs', 'phase3-port-schedules', 'phase3-carrier-schedule-queries', 'phase3-loading-finalize', 'phase3-outbound-dispatch', 'phase3-outbound-aggregate', 'phase3-dispatch-gate', 'phase3-waiting-for-loading', 'phase3-warehouse-rbac', 'phase3-order-splitting', 'manual-state-matrix', 'finance', 'finance-full-fixtures', 'finance-reconciliation-full', 'finance-settlement-full', 'customer-operating-statistics']
|
||||
const DEFAULT_MODULES = ['customers', 'overseas-storage-fees', 'system', 'master-data', 'suppliers', 'domestic-container', 'orders', 'fee-items', 'pricing', 'downloads', 'inbound-weight-ocr', 'inbound-measurement', 'phase3-container-loading', 'phase3-container-overview', 'phase3-loading', 'phase3-loading-model-bridge', 'phase3-pda-convergence', 'phase3-evidence-gate', 'supplemental', 'supplemental-boundaries', 'ocean-bookings', 'truck-appointments', 'service-center-flow', 'phase3-customs', 'phase3-port-schedules', 'phase3-carrier-schedule-queries', 'phase3-loading-finalize', 'phase3-outbound-dispatch', 'phase3-outbound-aggregate', 'phase3-dispatch-gate', 'phase3-waiting-for-loading', 'phase3-warehouse-rbac', 'phase3-order-splitting', 'manual-state-matrix', 'finance', 'finance-full-fixtures', 'finance-reconciliation-full', 'finance-settlement-full', 'customer-operating-statistics']
|
||||
const WEIGHT_IMAGE_PATH = 'C:/Users/BY-OY/Desktop/上传图片包/测量图片/测量图片/orig_1cf27695-b2c3-44b3-98e3-c0be8b80f79a.JPG'
|
||||
const CUSTOMER_FIELDS = [
|
||||
'id', 'tenantCode', 'tenantName', 'status', 'legalName', 'legalRepresentative',
|
||||
|
||||
@@ -8,11 +8,12 @@ import { run } from './finance-reconciliation-full.mjs'
|
||||
const here = path.dirname(fileURLToPath(import.meta.url))
|
||||
const source = fs.readFileSync(path.join(here, 'finance-reconciliation-full.mjs'), 'utf8')
|
||||
const financePermissions = [
|
||||
'platform:finance:customer-account:list', 'platform:finance:customer-statement:list',
|
||||
'platform:finance:reconciliation:create', 'platform:finance:reconciliation:review',
|
||||
'platform:finance:reconciliation:issue', 'platform:finance:reconciliation:adjust',
|
||||
'platform:finance:reconciliation:void', 'platform:finance:settlement-ar:create',
|
||||
'platform:finance:settlement-ar:view', 'platform:finance:settlement-ap:create',
|
||||
'platform:finance:settlement-ap:view',
|
||||
'platform:finance:settlement-ar:list', 'platform:finance:settlement-ap:create',
|
||||
'platform:finance:settlement-ap:list',
|
||||
]
|
||||
|
||||
function ok(data) {
|
||||
@@ -76,6 +77,12 @@ function fakeContext(withFixture = true, permissions = financePermissions, denyF
|
||||
if (method === 'GET' && requestPath === '/api/platform/v1/iam/authorization/permissions') {
|
||||
return ok(permissions)
|
||||
}
|
||||
if (method === 'GET' && requestPath === '/api/platform/finance/v1/accounts?page=1&pageSize=20') {
|
||||
return ok({ records: [{ customerId: '11', accountName: 'Fixture Account' }] })
|
||||
}
|
||||
if (method === 'GET' && requestPath.startsWith('/api/platform/finance/v1/accounts/11/ledger?')) {
|
||||
return ok({ records: [{ customerId: '11', statementId: '1', remark: marker }] })
|
||||
}
|
||||
if (method === 'GET' && requestPath.startsWith('/api/platform/v1/iam/tenants?')) {
|
||||
return ok({ records: [{ id: '11', status: 'ACTIVE' }, { id: '12', status: 'ACTIVE' }] })
|
||||
}
|
||||
@@ -100,7 +107,8 @@ function fakeContext(withFixture = true, permissions = financePermissions, denyF
|
||||
: feeSources.filter((item, index) => body.feeSourceIds.includes(String(1001 + index)))
|
||||
if (selected.some((item) => item.direction !== 'RECEIVABLE')) return { http: 200, body: { code: 'FIN_409' } }
|
||||
const statementId = String(++statementSequence)
|
||||
const statement = { statementId, status: 'DRAFT', version: 0, revisions: [],
|
||||
const statement = { statementId, customerId: '11', status: 'DRAFT', version: 0, revisions: [],
|
||||
createdBy: '7001', createdAt: '2026-08-10T09:00:00', updatedBy: '7001', updatedAt: '2026-08-10T09:00:00',
|
||||
lines: selected.map((item, index) => ({ statementLineId: `${statementId}${index + 1}`,
|
||||
feeSourceId: String(1001 + feeSources.indexOf(item)), statementAmount: 100 + index, adjustmentReason: null })) }
|
||||
statements.set(statementId, statement)
|
||||
@@ -137,6 +145,14 @@ function fakeContext(withFixture = true, permissions = financePermissions, denyF
|
||||
if (method === 'GET' && requestPath.startsWith('/api/platform/finance/v1/payable-settlements?')) {
|
||||
return ok([...settlements.values()].filter((item) => item.direction === 'PAYABLE'))
|
||||
}
|
||||
if (method === 'GET' && requestPath === '/api/platform/finance/v1/statements?customerId=11') {
|
||||
return ok([...statements.values()].map((item) => ({
|
||||
statementId: item.statementId, customerId: item.customerId, status: item.status,
|
||||
createdBy: item.createdBy, createdAt: item.createdAt, updatedBy: item.updatedBy, updatedAt: item.updatedAt,
|
||||
})))
|
||||
}
|
||||
const detailMatch = requestPath.match(/\/reconciliation-statements\/(\d+)$/)
|
||||
if (method === 'GET' && detailMatch) return ok(responseForStatement(statements.get(detailMatch[1])))
|
||||
throw new Error(`Unexpected request: ${method} ${requestPath}`)
|
||||
},
|
||||
addStep(module, name, response, passed, evidence = '', options = {}) {
|
||||
@@ -170,6 +186,16 @@ test('静态契约覆盖日期、SELECTED、三 AR、AP、状态机和数组输
|
||||
assert.match(source, /const groups = \[cutoffSources/)
|
||||
assert.match(source, /\/payable-settlements/)
|
||||
assert.match(source, /findSettlementInOfficialList/)
|
||||
assert.match(source, /verifyIssuedStatementReadback/)
|
||||
assert.match(source, /\/statements\?customerId=/)
|
||||
assert.match(source, /\/reconciliation-statements\/\$\{encodeURIComponent\(targetId\)\}/)
|
||||
assert.match(source, /statementRows\.every/)
|
||||
assert.match(source, /Array\.isArray\(detail\?\.lines\)/)
|
||||
assert.match(source, /Array\.isArray\(detail\?\.revisions\)/)
|
||||
assert.match(source, /\['createdBy', 'createdAt', 'updatedBy', 'updatedAt'\]/)
|
||||
assert.match(source, /STATEMENT_LIST_PERMISSION = 'platform:finance:customer-statement:list'/)
|
||||
assert.match(source, /LEGACY_STATEMENT_VIEW_PERMISSION = 'platform:finance:bill:view'/)
|
||||
assert.match(source, /legacy-customer-statement-view-permission/)
|
||||
assert.match(source, /`正式\$\{label\}列表包含同一 settlementId`/)
|
||||
assert.match(source, /findSettlementInOfficialList\(context, 'RECEIVABLE'/)
|
||||
assert.match(source, /findSettlementInOfficialList\(context, 'PAYABLE'/)
|
||||
@@ -181,6 +207,15 @@ test('静态契约覆盖日期、SELECTED、三 AR、AP、状态机和数组输
|
||||
assert.match(source, /http: response\?\.http \?\? null/)
|
||||
assert.match(source, /requestId: response\?\.requestId \?\? null/)
|
||||
assert.match(source, /traceId: response\?\.traceId \?\? null/)
|
||||
assert.match(source, /platform:finance:customer-account:list/)
|
||||
assert.match(source, /platform:finance:customer-statement:list/)
|
||||
assert.match(source, /platform:finance:settlement-ar:list/)
|
||||
assert.match(source, /platform:finance:settlement-ap:list/)
|
||||
assert.doesNotMatch(source, /platform:finance:settlement-(ar|ap):view/)
|
||||
assert.match(source, /\$\{BASE\}\/accounts\?page=1&pageSize=20/)
|
||||
assert.match(source, /\/accounts\/\$\{encodeURIComponent\(customerId\)\}\/ledger\?page=1&pageSize=200/)
|
||||
assert.match(source, /balance.*recharge.*prepayment/)
|
||||
assert.match(source, /accountReadback/)
|
||||
for (const action of ['SUBMIT', 'APPROVE', 'REJECT', 'ISSUE', 'ADJUST', 'VOID', 'REOPEN']) {
|
||||
assert.match(source, new RegExp(`['"]${action}['"]`))
|
||||
}
|
||||
@@ -210,11 +245,24 @@ test('run 发布一条日期链、三张独立已出账单、三张 AR 和一张
|
||||
&& call.path.includes('/receivable-settlements?customerId=11')).length, 3)
|
||||
assert.equal(context.calls.filter((call) => call.method === 'GET'
|
||||
&& call.path.includes('/payable-settlements?customerId=11')).length, 1)
|
||||
assert.equal(context.calls.filter((call) => call.method === 'GET'
|
||||
&& call.path === '/api/platform/finance/v1/statements?customerId=11').length, 3)
|
||||
assert.equal(context.calls.filter((call) => call.method === 'GET'
|
||||
&& /\/reconciliation-statements\/\d+$/.test(call.path)).length, 3)
|
||||
assert.equal(context.calls.some((call) => call.body?.generationMode === 'BY_CUTOFF'), false)
|
||||
const failedSteps = context.steps.filter((step) => !step.passed)
|
||||
assert.equal(failedSteps.length, 0, JSON.stringify(failedSteps, null, 2))
|
||||
})
|
||||
|
||||
test('旧客户对账单 view 权限只能形成兼容 blocker,不能作为列表/详情验收通过依据', async () => {
|
||||
const context = fakeContext(true, [...financePermissions, 'platform:finance:bill:view'])
|
||||
await run(context)
|
||||
assert.equal(context.calls.filter((call) => call.method !== 'GET').length, 0)
|
||||
assert.equal(context.steps.some((step) => !step.passed && step.deploymentBlocker
|
||||
&& step.evidence.includes('platform:finance:bill:view')), true)
|
||||
assert.equal(context.coverageGaps.some((gap) => gap.category === 'legacy-customer-statement-view-permission'), true)
|
||||
})
|
||||
|
||||
test('run 在 fixture 缺失时记录 blocker 且不发送 HTTP 请求', async () => {
|
||||
const context = fakeContext(false)
|
||||
await run(context)
|
||||
|
||||
@@ -2,16 +2,20 @@ const MODULE = 'finance-reconciliation-full'
|
||||
const SCENARIO = 'finance-full-reconciliation'
|
||||
const BASE = '/api/platform/finance/v1'
|
||||
const OPTIONS = { scenario: SCENARIO, deploymentBlocker: true }
|
||||
const STATEMENT_LIST_PERMISSION = 'platform:finance:customer-statement:list'
|
||||
const LEGACY_STATEMENT_VIEW_PERMISSION = 'platform:finance:bill:view'
|
||||
const REQUIRED_PERMISSIONS = [
|
||||
'platform:finance:customer-account:list',
|
||||
STATEMENT_LIST_PERMISSION,
|
||||
'platform:finance:reconciliation:create',
|
||||
'platform:finance:reconciliation:review',
|
||||
'platform:finance:reconciliation:issue',
|
||||
'platform:finance:reconciliation:adjust',
|
||||
'platform:finance:reconciliation:void',
|
||||
'platform:finance:settlement-ar:create',
|
||||
'platform:finance:settlement-ar:view',
|
||||
'platform:finance:settlement-ar:list',
|
||||
'platform:finance:settlement-ap:create',
|
||||
'platform:finance:settlement-ap:view',
|
||||
'platform:finance:settlement-ap:list',
|
||||
]
|
||||
|
||||
function id(value) { return value == null ? '' : String(value) }
|
||||
@@ -46,6 +50,41 @@ function sameIds(actual, expected) {
|
||||
return left.length === right.length && left.every((value, index) => value === right[index])
|
||||
}
|
||||
|
||||
function present(value) { return value !== undefined && value !== null && String(value).trim() !== '' }
|
||||
|
||||
async function verifyIssuedStatementReadback(context, fixture, statement, label) {
|
||||
const customerId = id(fixture.customerId)
|
||||
const targetId = statementId(statement)
|
||||
if (!customerId || !targetId) {
|
||||
context.addCoverageGap(MODULE, SCENARIO, `${label} 对账单列表/详情查回`,
|
||||
`必须使用本轮 fixture.customerId 和刚创建且已出账的 statementId;customerId=${customerId || 'missing'}, statementId=${targetId || 'missing'}`,
|
||||
'missing-issued-statement-readback-fixture', { marker: context.marker, customerId, statementId: targetId })
|
||||
return
|
||||
}
|
||||
const listResponse = await context.request('GET',
|
||||
`${BASE}/statements?customerId=${encodeURIComponent(customerId)}`)
|
||||
const summaries = context.apiSuccess(MODULE, `${label} 从正式对账单列表查回`, listResponse,
|
||||
`customerId=${customerId}, statementId=${targetId}, permission=${STATEMENT_LIST_PERMISSION}`, OPTIONS)
|
||||
const statementRows = rows(summaries)
|
||||
const found = statementRows.find((item) => statementId(item) === targetId)
|
||||
context.assert(MODULE, `${label} 正式对账单列表仅返回本轮客户且包含同一 statementId`, listResponse,
|
||||
statementRows.length > 0 && statementRows.every((item) => id(item?.customerId) === customerId) && Boolean(found),
|
||||
`customerId=${customerId}, statementId=${targetId}, rows=${statementRows.length}, found=${statementId(found) || 'missing'}`, OPTIONS)
|
||||
|
||||
const detailResponse = await context.request('GET',
|
||||
`${BASE}/reconciliation-statements/${encodeURIComponent(targetId)}`)
|
||||
const detail = context.apiSuccess(MODULE, `${label} 从正式对账单详情查回`, detailResponse,
|
||||
`customerId=${customerId}, statementId=${targetId}, permission=${STATEMENT_LIST_PERMISSION}`, OPTIONS)
|
||||
const auditShape = ['createdBy', 'createdAt', 'updatedBy', 'updatedAt']
|
||||
.every((field) => Object.hasOwn(detail ?? {}, field) && present(detail?.[field]))
|
||||
context.assert(MODULE, `${label} 对账单详情归属、明细、revision 与审计结构完整`, detailResponse,
|
||||
id(detail?.customerId) === customerId && statementId(detail) === targetId
|
||||
&& Array.isArray(detail?.lines) && detail.lines.length > 0
|
||||
&& Array.isArray(detail?.revisions) && detail.revisions.length > 0
|
||||
&& auditShape,
|
||||
`customerId=${id(detail?.customerId) || 'missing'}, statementId=${statementId(detail) || 'missing'}, lines=${list(detail?.lines).length}, revisions=${list(detail?.revisions).length}, auditShape=${auditShape}`, OPTIONS)
|
||||
}
|
||||
|
||||
async function findSettlementInOfficialList(context, direction, customerId, targetId, label) {
|
||||
const segment = direction === 'RECEIVABLE' ? 'receivable-settlements' : 'payable-settlements'
|
||||
const response = await context.request('GET',
|
||||
@@ -82,6 +121,42 @@ function diagnostic(response) {
|
||||
code: response?.body?.code == null ? null : String(response.body.code),
|
||||
requestId: response?.requestId ?? null, traceId: response?.traceId ?? null }
|
||||
}
|
||||
function accountRows(value) {
|
||||
const data = value?.body?.data
|
||||
return rows(data).concat(rows(data?.items))
|
||||
}
|
||||
function accountCustomerId(value) { return id(value?.customerId ?? value?.tenantId ?? value?.accountId ?? value?.id) }
|
||||
function hasWalletField(value) {
|
||||
return value && typeof value === 'object' && ['balance', 'recharge', 'prepayment'].some((field) => Object.hasOwn(value, field))
|
||||
}
|
||||
async function verifyCustomerAccountAndLedger(context, fixture) {
|
||||
const customerId = id(fixture.customerId)
|
||||
const accountResponse = await context.request('GET', `${BASE}/accounts?page=1&pageSize=20`)
|
||||
const accounts = accountRows(accountResponse)
|
||||
const account = accounts.find((item) => accountCustomerId(item) === customerId)
|
||||
context.assert(MODULE, '客户账户列表包含本轮 marker 客户且不暴露钱包字段', accountResponse,
|
||||
Boolean(account) && !accounts.some(hasWalletField),
|
||||
`customerId=${customerId}, accountCount=${accounts.length}, requestId=${accountResponse.requestId ?? 'null'}, traceId=${accountResponse.traceId ?? 'null'}`, OPTIONS)
|
||||
const ledgerResponse = await context.request('GET', `${BASE}/accounts/${encodeURIComponent(customerId)}/ledger?page=1&pageSize=200`)
|
||||
const ledger = accountRows(ledgerResponse)
|
||||
const rowsForCustomer = ledger.filter((item) => {
|
||||
const itemCustomer = accountCustomerId(item)
|
||||
return !itemCustomer || itemCustomer === customerId
|
||||
})
|
||||
const expectedIds = new Set([
|
||||
...(fixture.statements || []).map((item) => statementId(item)),
|
||||
...(fixture.receivableSettlements || []).map((item) => settlementId(item)),
|
||||
...(fixture.payableSettlements || []).map((item) => settlementId(item)),
|
||||
].filter(Boolean))
|
||||
const containsRoundTrip = rowsForCustomer.some((item) => expectedIds.has(statementId(item) || settlementId(item)
|
||||
|| id(item?.documentId ?? item?.businessId ?? item?.sourceId ?? item?.referenceId))
|
||||
|| String(item?.remark ?? '').includes(context.marker))
|
||||
context.assert(MODULE, '客户账户流水仅属于本轮客户并包含账单/收款或结算流水', ledgerResponse,
|
||||
rowsForCustomer.length === ledger.length && containsRoundTrip,
|
||||
`customerId=${customerId}, ledgerCount=${ledger.length}, requestId=${ledgerResponse.requestId ?? 'null'}, traceId=${ledgerResponse.traceId ?? 'null'}`, OPTIONS)
|
||||
fixture.root.accountReadback = { account: account || null, accountDiagnostic: diagnostic(accountResponse),
|
||||
ledgerCount: ledger.length, ledgerDiagnostic: diagnostic(ledgerResponse) }
|
||||
}
|
||||
function blocker(context, name, evidence, category, response = null) {
|
||||
context.addStep(MODULE, name, response, false, evidence, OPTIONS)
|
||||
context.addCoverageGap(MODULE, SCENARIO, name, evidence, category,
|
||||
@@ -251,6 +326,13 @@ export async function run(context) {
|
||||
const permissionResponse = await context.request('GET', '/api/platform/v1/iam/authorization/permissions')
|
||||
const permissions = context.apiSuccess(MODULE, '查询当前账号 Finance 有效权限', permissionResponse, '', OPTIONS)
|
||||
const granted = new Set(list(permissions).map(String))
|
||||
if (granted.has(LEGACY_STATEMENT_VIEW_PERMISSION)) {
|
||||
blocker(context, '客户对账单旧 view 权限兼容阻断',
|
||||
`检测到旧权限=${LEGACY_STATEMENT_VIEW_PERMISSION};列表和详情只能以正式权限=${STATEMENT_LIST_PERMISSION} 验收,旧 view 不得作为通过依据`,
|
||||
'legacy-customer-statement-view-permission', permissionResponse)
|
||||
finish(context, start)
|
||||
return
|
||||
}
|
||||
const missingPermissions = REQUIRED_PERMISSIONS.filter((permission) => !granted.has(permission))
|
||||
if (missingPermissions.length > 0) {
|
||||
blocker(context, 'Finance 写权限门禁',
|
||||
@@ -374,6 +456,7 @@ export async function run(context) {
|
||||
finish(context, start)
|
||||
return
|
||||
}
|
||||
await verifyIssuedStatementReadback(context, fixture, issued, `第 ${index + 1} 张已出账对账单`)
|
||||
issuedStatements.push(issued)
|
||||
if (index === 0) {
|
||||
context.assert(MODULE, 'SELECTED 日期范围只包含指定真实日期来源', created.response,
|
||||
@@ -455,5 +538,6 @@ export async function run(context) {
|
||||
feeSourceIds: payables.map((source) => source.feeSourceId),
|
||||
}] : []
|
||||
fixture.root.sourceMappings = fixture.sourceMappings
|
||||
await verifyCustomerAccountAndLedger(context, fixture)
|
||||
finish(context, start)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,10 @@ const response = (data, http = 200, code = '200') => ({ http, body: { code, data
|
||||
|
||||
function fakeContext(overrides = {}) {
|
||||
const balances = new Map([['101', 100], ['102', 100], ['103', 100], ['201', 100]])
|
||||
const paidAmounts = new Map([['101', 0], ['102', 0], ['103', 0], ['201', 0]])
|
||||
const statuses = new Map([['101', 'ISSUED'], ['102', 'ISSUED'], ['103', 'ISSUED'], ['201', 'ISSUED']])
|
||||
const versions = new Map([['101', 1], ['102', 1], ['103', 1], ['201', 1]])
|
||||
const actionIdempotency = new Map()
|
||||
const settlementToBill = new Map([['101', '11'], ['102', '12'], ['103', '13']])
|
||||
const billToSettlement = new Map([...settlementToBill].map(([s, b]) => [b, s]))
|
||||
const fixture = { marker: 'T1', customerId: '9', voucherFileId: '77',
|
||||
@@ -33,11 +37,21 @@ function fakeContext(overrides = {}) {
|
||||
async request(method, path, body, token, headers) {
|
||||
calls.push({ method, path, body, headers })
|
||||
const match = path.match(/\/settlements\/(\d+)/)
|
||||
if (method === 'GET' && match) return response({ settlementId: match[1], outstandingAmount: balances.get(match[1]), status: balances.get(match[1]) === 0 ? 'PAID' : 'PARTIALLY_PAID', version: calls.length })
|
||||
if (path.endsWith('/receipts')) { for (const allocation of body.allocations) balances.set(billToSettlement.get(String(allocation.billId)), balances.get(billToSettlement.get(String(allocation.billId))) - allocation.amount); return response({ receiptId: calls.length, allocations: body.allocations }) }
|
||||
if (method === 'GET' && match) return response({ settlementId: match[1], outstandingAmount: balances.get(match[1]), paidAmount: paidAmounts.get(match[1]), status: statuses.get(match[1]), version: versions.get(match[1]) })
|
||||
if (match && path.endsWith('/actions')) {
|
||||
const targetId = match[1]; const idem = headers['Idempotency-Key']; const fingerprint = JSON.stringify(body)
|
||||
const prior = actionIdempotency.get(idem)
|
||||
if (prior) return prior.fingerprint === fingerprint ? prior.response : response(null, 409, 'FIN_IDEMPOTENCY_CONFLICT')
|
||||
if (body.action === 'VOID' && paidAmounts.get(targetId) > 0) return response(null, 409, 'FIN_SETTLEMENT_ALREADY_PAID')
|
||||
if (body.expectedVersion !== versions.get(targetId)) return response(null, 409, 'FIN_VERSION_CONFLICT')
|
||||
const nextStatus = body.action === 'VOID' ? 'VOIDED' : 'ISSUED'; statuses.set(targetId, nextStatus); versions.set(targetId, versions.get(targetId) + 1)
|
||||
const result = response({ settlementId: targetId, outstandingAmount: balances.get(targetId), paidAmount: paidAmounts.get(targetId), status: nextStatus, version: versions.get(targetId) })
|
||||
actionIdempotency.set(idem, { fingerprint, response: result }); return result
|
||||
}
|
||||
if (path.endsWith('/receipts')) { for (const allocation of body.allocations) { const targetId = billToSettlement.get(String(allocation.billId)); balances.set(targetId, balances.get(targetId) - allocation.amount); paidAmounts.set(targetId, paidAmounts.get(targetId) + allocation.amount); statuses.set(targetId, balances.get(targetId) === 0 ? 'PAID' : 'PARTIALLY_PAID'); versions.set(targetId, versions.get(targetId) + 1) } return response({ receiptId: calls.length, allocations: body.allocations }) }
|
||||
if (match && path.endsWith('/transactions')) {
|
||||
const current = balances.get(match[1]); if (body.amount > current || current === 0) return response(null, 409, 'FIN_409')
|
||||
balances.set(match[1], current - body.amount); return response({ settlementId: match[1], outstandingAmount: current - body.amount, status: current - body.amount === 0 ? 'PAID' : 'PARTIALLY_PAID', version: calls.length })
|
||||
balances.set(match[1], current - body.amount); paidAmounts.set(match[1], paidAmounts.get(match[1]) + body.amount); statuses.set(match[1], current - body.amount === 0 ? 'PAID' : 'PARTIALLY_PAID'); versions.set(match[1], versions.get(match[1]) + 1); return response({ settlementId: match[1], outstandingAmount: current - body.amount, paidAmount: paidAmounts.get(match[1]), status: statuses.get(match[1]), version: versions.get(match[1]) })
|
||||
}
|
||||
throw new Error(`unexpected ${method} ${path}`)
|
||||
} }
|
||||
@@ -64,6 +78,24 @@ test('run 使用 receipts 完成应收部分、全额及多单分配,使用 tr
|
||||
assert.ok(context.createdResources.financeFullFixtures.payments.final)
|
||||
assert.equal(context.createdResources.financeFullFixtures.settlementFinalStates.receivable.outstandingAmount, 0)
|
||||
assert.equal(context.createdResources.financeFullFixtures.settlementFinalStates.payable.outstandingAmount, 0)
|
||||
const actions = context.calls.filter((call) => call.path.endsWith('/actions'))
|
||||
assert.deepEqual(actions.slice(0, 4).map((call) => call.body.action), ['VOID', 'VOID', 'REOPEN', 'REOPEN'])
|
||||
assert.equal(actions[0].headers['Idempotency-Key'], actions[1].headers['Idempotency-Key'])
|
||||
assert.equal(actions[0].headers['Idempotency-Key'], actions[2].headers['Idempotency-Key'])
|
||||
assert.notEqual(actions[2].body.reason, actions[0].body.reason)
|
||||
assert.notEqual(actions[3].headers['Idempotency-Key'], actions[0].headers['Idempotency-Key'])
|
||||
assert.ok(context.assertions.find((item) => item.name === '已收付款结算单拒绝作废且状态金额不变')?.condition)
|
||||
})
|
||||
|
||||
test('正式结算动作仅使用本轮 fixture,覆盖 VOID/REOPEN、重放、异参冲突和已付款拒绝', async () => {
|
||||
const context = fakeContext()
|
||||
await run(context)
|
||||
const actions = context.calls.filter((call) => call.path.match(/\/settlements\/(101|102|103|201)\/actions$/))
|
||||
assert.equal(actions.length, 5)
|
||||
assert.ok(actions.every((call) => ['101', '102', '103', '201'].includes(call.path.match(/\/settlements\/(\d+)\/actions$/)[1])))
|
||||
for (const name of ['未收付款结算单作废', '结算单作废同键同参幂等重放', '结算单动作同键异参冲突', '已作废结算单重开', '已收付款结算单拒绝作废且状态金额不变']) {
|
||||
assert.ok(context.assertions.find((item) => item.name === name)?.condition, name)
|
||||
}
|
||||
})
|
||||
|
||||
test('冻结共享输入使用 4 条 statements、3 条 AR 和 payableSettlements 数组', async () => {
|
||||
|
||||
@@ -9,7 +9,9 @@ const write = (context, method, path, body, operation) => context.request(method
|
||||
const settlementId = (value) => id(value?.settlementId ?? value?.summary?.settlementId ?? value?.id)
|
||||
const statementId = (value) => id(value?.statementId ?? value?.reconciliationStatementId ?? value?.id)
|
||||
const outstanding = (value) => Number(value?.outstandingAmount ?? value?.summary?.outstandingAmount ?? 0)
|
||||
const paid = (value) => Number(value?.paidAmount ?? value?.summary?.paidAmount ?? 0)
|
||||
const version = (value) => Number(value?.version ?? value?.summary?.version ?? 0)
|
||||
const status = (value) => id(value?.status ?? value?.summary?.status ?? value?.paymentStatus)
|
||||
|
||||
export function values(value) {
|
||||
if (Array.isArray(value)) return value.filter(Boolean)
|
||||
@@ -34,6 +36,61 @@ async function detail(context, targetId) {
|
||||
return { response, value: data(response) }
|
||||
}
|
||||
|
||||
async function verifyVoidReopen(context, item) {
|
||||
const targetId = settlementId(item)
|
||||
const before = await detail(context, targetId)
|
||||
if (!ok(before.response) || status(before.value) !== 'ISSUED' || outstanding(before.value) <= 0 || paid(before.value) !== 0) {
|
||||
gap(context, '取得未收付款 ISSUED 结算单', 'settlement VOID/REOPEN',
|
||||
`本轮 settlementId=${targetId || 'missing'} 不满足 ISSUED 且 paidAmount=0;status=${status(before.value)}, paid=${paid(before.value)}`,
|
||||
'settlement-action-fixture-unavailable')
|
||||
return null
|
||||
}
|
||||
const path = `${BASE}/settlements/${targetId}/actions`
|
||||
const voidBody = { action: 'VOID', reason: context.marker, expectedVersion: version(before.value) }
|
||||
const voidedResponse = await write(context, 'POST', path, voidBody, 'settlement-void')
|
||||
const voided = data(voidedResponse)
|
||||
context.assert(MODULE, '未收付款结算单作废', voidedResponse, ok(voidedResponse)
|
||||
&& status(voided) === 'VOIDED' && version(voided) === version(before.value) + 1,
|
||||
`settlementId=${targetId}, status=${status(voided)}, version=${version(voided)}`, OPTIONS)
|
||||
|
||||
const replayResponse = await write(context, 'POST', path, voidBody, 'settlement-void')
|
||||
const replayed = data(replayResponse)
|
||||
context.assert(MODULE, '结算单作废同键同参幂等重放', replayResponse, ok(replayResponse)
|
||||
&& status(replayed) === 'VOIDED' && version(replayed) === version(voided),
|
||||
`settlementId=${targetId}, version=${version(replayed)}`, OPTIONS)
|
||||
|
||||
const conflictResponse = await write(context, 'POST', path,
|
||||
{ action: 'REOPEN', reason: `${context.marker}-conflict`, expectedVersion: version(voided) }, 'settlement-void')
|
||||
context.assert(MODULE, '结算单动作同键异参冲突', conflictResponse, !ok(conflictResponse),
|
||||
`HTTP=${conflictResponse.http}, code=${id(conflictResponse.body?.code)}`, OPTIONS)
|
||||
|
||||
const reopenBody = { action: 'REOPEN', reason: context.marker, expectedVersion: version(voided) }
|
||||
const reopenedResponse = await write(context, 'POST', path, reopenBody, 'settlement-reopen')
|
||||
const reopened = data(reopenedResponse)
|
||||
context.assert(MODULE, '已作废结算单重开', reopenedResponse, ok(reopenedResponse)
|
||||
&& status(reopened) === 'ISSUED' && version(reopened) === version(voided) + 1,
|
||||
`settlementId=${targetId}, status=${status(reopened)}, version=${version(reopened)}`, OPTIONS)
|
||||
return reopened
|
||||
}
|
||||
|
||||
async function verifyPaidVoidRejected(context, item) {
|
||||
const targetId = settlementId(item)
|
||||
const before = await detail(context, targetId)
|
||||
if (!ok(before.response) || paid(before.value) <= 0) {
|
||||
gap(context, '取得已发生收付款结算单', 'paid settlement VOID rejection',
|
||||
`本轮 settlementId=${targetId || 'missing'} 没有有效收付款;paid=${paid(before.value)}`,
|
||||
'paid-settlement-fixture-unavailable')
|
||||
return
|
||||
}
|
||||
const response = await write(context, 'POST', `${BASE}/settlements/${targetId}/actions`,
|
||||
{ action: 'VOID', reason: context.marker, expectedVersion: version(before.value) }, 'paid-settlement-void')
|
||||
const after = await detail(context, targetId)
|
||||
context.assert(MODULE, '已收付款结算单拒绝作废且状态金额不变', response, !ok(response) && ok(after.response)
|
||||
&& status(after.value) === status(before.value) && version(after.value) === version(before.value)
|
||||
&& paid(after.value) === paid(before.value) && outstanding(after.value) === outstanding(before.value),
|
||||
`settlementId=${targetId}, status=${status(after.value)}, paid=${paid(after.value)}, outstanding=${outstanding(after.value)}, version=${version(after.value)}`, OPTIONS)
|
||||
}
|
||||
|
||||
async function receive(context, fixtures, item, requestedAmount, operation, label) {
|
||||
const targetId = settlementId(item)
|
||||
const billId = statementId(item) || id(item.reconciliationStatementId)
|
||||
@@ -87,6 +144,11 @@ export async function run(context) {
|
||||
manualIds.length > 0 && manualIds.every((feeId) => usedIds.has(feeId)),
|
||||
`manual=${manualIds.length}, settled=${manualIds.filter((feeId) => usedIds.has(feeId)).length}`, OPTIONS)
|
||||
|
||||
const actionTarget = validReceivables.find((item) => item.status === 'ISSUED')
|
||||
?? validPayables.find((item) => item.status === 'ISSUED')
|
||||
if (!actionTarget) gap(context, '取得结算单动作 Fixture', 'settlement VOID/REOPEN', '本轮没有未收付款且状态 ISSUED 的 AR/AP,禁止借历史单据')
|
||||
else await verifyVoidReopen(context, actionTarget)
|
||||
|
||||
const multiTargets = validReceivables
|
||||
if (multiTargets.length < 3) gap(context, '取得三张应收结算单', 'multi receipt allocation', '本轮没有三张独占应收单,禁止借历史单据')
|
||||
else {
|
||||
@@ -111,6 +173,7 @@ export async function run(context) {
|
||||
const final = await receive(context, fixtures, primary, outstanding(partial.settlement), 'ar-final-receipt', '应收全额收款完结')
|
||||
fixtures.receipts = { ...(fixtures.receipts || {}), partial: partial.receipt, final: final.receipt }
|
||||
fixtures.settlementFinalStates = { ...(fixtures.settlementFinalStates || {}), receivable: final.settlement }
|
||||
await verifyPaidVoidRejected(context, primary)
|
||||
}
|
||||
|
||||
const payable = validPayables[0]
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
const MODULE = 'phase3-container-overview'
|
||||
const OPTIONS = { scenario: 'phase3-container-overview-read-only', deploymentBlocker: true }
|
||||
const CONTAINERS = '/api/platform/v1/containers'
|
||||
|
||||
function id(value) { return value == null ? '' : String(value) }
|
||||
|
||||
export async function run(context) {
|
||||
const fixture = context.createdResources?.phase3ContainerLoading
|
||||
const containerId = id(fixture?.containerBookingId)
|
||||
const planId = id(fixture?.loadingPlanId)
|
||||
const warehouseId = id(fixture?.warehouseId)
|
||||
if (!containerId || !planId || !warehouseId || fixture?.marker !== context.marker) {
|
||||
context.addStep(MODULE, '复用智能装柜产生的货柜 overview fixture', null, false,
|
||||
`containerBookingId=${containerId || 'missing'}, loadingPlanId=${planId || 'missing'}, warehouseId=${warehouseId || 'missing'}`, OPTIONS)
|
||||
return
|
||||
}
|
||||
|
||||
const overviewResponse = await context.request('GET', `${CONTAINERS}/${encodeURIComponent(containerId)}/overview`)
|
||||
const overview = context.apiSuccess(MODULE, '查询货柜 overview 聚合详情', overviewResponse,
|
||||
`containerBookingId=${containerId}`, OPTIONS)
|
||||
context.assert(MODULE, 'overview 返回货柜身份、版本和时间线', overviewResponse,
|
||||
id(overview?.container?.id) === containerId
|
||||
&& Number.isFinite(Number(overview?.container?.version))
|
||||
&& Array.isArray(overview?.documents)
|
||||
&& Array.isArray(overview?.exceptionHistory)
|
||||
&& Array.isArray(overview?.timeline),
|
||||
`documents=${overview?.documents?.length ?? 'missing'}, timeline=${overview?.timeline?.length ?? 'missing'}`, OPTIONS)
|
||||
const timeline = Array.isArray(overview?.timeline) ? overview.timeline : []
|
||||
context.assert(MODULE, 'overview timeline 按 occurredAt 升序返回', overviewResponse,
|
||||
timeline.every((item, index) => index === 0 || new Date(timeline[index - 1]?.occurredAt) <= new Date(item?.occurredAt)),
|
||||
`events=${timeline.length}`, OPTIONS)
|
||||
|
||||
if (overview?.pda) {
|
||||
context.assert(MODULE, 'overview PDA 摘要字段完整', overviewResponse,
|
||||
['taskId', 'taskStatus', 'taskVersion', 'itemCount', 'completedItemCount', 'unresolvedItemCount']
|
||||
.every((field) => overview.pda[field] !== undefined)
|
||||
&& Array.isArray(overview.pda.photoCounts)
|
||||
&& Array.isArray(overview.pda.photos)
|
||||
&& Array.isArray(overview.pda.exceptionReasons),
|
||||
`taskId=${id(overview.pda.taskId)}, photos=${overview.pda.photos.length}`, OPTIONS)
|
||||
for (const [index, photo] of overview.pda.photos.entries()) {
|
||||
context.assert(MODULE, `overview PDA 照片摘要 ${index + 1} 字段完整`, overviewResponse,
|
||||
['evidenceId', 'fileId', 'status', 'downloadPath'].every((field) => photo[field] !== undefined),
|
||||
`fileId=${id(photo.fileId)}`, OPTIONS)
|
||||
}
|
||||
}
|
||||
|
||||
for (const documentType of ['DELIVERY_ORDER', 'PICKUP_ORDER']) {
|
||||
const response = await context.request('GET', `${CONTAINERS}/${encodeURIComponent(containerId)}/documents/${documentType}`)
|
||||
const data = context.apiSuccess(MODULE, `查询 ${documentType} 单证`, response, `documentType=${documentType}`, OPTIONS)
|
||||
context.assert(MODULE, `${documentType} 单证归属货柜`, response,
|
||||
data == null || id(data.containerId || containerId) === containerId,
|
||||
`containerBookingId=${containerId}`, OPTIONS)
|
||||
}
|
||||
|
||||
const clearance = await context.request('GET', `${CONTAINERS}/${encodeURIComponent(containerId)}/clearance`)
|
||||
context.apiSuccess(MODULE, '查询目的港清关资料', clearance, `containerBookingId=${containerId}`, OPTIONS)
|
||||
const customs = await context.request('GET', `/api/platform/v1/first-leg/loading-lists/${encodeURIComponent(planId)}/customs?warehouseId=${encodeURIComponent(warehouseId)}`)
|
||||
context.apiSuccess(MODULE, '查询出口报关资料', customs, `loadingPlanId=${planId}, warehouseId=${warehouseId}`, OPTIONS)
|
||||
|
||||
context.addStep(MODULE, '异常标记/取消、单证上传和完成锁定保持写入阻断', null, true,
|
||||
'默认全量脚本只读;写场景需显式独立授权和一次性测试柜次', OPTIONS)
|
||||
}
|
||||
@@ -23,12 +23,22 @@ $requiredTests = @(
|
||||
"iam/src/test/java/com/baiyue/iam/notification/NotificationBusinessEventConsumerTest.java",
|
||||
"iam/src/test/java/com/baiyue/iam/notification/NotificationSchedulerRecoveryTest.java",
|
||||
"iam/src/test/java/com/baiyue/iam/notification/NotificationConfigurationContractTest.java",
|
||||
"gateway/src/test/java/com/baiyue/gateway/config/GatewayFileRouteConfigurationTest.java"
|
||||
"gateway/src/test/java/com/baiyue/gateway/config/GatewayFileRouteConfigurationTest.java",
|
||||
"file/src/test/java/com/baiyue/file/controller/QualificationFileContractTest.java",
|
||||
"file/src/test/java/com/baiyue/file/controller/InternalBusinessFileControllerTest.java",
|
||||
"file/src/test/java/com/baiyue/file/service/InternalBusinessFileServiceTest.java",
|
||||
"firstleg/src/test/java/com/baiyue/firstleg/controller/ContainerControllerContractTest.java",
|
||||
"firstleg/src/test/java/com/baiyue/firstleg/controller/LoadingListControllerContractTest.java",
|
||||
"firstleg/src/test/java/com/baiyue/firstleg/repository/ContainerOverviewSqlContractTest.java",
|
||||
"firstleg/src/test/java/com/baiyue/firstleg/service/ContainerOverviewServiceTest.java",
|
||||
"firstleg/src/test/java/com/baiyue/firstleg/service/LoadingListServiceTest.java",
|
||||
"warehouse/src/test/java/com/baiyue/warehouse/repository/OverseasStorageFeeSqlContractTest.java",
|
||||
"warehouse/src/test/java/com/baiyue/warehouse/controller/PlatformOverseasStorageFeeControllerContractTest.java"
|
||||
)
|
||||
$mavenArgs = @("-f", $pom, "-pl", "order,firstleg,iam,gateway", "-am", "test")
|
||||
$mavenArgs = @("-f", $pom, "-pl", "order,firstleg,iam,gateway,file,warehouse", "-am", "test")
|
||||
|
||||
Write-Host "Backend regression root: $repoRoot"
|
||||
Write-Host "Scope: order + firstleg + iam + gateway Maven reactor (includes all module tests)"
|
||||
Write-Host "Scope: order + firstleg + iam + gateway + file + warehouse Maven reactor (includes all module tests)"
|
||||
Write-Host "Database/API access: none"
|
||||
|
||||
if (-not (Test-Path -LiteralPath $pom)) {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { existsSync, readFileSync } from 'node:fs'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import test from 'node:test'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const scriptsDir = dirname(fileURLToPath(import.meta.url))
|
||||
const backendScript = readFileSync(resolve(scriptsDir, 'backend-full-regression.ps1'), 'utf8')
|
||||
const adminAcceptance = readFileSync(resolve(scriptsDir, 'admin-test-env-acceptance.mjs'), 'utf8')
|
||||
|
||||
test('backend full regression includes file reactor and critical file/container coverage', () => {
|
||||
assert.match(backendScript, /-pl["',\s\S]*order,firstleg,iam,gateway,file/)
|
||||
for (const required of [
|
||||
'file/src/test/java/com/baiyue/file/controller/QualificationFileContractTest.java',
|
||||
'file/src/test/java/com/baiyue/file/controller/InternalBusinessFileControllerTest.java',
|
||||
'file/src/test/java/com/baiyue/file/service/InternalBusinessFileServiceTest.java',
|
||||
'firstleg/src/test/java/com/baiyue/firstleg/controller/ContainerControllerContractTest.java',
|
||||
'firstleg/src/test/java/com/baiyue/firstleg/controller/LoadingListControllerContractTest.java',
|
||||
'firstleg/src/test/java/com/baiyue/firstleg/repository/ContainerOverviewSqlContractTest.java',
|
||||
'firstleg/src/test/java/com/baiyue/firstleg/service/ContainerOverviewServiceTest.java',
|
||||
'firstleg/src/test/java/com/baiyue/firstleg/service/LoadingListServiceTest.java',
|
||||
]) assert.match(backendScript, new RegExp(required.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')))
|
||||
})
|
||||
|
||||
test('test-environment full acceptance registers container overview module', () => {
|
||||
const modulePath = resolve(scriptsDir, 'admin-test-env-modules', 'phase3-container-overview.mjs')
|
||||
if (!adminAcceptance.includes('phase3-container-overview') || !existsSync(modulePath)) {
|
||||
assert.fail('default acceptance module phase3-container-overview is not registered yet')
|
||||
}
|
||||
})
|
||||
@@ -11,8 +11,8 @@ $ErrorActionPreference = 'Stop'
|
||||
$Repo = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
|
||||
$Workspace = (Resolve-Path (Join-Path $Repo '..')).Path
|
||||
$Acceptance = Join-Path $Workspace '.codex-tmp\p23-04-acceptance'
|
||||
$ExpectedSha256 = 'CA250057CAB91D248E31BCC5A66CADE72F81EB9906B19B8E3CB09BD60B112F29'
|
||||
$ExpectedLines = 14708
|
||||
$ExpectedSha256 = 'BE7C5C98712D5202F5D187EA9A37C64EAB9F6AA9AE772C89A51F186507B1AE53'
|
||||
$ExpectedLines = 14706
|
||||
|
||||
if ($TargetHost -notin @('127.0.0.1', 'localhost')) {
|
||||
[Console]::Error.WriteLine('Refusing non-local database; only 127.0.0.1 or localhost is allowed.')
|
||||
|
||||
Reference in New Issue
Block a user