feat: 新增风险图谱算法与系统仪表盘及操作反馈体系

后端新增风险图谱算法模块、风险观察与反馈服务、规则 DSL
校验器和可解释性引擎,完善系统仪表盘和财务仪表盘统计,
优化 agent 运行和编排执行链路,清理旧开发文档,前端新增
系统趋势、负载热力图等多种仪表盘图表组件,完善操作反馈
对话框和工作台日期选择器,优化报销创建和审批详情交互,
补充单元测试覆盖。
This commit is contained in:
caoxiaozhu
2026-05-30 15:46:51 +08:00
parent 4c59941ec6
commit 7989f3a159
314 changed files with 30073 additions and 20626 deletions

View File

@@ -44,15 +44,18 @@ const ROUTE_DESCRIPTION_EXPENSE_TYPES = new Set(['train_ticket', 'flight_ticket'
const HOTEL_DESCRIPTION_EXPENSE_TYPES = new Set(['hotel_ticket'])
const DOCUMENT_TYPE_APPLICATION = 'application'
const DOCUMENT_TYPE_REIMBURSEMENT = 'reimbursement'
const RELATED_APPLICATION_STEP_LABEL = '关联单据'
const ARCHIVED_STEP_LABEL = '已归档'
const REIMBURSEMENT_PROGRESS_LABELS = [
'创建单据',
RELATED_APPLICATION_STEP_LABEL,
'待提交',
'AI预审',
'直属领导审批',
'财务审批',
'待付款',
'已付款'
'已付款',
ARCHIVED_STEP_LABEL
]
const APPLICATION_PROGRESS_LABELS = [
@@ -366,7 +369,7 @@ function resolveProgressCurrentIndex(approvalMeta, workflowNode) {
const normalizedNode = String(workflowNode || '').trim()
if (approvalMeta.key === 'completed') {
return 6
return 7
}
if (approvalMeta.key === 'pending_payment') {
@@ -380,7 +383,7 @@ function resolveProgressCurrentIndex(approvalMeta, workflowNode) {
return 5
}
if (normalizedNode.includes('归档') || normalizedNode.includes('入账')) {
return 6
return 7
}
if (normalizedNode.includes('财务')) {
return 4
@@ -589,6 +592,116 @@ function findLatestPaymentEvent(claim) {
)
}
function findApplicationHandoffEvent(claim) {
const handoffEvents = getRiskFlags(claim).filter((flag) => (
flag
&& typeof flag === 'object'
&& normalizeText(flag.source) === 'application_handoff'
&& normalizeText(flag.application_claim_no || flag.applicationClaimNo)
))
return getLatestEvent(handoffEvents) || handoffEvents[handoffEvents.length - 1] || null
}
function normalizeApplicationHandoffDetail(flag = {}) {
const detail = flag?.application_detail || flag?.applicationDetail || {}
return detail && typeof detail === 'object' ? detail : {}
}
function resolveRelatedApplicationAmountLabel(flag, detail, claim) {
const explicitLabel = normalizeText(
flag?.application_amount_label
|| flag?.applicationAmountLabel
|| detail?.application_amount_label
|| detail?.applicationAmountLabel
)
if (explicitLabel) return explicitLabel
const rawAmount = normalizeText(
flag?.application_amount
|| flag?.applicationAmount
|| flag?.application_budget_amount
|| flag?.applicationBudgetAmount
|| detail?.application_amount
|| detail?.applicationAmount
|| detail?.amount
|| claim?.amount
)
const amountValue = parseNumber(rawAmount)
return amountValue > 0 ? formatAmount(amountValue) : rawAmount
}
function resolveRelatedApplicationInfo(claim, typeLabel = '') {
const handoff = findApplicationHandoffEvent(claim)
if (!handoff) {
return null
}
const detail = normalizeApplicationHandoffDetail(handoff)
const claimNo = normalizeText(handoff.application_claim_no || handoff.applicationClaimNo)
const applicationType = normalizeText(
detail.application_type
|| detail.applicationType
|| handoff.application_type
|| handoff.applicationType
|| typeLabel
)
const location = normalizeText(
detail.application_location
|| detail.applicationLocation
|| detail.location
|| handoff.application_location
|| handoff.applicationLocation
|| claim?.location
)
const reason = normalizeText(
detail.application_reason
|| detail.applicationReason
|| detail.reason
|| handoff.application_reason
|| handoff.applicationReason
|| claim?.reason
)
const content = normalizeText(
detail.application_content
|| detail.applicationContent
|| handoff.application_content
|| handoff.applicationContent
) || [applicationType, location].filter(Boolean).join(' / ')
const rawTime = normalizeText(
detail.application_time
|| detail.applicationTime
|| detail.time
|| handoff.application_time
|| handoff.applicationTime
|| claim?.occurred_at
)
return {
id: normalizeText(handoff.application_claim_id || handoff.applicationClaimId),
claimNo,
content,
reason,
days: normalizeText(
detail.application_days
|| detail.applicationDays
|| detail.days
|| handoff.application_days
|| handoff.applicationDays
),
location,
time: formatDate(rawTime) || rawTime,
amountLabel: resolveRelatedApplicationAmountLabel(handoff, detail, claim),
statusLabel: normalizeText(handoff.application_status_label || handoff.applicationStatusLabel),
transportMode: normalizeText(
detail.application_transport_mode
|| detail.applicationTransportMode
|| detail.transport_mode
|| handoff.application_transport_mode
|| handoff.applicationTransportMode
)
}
}
function findLatestApplicationReturnEvent(claim) {
return getLatestEvent(
getRiskFlags(claim).filter((flag) => {
@@ -608,6 +721,28 @@ function findLatestApplicationReturnEvent(claim) {
)
}
function findMergedApplicationBudgetApprovalEvent(claim) {
return getLatestEvent(
getRiskFlags(claim).filter((flag) => {
if (!flag || typeof flag !== 'object') {
return false
}
const source = normalizeText(flag.source)
const eventType = normalizeText(flag.event_type || flag.eventType)
const previousStage = normalizeText(flag.previous_approval_stage || flag.previousApprovalStage)
const nextStage = normalizeText(flag.next_approval_stage || flag.nextApprovalStage)
const mergedFlag = Boolean(flag.budget_approval_merged || flag.budgetApprovalMerged)
return (
source === 'manual_approval'
&& eventType === 'expense_application_approval'
&& previousStage.includes('直属领导')
&& nextStage.includes('审批完成')
&& mergedFlag
)
})
)
}
function buildProgressStepMeta(time, detail = '', title = '') {
return {
time,
@@ -620,6 +755,15 @@ function buildCompletedStepMeta(claim, label) {
const stepLabel = normalizeText(label)
const employeeName = normalizeText(claim?.employee_name) || '申请人'
if (stepLabel === RELATED_APPLICATION_STEP_LABEL) {
const relatedApplication = resolveRelatedApplicationInfo(claim)
const createdAt = formatDateTime(claim?.created_at)
if (relatedApplication?.claimNo) {
return buildProgressStepMeta(`已关联 ${relatedApplication.claimNo}`, createdAt)
}
return buildProgressStepMeta('待核对关联单据', createdAt)
}
if (stepLabel === '创建单据' || stepLabel === '创建申请') {
const createdAt = formatDateTime(claim?.created_at)
return buildProgressStepMeta(stepLabel === '创建申请' ? `${employeeName}发起申请` : `${employeeName}创建`, createdAt)
@@ -694,6 +838,11 @@ function buildCompletedStepMeta(claim, label) {
return buildProgressStepMeta('归档入账', archivedAt)
}
if (stepLabel === ARCHIVED_STEP_LABEL) {
const archivedAt = formatDateTime(claim?.updated_at)
return buildProgressStepMeta(ARCHIVED_STEP_LABEL, archivedAt)
}
if (stepLabel === '审批完成') {
const completedAt = formatDateTime(claim?.updated_at)
return buildProgressStepMeta('审批完成', completedAt)
@@ -704,7 +853,7 @@ function buildCompletedStepMeta(claim, label) {
function resolveCurrentStepStartedAt(claim, label) {
const stepLabel = normalizeText(label)
if (stepLabel === '创建单据' || stepLabel === '创建申请') {
if (stepLabel === RELATED_APPLICATION_STEP_LABEL || stepLabel === '创建单据' || stepLabel === '创建申请') {
return claim?.created_at
}
if (stepLabel === '待提交') {
@@ -733,7 +882,7 @@ function resolveCurrentStepStartedAt(claim, label) {
const paymentEvent = findLatestPaymentEvent(claim)
return paymentEvent?.created_at || paymentEvent?.createdAt || claim?.updated_at || claim?.submitted_at
}
if (stepLabel === '归档入账' || stepLabel === '审批完成') {
if (stepLabel === '归档入账' || stepLabel === ARCHIVED_STEP_LABEL || stepLabel === '审批完成') {
return claim?.updated_at || claim?.submitted_at
}
return ''
@@ -746,17 +895,26 @@ function buildProgressSteps(approvalMeta, workflowNode, claim = {}, options = {}
&& Boolean(findLatestApplicationReturnEvent(claim))
&& approvalMeta.key === 'supplement'
)
const hasMergedApplicationBudgetApproval = (
documentTypeCode === DOCUMENT_TYPE_APPLICATION
&& Boolean(findMergedApplicationBudgetApprovalEvent(claim))
)
const progressLabels =
documentTypeCode === DOCUMENT_TYPE_APPLICATION
? hasApplicationReturnStep
? ['创建申请', '直属领导审批', '退回', '待提交']
: APPLICATION_PROGRESS_LABELS
: hasMergedApplicationBudgetApproval
? ['创建申请', '直属领导审批', '审批完成']
: APPLICATION_PROGRESS_LABELS
: REIMBURSEMENT_PROGRESS_LABELS
const currentIndex =
documentTypeCode === DOCUMENT_TYPE_APPLICATION
? hasApplicationReturnStep
? 3
: resolveApplicationProgressCurrentIndex(approvalMeta, workflowNode)
: Math.min(
resolveApplicationProgressCurrentIndex(approvalMeta, workflowNode),
Math.max(0, progressLabels.length - 1)
)
: resolveProgressCurrentIndex(approvalMeta, workflowNode)
const currentTime =
approvalMeta.key === 'completed'
@@ -902,6 +1060,7 @@ export function mapExpenseClaimToRequest(claim) {
const riskSummary = buildRiskSummary(claim?.risk_flags_json)
const expenseItems = buildExpenseItems(claim, riskSummary)
const applyDateTime = claim?.submitted_at || claim?.created_at
const relatedApplication = isApplicationDocument ? null : resolveRelatedApplicationInfo(claim, typeLabel)
return {
id: String(claim?.claim_no || claim?.id || '').trim(),
@@ -958,6 +1117,7 @@ export function mapExpenseClaimToRequest(claim) {
: `${expenseItems.length} 条费用明细,待补充票据`)
: '暂无费用明细',
note: String(claim?.reason || '').trim(),
relatedApplication,
progressSteps: buildProgressSteps(approvalMeta, workflowNode, claim, {
documentTypeCode: documentTypeMeta.documentTypeCode
}),