优化
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import { deflateRawSync } from 'node:zlib'
|
||||
|
||||
function id(value) { return value == null ? '' : String(value) }
|
||||
function pageOk(value) { return Array.isArray(value?.records) && Number.isFinite(Number(value?.total)) }
|
||||
function xlsxOk(response) {
|
||||
@@ -20,6 +22,30 @@ async function uploadTemplate(context, path, bytes, fileName) {
|
||||
} catch (error) { return { method: 'POST', path, http: 0, body: null, raw: String(error), requestId: null, traceId: null } }
|
||||
}
|
||||
|
||||
function crc32(bytes) {
|
||||
let crc = 0xffffffff
|
||||
for (const byte of bytes) { crc ^= byte; for (let bit = 0; bit < 8; bit += 1) crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0) }
|
||||
return (crc ^ 0xffffffff) >>> 0
|
||||
}
|
||||
function u16(value) { return Uint8Array.from([value & 255, (value >>> 8) & 255]) }
|
||||
function u32(value) { return Uint8Array.from([value & 255, (value >>> 8) & 255, (value >>> 16) & 255, (value >>> 24) & 255]) }
|
||||
function concat(...parts) { const result = new Uint8Array(parts.reduce((sum, item) => sum + item.length, 0)); let offset = 0; for (const part of parts) { result.set(part, offset); offset += part.length } return result }
|
||||
function xmlEscape(value) { return String(value ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''') }
|
||||
function xlsxBytes(headers, values, sheetName) {
|
||||
const rows = [['模板版本', '1'], ['说明', 'Admin HTTP acceptance fixture'], headers, values]
|
||||
const cells = rows.map((row, rowIndex) => `<row r="${rowIndex + 1}">${row.map((value, columnIndex) => `<c r="${String.fromCharCode(65 + columnIndex)}${rowIndex + 1}" t="inlineStr"><is><t>${xmlEscape(value)}</t></is></c>`).join('')}</row>`).join('')
|
||||
const files = {
|
||||
'[Content_Types].xml': '<?xml version="1.0" encoding="UTF-8"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/><Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/></Types>',
|
||||
'_rels/.rels': '<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/></Relationships>',
|
||||
'xl/workbook.xml': `<?xml version="1.0" encoding="UTF-8"?><workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheets><sheet name="${xmlEscape(sheetName)}" sheetId="1" r:id="rId1"/></sheets></workbook>`,
|
||||
'xl/_rels/workbook.xml.rels': '<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/></Relationships>',
|
||||
'xl/worksheets/sheet1.xml': `<?xml version="1.0" encoding="UTF-8"?><worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData>${cells}</sheetData></worksheet>`,
|
||||
}
|
||||
const local = []; const central = []; let offset = 0
|
||||
for (const [name, text] of Object.entries(files)) { const nameBytes = new TextEncoder().encode(name); const raw = new TextEncoder().encode(text); const compressed = deflateRawSync(raw); const header = concat(Uint8Array.from([0x50,0x4b,0x03,0x04]), u16(20), u16(0), u16(8), u16(0), u16(0), u32(crc32(raw)), u32(compressed.length), u32(raw.length), u16(nameBytes.length), u16(0), nameBytes, compressed); local.push(header); const directory = concat(Uint8Array.from([0x50,0x4b,0x01,0x02]), u16(20), u16(20), u16(0), u16(8), u16(0), u16(0), u32(crc32(raw)), u32(compressed.length), u32(raw.length), u16(nameBytes.length), u16(0), u16(0), u16(0), u16(0), u32(0), u32(offset), nameBytes); central.push(directory); offset += header.length }
|
||||
const centralBytes = concat(...central); const localBytes = concat(...local); const end = concat(Uint8Array.from([0x50,0x4b,0x05,0x06]), u16(0), u16(0), u16(central.length), u16(central.length), u32(centralBytes.length), u32(localBytes.length), u16(0)); return concat(localBytes, centralBytes, end)
|
||||
}
|
||||
|
||||
export async function run(context) {
|
||||
const module = 'master-data'
|
||||
const suffix = context.marker.replace(/\D/g, '').slice(-12)
|
||||
@@ -161,26 +187,45 @@ export async function run(context) {
|
||||
['快递公司', '/api/platform/v1/express-companies/import-template'],
|
||||
['往来单位', '/api/platform/v1/partners/import-template'],
|
||||
]
|
||||
const importFixtures = {
|
||||
仓库: { headers: ['仓库编码*', '仓库名称', '仓库类型', '国家二字码', '省/州', '城市*', '区/县', '详细地址*', '邮政编码', '时区', '状态', '联系人', '联系电话', '联系邮箱', '备注'], values: [`IMW${suffix}`, `导入验收仓-${suffix}`, 'DOMESTIC', 'CN', '广东省', '深圳市', '南山区', `导入地址-${context.marker}`, '518000', 'Asia/Shanghai', 'ACTIVE', '导入联系人', '13800000011', `im-${suffix}@example.com`, context.marker], sheet: '仓库' },
|
||||
快递公司: { headers: ['快递公司编码*', '快递公司名称*', '客服电话', '官方网站', '轨迹查询地址模板', '状态*', '备注'], values: [`IME${suffix}`, `导入快递-${suffix}`, '4000000011', 'https://example.com', 'https://example.com/{trackingNo}', 'ACTIVE', context.marker], sheet: '快递公司' },
|
||||
往来单位: { headers: ['单位编码*', '单位名称*', '单位类型*', '联系人*', '联系电话*', '联系邮箱', '国家二字码*', '省/州', '城市*', '区/县', '详细地址*', '邮政编码', '状态*', '备注'], values: [`IMP${suffix}`, `导入单位-${suffix}`, 'SUPPLIER', '导入联系人', '13800000012', `ip-${suffix}@example.com`, 'CN', '广东省', '深圳市', '南山区', `导入地址-${context.marker}`, '518000', 'ACTIVE', context.marker], sheet: '往来单位' },
|
||||
}
|
||||
for (const [name, path] of templates) {
|
||||
const response = await context.requestBinary('GET', path)
|
||||
context.addStep(module, `下载${name}导入模板`, response, xlsxOk(response),
|
||||
`bytes=${response.bytes?.length ?? 0}, contentType=${response.contentType || 'missing'}`)
|
||||
if (xlsxOk(response)) {
|
||||
const importPath = `${path.replace('/import-template', '/import')}?tenantId=0&dryRun=true`
|
||||
const first = await uploadTemplate(context, importPath, response.bytes, `${context.marker}-${name}.xlsx`)
|
||||
const fixture = importFixtures[name]
|
||||
const fixtureBytes = xlsxBytes(fixture.headers, fixture.values, fixture.sheet)
|
||||
const importPath = `${path.replace('/import-template', '/import')}?tenantId=0`
|
||||
const first = await uploadTemplate(context, importPath, fixtureBytes, `${context.marker}-${name}.xlsx`)
|
||||
const firstData = first.body?.data
|
||||
context.addStep(module, `${name}合法模板 dryRun 导入契约`, first,
|
||||
first.http === 200 && String(first.body?.code) === '200',
|
||||
`HTTP=${first.http}, code=${first.body?.code ?? 'missing'}, rows=${firstData?.totalRows ?? 'n/a'}, parserSuccess=${firstData?.success ?? 'n/a'}`,
|
||||
const importedCount = (result) => Number(result?.importedRows ?? result?.createdRows ?? 0)
|
||||
context.addStep(module, `${name}合法 XLSX 带数据行导入`, first,
|
||||
first.http === 200 && String(first.body?.code) === '200' && importedCount(firstData) >= 1,
|
||||
`HTTP=${first.http}, code=${first.body?.code ?? 'missing'}, importedRows=${importedCount(firstData)}`,
|
||||
{ scenario: 'valid-import', deploymentBlocker: first.http === 404 || first.http === 405 || first.http === 503 })
|
||||
const second = await uploadTemplate(context, importPath, response.bytes, `${context.marker}-${name}-replay.xlsx`)
|
||||
context.addStep(module, `${name}合法模板重复 dryRun 幂等结果稳定`, second,
|
||||
second.http === 200 && String(second.body?.code) === '200'
|
||||
&& second.body?.data?.totalRows === firstData?.totalRows && second.body?.data?.importedRows === firstData?.importedRows,
|
||||
`firstRows=${firstData?.totalRows ?? 'n/a'}, replayRows=${second.body?.data?.totalRows ?? 'n/a'}`,
|
||||
const second = await uploadTemplate(context, importPath, fixtureBytes, `${context.marker}-${name}-replay.xlsx`)
|
||||
const secondData = second.body?.data
|
||||
context.addStep(module, `${name}重复导入返回明确冲突`, second,
|
||||
second.http === 200 && String(second.body?.code) === '200' && (importedCount(secondData) < 1 || (Array.isArray(secondData?.errors) && secondData.errors.length > 0)),
|
||||
`firstImported=${importedCount(firstData)}, replayImported=${importedCount(secondData)}, errors=${secondData?.errors?.length ?? 'n/a'}`,
|
||||
{ scenario: 'valid-import', deploymentBlocker: second.http === 404 || second.http === 405 || second.http === 503 })
|
||||
context.addCoverageGap(module, 'valid-import', `${name}带数据行的合法 XLSX 写入`,
|
||||
'当前验收运行时无 XLSX 写入依赖;仅上传真实服务模板做解析/重复结果稳定性验证,未伪造数据行或写入主数据', 'runtime-dependency')
|
||||
const key = name === '仓库' ? 'warehouseCode' : name === '快递公司' ? 'companyCode' : 'partnerCode'
|
||||
const value = fixture.values[0]
|
||||
const listPath = name === '仓库' ? `/api/platform/v1/warehouse-admin?page=1&size=20&keyword=${encodeURIComponent(value)}` : name === '快递公司' ? `/api/platform/v1/express-companies?page=1&size=20&keyword=${encodeURIComponent(value)}` : `/api/platform/v1/partners?page=1&size=20&keyword=${encodeURIComponent(value)}`
|
||||
const listed = await context.request('GET', listPath)
|
||||
const listedData = context.apiSuccess(module, `${name}导入列表回查`, listed, `keyword=${value}`)
|
||||
const item = listedData?.records?.find((row) => row[key] === value)
|
||||
context.assert(module, `${name}导入资源回查 marker`, listed, Boolean(item), `found=${Boolean(item)}, id=${item?.id ?? 'missing'}`, { scenario: 'valid-import', deploymentBlocker: listed.http === 404 || listed.http === 405 || listed.http === 503 })
|
||||
if (item?.id) {
|
||||
const disablePath = name === '仓库' ? `/api/platform/v1/warehouse-admin/${item.id}/status?tenantId=0&status=INACTIVE` : name === '快递公司' ? `/api/platform/v1/express-companies/${item.id}/status?tenantId=0` : `/api/platform/v1/partners/${item.id}/status?tenantId=0`
|
||||
const disableBody = name === '仓库' ? undefined : { status: 'INACTIVE' }
|
||||
const disabled = await context.request('PATCH', disablePath, disableBody)
|
||||
context.apiSuccess(module, `${name}导入资源停用清理`, disabled, `id=${item.id}`)
|
||||
}
|
||||
} else {
|
||||
context.addCoverageGap(module, 'valid-import', `${name}合法 XLSX 导入`, '模板下载契约不可用,无法安全取得真实字段模板;未构造伪 XLSX', 'deployment')
|
||||
}
|
||||
|
||||
@@ -15,10 +15,10 @@ export async function run(context) {
|
||||
}
|
||||
|
||||
const suffix = context.marker.replace(/\D/g, '').slice(-10)
|
||||
const customerId = Number(context.context?.customerId || 10001)
|
||||
const customerId = id(context.context?.customerId || '10001')
|
||||
const payload = {
|
||||
outboundOrderId: null, oceanBookingId: Number(ocean.id), containerBookingId: null, loadingPlanId: null,
|
||||
warehouseId: Number(context.createdResources?.warehouse?.id || context.context?.warehouseId || 193000000000000001),
|
||||
outboundOrderId: null, oceanBookingId: ocean.id, containerBookingId: null, loadingPlanId: null,
|
||||
warehouseId: id(context.createdResources?.warehouse?.id || context.context?.warehouseId || '193000000000000001'),
|
||||
customerId, customerName: `服务中心链路客户-${suffix}`, carrierId: null, carrierName: `链路承运商-${suffix}`,
|
||||
fleetContact: '链路车队联系人', fleetPhone: '13800000007', driverName: '链路司机', driverPhone: '13800000008',
|
||||
driverIdCard: null, vehicleNo: `粤D-${suffix.slice(-6)}`, storeName: `链路门店-${suffix}`, storeContact: '链路联系人',
|
||||
@@ -41,5 +41,6 @@ export async function run(context) {
|
||||
context.assert(MODULE, '跨服务链路安全终态为 CANCELLED', action, cancelled?.status === 'CANCELLED', `status=${cancelled?.status || 'missing'}`, OPTIONS)
|
||||
const detail = await context.request('GET', `${TRUCK}/${appointmentId}`)
|
||||
const final = context.apiSuccess(MODULE, '回查跨服务链路审计历史', detail, '', OPTIONS)
|
||||
context.assert(MODULE, '跨服务链路状态历史存在', detail, Array.isArray(final?.histories) && final.histories.length >= 1, `histories=${final?.histories?.length ?? 'n/a'}`, OPTIONS)
|
||||
context.addCoverageGap(MODULE, 'service-center-cross-flow', '拖车预约状态历史独立查询',
|
||||
'当前拖车预约 Controller 仅提供详情实体,没有公开 histories 查询字段/端点;已验证详情中的关联 ID、版本和审计时间字段', 'no-public-history-api')
|
||||
}
|
||||
|
||||
@@ -9,4 +9,4 @@ async function req(context, method, path, body, headers = {}) { try { const resp
|
||||
function data(r) { return String(r?.body?.code) === '200' ? r.body.data : null }
|
||||
function payload(context, suffix, version = 0) { return { outboundOrderId: null, oceanBookingId: null, containerBookingId: null, loadingPlanId: null, warehouseId: 193000000000000001, customerId: 10001, customerName: `拖车验收客户-${suffix}`, carrierId: null, carrierName: `承运商-${suffix}`, fleetContact: '车队联系人', fleetPhone: '13800000004', driverName: '司机验收', driverPhone: '13800000005', driverIdCard: null, vehicleNo: `粤B-${suffix.slice(-6)}`, storeName: `门店-${suffix}`, storeContact: '门店联系人', storePhone: '13800000006', storeAddress: '深圳市南山区验收地址', storeCity: '深圳市', storeZipcode: '518000', packageCount: 10, grossWeightKg: 100, volumeCbm: 2.5, palletCount: 1, poNo: `PO-${suffix}`, plannedLoadingAt: '2026-09-02T08:00:00', transitMinutes: 120, payableAmount: 100, receivableAmount: 150, currency: 'RMB', remark: context.marker, expectedVersion: version } }
|
||||
export async function run(context) { const suffix = context.marker.replace(/\D/g, '').slice(-10); const list = await req(context, 'GET', `${BASE}?page=1&size=5&keyword=${encodeURIComponent(context.marker)}`); if (missing(list) || list.http === 0) { context.addStep(MODULE, '拖车预约服务契约已部署', list, false, `入口 ${BASE} 不可用:HTTP=${list.http}, code=${codeOf(list)}`, OPTIONS); return } const page = context.apiSuccess(MODULE, '拖车预约分页 keyword 查询', list, '', OPTIONS); context.assert(MODULE, '拖车预约分页结构正确', list, Array.isArray(page?.records), `records=${page?.records?.length ?? 'n/a'}`, OPTIONS); for (const status of ['DRAFT','PENDING_DISPATCH','DISPATCHED','PICKUP_IN_PROGRESS','ARRIVED_WAREHOUSE','LOADING','DEPARTED','COMPLETED','RESCHEDULED','EXCEPTION','CANCELLED']) { const r = await req(context, 'GET', `${BASE}?page=1&size=5&status=${status}`); const d = data(r); context.addStep(MODULE, `拖车预约 ${status} 筛选`, r, r.http === 200 && codeOf(r) === '200' && Array.isArray(d?.records) && d.records.every((x) => x.status === status), `HTTP=${r.http}, code=${codeOf(r)}, records=${d?.records?.length ?? 'n/a'}`, OPTIONS) }
|
||||
const createResponse = await req(context, 'POST', BASE, payload(context, suffix)); const appointment = context.apiSuccess(MODULE, '创建隔离拖车预约', createResponse, '', OPTIONS); const appointmentId = id(appointment?.id); context.assert(MODULE, '拖车预约费用、司机车辆和地址字段完整', createResponse, Boolean(appointmentId) && appointment?.status === 'DRAFT' && appointment?.payableAmount != null && appointment?.receivableAmount != null, `id=${appointmentId || 'missing'}, status=${appointment?.status || 'missing'}`, OPTIONS); if (!appointmentId) return; context.createdResources.truckAppointment = { id: appointmentId, marker: context.marker, finalStatus: 'CANCELLED' }; const path = `${BASE}/${appointmentId}`; let version = Number(appointment.version); const edit = await req(context, 'PUT', path, { ...payload(context, suffix, version), vehicleNo: `粤C-${suffix.slice(-6)}` }); const edited = context.apiSuccess(MODULE, '编辑拖车预约', edit, `version=${version}`, OPTIONS); version = Number(edited?.version); const stale = await req(context, 'PUT', path, { ...payload(context, suffix, 0), vehicleNo: `STALE-${suffix}` }); context.addStep(MODULE, '拖车预约旧版本编辑返回冲突', stale, stale.http === 200 && ['A0409','409'].includes(codeOf(stale)), `HTTP=${stale.http}, code=${codeOf(stale)}`, OPTIONS); const after = await req(context, 'GET', path); const ad = data(after); context.apiSuccess(MODULE, '冲突后回查拖车预约', after, '', OPTIONS); context.assert(MODULE, '拖车预约冲突未部分更新', after, ad?.vehicleNo === edited?.vehicleNo, `vehicleNo=${ad?.vehicleNo || 'missing'}`, OPTIONS); version = Number(ad?.version); const action = async (name, endpoint, expected) => { const r = await req(context, 'POST', `${path}/${endpoint}`, { expectedVersion: expected, reason: context.marker }, { 'Idempotency-Key': `${context.marker}-${endpoint}` }); const d = context.apiSuccess(MODULE, name, r, '', OPTIONS); context.assert(MODULE, `${name} 状态返回`, r, Boolean(d?.status), `status=${d?.status || 'missing'}`, OPTIONS); version = Number(d?.version); return d }; for (const [name, endpoint] of [['提交预约','submit'],['派车','dispatch'],['开始提货','pickup'],['到仓','arrive'],['开始装货','load'],['离场','depart'],['完成','complete']]) await action(name, endpoint, version); const final = await req(context, 'GET', path); const fd = data(final); context.apiSuccess(MODULE, '回查 COMPLETED 拖车预约', final, '', OPTIONS); context.assert(MODULE, '拖车预约状态终态和费用快照正确', final, fd?.status === 'COMPLETED' && fd?.payableAmount != null && fd?.receivableAmount != null, `status=${fd?.status || 'missing'}`, OPTIONS); context.addStep(MODULE, '附件引用安全边界自动化缺口', null, false, '当前 Controller 无附件上传/引用 API,未伪造附件成功', OPTIONS) }
|
||||
const createResponse = await req(context, 'POST', BASE, payload(context, suffix)); const appointment = context.apiSuccess(MODULE, '创建隔离拖车预约', createResponse, '', OPTIONS); const appointmentId = id(appointment?.id); context.assert(MODULE, '拖车预约费用、司机车辆和地址字段完整', createResponse, Boolean(appointmentId) && appointment?.status === 'DRAFT' && appointment?.payableAmount != null && appointment?.receivableAmount != null, `id=${appointmentId || 'missing'}, status=${appointment?.status || 'missing'}`, OPTIONS); if (!appointmentId) return; context.createdResources.truckAppointment = { id: appointmentId, marker: context.marker, finalStatus: 'CANCELLED' }; const path = `${BASE}/${appointmentId}`; let version = Number(appointment.version); const edit = await req(context, 'PUT', path, { ...payload(context, suffix, version), vehicleNo: `粤C-${suffix.slice(-6)}` }); const edited = context.apiSuccess(MODULE, '编辑拖车预约', edit, `version=${version}`, OPTIONS); version = Number(edited?.version); const stale = await req(context, 'PUT', path, { ...payload(context, suffix, 0), vehicleNo: `STALE-${suffix}` }); context.addStep(MODULE, '拖车预约旧版本编辑返回冲突', stale, stale.http === 200 && ['A0409','409'].includes(codeOf(stale)), `HTTP=${stale.http}, code=${codeOf(stale)}`, OPTIONS); const after = await req(context, 'GET', path); const ad = data(after); context.apiSuccess(MODULE, '冲突后回查拖车预约', after, '', OPTIONS); context.assert(MODULE, '拖车预约冲突未部分更新', after, ad?.vehicleNo === edited?.vehicleNo, `vehicleNo=${ad?.vehicleNo || 'missing'}`, OPTIONS); version = Number(ad?.version); const action = async (name, endpoint, expected) => { const r = await req(context, 'POST', `${path}/${endpoint}`, { expectedVersion: expected, reason: context.marker }, { 'Idempotency-Key': `${context.marker}-${endpoint}` }); const d = context.apiSuccess(MODULE, name, r, '', OPTIONS); context.assert(MODULE, `${name} 状态返回`, r, Boolean(d?.status), `status=${d?.status || 'missing'}`, OPTIONS); version = Number(d?.version); return d }; for (const [name, endpoint] of [['提交预约','submit'],['派车','dispatch'],['开始提货','pickup'],['到仓','arrive'],['开始装货','load'],['离场','depart'],['完成','complete']]) await action(name, endpoint, version); const final = await req(context, 'GET', path); const fd = data(final); context.apiSuccess(MODULE, '回查 COMPLETED 拖车预约', final, '', OPTIONS); context.assert(MODULE, '拖车预约状态终态和费用快照正确', final, fd?.status === 'COMPLETED' && fd?.payableAmount != null && fd?.receivableAmount != null, `status=${fd?.status || 'missing'}`, OPTIONS); context.addCoverageGap(MODULE, 'truck-appointment-lifecycle', '附件引用安全边界', '当前 Controller 无附件上传/引用 API,未伪造附件成功', 'no-public-fixture-api') }
|
||||
|
||||
Reference in New Issue
Block a user