fix(claim): align risk advice with expense rows

This commit is contained in:
caoxiaozhu
2026-06-15 20:53:48 +08:00
parent 5747e85acf
commit 792741709a
7 changed files with 596 additions and 178 deletions

View File

@@ -1657,7 +1657,15 @@ export default {
const cards = Array.isArray(aiAdvice.value?.riskCards) ? aiAdvice.value.riskCards : []
const actionableCards = cards.filter((card) => ['medium', 'high'].includes(normalizeRiskTone(card?.tone)))
return actionableCards.find((card) => String(card?.itemId || card?.item_id || '').trim() === itemId)
return actionableCards.find((card) => {
const cardItemIds = [
card?.itemId,
card?.item_id,
...(Array.isArray(card?.itemIds) ? card.itemIds : []),
...(Array.isArray(card?.item_ids) ? card.item_ids : [])
].map((value) => String(value || '').trim()).filter(Boolean)
return cardItemIds.includes(itemId)
})
|| actionableCards.find((card) => invoiceId && String(card?.invoiceId || card?.invoice_id || '').trim() === invoiceId)
|| actionableCards.find((card) => Number(card?.itemIndex || card?.item_index || 0) === itemIndex)
|| actionableCards.find((card) => itemIndex > 0 && String(card?.title || '').includes(`${itemIndex}`))

View File

@@ -181,6 +181,56 @@ function normalizeId(value) {
return normalizeText(value)
}
function normalizeIdList(value) {
const rawValues = Array.isArray(value)
? value
: normalizeText(value)
? [value]
: []
return [...new Set(rawValues.map((item) => normalizeId(item)).filter(Boolean))]
}
function buildExpenseItemIndexMap(expenseItems = []) {
const itemIndexById = new Map()
;(Array.isArray(expenseItems) ? expenseItems : []).forEach((item, index) => {
const itemId = normalizeId(item?.id)
if (itemId && !itemIndexById.has(itemId)) {
itemIndexById.set(itemId, index + 1)
}
})
return itemIndexById
}
function resolveRiskItemNumbers({ itemId = '', itemIds = [], itemIndex = null } = {}, expenseItems = []) {
const itemIndexById = buildExpenseItemIndexMap(expenseItems)
const itemNumbers = []
const explicitItemIndex = Number(itemIndex)
if (Number.isFinite(explicitItemIndex) && explicitItemIndex > 0) {
itemNumbers.push(Math.floor(explicitItemIndex))
}
const relatedItemIds = uniqueTexts([
normalizeId(itemId),
...normalizeIdList(itemIds)
])
relatedItemIds.forEach((relatedItemId) => {
const resolvedIndex = itemIndexById.get(relatedItemId)
if (resolvedIndex) {
itemNumbers.push(resolvedIndex)
}
})
return [...new Set(itemNumbers)].sort((left, right) => left - right)
}
function buildRiskTitleWithItemNumbers(title, itemNumbers = []) {
const cleanTitle = normalizeText(title) || '单据风险提示'
if (!itemNumbers.length || /^第\s*[\d、,\s]+\s*条[:]/.test(cleanTitle)) {
return cleanTitle
}
return `${itemNumbers.join('、')} 条:${cleanTitle}`
}
function resolveItemRiskFlag(item, claimRiskFlags) {
const itemId = normalizeId(item?.id)
if (!itemId || !Array.isArray(claimRiskFlags)) {
@@ -197,8 +247,9 @@ function resolveItemRiskFlag(item, claimRiskFlags) {
}
const flagItemId = normalizeId(flag.item_id || flag.itemId)
const flagItemIds = normalizeIdList(flag.item_ids || flag.itemIds)
const tone = resolveFlagTone(flag)
return flagItemId === itemId && isRiskTone(tone)
return (flagItemId === itemId || flagItemIds.includes(itemId)) && isRiskTone(tone)
}) || null
}
@@ -526,6 +577,78 @@ function buildManualReturnRiskCard(flag, businessStage = 'reimbursement') {
})
}
function isGenericAutoReviewSummaryFlag(flag) {
if (!flag || typeof flag !== 'object') {
return false
}
const source = normalizeText(flag.source)
const label = normalizeText(flag.label || flag.title || flag.name)
if (source !== 'submission_review' || !['自动检测重点复核', '自动检测提醒'].includes(label)) {
return false
}
const hasConcreteRule = normalizeText(flag.rule_code || flag.ruleCode || flag.hit_source || flag.hitSource)
const hasConcreteItem = (
normalizeText(flag.item_id || flag.itemId || flag.invoice_id || flag.invoiceId)
|| normalizeIdList(flag.item_ids || flag.itemIds).length
)
if (hasConcreteRule || hasConcreteItem) {
return false
}
return /^自动检测发现/.test(normalizeText(flag.message || flag.summary || flag.reason))
}
function isHotelOverStandardRiskText(value) {
const text = normalizeText(value)
return /住宿|酒店|宾馆/.test(text) && /超标|超出|报销标准|住宿标准|差标/.test(text)
}
function isCoveredByAttachmentHotelOverStandardRisk(flag, attachmentCards = []) {
if (!isHotelOverStandardRiskText(cardLikeText(flag))) {
return false
}
return attachmentCards.some((card) => isHotelOverStandardRiskText(cardLikeText(card)))
}
function isRouteLevelRiskText(value) {
const text = normalizeText(value)
return /行程|多城市|目的地|票据城市|差旅地点|中转|改签|异地/.test(text)
}
function isTravelRouteExpenseItem(item = {}) {
const text = [
item.name,
item.category,
item.desc,
item.detail,
item.itemType,
item.item_type
].map((value) => normalizeText(value)).join(' ')
if (/补贴|系统自动计算/.test(text)) {
return false
}
return /火车|高铁|机票|航班|交通票|出发|返回|中转|起始地|目的地|[--—~至]/.test(text)
}
function inferRelatedItemIdsForRisk(flag, risks, expenseItems) {
const text = [
cardLikeText(flag),
...listRiskTextValues(risks)
].map((value) => normalizeText(value)).join(' ')
if (!isRouteLevelRiskText(text)) {
return []
}
return (Array.isArray(expenseItems) ? expenseItems : [])
.filter((item) => normalizeId(item?.id) && isTravelRouteExpenseItem(item))
.map((item) => normalizeId(item.id))
}
function listRiskTextValues(risks) {
return Array.isArray(risks) ? risks : []
}
export function buildAttachmentRiskCards({
expenseItems = [],
attachmentMetaByItemId = {},
@@ -581,6 +704,12 @@ export function buildAttachmentRiskCards({
if (flag && typeof flag === 'object' && normalizeText(flag.source) === 'ai_pre_review') {
return []
}
if (isGenericAutoReviewSummaryFlag(flag)) {
return []
}
if (isCoveredByAttachmentHotelOverStandardRisk(flag, attachmentCards)) {
return []
}
if (!flag || typeof flag !== 'object') {
if (!isActionableRiskFlag(flag)) {
@@ -609,6 +738,7 @@ export function buildAttachmentRiskCards({
const source = normalizeText(flag.source)
const flagItemId = normalizeId(flag.item_id || flag.itemId)
const flagItemIds = normalizeIdList(flag.item_ids || flag.itemIds)
if (source === 'attachment_analysis' && flagItemId && attachmentRiskItemIds.has(flagItemId)) {
return []
}
@@ -626,6 +756,19 @@ export function buildAttachmentRiskCards({
const risks = flagPoints.length
? flagPoints
: [primaryRisk || fallbackRisk].filter(Boolean)
const relatedItemIds = flagItemIds.length
? flagItemIds
: inferRelatedItemIdsForRisk(flag, risks, expenseItems)
const itemIndex = Number(flag.item_index ?? flag.itemIndex ?? 0) || null
const relatedItemNumbers = resolveRiskItemNumbers({
itemId: flagItemId,
itemIds: relatedItemIds,
itemIndex
}, expenseItems)
const title = buildRiskTitleWithItemNumbers(
normalizeText(flag.title || flag.label || flag.name || flag.rule_name || flag.ruleCode || flag.rule_code) || '单据风险提示',
relatedItemNumbers
)
const summary = normalizeText(flag.summary || flag.message || flag.reason)
const ruleBasis = resolveClaimRiskRuleBasis(flag, {
risk: risks[0] || primaryRisk || fallbackRisk,
@@ -635,13 +778,14 @@ export function buildAttachmentRiskCards({
return risks.map((risk, pointIndex) => withRiskTags({
id: `claim-risk-${index}-${pointIndex}`,
itemId: flagItemId,
itemIndex: Number(flag.item_index ?? flag.itemIndex ?? 0) || null,
itemId: flagItemId || (relatedItemIds.length === 1 ? relatedItemIds[0] : ''),
itemIds: relatedItemIds,
itemIndex,
invoiceId: normalizeText(flag.invoice_id || flag.invoiceId),
businessStage: resolveFlagBusinessStage(flag, normalizedBusinessStage),
tone,
label: resolveRiskLevelLabel(tone),
title: normalizeText(flag.title || flag.label || flag.name || flag.rule_name || flag.ruleCode || flag.rule_code) || '单据风险提示',
title,
risk,
summary,
ruleBasis,