feat: 扩展风险规则体系、审批动态路由与预算中心列表化改造

- 新增 25+ 条风险规则(预算/报销/申请/通用类),完善风险规则模拟与反馈发布机制
- 引入费用审批动态路由、平台风险分级、预审与风险阶段管理
- 预算中心列表化改造,优化票据夹仪表盘与数字员工工作看板
- 新增 Hermes 风险线索收集器、Agent 链路追踪中心
- 扩展数字员工能力库(18 个领域 Skill)与交通费用自动预估
- 完善报销申请快速预览、权限控制与前端测试覆盖
This commit is contained in:
caoxiaozhu
2026-06-01 17:07:14 +08:00
parent 7989f3a159
commit 92444e7eae
285 changed files with 25075 additions and 2986 deletions

View File

@@ -4,7 +4,7 @@ from datetime import UTC, datetime, timedelta
from decimal import Decimal
from typing import Any
from sqlalchemy import or_, select
from sqlalchemy import func, or_, select
from sqlalchemy.orm import Session, selectinload
from app.algorithem.employee_behavior_profile import (
@@ -102,7 +102,8 @@ class EmployeeBehaviorProfileService(EmployeeBehaviorProfileMetricHelpers):
commit: bool = True,
) -> list[EmployeeBehaviorProfileSnapshot]:
self.ensure_storage_ready()
employee = self.db.get(Employee, employee_id)
requested_employee_id = str(employee_id or "").strip()
employee = self._resolve_employee_by_identifier(requested_employee_id)
if employee is None:
return []
@@ -161,10 +162,11 @@ class EmployeeBehaviorProfileService(EmployeeBehaviorProfileMetricHelpers):
expense_type_scope: str = "overall",
) -> EmployeeProfileLatestRead:
self.ensure_storage_ready()
employee = self.db.get(Employee, employee_id)
requested_employee_id = str(employee_id or "").strip()
employee = self._resolve_employee_by_identifier(requested_employee_id)
if employee is None:
return EmployeeProfileLatestRead(
employee_id=employee_id,
employee_id=requested_employee_id,
scene=scene,
window_days=window_days,
expense_type_scope=expense_type_scope,
@@ -172,22 +174,23 @@ class EmployeeBehaviorProfileService(EmployeeBehaviorProfileMetricHelpers):
)
resolved_scope = self._resolve_scope_from_claim(claim_id, expense_type_scope)
resolved_employee_id = employee.id
rows = self._load_latest_snapshots(
employee_id=employee_id,
employee_id=resolved_employee_id,
window_days=window_days,
expense_type_scope=resolved_scope,
scene=scene,
)
if not rows and claim_id:
self.refresh_employee_profiles(
employee_id=employee_id,
employee_id=resolved_employee_id,
window_days=(window_days,),
expense_type_scope=resolved_scope,
source_task_type="api_on_demand",
claim_id=claim_id,
)
rows = self._load_latest_snapshots(
employee_id=employee_id,
employee_id=resolved_employee_id,
window_days=window_days,
expense_type_scope=resolved_scope,
scene=scene,
@@ -201,6 +204,31 @@ class EmployeeBehaviorProfileService(EmployeeBehaviorProfileMetricHelpers):
expense_type_scope=resolved_scope,
)
def _resolve_employee_by_identifier(self, identifier: str) -> Employee | None:
normalized = str(identifier or "").strip()
if not normalized:
return None
employee = self.db.get(Employee, normalized)
if employee is not None:
return employee
normalized_email = normalized.lower()
conditions = [
Employee.name == normalized,
Employee.employee_no == normalized,
]
if "@" in normalized_email:
conditions.append(func.lower(Employee.email) == normalized_email)
stmt = (
select(Employee)
.where(or_(*conditions))
.order_by(Employee.created_at.asc())
.limit(1)
)
return self.db.scalars(stmt).first()
def _build_window_context(
self,
*,