feat: 新增预算后端服务与差旅风险规则库
后端新增预算模型、端点和服务模块,支持预算 CRUD 和余额 查询,清理旧生成规则文件并替换为按严重等级分类的差旅风 险规则库,优化认证权限和报销单访问策略,新增财务规则目 录和演示数据构建脚本,前端预算中心增加对话框交互,完善 审计页面运行时模型和元数据展示,补充单元测试。
This commit is contained in:
@@ -23,6 +23,21 @@ from app.services.agent_foundation_constants import (
|
||||
|
||||
logger = get_logger("app.services.agent_foundation")
|
||||
|
||||
EXPENSE_TYPE_SCENARIO_LABELS = {
|
||||
"travel": "差旅费",
|
||||
"hotel": "住宿费",
|
||||
"transport": "交通费",
|
||||
"meal": "业务招待费",
|
||||
"meeting": "会务费",
|
||||
"marketing": "市场推广费",
|
||||
"office": "办公用品费",
|
||||
"training": "培训费",
|
||||
"software": "软件服务费",
|
||||
"communication": "通信费",
|
||||
"welfare": "福利费",
|
||||
}
|
||||
|
||||
|
||||
class AgentFoundationRiskRuleMixin:
|
||||
def _iter_platform_risk_manifests(self) -> list[tuple[str, dict[str, object]]]:
|
||||
|
||||
@@ -123,8 +138,54 @@ class AgentFoundationRiskRuleMixin:
|
||||
|
||||
return "通用"
|
||||
|
||||
@staticmethod
|
||||
def _resolve_manifest_expense_types(manifest: dict[str, object]) -> list[str]:
|
||||
def _collect(value: object) -> list[str]:
|
||||
if isinstance(value, str):
|
||||
return [value]
|
||||
if isinstance(value, (list, tuple, set)):
|
||||
return [str(item or "").strip() for item in value]
|
||||
return []
|
||||
|
||||
candidates: list[str] = []
|
||||
candidates.extend(_collect(manifest.get("expense_types")))
|
||||
|
||||
applies_to = (
|
||||
manifest.get("applies_to") if isinstance(manifest.get("applies_to"), dict) else {}
|
||||
)
|
||||
candidates.extend(_collect(applies_to.get("expense_types")))
|
||||
|
||||
metadata = manifest.get("metadata") if isinstance(manifest.get("metadata"), dict) else {}
|
||||
candidates.extend(_collect(metadata.get("expense_types")))
|
||||
|
||||
normalized: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for item in candidates:
|
||||
value = item.strip().lower()
|
||||
if not value or value in seen:
|
||||
continue
|
||||
seen.add(value)
|
||||
normalized.append(value)
|
||||
return normalized
|
||||
|
||||
@staticmethod
|
||||
def _expense_type_scenario_labels(expense_types: list[str]) -> list[str]:
|
||||
labels: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for expense_type in expense_types:
|
||||
label = EXPENSE_TYPE_SCENARIO_LABELS.get(expense_type)
|
||||
if not label or label in seen:
|
||||
continue
|
||||
seen.add(label)
|
||||
labels.append(label)
|
||||
return labels
|
||||
|
||||
def _platform_risk_scenario_json(self, manifest: dict[str, object]) -> list[str]:
|
||||
|
||||
labels = self._expense_type_scenario_labels(self._resolve_manifest_expense_types(manifest))
|
||||
if labels:
|
||||
return labels
|
||||
|
||||
category = self._resolve_platform_risk_category(manifest)
|
||||
|
||||
return [category] if category else ["通用"]
|
||||
@@ -139,7 +200,7 @@ class AgentFoundationRiskRuleMixin:
|
||||
|
||||
risk_category = self._resolve_platform_risk_category(manifest)
|
||||
|
||||
return {
|
||||
config = {
|
||||
|
||||
"severity": str(fail_outcome.get("severity") or "medium"),
|
||||
|
||||
@@ -176,6 +237,20 @@ class AgentFoundationRiskRuleMixin:
|
||||
),
|
||||
|
||||
}
|
||||
metadata = manifest.get("metadata") if isinstance(manifest.get("metadata"), dict) else {}
|
||||
for key in (
|
||||
"finance_rule_code",
|
||||
"finance_rule_sheet",
|
||||
"business_stage",
|
||||
"expense_types",
|
||||
"budget_required",
|
||||
):
|
||||
value = manifest.get(key)
|
||||
if value is None and isinstance(metadata, dict):
|
||||
value = metadata.get(key)
|
||||
if value is not None:
|
||||
config[key] = value
|
||||
return config
|
||||
|
||||
def _build_platform_risk_seed_assets(self) -> list[AgentAsset]:
|
||||
|
||||
@@ -242,6 +317,12 @@ class AgentFoundationRiskRuleMixin:
|
||||
before_count = len(existing_codes)
|
||||
|
||||
self._ensure_platform_risk_rules_from_library(existing_codes)
|
||||
manifest_codes = {
|
||||
str(manifest.get("rule_code") or "").strip()
|
||||
for _, manifest in self._iter_platform_risk_manifests()
|
||||
if str(manifest.get("rule_code") or "").strip()
|
||||
}
|
||||
self._hide_stale_demo_risk_rules(manifest_codes)
|
||||
|
||||
self.db.flush()
|
||||
|
||||
@@ -265,6 +346,25 @@ class AgentFoundationRiskRuleMixin:
|
||||
|
||||
return manifest_count
|
||||
|
||||
def _hide_stale_demo_risk_rules(self, manifest_codes: set[str]) -> None:
|
||||
assets = self.db.scalars(
|
||||
select(AgentAsset).where(AgentAsset.asset_type == AgentAssetType.RULE.value)
|
||||
).all()
|
||||
for asset in assets:
|
||||
config = asset.config_json if isinstance(asset.config_json, dict) else {}
|
||||
if config.get("source_ref") != "费用管控 Demo 风险规则库":
|
||||
continue
|
||||
if asset.code in manifest_codes:
|
||||
continue
|
||||
asset.status = AgentAssetStatus.DISABLED.value
|
||||
asset.config_json = {
|
||||
**config,
|
||||
"enabled": False,
|
||||
"tag": "废弃风险规则",
|
||||
"deprecated": True,
|
||||
"deprecated_reason": "对应风险规则 JSON 已删除,不再参与费用管控 Demo。",
|
||||
}
|
||||
|
||||
def _ensure_platform_risk_rules_from_library(self, existing_codes: set[str]) -> None:
|
||||
|
||||
for file_name, manifest in self._iter_platform_risk_manifests():
|
||||
|
||||
Reference in New Issue
Block a user