feat: 新增员工行为画像算法与费用风险标签体系

后端新增员工行为画像算法模块,支持标签规则引擎和评分计算,
完善员工模型、银行信息、序列化和导入逻辑,优化报销审批流
和工作流常量,增强 Hermes 同步和知识同步能力,前端新增费
用画像详情弹窗、雷达图和风险卡片组件,完善登录页和工作台
样式,优化文档中心和归档中心交互,补充单元测试。
This commit is contained in:
caoxiaozhu
2026-05-28 12:09:49 +08:00
parent 04cd6d0f81
commit 8a4a777be7
96 changed files with 9835 additions and 704 deletions

View File

@@ -33,6 +33,18 @@ test('document center archived rows are detected from archive flag or request st
}),
false
)
assert.equal(
isArchivedDocumentRow({
rawRequest: { status: 'pending_payment', approval_stage: '待付款' }
}),
false
)
assert.equal(
isArchivedDocumentRow({
rawRequest: { status: 'paid', approval_stage: '已付款' }
}),
true
)
assert.equal(
isArchivedDocumentRow({
rawRequest: { status: 'approved', approval_stage: '部门审批', approvalKey: 'completed' }
@@ -45,10 +57,11 @@ test('document center all scope excludes archived rows from merged lists', () =>
const rows = excludeArchivedDocumentRows([
{ claimId: 'a', archived: true },
{ claimId: 'b', rawRequest: { status: 'approved', approval_stage: '归档入账' } },
{ claimId: 'c', rawRequest: { status: 'submitted', approval_stage: '部门审批' } }
{ claimId: 'c', rawRequest: { status: 'submitted', approval_stage: '部门审批' } },
{ claimId: 'd', rawRequest: { status: 'pending_payment', approval_stage: '待付款' } }
])
assert.deepEqual(rows.map((row) => row.claimId), ['c'])
assert.deepEqual(rows.map((row) => row.claimId), ['c', 'd'])
})
test('application scope does not mark submitted approval application rows as new', () => {

View File

@@ -201,7 +201,7 @@ test('documents center switches filter conditions by category tab', () => {
)
assert.match(
documentsCenterView,
/\[DOCUMENT_SCOPE_ARCHIVE\]: \{[\s\S]*dateLabel: '归档时间'[\s\S]*statusTitle: '归档状态'[\s\S]*statusTabs: \['全部', '已完成'\]/
/\[DOCUMENT_SCOPE_ARCHIVE\]: \{[\s\S]*dateLabel: '归档时间'[\s\S]*statusTitle: '归档状态'[\s\S]*statusTabs: \['全部', '已付款', '已完成'\]/
)
assert.match(documentsCenterView, /v-if="showDocumentTypeFilter" class="document-filter"/)
assert.match(documentsCenterView, /:placeholder="activeFilterConfig\.searchPlaceholder"/)

View File

@@ -19,6 +19,10 @@ test('isArchivedExpenseClaim recognizes finance archive stage', () => {
}),
true
)
assert.equal(
isArchivedExpenseClaim({ status: 'paid', approval_stage: '已付款' }),
true
)
})
test('isArchivedExpenseClaim ignores in-progress claims', () => {
@@ -26,6 +30,10 @@ test('isArchivedExpenseClaim ignores in-progress claims', () => {
isArchivedExpenseClaim({ status: 'submitted', approval_stage: '财务审批' }),
false
)
assert.equal(
isArchivedExpenseClaim({ status: 'pending_payment', approval_stage: '待付款' }),
false
)
})
test('archive data stays available through api client but archive center is removed from navigation', () => {

View File

@@ -399,7 +399,7 @@ test('ticket description helper does not show the destination city as detail tex
assert.equal(request.expenseItems.find((item) => item.id === 'ship')?.name, '轮船票')
})
test('completed finance approval marks finance and archive progress steps', () => {
test('finance approval moves reimbursement to pending payment step', () => {
const request = mapExpenseClaimToRequest({
id: 'claim-finance-completed',
claim_no: 'EXP-202605-004',
@@ -414,8 +414,8 @@ test('completed finance approval marks finance and archive progress steps', () =
submitted_at: '2026-05-20T02:00:00.000Z',
created_at: '2026-05-20T01:30:00.000Z',
updated_at: '2026-05-20T04:00:00.000Z',
status: 'approved',
approval_stage: '归档入账',
status: 'pending_payment',
approval_stage: '待付款',
risk_flags_json: [
{
source: 'manual_approval',
@@ -428,7 +428,7 @@ test('completed finance approval marks finance and archive progress steps', () =
source: 'finance_approval',
operator: '财务复核',
previous_approval_stage: '财务审批',
next_approval_stage: '归档入账',
next_approval_stage: '待付款',
created_at: '2026-05-20T04:00:00.000Z'
}
],
@@ -436,14 +436,62 @@ test('completed finance approval marks finance and archive progress steps', () =
})
const financeStep = request.progressSteps.find((step) => step.label === '财务审批')
const archiveStep = request.progressSteps.find((step) => step.label === '归档入账')
const paymentStep = request.progressSteps.find((step) => step.label === '待付款')
assert.equal(request.riskSummary, '无')
assert.equal(request.workflowNode, '归档入账')
assert.equal(request.workflowNode, '待付款')
assert.equal(request.approvalKey, 'pending_payment')
assert.equal(financeStep.time, '财务复核通过')
assert.match(financeStep.detail, /2026-05-20/)
assert.equal(archiveStep.time, '归档入账')
assert.equal(archiveStep.done, true)
assert.equal(paymentStep.current, true)
assert.equal(paymentStep.done, false)
})
test('paid reimbursement marks payment progress step as complete', () => {
const request = mapExpenseClaimToRequest({
id: 'claim-finance-paid',
claim_no: 'EXP-202605-005',
employee_name: '张三',
department_name: '市场部',
expense_type: 'transport',
reason: '交通报销',
location: '上海',
amount: 88,
invoice_count: 1,
occurred_at: '2026-05-20T01:00:00.000Z',
submitted_at: '2026-05-20T02:00:00.000Z',
created_at: '2026-05-20T01:30:00.000Z',
updated_at: '2026-05-20T05:00:00.000Z',
status: 'paid',
approval_stage: '已付款',
risk_flags_json: [
{
source: 'finance_approval',
operator: '财务复核',
previous_approval_stage: '财务审批',
next_approval_stage: '待付款',
created_at: '2026-05-20T04:00:00.000Z'
},
{
source: 'payment',
event_type: 'expense_claim_payment_completed',
operator: '财务付款',
previous_approval_stage: '待付款',
next_approval_stage: '已付款',
created_at: '2026-05-20T05:00:00.000Z'
}
],
items: []
})
const paymentStep = request.progressSteps.find((step) => step.label === '待付款')
const paidStep = request.progressSteps.find((step) => step.label === '已付款')
assert.equal(request.workflowNode, '已付款')
assert.equal(request.approvalStatus, '已付款')
assert.equal(paymentStep.time, '待付款')
assert.equal(paidStep.time, '已付款')
assert.equal(paidStep.done, true)
})
test('current direct manager step shows how long the claim has stayed there', () => {

View File

@@ -63,6 +63,22 @@ test('detects archived claim view models for delete permission gating', () => {
}),
false
)
assert.equal(
isArchivedRequestView({
status: 'pending_payment',
approval_stage: '待付款',
approvalKey: 'pending_payment'
}),
false
)
assert.equal(
isArchivedRequestView({
status: 'paid',
approval_stage: '已付款',
approvalKey: 'completed'
}),
true
)
assert.equal(
isArchivedRequestView({
status: 'approved',
@@ -84,3 +100,18 @@ test('detects archived claim view models for delete permission gating', () => {
false
)
})
test('normalizes pending payment backend claims', () => {
const request = normalizeRequestForUi({
id: 'EXP-202605-004',
claim_id: 'claim-4',
status: 'pending_payment',
approval_stage: '待付款',
expense_type: 'transport',
amount: 88
})
assert.equal(request.approvalKey, 'pending_payment')
assert.equal(request.approvalStatus, '待付款')
assert.equal(request.node, '待付款')
})