"""计提公式模板管理服务。 页面可维护:名称、说明、参数绑定(受骨架角色约束);公式骨架是受控枚举, 创建后锁定,避免自由表达式注入与执行错位。系统模板随迁移种子初始化, 禁删且骨架/编码/参数锁定;自定义模板无岗位引用才可删除。 """ 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