fix(phase4): harden development acceptance adapters
This commit is contained in:
@@ -30,10 +30,10 @@ const overseasGateway = normalizeOrigin(option('--overseas-gateway') || process.
|
||||
const password = acceptancePassword()
|
||||
if (!password) throw new Error('缺少 CHONGCHONG_ADMIN_ACCEPTANCE_PASSWORD 或本机开发验收凭据文件')
|
||||
|
||||
const executable = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm'
|
||||
const result = spawnSync(executable, ['exec', 'vitest', 'run', '--config', 'vitest.phase4-development.config.ts'], {
|
||||
const result = spawnSync('pnpm', ['exec', 'vitest', 'run', '--config', 'vitest.phase4-development.config.ts'], {
|
||||
cwd: process.cwd(),
|
||||
stdio: 'inherit',
|
||||
shell: process.platform === 'win32',
|
||||
env: {
|
||||
...process.env,
|
||||
ADMIN_PHASE4_LIVE: '1',
|
||||
@@ -45,4 +45,5 @@ const result = spawnSync(executable, ['exec', 'vitest', 'run', '--config', 'vite
|
||||
VITE_OVERSEAS_API_BASE_URL: `${overseasGateway}/api`,
|
||||
},
|
||||
})
|
||||
if (result.error) throw result.error
|
||||
process.exit(result.status ?? 1)
|
||||
|
||||
@@ -10,6 +10,8 @@ test('development runner requires an explicit development environment and gatewa
|
||||
assert.match(runner, /environment !== 'development'/)
|
||||
assert.match(runner, /--gateway/)
|
||||
assert.match(runner, /--overseas-gateway/)
|
||||
assert.match(runner, /shell: process\.platform === 'win32'/)
|
||||
assert.match(runner, /if \(result\.error\) throw result\.error/)
|
||||
assert.doesNotMatch(runner, /ADMIN_ACCEPTANCE_PASSWORD\s*[:=]\s*['"][^'"]+['"]/)
|
||||
})
|
||||
|
||||
|
||||
@@ -138,8 +138,8 @@ describe('overseas master data API', () => {
|
||||
const error = await exportVehicles({}).catch((reason: unknown) => reason)
|
||||
expect(error).toBeInstanceOf(ApiResponseError)
|
||||
expect(error).toMatchObject({ code: 'A0409', message: '导出条件已变化', requestId: 'request-export' })
|
||||
expect(overseasHttp.get).toHaveBeenNthCalledWith(1, '/platform/v1/last-leg/master-data/overseas-quotes/export', { params: { status: 'ACTIVE' }, responseType: 'blob' })
|
||||
expect(overseasHttp.get).toHaveBeenNthCalledWith(2, '/platform/v1/last-leg/master-data/exchange-rates/export', { params: {}, responseType: 'blob' })
|
||||
expect(overseasHttp.get).toHaveBeenNthCalledWith(3, '/platform/v1/last-leg/master-data/vehicles/export', { params: { ownership: 'OWNED' }, responseType: 'blob' })
|
||||
expect(overseasHttp.get).toHaveBeenNthCalledWith(1, '/platform/v1/last-leg/master-data/overseas-quotes/export', { params: { status: 'ACTIVE' }, responseType: 'arraybuffer' })
|
||||
expect(overseasHttp.get).toHaveBeenNthCalledWith(2, '/platform/v1/last-leg/master-data/exchange-rates/export', { params: {}, responseType: 'arraybuffer' })
|
||||
expect(overseasHttp.get).toHaveBeenNthCalledWith(3, '/platform/v1/last-leg/master-data/vehicles/export', { params: { ownership: 'OWNED' }, responseType: 'arraybuffer' })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -34,27 +34,52 @@ const normalizeQuoteDetail = (value: QuoteDetailWire): OverseasWarehouseQuoteDet
|
||||
const normalizeExchangeRate = (value: ExchangeRateWire): MonthlyExchangeRate => ({ ...value, usdToCnyAverage: numeric(value.usdToCnyAverage), recordVersion: numeric(value.recordVersion) })
|
||||
const normalizeVehicle = (value: VehicleWire): VehicleDetail => ({ ...value, fuelConsumptionPer100Km: numeric(value.fuelConsumptionPer100Km), mileageKm: numeric(value.mileageKm), recordVersion: numeric(value.recordVersion) })
|
||||
|
||||
async function csvError(blob: Blob, status: number) {
|
||||
if (!blob.type.toLowerCase().includes('json')) return null
|
||||
type DownloadPayload = Blob | ArrayBuffer | ArrayBufferView | string
|
||||
|
||||
async function downloadText(payload: DownloadPayload) {
|
||||
if (typeof payload === 'string') return payload
|
||||
if (payload instanceof Blob) return payload.text()
|
||||
const bytes = payload instanceof ArrayBuffer
|
||||
? new Uint8Array(payload)
|
||||
: new Uint8Array(payload.buffer, payload.byteOffset, payload.byteLength)
|
||||
return new TextDecoder().decode(bytes)
|
||||
}
|
||||
|
||||
async function csvError(payload: DownloadPayload, status: number) {
|
||||
const contentType = payload instanceof Blob ? payload.type.toLowerCase() : ''
|
||||
const text = await downloadText(payload)
|
||||
if (!contentType.includes('json') && !/^\s*[\[{]/.test(text)) return null
|
||||
try {
|
||||
const payload = JSON.parse(await blob.text()) as Partial<ApiEnvelope<unknown>>
|
||||
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) throw new Error('invalid envelope')
|
||||
return new ApiResponseError(payload.message || 'CSV 导出失败', payload.code || 'DOWNLOAD_FAILED', status,
|
||||
payload.data, payload.requestId, payload.traceId)
|
||||
const envelope = JSON.parse(text) as Partial<ApiEnvelope<unknown>>
|
||||
if (!envelope || typeof envelope !== 'object' || Array.isArray(envelope)) throw new Error('invalid envelope')
|
||||
return new ApiResponseError(envelope.message || 'CSV 导出失败', envelope.code || 'DOWNLOAD_FAILED', status,
|
||||
envelope.data, envelope.requestId, envelope.traceId)
|
||||
} catch (error) {
|
||||
if (error instanceof ApiResponseError) return error
|
||||
return new ApiResponseError('CSV 导出接口返回了无法识别的 JSON 响应', 'INVALID_DOWNLOAD_RESPONSE', status)
|
||||
}
|
||||
}
|
||||
function csvBlob(payload: DownloadPayload) {
|
||||
if (payload instanceof Blob) return payload
|
||||
if (typeof payload === 'string') return new Blob([payload], { type: 'text/csv;charset=utf-8' })
|
||||
const bytes = payload instanceof ArrayBuffer
|
||||
? new Uint8Array(payload)
|
||||
: new Uint8Array(payload.buffer, payload.byteOffset, payload.byteLength)
|
||||
const copy = new Uint8Array(bytes.byteLength)
|
||||
copy.set(bytes)
|
||||
return new Blob([copy.buffer], { type: 'text/csv;charset=utf-8' })
|
||||
}
|
||||
async function download(path: string, params: object) {
|
||||
try {
|
||||
const blob = await overseasHttp.get<Blob, Blob>(path, { params, responseType: 'blob' })
|
||||
const error = await csvError(blob, 200)
|
||||
const payload = await overseasHttp.get<DownloadPayload, DownloadPayload>(path, { params, responseType: 'arraybuffer' })
|
||||
const error = await csvError(payload, 200)
|
||||
if (error) throw error
|
||||
return blob
|
||||
return csvBlob(payload)
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error) && error.response?.data instanceof Blob) {
|
||||
const responseError = await csvError(error.response.data, error.response.status)
|
||||
const axiosError = axios.isAxiosError(error) ? error : null
|
||||
const responsePayload = axiosError?.response?.data as DownloadPayload | undefined
|
||||
if (responsePayload !== undefined) {
|
||||
const responseError = await csvError(responsePayload, axiosError?.response?.status || 0)
|
||||
if (responseError) throw responseError
|
||||
}
|
||||
throw error
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { overseasHttp } from '@/shared/api/overseasHttp'
|
||||
import { unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
|
||||
import type { OverseasPageResult } from './lastMileOrders'
|
||||
import { normalizeOverseasPage, type OverseasPagePayload } from './lastMileOrders'
|
||||
|
||||
export interface DispatchQuery {
|
||||
page: number
|
||||
@@ -106,11 +106,11 @@ export interface DispatchItemDto {
|
||||
}
|
||||
|
||||
export async function listDispatches(query: DispatchQuery) {
|
||||
const response = await overseasHttp.get<ApiEnvelope<OverseasPageResult<DispatchSummaryDto>>, ApiEnvelope<OverseasPageResult<DispatchSummaryDto>>>(
|
||||
const response = await overseasHttp.get<ApiEnvelope<OverseasPagePayload<DispatchSummaryDto>>, ApiEnvelope<OverseasPagePayload<DispatchSummaryDto>>>(
|
||||
'/platform/v1/last-leg/dispatches',
|
||||
{ params: query },
|
||||
)
|
||||
return unwrapApiResponse(response)
|
||||
return normalizeOverseasPage(unwrapApiResponse(response))
|
||||
}
|
||||
|
||||
export async function getDispatch(dispatchNo: string) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { overseasHttp } from '@/shared/api/overseasHttp'
|
||||
import { unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
|
||||
import type { OverseasPageResult } from './lastMileOrders'
|
||||
import { normalizeOverseasPage, type OverseasPagePayload } from './lastMileOrders'
|
||||
|
||||
export interface InboundOrderQuery {
|
||||
page: number
|
||||
@@ -67,10 +67,10 @@ export interface FbaAppointmentDto {
|
||||
export type UpdateFbaAppointmentPayload = Omit<BatchFbaAppointmentPayload, 'lineIds' | 'appointmentNo'>
|
||||
|
||||
export async function listInboundOrders(query: InboundOrderQuery) {
|
||||
const response = await overseasHttp.get<ApiEnvelope<OverseasPageResult<InboundOrderSummaryDto>>, ApiEnvelope<OverseasPageResult<InboundOrderSummaryDto>>>(
|
||||
const response = await overseasHttp.get<ApiEnvelope<OverseasPagePayload<InboundOrderSummaryDto>>, ApiEnvelope<OverseasPagePayload<InboundOrderSummaryDto>>>(
|
||||
'/platform/v1/last-leg/inbound-plans', { params: query },
|
||||
)
|
||||
return unwrapApiResponse(response)
|
||||
return normalizeOverseasPage(unwrapApiResponse(response))
|
||||
}
|
||||
|
||||
export async function createFbaAppointment(payload: BatchFbaAppointmentPayload) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { overseasHttp } from '@/shared/api/overseasHttp'
|
||||
import { unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
|
||||
import type { OverseasPageResult } from './lastMileOrders'
|
||||
import { normalizeOverseasPage, type OverseasPagePayload } from './lastMileOrders'
|
||||
|
||||
export interface InboundPlanQuery {
|
||||
customerId: string
|
||||
@@ -58,7 +58,7 @@ export interface InboundPlanDetailDto {
|
||||
|
||||
export async function listInboundPlans(query: InboundPlanQuery) {
|
||||
const { customerId, ...params } = query
|
||||
return unwrapApiResponse(await overseasHttp.get<ApiEnvelope<OverseasPageResult<InboundPlanSummaryDto>>, ApiEnvelope<OverseasPageResult<InboundPlanSummaryDto>>>('/platform/v1/last-leg/inbound-plans', { params: { customerId, ...params } }))
|
||||
return normalizeOverseasPage(unwrapApiResponse(await overseasHttp.get<ApiEnvelope<OverseasPagePayload<InboundPlanSummaryDto>>, ApiEnvelope<OverseasPagePayload<InboundPlanSummaryDto>>>('/platform/v1/last-leg/inbound-plans', { params: { customerId, ...params } })))
|
||||
}
|
||||
|
||||
export async function getInboundPlan(planNo: string, customerId: string) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { overseasHttp } from '@/shared/api/overseasHttp'
|
||||
import { unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
|
||||
import type { OverseasPageResult } from './lastMileOrders'
|
||||
import { normalizeOverseasPage, type OverseasPagePayload } from './lastMileOrders'
|
||||
|
||||
export interface InventoryBalanceDto {
|
||||
id: string
|
||||
@@ -110,10 +110,10 @@ export interface InventoryBalanceQuery {
|
||||
function params(query: InventoryBalanceQuery) { return { ...query } }
|
||||
|
||||
export async function listInventoryBalances(query: InventoryBalanceQuery) {
|
||||
const response = await overseasHttp.get<ApiEnvelope<OverseasPageResult<InventoryBalanceDto>>, ApiEnvelope<OverseasPageResult<InventoryBalanceDto>>>(
|
||||
const response = await overseasHttp.get<ApiEnvelope<OverseasPagePayload<InventoryBalanceDto>>, ApiEnvelope<OverseasPagePayload<InventoryBalanceDto>>>(
|
||||
'/platform/v1/last-leg/inventory/balances', { params: params(query) },
|
||||
)
|
||||
return unwrapApiResponse(response)
|
||||
return normalizeOverseasPage(unwrapApiResponse(response))
|
||||
}
|
||||
|
||||
export async function getInventorySummary(query: InventoryBalanceQuery) {
|
||||
@@ -153,9 +153,9 @@ export interface InventoryLedgerQuery {
|
||||
}
|
||||
|
||||
export async function listInventoryLedger(query: InventoryLedgerQuery) {
|
||||
const response = await overseasHttp.get<ApiEnvelope<OverseasPageResult<InventoryLedgerDto>>, ApiEnvelope<OverseasPageResult<InventoryLedgerDto>>>(
|
||||
const response = await overseasHttp.get<ApiEnvelope<OverseasPagePayload<InventoryLedgerDto>>, ApiEnvelope<OverseasPagePayload<InventoryLedgerDto>>>(
|
||||
'/platform/v1/last-leg/inventory/ledger',
|
||||
{ params: { ...query } },
|
||||
)
|
||||
return unwrapApiResponse(response)
|
||||
return normalizeOverseasPage(unwrapApiResponse(response))
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ describe('last-mile order overseas API', () => {
|
||||
beforeEach(() => vi.mocked(overseasHttp.get).mockReset())
|
||||
|
||||
it('loads all platform orders without a customer scope', async () => {
|
||||
vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', message: 'success', data: { records: [], page: 2, size: 20, total: 0, pages: 0 } })
|
||||
await expect(listLastMileOrders({ page: 2, size: 20, orderType: 'FIRST_LEG', currentStatus: 'PENDING_ASSIGNMENT', warehouse: 'LAX' })).resolves.toMatchObject({ page: 2 })
|
||||
vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', message: 'success', data: { records: [], page: '2', size: '20', total: '0', pages: '0' } })
|
||||
await expect(listLastMileOrders({ page: 2, size: 20, orderType: 'FIRST_LEG', currentStatus: 'PENDING_ASSIGNMENT', warehouse: 'LAX' })).resolves.toEqual({ records: [], page: 2, size: 20, total: 0, pages: 0 })
|
||||
expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/orders', {
|
||||
params: { page: 2, size: 20, orderType: 'FIRST_LEG', currentStatus: 'PENDING_ASSIGNMENT', warehouse: 'LAX' },
|
||||
})
|
||||
|
||||
@@ -9,6 +9,17 @@ export interface OverseasPageResult<T> {
|
||||
pages: number
|
||||
}
|
||||
|
||||
export type OverseasPagePayload<T> = Omit<OverseasPageResult<T>, 'page' | 'size' | 'total' | 'pages'> & {
|
||||
page: number | string
|
||||
size: number | string
|
||||
total: number | string
|
||||
pages: number | string
|
||||
}
|
||||
|
||||
export function normalizeOverseasPage<T>(value: OverseasPagePayload<T>): OverseasPageResult<T> {
|
||||
return { ...value, page: Number(value.page), size: Number(value.size), total: Number(value.total), pages: Number(value.pages) }
|
||||
}
|
||||
|
||||
export interface LastMileOrderQuery {
|
||||
page: number
|
||||
size: number
|
||||
@@ -118,11 +129,11 @@ export interface LastMileOrderDetailDto {
|
||||
}
|
||||
|
||||
export async function listLastMileOrders(query: LastMileOrderQuery) {
|
||||
const response = await overseasHttp.get<ApiEnvelope<OverseasPageResult<LastMileOrderSummaryDto>>, ApiEnvelope<OverseasPageResult<LastMileOrderSummaryDto>>>(
|
||||
const response = await overseasHttp.get<ApiEnvelope<OverseasPagePayload<LastMileOrderSummaryDto>>, ApiEnvelope<OverseasPagePayload<LastMileOrderSummaryDto>>>(
|
||||
'/platform/v1/last-leg/orders',
|
||||
{ params: query },
|
||||
)
|
||||
return unwrapApiResponse(response)
|
||||
return normalizeOverseasPage(unwrapApiResponse(response))
|
||||
}
|
||||
|
||||
export async function getLastMileOrder(orderId: string) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { overseasHttp } from '@/shared/api/overseasHttp'
|
||||
import { unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
|
||||
import type { OverseasPageResult } from './lastMileOrders'
|
||||
import { normalizeOverseasPage, type OverseasPagePayload } from './lastMileOrders'
|
||||
|
||||
export interface CustomerWarehouseDto {
|
||||
id: string
|
||||
@@ -44,19 +44,19 @@ export interface UpdatePalletPayload extends SavePalletPayload {
|
||||
}
|
||||
|
||||
export async function listCustomerWarehouses() {
|
||||
const response = await overseasHttp.get<ApiEnvelope<OverseasPageResult<CustomerWarehouseDto>>, ApiEnvelope<OverseasPageResult<CustomerWarehouseDto>>>(
|
||||
const response = await overseasHttp.get<ApiEnvelope<OverseasPagePayload<CustomerWarehouseDto>>, ApiEnvelope<OverseasPagePayload<CustomerWarehouseDto>>>(
|
||||
'/platform/v1/warehouse-admin',
|
||||
{ params: { page: 1, size: 200 } },
|
||||
)
|
||||
return unwrapApiResponse(response)
|
||||
return normalizeOverseasPage(unwrapApiResponse(response))
|
||||
}
|
||||
|
||||
export async function listPallets(warehouseId: string, query: { page: number; size: number; keyword?: string; palletType?: string; status?: string }) {
|
||||
const response = await overseasHttp.get<ApiEnvelope<OverseasPageResult<PalletDto>>, ApiEnvelope<OverseasPageResult<PalletDto>>>(
|
||||
const response = await overseasHttp.get<ApiEnvelope<OverseasPagePayload<PalletDto>>, ApiEnvelope<OverseasPagePayload<PalletDto>>>(
|
||||
`/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/pallets`,
|
||||
{ params: { ...query } },
|
||||
)
|
||||
return unwrapApiResponse(response)
|
||||
return normalizeOverseasPage(unwrapApiResponse(response))
|
||||
}
|
||||
|
||||
export async function getPallet(warehouseId: string, palletId: string) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { overseasHttp } from '@/shared/api/overseasHttp'
|
||||
import { unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
|
||||
import type { OverseasPageResult } from './lastMileOrders'
|
||||
import { normalizeOverseasPage, type OverseasPagePayload } from './lastMileOrders'
|
||||
|
||||
export interface RelabelOrderQuery {
|
||||
page: number
|
||||
@@ -92,10 +92,10 @@ export interface RelabelDetailDto {
|
||||
}
|
||||
|
||||
export async function listRelabelOrders(query: RelabelOrderQuery) {
|
||||
const response = await overseasHttp.get<ApiEnvelope<OverseasPageResult<RelabelSummaryDto>>, ApiEnvelope<OverseasPageResult<RelabelSummaryDto>>>(
|
||||
const response = await overseasHttp.get<ApiEnvelope<OverseasPagePayload<RelabelSummaryDto>>, ApiEnvelope<OverseasPagePayload<RelabelSummaryDto>>>(
|
||||
'/platform/v1/last-leg/relabel-applications', { params: query },
|
||||
)
|
||||
return unwrapApiResponse(response)
|
||||
return normalizeOverseasPage(unwrapApiResponse(response))
|
||||
}
|
||||
|
||||
export async function getRelabelOrder(relabelNo: string) {
|
||||
@@ -106,11 +106,11 @@ export async function getRelabelOrder(relabelNo: string) {
|
||||
}
|
||||
|
||||
export async function listRelabelWarehouseOptions(customerId: string) {
|
||||
const response = await overseasHttp.get<ApiEnvelope<OverseasPageResult<RelabelWarehouseOptionDto>>, ApiEnvelope<OverseasPageResult<RelabelWarehouseOptionDto>>>(
|
||||
const response = await overseasHttp.get<ApiEnvelope<OverseasPagePayload<RelabelWarehouseOptionDto>>, ApiEnvelope<OverseasPagePayload<RelabelWarehouseOptionDto>>>(
|
||||
'/platform/v1/warehouse-admin',
|
||||
{ params: { customerId, page: 1, size: 200, status: 'ACTIVE' } },
|
||||
)
|
||||
return unwrapApiResponse(response)
|
||||
return normalizeOverseasPage(unwrapApiResponse(response))
|
||||
}
|
||||
|
||||
export async function createRelabelOrder(customerId: string, payload: CreateRelabelPayload) {
|
||||
|
||||
@@ -36,7 +36,7 @@ describe('PalletsView', () => {
|
||||
await flushPromises()
|
||||
wrapper.findComponent(QueryToolbar).vm.$emit('search', { warehouseId: '200', code: '', type: '', status: '' })
|
||||
await flushPromises()
|
||||
await wrapper.findAll('button').find((button) => button.text().includes('编辑'))!.trigger('click')
|
||||
await wrapper.findAll('button.el-button--small').filter((button) => button.text().includes('编辑')).at(-1)!.trigger('click')
|
||||
await flushPromises()
|
||||
const dialog = wrapper.findComponent(AppDialog)
|
||||
dialog.vm.$emit('confirm')
|
||||
|
||||
Reference in New Issue
Block a user