feat: 新增风险图谱算法与系统仪表盘及操作反馈体系
后端新增风险图谱算法模块、风险观察与反馈服务、规则 DSL 校验器和可解释性引擎,完善系统仪表盘和财务仪表盘统计, 优化 agent 运行和编排执行链路,清理旧开发文档,前端新增 系统趋势、负载热力图等多种仪表盘图表组件,完善操作反馈 对话框和工作台日期选择器,优化报销创建和审批详情交互, 补充单元测试覆盖。
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from datetime import UTC, datetime
|
||||
from decimal import Decimal, InvalidOperation
|
||||
|
||||
from sqlalchemy import select
|
||||
@@ -19,6 +19,7 @@ from app.services.document_numbering import (
|
||||
build_document_number,
|
||||
generate_unique_expense_claim_no,
|
||||
)
|
||||
from app.services.user_agent_application_dates import expand_application_time_with_days
|
||||
from app.services.user_agent_application_locations import normalize_application_location
|
||||
|
||||
APPLICATION_CONTEXT_VALUES = {
|
||||
@@ -160,11 +161,10 @@ class UserAgentApplicationMixin:
|
||||
manager_name = str(facts.get("manager_name") or "").strip() or "直属领导"
|
||||
return "\n\n".join(
|
||||
[
|
||||
f"当前操作已完成,单据已经推送给 {manager_name} 进行审核,请耐心等待。",
|
||||
"申请单据已生成,并已进入审批流程。",
|
||||
f"系统已推送给 {manager_name} 审核,当前节点:{manager_name}审核中。",
|
||||
f"申请单号:{application_no}",
|
||||
"申请信息:\n" + self._build_application_summary_table(facts),
|
||||
f"当前状态:{manager_name}审核中。",
|
||||
"费用预估:预计费用已随申请提交,等待领导审核确认。",
|
||||
"下方是简要单据信息。需要查看完整详情时,请点击快捷方式进入单据详情。",
|
||||
]
|
||||
)
|
||||
|
||||
@@ -217,6 +217,7 @@ class UserAgentApplicationMixin:
|
||||
facts["time"] = self._expand_application_time_with_days(
|
||||
facts.get("time", ""),
|
||||
facts.get("days", ""),
|
||||
payload.context_json or {},
|
||||
)
|
||||
return facts
|
||||
|
||||
@@ -467,81 +468,16 @@ class UserAgentApplicationMixin:
|
||||
return text
|
||||
|
||||
@staticmethod
|
||||
def _expand_application_time_with_days(time_text: str, days_text: str) -> str:
|
||||
normalized_time = str(time_text or "").strip()
|
||||
if not normalized_time or re.search(r"\s*(?:至|到|~|-{2,}|—)\s*", normalized_time):
|
||||
return normalized_time
|
||||
|
||||
days = UserAgentApplicationMixin._resolve_application_days_count(days_text)
|
||||
if not days:
|
||||
return normalized_time
|
||||
|
||||
match = re.search(
|
||||
r"(?P<date>20\d{2}[-/.年]\d{1,2}[-/.月]\d{1,2}日?)",
|
||||
normalized_time,
|
||||
def _expand_application_time_with_days(
|
||||
time_text: str,
|
||||
days_text: str,
|
||||
context_json: dict[str, object] | None = None,
|
||||
) -> str:
|
||||
return expand_application_time_with_days(
|
||||
time_text,
|
||||
days_text,
|
||||
context_json=context_json or {},
|
||||
)
|
||||
if not match:
|
||||
return normalized_time
|
||||
|
||||
parsed_start = UserAgentApplicationMixin._parse_application_date(match.group("date"))
|
||||
if parsed_start is None:
|
||||
return normalized_time
|
||||
|
||||
end_date = parsed_start + timedelta(days=days)
|
||||
return f"{parsed_start:%Y-%m-%d} 至 {end_date:%Y-%m-%d}"
|
||||
|
||||
@staticmethod
|
||||
def _resolve_application_days_count(days_text: str) -> int:
|
||||
text = str(days_text or "").strip()
|
||||
if not text:
|
||||
return 0
|
||||
digit_match = re.search(r"\d+", text)
|
||||
if digit_match:
|
||||
return max(0, int(digit_match.group(0)))
|
||||
|
||||
chinese_match = re.search(r"[一二两三四五六七八九十]{1,3}", text)
|
||||
if not chinese_match:
|
||||
return 0
|
||||
return UserAgentApplicationMixin._parse_chinese_number(chinese_match.group(0))
|
||||
|
||||
@staticmethod
|
||||
def _parse_chinese_number(value: str) -> int:
|
||||
digits = {
|
||||
"一": 1,
|
||||
"二": 2,
|
||||
"两": 2,
|
||||
"三": 3,
|
||||
"四": 4,
|
||||
"五": 5,
|
||||
"六": 6,
|
||||
"七": 7,
|
||||
"八": 8,
|
||||
"九": 9,
|
||||
}
|
||||
text = str(value or "").strip()
|
||||
if not text:
|
||||
return 0
|
||||
if text == "十":
|
||||
return 10
|
||||
if "十" in text:
|
||||
left, _, right = text.partition("十")
|
||||
tens = digits.get(left, 1) if left else 1
|
||||
ones = digits.get(right, 0) if right else 0
|
||||
return tens * 10 + ones
|
||||
return digits.get(text, 0)
|
||||
|
||||
@staticmethod
|
||||
def _parse_application_date(value: str) -> datetime | None:
|
||||
normalized = str(value or "").strip().rstrip("日").replace("年", "-").replace("月", "-")
|
||||
normalized = normalized.replace("/", "-").replace(".", "-")
|
||||
parts = [part for part in normalized.split("-") if part]
|
||||
if len(parts) != 3:
|
||||
return None
|
||||
try:
|
||||
year, month, day = (int(part) for part in parts)
|
||||
return datetime(year, month, day)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def _resolve_application_amount(
|
||||
self,
|
||||
|
||||
Reference in New Issue
Block a user