diff --git a/src/modules/inbounds/measurement/api.test.ts b/src/modules/inbounds/measurement/api.test.ts index 37ea5866..3beddf71 100644 --- a/src/modules/inbounds/measurement/api.test.ts +++ b/src/modules/inbounds/measurement/api.test.ts @@ -45,7 +45,7 @@ describe('inbound measurement API', () => { await expect(getInboundMeasurement('10001', '7001')).resolves.toEqual(detail) expect(http.get).toHaveBeenCalledWith('/platform/v1/first-leg/inbounds/7001/measurement', { - params: { tenantId: '10001' }, + params: { tenantId: '10001', requestAt: expect.any(Number) }, }) }) diff --git a/src/modules/inbounds/measurement/api.ts b/src/modules/inbounds/measurement/api.ts index fecb83f6..6dc22d15 100644 --- a/src/modules/inbounds/measurement/api.ts +++ b/src/modules/inbounds/measurement/api.ts @@ -37,7 +37,9 @@ export async function getInboundMeasurement(customerId: string, inboundId: strin // items[].boxCode, machineNo, deviceMeasuredAt. Keep the response aligned with the backend OpenAPI. const response = await http.get, ApiEnvelope>( measurementPath(inboundId), - { params: { tenantId: customerId } }, + // Calculation-mode saves use a sibling PUT endpoint. A cache may otherwise keep + // returning the pre-save measurement and make a successful update look failed. + { params: { tenantId: customerId, requestAt: Date.now() } }, ) return unwrapApiResponse(response) } diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts b/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts index a4bbbc6d..f4317f72 100644 --- a/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts +++ b/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts @@ -323,6 +323,7 @@ describe('UnifiedOrdersView contract', () => { expect(source).toContain('仓库修改外箱(宽)') expect(source).toContain('仓库修改外箱(高)') expect(source).toContain('saveWarehouseOuterDimensions') + expect(source).toContain("'MEASURE_REJECTED', 'WAIT_SPLIT'") expect(source).not.toContain('请至少输入一项需要修改的外箱尺寸') expect(source).toContain(".map((value) => value == null || value === '' ? null : Number(value))") expect(source).toContain('相关数值已重新计算') @@ -366,12 +367,11 @@ describe('UnifiedOrdersView contract', () => { expect(source).toContain("updateMeasurementSectionMode('actual', $event)") expect(source).not.toContain("updateMeasurementSectionMode('outer', $event)") expect(source.match(/@change="selectAndSaveOuterMeasurementMode\(\$event\)"/g)).toHaveLength(2) - expect(source).toContain('@visible-change="handleOuterModeDropdownVisibleChange($event)"') + expect(source).not.toContain('@visible-change="handleOuterModeDropdownVisibleChange($event)"') expect(source.match(/:disabled="Boolean\(measurementStrategySavingId\) \|\| !can\('platform:firstleg:inbound:measure:edit'\)"/g)).toHaveLength(2) expect(source).toContain('const previousMode = measurementSectionModes.outer') expect(source).toContain('if (!saved) measurementSectionModes.outer = previousMode') - expect(source).toContain('const selectedSameMode = outerModeAtDropdownOpen.value === measurementSectionModes.outer') - expect(source).toContain('await nextTick()') + expect(source).not.toContain('handleOuterModeDropdownVisibleChange') expect(source).toContain('const saved = await updateMeasurementCalculationMode(null, mode, ruleId)') expect(source).toContain('calculatedMeasurementRow(row, measurementSectionModes.customer, true)') expect(source).toContain('calculatedMeasurementRow(source, measurementSectionModes.outer)') @@ -381,6 +381,7 @@ describe('UnifiedOrdersView contract', () => { expect(source).not.toContain("!customerSource && mode !== 'ACTUAL'") expect(source).toContain('rawDimensions.map((value) => value + rule.edgeAdditionCm * 2)') expect(source).toContain('matchedSpecificationCoefficient(chargeRule, calculationSource)') + expect(source).toContain(".find((item) => item.id === boundChargeRuleId && item.status === 'ACTIVE')") expect(source).toContain('listCustomerChargeRules(customerId, tenantId)') expect(source).toContain('matchedDimensions.map((value) => roundedUp(value * specificationCoefficient, 3))') expect(source).toContain('const selectedMode: MeasurementCalculationMode = measurementSectionModes.outer') @@ -405,9 +406,14 @@ describe('UnifiedOrdersView contract', () => { expect(source).toContain('outerLengthCm: item.outerLengthCm') expect(source).toContain('outerWidthCm: item.outerWidthCm') expect(source).toContain('outerHeightCm: item.outerHeightCm') - expect(source).toContain("String(item.chargeRuleId) === String(activeRule!.id)") - expect(source).toContain('item.calculatedOuterLengthCm, item.calculatedOuterWidthCm, item.calculatedOuterHeightCm') - expect(source).toContain('if (dimensions.every((dimension) => dimension == null)) return true') + expect(source).toContain('const savedDraft = await saveInboundMeasurementDraft') + expect(source).toContain('version: savedDraft.version') + expect(source).toContain('waitForSavedMeasurementVersion(') + expect(source).toContain('Number(refreshed.version) >= savedVersion') + expect(source).toContain('setTimeout(resolve, 250)') + expect(source).toContain("chargeRuleId: mode === 'MULTIPLY' ? activeRule!.id : null") + expect(source).toContain('calculatedOuterLengthCm: null') + expect(source).not.toContain('计算策略保存后回读不一致,请刷新后重试') expect(source).toContain('已保存并绑定计算规则,体积已重新计算') expect(source).toContain('const measuredGrossWeight = Number(measurementDetail.value?.actualGrossWeightKg)') expect(source).toContain('const measuredNetWeight = Number(measurementDetail.value?.actualNetWeightKg)') diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.vue b/src/modules/unified-orders/pages/UnifiedOrdersView.vue index d58d412f..fbcb3e86 100644 --- a/src/modules/unified-orders/pages/UnifiedOrdersView.vue +++ b/src/modules/unified-orders/pages/UnifiedOrdersView.vue @@ -188,7 +188,6 @@ type MeasurementCalculationMode = 'ACTUAL' | 'ADD' | 'MULTIPLY' const measurementSectionModes = reactive>({ customer: 'ACTUAL', outer: 'ACTUAL', actual: 'ACTUAL', }) -const outerModeAtDropdownOpen = ref(null) const cargoImagePreviewUrls = new Set() const measurementDialogOpen = ref(false) const openWarehouseSuggestionOnMeasurementLoad = ref(false) @@ -375,7 +374,10 @@ function calculatedMeasurementRow( const matchedDimensions = mode === 'ADD' && rule ? rawDimensions.map((value) => value + rule.edgeAdditionCm * 2) : rawDimensions - const chargeRule = activeMeasurementChargeRule.value + const boundChargeRuleId = String(source.chargeRuleId || '') + const chargeRule = measurementChargeRules.value + .find((item) => item.id === boundChargeRuleId && item.status === 'ACTIVE') + || activeMeasurementChargeRule.value const specificationCoefficient = mode === 'MULTIPLY' && chargeRule ? matchedSpecificationCoefficient(chargeRule, calculationSource) : 1 @@ -460,19 +462,17 @@ async function selectAndSaveOuterMeasurementMode(mode: MeasurementCalculationMod if (!saved) measurementSectionModes.outer = previousMode } -async function handleOuterModeDropdownVisibleChange(visible: boolean) { - if (visible) { - outerModeAtDropdownOpen.value = measurementSectionModes.outer - return - } - // Element Plus may emit dropdown-close before change. Wait until the change handler has - // updated the mode/saving state, otherwise one click can submit both the old and new mode. - await nextTick() - const selectedSameMode = outerModeAtDropdownOpen.value === measurementSectionModes.outer - outerModeAtDropdownOpen.value = null - if (selectedSameMode && !measurementStrategySavingId.value) { - void selectAndSaveOuterMeasurementMode(measurementSectionModes.outer) +async function waitForSavedMeasurementVersion(tenantId: string, inboundId: string, savedVersion: number) { + for (let attempt = 0; attempt < 6; attempt += 1) { + try { + const refreshed = await getInboundMeasurement(tenantId, inboundId) + if (Number(refreshed.version) >= savedVersion) return refreshed + } catch { + // 保存结果已由 PUT 确认,短暂的详情读取失败只影响同步等待。 + } + await new Promise((resolve) => setTimeout(resolve, 250)) } + return null } const statusTabs = computed(() => [ @@ -895,9 +895,6 @@ async function updateMeasurementCalculationMode(row: Record | n const targetIds = new Set(measurement.items .filter((item) => !row || (itemId ? String(item.id) === itemId : normalizedIdentifier(item.itemNo) === itemNo)) .map((item) => String(item.id))) - const targetItemNos = new Set(measurement.items - .filter((item) => targetIds.has(String(item.id))) - .map((item) => normalizedIdentifier(item.itemNo))) if (!targetIds.size) { ElMessage.warning('当前数据尚未关联实测明细,暂不能设置计算方式') return false @@ -921,7 +918,7 @@ async function updateMeasurementCalculationMode(row: Record | n ElMessage.warning('该客户尚未启用系数配置,请先到客户计费规则中设置默认规则') return false } - await saveInboundMeasurementDraft(String(summary.tenantId), String(summary.inboundId), { + const savedDraft = await saveInboundMeasurementDraft(String(summary.tenantId), String(summary.inboundId), { measurementId: String(measurement.measurementId), version: measurement.version, actualGoodsName: measurement.actualGoodsName, @@ -951,25 +948,28 @@ async function updateMeasurementCalculationMode(row: Record | n remark: item.remark, })), }) - measurementLoadedKey.value = '' - await loadMeasurementDetails() - const persistedItems = measurementDetail.value?.items - .filter((item) => targetItemNos.has(normalizedIdentifier(item.itemNo))) || [] - const persisted = persistedItems.length === targetItemNos.size && persistedItems.every((item) => ( - item.coefficientMode === mode - && (mode !== 'MULTIPLY' || String(item.chargeRuleId) === String(activeRule!.id)) - && [item.calculatedOuterLengthCm, item.calculatedOuterWidthCm, item.calculatedOuterHeightCm] - .every((value, index, dimensions) => { - // 旧服务实例可能尚未返回新版计算结果字段;计算方式已回读成功时不能误判并回滚下拉。 - if (dimensions.every((dimension) => dimension == null)) return true - if (value == null) return false - const expected = calculatedMeasurementRow(item as unknown as Record, mode) - return Math.abs(Number(value) - Number([expected.lengthCm, expected.widthCm, expected.heightCm][index])) < 0.001 - }) - )) - if (!persisted) { - throw new Error('计算策略保存后回读不一致,请刷新后重试') + // PUT 返回的新版本已经代表事务保存成功。详情查询可能经过读副本并短暂返回旧版本, + // 不能用紧随其后的旧数据否定本次成功保存;先按新版本和本次规则更新本地视图。 + measurementDetail.value = { + ...measurement, + version: savedDraft.version, + measurementStatus: savedDraft.status, + items: measurement.items.map((item) => targetIds.has(String(item.id)) ? { + ...item, + coefficientMode: mode, + chargeRuleId: mode === 'MULTIPLY' ? activeRule!.id : null, + calculatedOuterLengthCm: null, + calculatedOuterWidthCm: null, + calculatedOuterHeightCm: null, + } : item), } + measurementSectionModes.outer = mode + const refreshed = await waitForSavedMeasurementVersion( + String(summary.tenantId), String(summary.inboundId), savedDraft.version, + ) + if (refreshed) measurementDetail.value = refreshed + measurementSectionModes.outer = mode + measurementLoadedKey.value = '' ElMessage.success(`${row ? '当前数据' : '全部测量数据'}已保存并绑定计算规则,体积已重新计算`) return true } catch (error) { @@ -985,7 +985,9 @@ async function updateMeasurementCalculationMode(row: Record | n async function saveWarehouseOuterDimensions(row: Record) { const summary = detail.value?.summary const measurement = measurementDetail.value - const editableMeasurementStatuses = new Set(['WAIT_MEASURE', 'MEASURING', 'MEASURE_PENDING', 'MEASURE_REJECTED']) + const editableMeasurementStatuses = new Set([ + 'WAIT_MEASURE', 'MEASURING', 'MEASURE_PENDING', 'MEASURE_REJECTED', 'WAIT_SPLIT', + ]) const currentStatus = editableMeasurementStatuses.has(measurement?.processStatus || '') ? measurement!.processStatus : summary?.mainStatus || measurement?.processStatus || '' @@ -2125,7 +2127,7 @@ onBeforeUnmount(() => {
箱规计算规则选择 - +