196 lines
6.8 KiB
Python
196 lines
6.8 KiB
Python
|
|
"""计提公式模板管理服务。
|
|||
|
|
|
|||
|
|
页面可维护:名称、说明、参数绑定(受骨架角色约束);公式骨架是受控枚举,
|
|||
|
|
创建后锁定,避免自由表达式注入与执行错位。系统模板随迁移种子初始化,
|
|||
|
|
禁删且骨架/编码/参数锁定;自定义模板无岗位引用才可删除。
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
from sqlalchemy.exc import IntegrityError
|
|||
|
|
|
|||
|
|
from app.core.constants import ACTION_CREATE, ACTION_DELETE, ACTION_UPDATE
|
|||
|
|
from app.core.exceptions import BadRequestError, ForbiddenError, NotFoundError
|
|||
|
|
from app.core.response import PageResult
|
|||
|
|
from app.models import CalcTemplateDefinition, User
|
|||
|
|
from app.repositories import calc_template_repository
|
|||
|
|
from app.schemas.calc_template import (
|
|||
|
|
CalcTemplateCreateIn,
|
|||
|
|
CalcTemplateOut,
|
|||
|
|
CalcTemplateUpdateIn,
|
|||
|
|
)
|
|||
|
|
from app.services import audit_service
|
|||
|
|
from app.services.audit_service import AuditRecord, RequestMeta
|
|||
|
|
from app.services.calc_templates import FORMULAS, get_formula
|
|||
|
|
from app.services.param_registry import PARAM_REGISTRY
|
|||
|
|
|
|||
|
|
MODULE_CALC_TEMPLATE = "公式模板"
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _to_out(row: CalcTemplateDefinition, role_count: int = 0) -> CalcTemplateOut:
|
|||
|
|
return CalcTemplateOut(
|
|||
|
|
id=row.id,
|
|||
|
|
template_code=row.template_code,
|
|||
|
|
template_name=row.template_name,
|
|||
|
|
formula_type=row.formula_type,
|
|||
|
|
params=dict(row.params_json or {}),
|
|||
|
|
description=row.description,
|
|||
|
|
sort_order=row.sort_order,
|
|||
|
|
status=row.status,
|
|||
|
|
is_system=row.is_system,
|
|||
|
|
role_count=role_count,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _validate_params(formula_type: str, params: dict[str, str]) -> dict[str, str]:
|
|||
|
|
"""骨架角色必须全覆盖且不引入多余角色;目标参数必须已注册。"""
|
|||
|
|
spec = get_formula(formula_type)
|
|||
|
|
expected = set(spec.roles)
|
|||
|
|
provided = set(params)
|
|||
|
|
if provided != expected:
|
|||
|
|
missing = sorted(expected - provided)
|
|||
|
|
extra = sorted(provided - expected)
|
|||
|
|
details = []
|
|||
|
|
if missing:
|
|||
|
|
details.append("缺少参数角色:" + "、".join(missing))
|
|||
|
|
if extra:
|
|||
|
|
details.append("多余参数角色:" + "、".join(extra))
|
|||
|
|
raise BadRequestError(";".join(details))
|
|||
|
|
unknown = sorted(code for code in params.values() if code not in PARAM_REGISTRY)
|
|||
|
|
if unknown:
|
|||
|
|
raise BadRequestError("参数编码未注册:" + "、".join(unknown))
|
|||
|
|
return {role: params[role] for role in spec.roles}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def list_templates(session) -> PageResult[CalcTemplateOut]:
|
|||
|
|
"""模板列表(含停用与引用计数,供管理页)。"""
|
|||
|
|
rows = calc_template_repository.list_all(session)
|
|||
|
|
counts = calc_template_repository.count_roles_by_codes(session, [r.template_code for r in rows])
|
|||
|
|
return PageResult[CalcTemplateOut](
|
|||
|
|
records=[_to_out(row, counts.get(row.template_code, 0)) for row in rows],
|
|||
|
|
total=len(rows),
|
|||
|
|
page_num=1,
|
|||
|
|
page_size=max(len(rows), 1),
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def list_formula_options() -> list[dict]:
|
|||
|
|
"""骨架下拉(受控集合)。"""
|
|||
|
|
return [
|
|||
|
|
{"code": spec.code, "name": spec.name, "description": spec.description, "roles": list(spec.roles)}
|
|||
|
|
for spec in FORMULAS.values()
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
|
|||
|
|
def create_template(
|
|||
|
|
session,
|
|||
|
|
payload: CalcTemplateCreateIn,
|
|||
|
|
*,
|
|||
|
|
operator: User,
|
|||
|
|
meta: RequestMeta,
|
|||
|
|
) -> CalcTemplateOut:
|
|||
|
|
"""新增模板。编码唯一;骨架受控;参数绑定按骨架角色校验。"""
|
|||
|
|
if calc_template_repository.get_by_code(session, payload.template_code) is not None:
|
|||
|
|
raise BadRequestError(f"模板编码 {payload.template_code} 已存在")
|
|||
|
|
try:
|
|||
|
|
get_formula(payload.formula_type)
|
|||
|
|
except ValueError as exc:
|
|||
|
|
raise BadRequestError(str(exc)) from exc
|
|||
|
|
params = _validate_params(payload.formula_type, payload.params)
|
|||
|
|
row = CalcTemplateDefinition(
|
|||
|
|
template_code=payload.template_code,
|
|||
|
|
template_name=payload.template_name,
|
|||
|
|
formula_type=payload.formula_type,
|
|||
|
|
params_json=params,
|
|||
|
|
description=payload.description,
|
|||
|
|
sort_order=payload.sort_order,
|
|||
|
|
status=1,
|
|||
|
|
is_system=0,
|
|||
|
|
)
|
|||
|
|
try:
|
|||
|
|
calc_template_repository.create(session, row)
|
|||
|
|
session.commit()
|
|||
|
|
except IntegrityError as exc:
|
|||
|
|
session.rollback()
|
|||
|
|
raise BadRequestError(f"模板编码 {payload.template_code} 已存在") from exc
|
|||
|
|
with audit_service.audit(
|
|||
|
|
session,
|
|||
|
|
operator=operator,
|
|||
|
|
record=AuditRecord(
|
|||
|
|
module=MODULE_CALC_TEMPLATE,
|
|||
|
|
action=ACTION_CREATE,
|
|||
|
|
target=payload.template_code,
|
|||
|
|
summary=f"新增计提公式模板 {payload.template_name}",
|
|||
|
|
detail={"formula_type": payload.formula_type, "params": params},
|
|||
|
|
),
|
|||
|
|
meta=meta,
|
|||
|
|
):
|
|||
|
|
pass
|
|||
|
|
return _to_out(row)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def update_template(
|
|||
|
|
session,
|
|||
|
|
template_id: int,
|
|||
|
|
payload: CalcTemplateUpdateIn,
|
|||
|
|
*,
|
|||
|
|
operator: User,
|
|||
|
|
meta: RequestMeta,
|
|||
|
|
) -> CalcTemplateOut:
|
|||
|
|
"""编辑模板。系统模板参数锁定;自定义模板参数仍按骨架角色校验。"""
|
|||
|
|
row = calc_template_repository.get_by_id(session, template_id, for_update=True)
|
|||
|
|
if row is None:
|
|||
|
|
raise NotFoundError("模板不存在")
|
|||
|
|
params = dict(row.params_json or {})
|
|||
|
|
if row.is_system != 1:
|
|||
|
|
params = _validate_params(row.formula_type, payload.params)
|
|||
|
|
row.template_name = payload.template_name
|
|||
|
|
row.description = payload.description
|
|||
|
|
row.sort_order = payload.sort_order
|
|||
|
|
row.params_json = params
|
|||
|
|
session.commit()
|
|||
|
|
with audit_service.audit(
|
|||
|
|
session,
|
|||
|
|
operator=operator,
|
|||
|
|
record=AuditRecord(
|
|||
|
|
module=MODULE_CALC_TEMPLATE,
|
|||
|
|
action=ACTION_UPDATE,
|
|||
|
|
target=row.template_code,
|
|||
|
|
summary=f"修改计提公式模板 {row.template_name}",
|
|||
|
|
detail={"params": params},
|
|||
|
|
),
|
|||
|
|
meta=meta,
|
|||
|
|
):
|
|||
|
|
pass
|
|||
|
|
return _to_out(row)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def delete_template(
|
|||
|
|
session,
|
|||
|
|
template_id: int,
|
|||
|
|
*,
|
|||
|
|
operator: User,
|
|||
|
|
meta: RequestMeta,
|
|||
|
|
) -> None:
|
|||
|
|
"""删除模板。系统模板禁删;被岗位引用禁删。"""
|
|||
|
|
row = calc_template_repository.get_by_id(session, template_id, for_update=True)
|
|||
|
|
if row is None:
|
|||
|
|
raise NotFoundError("模板不存在")
|
|||
|
|
if row.is_system == 1:
|
|||
|
|
raise ForbiddenError("系统内置模板不可删除")
|
|||
|
|
counts = calc_template_repository.count_roles_by_codes(session, [row.template_code])
|
|||
|
|
if counts.get(row.template_code):
|
|||
|
|
raise BadRequestError("模板已被岗位引用,不能删除;可先调整岗位模板后再删除")
|
|||
|
|
calc_template_repository.delete(session, row)
|
|||
|
|
session.commit()
|
|||
|
|
with audit_service.audit(
|
|||
|
|
session,
|
|||
|
|
operator=operator,
|
|||
|
|
record=AuditRecord(
|
|||
|
|
module=MODULE_CALC_TEMPLATE,
|
|||
|
|
action=ACTION_DELETE,
|
|||
|
|
target=row.template_code,
|
|||
|
|
summary=f"删除计提公式模板 {row.template_name}",
|
|||
|
|
),
|
|||
|
|
meta=meta,
|
|||
|
|
):
|
|||
|
|
pass
|