feat: 新增风险图谱算法与系统仪表盘及操作反馈体系
后端新增风险图谱算法模块、风险观察与反馈服务、规则 DSL 校验器和可解释性引擎,完善系统仪表盘和财务仪表盘统计, 优化 agent 运行和编排执行链路,清理旧开发文档,前端新增 系统趋势、负载热力图等多种仪表盘图表组件,完善操作反馈 对话框和工作台日期选择器,优化报销创建和审批详情交互, 补充单元测试覆盖。
This commit is contained in:
143
web/src/services/riskObservations.js
Normal file
143
web/src/services/riskObservations.js
Normal file
@@ -0,0 +1,143 @@
|
||||
import { apiRequest } from './api.js'
|
||||
|
||||
function toNumber(value, fallback = 0) {
|
||||
const number = Number(value)
|
||||
return Number.isFinite(number) ? number : fallback
|
||||
}
|
||||
|
||||
function toArray(value) {
|
||||
return Array.isArray(value) ? value : []
|
||||
}
|
||||
|
||||
function toObject(value) {
|
||||
return value && typeof value === 'object' && !Array.isArray(value) ? value : {}
|
||||
}
|
||||
|
||||
export function normalizeRiskObservation(item = {}) {
|
||||
return {
|
||||
id: String(item.id || '').trim(),
|
||||
observationKey: String(item.observation_key || item.observationKey || '').trim(),
|
||||
claimId: String(item.claim_id || item.claimId || '').trim(),
|
||||
claimNo: String(item.claim_no || item.claimNo || '').trim(),
|
||||
riskType: String(item.risk_type || item.riskType || '').trim(),
|
||||
riskSignal: String(item.risk_signal || item.riskSignal || '').trim(),
|
||||
title: String(item.title || '').trim(),
|
||||
description: String(item.description || '').trim(),
|
||||
riskScore: toNumber(item.risk_score ?? item.riskScore),
|
||||
riskLevel: String(item.risk_level || item.riskLevel || '').trim(),
|
||||
confidenceScore: toNumber(item.confidence_score ?? item.confidenceScore),
|
||||
controlStage: String(item.control_stage || item.controlStage || '').trim(),
|
||||
controlMode: String(item.control_mode || item.controlMode || '').trim(),
|
||||
automationMode: String(item.automation_mode || item.automationMode || '').trim(),
|
||||
source: String(item.source || '').trim(),
|
||||
algorithmVersion: String(item.algorithm_version || item.algorithmVersion || '').trim(),
|
||||
status: String(item.status || '').trim(),
|
||||
feedbackStatus: String(item.feedback_status || item.feedbackStatus || '').trim(),
|
||||
contributionScores: toObject(item.contribution_scores || item.contributionScores),
|
||||
baseline: toObject(item.baseline),
|
||||
evidence: toArray(item.evidence),
|
||||
graphNodeKeys: toArray(item.graph_node_keys || item.graphNodeKeys),
|
||||
graphEdgeKeys: toArray(item.graph_edge_keys || item.graphEdgeKeys),
|
||||
policyRefs: toArray(item.policy_refs || item.policyRefs),
|
||||
similarCaseClaimIds: toArray(item.similar_case_claim_ids || item.similarCaseClaimIds),
|
||||
ontology: toObject(item.ontology_json || item.ontologyJson),
|
||||
decisionTrace: toObject(item.decision_trace || item.decisionTrace),
|
||||
feedbackItems: toArray(item.feedback_items || item.feedbackItems),
|
||||
createdAt: String(item.created_at || item.createdAt || '').trim(),
|
||||
updatedAt: String(item.updated_at || item.updatedAt || '').trim()
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeRiskObservationDashboard(payload = {}) {
|
||||
return {
|
||||
windowDays: toNumber(payload.window_days ?? payload.windowDays, 30),
|
||||
totalObservations: toNumber(payload.total_observations ?? payload.totalObservations),
|
||||
pendingCount: toNumber(payload.pending_count ?? payload.pendingCount),
|
||||
highOrAboveCount: toNumber(payload.high_or_above_count ?? payload.highOrAboveCount),
|
||||
confirmedCount: toNumber(payload.confirmed_count ?? payload.confirmedCount),
|
||||
falsePositiveCount: toNumber(payload.false_positive_count ?? payload.falsePositiveCount),
|
||||
totalAmount: toNumber(payload.total_amount ?? payload.totalAmount),
|
||||
averageScore: toNumber(payload.average_score ?? payload.averageScore),
|
||||
confirmationRate: toNumber(payload.confirmation_rate ?? payload.confirmationRate),
|
||||
falsePositiveRate: toNumber(payload.false_positive_rate ?? payload.falsePositiveRate),
|
||||
candidateRuleCount: toNumber(payload.candidate_rule_count ?? payload.candidateRuleCount),
|
||||
levelDistribution: toObject(payload.level_distribution || payload.levelDistribution),
|
||||
statusDistribution: toObject(payload.status_distribution || payload.statusDistribution),
|
||||
signalDistribution: toObject(payload.signal_distribution || payload.signalDistribution),
|
||||
sourceDistribution: toObject(payload.source_distribution || payload.sourceDistribution),
|
||||
automationDistribution: toObject(
|
||||
payload.automation_distribution || payload.automationDistribution
|
||||
),
|
||||
departmentDistribution: toObject(
|
||||
payload.department_distribution || payload.departmentDistribution
|
||||
),
|
||||
expenseTypeDistribution: toObject(
|
||||
payload.expense_type_distribution || payload.expenseTypeDistribution
|
||||
),
|
||||
riskTypeDistribution: toObject(payload.risk_type_distribution || payload.riskTypeDistribution),
|
||||
supplierDistribution: toObject(payload.supplier_distribution || payload.supplierDistribution),
|
||||
employeeGradeDistribution: toObject(
|
||||
payload.employee_grade_distribution || payload.employeeGradeDistribution
|
||||
),
|
||||
dailyTrend: toArray(payload.daily_trend || payload.dailyTrend),
|
||||
topRiskSignals: toArray(payload.top_risk_signals || payload.topRiskSignals),
|
||||
topDepartments: toArray(payload.top_departments || payload.topDepartments),
|
||||
topEmployees: toArray(payload.top_employees || payload.topEmployees),
|
||||
topSuppliers: toArray(payload.top_suppliers || payload.topSuppliers),
|
||||
topExpenseTypes: toArray(payload.top_expense_types || payload.topExpenseTypes),
|
||||
topRules: toArray(payload.top_rules || payload.topRules),
|
||||
recentHighObservations: toArray(
|
||||
payload.recent_high_observations || payload.recentHighObservations
|
||||
).map(normalizeRiskObservation)
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchRiskObservationDashboard(options = {}) {
|
||||
const windowDays = Math.max(1, Math.min(toNumber(options.windowDays || 30), 365))
|
||||
const limit = Math.max(1, Math.min(toNumber(options.limit || 500), 2000))
|
||||
const search = new URLSearchParams()
|
||||
search.set('window_days', String(windowDays))
|
||||
search.set('limit', String(limit))
|
||||
|
||||
const payload = await apiRequest(`/risk-observations/dashboard?${search.toString()}`, {
|
||||
timeoutMs: toNumber(options.timeoutMs || 3500),
|
||||
timeoutMessage: '风险看板数据加载超时,请稍后重试。'
|
||||
})
|
||||
|
||||
return normalizeRiskObservationDashboard(payload)
|
||||
}
|
||||
|
||||
export async function fetchClaimRiskObservations(claimId, options = {}) {
|
||||
const normalizedClaimId = String(claimId || '').trim()
|
||||
if (!normalizedClaimId) {
|
||||
return []
|
||||
}
|
||||
|
||||
const payload = await apiRequest(
|
||||
`/risk-observations/claim/${encodeURIComponent(normalizedClaimId)}`,
|
||||
{
|
||||
timeoutMs: toNumber(options.timeoutMs || 3500),
|
||||
timeoutMessage: '风险证据链加载超时,请稍后重试。'
|
||||
}
|
||||
)
|
||||
|
||||
return toArray(payload).map(normalizeRiskObservation)
|
||||
}
|
||||
|
||||
export async function fetchRunRiskObservations(runId, options = {}) {
|
||||
const normalizedRunId = String(runId || '').trim()
|
||||
if (!normalizedRunId) {
|
||||
return []
|
||||
}
|
||||
|
||||
const search = new URLSearchParams()
|
||||
search.set('run_id', normalizedRunId)
|
||||
search.set('limit', String(Math.max(1, Math.min(toNumber(options.limit || 100), 200))))
|
||||
|
||||
const payload = await apiRequest(`/risk-observations?${search.toString()}`, {
|
||||
timeoutMs: toNumber(options.timeoutMs || 3500),
|
||||
timeoutMessage: '本次运行风险观察加载超时,请稍后重试。'
|
||||
})
|
||||
|
||||
return toArray(payload.items || payload).map(normalizeRiskObservation)
|
||||
}
|
||||
Reference in New Issue
Block a user