2026-05-29 10:13:49 +08:00
|
|
|
|
import {
|
|
|
|
|
|
JSON_RISK_DETAIL_MODE,
|
|
|
|
|
|
LEGACY_RISK_SCENARIO_KEYS,
|
|
|
|
|
|
RISK_SCENARIO_VALUES,
|
|
|
|
|
|
RULE_TAB_TAG_ALIASES,
|
|
|
|
|
|
SPREADSHEET_DETAIL_MODE,
|
|
|
|
|
|
TAB_META,
|
|
|
|
|
|
TYPE_META
|
|
|
|
|
|
} from './auditViewMetadata.js'
|
|
|
|
|
|
import {
|
|
|
|
|
|
isPlainObject,
|
|
|
|
|
|
normalizeText,
|
|
|
|
|
|
readConfigJson
|
|
|
|
|
|
} from './auditViewDataUtils.js'
|
|
|
|
|
|
import { formatScenarioList } from './auditViewFormatters.js'
|
|
|
|
|
|
const EXPENSE_TYPE_SCENARIO_LABELS = {
|
2026-06-01 17:07:14 +08:00
|
|
|
|
all: '全部',
|
2026-05-29 10:13:49 +08:00
|
|
|
|
travel: '差旅费',
|
|
|
|
|
|
hotel: '住宿费',
|
|
|
|
|
|
transport: '交通费',
|
|
|
|
|
|
meal: '业务招待费',
|
|
|
|
|
|
meeting: '会务费',
|
|
|
|
|
|
marketing: '市场推广费',
|
|
|
|
|
|
office: '办公用品费',
|
|
|
|
|
|
training: '培训费',
|
|
|
|
|
|
software: '软件服务费',
|
|
|
|
|
|
communication: '通信费',
|
|
|
|
|
|
welfare: '福利费'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function readRuleDocumentMeta(value) {
|
|
|
|
|
|
const configJson = readConfigJson(value)
|
|
|
|
|
|
return isPlainObject(configJson.rule_document) ? configJson.rule_document : null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function isSpreadsheetRuleSource(value) {
|
|
|
|
|
|
const configJson = readConfigJson(value)
|
|
|
|
|
|
return normalizeText(configJson.detail_mode || configJson.rule_detail_mode).toLowerCase() === SPREADSHEET_DETAIL_MODE
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function isJsonRiskRuleSource(value) {
|
|
|
|
|
|
const configJson = readConfigJson(value)
|
|
|
|
|
|
return normalizeText(configJson.detail_mode || configJson.rule_detail_mode).toLowerCase() === JSON_RISK_DETAIL_MODE
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function normalizeRuleTagValue(value) {
|
|
|
|
|
|
return normalizeText(value).toLowerCase().replace(/[\s_-]+/g, '')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function collectRuleTagValues(source) {
|
|
|
|
|
|
const configJson = readConfigJson(source)
|
|
|
|
|
|
const rawValues = [
|
|
|
|
|
|
configJson.tag,
|
|
|
|
|
|
configJson.rule_tag,
|
|
|
|
|
|
...(Array.isArray(configJson.tags) ? configJson.tags : []),
|
|
|
|
|
|
...(Array.isArray(configJson.rule_tags) ? configJson.rule_tags : [])
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
return rawValues.map((item) => normalizeText(item)).filter(Boolean)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function resolveRuleTabId(source) {
|
|
|
|
|
|
const code = normalizeText(source?.code || '').toLowerCase()
|
|
|
|
|
|
if (code.startsWith('risk.')) {
|
|
|
|
|
|
return 'riskRules'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (isJsonRiskRuleSource(source)) {
|
|
|
|
|
|
return 'riskRules'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const normalizedTags = collectRuleTagValues(source).map((item) => normalizeRuleTagValue(item))
|
|
|
|
|
|
|
|
|
|
|
|
if (normalizedTags.some((item) => RULE_TAB_TAG_ALIASES.riskRules.has(item))) {
|
|
|
|
|
|
return 'riskRules'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (normalizedTags.some((item) => RULE_TAB_TAG_ALIASES.financialRules.has(item))) {
|
|
|
|
|
|
return 'financialRules'
|
|
|
|
|
|
}
|
|
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function resolveTabId(source, typeKey) {
|
|
|
|
|
|
if (typeKey === 'rules') {
|
|
|
|
|
|
return resolveRuleTabId(source)
|
|
|
|
|
|
}
|
|
|
|
|
|
return typeKey
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function resolveTabMeta(tabId, typeKey) {
|
|
|
|
|
|
if (TAB_META[tabId]) {
|
|
|
|
|
|
return TAB_META[tabId]
|
|
|
|
|
|
}
|
|
|
|
|
|
if (typeKey === 'rules') {
|
|
|
|
|
|
return {
|
|
|
|
|
|
...TYPE_META.rules,
|
|
|
|
|
|
typeKey: 'rules',
|
|
|
|
|
|
badgeTone: 'primary'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return TAB_META[typeKey]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function resolveRiskRuleDescription(payload) {
|
|
|
|
|
|
if (!isPlainObject(payload)) {
|
|
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
return normalizeText(payload.description)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function resolveRiskRuleSourceRef(payload) {
|
|
|
|
|
|
if (!isPlainObject(payload)) {
|
|
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
const metadata = isPlainObject(payload.metadata) ? payload.metadata : {}
|
|
|
|
|
|
return normalizeText(metadata.source_ref)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function inferRiskCategoryFromCode(code) {
|
|
|
|
|
|
const normalized = normalizeText(code).toLowerCase()
|
|
|
|
|
|
if (normalized.startsWith('risk.travel.')) {
|
|
|
|
|
|
return '差旅'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (normalized.startsWith('risk.invoice.')) {
|
|
|
|
|
|
return '发票'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (normalized.includes('entertainment') || normalized.includes('meal_localized')) {
|
|
|
|
|
|
return '餐饮招待'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (normalized.includes('consecutive_transport')) {
|
|
|
|
|
|
return '交通出行'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (normalized.startsWith('risk.expense.')) {
|
|
|
|
|
|
return '费用科目'
|
|
|
|
|
|
}
|
|
|
|
|
|
return '通用'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function normalizeRiskScenarioCategory(value) {
|
|
|
|
|
|
const normalized = normalizeText(value)
|
|
|
|
|
|
const alias = normalized === '通讯费' ? '通信费' : normalized
|
|
|
|
|
|
return RISK_SCENARIO_VALUES.has(alias) ? alias : ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function normalizeExpenseTypeScenarioLabels(value) {
|
|
|
|
|
|
const values = Array.isArray(value) ? value : normalizeText(value) ? [value] : []
|
2026-06-01 17:07:14 +08:00
|
|
|
|
if (values.some((item) => ['all', '*', 'overall', 'general', '全部', '通用'].includes(normalizeText(item).toLowerCase()))) {
|
|
|
|
|
|
return ['全部']
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-29 10:13:49 +08:00
|
|
|
|
const labels = []
|
|
|
|
|
|
const seen = new Set()
|
|
|
|
|
|
|
|
|
|
|
|
values.forEach((item) => {
|
|
|
|
|
|
const key = normalizeText(item).toLowerCase()
|
|
|
|
|
|
const label = EXPENSE_TYPE_SCENARIO_LABELS[key] || normalizeRiskScenarioCategory(item)
|
|
|
|
|
|
if (!label || seen.has(label)) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
seen.add(label)
|
|
|
|
|
|
labels.push(label)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
return labels
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function readRiskRuleExpenseTypes(source) {
|
|
|
|
|
|
const configJson = readConfigJson(source)
|
|
|
|
|
|
const metadata = isPlainObject(configJson.metadata) ? configJson.metadata : {}
|
|
|
|
|
|
const appliesTo = isPlainObject(configJson.applies_to) ? configJson.applies_to : {}
|
|
|
|
|
|
const values = []
|
|
|
|
|
|
|
|
|
|
|
|
;[
|
|
|
|
|
|
configJson.expense_types,
|
|
|
|
|
|
metadata.expense_types,
|
|
|
|
|
|
appliesTo.expense_types,
|
|
|
|
|
|
source?.expense_types
|
|
|
|
|
|
].forEach((item) => {
|
|
|
|
|
|
if (Array.isArray(item)) {
|
|
|
|
|
|
values.push(...item)
|
|
|
|
|
|
} else if (normalizeText(item)) {
|
|
|
|
|
|
values.push(item)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
return values
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function readScenarioItems(source) {
|
|
|
|
|
|
if (Array.isArray(source?.scenario_json)) {
|
|
|
|
|
|
return source.scenario_json
|
|
|
|
|
|
}
|
|
|
|
|
|
if (Array.isArray(source?.scenarioList)) {
|
|
|
|
|
|
return source.scenarioList
|
|
|
|
|
|
}
|
|
|
|
|
|
return []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function resolveRiskRuleCategory(source) {
|
|
|
|
|
|
const configJson = readConfigJson(source)
|
|
|
|
|
|
const expenseScenarioLabels = normalizeExpenseTypeScenarioLabels(readRiskRuleExpenseTypes(source))
|
|
|
|
|
|
if (expenseScenarioLabels.length) {
|
|
|
|
|
|
return formatScenarioList(expenseScenarioLabels)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const expenseCategoryLabel =
|
|
|
|
|
|
normalizeText(configJson.expense_category_label) ||
|
|
|
|
|
|
normalizeText(configJson.metadata?.expense_category_label) ||
|
|
|
|
|
|
normalizeText(source?.expense_category_label)
|
|
|
|
|
|
if (expenseCategoryLabel) {
|
|
|
|
|
|
return expenseCategoryLabel
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const explicit = normalizeRiskScenarioCategory(configJson.risk_category)
|
|
|
|
|
|
if (explicit) {
|
|
|
|
|
|
return explicit
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const payloadCategory = normalizeRiskScenarioCategory(source?.risk_category)
|
|
|
|
|
|
if (payloadCategory) {
|
|
|
|
|
|
return payloadCategory
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const scenarioItems = readScenarioItems(source)
|
|
|
|
|
|
const businessScenario = scenarioItems
|
|
|
|
|
|
.map((item) => normalizeText(item))
|
|
|
|
|
|
.find((item) => item && !LEGACY_RISK_SCENARIO_KEYS.has(item) && RISK_SCENARIO_VALUES.has(item))
|
|
|
|
|
|
if (businessScenario) {
|
|
|
|
|
|
return businessScenario
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return inferRiskCategoryFromCode(source?.code)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function inferFinancialRuleCategory(source) {
|
|
|
|
|
|
const configJson = readConfigJson(source)
|
|
|
|
|
|
const explicit =
|
|
|
|
|
|
normalizeRiskScenarioCategory(configJson.scenario_category) ||
|
|
|
|
|
|
normalizeRiskScenarioCategory(configJson.ai_review_category) ||
|
|
|
|
|
|
normalizeRiskScenarioCategory(configJson.risk_category) ||
|
|
|
|
|
|
normalizeRiskScenarioCategory(source?.scenario_category) ||
|
|
|
|
|
|
normalizeRiskScenarioCategory(source?.risk_category)
|
|
|
|
|
|
if (explicit) {
|
|
|
|
|
|
return explicit
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const scenarioCategory = readScenarioItems(source)
|
|
|
|
|
|
.map((item) => normalizeRiskScenarioCategory(item))
|
|
|
|
|
|
.find(Boolean)
|
|
|
|
|
|
if (scenarioCategory) {
|
|
|
|
|
|
return scenarioCategory
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const configRuntimeRule = isPlainObject(configJson.runtime_rule) ? configJson.runtime_rule : {}
|
|
|
|
|
|
const haystack = [
|
|
|
|
|
|
source?.code,
|
|
|
|
|
|
source?.name,
|
|
|
|
|
|
source?.description,
|
|
|
|
|
|
configJson.runtime_kind,
|
|
|
|
|
|
configRuntimeRule.kind,
|
|
|
|
|
|
configRuntimeRule.scenario,
|
|
|
|
|
|
configRuntimeRule.template_key,
|
|
|
|
|
|
...readScenarioItems(source)
|
|
|
|
|
|
]
|
|
|
|
|
|
.map((item) => normalizeText(item).toLowerCase())
|
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
|
.join(' ')
|
|
|
|
|
|
|
|
|
|
|
|
if (!haystack) {
|
|
|
|
|
|
return '通用'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (/(travel|trip|差旅|出差|住宿|酒店)/i.test(haystack)) {
|
|
|
|
|
|
return '差旅'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (/(invoice|receipt|attachment|票据|发票|单据|附件)/i.test(haystack)) {
|
|
|
|
|
|
return '发票'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (/(meal|dining|entertainment|餐饮|招待|餐费|用餐)/i.test(haystack)) {
|
|
|
|
|
|
return '餐饮招待'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (/(transport|traffic|taxi|交通|出行|打车|机票|火车|高铁|地铁|公交)/i.test(haystack)) {
|
|
|
|
|
|
return '交通出行'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (/(office|material|suppl|办公|物料|耗材)/i.test(haystack)) {
|
|
|
|
|
|
return '办公物料'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (/(communication|telecom|phone|通信|通讯|手机)/i.test(haystack)) {
|
|
|
|
|
|
return '通信费'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (/(welfare|福利)/i.test(haystack)) {
|
|
|
|
|
|
return '福利费'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (/(expense_standard|费用科目|费用标准|补贴|科目)/i.test(haystack)) {
|
|
|
|
|
|
return '费用科目'
|
|
|
|
|
|
}
|
|
|
|
|
|
return '通用'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function resolveRuleScenarioCategory(source, tabId = '') {
|
|
|
|
|
|
const scenarioList = resolveRuleScenarioList(source, tabId)
|
|
|
|
|
|
if (scenarioList.length) {
|
|
|
|
|
|
return formatScenarioList(scenarioList)
|
|
|
|
|
|
}
|
|
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function resolveRuleScenarioList(source, tabId = '') {
|
|
|
|
|
|
const resolvedTabId = tabId || resolveRuleTabId(source)
|
|
|
|
|
|
if (resolvedTabId === 'riskRules' || isJsonRiskRuleSource(source)) {
|
|
|
|
|
|
const expenseScenarioLabels = normalizeExpenseTypeScenarioLabels(readRiskRuleExpenseTypes(source))
|
|
|
|
|
|
if (expenseScenarioLabels.length) {
|
|
|
|
|
|
return expenseScenarioLabels
|
|
|
|
|
|
}
|
|
|
|
|
|
const riskCategory = resolveRiskRuleCategory(source)
|
|
|
|
|
|
return riskCategory ? [riskCategory] : []
|
|
|
|
|
|
}
|
|
|
|
|
|
if (resolvedTabId === 'financialRules') {
|
|
|
|
|
|
const financialCategory = inferFinancialRuleCategory(source)
|
|
|
|
|
|
return financialCategory ? [financialCategory] : []
|
|
|
|
|
|
}
|
|
|
|
|
|
return []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function buildRiskListSubtitle(text, maxLength = 42) {
|
|
|
|
|
|
const normalized = normalizeText(text)
|
|
|
|
|
|
if (!normalized) {
|
|
|
|
|
|
return '平台内置风险规则'
|
|
|
|
|
|
}
|
|
|
|
|
|
const firstSentence = normalized.split(/[。;;!?\n]/)[0] || normalized
|
|
|
|
|
|
if (firstSentence.length <= maxLength) {
|
|
|
|
|
|
return firstSentence
|
|
|
|
|
|
}
|
|
|
|
|
|
return `${firstSentence.slice(0, maxLength)}…`
|
|
|
|
|
|
}
|