feat: 新增预算后端服务与差旅风险规则库
后端新增预算模型、端点和服务模块,支持预算 CRUD 和余额 查询,清理旧生成规则文件并替换为按严重等级分类的差旅风 险规则库,优化认证权限和报销单访问策略,新增财务规则目 录和演示数据构建脚本,前端预算中心增加对话框交互,完善 审计页面运行时模型和元数据展示,补充单元测试。
This commit is contained in:
@@ -35,6 +35,20 @@ import {
|
||||
resolveRiskRuleSeverityLabel
|
||||
} from './auditViewRiskRuleModel.js'
|
||||
|
||||
const EXPENSE_TYPE_SCENARIO_LABELS = {
|
||||
travel: '差旅费',
|
||||
hotel: '住宿费',
|
||||
transport: '交通费',
|
||||
meal: '业务招待费',
|
||||
meeting: '会务费',
|
||||
marketing: '市场推广费',
|
||||
office: '办公用品费',
|
||||
training: '培训费',
|
||||
software: '软件服务费',
|
||||
communication: '通信费',
|
||||
welfare: '福利费'
|
||||
}
|
||||
|
||||
export {
|
||||
DETAIL_TITLES,
|
||||
DOMAIN_LABELS,
|
||||
@@ -375,7 +389,48 @@ export function inferRiskCategoryFromCode(code) {
|
||||
|
||||
export function normalizeRiskScenarioCategory(value) {
|
||||
const normalized = normalizeText(value)
|
||||
return RISK_SCENARIO_VALUES.has(normalized) ? normalized : ''
|
||||
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) {
|
||||
@@ -390,6 +445,11 @@ export function readScenarioItems(source) {
|
||||
|
||||
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) ||
|
||||
@@ -471,23 +531,43 @@ export function inferFinancialRuleCategory(source) {
|
||||
if (/(office|material|suppl|办公|物料|耗材)/i.test(haystack)) {
|
||||
return '办公物料'
|
||||
}
|
||||
if (/(communication|telecom|phone|expense_standard|费用科目|费用标准|通信|通讯|手机|补贴|福利|科目)/i.test(haystack)) {
|
||||
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 resolvedTabId = tabId || resolveRuleTabId(source)
|
||||
if (resolvedTabId === 'riskRules' || isJsonRiskRuleSource(source)) {
|
||||
return resolveRiskRuleCategory(source)
|
||||
}
|
||||
if (resolvedTabId === 'financialRules') {
|
||||
return inferFinancialRuleCategory(source)
|
||||
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) {
|
||||
@@ -950,6 +1030,18 @@ export function buildListItem(asset) {
|
||||
const businessStage = usesJsonRiskRule
|
||||
? resolveRiskRuleBusinessStage(asset)
|
||||
: { value: '', label: '' }
|
||||
const ruleScenarioList = typeKey === 'rules' ? resolveRuleScenarioList(asset, tabId) : []
|
||||
const riskScoreLevel = usesJsonRiskRule
|
||||
? resolveRiskRuleScoreLevel(asset.config_json, asset.config_json)
|
||||
: ''
|
||||
const riskLevelValue = usesJsonRiskRule
|
||||
? riskScoreLevel || resolveRiskRuleSeverity(asset.config_json)
|
||||
: ''
|
||||
const riskLevelLabel = usesJsonRiskRule
|
||||
? riskScoreLevel
|
||||
? resolveRiskRuleScoreLabel(asset.config_json, asset.config_json) || resolveRiskRuleSeverityLabel(asset.config_json)
|
||||
: resolveRiskRuleSeverityLabel(asset.config_json)
|
||||
: ''
|
||||
|
||||
return {
|
||||
id: asset.id,
|
||||
@@ -966,12 +1058,16 @@ export function buildListItem(asset) {
|
||||
summary: listSubtitle,
|
||||
listSubtitle,
|
||||
category: resolveDomainLabel(asset.domain),
|
||||
owner: isRiskRule ? reviewer : asset.owner,
|
||||
owner: isRiskRule ? creator : asset.owner,
|
||||
reviewer,
|
||||
scope: typeKey === 'rules' ? ruleScenarioCategory || '通用' : formatScenarioList(asset.scenario_json),
|
||||
riskCategory: ruleScenarioCategory,
|
||||
scenarioList: ruleScenarioList,
|
||||
businessStageValue: businessStage.value,
|
||||
businessStageLabel: businessStage.label,
|
||||
riskLevelValue,
|
||||
riskLevelLabel,
|
||||
riskLevelTone: riskLevelValue,
|
||||
model: buildRowRuntime(asset, typeKey),
|
||||
version: workingVersion,
|
||||
versionDisplay: typeKey === 'rules' ? `${changeCount} 次` : workingVersion,
|
||||
@@ -1304,6 +1400,7 @@ export function buildDetailViewModel(detail, runs) {
|
||||
const ruleTemplateLabel = normalizeText(configJson.rule_template_label) || resolveRuleTemplateLabel(ruleTemplateKey)
|
||||
const runtimeKind = normalizeText(configJson.runtime_kind || previewRuntimeRule.kind) || 'policy_rule_draft'
|
||||
const ruleScenarioCategory = typeKey === 'rules' ? resolveRuleScenarioCategory(detail, tabId) : ''
|
||||
const ruleScenarioList = typeKey === 'rules' ? resolveRuleScenarioList(detail, tabId) : []
|
||||
const isEnabledValue = usesJsonRiskRule ? resolveRiskRuleEnabled(detail) : true
|
||||
const generationStatus = normalizeText(configJson.generation_status || detail.status)
|
||||
const riskRuleGenerationFailed = usesJsonRiskRule && (detail.status === 'failed' || generationStatus === 'failed')
|
||||
@@ -1404,8 +1501,8 @@ export function buildDetailViewModel(detail, runs) {
|
||||
latestTestSummary: detail.latest_test_summary || detail.latestTestSummary || null,
|
||||
riskCategory: typeKey === 'rules' ? ruleScenarioCategory : '',
|
||||
ruleDocument,
|
||||
scenarioList: typeKey === 'rules' && ruleScenarioCategory
|
||||
? [ruleScenarioCategory]
|
||||
scenarioList: typeKey === 'rules' && ruleScenarioList.length
|
||||
? ruleScenarioList
|
||||
: Array.isArray(detail.scenario_json)
|
||||
? [...detail.scenario_json]
|
||||
: [],
|
||||
|
||||
Reference in New Issue
Block a user