优化
This commit is contained in:
@@ -1,20 +0,0 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import ts from 'typescript'
|
||||
|
||||
test('计费重确认保留大整数来源和原幂等键,只提交预期重量版本', async () => {
|
||||
const calls = []
|
||||
const source = readFileSync(new URL('./chargeableWeights.ts', import.meta.url), 'utf8')
|
||||
const code = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS } }).outputText
|
||||
const exports = {}
|
||||
new Function('require', 'exports', code)(() => ({ apiRequest: async request => { calls.push(request); return [] } }), exports)
|
||||
const id = '9223372036854775806'
|
||||
await exports.getChargeableWeights(id)
|
||||
await exports.confirmChargeableWeight({ inboundId: id, fingerprint: 'abc' }, 'original-command')
|
||||
assert.equal(calls[0].url, `/merchant/v1/first-leg/orders/${id}/chargeable-weights`)
|
||||
assert.equal(calls[1].url, `/merchant/v1/first-leg/chargeable-weights/${id}/confirm`)
|
||||
assert.deepEqual(calls[1].headers, { 'Idempotency-Key': 'original-command' })
|
||||
assert.deepEqual(calls[1].data, { expectedFingerprint: 'abc' })
|
||||
assert.equal(calls.some(call => call.url.includes('inbound-billing')), false)
|
||||
})
|
||||
@@ -1,16 +0,0 @@
|
||||
import { apiRequest } from '@/api/http'
|
||||
export interface ChargeableWeight {
|
||||
inboundId: string
|
||||
orderId: string
|
||||
chargeableWeightKg: number
|
||||
fingerprint: string
|
||||
confirmed: boolean
|
||||
confirmedAt: string | null
|
||||
}
|
||||
export function getChargeableWeights(orderId: string) {
|
||||
return apiRequest<ChargeableWeight[]>({ method: 'GET', url: `/merchant/v1/first-leg/orders/${encodeURIComponent(orderId)}/chargeable-weights` })
|
||||
}
|
||||
export function confirmChargeableWeight(weight: ChargeableWeight, key: string) {
|
||||
return apiRequest<ChargeableWeight>({ method: 'POST', url: `/merchant/v1/first-leg/chargeable-weights/${encodeURIComponent(weight.inboundId)}/confirm`,
|
||||
headers: { 'Idempotency-Key': key }, data: { expectedFingerprint: weight.fingerprint } })
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import ts from 'typescript'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const flush = async () => { for (let i = 0; i < 8; i++) await Promise.resolve() }
|
||||
function harness(api, props = { orderId: 'one', canConfirm: true }) {
|
||||
let changed, disposed
|
||||
const events = []
|
||||
const source = readFileSync(new URL('./ChargeableWeightDialog.vue', import.meta.url), 'utf8').split('<script setup lang="ts">')[1].split('</script>')[0]
|
||||
const code = ts.transpileModule(source + '\nexports.state = { rows, loading, saving, error, confirm, load };', {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 },
|
||||
}).outputText
|
||||
const exports = {}
|
||||
new Function('require', 'exports', 'defineProps', 'defineEmits', code)(name => {
|
||||
if (name === 'vue') return { ref, watch: (_, callback) => { changed = callback; callback() }, onBeforeUnmount: callback => { disposed = callback } }
|
||||
if (name === 'element-plus') return { ElMessage: { success() {} } }
|
||||
return api
|
||||
}, exports, () => props, () => name => events.push(name))
|
||||
return { ...exports.state, props, change: () => changed(), dispose: () => disposed(), events }
|
||||
}
|
||||
|
||||
test('确认结果未知时重试原命令,成功后回查并通知工作台刷新', async () => {
|
||||
const row = { inboundId: '100', fingerprint: 'weight-v1', chargeableWeightKg: 50, confirmed: false }
|
||||
const commands = []
|
||||
let fail = true
|
||||
const h = harness({ getChargeableWeights: async () => [row], confirmChargeableWeight: async (_, key) => {
|
||||
commands.push(key); if (fail) throw new Error('响应丢失'); row.confirmed = true
|
||||
} })
|
||||
await flush(); await h.confirm(row)
|
||||
assert.equal(h.error.value, '响应丢失'); assert.equal(h.saving.value, '')
|
||||
fail = false; await h.confirm(row)
|
||||
assert.equal(commands[0], commands[1]); assert.deepEqual(h.events, ['confirmed'])
|
||||
assert.equal(h.rows.value[0].confirmed, true); assert.equal(h.saving.value, '')
|
||||
})
|
||||
|
||||
test('切换订单或关闭后丢弃迟到的读取结果,无权限不能确认', async () => {
|
||||
let resolveOld
|
||||
const h = harness({ getChargeableWeights: order => order === 'one' ? new Promise(resolve => { resolveOld = resolve }) : Promise.resolve([]),
|
||||
confirmChargeableWeight: () => { throw new Error('无权限不得写入') } }, { orderId: 'one', canConfirm: false })
|
||||
h.props.orderId = 'two'; h.change(); await flush()
|
||||
resolveOld([{ inboundId: 'old' }]); await flush()
|
||||
assert.deepEqual(h.rows.value, [])
|
||||
await h.confirm({ inboundId: 'old', fingerprint: 'old' })
|
||||
assert.deepEqual(h.events, [])
|
||||
h.dispose()
|
||||
})
|
||||
@@ -1,55 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getChargeableWeights, confirmChargeableWeight, type ChargeableWeight } from '../api/chargeableWeights'
|
||||
const props = defineProps<{ orderId: string; canConfirm: boolean }>()
|
||||
const emit = defineEmits<{ close: []; confirmed: [] }>()
|
||||
const rows = ref<ChargeableWeight[]>([])
|
||||
const loading = ref(false)
|
||||
const saving = ref('')
|
||||
const error = ref('')
|
||||
const commands = new Map<string, string>()
|
||||
let sequence = 0
|
||||
let confirmationSequence = 0
|
||||
async function load() {
|
||||
const ticket = ++sequence
|
||||
loading.value = true; error.value = ''; rows.value = []
|
||||
try { const data = await getChargeableWeights(props.orderId); if (ticket === sequence) rows.value = data }
|
||||
catch (e) { if (ticket === sequence) error.value = e instanceof Error ? e.message : '计费重加载失败' }
|
||||
finally { if (ticket === sequence) loading.value = false }
|
||||
}
|
||||
async function confirm(row: ChargeableWeight) {
|
||||
if (saving.value || !props.canConfirm) return
|
||||
const ticket = sequence
|
||||
const attempt = ++confirmationSequence
|
||||
const fingerprint = `${row.inboundId}:${row.fingerprint}`
|
||||
const key = commands.get(fingerprint) ?? `weight-${crypto.randomUUID()}`
|
||||
commands.set(fingerprint, key); saving.value = row.inboundId; error.value = ''
|
||||
try {
|
||||
await confirmChargeableWeight(row, key)
|
||||
if (ticket !== sequence) return
|
||||
ElMessage.success('计费重已确认'); emit('confirmed'); await load()
|
||||
} catch (e) { if (ticket === sequence) error.value = e instanceof Error ? e.message : '确认失败,请刷新后重试' }
|
||||
finally { if (attempt === confirmationSequence) saving.value = '' }
|
||||
}
|
||||
watch(() => props.orderId, () => { confirmationSequence++; saving.value = ''; commands.clear(); void load() }, { immediate: true })
|
||||
onBeforeUnmount(() => { sequence++; confirmationSequence++ })
|
||||
</script>
|
||||
<template>
|
||||
<el-dialog :model-value="true" title="确认计费重" width="560px" :close-on-click-modal="false" @close="emit('close')">
|
||||
<el-alert v-if="error" :title="error" type="error" :closable="false" />
|
||||
<el-button v-if="error" :disabled="!!saving" @click="load">重新加载</el-button>
|
||||
<div v-loading="loading">
|
||||
<el-empty v-if="!loading && !error && !rows.length" description="暂无已审核的计费重" />
|
||||
<div v-for="row in rows" :key="row.inboundId" class="weight-row">
|
||||
<span>来源入仓单:{{ row.inboundId }}<br>计费重:{{ row.chargeableWeightKg }} kg</span>
|
||||
<el-tag v-if="row.confirmed" type="success">已确认</el-tag>
|
||||
<el-button v-else-if="canConfirm" type="primary" :disabled="!!saving" :loading="saving === row.inboundId" @click="confirm(row)">确认计费重</el-button>
|
||||
<span v-else>待确认</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<style scoped>
|
||||
.weight-row { display: flex; justify-content: space-between; align-items: center; gap: 16px; padding: 12px 0; }
|
||||
</style>
|
||||
@@ -30,7 +30,7 @@ test('shows the published merchant inbound, measurement details and customer con
|
||||
}
|
||||
assert.match(source, /formatInboundMoney\(props\.data\.latestFee\.totalAmount/)
|
||||
assert.match(source, /formatConversionPrecision\(/)
|
||||
assert.match(source, /确认费用/)
|
||||
assert.match(source, /title="确认计费重"/)
|
||||
assert.match(source, /确认后当前费用版本将锁定/)
|
||||
assert.match(source, /customerConfirmationStatus === 'CONFIRMED'/)
|
||||
assert.match(source, /confirmMerchantInboundFee/)
|
||||
|
||||
@@ -77,7 +77,7 @@ async function confirmFee() {
|
||||
<el-tag :type="props.data.latestFee.customerConfirmationStatus === 'CONFIRMED' ? 'success' : 'warning'" effect="light">
|
||||
{{ props.data.latestFee.customerConfirmationStatus === 'CONFIRMED' ? '客户已确认 · 费用已锁定' : '费用已发布 · 待确认' }}
|
||||
</el-tag>
|
||||
<el-button v-if="props.data.latestFee.customerConfirmationStatus !== 'CONFIRMED' && props.canConfirm" type="primary" @click="feeDialogOpen = true">确认费用</el-button>
|
||||
<el-button v-if="props.data.latestFee.customerConfirmationStatus !== 'CONFIRMED' && props.canConfirm" type="primary" @click="feeDialogOpen = true">确认计费重</el-button>
|
||||
<el-button v-else plain @click="feeDialogOpen = true">查看费用</el-button>
|
||||
</div>
|
||||
</header>
|
||||
@@ -198,7 +198,7 @@ async function confirmFee() {
|
||||
|
||||
</section>
|
||||
|
||||
<el-dialog v-model="feeDialogOpen" class="billing-confirm-dialog" title="确认费用" width="min(1180px, calc(100vw - 40px))" append-to-body @closed="emit('closed')">
|
||||
<el-dialog v-model="feeDialogOpen" class="billing-confirm-dialog" title="确认计费重" width="min(1180px, calc(100vw - 40px))" append-to-body @closed="emit('closed')">
|
||||
<div v-if="props.loading" class="inbound-billing-loading" aria-live="polite" aria-busy="true">
|
||||
<el-skeleton :rows="4" animated />
|
||||
<span class="sr-only">正在加载费用</span>
|
||||
|
||||
@@ -642,8 +642,11 @@ test('opens billing-weight confirmation independently from order details', () =>
|
||||
assert.match(source, /unavailable-message="未推送费用"/)
|
||||
})
|
||||
|
||||
test('shows billing-weight confirmation only when the backend marks it pending', () => {
|
||||
assert.match(source, /if \(canConfirmInboundFee\.value && order\.businessSummary\.customerFeeConfirmationRequired\) \{\s*actions\.push\(\{ key: 'fee'/)
|
||||
test('uses one published-fee dialog for billing-weight confirmation on every order row', () => {
|
||||
const actions = source.slice(source.indexOf('function getOrderRowActions'), source.indexOf('function getOrderRowActions') + 1800)
|
||||
assert.match(actions, /actions\.push\(\{ key: 'fee', label: '确认计费重', run: \(\) => openFeeConfirmation\(order\) \}\)/)
|
||||
assert.doesNotMatch(actions, /customerFeeConfirmationRequired|label: '确认费用'|key: 'weight'/)
|
||||
assert.doesNotMatch(source, /ChargeableWeightDialog|weightConfirmationOrderId/)
|
||||
assert.match(source, /:row-actions="getOrderRowActions"/)
|
||||
assert.match(orderStoreSource, /customerFeeConfirmationRequired: summary\?\.customerFeeConfirmationRequired \?\? false/)
|
||||
})
|
||||
|
||||
@@ -9,7 +9,6 @@ import DataTable, { type DataTableColumn } from '@/components/common/DataTable.v
|
||||
import type { TableAction } from '@/components/common/tableActions'
|
||||
import { ApiError } from '@/api/http'
|
||||
import FirstLegOrderEditorShell from '@/modules/first-leg/components/FirstLegOrderEditorShell.vue'
|
||||
import ChargeableWeightDialog from '@/modules/first-leg/components/ChargeableWeightDialog.vue'
|
||||
import InboundBillingPanel from '@/modules/first-leg/components/InboundBillingPanel.vue'
|
||||
import InboundCodeAssociationDialog from '@/modules/first-leg/components/InboundCodeAssociationDialog.vue'
|
||||
import OrderProgressPanel from '@/modules/first-leg/components/OrderProgressPanel.vue'
|
||||
@@ -268,7 +267,6 @@ const inboundBillingUnavailableMessage = ref('还未测量推送')
|
||||
const inboundBillingError = ref('')
|
||||
const inboundBillingOrderId = ref('')
|
||||
const feeConfirmationOpen = ref(false)
|
||||
const weightConfirmationOrderId = ref('')
|
||||
const customsByOrder = reactive<Record<string, MerchantCustomsContainer | null | undefined>>({})
|
||||
const customsEligibilityPending = new Set<string>()
|
||||
const customsDialogOpen = ref(false)
|
||||
@@ -1363,12 +1361,7 @@ function getOrderRowActions(row: object): TableAction[] {
|
||||
if (canCreateOrder.value) {
|
||||
actions.push({ key: 'copy', label: '复制下单', loading: copyingOrderId.value === order.id, run: () => copyOrder(order) })
|
||||
}
|
||||
if (canConfirmInboundFee.value && !['DRAFT', 'CANCELLED', 'REJECTED', 'SUBMITTED'].includes(order.status)) {
|
||||
actions.push({ key: 'weight', label: '确认计费重', run: () => { weightConfirmationOrderId.value = order.id } })
|
||||
}
|
||||
if (canConfirmInboundFee.value && order.businessSummary.customerFeeConfirmationRequired) {
|
||||
actions.push({ key: 'fee', label: '确认费用', run: () => openFeeConfirmation(order) })
|
||||
}
|
||||
actions.push({ key: 'fee', label: '确认计费重', run: () => openFeeConfirmation(order) })
|
||||
if (canEditOrder.value && canClientEditFirstLegOrder(order.status)) {
|
||||
actions.push({ key: 'edit', label: '编辑', loading: openingEditor.value, run: () => openEdit(order) })
|
||||
}
|
||||
@@ -2560,7 +2553,6 @@ async function searchOrders(model: QueryModel) {
|
||||
@confirmed="handleInboundFeeConfirmed"
|
||||
@closed="feeConfirmationOpen = false"
|
||||
/>
|
||||
<ChargeableWeightDialog v-if="weightConfirmationOrderId" :order-id="weightConfirmationOrderId" :can-confirm="canConfirmInboundFee" @close="weightConfirmationOrderId = ''" @confirmed="handleInboundFeeConfirmed" />
|
||||
<CustomsDocumentsDialog
|
||||
v-model="customsDialogOpen"
|
||||
:order-id="customsDialogOrderId"
|
||||
|
||||
Reference in New Issue
Block a user