feat: 增强风险规则生成引擎与预算中心页面

后端拆分风险规则生成为解释器、语义分析、本体对齐等子模块,
优化模板执行和流程图生成,完善员工种子数据和导入逻辑,增强
报销单权限策略和草稿持久化,前端新增预算中心视图和趋势图
组件,重构审计页面和风险规则测试对话框交互,完善文档中心
和报销创建页面细节,补充单元测试覆盖。
This commit is contained in:
caoxiaozhu
2026-05-26 09:15:14 +08:00
parent d0e946cf47
commit 0e861d8fa6
150 changed files with 14953 additions and 4099 deletions

View File

@@ -33,9 +33,11 @@ from app.services.agent_asset_spreadsheet import RISK_RULES_LIBRARY
from app.services.agent_foundation import AgentFoundationService
from app.services.audit import AuditLogService
from app.services.document_intelligence import build_document_insight
from app.services.document_numbering import is_application_claim_no
from app.services.expense_claim_access_policy import ExpenseClaimAccessPolicy
from app.services.expense_claim_attachment_presentation import ExpenseClaimAttachmentPresentation
from app.services.expense_claim_attachment_storage import ExpenseClaimAttachmentStorage
from app.services.expense_claim_application_handoff import ExpenseClaimApplicationHandoffMixin
from app.services.expense_claim_attachment_analysis import ExpenseClaimAttachmentAnalysisMixin
from app.services.expense_claim_attachment_document import ExpenseClaimAttachmentDocumentMixin
from app.services.expense_claim_attachment_operations import ExpenseClaimAttachmentOperationsMixin
@@ -124,6 +126,7 @@ from app.services.ocr import OcrService
class ExpenseClaimService(
ExpenseClaimApplicationHandoffMixin,
ExpenseClaimAttachmentOperationsMixin,
ExpenseClaimReviewPreviewMixin,
ExpenseClaimDraftFlowMixin,
@@ -153,7 +156,7 @@ class ExpenseClaimService(
or ""
).strip().lower()
return (
claim_no.startswith("APP-")
is_application_claim_no(claim_no)
or expense_type == "application"
or expense_type.endswith("_application")
or document_type in {"application", "expense_application"}
@@ -492,9 +495,28 @@ class ExpenseClaimService(
def delete_claim(self, claim_id: str, current_user: CurrentUserContext) -> ExpenseClaim | None:
claim = self.get_claim(claim_id, current_user)
if claim is None and (
current_user.is_admin or self._access_policy.has_archive_center_access(current_user)
):
candidate_claim = self.db.scalar(
select(ExpenseClaim)
.options(
selectinload(ExpenseClaim.items),
selectinload(ExpenseClaim.employee).selectinload(Employee.manager),
selectinload(ExpenseClaim.employee).selectinload(Employee.roles),
)
.where(ExpenseClaim.id == claim_id)
)
if candidate_claim is not None and (
current_user.is_admin or self._access_policy.is_archived_claim(candidate_claim)
):
claim = candidate_claim
if claim is None:
return None
if self._access_policy.is_archived_claim(claim) and not current_user.is_admin:
raise ValueError("已归档单据不能删除,只有高级管理员可以执行删除。")
if not self._access_policy.has_claim_delete_access(current_user):
self._ensure_draft_claim(claim)
if not self._access_policy.is_claim_owned_by_current_user(claim, current_user):
@@ -642,7 +664,7 @@ class ExpenseClaimService(
label = "领导审批通过"
next_status = "approved"
next_stage = "审批完成"
default_message = "{operator}审批通过,申请流程完成。"
default_message = "{operator}确认审核,申请流程完成并生成报销草稿"
else:
event_type = "expense_claim_approval"
label = "领导审批通过"
@@ -663,9 +685,12 @@ class ExpenseClaimService(
else:
raise ValueError("当前节点不支持审批通过。")
approval_opinion = str(opinion or "").strip()
if previous_stage == "直属领导审批" and not approval_opinion:
raise ValueError("领导审核意见不能为空,请填写意见后再确认审核。")
before_json = self._serialize_claim(claim)
operator = self._access_policy.resolve_current_user_display_name(current_user)
approval_opinion = str(opinion or "").strip()
approval_flag = {
"source": approval_source,
"event_type": event_type,
@@ -692,6 +717,12 @@ class ExpenseClaimService(
claim.approval_stage = next_stage
if claim.submitted_at is None:
claim.submitted_at = datetime.now(UTC)
if is_application_claim and previous_stage == "直属领导审批":
generated_draft = self._create_reimbursement_draft_from_application(
application_claim=claim,
approval_flag=approval_flag,
operator=operator,
)
claim.risk_flags_json = [*list(claim.risk_flags_json or []), approval_flag]
self.db.commit()