feat: 新增风险图谱算法与系统仪表盘及操作反馈体系
后端新增风险图谱算法模块、风险观察与反馈服务、规则 DSL 校验器和可解释性引擎,完善系统仪表盘和财务仪表盘统计, 优化 agent 运行和编排执行链路,清理旧开发文档,前端新增 系统趋势、负载热力图等多种仪表盘图表组件,完善操作反馈 对话框和工作台日期选择器,优化报销创建和审批详情交互, 补充单元测试覆盖。
This commit is contained in:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user