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

@@ -17,6 +17,7 @@ from app.models.employee import Employee
from app.models.employee_behavior_profile import EmployeeBehaviorProfileSnapshot
from app.models.financial_record import ExpenseClaim, ExpenseClaimItem
from app.models.organization import OrganizationUnit
from app.models.user_session_metric import UserSessionMetric
from app.services.employee_behavior_profile_service import EmployeeBehaviorProfileService
from app.services.hermes_employee_profile_scanner import HermesEmployeeProfileScannerService
from app.services.hermes_scheduler import HermesScheduler
@@ -167,6 +168,36 @@ def _build_claim(
)
def add_closed_user_session(
db: Session,
*,
session_id: str,
username: str,
display_name: str = "",
employee_no: str = "",
duration_ms: int = 30 * 60 * 1000,
) -> None:
logout_at = datetime.now(UTC) - timedelta(minutes=1)
login_at = logout_at - timedelta(milliseconds=duration_ms)
db.add(
UserSessionMetric(
session_id=session_id,
username=username,
display_name=display_name or username,
employee_no=employee_no,
email=username if "@" in username else "",
login_at=login_at,
logout_at=logout_at,
last_activity_at=logout_at,
duration_ms=duration_ms,
activity_event_count=8,
logout_reason="manual",
status="closed",
)
)
db.commit()
def test_service_scans_snapshots_and_filters_approval_scene() -> None:
session_factory = build_session_factory()
with session_factory() as db:
@@ -238,6 +269,18 @@ def test_current_employee_profile_endpoint_resolves_login_user() -> None:
session_factory = build_session_factory()
with session_factory() as db:
seed_profile_data(db)
EmployeeBehaviorProfileService(db).refresh_employee_profiles(
employee_id="emp-main",
window_days=(90,),
expense_type_scope="overall",
)
add_closed_user_session(
db,
session_id="session-employee-current",
username="zhangsan@example.com",
display_name="张三",
employee_no="E1001",
)
app = create_app()
@@ -266,6 +309,9 @@ def test_current_employee_profile_endpoint_resolves_login_user() -> None:
assert {item["profile_type"] for item in payload["profiles"]} >= {"expense", "ai_usage"}
ai_profile = next(item for item in payload["profiles"] if item["profile_type"] == "ai_usage")
assert ai_profile["metrics"]["ai_run_duration_ms"] == 120
assert ai_profile["metrics"]["online_duration_ms"] == 30 * 60 * 1000
assert ai_profile["metrics"]["usage_duration_ms"] == 30 * 60 * 1000
assert ai_profile["metrics"]["usage_duration_mode"] == "online_session"
assert payload["profile_tags"]
assert payload["radar"]["dimensions"]
@@ -336,6 +382,98 @@ def test_current_admin_profile_endpoint_returns_account_usage_profile() -> None:
assert payload["radar"]["dimensions"]
def test_current_admin_profile_endpoint_uses_online_session_without_agent_runs() -> None:
session_factory = build_session_factory()
with session_factory() as db:
add_closed_user_session(
db,
session_id="session-admin-online",
username="admin",
display_name="admin",
duration_ms=12 * 60 * 1000,
)
app = create_app()
def override_db() -> Generator[Session, None, None]:
db = session_factory()
try:
yield db
finally:
db.close()
app.dependency_overrides[get_db] = override_db
client = TestClient(app)
response = client.get(
"/api/v1/employee-profiles/me/latest",
params={
"scene": "operations",
"window_days": 90,
"expense_type_scope": "overall",
},
headers={"x-auth-username": "admin", "x-auth-name": "admin", "x-auth-is-admin": "true"},
)
assert response.status_code == 200
payload = response.json()
assert payload["employee_id"] == "admin"
assert payload["empty_reason"] == ""
metrics = payload["profiles"][0]["metrics"]
assert metrics["ai_run_count"] == 0
assert metrics["online_duration_ms"] == 12 * 60 * 1000
assert metrics["usage_duration_ms"] == 12 * 60 * 1000
assert metrics["usage_duration_mode"] == "online_session"
def test_finish_session_endpoint_closes_active_session() -> None:
session_factory = build_session_factory()
login_at = datetime.now(UTC) - timedelta(minutes=9)
with session_factory() as db:
db.add(
UserSessionMetric(
session_id="session-active-finish",
username="zhangsan@example.com",
display_name="张三",
employee_no="E1001",
email="zhangsan@example.com",
login_at=login_at,
last_activity_at=login_at,
status="active",
)
)
db.commit()
app = create_app()
def override_db() -> Generator[Session, None, None]:
db = session_factory()
try:
yield db
finally:
db.close()
app.dependency_overrides[get_db] = override_db
client = TestClient(app)
response = client.post(
"/api/v1/auth/sessions/session-active-finish/finish",
json={
"reason": "manual",
"lastActivityAt": datetime.now(UTC).isoformat(),
"activityEventCount": 5,
"pagePath": "/workbench",
},
)
assert response.status_code == 200
payload = response.json()
assert payload["sessionId"] == "session-active-finish"
assert payload["durationMs"] > 0
with session_factory() as db:
session = db.query(UserSessionMetric).filter_by(session_id="session-active-finish").one()
assert session.status == "closed"
assert session.activity_event_count == 5
def test_hermes_scheduler_parses_weekly_profile_cron() -> None:
scheduler = HermesScheduler()