feat: 新增预算中心本体与风险规则评分回填
后端新增预算本体解析模块和风险规则评分回填服务,优化规则 生成本体对齐和提示词构建,增强费用类型关键词和本体验证, 完善报销查询和审计接口,前端预算中心页面增加对话框和本 体工具函数,重构审计页面元数据和视图模型,补充单元测试。
This commit is contained in:
@@ -83,8 +83,10 @@ EXPENSE_TYPE_LABELS = {
|
||||
"meal": "业务招待费",
|
||||
"meeting": "会务费",
|
||||
"entertainment": "业务招待费",
|
||||
"marketing": "市场推广费",
|
||||
"office": "办公用品费",
|
||||
"training": "培训费",
|
||||
"software": "软件服务费",
|
||||
"communication": "通讯费",
|
||||
"welfare": "福利费",
|
||||
"other": "其他费用",
|
||||
@@ -131,7 +133,9 @@ class OrchestratorDatabaseQueryBuilder:
|
||||
message=message,
|
||||
)
|
||||
count_stmt = select(func.count()).select_from(ExpenseClaim)
|
||||
amount_stmt = select(func.coalesce(func.sum(ExpenseClaim.amount), 0)).select_from(ExpenseClaim)
|
||||
amount_stmt = select(func.coalesce(func.sum(ExpenseClaim.amount), 0)).select_from(
|
||||
ExpenseClaim
|
||||
)
|
||||
for condition in conditions:
|
||||
count_stmt = count_stmt.where(condition)
|
||||
amount_stmt = amount_stmt.where(condition)
|
||||
@@ -148,7 +152,9 @@ class OrchestratorDatabaseQueryBuilder:
|
||||
|
||||
if recent_window_applied:
|
||||
reference_now = self._resolve_reference_now(context_json)
|
||||
recent_window_start, recent_window_end = self._resolve_expense_recent_window_bounds(reference_now)
|
||||
recent_window_start, recent_window_end = self._resolve_expense_recent_window_bounds(
|
||||
reference_now
|
||||
)
|
||||
recent_condition = self._build_expense_recent_window_condition(
|
||||
recent_window_start,
|
||||
recent_window_end,
|
||||
@@ -157,9 +163,13 @@ class OrchestratorDatabaseQueryBuilder:
|
||||
window_start_date = recent_window_start.date().isoformat()
|
||||
window_end_date = (recent_window_end - timedelta(microseconds=1)).date().isoformat()
|
||||
|
||||
recent_count_stmt = select(func.count()).select_from(ExpenseClaim).where(recent_condition)
|
||||
recent_amount_stmt = select(func.coalesce(func.sum(ExpenseClaim.amount), 0)).select_from(ExpenseClaim).where(
|
||||
recent_condition
|
||||
recent_count_stmt = (
|
||||
select(func.count()).select_from(ExpenseClaim).where(recent_condition)
|
||||
)
|
||||
recent_amount_stmt = (
|
||||
select(func.coalesce(func.sum(ExpenseClaim.amount), 0))
|
||||
.select_from(ExpenseClaim)
|
||||
.where(recent_condition)
|
||||
)
|
||||
for condition in conditions:
|
||||
recent_count_stmt = recent_count_stmt.where(condition)
|
||||
@@ -189,7 +199,11 @@ class OrchestratorDatabaseQueryBuilder:
|
||||
"record_count": display_count,
|
||||
"total_amount": round(display_amount, 2),
|
||||
"scope_label": scope_label,
|
||||
"title": f"最近 {len(preview_claims)} 条{scope_label}" if preview_claims else f"{scope_label}筛选结果",
|
||||
"title": (
|
||||
f"最近 {len(preview_claims)} 条{scope_label}"
|
||||
if preview_claims
|
||||
else f"{scope_label}筛选结果"
|
||||
),
|
||||
"scoped_to_current_user": scoped_to_current_user,
|
||||
"recent_window_applied": recent_window_applied,
|
||||
"window_days": EXPENSE_QUERY_RECENT_WINDOW_DAYS if recent_window_applied else None,
|
||||
@@ -280,7 +294,8 @@ class OrchestratorDatabaseQueryBuilder:
|
||||
reference_now: datetime,
|
||||
) -> tuple[datetime, datetime]:
|
||||
normalized_now = reference_now.astimezone(UTC)
|
||||
window_end = normalized_now.replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1)
|
||||
window_end = normalized_now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
window_end += timedelta(days=1)
|
||||
window_start = window_end - timedelta(days=EXPENSE_QUERY_RECENT_WINDOW_DAYS)
|
||||
return window_start, window_end
|
||||
|
||||
@@ -300,7 +315,11 @@ class OrchestratorDatabaseQueryBuilder:
|
||||
self,
|
||||
conditions: list[Any],
|
||||
) -> list[dict[str, Any]]:
|
||||
stmt = select(ExpenseClaim.status, func.count()).select_from(ExpenseClaim).group_by(ExpenseClaim.status)
|
||||
stmt = (
|
||||
select(ExpenseClaim.status, func.count())
|
||||
.select_from(ExpenseClaim)
|
||||
.group_by(ExpenseClaim.status)
|
||||
)
|
||||
for condition in conditions:
|
||||
stmt = stmt.where(condition)
|
||||
|
||||
@@ -356,7 +375,10 @@ class OrchestratorDatabaseQueryBuilder:
|
||||
"claim_no": claim.claim_no,
|
||||
"employee_name": claim.employee_name,
|
||||
"expense_type": claim.expense_type,
|
||||
"expense_type_label": EXPENSE_TYPE_LABELS.get(claim.expense_type, claim.expense_type or "报销"),
|
||||
"expense_type_label": EXPENSE_TYPE_LABELS.get(
|
||||
claim.expense_type,
|
||||
claim.expense_type or "报销",
|
||||
),
|
||||
"amount": round(float(claim.amount), 2),
|
||||
"status": claim.status,
|
||||
"status_label": status_label,
|
||||
@@ -378,7 +400,11 @@ class OrchestratorDatabaseQueryBuilder:
|
||||
normalized_flags: list[dict[str, str]] = []
|
||||
for index, raw_flag in enumerate(raw_flags, start=1):
|
||||
if isinstance(raw_flag, dict):
|
||||
raw_level = str(raw_flag.get("severity") or raw_flag.get("level") or "").strip().lower()
|
||||
raw_level = (
|
||||
str(raw_flag.get("severity") or raw_flag.get("level") or "")
|
||||
.strip()
|
||||
.lower()
|
||||
)
|
||||
level = raw_level if raw_level in EXPENSE_RISK_LEVEL_LABELS else "medium"
|
||||
summary = str(
|
||||
raw_flag.get("message")
|
||||
@@ -397,7 +423,11 @@ class OrchestratorDatabaseQueryBuilder:
|
||||
raw_text = str(raw_flag or "").strip()
|
||||
if not raw_text:
|
||||
continue
|
||||
level = "high" if any(keyword in raw_text for keyword in ("高风险", "超标", "重复", "异常")) else "medium"
|
||||
level = (
|
||||
"high"
|
||||
if any(keyword in raw_text for keyword in ("高风险", "超标", "重复", "异常"))
|
||||
else "medium"
|
||||
)
|
||||
summary = raw_text
|
||||
detail = raw_text
|
||||
title = EXPENSE_RISK_LEVEL_LABELS[level]
|
||||
@@ -436,14 +466,16 @@ class OrchestratorDatabaseQueryBuilder:
|
||||
dict.fromkeys(
|
||||
str(item.normalized_value or item.value or "").strip().upper()
|
||||
for item in ontology.entities
|
||||
if item.type == "expense_claim" and str(item.normalized_value or item.value or "").strip()
|
||||
if item.type == "expense_claim"
|
||||
and str(item.normalized_value or item.value or "").strip()
|
||||
)
|
||||
)
|
||||
expense_types = list(
|
||||
dict.fromkeys(
|
||||
str(item.normalized_value or item.value or "").strip()
|
||||
for item in ontology.entities
|
||||
if item.type == "expense_type" and str(item.normalized_value or item.value or "").strip()
|
||||
if item.type == "expense_type"
|
||||
and str(item.normalized_value or item.value or "").strip()
|
||||
)
|
||||
)
|
||||
project_values = self._collect_expense_query_filter_values(ontology, "project")
|
||||
@@ -551,7 +583,11 @@ class OrchestratorDatabaseQueryBuilder:
|
||||
else:
|
||||
scope_label = "全部报销单"
|
||||
|
||||
return conditions, self._compose_expense_scope_label(scope_label, status_values), scoped_to_current_user
|
||||
return (
|
||||
conditions,
|
||||
self._compose_expense_scope_label(scope_label, status_values),
|
||||
scoped_to_current_user,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _resolve_expense_query_status_values(
|
||||
|
||||
Reference in New Issue
Block a user