feat: 新增风险图谱算法与系统仪表盘及操作反馈体系
后端新增风险图谱算法模块、风险观察与反馈服务、规则 DSL 校验器和可解释性引擎,完善系统仪表盘和财务仪表盘统计, 优化 agent 运行和编排执行链路,清理旧开发文档,前端新增 系统趋势、负载热力图等多种仪表盘图表组件,完善操作反馈 对话框和工作台日期选择器,优化报销创建和审批详情交互, 补充单元测试覆盖。
This commit is contained in:
128
server/src/app/services/user_agent_application_dates.py
Normal file
128
server/src/app/services/user_agent_application_dates.py
Normal file
@@ -0,0 +1,128 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from datetime import UTC, date, datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
|
||||
def expand_application_time_with_days(
|
||||
time_text: str,
|
||||
days_text: str,
|
||||
*,
|
||||
context_json: dict[str, Any] | None = None,
|
||||
) -> str:
|
||||
normalized_time = str(time_text or "").strip()
|
||||
days = resolve_application_days_count(days_text)
|
||||
if not days:
|
||||
return normalized_time
|
||||
|
||||
if normalized_time and re.search(r"\s*(?:至|到|~|-{2,}|—)\s*", normalized_time):
|
||||
return normalized_time
|
||||
|
||||
parsed_start = _resolve_start_date(normalized_time, context_json or {})
|
||||
if parsed_start is None:
|
||||
return normalized_time
|
||||
|
||||
end_date = parsed_start + timedelta(days=max(days - 1, 0))
|
||||
start_text = f"{parsed_start:%Y-%m-%d}"
|
||||
end_text = f"{end_date:%Y-%m-%d}"
|
||||
return start_text if start_text == end_text else f"{start_text} 至 {end_text}"
|
||||
|
||||
|
||||
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 _parse_chinese_number(chinese_match.group(0))
|
||||
|
||||
|
||||
def _resolve_start_date(time_text: str, context_json: dict[str, Any]) -> date | None:
|
||||
if time_text:
|
||||
match = re.search(
|
||||
r"(?P<date>20\d{2}[-/.年]\d{1,2}[-/.月]\d{1,2}日?)",
|
||||
time_text,
|
||||
)
|
||||
if match:
|
||||
return _parse_application_date(match.group("date"))
|
||||
return None
|
||||
return _resolve_client_today(context_json)
|
||||
|
||||
|
||||
def _resolve_client_today(context_json: dict[str, Any]) -> date:
|
||||
raw_now = str(context_json.get("client_now_iso") or "").strip()
|
||||
parsed_now = _parse_client_now(raw_now)
|
||||
if parsed_now is None:
|
||||
return datetime.now(UTC).date()
|
||||
|
||||
offset_minutes = _parse_timezone_offset_minutes(
|
||||
context_json.get("client_timezone_offset_minutes"),
|
||||
)
|
||||
if offset_minutes is not None:
|
||||
parsed_now = parsed_now - timedelta(minutes=offset_minutes)
|
||||
return parsed_now.date()
|
||||
|
||||
|
||||
def _parse_client_now(value: str) -> datetime | None:
|
||||
if not value:
|
||||
return None
|
||||
normalized = value.replace("Z", "+00:00")
|
||||
try:
|
||||
parsed = datetime.fromisoformat(normalized)
|
||||
except ValueError:
|
||||
return None
|
||||
if parsed.tzinfo is None:
|
||||
parsed = parsed.replace(tzinfo=UTC)
|
||||
return parsed.astimezone(UTC)
|
||||
|
||||
|
||||
def _parse_timezone_offset_minutes(value: Any) -> int | None:
|
||||
try:
|
||||
return int(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def _parse_application_date(value: str) -> date | 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 date(year, month, day)
|
||||
except ValueError:
|
||||
return None
|
||||
Reference in New Issue
Block a user