From 6a038c89caa7f21166ba9287ca01612bbd4a8e35 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Thu, 10 Sep 2026 11:02:52 +0800 Subject: [PATCH] fix(admin): update submitted measurement calculation strategy --- src/modules/inbounds/index.ts | 2 +- src/modules/inbounds/measurement/api.test.ts | 15 +++- src/modules/inbounds/measurement/api.ts | 15 ++++ src/modules/inbounds/measurement/types.ts | 17 +++++ .../pages/UnifiedOrdersView.test.ts | 21 +++--- .../pages/UnifiedOrdersView.vue | 73 +++++-------------- 6 files changed, 74 insertions(+), 69 deletions(-) diff --git a/src/modules/inbounds/index.ts b/src/modules/inbounds/index.ts index ee497e10..d55cec86 100644 --- a/src/modules/inbounds/index.ts +++ b/src/modules/inbounds/index.ts @@ -1,4 +1,4 @@ -export { getInboundMeasurement, saveInboundMeasurementDraft } from './measurement/api' +export { getInboundMeasurement, saveInboundMeasurementDraft, updateMeasurementCalculationStrategy } from './measurement/api' export { createInboundHoldIdempotencyKey, downloadPlatformInboundHoldEvidence, getPlatformInboundHold, holdPlatformInbound, releasePlatformInbound, uploadPlatformInboundHoldEvidence } from './core/api' export { default as InboundMeasurementPanel } from './measurement/InboundMeasurementPanel.vue' export { default as InboundMeasurementDialog } from './measurement/InboundMeasurementDialog.vue' diff --git a/src/modules/inbounds/measurement/api.test.ts b/src/modules/inbounds/measurement/api.test.ts index 3beddf71..9437524f 100644 --- a/src/modules/inbounds/measurement/api.test.ts +++ b/src/modules/inbounds/measurement/api.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { http } from '@/shared/api/http' -import { createManualCartonScan, downloadInboundMeasurementImage, getInboundMeasurement, saveInboundMeasurementDraft, submitInboundMeasurement, updateConfirmedPackageCount } from './api' +import { createManualCartonScan, downloadInboundMeasurementImage, getInboundMeasurement, saveInboundMeasurementDraft, submitInboundMeasurement, updateConfirmedPackageCount, updateMeasurementCalculationStrategy } from './api' import type { SaveInboundMeasurementDraftRequest } from './types' vi.mock('@/shared/api/http', () => ({ @@ -76,6 +76,19 @@ describe('inbound measurement API', () => { ) }) + it('updates calculation strategy without resaving the frozen measurement batch', async () => { + const payload = { measurementId: '9001', version: 3, coefficientMode: 'ADD' as const, chargeRuleId: null } + const saved = { ...payload, version: 4, status: 'SUBMITTED' } + vi.mocked(http.put).mockResolvedValue({ code: '200', message: 'success', data: saved }) + + await expect(updateMeasurementCalculationStrategy('10001', '7001', payload)).resolves.toEqual(saved) + expect(http.put).toHaveBeenCalledWith( + '/platform/v1/first-leg/inbounds/7001/measurement/calculation-strategy', + payload, + { params: { tenantId: '10001' } }, + ) + }) + it('downloads the current measurement image using the scoped file route', async () => { const image = new Blob(['image-data'], { type: 'image/jpeg' }) vi.mocked(http.get).mockResolvedValue(image) diff --git a/src/modules/inbounds/measurement/api.ts b/src/modules/inbounds/measurement/api.ts index 6dc22d15..779e04ce 100644 --- a/src/modules/inbounds/measurement/api.ts +++ b/src/modules/inbounds/measurement/api.ts @@ -7,6 +7,8 @@ import type { ConfirmedPackageCountRequest, SaveInboundMeasurementDraftRequest, SubmitInboundMeasurementRequest, + UpdateMeasurementCalculationStrategyRequest, + MeasurementCalculationStrategyUpdated, ManualCartonScanRequest, } from './types' @@ -57,6 +59,19 @@ export async function saveInboundMeasurementDraft( return unwrapApiResponse(response) } +export async function updateMeasurementCalculationStrategy( + customerId: string, + inboundId: string, + payload: UpdateMeasurementCalculationStrategyRequest, +) { + const response = await http.put, ApiEnvelope>( + `${measurementPath(inboundId)}/calculation-strategy`, + payload, + { params: { tenantId: customerId } }, + ) + return unwrapApiResponse(response) +} + export async function submitInboundMeasurement( customerId: string, inboundId: string, diff --git a/src/modules/inbounds/measurement/types.ts b/src/modules/inbounds/measurement/types.ts index dfba6efa..560f585c 100644 --- a/src/modules/inbounds/measurement/types.ts +++ b/src/modules/inbounds/measurement/types.ts @@ -9,7 +9,12 @@ export type InboundMeasurementProcessStatus = | 'WAIT_PRICING' | 'FEE_CONFIRMING' | 'INBOUNDING' + | 'WAIT_PALLET' + | 'WAIT_PALLETIZE' | 'WAIT_SPLIT' + | 'LOADING' + | 'LOADED' + | 'DELIVERED' | 'VOIDED' export interface InboundMeasurementItem { @@ -158,6 +163,18 @@ export interface InboundMeasurementDraftSaved { status: string } +export interface UpdateMeasurementCalculationStrategyRequest { + measurementId: MeasurementIdentifier + version: number + coefficientMode: 'ACTUAL' | 'ADD' | 'MULTIPLY' + chargeRuleId?: MeasurementIdentifier | null +} + +export interface MeasurementCalculationStrategyUpdated extends InboundMeasurementDraftSaved { + coefficientMode: 'ACTUAL' | 'ADD' | 'MULTIPLY' + chargeRuleId: MeasurementIdentifier | null +} + export interface ConfirmedPackageCountRequest { measurementVersion: number confirmedPackageCount: number diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts b/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts index 5b58b53b..cd7a6414 100644 --- a/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts +++ b/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts @@ -7,11 +7,11 @@ const source = readFileSync(resolve(process.cwd(), 'src/modules/unified-orders/p const routeSource = readFileSync(resolve(process.cwd(), 'src/modules/unified-orders/routes.ts'), 'utf8').replace(/\r\n/g, '\n') describe('UnifiedOrdersView contract', () => { - it('loads the reference unit price while rechecking the confirmation lock before sending', () => { + it('loads the reference unit price and lets the backend enforce the combined final lock', () => { const openDialogSource = source.slice(source.indexOf('async function openFeeConfirmDialog()'), source.indexOf('async function executeConfirmAndSendFee()')) const sendFeeSource = source.slice(source.indexOf('async function executeConfirmAndSendFee()'), source.indexOf('function handleOrderSelectionChange')) expect(openDialogSource).toContain('getLatestInboundFee') - expect(sendFeeSource).toContain('getLatestInboundFee') + expect(sendFeeSource).not.toContain('getLatestInboundFee') expect(sendFeeSource).toContain('adjustPublishedInboundFee') expect(source).toContain('价格表参考单价') expect(source).toContain('本次对客单价') @@ -385,7 +385,7 @@ describe('UnifiedOrdersView contract', () => { expect(source).toContain('label="箱规系数" value="MULTIPLY"') }) - it('keeps preview modes independent and only persists outer-carton selectors while the measurement draft is editable', () => { + it('keeps preview modes independent and persists outer-carton strategy through its dedicated endpoint', () => { expect(source).toContain("measurementSectionModes = reactive>") expect(source).toContain(':model-value="measurementSectionModes.customer"') expect(source).toContain(':model-value="measurementSectionModes.outer"') @@ -395,10 +395,10 @@ describe('UnifiedOrdersView contract', () => { expect(source).not.toContain("updateMeasurementSectionMode('outer', $event)") expect(source.match(/@change="selectAndSaveOuterMeasurementMode\(\$event\)"/g)).toHaveLength(2) expect(source).not.toContain('@visible-change="handleOuterModeDropdownVisibleChange($event)"') - expect(source.match(/:disabled="Boolean\(measurementStrategySavingId\) \|\| !measurementDraftEditable"/g)).toHaveLength(2) - expect(source).toContain("measurementDetail.value?.processStatus === 'MEASURE_PENDING'") - expect(source).toContain('测量已提交待审核;如需修改箱规计算规则,请先退回测量') - expect(source).toContain('if (!measurementDraftEditable.value)') + expect(source.match(/:disabled="Boolean\(measurementStrategySavingId\) \|\| !measurementStrategyEditable"/g)).toHaveLength(2) + expect(source).toContain('const measurementStrategyEditable = computed') + expect(source).toContain('客户已确认且该票已装柜完成后,箱规计算规则将冻结') + expect(source).toContain('if (!measurementStrategyEditable.value)') expect(source).toContain('const previousMode = measurementSectionModes.outer') expect(source).toContain('if (!saved) measurementSectionModes.outer = previousMode') expect(source).not.toContain('handleOuterModeDropdownVisibleChange') @@ -432,11 +432,8 @@ describe('UnifiedOrdersView contract', () => { expect(source).not.toContain(':disabled="confirmFeeSending || measurementLoading || !hasActualMeasurementData"') expect(source).toContain('selectAndSaveOuterMeasurementMode($event)') expect(source).toContain('const saved = await updateMeasurementCalculationMode(null, mode, ruleId)') - expect(source).toContain('coefficientMode: targetIds.has(String(item.id)) ? mode') - expect(source).toContain('outerLengthCm: item.outerLengthCm') - expect(source).toContain('outerWidthCm: item.outerWidthCm') - expect(source).toContain('outerHeightCm: item.outerHeightCm') - expect(source).toContain('const savedDraft = await saveInboundMeasurementDraft') + expect(source).toContain('coefficientMode: mode') + expect(source).toContain('const savedDraft = await updateMeasurementCalculationStrategy') expect(source).toContain('version: savedDraft.version') expect(source).toContain('waitForSavedMeasurementVersion(') expect(source).toContain('Number(refreshed.version) >= savedVersion') diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.vue b/src/modules/unified-orders/pages/UnifiedOrdersView.vue index a5e5a46f..5af5964d 100644 --- a/src/modules/unified-orders/pages/UnifiedOrdersView.vue +++ b/src/modules/unified-orders/pages/UnifiedOrdersView.vue @@ -73,6 +73,7 @@ import { releasePlatformInbound, uploadPlatformInboundHoldEvidence, saveInboundMeasurementDraft, + updateMeasurementCalculationStrategy, InboundFeePanel, InboundMeasurementDialog, MeasurementEvidenceThumbnail, @@ -231,10 +232,15 @@ const measurementDraftEditable = computed(() => ( can('platform:firstleg:inbound:measure:edit') && measurementDraftEditableStatuses.has(measurementDetail.value?.processStatus || '') )) -const measurementDraftLockedHint = computed(() => { +const measurementStrategyEditable = computed(() => ( + can('platform:firstleg:inbound:measure:edit') + && Boolean(measurementDetail.value?.measurementId) + && measurementDetail.value?.version != null +)) +const measurementStrategyLockedHint = computed(() => { if (!can('platform:firstleg:inbound:measure:edit')) return '当前账号缺少测量编辑权限' - if (measurementDetail.value?.processStatus === 'MEASURE_PENDING') return '测量已提交待审核;如需修改箱规计算规则,请先退回测量' - return '当前测量状态已冻结,不能修改箱规计算规则' + if (!measurementDetail.value?.measurementId) return '当前没有可修改的测量批次' + return '客户已确认且该票已装柜完成后,箱规计算规则将冻结' }) const cargoImagePreviewUrls = new Set() const measurementDialogOpen = ref(false) @@ -528,8 +534,8 @@ function updateMeasurementSectionMode(section: MeasurementSection, mode: Measure async function selectAndSaveOuterMeasurementMode(mode: MeasurementCalculationMode, ruleId?: string) { if (measurementStrategySavingId.value) return - if (!measurementDraftEditable.value) { - ElMessage.warning(measurementDraftLockedHint.value) + if (!measurementStrategyEditable.value) { + ElMessage.warning(measurementStrategyLockedHint.value) return } const previousMode = measurementSectionModes.outer @@ -985,7 +991,7 @@ function measurementCalculationModeForRow(row: Record): Measure async function updateMeasurementCalculationMode(row: Record | null, mode: MeasurementCalculationMode, selectedRuleId?: string): Promise { const summary = detail.value?.summary const measurement = measurementDetail.value - if (!summary?.inboundId || !measurement?.measurementId || measurement.version == null || !measurementDraftEditable.value) return false + if (!summary?.inboundId || !measurement?.measurementId || measurement.version == null || !measurementStrategyEditable.value) return false const itemId = normalizedIdentifier(row?.id) const itemNo = normalizedIdentifier(row?.itemNo) const targetIds = new Set(measurement.items @@ -1014,35 +1020,11 @@ async function updateMeasurementCalculationMode(row: Record | n ElMessage.warning('该客户尚未启用系数配置,请先到客户计费规则中设置默认规则') return false } - const savedDraft = await saveInboundMeasurementDraft(String(summary.tenantId), String(summary.inboundId), { + const savedDraft = await updateMeasurementCalculationStrategy(String(summary.tenantId), String(summary.inboundId), { measurementId: String(measurement.measurementId), version: measurement.version, - actualGoodsName: measurement.actualGoodsName, - items: measurement.items.filter((item) => Number(item.packageCount) > 0).map((item) => ({ - inboundDetailId: item.inboundDetailId ?? undefined, - boxCode: item.boxCode, - itemNo: item.itemNo, - boxSpecification: item.boxSpecification, - packageCount: item.packageCount, - lengthCm: item.lengthCm, - widthCm: item.widthCm, - heightCm: item.heightCm, - unitWeightKg: item.unitWeightKg, - intelligentImageFileId: item.intelligentImageFileId, - machineNo: item.machineNo, - measuredAt: item.deviceMeasuredAt, - overLimitWarning: item.overLimitWarning, - // 人工外箱值是本次策略的计算基数,切换策略时必须一并保留。 - outerLengthCm: item.outerLengthCm, - outerWidthCm: item.outerWidthCm, - outerHeightCm: item.outerHeightCm, - chargeRuleId: (targetIds.has(String(item.id)) ? mode : item.coefficientMode) === 'MULTIPLY' - ? (targetIds.has(String(item.id)) || !measurementChargeRules.value.some((rule) => rule.id === String(item.chargeRuleId) && rule.status === 'ACTIVE') - ? activeRule!.id : item.chargeRuleId) - : null, - coefficientMode: targetIds.has(String(item.id)) ? mode : item.coefficientMode || 'ACTUAL', - remark: item.remark, - })), + coefficientMode: mode, + chargeRuleId: mode === 'MULTIPLY' ? activeRule!.id : null, }) // PUT 返回的新版本已经代表事务保存成功。详情查询可能经过读副本并短暂返回旧版本, // 不能用紧随其后的旧数据否定本次成功保存;先按新版本和本次规则更新本地视图。 @@ -1691,11 +1673,6 @@ async function openFeeConfirmDialog() { volumeSource: 'ACTUAL', }, createFeeActionIdempotencyKey('calculate')) } - if (snapshot.customerConfirmationStatus === 'CONFIRMED' || snapshot.customerConfirmedAt) { - ElMessage.warning('客户已确认该费用,费用已锁定,不允许再修改或重新发送') - feeConfirmOpen.value = false - return - } feeConfirmReference.value = snapshot feeConfirmUnitPrice.value = Number(snapshot.unitPrice) } catch (error) { @@ -1728,20 +1705,6 @@ async function executeConfirmAndSendFee() { try { const tenantId = String(summary.tenantId) const inboundId = String(summary.inboundId) - try { - const snapshot = await getLatestInboundFee(tenantId, inboundId) - if (snapshot.customerConfirmationStatus === 'CONFIRMED' || snapshot.customerConfirmedAt) { - ElMessage.warning('客户已确认该费用,费用已锁定,不允许再修改或重新发送') - feeConfirmOpen.value = false - return - } - } catch (error) { - const details = getApiErrorDetails(error, '费用状态查询失败,请刷新后重试') - if (details.code !== 'A0404') { - ElMessage.error(details.message) - return - } - } let snapshot = await calculateInboundFee(tenantId, inboundId, { selectedWeightKg, selectedVolumeCbm, @@ -2453,7 +2416,7 @@ onBeforeUnmount(() => {
-
+
合计箱数:{{ measurementTotals(outerMeasurementRows).count ?? '—' }}总毛重:{{ formatNumber(measurementTotals(outerMeasurementRows).gross) }}总体积:{{ formatNumber(measurementTotals(outerMeasurementRows).volume, 6) }}总材积:{{ formatNumber(measurementTotals(outerMeasurementRows).volumetric) }}
@@ -2461,12 +2424,12 @@ onBeforeUnmount(() => {
箱规计算规则选择 - + - {{ measurementDraftEditable ? '规则参数来源:客户管理' : measurementDraftLockedHint }} + {{ measurementStrategyEditable ? '规则参数来源:客户管理' : measurementStrategyLockedHint }}