feat: 新增预算后端服务与差旅风险规则库

后端新增预算模型、端点和服务模块,支持预算 CRUD 和余额
查询,清理旧生成规则文件并替换为按严重等级分类的差旅风
险规则库,优化认证权限和报销单访问策略,新增财务规则目
录和演示数据构建脚本,前端预算中心增加对话框交互,完善
审计页面运行时模型和元数据展示,补充单元测试。
This commit is contained in:
caoxiaozhu
2026-05-26 17:29:35 +08:00
parent e1e515ecae
commit e7bef0883d
85 changed files with 6443 additions and 1497 deletions

View File

@@ -1,30 +1,31 @@
from collections.abc import Generator
from dataclasses import dataclass
from typing import Annotated
from fastapi import Depends, Header, HTTPException, status
from sqlalchemy.orm import Session
from app.db.session import get_session_factory
def get_db() -> Generator[Session, None, None]:
db = get_session_factory()()
try:
yield db
finally:
db.close()
@dataclass(slots=True)
from collections.abc import Generator
from dataclasses import dataclass
from typing import Annotated
from fastapi import Depends, Header, HTTPException, status
from sqlalchemy.orm import Session
from app.db.session import get_session_factory
def get_db() -> Generator[Session, None, None]:
db = get_session_factory()()
try:
yield db
finally:
db.close()
@dataclass(slots=True)
class CurrentUserContext:
username: str
name: str
role_codes: list[str]
is_admin: bool
department_name: str = ""
cost_center: str = ""
def get_current_user(
x_auth_username: Annotated[
str | None,
@@ -46,34 +47,75 @@ def get_current_user(
str | None,
Header(description="当前登录人的所属部门。"),
] = None,
x_auth_cost_center: Annotated[
str | None,
Header(description="当前登录人的成本中心。"),
] = None,
) -> CurrentUserContext:
role_codes = [item.strip() for item in (x_auth_role_codes or "").split(",") if item.strip()]
is_admin = str(x_auth_is_admin or "").strip().lower() in {"1", "true", "yes", "on"}
username = (x_auth_username or "").strip()
name = (x_auth_name or username).strip()
if not username and not name:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="请先登录后再访问知识库。",
)
return CurrentUserContext(
username=username or name,
role_codes = [
_normalize_role_code(item)
for item in (x_auth_role_codes or "").split(",")
if _normalize_role_code(item)
]
username = (x_auth_username or "").strip()
name = (x_auth_name or username).strip()
is_admin = _resolve_platform_admin_flag(
username=username,
name=name,
role_codes=role_codes,
header_value=x_auth_is_admin,
)
if not username and not name:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="请先登录后再访问知识库。",
)
return CurrentUserContext(
username=username or name,
name=name or username,
role_codes=role_codes,
is_admin=is_admin,
department_name=(x_auth_department or "").strip(),
cost_center=(x_auth_cost_center or "").strip(),
)
def _normalize_role_code(value: str | None) -> str:
role_code = str(value or "").strip().lower()
if role_code == "auditor":
return "budget_monitor"
return role_code
def _current_user_role_codes(current_user: CurrentUserContext) -> set[str]:
return {_normalize_role_code(item) for item in current_user.role_codes if _normalize_role_code(item)}
def _resolve_platform_admin_flag(
*,
username: str,
name: str,
role_codes: list[str],
header_value: str | None,
) -> bool:
if str(header_value or "").strip().lower() in {"1", "true", "yes", "on"}:
return True
identities = {
str(username or "").strip().lower(),
str(name or "").strip().lower(),
}
return "admin" in identities or "admin" in {_normalize_role_code(item) for item in role_codes}
def require_admin_user(
current_user: Annotated[CurrentUserContext, Depends(get_current_user)],
) -> CurrentUserContext:
if current_user.is_admin or "manager" in current_user.role_codes:
if current_user.is_admin or "manager" in _current_user_role_codes(current_user):
return current_user
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="只有管理员可以上传、删除或修改知识库文件。",
@@ -95,24 +137,58 @@ def require_platform_admin_user(
def require_rule_editor_user(
current_user: Annotated[CurrentUserContext, Depends(get_current_user)],
) -> CurrentUserContext:
role_codes = {item.strip() for item in current_user.role_codes}
role_codes = _current_user_role_codes(current_user)
if current_user.is_admin or "manager" in role_codes or "finance" in role_codes:
return current_user
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="只有财务人员或高级管理人员可以编辑规则草稿。",
detail="只有财务人员或高级财务人员可以编辑规则草稿。",
)
def require_rule_reviewer_user(
current_user: Annotated[CurrentUserContext, Depends(get_current_user)],
) -> CurrentUserContext:
role_codes = {item.strip() for item in current_user.role_codes}
role_codes = _current_user_role_codes(current_user)
if current_user.is_admin or "manager" in role_codes:
return current_user
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="只有高级管理人员或 admin 管理员可以执行该操作。",
detail="只有高级财务人员或 admin 管理员可以执行该操作。",
)
def require_budget_viewer_user(
current_user: Annotated[CurrentUserContext, Depends(get_current_user)],
) -> CurrentUserContext:
role_codes = _current_user_role_codes(current_user)
if current_user.is_admin or role_codes & {"budget_monitor", "executive"}:
return current_user
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="只有预算监控员或高级财务人员可以查看预算中心。",
)
def require_budget_editor_user(
current_user: Annotated[CurrentUserContext, Depends(get_current_user)],
) -> CurrentUserContext:
role_codes = _current_user_role_codes(current_user)
if current_user.is_admin or "executive" in role_codes:
return current_user
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="只有 admin 管理员或高级财务人员可以维护预算额度。",
)
def is_budget_scope_limited_user(current_user: CurrentUserContext) -> bool:
if current_user.is_admin:
return False
role_codes = _current_user_role_codes(current_user)
return "budget_monitor" in role_codes and "executive" not in role_codes