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
@@ -0,0 +1,66 @@
|
||||
"""计提公式模板数据访问。
|
||||
|
||||
本层只 flush 不 commit —— 事务边界由 services 层控制。
|
||||
"""
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models import BusinessRole, CalcTemplateDefinition
|
||||
|
||||
|
||||
def get_by_id(session: Session, template_id: int, *, for_update: bool = False) -> CalcTemplateDefinition | None:
|
||||
"""按主键查询;删除校验走行锁。"""
|
||||
if for_update:
|
||||
stmt = select(CalcTemplateDefinition).where(CalcTemplateDefinition.id == template_id).with_for_update()
|
||||
return session.execute(stmt).scalar_one_or_none()
|
||||
return session.get(CalcTemplateDefinition, template_id)
|
||||
|
||||
|
||||
def get_by_code(session: Session, template_code: str) -> CalcTemplateDefinition | None:
|
||||
"""按编码查询模板。编码是岗位绑定与参数解析的锚点。"""
|
||||
stmt = select(CalcTemplateDefinition).where(CalcTemplateDefinition.template_code == template_code)
|
||||
return session.execute(stmt).scalar_one_or_none()
|
||||
|
||||
|
||||
def list_all(session: Session) -> list[CalcTemplateDefinition]:
|
||||
"""全部模板(含停用,供管理列表展示引用状态)。"""
|
||||
stmt = select(CalcTemplateDefinition).order_by(
|
||||
CalcTemplateDefinition.sort_order, CalcTemplateDefinition.id
|
||||
)
|
||||
return list(session.execute(stmt).scalars())
|
||||
|
||||
|
||||
def list_active(session: Session) -> list[CalcTemplateDefinition]:
|
||||
"""启用模板,供岗位表单下拉。"""
|
||||
stmt = (
|
||||
select(CalcTemplateDefinition)
|
||||
.where(CalcTemplateDefinition.status == 1)
|
||||
.order_by(CalcTemplateDefinition.sort_order, CalcTemplateDefinition.id)
|
||||
)
|
||||
return list(session.execute(stmt).scalars())
|
||||
|
||||
|
||||
def count_roles_by_codes(session: Session, codes: list[str]) -> dict[str, int]:
|
||||
"""批量统计各模板被岗位引用的数量,用于删除保护与列表展示。"""
|
||||
if not codes:
|
||||
return {}
|
||||
rows = session.execute(
|
||||
select(BusinessRole.calc_template, func.count(BusinessRole.id))
|
||||
.where(BusinessRole.calc_template.in_(codes))
|
||||
.group_by(BusinessRole.calc_template)
|
||||
).all()
|
||||
return {code: int(count) for code, count in rows}
|
||||
|
||||
|
||||
def create(session: Session, template: CalcTemplateDefinition) -> CalcTemplateDefinition:
|
||||
"""新增模板。只 flush 以取得自增 id。"""
|
||||
session.add(template)
|
||||
session.flush()
|
||||
return template
|
||||
|
||||
|
||||
def delete(session: Session, template: CalcTemplateDefinition) -> None:
|
||||
"""删除模板。调用方须先完成保护规则校验。"""
|
||||
session.delete(template)
|
||||
session.flush()
|
||||
@@ -0,0 +1,59 @@
|
||||
"""监控查询只使用固定字段与有界窗口;事务由服务层负责。"""
|
||||
|
||||
from sqlalchemy import func, select
|
||||
|
||||
from app.models.monitor import MonitorEvent, MonitorRequestMinute, MonitorSnapshot
|
||||
|
||||
|
||||
def snapshots(session, start, end):
|
||||
return session.scalars(
|
||||
select(MonitorSnapshot)
|
||||
.where(MonitorSnapshot.minute >= start, MonitorSnapshot.minute < end)
|
||||
.order_by(MonitorSnapshot.minute)
|
||||
.limit(10082)
|
||||
).all()
|
||||
|
||||
|
||||
def request_minutes(session, start, end, route=None):
|
||||
statement = select(MonitorRequestMinute).where(
|
||||
MonitorRequestMinute.minute >= start, MonitorRequestMinute.minute < end
|
||||
)
|
||||
if route:
|
||||
statement = statement.where(MonitorRequestMinute.route == route)
|
||||
return session.scalars(
|
||||
statement.order_by(MonitorRequestMinute.minute.desc()).limit(20001)
|
||||
).all()
|
||||
|
||||
|
||||
def events(session, query, start, end):
|
||||
filters = [MonitorEvent.last_seen >= start, MonitorEvent.first_seen < end]
|
||||
if query.kind:
|
||||
filters.append(MonitorEvent.kind == query.kind)
|
||||
if query.request_id:
|
||||
filters.append(MonitorEvent.request_id == query.request_id)
|
||||
if query.route:
|
||||
filters.append(MonitorEvent.route == query.route)
|
||||
if query.active_only:
|
||||
filters.extend([MonitorEvent.kind == "HEALTH", MonitorEvent.resolved_at.is_(None)])
|
||||
total = session.scalar(select(func.count()).select_from(MonitorEvent).where(*filters)) or 0
|
||||
rows = session.scalars(
|
||||
select(MonitorEvent)
|
||||
.where(*filters)
|
||||
.order_by(MonitorEvent.last_seen.desc(), MonitorEvent.id.desc())
|
||||
.offset(query.offset)
|
||||
.limit(query.page_size)
|
||||
).all()
|
||||
return rows, total
|
||||
|
||||
|
||||
def active_events(session):
|
||||
return session.scalars(
|
||||
select(MonitorEvent)
|
||||
.where(MonitorEvent.kind == "HEALTH", MonitorEvent.resolved_at.is_(None))
|
||||
.order_by(MonitorEvent.last_seen.desc())
|
||||
.limit(100)
|
||||
).all()
|
||||
|
||||
|
||||
def event(session, event_id):
|
||||
return session.get(MonitorEvent, event_id)
|
||||
@@ -1,6 +1,6 @@
|
||||
"""T+ 管理库访问;只 flush,事务由 service 管理。"""
|
||||
|
||||
from sqlalchemy import func, select, update
|
||||
from sqlalchemy import case, func, select, update
|
||||
from sqlalchemy.orm import Session, defer
|
||||
|
||||
from app.models import TPlusSyncBatch, TPlusSyncError
|
||||
@@ -129,6 +129,24 @@ def status_counts(session: Session) -> dict[str, int]:
|
||||
)
|
||||
|
||||
|
||||
def progress_reports(session: Session, numbers):
|
||||
if not numbers:
|
||||
return {}
|
||||
rows = session.execute(select(TPlusSyncBatch.batch_no,
|
||||
TPlusSyncBatch.audit_result_json["progress"],
|
||||
TPlusSyncBatch.audit_result_json["observed_entities"],
|
||||
).where(TPlusSyncBatch.batch_no.in_(numbers)))
|
||||
return {number: {"progress": progress, "observed_entities": observed or []}
|
||||
for number, progress, observed in rows}
|
||||
|
||||
|
||||
def current_batch(session: Session):
|
||||
return session.scalar(select(TPlusSyncBatch).order_by(
|
||||
case((TPlusSyncBatch.status == "RUNNING", 0), (TPlusSyncBatch.status == "QUEUED", 1), else_=2),
|
||||
TPlusSyncBatch.id.desc(),
|
||||
).limit(1))
|
||||
|
||||
|
||||
def latest_audit(session: Session):
|
||||
"""最近完成的核验;返回批次供查询层过滤报告。"""
|
||||
return session.scalar(
|
||||
|
||||
@@ -37,7 +37,7 @@ def get_role_codes(user: User) -> list[str]:
|
||||
依赖 `User.user_roles` 的 `lazy="selectin"` 预加载(见模型定义),
|
||||
故此处遍历不会触发 N+1 查询。
|
||||
"""
|
||||
return [link.role.role_code for link in user.user_roles if link.role is not None]
|
||||
return [link.role.role_code for link in user.user_roles if link.role is not None and link.role.status == 1]
|
||||
|
||||
|
||||
def touch_last_login(session: Session, user: User, moment: datetime) -> None:
|
||||
|
||||
Reference in new issue
Block a user