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
@@ -1,22 +1,140 @@
|
||||
"""计提公式模板注册表(代码内有限集合)。
|
||||
"""计提公式模板骨架注册与解析。
|
||||
|
||||
一期不引入规则引擎的落地方式:公式骨架固定(本表枚举),变化的是参数值
|
||||
(系统设置维护)。岗位角色(business_roles.calc_template)绑定模板编码,
|
||||
新增岗位 = 选模板 + 配参数,无需改代码(需求 #28)。
|
||||
新增公式形态(如未来「按产品数量计提」)需要新增模板 —— 一期接受,
|
||||
岗位表单中模板为带说明的受控下拉,避免误以为可自由写公式。
|
||||
一期骨架仍是代码内受控集合:页面可维护模板名称、说明、参数绑定(JSON),
|
||||
但不允许自由表达式,避免注入与算错。数据库定义优先;迁移前的旧库用
|
||||
`SYSTEM_TEMPLATES` 兜底解析,保证读写与历史行为一致。
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class TemplateSpec:
|
||||
"""公式模板元数据。"""
|
||||
class FormulaSpec:
|
||||
"""公式骨架元数据:参数角色 → 模板参数映射键。"""
|
||||
|
||||
code: str
|
||||
name: str
|
||||
params: tuple[str, ...]
|
||||
roles: tuple[str, ...]
|
||||
description: str
|
||||
|
||||
|
||||
# 骨架类型(formula_type)。新增骨架需同步执行引擎分派点。
|
||||
FORMULAS: dict[str, FormulaSpec] = {
|
||||
"linear_decay": FormulaSpec(
|
||||
code="linear_decay",
|
||||
name="天数线性递减",
|
||||
roles=("start", "slope", "floor", "boundary"),
|
||||
description="按回款天数线性递减,超过截止天数取下限比例",
|
||||
),
|
||||
"fixed_profit": FormulaSpec(
|
||||
code="fixed_profit",
|
||||
name="折算毛利固定比例",
|
||||
roles=("rate",),
|
||||
description="折算毛利润额 × 固定比例",
|
||||
),
|
||||
"fixed_sales": FormulaSpec(
|
||||
code="fixed_sales",
|
||||
name="销售额固定比例",
|
||||
roles=("rate",),
|
||||
description="本人销售额 × 固定比例",
|
||||
),
|
||||
"manager_share": FormulaSpec(
|
||||
code="manager_share",
|
||||
name="经理团队分成",
|
||||
roles=("team", "company"),
|
||||
description="本团队绩效 × 团队比例 + 全公司绩效 × 公司比例",
|
||||
),
|
||||
"prepay_fixed": FormulaSpec(
|
||||
code="prepay_fixed",
|
||||
name="预付款固定比例",
|
||||
roles=("rate",),
|
||||
description="预付回款额 × 固定比例",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class TemplateDefinition:
|
||||
"""模板定义解析结果:数据库行或旧库兜底系统模板。"""
|
||||
|
||||
template_code: str
|
||||
template_name: str
|
||||
formula_type: str
|
||||
params: dict[str, str]
|
||||
description: str | None
|
||||
is_system: bool
|
||||
status: int
|
||||
|
||||
|
||||
def get_formula(code: str) -> FormulaSpec:
|
||||
"""取骨架元数据;未知编码属配置错误,调用方转用户错误。"""
|
||||
spec = FORMULAS.get(code)
|
||||
if spec is None:
|
||||
raise ValueError(f"公式骨架未注册:{code}")
|
||||
return spec
|
||||
|
||||
|
||||
def system_definition(code: str) -> TemplateDefinition | None:
|
||||
"""旧库兜底:迁移未应用时按内置注册表合成定义,行为与历史一致。"""
|
||||
spec = TEMPLATES.get(code)
|
||||
if spec is None:
|
||||
return None
|
||||
return TemplateDefinition(
|
||||
template_code=spec.code,
|
||||
template_name=spec.name,
|
||||
formula_type=spec.formula_type,
|
||||
params=dict(spec.params),
|
||||
description=spec.description,
|
||||
is_system=True,
|
||||
status=1,
|
||||
)
|
||||
|
||||
|
||||
def resolve_definition(session: Session, code: str) -> TemplateDefinition:
|
||||
"""按编码解析模板定义:数据库优先,旧库兜底系统模板。"""
|
||||
from app.repositories import calc_template_repository
|
||||
|
||||
row = calc_template_repository.get_by_code(session, code)
|
||||
if row is not None:
|
||||
return TemplateDefinition(
|
||||
template_code=row.template_code,
|
||||
template_name=row.template_name,
|
||||
formula_type=row.formula_type,
|
||||
params=dict(row.params_json or {}),
|
||||
description=row.description,
|
||||
is_system=row.is_system == 1,
|
||||
status=row.status,
|
||||
)
|
||||
fallback = system_definition(code)
|
||||
if fallback is None:
|
||||
raise ValueError(f"计提公式模板未定义:{code}")
|
||||
return fallback
|
||||
|
||||
|
||||
def formula_type_of(session: Session, code: str) -> str:
|
||||
"""执行分派用:模板 → 公式骨架类型。"""
|
||||
return resolve_definition(session, code).formula_type
|
||||
|
||||
|
||||
# 模板参数角色到参数编码的内置映射(与 V28 种子一致;旧库兜底用)。
|
||||
_TPL_SALESPERSON = {
|
||||
"start": "salesperson.rate.start",
|
||||
"slope": "salesperson.rate.slope",
|
||||
"floor": "salesperson.rate.floor",
|
||||
"boundary": "salesperson.days.boundary",
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class TemplateSpec:
|
||||
"""旧库兜底模板元数据(历史编码锚点)。"""
|
||||
|
||||
code: str
|
||||
name: str
|
||||
formula_type: str
|
||||
params: dict[str, str]
|
||||
description: str
|
||||
|
||||
|
||||
@@ -24,49 +142,36 @@ TEMPLATES: dict[str, TemplateSpec] = {
|
||||
"TPL_SALESPERSON_DECAY": TemplateSpec(
|
||||
code="TPL_SALESPERSON_DECAY",
|
||||
name="业务员个人户·天数线性递减",
|
||||
params=(
|
||||
"salesperson.rate.start",
|
||||
"salesperson.rate.slope",
|
||||
"salesperson.rate.floor",
|
||||
"salesperson.days.boundary",
|
||||
),
|
||||
formula_type="linear_decay",
|
||||
params=_TPL_SALESPERSON,
|
||||
description="分段:≤分界天数 = 起始 − 天数×斜率;>分界天数 = 下限",
|
||||
),
|
||||
"TPL_MANAGER_SHARE": TemplateSpec(
|
||||
code="TPL_MANAGER_SHARE",
|
||||
name="营销经理·团队与全公司分成",
|
||||
params=("manager.share.team", "manager.share.company"),
|
||||
description="聚合:∑本团队业务员绩效×12.5% + ∑全公司业务员绩效×5%",
|
||||
formula_type="manager_share",
|
||||
params={"team": "manager.share.team", "company": "manager.share.company"},
|
||||
description="聚合:本团队业务员绩效×团队比例 + 全公司业务员绩效×公司比例",
|
||||
),
|
||||
"TPL_ENTERPRISE_FIXED": TemplateSpec(
|
||||
code="TPL_ENTERPRISE_FIXED",
|
||||
name="企业户·固定计提",
|
||||
params=("enterprise.rate",),
|
||||
formula_type="fixed_profit",
|
||||
params={"rate": "enterprise.rate"},
|
||||
description="乘法:折算毛利润额 × 计提比例",
|
||||
),
|
||||
"TPL_INFO_SALES_FIXED": TemplateSpec(
|
||||
code="TPL_INFO_SALES_FIXED",
|
||||
name="信息员·销售额固定比例",
|
||||
params=("info.rate",),
|
||||
formula_type="fixed_sales",
|
||||
params={"rate": "info.rate"},
|
||||
description="乘法:本人销售额 × 计提比例",
|
||||
),
|
||||
"TPL_PREPAY_FIXED": TemplateSpec(
|
||||
code="TPL_PREPAY_FIXED",
|
||||
name="预付款·固定计提",
|
||||
params=("prepay.rate",),
|
||||
description="乘法:回款额 × 计提比例",
|
||||
formula_type="prepay_fixed",
|
||||
params={"rate": "prepay.rate"},
|
||||
description="乘法:预付回款额 × 计提比例",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def get_template(code: str) -> TemplateSpec:
|
||||
"""取模板元数据。未注册编码属代码缺陷(500),不是用户错误。"""
|
||||
tpl = TEMPLATES.get(code)
|
||||
if tpl is None:
|
||||
raise ValueError(f"公式模板编码未注册:{code}")
|
||||
return tpl
|
||||
|
||||
|
||||
def required_params(code: str) -> tuple[str, ...]:
|
||||
"""模板声明的参数编码列表(岗位表单展示与校验用)。"""
|
||||
return get_template(code).params
|
||||
Reference in new issue
Block a user