From ad72ab8406a34ad2a4cc181e662705c9a1cd389c Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Fri, 30 Oct 2026 17:59:48 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cross-border/api/businessOrders.ts | 1 + src/modules/cross-border/api/orderBudgets.ts | 17 ++-- .../cross-border/businessOrders.test.mjs | 16 +--- .../components/BusinessOrdersView.vue | 22 +++-- .../components/OrderFundingStatus.vue | 66 +++++++++++++ .../components/TransportDemandForm.vue | 2 +- .../cross-border/orderFundingStatus.test.mjs | 95 +++++++++++++++++++ src/modules/cross-border/procurement.test.mjs | 10 +- .../cross-border/procurementPresentation.ts | 4 +- .../cross-border/views/ProcurementView.vue | 18 ++-- 10 files changed, 209 insertions(+), 42 deletions(-) create mode 100644 src/modules/cross-border/components/OrderFundingStatus.vue create mode 100644 src/modules/cross-border/orderFundingStatus.test.mjs diff --git a/src/modules/cross-border/api/businessOrders.ts b/src/modules/cross-border/api/businessOrders.ts index a4e37e3..4348246 100644 --- a/src/modules/cross-border/api/businessOrders.ts +++ b/src/modules/cross-border/api/businessOrders.ts @@ -59,6 +59,7 @@ export interface TransportDetail { originAddress: string | null destinationAddress: string | null remark: string | null + histories?: Array<{ operationType: string; toStatus: string; remark: string | null }> } export interface TransportCreate { diff --git a/src/modules/cross-border/api/orderBudgets.ts b/src/modules/cross-border/api/orderBudgets.ts index afbe31d..16d8eab 100644 --- a/src/modules/cross-border/api/orderBudgets.ts +++ b/src/modules/cross-border/api/orderBudgets.ts @@ -1,13 +1,10 @@ import { apiRequest } from '@/api/http' -interface BudgetResult { status: string; usable: boolean; amountCny: number | string; message: string; orderNo: string | null } +export interface BudgetResult { status: string; usable: boolean; amountCny: number | string; message: string; orderNo: string | null } const base = '/merchant/v1/last-leg/order-budgets' -// 资金回执成功才继续下单;超时保留当前幂等键,允许用户再次核对。 -export async function requireOrderBudget(kind: 'procurement' | 'trucking', key: string, amount: number, currency: string) { - const url = `${base}/${kind}/${encodeURIComponent(key)}` - let result = await apiRequest({ method: 'POST', url, data: { amount, currency } }) - for (let attempt = 0; result.status === 'FREEZE_PENDING' && attempt < 12; attempt++) { - await new Promise(resolve => setTimeout(resolve, 1000)) - result = await apiRequest({ method: 'GET', url }) - } - if (!result.usable) throw new Error(result.message || '余额及授信校验未完成,请稍后重试') +// 创建订单时后端在同一事务记录冻结请求,客户端只在详情核对状态和重试明确失败。 +export function readOrderFunding(kind: 'procurement' | 'trucking', orderNo: string) { + return apiRequest({ method: 'GET', url: base + '/' + kind + '/orders/' + encodeURIComponent(orderNo) }) +} +export function retryOrderFunding(kind: 'procurement' | 'trucking', orderNo: string) { + return apiRequest({ method: 'POST', url: base + '/' + kind + '/orders/' + encodeURIComponent(orderNo) + '/retry' }) } diff --git a/src/modules/cross-border/businessOrders.test.mjs b/src/modules/cross-border/businessOrders.test.mjs index 62f1d20..571a53d 100644 --- a/src/modules/cross-border/businessOrders.test.mjs +++ b/src/modules/cross-border/businessOrders.test.mjs @@ -76,7 +76,7 @@ test('covers list detail create loading error empty pagination and actions-first assert.match(view, /key: 'actions', label: '操作', width: 90, fixed: 'left'/) assert.match(view, /class="business-table-card"/) assert.doesNotMatch(procurementView, /inventoryQuantity|outboundQuantity/) - assert.match(procurementView, /label: '拒绝原因'/) + assert.match(procurementView, /label: '失败 \/ 拒绝原因'/) assert.match(procurementView, /reviewRemark/) assert.match(procurementView, /procurementStatuses/) assert.match(procurementView, /ProcurementDetailPanel/) @@ -213,15 +213,9 @@ test('looseboard requires both times and rejects backwards schedule', () => { }) -test('truck creation waits for budget freeze and never submits after insufficient funds', async () => { - let release - const gate = new Promise(resolve => { release = resolve }) - const current = transportHarness('trucking', false, () => gate) +test('truck creation does not block on a separate budget reservation', async () => { + const current = transportHarness('trucking', false, async () => { throw new Error('旧冻结接口不应调用') }) Object.assign(current.form, { vehicleType: 'PALLET_30', estimatedPalletCount: 3, budgetAmount: 100, origin: 'LA', destination: 'ONT', originContactName: 'A', originContactPhone: '1', destinationContactName: 'B', destinationContactPhone: '2', pickupAt: '2026-10-20T09:00:00', deliveryAt: '2026-10-20T15:00:00' }) - const pending = current.submit(); await Promise.resolve(); await Promise.resolve() - assert.equal(current.calls.length, 0) - release(); await pending; assert.equal(current.calls.length, 1) - const failed = transportHarness('trucking', false, async () => { throw new Error('余额及授信不足') }) - Object.assign(failed.form, { ...current.form, vehicleType: 'PALLET_30', estimatedPalletCount: 3, budgetAmount: 100, origin: 'LA', destination: 'ONT', originContactName: 'A', originContactPhone: '1', destinationContactName: 'B', destinationContactPhone: '2', pickupAt: '2026-10-20T09:00:00', deliveryAt: '2026-10-20T15:00:00' }) - await failed.submit(); assert.equal(failed.calls.length, 0); assert.equal(failed.form.budgetAmount, 100) + await current.submit() + assert.equal(current.calls.length, 1) }) diff --git a/src/modules/cross-border/components/BusinessOrdersView.vue b/src/modules/cross-border/components/BusinessOrdersView.vue index 0405ab6..a622b9f 100644 --- a/src/modules/cross-border/components/BusinessOrdersView.vue +++ b/src/modules/cross-border/components/BusinessOrdersView.vue @@ -3,7 +3,7 @@ import { computed, onMounted, reactive, ref, watch } from 'vue' import { useRoute, useRouter } from 'vue-router' import { ElMessage, type FormInstance } from 'element-plus' import TransportDemandForm from './TransportDemandForm.vue' -import { requireOrderBudget } from '../api/orderBudgets' +import OrderFundingStatus from './OrderFundingStatus.vue' import { createTransportDemandForm, transportDemandError, buildTransportDemand, type TransportDemandForm as DemandFormModel } from '../transportDemandForm' import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue' import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue' @@ -67,6 +67,7 @@ function transportColumns(looseboard: boolean): ColumnDefinition[] { { key: 'pickupAt', label: pickupLabel, width: 165 }, { key: 'deliveryAt', label: deliveryLabel, width: 165 }, ]), { key: 'status', label: '状态', width: 130 }, + ...(!looseboard ? [{ key: 'failureReason', label: '失败 / 拒绝原因', width: 240 }] : []), { key: 'fleetArrangement', label: '承运安排', width: 190 }, ] } @@ -105,6 +106,8 @@ const queryFields = computed(() => [ type: 'select', placeholder: '全部', options: [ + { label: '订单受理中', value: 'FUNDING_PENDING' }, + { label: '订单处理失败', value: 'FUNDING_FAILED' }, { label: '待审核', value: 'PENDING_REVIEW' }, { label: '已通过', value: 'APPROVED' }, { label: '已拒绝', value: 'REJECTED' }, @@ -144,7 +147,7 @@ const tableColumns = computed(() => [ ...configuration.value.columns.map((column) => ({ ...column, prop: column.key, - slotName: ['orderNo', 'status', 'fleetArrangement'].includes(column.key) ? column.key : undefined, + slotName: ['orderNo', 'status', 'failureReason', 'fleetArrangement'].includes(column.key) ? column.key : undefined, formatter: (row: object) => displayCell(row, column.key), })), ]) @@ -164,10 +167,10 @@ function localMinute(value: unknown) { return value ? String(value).replace('T', function transportTypeLabel(value: unknown) { return ({ PORT_PICKUP: '港口提柜', WAREHOUSE_TO_WAREHOUSE: '仓到仓', WAREHOUSE_TO_DOOR: '仓到门' } as Record)[String(value)] || text(value) } function statusLabel(value: unknown) { if (poolOrderStatusLabels[String(value)]) return poolOrderStatusLabels[String(value)] - return ({ PENDING_REVIEW: '待审核', APPROVED: '已通过', REJECTED: '已拒绝', PENDING_ACCEPTANCE: '待接单', PROCESSING: '处理中', IN_TRANSIT: '运输中', COMPLETED: '已完成', CANCELLED: '已取消' } as Record)[String(value)] || text(value) + return ({ FUNDING_PENDING: '订单受理中', FUNDING_FAILED: '订单处理失败', PENDING_REVIEW: '待审核', APPROVED: '已通过', REJECTED: '已拒绝', PENDING_ACCEPTANCE: '待接单', PROCESSING: '处理中', IN_TRANSIT: '运输中', COMPLETED: '已完成', CANCELLED: '已取消' } as Record)[String(value)] || text(value) } function statusTone(value: unknown) { - return ({ APPROVED: 'success', COMPLETED: 'success', REJECTED: 'danger', CANCELLED: 'info', PENDING_REVIEW: 'warning', PENDING_ACCEPTANCE: 'warning', PROCESSING: 'primary', IN_TRANSIT: 'primary' } as Record)[String(value)] || 'info' + return ({ APPROVED: 'success', COMPLETED: 'success', REJECTED: 'danger', CANCELLED: 'info', FUNDING_PENDING: 'warning', FUNDING_FAILED: 'danger', PENDING_REVIEW: 'warning', PENDING_ACCEPTANCE: 'warning', PROCESSING: 'primary', IN_TRANSIT: 'primary' } as Record)[String(value)] || 'info' } function displayCell(candidate: unknown, key: string) { const row = candidate as BusinessRow @@ -264,15 +267,17 @@ async function submit() { try { if (props.kind === 'trucking') { const payload = { ...buildTransportDemand('truck', form), idempotencyKey: stableSubmitKey } - await requireOrderBudget('trucking', stableSubmitKey, payload.budgetAmount!, payload.currency) await createTransport('trucking', payload) } else await createTransport('looseboard', { ...buildTransportDemand('looseboard', form), idempotencyKey: stableSubmitKey }) - ElMessage.success('订单提交成功') + ElMessage.success(props.kind === 'trucking' ? '本土卡订单已受理,资金校验完成后进入平台审核' : '订单提交成功') resetForm() if (props.createPage) await router.push({ name: listRouteName.value }) else { mode.value = 'list'; await load() } - } catch (cause) { if (/已释放|已过期/.test(errorMessage(cause))) submitKey.value = ''; ElMessage.error(errorMessage(cause)) } + } catch (cause) { + if (/已释放|已过期/.test(errorMessage(cause))) submitKey.value = '' + ElMessage.error(errorMessage(cause)) + } finally { submitting.value = false } } @@ -298,6 +303,7 @@ onMounted(() => { if (props.detailPage) void loadDetail(); else if (!props.creat + @@ -308,6 +314,8 @@ onMounted(() => { if (props.detailPage) void loadDetail(); else if (!props.creat
← 返回列表

{{ detail?.summary.orderNo || '订单详情' }}

{{ detailStatus }} + 刷新 +

订单信息

diff --git a/src/modules/cross-border/components/OrderFundingStatus.vue b/src/modules/cross-border/components/OrderFundingStatus.vue new file mode 100644 index 0000000..fb676aa --- /dev/null +++ b/src/modules/cross-border/components/OrderFundingStatus.vue @@ -0,0 +1,66 @@ + + + diff --git a/src/modules/cross-border/components/TransportDemandForm.vue b/src/modules/cross-border/components/TransportDemandForm.vue index 1c880b5..7cc1223 100644 --- a/src/modules/cross-border/components/TransportDemandForm.vue +++ b/src/modules/cross-border/components/TransportDemandForm.vue @@ -70,7 +70,7 @@ defineExpose({
- {{ isCreate ? '返回列表' : '取消' }} + 取消 {{ editing ? '保存修改' : '提交订单' }}
diff --git a/src/modules/cross-border/orderFundingStatus.test.mjs b/src/modules/cross-border/orderFundingStatus.test.mjs new file mode 100644 index 0000000..4ff9951 --- /dev/null +++ b/src/modules/cross-border/orderFundingStatus.test.mjs @@ -0,0 +1,95 @@ +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import test from 'node:test' +import { runInNewContext } from 'node:vm' +import ts from 'typescript' + +const source = readFileSync(new URL('./components/OrderFundingStatus.vue', import.meta.url), 'utf8') +const script = source.match(/