feat(后端): 添加授权体系、系统监控与试用期计提
按 2026-11-02 需求补齐计划交付后端能力: - 角色授权:权限目录化,接口统一准入校验,未登记默认拒绝; 管理员判断改为按数据库角色重新判定,防止权限码持有者绕过 - 系统监控:单进程采集资源、请求与异常指标,分钟级聚合, 阈值配置化;敏感配置增加 repr 保护,Redis 密码 URL 转义 - 试用期计提:按回款日判定并写入快照,年度结清与调整单联动; 人员模型补试用期、离职日期与工资版本历史字段 - 计提模板:公式骨架受控,页面维护名称与参数绑定 - T+ 采集:自动化配置表与进度查询 内置角色禁停用保护对齐模块注释;核算规则版本提为常量并 补充批量核算失败堆栈日志。 影响范围:后端全部模块、数据库迁移 V22-V28 验证:uv run ruff check 通过;922 个单元测试全部通过; 集成测试因本机 MySQL 数据卷被其他容器锁定暂未运行
This commit is contained in:
1 parent
02fb06e0af
commit
1461a8d113
149 files changed
+8736
-368
No files matched your search
@@ -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"],
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user