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

+21 -18
View File
@@ -559,7 +559,11 @@ def _personal_rate(
return _RateDecision(rate, "企业户:不按回款天数,统一按固定比例计提", None, False)
template = role.calc_template if role else "TPL_SALESPERSON_DECAY"
if template == "TPL_MANAGER_SHARE":
from app.services.calc_templates import resolve_definition
definition = resolve_definition(session, template)
params = definition.params
if definition.formula_type == "manager_share":
# #12 答复(2026-10-19)参数化:manager.personal.included=1 时个人户
# 单按业务员递减公式单笔计提(个人户单本质是正常业务,有发货/回款/
# 毛利;聚合基数开关在 settlement_service 同参数消费)——0 时单笔落 0
@@ -570,35 +574,33 @@ def _personal_rate(
None,
True,
)
start = parse_fraction(snapshot.get("salesperson.rate.start"))
slope = parse_fraction(snapshot.get("salesperson.rate.slope"))
floor = parse_fraction(snapshot.get("salesperson.rate.floor"))
boundary = int(parse_fraction(snapshot.get("salesperson.days.boundary")))
decay = resolve_definition(session, "TPL_SALESPERSON_DECAY").params
start = parse_fraction(snapshot.get(decay.get("start", "salesperson.rate.start")))
slope = parse_fraction(snapshot.get(decay.get("slope", "salesperson.rate.slope")))
floor = parse_fraction(snapshot.get(decay.get("floor", "salesperson.rate.floor")))
boundary = int(parse_fraction(snapshot.get(decay.get("boundary", "salesperson.days.boundary"))))
rate = salesperson_rate(days, start=start, slope=slope, floor=floor, boundary_days=boundary)
note = f"经理个人户(已开启计入分成基数):按业务员递减,回款 {days} 天,起始 {start}‰"
return _RateDecision(rate, note, None, False)
if template == "TPL_INFO_SALES_FIXED":
if definition.formula_type == "fixed_sales":
# 信息员基数=本人销售额(覆盖折算与阈值),‰ 换算同防线
rate = parse_fraction(snapshot.get("info.rate")) / 1000
return _RateDecision(rate, "信息员:按本人销售额乘计提比例,不随回款天数递减", None, False)
if template == "TPL_SALESPERSON_DECAY":
start = parse_fraction(snapshot.get("salesperson.rate.start"))
slope = parse_fraction(snapshot.get("salesperson.rate.slope"))
floor = parse_fraction(snapshot.get("salesperson.rate.floor"))
rate = parse_fraction(snapshot.get(params.get("rate", "info.rate"))) / 1000
return _RateDecision(rate, "按岗位固定比例计提,不随回款天数递减", None, False)
if definition.formula_type == "linear_decay":
start = parse_fraction(snapshot.get(params.get("start", "salesperson.rate.start")))
slope = parse_fraction(snapshot.get(params.get("slope", "salesperson.rate.slope")))
floor = parse_fraction(snapshot.get(params.get("floor", "salesperson.rate.floor")))
# int(参数原文) 对合法入库的 '360.0' 会抛 ValueError 使整月核算 500;
# 经 Fraction 转换再取整(INT 型参数入库时已校验分母为 1,取整无损)
boundary = int(parse_fraction(snapshot.get("salesperson.days.boundary")))
boundary = int(parse_fraction(snapshot.get(params.get("boundary", "salesperson.days.boundary"))))
rate = salesperson_rate(days, start=start, slope=slope, floor=floor, boundary_days=boundary)
note = (
f"回款 {days} 天,从起始 {start}‰ 按日递减"
f"(满 {boundary} 天后固定按 {floor}‰ 计算)"
)
return _RateDecision(rate, note, None, False)
# 提示给模板中文名而非编码:业务人员在岗位角色页看到的是「营销经理·团队与全公司分成」
from app.services.calc_templates import TEMPLATES
tpl_name = TEMPLATES[template].name if template in TEMPLATES else template
raise BadRequestError(f"「{tpl_name}」不支持单笔核算,请检查该人员的岗位设置")
# 提示给模板中文名而非编码:业务人员在岗位角色页看到的是配置名称
raise BadRequestError(f"「{definition.template_name}」不支持单笔核算,请检查该人员的岗位设置")
def _role_of(session: Session, person):
@@ -697,6 +699,7 @@ def batch_calc(
}
)
except Exception as exc:
logger.exception("批量核算单笔失败 | receipt_id=%s", rid)
results.append(
{"receipt_id": rid, "ok": False, "error": f"{type(exc).__name__}: {exc}"}
)