This commit is contained in:
2026-10-20 14:22:55 +08:00
parent bf2508b6d6
commit e0c0316487
11 changed files with 681 additions and 5 deletions
@@ -0,0 +1,17 @@
package com.baiyue.lastleg.dispatch.repository;
import com.baiyue.lastleg.dispatch.mapper.DispatchBolMapper;
import org.springframework.stereotype.Repository;
import java.util.List;
/** BOL专用只读事实入口,应用服务不直接持有Mapper。 */
@Repository
public class DispatchBolRepository {
private final DispatchBolMapper mapper;
/** 装配同一次事务使用的来源及分配读取。 */
public DispatchBolRepository(DispatchBolMapper mapper){this.mapper=mapper;}
/** 返回完整明细身份、原调度箱数及逐来源打印资料。 */
public List<DispatchBolMapper.Cargo> cargo(long dispatchId){return mapper.findWholeDispatch(dispatchId);}
/** 仅正式FBA预约分配有外部预约号;客户仓内部DISPATCH批次不伪造约号。 */
public List<DispatchBolMapper.Allocation> allocations(long dispatchId){return mapper.findAppointmentAllocations(dispatchId);}
}
+6 -5
View File
@@ -467,12 +467,13 @@ Client 全量为单文件,新增的客户账单、海外仓自助、跨境采
已知待验收缺口(不计作已覆盖通过):
- `phase4-overseas-dispatch` 要求两个 FBA 目的地及同目的地双实托;默认主链当前只产生单目的地(9/23 证据 `destinations=ABE4`),前置不足按 `notRun` 阻断,报告中 `dispatchStatus=NOT_RUN`,并连带 `phase5-overseas-fulfillment` 阻断。补齐主链多目的仓样本后重跑。
- `appointment-work` 写命令(`SAVE_FOLLOWUP`、`CONFIRM_ROUTE`、`SAVE_REQUIREMENT`)需要本轮可编辑工作项与操作人 ID。
- `self-operated-analytics` 数据采集任务与重试需要领星账号和真实采集批次。
- `fleet-management` 车次写命令需要本轮排车、车辆与司机资源。
- Client 海外仓贴标、仓储转运、货物废弃和退货换标的创建链路需要平台海外 PDA 链产出的库存;当前只覆盖客户范围列表和退货预报写链路。
- `phase4-overseas-dispatch`:主链已内置 `FBA-SECOND`(`destinationIndex=1`)强制第二 FBA 目的地;真实 HTTP 仍需 PDA 作业员凭据才能展开该变体。未验证前按 `notRun` 阻断并标记 `dispatchStatus=NOT_RUN`,连带 `phase5-overseas-fulfillment`。
- `appointment-work` 写命令(`SAVE_FOLLOWUP`、`CONFIRM_ROUTE`、`SAVE_REQUIREMENT`)需要本轮可编辑工作项与操作人 ID;真实 HTTP 依赖 PDA 链产生工作事项。
- `self-operated-analytics` 数据采集任务与重试需要领星账号和真实采集批次;2026-10-20 空参 HTTP 返回 `B0001`,已按非阻断缺口记录。
- `fleet-management` 只读(车次、出车记录、车队报价、拼车需求)已通过真实 HTTP;车次写命令仍需要本轮排车、车辆与司机资源。
- Client 海外仓只读列表与退货预报写链路已通过真实 HTTP(2026-10-20 Client 全量 406 项通过、2 项跳过);贴标、仓储转运、货物废弃和退货换标的创建链路仍需要平台海外 PDA 链产出的库存。
- `pda-releases` 真实 APK/WGT 上传、发布和停发需人工准备有效安装包。
- Client 入库计划与派送单详情在新建客户无数据时跳过,需先由平台海外链产出单据后定向复核。
## 本地离线合同运行边界
@@ -0,0 +1,92 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { readFileSync } from 'node:fs'
import { run } from './appointment-work.mjs'
const entry = readFileSync(new URL('../admin-test-env-acceptance.mjs', import.meta.url), 'utf8')
const moduleSource = readFileSync(new URL('./appointment-work.mjs', import.meta.url), 'utf8')
function fakeContext(overrides = {}) {
const calls = []
const gaps = []
const steps = []
const context = {
marker: 'ADMIN-API-OFFLINE',
calls,
gaps,
steps,
async request(method, path, body) {
calls.push({ method, path, body })
const url = new URL(path, 'https://offline.invalid')
if (url.pathname.endsWith('/calendar')) {
const from = url.searchParams.get('from')
const to = url.searchParams.get('to')
return { http: 200, body: { code: '200', data: { from, to, total: 2, days: [
{ date: from, total: 1, typeCounts: { DELIVERY: 1 } },
{ date: to, total: 1, typeCounts: { PICKUP: 1 } },
] } } }
}
if (url.pathname.endsWith('/summary')) {
return { http: 200, body: { code: '200', data: { all: 2, followToday: 1, overdue: 0, undated: 1, review: 0 } } }
}
if (url.pathname.endsWith('/items')) {
return { http: 200, body: { code: '200', data: overrides.emptyPage
? { records: [], page: 1, size: 20, total: 0, pages: 0 }
: { records: [{ workItemKey: 'WORK-1', type: 'DELIVERY', flow: 'TRANSFER' }], page: 1, size: 20, total: 1, pages: 1 } } }
}
if (url.pathname.includes('/items/WORK-1')) {
return { http: 200, body: { code: '200', data: {
item: { workItemKey: 'WORK-1', type: 'DELIVERY', flow: 'TRANSFER', containers: [{ containerBookingId: 'C-1' }], allowedActions: [] },
references: [], history: [],
} } }
}
if (url.pathname.endsWith('/commands')) {
return { http: 200, body: { code: 'A0404', message: '工作事项不存在' } }
}
throw new Error(`unexpected offline request ${method} ${path}`)
},
apiSuccess(module, name, response) {
assert.equal(response.http, 200)
assert.equal(String(response.body?.code), '200')
steps.push({ module, name, passed: true })
return response.body.data
},
assert(module, name, response, condition, evidence) {
assert.equal(Boolean(condition), true, `${name}: ${evidence}`)
steps.push({ module, name, passed: true })
return true
},
addCoverageGap(module, scenario, name, reason, category, metadata) {
gaps.push({ module, scenario, name, reason, category, metadata })
},
}
return context
}
test('预约工作模块在默认全量中注册且只读查询官方工作事项接口', () => {
assert.match(entry, /const CONFIGURED_MODULES = \[[^\n]*'appointment-work'/)
assert.match(moduleSource, /\/api\/platform\/v1\/last-leg\/appointment-work/)
for (const path of ['/calendar', '/summary', '/items', '/commands']) assert.match(moduleSource, new RegExp(path.replace('/', '\\/')))
assert.match(moduleSource, /command: 'SAVE_FOLLOWUP'/)
assert.doesNotMatch(moduleSource, /this\.addStep|writeFileSync|execSync/)
})
test('预约工作只读链按日历、汇总、分页、详情顺序调用,唯一写入是不存在事项的拒绝命令', async () => {
const context = fakeContext()
await run(context)
assert.deepEqual(context.calls.map((row) => row.method), ['GET', 'GET', 'GET', 'GET', 'POST'])
assert.ok(context.calls[0].path.includes('/calendar?'))
assert.ok(context.calls[1].path.endsWith('/summary'))
assert.ok(context.calls[2].path.includes('/items?view=ALL&page=1&size=20'))
assert.ok(context.calls[3].path.includes('/items/WORK-1'))
assert.ok(context.calls[4].path.includes('/items/FULL-ACCEPTANCE-MISSING-WORK-ITEM/commands'))
assert.equal(context.calls[4].body.command, 'SAVE_FOLLOWUP')
assert.ok(context.gaps.some((gap) => gap.name === '预约写命令正向链路' && gap.metadata.blocking === false))
})
test('无本轮工作事项时记录非阻断覆盖缺口而不是虚构详情或命令结果', async () => {
const context = fakeContext({ emptyPage: true })
await run(context)
assert.equal(context.calls.length, 3)
assert.ok(context.gaps.some((gap) => gap.name === '预约工作项详情与命令' && gap.metadata.blocking === false))
})
@@ -0,0 +1,82 @@
function count(value) {
const number = Number(value)
return Number.isFinite(number) ? number : NaN
}
function shanghaiDate(offsetDays) {
return new Intl.DateTimeFormat('en-CA', {
timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit',
}).format(new Date(Date.now() + offsetDays * 86_400_000))
}
export async function run(context) {
const module = 'appointment-work'
const base = '/api/platform/v1/last-leg/appointment-work'
const from = shanghaiDate(-7)
const to = shanghaiDate(7)
const calendarResponse = await context.request('GET', `${base}/calendar?from=${from}&to=${to}`)
const calendar = context.apiSuccess(module, '查询预约工作日历', calendarResponse)
const days = Array.isArray(calendar?.days) ? calendar.days : []
const dayTotal = days.reduce((sum, day) => sum + Number(day?.total || 0), 0)
context.assert(module, '日历区间、日汇总与总数守恒', calendarResponse,
calendar?.from === from && calendar?.to === to && count(calendar?.total) === dayTotal,
`from=${calendar?.from || 'missing'},to=${calendar?.to || 'missing'},total=${calendar?.total ?? 'missing'},dayTotal=${dayTotal}`)
for (const day of days.slice(0, 3)) {
const typeTotal = Object.values(day?.typeCounts || {}).reduce((sum, value) => sum + Number(value || 0), 0)
context.assert(module, `日历日 ${day?.date} 类型汇总不超过当日总数`, calendarResponse,
Number(day?.total || 0) >= typeTotal,
`date=${day?.date || 'missing'},total=${day?.total ?? 'missing'},typeTotal=${typeTotal}`)
}
const summaryResponse = await context.request('GET', `${base}/summary`)
const summary = context.apiSuccess(module, '查询预约工作汇总', summaryResponse)
const summaryFields = ['all', 'followToday', 'overdue', 'undated', 'review']
context.assert(module, '工作汇总五项计数完整', summaryResponse,
summaryFields.every((field) => Number.isFinite(Number(summary?.[field]))),
summaryFields.map((field) => `${field}=${summary?.[field] ?? 'missing'}`).join(','))
const pageResponse = await context.request('GET', `${base}/items?view=ALL&page=1&size=20`)
const page = context.apiSuccess(module, '查询预约工作事项首页', pageResponse)
const records = Array.isArray(page?.records) ? page.records : []
const total = count(page?.total)
context.assert(module, '工作事项分页完整且不截断', pageResponse,
count(page?.page) === 1 && count(page?.size) === 20 && Number.isFinite(total)
&& records.length === Math.min(20, total) && count(page?.pages) === Math.ceil(total / 20),
`page=${page?.page},size=${page?.size},total=${page?.total},records=${records.length},pages=${page?.pages}`)
const first = records[0]
if (!first) {
context.addCoverageGap(module, 'appointment-work', '预约工作项详情与命令',
'本轮主链未产生预约工作事项;需先完成订单、柜、船节点或排车链后重跑', 'not-implemented',
{ blocking: false })
return
}
const detailResponse = await context.request('GET', `${base}/items/${encodeURIComponent(first.workItemKey)}`)
const detail = context.apiSuccess(module, '查询当前工作事项详情', detailResponse)
context.assert(module, '工作详情包含身份、来源与可执行动作', detailResponse,
detail?.item?.workItemKey === first.workItemKey
&& ['ROUTE', 'PICKUP', 'WAREHOUSE_RECEIPT', 'DELIVERY', 'TRANSPORT', 'EMPTY_RETURN'].includes(detail?.item?.type)
&& ['UNKNOWN', 'TRANSFER', 'DIRECT'].includes(detail?.item?.flow)
&& Array.isArray(detail?.item?.containers) && detail.item.containers.length > 0
&& Array.isArray(detail?.item?.allowedActions) && Array.isArray(detail?.references)
&& Array.isArray(detail?.history),
`key=${detail?.item?.workItemKey},type=${detail?.item?.type},flow=${detail?.item?.flow},`
+ `actions=${detail?.item?.allowedActions?.length ?? 'missing'}`)
const invalidKey = 'FULL-ACCEPTANCE-MISSING-WORK-ITEM'
const invalidResponse = await context.request('POST', `${base}/items/${encodeURIComponent(invalidKey)}/commands`, {
command: 'SAVE_FOLLOWUP',
payload: { ownerId: '1', followupDate: to, note: `missing-${context.marker}` },
expectedVersion: 1,
idempotencyKey: `${context.marker}-missing-work`,
})
context.assert(module, '不存在的工作事项命令被拒绝', invalidResponse,
invalidResponse.http === 200 && ['A0403', 'A0404'].includes(String(invalidResponse.body?.code)),
`HTTP=${invalidResponse.http},code=${invalidResponse.body?.code ?? 'missing'}`)
context.addCoverageGap(module, 'appointment-work', '预约写命令正向链路',
'当前仅验证拒绝路径;SAVE_FOLLOWUP/CONFIRM_ROUTE/SAVE_REQUIREMENT 需要本轮可编辑工作项与操作人 ID,待下一批接入',
'not-implemented', { blocking: false })
}
+36
View File
@@ -0,0 +1,36 @@
function id(value) { return value == null ? '' : String(value) }
export async function run(context) {
const module = 'audit'
const base = '/api/platform/v1/audit/logs'
const pageResponse = await context.request('GET', `${base}?page=1&size=20`)
const page = context.apiSuccess(module, '查询平台操作日志首页', pageResponse)
const records = Array.isArray(page?.records) ? page.records : []
const total = Number(page?.total)
context.assert(module, '日志分页完整且标识字段齐全', pageResponse,
Number(page?.page) === 1 && Number(page?.size) === 20 && Number.isFinite(total)
&& records.length === Math.min(20, total)
&& records.every((row) => id(row.id) && row.eventType && row.occurredAt),
`page=${page?.page},size=${page?.size},total=${page?.total},records=${records.length}`)
context.assert(module, '日志列表不返回变更前后快照', pageResponse,
records.every((row) => !Object.hasOwn(row, 'beforeData') && !Object.hasOwn(row, 'afterData')),
`records=${records.length}`)
if (records[0]) {
const detailResponse = await context.request('GET', `${base}/${encodeURIComponent(records[0].id)}`)
const detail = context.apiSuccess(module, '查询当前日志详情', detailResponse)
context.assert(module, '日志详情与列表身份一致', detailResponse,
id(detail?.id) === id(records[0].id) && Object.hasOwn(detail || {}, 'httpMethod'),
`id=${detail?.id},eventType=${detail?.eventType}`)
} else {
context.addCoverageGap(module, 'audit', '日志详情',
'测试环境当前没有审计日志;先执行本轮业务写入后重跑', 'not-implemented', { blocking: false })
}
const missingResponse = await context.request('GET', `${base}/999999999999999`)
context.assert(module, '不存在的日志详情被拒绝', missingResponse,
(missingResponse.http === 200 && ['A0403', 'A0404'].includes(String(missingResponse.body?.code)))
|| missingResponse.http === 404,
`HTTP=${missingResponse.http},code=${missingResponse.body?.code ?? 'missing'}`)
}
@@ -0,0 +1,90 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { readFileSync } from 'node:fs'
import { run as runFleet } from './fleet-management.mjs'
import { run as runAnalytics } from './self-operated-analytics.mjs'
const entry = readFileSync(new URL('../admin-test-env-acceptance.mjs', import.meta.url), 'utf8')
function context(handler) {
const calls = []
const gaps = []
return {
marker: 'ADMIN-API-OFFLINE',
calls,
gaps,
async request(method, path) {
calls.push({ method, path })
return handler(method, path)
},
apiSuccess(_module, _name, response) {
assert.equal(response.http, 200)
assert.equal(String(response.body?.code), '200')
return response.body.data
},
assert(_module, name, _response, condition, evidence) {
assert.equal(Boolean(condition), true, `${name}: ${evidence}`)
return true
},
addCoverageGap(module, scenario, name, reason, category, metadata) {
gaps.push({ module, scenario, name, reason, category, metadata })
},
}
}
test('车队管理模块注册为默认模块并只读核对车次、出车、报价与拼车需求', async () => {
assert.match(entry, /const CONFIGURED_MODULES = \[[^\n]*'fleet-management'/)
const tripRow = { tripNo: 'TRIP-1', status: 'PLANNED', recordVersion: 0 }
const ctx = context((method, path) => {
if (path.endsWith('/fleet/trips/FLEET-MISSING-TRIP')) {
return { http: 200, body: { code: 'A0404', message: '车次不存在' } }
}
if (path.includes('/fleet/trips/TRIP-1')) {
return { http: 200, body: { code: '200', data: {
trip: tripRow, dispatches: [], loadingReview: { recordVersion: null }, previousDispatches: [] } } }
}
if (path.includes('/fleet/trips')) {
return { http: 200, body: { code: '200', data: { records: [tripRow], page: 1, size: 20, total: 1, pages: 1 } } }
}
if (path.includes('/fleet/operations')) {
return { http: 200, body: { code: '200', data: {
page: { records: [], page: 1, size: 20, total: 0, pages: 0 },
summary: { trips: 0, inTransit: 0, completed: 0, physicalPallets: 0, palletSpaces: 0, ownPackages: 0,
externalPackages: 0, externalAgreedAmounts: [], costs: { visible: true, unrecordedTrips: null, unallocatedTrips: null, totals: [] } },
charts: { physicalPallets: [], destinationTrips: [], externalAgreedAmounts: [], externalPalletRates: [] },
} } }
}
if (path.includes('/fleet/quotes')) {
return { http: 200, body: { code: '200', data: { records: [{ quoteNo: 'Q-1', status: 'ACTIVE', recordVersion: '0' }], page: 1, size: 20, total: 1, pages: 1 } } }
}
if (path.includes('/fleet/capacity-offers')) {
return { http: 200, body: { code: '200', data: { records: [{ status: 'OPEN' }], page: 1, size: 20, total: 1, pages: 1 } } }
}
if (path.endsWith('/fleet/trips/FLEET-MISSING-TRIP')) {
return { http: 200, body: { code: 'A0404', message: '车次不存在' } }
}
throw new Error(`unexpected ${method} ${path}`)
})
await runFleet(ctx)
assert.deepEqual(ctx.calls.map((row) => row.method), ['GET', 'GET', 'GET', 'GET', 'GET', 'GET'])
assert.ok(ctx.calls[0].path.includes('/fleet/trips?page=1&size=20'))
assert.ok(ctx.calls[2].path.includes('/fleet/operations?page=1&size=20&from='))
assert.ok(ctx.calls[4].path.includes('/fleet/capacity-offers'))
assert.ok(ctx.gaps.some((gap) => gap.name === '车次写命令' && gap.metadata.blocking === false))
})
test('自营分析模块注册为默认模块,空参不可用时记录缺口而不是虚构看板', async () => {
assert.match(entry, /const CONFIGURED_MODULES = \[[^\n]*'self-operated-analytics'/)
const unavailable = context(() => ({ http: 200, body: { code: 'A0400', message: '缺少采集账号' } }))
await runAnalytics(unavailable)
assert.equal(unavailable.calls.length, 2)
assert.ok(unavailable.gaps.some((gap) => gap.name === '经营看板' && gap.metadata.blocking === false))
assert.ok(unavailable.gaps.some((gap) => gap.name === '货主目录' && gap.metadata.blocking === false))
const available = context((_method, path) => path.endsWith('/dashboard')
? { http: 200, body: { code: '200', data: { metrics: [], trend: [] } } }
: { http: 200, body: { code: '200', data: { records: [], page: 1, size: 20, total: 0, pages: 0 } } })
await runAnalytics(available)
assert.equal(available.gaps.some((gap) => gap.name === '经营看板'), false)
assert.ok(available.gaps.some((gap) => gap.category === 'external-third-party' && gap.metadata.blocking === false))
})
@@ -0,0 +1,79 @@
function count(value) {
const number = Number(value)
return Number.isFinite(number) ? number : NaN
}
function id(value) { return value == null ? '' : String(value) }
function isoInstant(offsetDays, endOfDay = false) {
const day = new Date(Date.now() + offsetDays * 86_400_000).toISOString().slice(0, 10)
return `${day}T${endOfDay ? '23:59:59' : '00:00:00'}Z`
}
function pageOf(value) {
return value && Array.isArray(value.records) && Number.isFinite(count(value.total))
}
export async function run(context) {
const module = 'fleet-management'
const base = '/api/platform/v1/last-leg/fleet'
const tripsResponse = await context.request('GET', `${base}/trips?page=1&size=20`)
const trips = context.apiSuccess(module, '查询车次计划列表', tripsResponse)
context.assert(module, '车次分页与状态字段完整', tripsResponse,
pageOf(trips) && trips.records.every((row) => id(row.tripNo) && row.status && Number.isFinite(count(row.recordVersion))),
`total=${trips?.total ?? 'missing'},records=${trips?.records?.length ?? 'missing'}`)
const tripNo = trips?.records?.[0]?.tripNo
if (tripNo) {
const detailResponse = await context.request('GET', `${base}/trips/${encodeURIComponent(tripNo)}`)
const detail = context.apiSuccess(module, '查询当前车次详情', detailResponse)
context.assert(module, '车次详情包含四页签所需事实', detailResponse,
id(detail?.trip?.tripNo) === id(tripNo)
&& detail?.trip?.status
&& Array.isArray(detail?.dispatches)
&& detail?.loadingReview != null,
`tripNo=${detail?.trip?.tripNo},status=${detail?.trip?.status},dispatches=${detail?.dispatches?.length ?? 'missing'}`)
} else {
context.addCoverageGap(module, 'fleet-management', '车次详情',
'当前没有车次样本;先由海外排车/车队入口创建本轮车次后重跑', 'not-implemented', { blocking: false })
}
const operationsResponse = await context.request('GET',
`${base}/operations?page=1&size=20&from=${isoInstant(-30)}&to=${isoInstant(0, true)}`)
const operations = context.apiSuccess(module, '查询出车记录', operationsResponse)
context.assert(module, '出车记录分页、汇总与图表结构完整', operationsResponse,
pageOf(operations?.page)
&& Number.isFinite(count(operations?.summary?.trips))
&& Array.isArray(operations?.summary?.externalAgreedAmounts)
&& operations?.charts && Array.isArray(operations.charts.physicalPallets),
`trips=${operations?.summary?.trips ?? 'missing'},pageTotal=${operations?.page?.total ?? 'missing'}`)
context.assert(module, '未登记或未分摊成本不按零展示', operationsResponse,
operations?.summary?.costs == null
|| (typeof operations.summary.costs.visible === 'boolean'
&& (operations.summary.costs.unrecordedTrips == null || Number.isFinite(count(operations.summary.costs.unrecordedTrips)))),
JSON.stringify(operations?.summary?.costs || {}).slice(0, 200))
const quotesResponse = await context.request('GET', `${base}/quotes?page=1&size=20`)
const quotes = context.apiSuccess(module, '查询车队报价', quotesResponse)
context.assert(module, '车队报价分页字段完整', quotesResponse,
pageOf(quotes) && quotes.records.every((row) => id(row.quoteNo) && row.status != null
&& (row.recordVersion == null || Number.isFinite(count(row.recordVersion)))),
`total=${quotes?.total ?? 'missing'},records=${quotes?.records?.length ?? 'missing'}`)
const offersResponse = await context.request('GET', `${base}/capacity-offers?page=1&size=20`)
const offers = context.apiSuccess(module, '查询内部拼车需求池', offersResponse)
context.assert(module, '拼车需求分页字段完整', offersResponse,
pageOf(offers) && offers.records.every((row) => row.status != null),
`total=${offers?.total ?? 'missing'},records=${offers?.records?.length ?? 'missing'}`)
const missingResponse = await context.request('GET', `${base}/trips/FLEET-MISSING-TRIP`)
context.assert(module, '不存在的车次详情被拒绝', missingResponse,
(missingResponse.http === 200 && ['A0403', 'A0404'].includes(String(missingResponse.body?.code)))
|| missingResponse.http === 404,
`HTTP=${missingResponse.http},code=${missingResponse.body?.code ?? 'missing'}`)
context.addCoverageGap(module, 'fleet-management', '车次写命令',
'车次计划/分配/审核/发车/结案依赖本轮排车与车辆资源;本模块只读核对列表、详情、出车记录、报价和拼车需求',
'not-implemented', { blocking: false })
}
@@ -0,0 +1,114 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { readFileSync } from 'node:fs'
import { run as runPdaReleases } from './pda-releases.mjs'
import { run as runAudit } from './audit.mjs'
import { run as runSailingCalendar } from './sailing-calendar.mjs'
const entry = readFileSync(new URL('../admin-test-env-acceptance.mjs', import.meta.url), 'utf8')
function context(handler) {
const calls = []
const gaps = []
return {
marker: 'ADMIN-API-OFFLINE',
calls,
gaps,
async request(method, path, body) {
calls.push({ method, path, body })
return handler(method, path, body)
},
async requestMultipart(method, path, form) {
calls.push({ method, path, multipart: form })
return handler(method, path, form)
},
apiSuccess(_module, _name, response) {
assert.equal(response.http, 200)
assert.equal(String(response.body?.code), '200')
return response.body.data
},
assert(_module, name, _response, condition, evidence) {
assert.equal(Boolean(condition), true, `${name}: ${evidence}`)
return true
},
addCoverageGap(module, scenario, name, reason, category, metadata) {
gaps.push({ module, scenario, name, reason, category, metadata })
},
}
}
test('PDA 版本模块注册为默认模块并只读核对双端发布列表', async () => {
assert.match(entry, /const CONFIGURED_MODULES = \[[^\n]*'pda-releases'/)
const releases = (app) => [{
id: '1', app, appid: `__UNI__${app}`, packageType: 'apk', versionName: '1.0.0', versionCode: 1,
minAppVersionCode: 1, mandatory: false, releaseNotes: 'offline', status: 'PUBLISHED', current: true,
sha256: 'a'.repeat(64), sizeBytes: 1024, downloadUrl: '/download', createdAt: '2026-10-01T00:00:00Z',
publishedAt: '2026-10-01T00:00:00Z',
}]
const ctx = context((method, path) => {
if (method === 'GET') return { http: 200, body: { code: '200', data: releases(path.endsWith('/domestic') ? 'domestic' : 'overseas') } }
if (method === 'POST' && path.includes('/publish')) return { http: 200, body: { code: 'A0404', message: '不存在' } }
if (method === 'POST' && path.includes('/withdraw')) return { http: 200, body: { code: 'A0404', message: '不存在' } }
return { http: 400, body: { code: 'A0400', message: '文件不能为空' } }
})
await runPdaReleases(ctx)
assert.deepEqual(ctx.calls.map((row) => row.method), ['GET', 'GET', 'POST', 'POST', 'POST'])
assert.ok(ctx.calls[0].path.endsWith('/domestic'))
assert.ok(ctx.calls[1].path.endsWith('/overseas'))
assert.ok(ctx.gaps.some((gap) => gap.name === '真实 APK/WGT 上传、发布、停发与下载' && gap.metadata.blocking === false))
})
test('平台审计模块注册为默认模块,列表不泄漏快照且详情身份一致', async () => {
assert.match(entry, /const CONFIGURED_MODULES = \[[^\n]*'audit'/)
const row = { id: '7', eventId: 'e-7', eventType: 'ORDER_SUBMIT', resourceType: 'ORDER', resourceId: '1',
accountId: '0', operatorTenantId: '0', userId: '1', occurredAt: '2026-10-01T00:00:00Z', success: true }
const detailCtx = context((method, path) => {
if (path.endsWith('/999999999999999')) return { http: 200, body: { code: 'A0404', message: '不存在' } }
if (path.endsWith('/7')) return { http: 200, body: { code: '200', data: { ...row, httpMethod: 'POST' } } }
return { http: 200, body: { code: '200', data: { records: [row], page: 1, size: 20, total: 1, pages: 1 } } }
})
const withSnapshot = context((method, path) => {
if (path.includes('/audit/logs/')) return { http: 200, body: { code: 'A0404', message: '不存在' } }
return { http: 200, body: { code: '200', data: { records: [{ ...row, beforeData: '{}' }], page: 1, size: 20, total: 1, pages: 1 } } }
})
await runAudit(detailCtx)
assert.equal(detailCtx.calls[0].method, 'GET')
assert.ok(detailCtx.calls[0].path.includes('/audit/logs?page=1&size=20'))
assert.equal(detailCtx.calls[1].path, '/api/platform/v1/audit/logs/7')
assert.ok(detailCtx.calls[2].path.endsWith('/999999999999999'))
await assert.rejects(runAudit(withSnapshot), /不返回变更前后快照/)
})
test('船期日历模块注册为默认模块并核对月历、渠道、模板与异常分配', async () => {
assert.match(entry, /const CONFIGURED_MODULES = \[[^\n]*'sailing-calendar'/)
const summary = {
actualReceivedCbm: 1, catchableForecastCbm: 2, riskUnmatchedCbm: 0, bookedContainerCount: 1,
recommendedContainerCount: 1, pendingContainerCount: 0, lateBatchCount: 0, overCapacity: false,
planningCapacityCbm: 10, bookedPlanningCapacityCbm: 5, catchableReceivedCbm: 1, lateReceivedCbm: 0,
snapshotAt: null, sailingCount: 1,
}
const sailing = {
id: '11', sailingNo: 'SAIL-11', warehouseId: '1', channelCode: 'SEA', channelName: '海运', templateId: null,
channelId: null, etaAt: '2026-10-20T00:00:00', sourceType: 'MANUAL', plannedCapacityCbm: 10,
volumeSummary: summary, version: 0,
}
const ctx = context((method, path) => {
if (path.includes('/calendar')) return { http: 200, body: { code: '200', data: {
records: [sailing], summary, filterOptions: { warehouses: [], channels: [], departurePorts: [], destinationPorts: [] } } } }
if (path.includes('/sailing-channels')) return { http: 200, body: { code: '200', data: [{ id: '1', channelCode: 'SEA', defaultCutoffDays: 3, version: 0 }] } }
if (path.includes('/sailing-templates')) return { http: 200, body: { code: '200', data: { records: [{ id: '1', channelId: '1', warehouseId: '1', weekdays: [1, 2], cutoffDays: 3, version: 0 }] } } }
if (path.includes('/sailing-assignments/exceptions')) return { http: 200, body: { code: '200', data: { records: [{ assignmentId: '1', volumeCbm: 1 }], total: 1 } } }
if (path.endsWith('/11')) return { http: 200, body: { code: '200', data: {
sailing: { ...sailing, volumeSummary: summary }, histories: [] } } }
if (path.endsWith('/999999999999999')) return { http: 200, body: { code: 'A0404', message: '不存在' } }
throw new Error(`unexpected ${method} ${path}`)
})
await runSailingCalendar(ctx)
assert.deepEqual(ctx.calls.map((row) => row.method), ['GET', 'GET', 'GET', 'GET', 'GET', 'GET'])
assert.ok(ctx.calls[0].path.includes('/sailings/calendar?month='))
assert.ok(ctx.calls[1].path.endsWith('/sailing-channels'))
assert.ok(ctx.calls[2].path.includes('/sailing-templates?page=1&size=200'))
assert.ok(ctx.calls[3].path.includes('/sailing-assignments/exceptions'))
assert.ok(ctx.calls[4].path.endsWith('/11'))
assert.ok(ctx.calls[5].path.endsWith('/999999999999999'))
})
@@ -0,0 +1,46 @@
function id(value) { return value == null ? '' : String(value) }
function validRelease(row) {
return row && typeof row === 'object'
&& ['domestic', 'overseas'].includes(row.app)
&& ['apk', 'wgt'].includes(row.packageType)
&& typeof row.versionName === 'string' && row.versionName.length > 0
&& Number.isSafeInteger(Number(row.versionCode))
&& ['DRAFT', 'PUBLISHED', 'WITHDRAWN'].includes(row.status)
&& typeof row.current === 'boolean'
&& /^[a-f0-9]{64}$/i.test(String(row.sha256 || ''))
&& Number.isSafeInteger(Number(row.sizeBytes)) && Number(row.sizeBytes) > 0
}
export async function run(context) {
const module = 'pda-releases'
const base = '/api/platform/v1/files/pda-releases'
for (const app of ['domestic', 'overseas']) {
const response = await context.request('GET', `${base}/${app}`)
const releases = context.apiSuccess(module, `查询 ${app} PDA 发布列表`, response)
context.assert(module, `${app} 发布列表结构与版本字段完整`, response,
Array.isArray(releases) && releases.every(validRelease),
`count=${Array.isArray(releases) ? releases.length : 'not-array'}`)
const currentCount = Array.isArray(releases) ? releases.filter((row) => row.current === true).length : -1
context.assert(module, `${app} 当前生效版本不超过一个发布类型`, response,
currentCount <= 2, `current=${currentCount}`)
}
const emptyUpload = await context.requestMultipart('POST', `${base}/domestic`, new FormData())
context.assert(module, '无安装包的上传被拒绝', emptyUpload,
emptyUpload.http >= 400 || String(emptyUpload.body?.code) !== '200',
`HTTP=${emptyUpload.http},code=${emptyUpload.body?.code ?? 'missing'}`)
const missingId = '999999999999999'
for (const action of ['publish', 'withdraw']) {
const response = await context.request('POST', `${base}/${missingId}/${action}`, {})
context.assert(module, `不存在版本 ${action} 被拒绝`, response,
response.http === 200 && ['A0403', 'A0404', 'A0409'].includes(String(response.body?.code)),
`HTTP=${response.http},code=${response.body?.code ?? 'missing'}`)
}
context.addCoverageGap(module, 'pda-releases', '真实 APK/WGT 上传、发布、停发与下载',
'脚本不上传真实安装包;需人工准备有效 domestic/overseas APK 或 WGT 后定向执行,避免伪造签名与版本号',
'not-implemented', { blocking: false })
}
@@ -0,0 +1,80 @@
function count(value) {
const number = Number(value)
return Number.isFinite(number) ? number : NaN
}
function currentMonth() {
return new Intl.DateTimeFormat('en-CA', {
timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit',
}).format(new Date()).slice(0, 7)
}
function id(value) { return value == null ? '' : String(value) }
function listOf(value) {
if (Array.isArray(value)) return value
if (Array.isArray(value?.records)) return value.records
return null
}
export async function run(context) {
const module = 'sailing-calendar'
const base = '/api/platform/v1/sailings'
const month = currentMonth()
const calendarResponse = await context.request('GET', `${base}/calendar?month=${month}`)
const calendar = context.apiSuccess(module, '查询船期月历', calendarResponse)
const records = Array.isArray(calendar?.records) ? calendar.records : []
const summary = calendar?.summary
const filterOptions = calendar?.filterOptions
context.assert(module, '月历记录、汇总与筛选选项结构完整', calendarResponse,
records.every((row) => id(row.id) && id(row.sailingNo) && 'etaAt' in row)
&& summary && Number.isFinite(count(summary.sailingCount))
&& filterOptions && Array.isArray(filterOptions.warehouses) && Array.isArray(filterOptions.channels),
`month=${month},records=${records.length},sailingCount=${summary?.sailingCount ?? 'missing'}`)
context.assert(module, '月历容量汇总均为非负有限数', calendarResponse,
['actualReceivedCbm', 'catchableForecastCbm', 'riskUnmatchedCbm'].every((field) =>
Number.isFinite(count(summary?.[field])) && count(summary?.[field]) >= 0),
JSON.stringify(summary || {}).slice(0, 200))
const channelsResponse = await context.request('GET', '/api/platform/v1/sailing-channels')
const channels = context.apiSuccess(module, '查询船期渠道资源', channelsResponse)
const channelRows = listOf(channels) ?? []
context.assert(module, '渠道列表字段完整', channelsResponse,
channelRows.every((row) => id(row.id) && id(row.channelCode) && Number.isFinite(count(row.defaultCutoffDays))),
`count=${channelRows.length}`)
const templatesResponse = await context.request('GET', '/api/platform/v1/sailing-templates?page=1&size=200')
const templates = context.apiSuccess(module, '查询船期模板', templatesResponse)
const templateRows = listOf(templates) ?? []
context.assert(module, '模板列表字段完整', templatesResponse,
templateRows.every((row) => id(row.id) && id(row.channelId) && id(row.warehouseId) && Array.isArray(row.weekdays)),
`count=${templateRows.length}`)
const exceptionsResponse = await context.request('GET', '/api/platform/v1/sailing-assignments/exceptions?page=1&size=20')
const exceptions = context.apiSuccess(module, '查询船期异常分配', exceptionsResponse)
const exceptionRows = listOf(exceptions) ?? []
context.assert(module, '异常分配列表字段完整', exceptionsResponse,
exceptionRows.every((row) => id(row.assignmentId) && Number.isFinite(count(row.volumeCbm))),
`count=${exceptionRows.length},total=${exceptions?.total ?? 'array'}`)
if (records[0]) {
const detailResponse = await context.request('GET', `${base}/${encodeURIComponent(records[0].id)}`)
const detail = context.apiSuccess(module, '查询当前船期详情', detailResponse)
context.assert(module, '船期详情包含版本与货量摘要', detailResponse,
id(detail?.sailing?.id) === id(records[0].id)
&& Number.isFinite(count(detail?.sailing?.version))
&& detail?.sailing?.volumeSummary != null
&& Array.isArray(detail?.histories),
`id=${detail?.sailing?.id},version=${detail?.sailing?.version}`)
} else {
context.addCoverageGap(module, 'sailing-calendar', '船期详情与变更历史',
'当前月份没有船期记录;需先由订舱/模板生成本轮船期后重跑', 'not-implemented', { blocking: false })
}
const missingResponse = await context.request('GET', `${base}/999999999999999`)
context.assert(module, '不存在的船期详情被拒绝', missingResponse,
(missingResponse.http === 200 && ['A0403', 'A0404'].includes(String(missingResponse.body?.code)))
|| missingResponse.http === 404,
`HTTP=${missingResponse.http},code=${missingResponse.body?.code ?? 'missing'}`)
}
@@ -0,0 +1,39 @@
function count(value) {
const number = Number(value)
return Number.isFinite(number) ? number : NaN
}
export async function run(context) {
const module = 'self-operated-analytics'
const base = '/api/platform/v1/last-leg/self-operated-analytics'
const dashboardResponse = await context.request('GET', `${base}/dashboard`)
if (dashboardResponse.http === 200 && String(dashboardResponse.body?.code) === '200') {
const dashboard = context.apiSuccess(module, '查询自营经营看板', dashboardResponse)
context.assert(module, '经营看板六项指标与趋势结构完整', dashboardResponse,
dashboard && typeof dashboard === 'object'
&& (dashboard.metrics == null || Array.isArray(dashboard.metrics))
&& (dashboard.trend == null || Array.isArray(dashboard.trend)),
`keys=${Object.keys(dashboard || {}).length}`)
} else {
context.addCoverageGap(module, 'self-operated-analytics', '经营看板',
`需要本轮采集账号与仓库上下文;当前空参请求 HTTP=${dashboardResponse.http}, code=${dashboardResponse.body?.code ?? 'missing'}`,
'not-implemented', { blocking: false })
}
const ownersResponse = await context.request('GET', `${base}/owners?page=1&size=20`)
if (ownersResponse.http === 200 && String(ownersResponse.body?.code) === '200') {
const owners = context.apiSuccess(module, '查询自营货主目录', ownersResponse)
context.assert(module, '货主目录分页字段完整', ownersResponse,
owners && Array.isArray(owners.records) && Number.isFinite(count(owners.total)),
`total=${owners?.total ?? 'missing'}`)
} else {
context.addCoverageGap(module, 'self-operated-analytics', '货主目录',
`需要本轮采集账号上下文;HTTP=${ownersResponse.http}, code=${ownersResponse.body?.code ?? 'missing'}`,
'not-implemented', { blocking: false })
}
context.addCoverageGap(module, 'self-operated-analytics', '数据采集任务与重试',
'采集任务同步/重试依赖领星外部账号与真实采集批次;本模块只读核对看板与货主目录,缺账号时记录覆盖缺口',
'external-third-party', { blocking: false })
}