feat: 增强风险规则生成引擎与预算中心页面
后端拆分风险规则生成为解释器、语义分析、本体对齐等子模块, 优化模板执行和流程图生成,完善员工种子数据和导入逻辑,增强 报销单权限策略和草稿持久化,前端新增预算中心视图和趋势图 组件,重构审计页面和风险规则测试对话框交互,完善文档中心 和报销创建页面细节,补充单元测试覆盖。
This commit is contained in:
@@ -69,9 +69,7 @@ import {
|
||||
} from './auditViewModel.js'
|
||||
import {
|
||||
createDefaultRiskRuleForm,
|
||||
RISK_RULE_CREATE_DOMAIN_OPTIONS,
|
||||
RISK_RULE_EXPENSE_CATEGORY_OPTIONS,
|
||||
RISK_RULE_LEVEL_OPTIONS
|
||||
RISK_RULE_EXPENSE_CATEGORY_OPTIONS
|
||||
} from './auditViewRiskRuleModel.js'
|
||||
|
||||
export default {
|
||||
@@ -141,6 +139,7 @@ export default {
|
||||
let spreadsheetOnlyOfficeHadLocalEdits = false
|
||||
let spreadsheetOnlyOfficeSyncSeq = 0
|
||||
let spreadsheetOnlyOfficeChangePollTimer = null
|
||||
const riskRuleGenerationPollTimers = new Map()
|
||||
const assetBuckets = ref({
|
||||
financialRules: [],
|
||||
riskRules: [],
|
||||
@@ -162,7 +161,7 @@ export default {
|
||||
const showMetricColumn = computed(() => activeMeta.value.showMetricColumn !== false)
|
||||
const showVersionColumn = computed(() => activeMeta.value.showVersionColumn !== false)
|
||||
const showStatusColumn = computed(() => activeMeta.value.showStatusColumn !== false)
|
||||
const showOnlineColumn = computed(() => activeType.value === 'riskRules')
|
||||
const showOnlineColumn = computed(() => false)
|
||||
const showEnabledColumn = computed(() => activeType.value === 'riskRules')
|
||||
const selectedSkillIsRule = computed(() => selectedSkill.value?.type === 'rules')
|
||||
const selectedSkillUsesSpreadsheet = computed(
|
||||
@@ -188,11 +187,19 @@ export default {
|
||||
const riskRuleInReview = computed(
|
||||
() => selectedSkillUsesJsonRisk.value && selectedSkill.value?.statusValue === 'review'
|
||||
)
|
||||
const riskRuleGenerationBusy = computed(
|
||||
() => selectedSkillUsesJsonRisk.value && selectedSkill.value?.statusValue === 'generating'
|
||||
)
|
||||
const riskRuleGenerationFailed = computed(
|
||||
() => selectedSkillUsesJsonRisk.value && selectedSkill.value?.statusValue === 'failed'
|
||||
)
|
||||
const canOpenRiskRuleTest = computed(
|
||||
() =>
|
||||
selectedSkillUsesJsonRisk.value &&
|
||||
canEditSelected.value &&
|
||||
Boolean(selectedSkill.value?.id) &&
|
||||
!riskRuleGenerationBusy.value &&
|
||||
!riskRuleGenerationFailed.value &&
|
||||
!detailBusy.value
|
||||
)
|
||||
const canDeleteRiskRule = computed(
|
||||
@@ -203,11 +210,17 @@ export default {
|
||||
!normalizeText(selectedSkill.value?.publishedVersion).replace('-', '') &&
|
||||
!detailBusy.value
|
||||
)
|
||||
const canSubmitRiskRuleReview = computed(
|
||||
const canOpenRiskRuleReviewSubmit = computed(
|
||||
() =>
|
||||
selectedSkillUsesJsonRisk.value &&
|
||||
canSubmitReview.value &&
|
||||
!riskRuleInReview.value &&
|
||||
!riskRuleGenerationBusy.value &&
|
||||
!riskRuleGenerationFailed.value
|
||||
)
|
||||
const canSubmitRiskRuleReview = computed(
|
||||
() =>
|
||||
canOpenRiskRuleReviewSubmit.value &&
|
||||
riskRuleTestPassed.value
|
||||
)
|
||||
const canReturnRiskRule = computed(
|
||||
@@ -355,8 +368,8 @@ export default {
|
||||
const showRiskScenarioFilter = computed(() =>
|
||||
['financialRules', 'riskRules'].includes(activeType.value)
|
||||
)
|
||||
const showStatusFilter = computed(() => activeType.value !== 'riskRules')
|
||||
const showOnlineFilter = computed(() => activeType.value === 'riskRules')
|
||||
const showStatusFilter = computed(() => true)
|
||||
const showOnlineFilter = computed(() => false)
|
||||
const showEnabledFilter = computed(() => activeType.value === 'riskRules')
|
||||
const selectedRiskScenarioLabel = computed(
|
||||
() =>
|
||||
@@ -618,6 +631,11 @@ export default {
|
||||
return
|
||||
}
|
||||
const naturalLanguage = String(riskRuleCreateForm.value.natural_language || '').trim()
|
||||
const ruleTitle = String(riskRuleCreateForm.value.rule_title || '').trim()
|
||||
if (ruleTitle.length < 2) {
|
||||
toast('请输入至少 2 个字的规则标题。')
|
||||
return
|
||||
}
|
||||
if (naturalLanguage.length < 8) {
|
||||
toast('请至少输入 8 个字的风险规则描述。')
|
||||
return
|
||||
@@ -627,11 +645,9 @@ export default {
|
||||
try {
|
||||
const detail = await generateRiskRuleAsset(
|
||||
{
|
||||
business_domain: riskRuleCreateForm.value.business_domain,
|
||||
expense_category: riskRuleCreateForm.value.business_domain === 'expense'
|
||||
? riskRuleCreateForm.value.expense_category
|
||||
: null,
|
||||
risk_level: riskRuleCreateForm.value.risk_level,
|
||||
business_domain: 'expense',
|
||||
expense_category: riskRuleCreateForm.value.expense_category,
|
||||
rule_title: ruleTitle,
|
||||
requires_attachment: Boolean(riskRuleCreateForm.value.requires_attachment),
|
||||
natural_language: naturalLanguage
|
||||
},
|
||||
@@ -639,9 +655,8 @@ export default {
|
||||
)
|
||||
riskRuleCreateOpen.value = false
|
||||
await refreshCurrentAssets()
|
||||
selectedSkill.value = buildDetailViewModel(detail, runs.value)
|
||||
await loadRiskRuleJson(detail.id)
|
||||
toast('风险规则草稿已生成,请在详情中核对业务说明和判断流程。')
|
||||
scheduleRiskRuleGenerationPoll(detail.id)
|
||||
toast('风险规则已进入后台生成,列表会先显示生成中。')
|
||||
} catch (error) {
|
||||
toast(error?.message || '风险规则生成失败,请稍后重试。')
|
||||
} finally {
|
||||
@@ -649,6 +664,40 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
function stopRiskRuleGenerationPoll(assetId) {
|
||||
const timer = riskRuleGenerationPollTimers.get(assetId)
|
||||
if (timer) {
|
||||
window.clearTimeout(timer)
|
||||
riskRuleGenerationPollTimers.delete(assetId)
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleRiskRuleGenerationPoll(assetId, attempt = 0) {
|
||||
const normalizedAssetId = normalizeText(assetId)
|
||||
if (!normalizedAssetId) {
|
||||
return
|
||||
}
|
||||
stopRiskRuleGenerationPoll(normalizedAssetId)
|
||||
const timer = window.setTimeout(async () => {
|
||||
try {
|
||||
await refreshCurrentAssets()
|
||||
const latest = (assetBuckets.value.riskRules || []).find((item) => item.id === normalizedAssetId)
|
||||
if (!latest || latest.statusValue !== 'generating' || attempt >= 59) {
|
||||
riskRuleGenerationPollTimers.delete(normalizedAssetId)
|
||||
return
|
||||
}
|
||||
scheduleRiskRuleGenerationPoll(normalizedAssetId, attempt + 1)
|
||||
} catch {
|
||||
if (attempt < 59) {
|
||||
scheduleRiskRuleGenerationPoll(normalizedAssetId, attempt + 1)
|
||||
} else {
|
||||
riskRuleGenerationPollTimers.delete(normalizedAssetId)
|
||||
}
|
||||
}
|
||||
}, attempt === 0 ? 1200 : 3000)
|
||||
riskRuleGenerationPollTimers.set(normalizedAssetId, timer)
|
||||
}
|
||||
|
||||
async function persistRuleRuntimeConfig(asset, runtimeRule) {
|
||||
await updateAgentAsset(
|
||||
asset.id,
|
||||
@@ -1033,6 +1082,9 @@ export default {
|
||||
loadSpreadsheetChangeRecords(assetId).catch(() => {})
|
||||
}
|
||||
if (selectedSkill.value.usesJsonRiskRule) {
|
||||
if (selectedSkill.value.riskRuleGenerationFailed || selectedSkill.value.riskRuleGenerationBusy) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await loadRiskRuleJson(assetId)
|
||||
} catch (jsonError) {
|
||||
@@ -1143,6 +1195,10 @@ export default {
|
||||
}
|
||||
|
||||
function openAssetDetail(asset) {
|
||||
if (asset?.usesJsonRiskRule && asset.statusValue === 'generating') {
|
||||
toast('规则仍在后台生成中,生成完成后才能进入详情。')
|
||||
return
|
||||
}
|
||||
destroySpreadsheetOnlyOfficeEditor()
|
||||
spreadsheetOnlyOfficeError.value = ''
|
||||
spreadsheetOnlyOfficeLoading.value = false
|
||||
@@ -1397,11 +1453,13 @@ export default {
|
||||
}
|
||||
|
||||
async function openSubmitReviewDialog() {
|
||||
if (!selectedSkill.value || !canSubmitReview.value || detailBusy.value) {
|
||||
if (
|
||||
selectedSkillUsesJsonRisk.value &&
|
||||
!canOpenRiskRuleReviewSubmit.value
|
||||
) {
|
||||
return
|
||||
}
|
||||
if (selectedSkillUsesJsonRisk.value && !riskRuleTestPassed.value) {
|
||||
toast('请先在“测试规则”中保存测试通过报告,再提交审核。')
|
||||
if (!selectedSkill.value || !canSubmitReview.value || detailBusy.value) {
|
||||
return
|
||||
}
|
||||
reviewSubmitVersion.value = selectedSkill.value.workingVersion || selectedSkill.value.displayVersion || ''
|
||||
@@ -1698,6 +1756,8 @@ export default {
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
destroySpreadsheetOnlyOfficeEditor()
|
||||
riskRuleGenerationPollTimers.forEach((timer) => window.clearTimeout(timer))
|
||||
riskRuleGenerationPollTimers.clear()
|
||||
document.removeEventListener('click', handleDocumentClick)
|
||||
})
|
||||
|
||||
@@ -1753,6 +1813,7 @@ export default {
|
||||
canCreateRiskRule,
|
||||
canOpenRiskRuleTest,
|
||||
canDeleteRiskRule,
|
||||
canOpenRiskRuleReviewSubmit,
|
||||
canSubmitRiskRuleReview,
|
||||
canReturnRiskRule,
|
||||
canPublishRiskRule,
|
||||
@@ -1790,9 +1851,7 @@ export default {
|
||||
riskRuleReturnOpen,
|
||||
riskRulePublishOpen,
|
||||
riskRuleReturnNote,
|
||||
riskRuleCreateDomainOptions: RISK_RULE_CREATE_DOMAIN_OPTIONS,
|
||||
riskRuleExpenseCategoryOptions: RISK_RULE_EXPENSE_CATEGORY_OPTIONS,
|
||||
riskRuleLevelOptions: RISK_RULE_LEVEL_OPTIONS,
|
||||
showReviewNote,
|
||||
spreadsheetUploadInput,
|
||||
spreadsheetOnlyOfficeLoading,
|
||||
|
||||
Reference in New Issue
Block a user