修复旧码同名明细核销选择

This commit is contained in:
andy
2026-07-28 12:43:31 +08:00
parent bcfe1688b1
commit 042fc2ac80
3 changed files with 168 additions and 27 deletions

View File

@@ -3,7 +3,9 @@ import { test } from 'node:test'
import {
findWriteOffItem,
getWriteOffItemAvailability,
getWriteOffItemQuantity
getWriteOffItemQuantity,
getWriteOffStatusText,
resolveWriteOffItemSelection
} from '../writeOffItem.ts'
const items = [
@@ -51,3 +53,64 @@ test('marks legacy or unsupported or completed item unavailable', () => {
assert.equal(getWriteOffItemAvailability({ ...items[0], snapshotOperationSupported: false }).available, false)
assert.equal(getWriteOffItemAvailability({ ...items[0], writeOffCount: 2 }).available, false)
})
test('locks scanned v2 code to the specified write off item id', () => {
const selection = resolveWriteOffItemSelection(items, {
source: 'scan',
orderWriteOffItemId: 'item-2',
packageName: '双人套餐'
})
assert.equal(selection.locked, true)
assert.deepEqual(selection.options.map((item) => item.orderWriteOffItemId), ['item-2'])
assert.equal(selection.selectedItemId, 'item-2')
})
test('does not auto select when legacy package name matches multiple available items', () => {
const selection = resolveWriteOffItemSelection(items, {
source: 'scan',
packageName: '双人套餐'
})
assert.equal(selection.mismatch, false)
assert.equal(selection.requiresManualSelection, true)
assert.equal(selection.selectedItemId, '')
assert.deepEqual(selection.options.map((item) => item.orderWriteOffItemId), ['item-1', 'item-2'])
})
test('auto selects the only available item matched by legacy package name', () => {
const selection = resolveWriteOffItemSelection(
[
{ ...items[0], writeOffCount: 2 },
items[1]
],
{
source: 'scan',
packageName: '双人套餐'
}
)
assert.equal(selection.selectedItemId, 'item-2')
assert.equal(selection.requiresManualSelection, false)
})
test('default selection skips unavailable items', () => {
const selection = resolveWriteOffItemSelection([
{ ...items[0], writeOffCount: 2 },
items[1]
])
assert.equal(selection.selectedItemId, 'item-2')
})
test('infers partial write off status from quantity when status is 0', () => {
assert.equal(getWriteOffStatusText({ ...items[0], writeOffStatus: 0, writeOffCount: 0 }), '待核销')
assert.equal(getWriteOffStatusText({ ...items[0], writeOffStatus: 0, writeOffCount: 1 }), '部分核销')
assert.equal(getWriteOffStatusText({ ...items[0], writeOffStatus: 0, writeOffCount: 2 }), '已核销')
assert.equal(getWriteOffStatusText({ ...items[0], writeOffStatus: 1, writeOffCount: 0 }), '已核销')
})
test('does not override explicit non-zero write off status with quantity inference', () => {
assert.equal(getWriteOffStatusText({ ...items[0], writeOffStatus: 2, writeOffCount: 0 }), '部分核销')
assert.equal(getWriteOffStatusText({ ...items[0], writeOffStatus: 'PART_WRITE_OFF', writeOffCount: 0 }), '部分核销')
})

View File

@@ -17,6 +17,21 @@ export interface WriteOffItemAvailability {
reason: string
}
export interface WriteOffItemSelectionInput {
source?: string
orderWriteOffItemId?: string
packageName?: string
fallbackName?: string
}
export interface WriteOffItemSelection {
options: CommodityPackageConfig[]
selectedItemId: string
locked: boolean
mismatch: boolean
requiresManualSelection: boolean
}
const statusTextMap: Record<string, string> = {
'0': '待核销',
'1': '已核销',
@@ -86,9 +101,79 @@ export const getFirstAvailableWriteOffItem = (items: CommodityPackageConfig[]) =
return items.find((item) => getWriteOffItemAvailability(item).available)
}
const matchesWriteOffItemName = (item: CommodityPackageConfig, packageName: string, fallbackName = '') => {
return getWriteOffItemName(item, fallbackName) === packageName || item.packageName === packageName
}
export const resolveWriteOffItemSelection = (
items: CommodityPackageConfig[],
input: WriteOffItemSelectionInput = {}
): WriteOffItemSelection => {
const source = String(input.source || '')
const itemId = String(input.orderWriteOffItemId || '').trim()
const packageName = String(input.packageName || '').trim()
const isScan = source === 'scan'
const locked = isScan && Boolean(itemId)
let options = items
if (locked) {
options = items.filter((item) => getWriteOffItemId(item) === itemId)
} else if (isScan && packageName) {
options = items.filter((item) => matchesWriteOffItemName(item, packageName, input.fallbackName))
}
const mismatch = isScan && Boolean(itemId || packageName) && options.length === 0
const availableOptions = options.filter((item) => getWriteOffItemAvailability(item).available)
if (locked) {
return {
options,
selectedItemId: getWriteOffItemId(options[0]),
locked,
mismatch,
requiresManualSelection: false
}
}
if (isScan && packageName && availableOptions.length > 1) {
return {
options,
selectedItemId: '',
locked,
mismatch,
requiresManualSelection: true
}
}
const matchedById = itemId ? options.find((item) => getWriteOffItemId(item) === itemId) : undefined
const selectedItem = (
matchedById && getWriteOffItemAvailability(matchedById).available
? matchedById
: availableOptions[0] || matchedById || options[0]
)
return {
options,
selectedItemId: getWriteOffItemId(selectedItem),
locked,
mismatch,
requiresManualSelection: false
}
}
export const getWriteOffStatusText = (item?: CommodityPackageConfig) => {
const status = item?.writeOffStatus ?? item?.packageStatus
if (status === undefined || status === null || status === '') return ''
const key = String(status)
const writtenOff = toNumber(item?.writeOffCount)
const total = toNumber(item?.count)
if (key === '1') return '已核销'
if ((status === undefined || status === null || status === '' || key === '0') && (writtenOff !== undefined || total !== undefined)) {
const writtenOffCount = Math.max(writtenOff ?? 0, 0)
const totalCount = Math.max(total ?? 0, 0)
if (writtenOffCount <= 0) return '待核销'
if (totalCount > 0 && writtenOffCount >= totalCount) return '已核销'
return '部分核销'
}
if (status === undefined || status === null || status === '') return ''
return statusTextMap[key] || key
}

View File

@@ -9,14 +9,13 @@ import StatusTag from '@/components/StatusTag.vue'
import type { CommodityPackageConfig, UserOrderDetail } from '@/types/order'
import { canWriteOff } from '@/utils/constants'
import {
findWriteOffItem,
getFirstAvailableWriteOffItem,
getWriteOffItemAvailability,
getWriteOffItemContent,
getWriteOffItemId,
getWriteOffItemName,
getWriteOffItemQuantity,
getWriteOffStatusText,
resolveWriteOffItemSelection,
toNumber
} from '@/utils/writeOffItem'
import {
@@ -36,8 +35,6 @@ const orderId = computed(() => String(route.query.orderId || ''))
const routePackageName = computed(() => String(route.query.packageName || '').trim())
const routeWriteOffItemId = computed(() => String(route.query.orderWriteOffItemId || '').trim())
const isScanSource = computed(() => String(route.query.source || '') === 'scan')
const isScanItemLocked = computed(() => isScanSource.value && Boolean(routeWriteOffItemId.value))
const hasScanMatchCriteria = computed(() => isScanSource.value && Boolean(routeWriteOffItemId.value || routePackageName.value))
const packageConfigs = computed<CommodityPackageConfig[]>(() => {
if (!detail.value) return []
@@ -51,24 +48,22 @@ const packageConfigs = computed<CommodityPackageConfig[]>(() => {
]
})
const packageOptions = computed(() => {
if (isScanSource.value && routeWriteOffItemId.value) {
return packageConfigs.value.filter((item) => getWriteOffItemId(item) === routeWriteOffItemId.value)
}
if (isScanSource.value && routePackageName.value) {
return packageConfigs.value.filter(
(item) =>
getWriteOffItemName(item, detail.value?.commodityName) === routePackageName.value ||
item.packageName === routePackageName.value
)
}
return packageConfigs.value
const itemSelection = computed(() => {
return resolveWriteOffItemSelection(packageConfigs.value, {
source: String(route.query.source || ''),
orderWriteOffItemId: routeWriteOffItemId.value,
packageName: routePackageName.value,
fallbackName: detail.value?.commodityName
})
})
const packageMismatch = computed(() => hasScanMatchCriteria.value && packageOptions.value.length === 0)
const packageOptions = computed(() => itemSelection.value.options)
const isScanItemLocked = computed(() => itemSelection.value.locked)
const packageMismatch = computed(() => itemSelection.value.mismatch)
const requiresManualSelection = computed(() => itemSelection.value.requiresManualSelection)
const selectedPackage = computed(() => {
return packageOptions.value.find((item) => getWriteOffItemId(item) === selectedItemId.value) || packageOptions.value[0]
return packageOptions.value.find((item) => getWriteOffItemId(item) === selectedItemId.value)
})
const quantityInfo = computed(() => getWriteOffItemQuantity(selectedPackage.value))
@@ -119,13 +114,7 @@ const loadDetail = async () => {
loading.value = true
try {
detail.value = await fetchOrderDetail(orderId.value)
const matchedPackage = findWriteOffItem(packageConfigs.value, {
orderWriteOffItemId: routeWriteOffItemId.value,
packageName: routePackageName.value
})
const firstAvailablePackage = getFirstAvailableWriteOffItem(packageOptions.value)
const fallbackPackage = packageOptions.value[0] || packageConfigs.value[0]
selectedItemId.value = getWriteOffItemId(matchedPackage || firstAvailablePackage || fallbackPackage)
selectedItemId.value = itemSelection.value.selectedItemId
} finally {
loading.value = false
}
@@ -282,6 +271,10 @@ onMounted(loadDetail)
<BadgeCheck :size="20" />
<span>该订单当前不可核销请返回订单详情核对状态</span>
</section>
<section v-else-if="requiresManualSelection" class="panel warning-panel">
<BadgeCheck :size="20" />
<span>扫码套餐匹配到多个可核销明细请选择后再核销</span>
</section>
<section v-else-if="selectedPackage && !selectedItemAvailability.available" class="panel warning-panel">
<BadgeCheck :size="20" />
<span>{{ selectedItemAvailability.reason }}</span>