Files
X-Financial/web/src/views/scripts/auditViewRuleClassifier.js
caoxiaozhu 92444e7eae feat: 扩展风险规则体系、审批动态路由与预算中心列表化改造
- 新增 25+ 条风险规则(预算/报销/申请/通用类),完善风险规则模拟与反馈发布机制
- 引入费用审批动态路由、平台风险分级、预审与风险阶段管理
- 预算中心列表化改造,优化票据夹仪表盘与数字员工工作看板
- 新增 Hermes 风险线索收集器、Agent 链路追踪中心
- 扩展数字员工能力库(18 个领域 Skill)与交通费用自动预估
- 完善报销申请快速预览、权限控制与前端测试覆盖
2026-06-01 17:07:14 +08:00

334 lines
9.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 = {
all: '全部',
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] : []
if (values.some((item) => ['all', '*', 'overall', 'general', '全部', '通用'].includes(normalizeText(item).toLowerCase()))) {
return ['全部']
}
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)}`
}