feat: 扩展风险规则体系、审批动态路由与预算中心列表化改造

- 新增 25+ 条风险规则(预算/报销/申请/通用类),完善风险规则模拟与反馈发布机制
- 引入费用审批动态路由、平台风险分级、预审与风险阶段管理
- 预算中心列表化改造,优化票据夹仪表盘与数字员工工作看板
- 新增 Hermes 风险线索收集器、Agent 链路追踪中心
- 扩展数字员工能力库(18 个领域 Skill)与交通费用自动预估
- 完善报销申请快速预览、权限控制与前端测试覆盖
This commit is contained in:
caoxiaozhu
2026-06-01 17:07:14 +08:00
parent 7989f3a159
commit 92444e7eae
285 changed files with 25075 additions and 2986 deletions

View File

@@ -65,6 +65,12 @@ const APPLICATION_PROGRESS_LABELS = [
'审批完成'
]
const APPLICATION_PROGRESS_LABELS_WITHOUT_BUDGET = [
'创建申请',
'直属领导审批',
'审批完成'
]
function parseNumber(value) {
const nextValue = Number(value)
return Number.isFinite(nextValue) ? nextValue : 0
@@ -471,11 +477,13 @@ function resolveApplicationApproverName(claim) {
function resolveApplicationBudgetApproverName(claim) {
const routeEvent = findApprovalEventForStep(claim, '直属领导审批')
return resolveDisplayName(
claim?.budget_approver_name,
claim?.budgetApproverName,
routeEvent?.next_approver_name,
routeEvent?.nextApproverName,
routeEvent?.budget_approver_name,
routeEvent?.budgetApproverName
) || 'P8预算监控者'
) || '预算管理者'
}
function resolveProgressDisplayLabel(label, documentTypeCode, claim, approvalMeta) {
@@ -592,19 +600,36 @@ 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 : {}
const reviewValues = flag?.review_form_values || flag?.reviewFormValues || {}
const sceneSelection = flag?.expense_scene_selection || flag?.expenseSceneSelection || {}
return [sceneSelection, reviewValues, detail]
.filter((item) => item && typeof item === 'object')
.reduce((acc, item) => ({ ...acc, ...item }), {})
}
function resolveApplicationField(flag = {}, detail = {}, snakeKey, camelKey = '') {
return normalizeText(
flag?.[snakeKey]
|| (camelKey ? flag?.[camelKey] : '')
|| detail?.[snakeKey]
|| (camelKey ? detail?.[camelKey] : '')
)
}
function resolveRelatedApplicationClaimNo(flag = {}) {
const detail = normalizeApplicationHandoffDetail(flag)
return resolveApplicationField(flag, detail, 'application_claim_no', 'applicationClaimNo')
}
function findRelatedApplicationEvent(claim) {
const events = getRiskFlags(claim).filter((flag) => (
flag
&& typeof flag === 'object'
&& resolveRelatedApplicationClaimNo(flag)
))
return getLatestEvent(events) || events[events.length - 1] || null
}
function resolveRelatedApplicationAmountLabel(flag, detail, claim) {
@@ -631,53 +656,57 @@ function resolveRelatedApplicationAmountLabel(flag, detail, claim) {
}
function resolveRelatedApplicationInfo(claim, typeLabel = '') {
const handoff = findApplicationHandoffEvent(claim)
if (!handoff) {
const relatedEvent = findRelatedApplicationEvent(claim)
if (!relatedEvent) {
return null
}
const detail = normalizeApplicationHandoffDetail(handoff)
const claimNo = normalizeText(handoff.application_claim_no || handoff.applicationClaimNo)
const detail = normalizeApplicationHandoffDetail(relatedEvent)
const claimNo = resolveRelatedApplicationClaimNo(relatedEvent)
const applicationType = normalizeText(
detail.application_type
|| detail.applicationType
|| handoff.application_type
|| handoff.applicationType
|| relatedEvent.application_type
|| relatedEvent.applicationType
|| typeLabel
)
const location = normalizeText(
detail.application_location
|| detail.applicationLocation
|| detail.location
|| handoff.application_location
|| handoff.applicationLocation
|| relatedEvent.application_location
|| relatedEvent.applicationLocation
|| claim?.location
)
const reason = normalizeText(
detail.application_reason
|| detail.applicationReason
|| detail.reason
|| handoff.application_reason
|| handoff.applicationReason
|| relatedEvent.application_reason
|| relatedEvent.applicationReason
|| claim?.reason
)
const content = normalizeText(
detail.application_content
|| detail.applicationContent
|| handoff.application_content
|| handoff.applicationContent
|| relatedEvent.application_content
|| relatedEvent.applicationContent
) || [applicationType, location].filter(Boolean).join(' / ')
const rawTime = normalizeText(
detail.application_time
|| detail.applicationTime
|| detail.time
|| handoff.application_time
|| handoff.applicationTime
|| detail.application_date
|| detail.applicationDate
|| relatedEvent.application_time
|| relatedEvent.applicationTime
|| relatedEvent.application_date
|| relatedEvent.applicationDate
|| claim?.occurred_at
)
return {
id: normalizeText(handoff.application_claim_id || handoff.applicationClaimId),
id: resolveApplicationField(relatedEvent, detail, 'application_claim_id', 'applicationClaimId'),
claimNo,
content,
reason,
@@ -685,19 +714,19 @@ function resolveRelatedApplicationInfo(claim, typeLabel = '') {
detail.application_days
|| detail.applicationDays
|| detail.days
|| handoff.application_days
|| handoff.applicationDays
|| relatedEvent.application_days
|| relatedEvent.applicationDays
),
location,
time: formatDate(rawTime) || rawTime,
amountLabel: resolveRelatedApplicationAmountLabel(handoff, detail, claim),
statusLabel: normalizeText(handoff.application_status_label || handoff.applicationStatusLabel),
amountLabel: resolveRelatedApplicationAmountLabel(relatedEvent, detail, claim),
statusLabel: resolveApplicationField(relatedEvent, detail, 'application_status_label', 'applicationStatusLabel'),
transportMode: normalizeText(
detail.application_transport_mode
|| detail.applicationTransportMode
|| detail.transport_mode
|| handoff.application_transport_mode
|| handoff.applicationTransportMode
|| relatedEvent.application_transport_mode
|| relatedEvent.applicationTransportMode
)
}
}
@@ -743,6 +772,42 @@ function findMergedApplicationBudgetApprovalEvent(claim) {
)
}
function applicationRequiresBudgetReviewStep(claim, workflowNode) {
const node = normalizeText(workflowNode || claim?.approval_stage || claim?.workflowNode)
if (node.includes('预算')) {
return true
}
return getRiskFlags(claim).some((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 routeDecision = flag.route_decision || flag.routeDecision || {}
if (source === 'approval_routing' && flag.requires_budget_review === true) {
return true
}
if (
routeDecision
&& typeof routeDecision === 'object'
&& routeDecision.requires_budget_review === true
) {
return true
}
return (
source === 'budget_approval'
|| eventType === 'expense_application_budget_approval'
|| previousStage.includes('预算')
|| nextStage.includes('预算')
)
})
}
function buildProgressStepMeta(time, detail = '', title = '') {
return {
time,
@@ -788,7 +853,7 @@ function buildCompletedStepMeta(claim, label) {
approvalEvent.operatorName,
stepLabel === '直属领导审批' ? claim?.manager_name : '',
stepLabel === '预算管理者审批' ? approvalEvent.next_approver_name : ''
) || (stepLabel === '财务审批' ? '财务' : stepLabel === '预算管理者审批' ? '预算监控者' : '直属领导')
) || (stepLabel === '财务审批' ? '财务' : stepLabel === '预算管理者审批' ? '预算管理者' : '直属领导')
const approvedAt = formatDateTime(approvalEvent.created_at || approvalEvent.createdAt)
return buildProgressStepMeta(`${operator}通过`, approvedAt, `${operator}审批通过 ${approvedAt}`.trim())
}
@@ -899,13 +964,20 @@ function buildProgressSteps(approvalMeta, workflowNode, claim = {}, options = {}
documentTypeCode === DOCUMENT_TYPE_APPLICATION
&& Boolean(findMergedApplicationBudgetApprovalEvent(claim))
)
const shouldShowApplicationBudgetStep = (
documentTypeCode === DOCUMENT_TYPE_APPLICATION
&& !hasMergedApplicationBudgetApproval
&& applicationRequiresBudgetReviewStep(claim, workflowNode)
)
const progressLabels =
documentTypeCode === DOCUMENT_TYPE_APPLICATION
? hasApplicationReturnStep
? ['创建申请', '直属领导审批', '退回', '待提交']
: hasMergedApplicationBudgetApproval
? ['创建申请', '直属领导审批', '审批完成']
: APPLICATION_PROGRESS_LABELS
: shouldShowApplicationBudgetStep
? APPLICATION_PROGRESS_LABELS
: APPLICATION_PROGRESS_LABELS_WITHOUT_BUDGET
: REIMBURSEMENT_PROGRESS_LABELS
const currentIndex =
documentTypeCode === DOCUMENT_TYPE_APPLICATION
@@ -1061,12 +1133,17 @@ export function mapExpenseClaimToRequest(claim) {
const expenseItems = buildExpenseItems(claim, riskSummary)
const applyDateTime = claim?.submitted_at || claim?.created_at
const relatedApplication = isApplicationDocument ? null : resolveRelatedApplicationInfo(claim, typeLabel)
const employeeId = String(claim?.employee_id || claim?.employeeId || '').trim()
const employeeName = String(claim?.employee_name || claim?.employeeName || '').trim()
return {
id: String(claim?.claim_no || claim?.id || '').trim(),
claimNo: String(claim?.claim_no || claim?.id || '').trim(),
claimId: String(claim?.id || '').trim(),
status: String(claim?.status || '').trim(),
employeeId,
employee_id: employeeId,
profileEmployeeId: employeeId || employeeName,
person: String(claim?.employee_name || '').trim() || '待补充',
dept: String(claim?.department_name || '').trim() || '待补充',
departmentName: String(claim?.department_name || '').trim() || '待补充',
@@ -1074,6 +1151,9 @@ export function mapExpenseClaimToRequest(claim) {
employeePosition: String(claim?.employee_position || '').trim(),
employeeGrade: String(claim?.employee_grade || '').trim(),
managerName: resolveDisplayName(claim?.manager_name),
budgetApproverName: resolveDisplayName(claim?.budget_approver_name, claim?.budgetApproverName),
budgetApproverGrade: String(claim?.budget_approver_grade || claim?.budgetApproverGrade || '').trim(),
budgetApproverRoleCode: String(claim?.budget_approver_role_code || claim?.budgetApproverRoleCode || '').trim(),
roleLabels: Array.isArray(claim?.role_labels) ? claim.role_labels.filter(Boolean) : [],
entity: '',
typeCode,