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

@@ -2,6 +2,7 @@ import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import test from 'node:test'
import { fileURLToPath } from 'node:url'
import { ref } from 'vue'
import {
buildLocallySyncedReviewPayload,
@@ -10,6 +11,7 @@ import {
isTravelReviewPayload,
resolveReviewFooterActions
} from '../src/views/scripts/travelReimbursementReviewModel.js'
import { useTravelReimbursementAttachments } from '../src/views/scripts/useTravelReimbursementAttachments.js'
import { renderMarkdown } from '../src/utils/markdown.js'
const createViewTemplate = readFileSync(
@@ -407,7 +409,7 @@ test('review drawer save action is disabled while receipt recognition is submitt
test('draft creation starts composer attachment persistence after response rendering', () => {
assert.match(
submitComposerScript,
/void syncComposerFilesToDraft\(resolvedDraftClaimId, files\)\s*\.then\(\(\) => \{\s*persistSessionState\(\)\s*\}\)\s*\.catch\(\(error\) => \{/s
/void syncComposerFilesToDraft\(resolvedDraftClaimId, files\)\s*\.then\(\(syncResult\) => \{[\s\S]*persistSessionState\(\)[\s\S]*emitRequestUpdated\?\.\(\{/s
)
assert.doesNotMatch(
submitComposerScript,
@@ -422,10 +424,84 @@ test('draft creation starts composer attachment persistence after response rende
assert.match(attachmentsScript, /const normalizedMatchBuckets = new Map\(\)/)
assert.match(
attachmentsScript,
/const targetItem = nextExactMatch \|\| nextNormalizedMatch \|\| fallbackMatch/
/nextExactMatch \|\| nextNormalizedMatch \|\| fallbackMatch \|\| emptyFallbackMatch/
)
})
test('detail smart-entry receipt sync uploads files to existing empty items and creates a row when needed', async () => {
const uploadCalls = []
let createCount = 0
const claimSnapshots = [
{
items: [
{ id: 'item-empty-1', item_type: 'hotel_ticket', invoice_id: '' },
{ id: 'item-persisted', item_type: 'train_ticket', invoice_id: 'claim-1/item-persisted/old.pdf' }
]
},
{
items: [
{ id: 'item-empty-1', item_type: 'hotel_ticket', invoice_id: '' },
{ id: 'item-persisted', item_type: 'train_ticket', invoice_id: 'claim-1/item-persisted/old.pdf' }
]
}
]
const attachments = useTravelReimbursementAttachments({
isKnowledgeSession: ref(false),
reviewFilePreviews: ref([]),
linkedRequest: ref({}),
draftClaimId: ref('claim-1'),
activeReviewPayload: ref(null),
reviewInlinePendingFiles: ref([]),
reviewInlineForm: ref({}),
reviewInlineEditorKey: ref(''),
composerUploadIntent: ref(''),
submitting: ref(false),
reviewActionBusy: ref(false),
toast: () => {},
fileInputRef: ref(null),
createExpenseClaimItem: async () => {
createCount += 1
return {
items: [
{ id: 'item-created-1', item_type: 'taxi_receipt', invoice_id: '' }
]
}
},
fetchExpenseClaimDetail: async () => claimSnapshots.shift() || { items: [] },
fetchExpenseClaimItemAttachmentMeta: async () => null,
fetchExpenseClaimAttachmentAsset: async () => new Blob(['preview']),
uploadExpenseClaimItemAttachment: async (claimId, itemId, file) => {
uploadCalls.push({ claimId, itemId, fileName: file.name })
return {}
},
extractReviewAttachmentNames: () => [],
mergeFilesWithLimit: (existing, incoming) => ({ files: [...existing, ...incoming], overflowCount: 0 }),
mergeFilePreviews: (existing, incoming) => [...existing, ...incoming],
isTemporaryPreviewUrl: () => false,
resolveAttachmentPreviewKind: () => '',
resolveDocumentPreview: () => null,
buildFilePreviews: () => [],
buildFileIdentity: (file) => file.name,
MAX_ATTACHMENTS: 5,
VISIBLE_ATTACHMENT_CHIPS: 3,
clearInlineReviewFieldError: () => {}
})
const result = await attachments.syncComposerFilesToDraft('claim-1', [
{ name: 'hotel.pdf' },
{ name: 'taxi.pdf' }
])
assert.deepEqual(uploadCalls, [
{ claimId: 'claim-1', itemId: 'item-empty-1', fileName: 'hotel.pdf' },
{ claimId: 'claim-1', itemId: 'item-created-1', fileName: 'taxi.pdf' }
])
assert.equal(createCount, 1)
assert.equal(result.uploadedCount, 2)
assert.equal(result.skippedCount, 0)
})
test('review summary renders markdown and save draft relies on backend response only', () => {
assert.match(
createViewTemplate,