feat: 新增风险图谱算法与系统仪表盘及操作反馈体系
后端新增风险图谱算法模块、风险观察与反馈服务、规则 DSL 校验器和可解释性引擎,完善系统仪表盘和财务仪表盘统计, 优化 agent 运行和编排执行链路,清理旧开发文档,前端新增 系统趋势、负载热力图等多种仪表盘图表组件,完善操作反馈 对话框和工作台日期选择器,优化报销创建和审批详情交互, 补充单元测试覆盖。
This commit is contained in:
@@ -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
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user