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

@@ -2489,7 +2489,7 @@ def test_list_claims_scopes_to_current_user_id_even_when_names_duplicate() -> No
assert claims[0].claim_no == "EXP-DUP-001"
def test_list_claims_allows_finance_to_view_all_records() -> None:
def test_list_claims_limits_finance_to_personal_records() -> None:
current_user = CurrentUserContext(
username="finance@example.com",
name="财务",
@@ -2501,8 +2501,8 @@ def test_list_claims_allows_finance_to_view_all_records() -> None:
db.add_all(
[
ExpenseClaim(
claim_no="EXP-FIN-101",
employee_name="",
claim_no="EXP-FIN-OWN",
employee_name="财务",
department_name="A部",
project_code="PRJ-A",
expense_type="travel",
@@ -2518,7 +2518,7 @@ def test_list_claims_allows_finance_to_view_all_records() -> None:
risk_flags_json=[],
),
ExpenseClaim(
claim_no="EXP-FIN-102",
claim_no="EXP-FIN-OTHER-DRAFT",
employee_name="",
department_name="B部",
project_code="PRJ-B",
@@ -2529,9 +2529,9 @@ def test_list_claims_allows_finance_to_view_all_records() -> None:
currency="CNY",
invoice_count=1,
occurred_at=datetime(2026, 5, 11, 12, 0, tzinfo=UTC),
submitted_at=datetime(2026, 5, 11, 13, 0, tzinfo=UTC),
status="approved",
approval_stage="completed",
submitted_at=None,
status="draft",
approval_stage="待提交",
risk_flags_json=[],
),
]
@@ -2541,10 +2541,10 @@ def test_list_claims_allows_finance_to_view_all_records() -> None:
claims = ExpenseClaimService(db).list_claims(current_user)
assert len(claims) == 1
assert claims[0].claim_no == "EXP-FIN-101"
assert claims[0].claim_no == "EXP-FIN-OWN"
def test_list_claims_allows_executive_to_view_all_records() -> None:
def test_list_claims_limits_executive_to_personal_records() -> None:
current_user = CurrentUserContext(
username="executive@example.com",
name="高管",
@@ -2556,8 +2556,8 @@ def test_list_claims_allows_executive_to_view_all_records() -> None:
db.add_all(
[
ExpenseClaim(
claim_no="EXP-EXE-101",
employee_name="",
claim_no="EXP-EXE-OWN",
employee_name="高管",
department_name="A部",
project_code="PRJ-A",
expense_type="travel",
@@ -2573,7 +2573,7 @@ def test_list_claims_allows_executive_to_view_all_records() -> None:
risk_flags_json=[],
),
ExpenseClaim(
claim_no="EXP-EXE-102",
claim_no="EXP-EXE-OTHER-DRAFT",
employee_name="",
department_name="B部",
project_code="PRJ-B",
@@ -2584,9 +2584,9 @@ def test_list_claims_allows_executive_to_view_all_records() -> None:
currency="CNY",
invoice_count=1,
occurred_at=datetime(2026, 5, 11, 12, 0, tzinfo=UTC),
submitted_at=datetime(2026, 5, 11, 13, 0, tzinfo=UTC),
status="approved",
approval_stage="completed",
submitted_at=None,
status="draft",
approval_stage="待提交",
risk_flags_json=[],
),
]
@@ -2596,7 +2596,7 @@ def test_list_claims_allows_executive_to_view_all_records() -> None:
claims = ExpenseClaimService(db).list_claims(current_user)
assert len(claims) == 1
assert claims[0].claim_no == "EXP-EXE-101"
assert claims[0].claim_no == "EXP-EXE-OWN"
def test_list_claims_keeps_own_archived_claim_for_finance_applicant() -> None:
@@ -3411,7 +3411,20 @@ def test_direct_manager_can_route_application_claim_to_budget_approval_then_budg
submitted_at=datetime(2026, 5, 25, 10, 0, tzinfo=UTC),
status="submitted",
approval_stage="直属领导审批",
risk_flags_json=[],
risk_flags_json=[
{
"source": "application_detail",
"application_detail": {
"application_type": "差旅费用申请",
"time": "2026-05-25 至 2026-05-27",
"location": "上海",
"reason": "支撑国网服务器上线部署",
"days": "3 天",
"transport_mode": "高铁",
"amount": "12000.00",
},
}
],
)
db.add(claim)
db.commit()
@@ -3475,6 +3488,10 @@ def test_direct_manager_can_route_application_claim_to_budget_approval_then_budg
and flag.get("source") == "application_handoff"
and flag.get("event_type") == "expense_application_to_reimbursement_draft"
and flag.get("application_claim_no") == "APP-20260525-APPROVE"
and flag.get("application_detail", {}).get("application_content") == "差旅费用申请 / 上海"
and flag.get("application_detail", {}).get("application_reason") == "支撑国网服务器上线部署"
and flag.get("application_detail", {}).get("application_days") == "3 天"
and flag.get("application_detail", {}).get("application_transport_mode") == "高铁"
and flag.get("leader_opinion") == "业务必要,同意申请。"
and flag.get("budget_opinion") == "预算额度可承接,同意。"
for flag in generated_draft.risk_flags_json
@@ -3493,6 +3510,114 @@ def test_direct_manager_can_route_application_claim_to_budget_approval_then_budg
)
def test_direct_manager_budget_monitor_completes_application_claim_without_duplicate_budget_approval() -> None:
manager_user = CurrentUserContext(
username="manager-budget-monitor-application@example.com",
name="李预算经理",
role_codes=["manager", "budget_monitor", "executive"],
is_admin=False,
)
with build_session() as db:
budget_role = _seed_budget_monitor_role(db)
department = OrganizationUnit(
unit_code="DELIVERY-BUDGET-MERGED",
name="交付部",
unit_type="department",
)
manager = Employee(
employee_no="E8112-MERGED",
name="李预算经理",
email="manager-budget-monitor-application@example.com",
grade="P8",
organization_unit=department,
roles=[budget_role],
)
employee = Employee(
employee_no="E8113-MERGED",
name="张三",
email="zhangsan-budget-monitor-application@example.com",
manager=manager,
organization_unit=department,
)
db.add_all([department, manager, employee])
db.flush()
claim = ExpenseClaim(
claim_no="APP-20260525-MERGED",
employee_id=employee.id,
employee_name="张三",
department_id=department.id,
department_name="交付部",
project_code="PRJ-A",
expense_type="travel_application",
reason="支撑国网服务器上线部署",
location="上海",
amount=Decimal("12000.00"),
currency="CNY",
invoice_count=0,
occurred_at=datetime(2026, 5, 25, 9, 0, tzinfo=UTC),
submitted_at=datetime(2026, 5, 25, 10, 0, tzinfo=UTC),
status="submitted",
approval_stage="直属领导审批",
risk_flags_json=[],
)
db.add(claim)
db.commit()
claim_id = claim.id
approved = ExpenseClaimService(db).approve_claim(
claim_id,
manager_user,
opinion="业务必要且预算可承接,同意申请。",
)
assert approved is not None
assert approved.status == "approved"
assert approved.approval_stage == "审批完成"
assert db.query(ExpenseClaim).filter(ExpenseClaim.claim_no.like("RE-%")).count() == 1
assert not any(
isinstance(flag, dict)
and flag.get("next_approval_stage") == "预算管理者审批"
for flag in approved.risk_flags_json
)
assert any(
isinstance(flag, dict)
and flag.get("source") == "manual_approval"
and flag.get("event_type") == "expense_application_approval"
and flag.get("label") == "领导及预算审核通过"
and flag.get("opinion") == "业务必要且预算可承接,同意申请。"
and flag.get("previous_approval_stage") == "直属领导审批"
and flag.get("next_status") == "approved"
and flag.get("next_approval_stage") == "审批完成"
and flag.get("budget_approval_merged") is True
and flag.get("budget_approval_merged_reason") == "direct_manager_is_department_budget_monitor"
for flag in approved.risk_flags_json
)
generated_draft = db.query(ExpenseClaim).filter(ExpenseClaim.claim_no.like("RE-%")).one()
assert generated_draft.status == "draft"
assert generated_draft.expense_type == "travel"
reviewer_claims = ExpenseClaimService(db).list_claims(manager_user)
assert all(claim.claim_no != generated_draft.claim_no for claim in reviewer_claims)
applicant_claims = ExpenseClaimService(db).list_claims(
CurrentUserContext(
username="zhangsan-budget-monitor-application@example.com",
name="张三",
role_codes=[],
is_admin=False,
)
)
assert any(claim.claim_no == generated_draft.claim_no for claim in applicant_claims)
assert any(
isinstance(flag, dict)
and flag.get("source") == "application_handoff"
and flag.get("event_type") == "expense_application_to_reimbursement_draft"
and flag.get("application_claim_no") == "APP-20260525-MERGED"
and flag.get("leader_opinion") == "业务必要且预算可承接,同意申请。"
and flag.get("budget_opinion") == "业务必要且预算可承接,同意申请。"
for flag in generated_draft.risk_flags_json
)
def test_direct_manager_return_application_claim_records_return_node_and_opinion() -> None:
manager_user = CurrentUserContext(
username="manager-application-return@example.com",