1
This commit is contained in:
@@ -14,13 +14,26 @@ function requireData(context, name, response) {
|
||||
return value
|
||||
}
|
||||
|
||||
function assertBinaryCsv(context, name, response) {
|
||||
function assertBinaryCsv(context, name, response, expectedHeader, expectedTokens) {
|
||||
const text = response?.bytes instanceof Uint8Array ? new TextDecoder().decode(response.bytes).replace(/^\uFEFF/, '') : ''
|
||||
const header = text.split(/\r?\n/, 1)[0]
|
||||
context.assert(MODULE, name, response,
|
||||
response?.http === 200
|
||||
&& String(response.contentType || '').toLowerCase().startsWith('text/csv')
|
||||
&& response.bytes instanceof Uint8Array && response.bytes.length > 0,
|
||||
`HTTP=${response?.http ?? 0}, contentType=${response?.contentType || 'missing'}, bytes=${response?.bytes?.length || 0}`,
|
||||
BLOCKER)
|
||||
context.assert(MODULE, `${name}表头与marker内容`, response,
|
||||
header === expectedHeader && expectedTokens.every((token) => text.includes(String(token))),
|
||||
`header=${header || 'missing'}, expectedTokens=${expectedTokens.join('|')}`, BLOCKER)
|
||||
}
|
||||
|
||||
const optionIds = (values) => Array.isArray(values) ? values.map((item) => asId(item?.warehouseId)).filter(Boolean) : []
|
||||
|
||||
function assertForbidden(context, name, response) {
|
||||
context.assert(MODULE, name, response,
|
||||
response?.http === 403 || (response?.http === 200 && ['403', 'A0403'].includes(String(response?.body?.code))),
|
||||
`HTTP=${response?.http ?? 0}, code=${response?.body?.code ?? 'missing'}`, BLOCKER)
|
||||
}
|
||||
|
||||
async function safeCleanup(context, name, action) {
|
||||
@@ -97,6 +110,16 @@ async function createAndVerifyPartnerWarehouse(context, resource) {
|
||||
BLOCKER)
|
||||
}
|
||||
|
||||
async function captureWarehouseOptionsBeforeCreate(context, resource) {
|
||||
const response = await context.requestOverseas('GET', `${OVERSEAS_BASE}/overseas-quotes/warehouse-options`)
|
||||
const options = requireData(context, '创建合作仓前查询报价候选缓存基线', response)
|
||||
context.assert(MODULE, '合作仓候选基线为规范数组', response,
|
||||
Array.isArray(options) && options.every((item) => asId(item?.warehouseId)
|
||||
&& item.warehouseCode && item.warehouseName && /^[A-Z]{2}$/.test(item.countryCode)),
|
||||
`count=${Array.isArray(options) ? options.length : 'invalid'}`, BLOCKER)
|
||||
resource.partnerWarehouseOptionsBefore = optionIds(options)
|
||||
}
|
||||
|
||||
function quoteCreatePayload(context, warehouseId) {
|
||||
return {
|
||||
name: `${context.marker} 海外报价`, warehouseId,
|
||||
@@ -127,27 +150,25 @@ async function quoteLifecycle(context, resource) {
|
||||
Array.isArray(options) && options.every((item) => asId(item.warehouseId)
|
||||
&& item.warehouseCode && item.warehouseName && item.countryCode),
|
||||
`count=${Array.isArray(options) ? options.length : 'invalid'}`, BLOCKER)
|
||||
if (Array.isArray(options) && options.length === 0) {
|
||||
resource.quote = {
|
||||
skipped: true,
|
||||
reason: 'warehouse-options-empty-cache',
|
||||
finalStatus: null,
|
||||
}
|
||||
context.addCoverageGap(MODULE, BLOCKER.scenario, '海外报价完整写生命周期',
|
||||
'warehouse-options 返回空数组;已创建的 OVERSEAS_PARTNER 可能尚未穿透服务端30分钟空缓存。本轮跳过报价且不猜测 warehouseId,继续验收汇率与车辆。',
|
||||
'deployment-prerequisite', {
|
||||
marker: context.marker,
|
||||
partnerWarehouseId: resource.partnerWarehouse?.id || null,
|
||||
http: optionsResponse?.http ?? null,
|
||||
code: optionsResponse?.body?.code ?? null,
|
||||
requestId: optionsResponse?.requestId ?? null,
|
||||
traceId: optionsResponse?.traceId ?? null,
|
||||
})
|
||||
return false
|
||||
const createdId = resource.partnerWarehouse?.id
|
||||
if (resource.partnerWarehouseOptionsBefore?.length === 0) {
|
||||
context.assert(MODULE, '空候选不负缓存30分钟', optionsResponse,
|
||||
optionIds(options).includes(createdId), `createdId=${createdId}, ids=${optionIds(options).join(',') || 'empty'}`, BLOCKER)
|
||||
}
|
||||
const createdOption = options.find((item) => asId(item.warehouseId) === resource.partnerWarehouse?.id)
|
||||
const option = createdOption || options[0]
|
||||
if (!option) throw new Error('海外报价接口未返回可用合作仓候选')
|
||||
|
||||
const refreshResponse = await context.requestOverseas('GET',
|
||||
`${OVERSEAS_BASE}/overseas-quotes/warehouse-options?refresh=true`)
|
||||
const refreshed = requireData(context, '强制刷新海外报价合作仓候选', refreshResponse)
|
||||
const option = Array.isArray(refreshed) ? refreshed.find((item) => asId(item.warehouseId) === createdId) : null
|
||||
context.assert(MODULE, '刷新后新建合作仓立即可见', refreshResponse, Boolean(option),
|
||||
`createdId=${createdId}, ids=${optionIds(refreshed).join(',') || 'empty'}`, BLOCKER)
|
||||
if (!option) throw new Error(`warehouse-options refresh=true 未返回本轮合作仓 ${createdId}`)
|
||||
|
||||
const cachedResponse = await context.requestOverseas('GET', `${OVERSEAS_BASE}/overseas-quotes/warehouse-options`)
|
||||
const cached = requireData(context, '刷新后读取30分钟正缓存', cachedResponse)
|
||||
context.assert(MODULE, '30分钟正缓存与刷新快照一致', cachedResponse,
|
||||
JSON.stringify(cached) === JSON.stringify(refreshed),
|
||||
`refreshed=${optionIds(refreshed).join(',')}, cached=${optionIds(cached).join(',')}`, BLOCKER)
|
||||
|
||||
const payload = quoteCreatePayload(context, asId(option.warehouseId))
|
||||
const createResponse = await context.requestOverseas('POST', `${OVERSEAS_BASE}/overseas-quotes`, payload)
|
||||
@@ -164,6 +185,13 @@ async function quoteLifecycle(context, resource) {
|
||||
&& Array.isArray(detail?.warehouseFees) && detail.warehouseFees.length > 0,
|
||||
`id=${asId(detail?.summary?.id) || 'missing'}, version=${detail?.summary?.recordVersion ?? 'missing'}`, BLOCKER)
|
||||
|
||||
const listResponse = await context.requestOverseas('GET',
|
||||
`${OVERSEAS_BASE}/overseas-quotes?page=1&size=20&keyword=${encodeURIComponent(context.marker)}`)
|
||||
const page = requireData(context, '分页查询marker海外报价', listResponse)
|
||||
context.assert(MODULE, '报价列表包含本轮长ID与recordVersion', listResponse,
|
||||
Array.isArray(page?.records) && page.records.some((item) => asId(item.id) === id && recordVersion(item.recordVersion)),
|
||||
`id=${id}, total=${page?.total ?? 'missing'}`, BLOCKER)
|
||||
|
||||
const staleVersion = Number(detail.summary.recordVersion)
|
||||
const updatePayload = quoteUpdatePayload({ ...payload, transitPrices: payload.transitPrices }, context, staleVersion)
|
||||
const updateResponse = await context.requestOverseas('PUT', path, updatePayload)
|
||||
@@ -191,7 +219,9 @@ async function quoteLifecycle(context, resource) {
|
||||
detail.summary.status === 'ACTIVE', `status=${detail.summary.status}`, BLOCKER)
|
||||
|
||||
const csv = await context.requestOverseasBinary('GET', `${OVERSEAS_BASE}/overseas-quotes/export?keyword=${encodeURIComponent(context.marker)}`)
|
||||
assertBinaryCsv(context, '导出海外报价CSV', csv)
|
||||
assertBinaryCsv(context, '导出海外报价CSV', csv,
|
||||
'报价ID,报价名称,合作仓编码,合作仓名称,报价方,币种,生效开始,生效结束,版本号,状态,更新时间',
|
||||
[id, context.marker])
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -252,9 +282,17 @@ async function exchangeLifecycle(context, resource) {
|
||||
context.assert(MODULE, '汇率状态切换为INACTIVE', statusResponse,
|
||||
rate.status === 'INACTIVE', `status=${rate.status}`, BLOCKER)
|
||||
|
||||
const listResponse = await context.requestOverseas('GET',
|
||||
`${OVERSEAS_BASE}/exchange-rates?page=1&size=20&monthFrom=${month}&monthTo=${month}&status=INACTIVE`)
|
||||
const page = requireData(context, '分页查询marker月汇率', listResponse)
|
||||
context.assert(MODULE, '汇率列表包含本轮月份与recordVersion', listResponse,
|
||||
Array.isArray(page?.records) && page.records.some((item) => item.month === month && recordVersion(item.recordVersion)),
|
||||
`month=${month}, total=${page?.total ?? 'missing'}`, BLOCKER)
|
||||
|
||||
const csv = await context.requestOverseasBinary('GET',
|
||||
`${OVERSEAS_BASE}/exchange-rates/export?monthFrom=${month}&monthTo=${month}`)
|
||||
assertBinaryCsv(context, '导出marker月汇率CSV', csv)
|
||||
assertBinaryCsv(context, '导出marker月汇率CSV', csv,
|
||||
'月份,基准币种,报价币种,月平均汇率,状态,更新人,更新时间', [month, 'INACTIVE'])
|
||||
}
|
||||
|
||||
function vehiclePayload(context, edited = false) {
|
||||
@@ -316,8 +354,48 @@ async function vehicleLifecycle(context, resource) {
|
||||
context.assert(MODULE, '车辆状态切换为MAINTENANCE', statusResponse,
|
||||
vehicle.status === 'MAINTENANCE', `status=${vehicle.status}`, BLOCKER)
|
||||
|
||||
const listResponse = await context.requestOverseas('GET',
|
||||
`${OVERSEAS_BASE}/vehicles?page=1&size=20&keyword=${encodeURIComponent(createPayload.plateNumber)}`)
|
||||
const page = requireData(context, '分页查询marker车辆', listResponse)
|
||||
context.assert(MODULE, '车辆列表包含本轮长ID与recordVersion', listResponse,
|
||||
Array.isArray(page?.records) && page.records.some((item) => asId(item.id) === id && recordVersion(item.recordVersion)),
|
||||
`id=${id}, total=${page?.total ?? 'missing'}`, BLOCKER)
|
||||
|
||||
const csv = await context.requestOverseasBinary('GET', `${OVERSEAS_BASE}/vehicles/export?keyword=${encodeURIComponent(createPayload.plateNumber)}`)
|
||||
assertBinaryCsv(context, '导出marker车辆CSV', csv)
|
||||
assertBinaryCsv(context, '导出marker车辆CSV', csv,
|
||||
'车辆ID,车牌号,车型,油耗(L/100km),当前公里码(km),司机或外派单位,联系电话,区域,车辆归属,状态,更新时间',
|
||||
[id, createPayload.plateNumber, 'MAINTENANCE'])
|
||||
}
|
||||
|
||||
async function verifyCanonicalPermissionDenial(context, resource) {
|
||||
const suffix = context.marker.replace(/[^A-Za-z0-9]/g, '').slice(-18).toUpperCase()
|
||||
const roleResponse = await context.request('POST', '/api/platform/v1/iam/roles', {
|
||||
roleCode: `P4_MD_DENY_${suffix}`.slice(0, 64), roleName: `四期主数据拒绝-${suffix}`, description: context.marker,
|
||||
})
|
||||
const role = requireData(context, '创建无主数据权限角色', roleResponse)
|
||||
const roleId = asId(role?.id)
|
||||
if (!roleId) throw new Error('无主数据权限角色未返回ID')
|
||||
resource.permissionProbe = { roleId, membershipId: null, roleStatus: 'ACTIVE', userStatus: null }
|
||||
|
||||
const phone = await context.findAvailablePhone(MODULE)
|
||||
const username = `p4_md_deny_${suffix}`.slice(0, 64).toLowerCase()
|
||||
const password = `P4md${suffix.slice(-8)}A1!`
|
||||
const userResponse = await context.request('POST', '/api/platform/v1/iam/users', {
|
||||
username, displayName: `四期主数据拒绝-${suffix}`, phone,
|
||||
email: `${username}@example.com`, tenantId: '0', roleIds: [roleId], password, status: 'ACTIVE',
|
||||
})
|
||||
const user = requireData(context, '创建无主数据权限用户', userResponse)
|
||||
const membershipId = asId(user?.membershipId)
|
||||
if (!membershipId) throw new Error('无主数据权限用户未返回membershipId')
|
||||
resource.permissionProbe.membershipId = membershipId
|
||||
resource.permissionProbe.userStatus = 'ACTIVE'
|
||||
|
||||
const loginResponse = await context.request('POST', '/api/auth/login', { username, password }, '')
|
||||
const login = requireData(context, '无主数据权限用户登录', loginResponse)
|
||||
if (!login?.overseasAccessToken) throw new Error('无主数据权限用户未签发海外Token')
|
||||
const denied = await context.requestOverseas('GET',
|
||||
`${OVERSEAS_BASE}/overseas-quotes?page=1&size=1`, undefined, {}, login.overseasAccessToken)
|
||||
assertForbidden(context, '无quote:list权限访问海外报价被拒绝', denied)
|
||||
}
|
||||
|
||||
async function cleanupPartnerWarehouse(context, resource) {
|
||||
@@ -355,16 +433,33 @@ async function cleanupVersioned(context, resource, key, path, extract) {
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanupPermissionProbe(context, resource) {
|
||||
const probe = resource.permissionProbe
|
||||
if (!probe) return
|
||||
if (probe.membershipId && probe.userStatus !== 'DISABLED') {
|
||||
const response = await context.request('PUT', `/api/platform/v1/iam/users/${probe.membershipId}/status`, { status: 'DISABLED' })
|
||||
context.apiSuccess(MODULE, '无权限探针用户finally停用', response, `membershipId=${probe.membershipId}`, BLOCKER)
|
||||
if (success(response)) probe.userStatus = 'DISABLED'
|
||||
}
|
||||
if (probe.roleId && probe.roleStatus !== 'DISABLED') {
|
||||
const response = await context.request('PUT', `/api/platform/v1/iam/roles/${probe.roleId}/status`, { status: 'DISABLED' })
|
||||
context.apiSuccess(MODULE, '无权限探针角色finally停用', response, `roleId=${probe.roleId}`, BLOCKER)
|
||||
if (success(response)) probe.roleStatus = 'DISABLED'
|
||||
}
|
||||
}
|
||||
|
||||
export async function run(context) {
|
||||
const resource = { marker: context.marker }
|
||||
context.createdResources.phase4OverseasMasterData = resource
|
||||
try {
|
||||
await continueLifecycle(context, '海外报价', async () => {
|
||||
await captureWarehouseOptionsBeforeCreate(context, resource)
|
||||
await createAndVerifyPartnerWarehouse(context, resource)
|
||||
await quoteLifecycle(context, resource)
|
||||
})
|
||||
await continueLifecycle(context, '月度汇率', () => exchangeLifecycle(context, resource))
|
||||
await continueLifecycle(context, '车辆主数据', () => vehicleLifecycle(context, resource))
|
||||
await continueLifecycle(context, 'canonical权限拒绝', () => verifyCanonicalPermissionDenial(context, resource))
|
||||
} finally {
|
||||
await safeCleanup(context, '海外报价', async () => {
|
||||
if (!resource.quote?.id) return
|
||||
@@ -382,5 +477,6 @@ export async function run(context) {
|
||||
await cleanupVersioned(context, resource, 'vehicle', path, (value) => value)
|
||||
})
|
||||
await safeCleanup(context, '合作仓', () => cleanupPartnerWarehouse(context, resource))
|
||||
await safeCleanup(context, 'canonical权限探针', () => cleanupPermissionProbe(context, resource))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user