feat(后端): 添加授权体系、系统监控与试用期计提

按 2026-11-02 需求补齐计划交付后端能力:
- 角色授权:权限目录化,接口统一准入校验,未登记默认拒绝;
  管理员判断改为按数据库角色重新判定,防止权限码持有者绕过
- 系统监控:单进程采集资源、请求与异常指标,分钟级聚合,
  阈值配置化;敏感配置增加 repr 保护,Redis 密码 URL 转义
- 试用期计提:按回款日判定并写入快照,年度结清与调整单联动;
  人员模型补试用期、离职日期与工资版本历史字段
- 计提模板:公式骨架受控,页面维护名称与参数绑定
- T+ 采集:自动化配置表与进度查询

内置角色禁停用保护对齐模块注释;核算规则版本提为常量并
补充批量核算失败堆栈日志。

影响范围:后端全部模块、数据库迁移 V22-V28
验证:uv run ruff check 通过;922 个单元测试全部通过;
集成测试因本机 MySQL 数据卷被其他容器锁定暂未运行
This commit is contained in:
liuwenxin committed 2026-11-03 11:20:55 +08:00
1 parent 02fb06e0af
commit 1461a8d113
149 files changed
+8736 -368

No files matched your search

+54 -16
View File
@@ -6,8 +6,7 @@
- **待办与预警实时派生**:不建待办/预警表、不引调度器;处置动作本身
已有操作日志可追溯。与 FR-035「预警产生/消除留痕」的差距记录在
计划开放项,待业务确认是否物化
- **⑥ 同步模块未启用**:同步失败待办、同步连续失败预警以
``sync_available=false`` 显式降级,不返回伪造的 0 条
- 同步记录与采集开关分别展示;读取关闭不抹掉已完成的采集记录
- 预警阈值走参数体系(``warning.*``),一次请求只解析一次快照
- **最近动态独立分页**(2026-10-23):操作日志可能很长,聚合响应里
只能塞前 20 条;拆成独立端点后翻页只重取这张列表
@@ -16,11 +15,14 @@
from datetime import date, datetime, timedelta
from decimal import Decimal
from sqlalchemy import func, inspect, select
from sqlalchemy.orm import Session
from app.core.config import get_settings
from app.core.exceptions import BadRequestError
from app.core.month import validate_month
from app.repositories import dashboard_repository, operation_log_repository, user_repository
from app.models import TPlusSyncBatch
from app.repositories import dashboard_repository, operation_log_repository
from app.repositories.query_utils import LogFilters
from app.schemas.dashboard import ActivityItem, ActivityPageOut, ActivityQuery
from app.services import approval_service
@@ -42,17 +44,54 @@ def get_dashboard(session: Session, month: str | None = None, *, current_user) -
"""
resolved = _normalize_month(month)
warning_params = _warning_params(session)
pending_statuses = approval_service.pending_statuses_for_roles(
user_repository.get_role_codes(current_user)
)
from app.services.authorization_service import calculate
permissions = calculate(session, current_user)["permissions"]
pending_statuses = approval_service.pending_statuses_for_roles([
role for role in ("finance", "leader") if "approvals:" + role in permissions
])
sync = _sync_status(session)
return {
"month": resolved,
"overview": _overview(session, resolved),
"todos": _todos(session, pending_statuses),
"warnings": _warnings(session, warning_params),
"todos": _todos(session, pending_statuses, sync),
"warnings": _warnings(session, warning_params, sync),
"sync": sync,
}
def _sync_status(session: Session) -> dict:
"""只读本地批次元数据,不连接源库,兼容尚未应用自动化迁移的开发库。"""
settings = get_settings()
result = {
"available": False,
"enabled": settings.t_plus_enabled,
"automation_enabled": settings.t_plus_automation_enabled,
"latest_status": None,
"last_success_at": None,
"historical_failure_total": 0,
}
if not inspect(session.connection()).has_table(TPlusSyncBatch.__tablename__):
return result
# 关联检查成功不能替代数据采集成功;仅查询原有列,避免加载 V20 新字段。
reads = TPlusSyncBatch.sync_mode.in_(("FULL", "INCREMENTAL"))
result["available"] = True
result["latest_status"] = session.scalar(
select(TPlusSyncBatch.status).where(reads).order_by(TPlusSyncBatch.id.desc()).limit(1)
)
result["last_success_at"] = session.scalar(
select(func.max(TPlusSyncBatch.finished_at)).where(
reads, TPlusSyncBatch.status == "SUCCEEDED"
)
)
result["historical_failure_total"] = session.scalar(
select(func.count()).select_from(TPlusSyncBatch).where(
reads, TPlusSyncBatch.status.in_(("FAILED", "PARTIAL"))
)
) or 0
return result
def _normalize_month(month: str | None) -> str:
"""未传月份按当月;非法月份在入口拦成 400(避免 500)。"""
if not month:
@@ -120,7 +159,7 @@ def _overview(session: Session, month: str) -> dict:
}
def _todos(session: Session, pending_statuses: tuple[int, ...]) -> dict:
def _todos(session: Session, pending_statuses: tuple[int, ...], sync: dict) -> dict:
"""待办事项(FR-034,澄清 #11 起步范围)。
``pending_statuses`` 已按当前用户角色解析:财务只看待财务复核、
@@ -156,9 +195,8 @@ def _todos(session: Session, pending_statuses: tuple[int, ...]) -> dict:
}
for row in receipts
],
# ⑥ T+ 同步未建:显式降级,不伪造「无失败」
"sync_available": False,
"sync_failure_total": 0,
"sync_available": sync["available"],
"sync_failure_total": sync["historical_failure_total"],
}
@@ -174,8 +212,8 @@ def _warning_params(session: Session) -> dict:
}
def _warnings(session: Session, params: dict) -> dict:
"""风险预警(FR-035):首批两条可算规则;同步规则待 ⑥。"""
def _warnings(session: Session, params: dict, sync: dict) -> dict:
"""风险预警(FR-035);采集状态与经营预警开关分别展示。"""
if not params["enabled"]:
return {
"enabled": False,
@@ -185,7 +223,7 @@ def _warnings(session: Session, params: dict) -> dict:
"total": 0,
"receipt_overdue_total": 0,
"approval_timeout_total": 0,
"sync_available": False,
"sync_available": sync["available"],
}
receipt_cutoff = date.today() - timedelta(days=params["receipt_overdue_days"])
@@ -245,7 +283,7 @@ def _warnings(session: Session, params: dict) -> dict:
"total": receipt_overdue_total + approval_timeout_total,
"receipt_overdue_total": receipt_overdue_total,
"approval_timeout_total": approval_timeout_total,
"sync_available": False,
"sync_available": sync["available"],
}