refactor(audit): reuse list shells and split models
This commit is contained in:
328
web/src/views/scripts/auditViewRuleClassifier.js
Normal file
328
web/src/views/scripts/auditViewRuleClassifier.js
Normal file
@@ -0,0 +1,328 @@
|
||||
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 = {
|
||||
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] : []
|
||||
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)}…`
|
||||
}
|
||||
Reference in New Issue
Block a user