package jnpf.elnService.impl; import cn.hutool.core.util.IdUtil; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.NullNode; import jnpf.elnEntity.formula.ElnFormulaForms; import jnpf.elnEntity.formula.ElnFormulaParameterSlotEntity; import jnpf.elnEntity.formula.ElnFormulaVersionEntity; import jnpf.elnEntity.formula.ElnFormulaVos; import jnpf.elnEntity.formula.ElnTemplateFormulaBindingEntity; import jnpf.elnEntity.runtime.ElnFormulaCalculationLogEntity; import jnpf.elnEntity.runtime.ElnRecordEntity; import jnpf.elnEntity.runtime.ElnRecordFieldValueEntity; import jnpf.elnEntity.runtime.ElnRecordFormulaCalculationResult; import jnpf.elnEntity.runtime.ElnRecordRepeatRowEntity; import jnpf.elnEntity.runtime.ElnRecordRepeatValueEntity; import jnpf.elnEntity.runtime.ElnRecordRevisionEntity; import jnpf.elnEntity.template.ElnTemplateBindingEntity; import jnpf.elnMapper.ElnFormulaCalculationLogMapper; import jnpf.elnMapper.ElnFormulaParameterSlotMapper; import jnpf.elnMapper.ElnFormulaVersionMapper; import jnpf.elnMapper.ElnRecordRepeatRowMapper; import jnpf.elnMapper.ElnRecordRepeatValueMapper; import jnpf.elnMapper.ElnTemplateBindingMapper; import jnpf.elnMapper.ElnTemplateFormulaBindingMapper; import jnpf.elnService.ElnRecordFormulaCalculationService; import jnpf.elnService.formula.FormulaDependencyGraph; import jnpf.elnService.formula.FormulaPreviewService; import jnpf.exception.DataException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; /** * 保持记录字段当前态、固定公式版本和正式计算日志在同一事务内。 * 求值委托既有 FormulaPreviewService,避免浏览器或运行期各自维护 DSL 语义。 */ @Service public class ElnRecordFormulaCalculationServiceImpl implements ElnRecordFormulaCalculationService { private static final String ACTIVE = "ACTIVE"; private static final String SUCCESS = "SUCCESS"; private static final String MISSING_INPUT = "MISSING_INPUT"; private static final String ERROR = "ERROR"; private final ElnTemplateFormulaBindingMapper formulaBindingMapper; private final ElnFormulaVersionMapper formulaVersionMapper; private final ElnFormulaParameterSlotMapper parameterMapper; private final ElnTemplateBindingMapper fieldBindingMapper; private final ElnFormulaCalculationLogMapper logMapper; private final ElnRecordRepeatRowMapper repeatRowMapper; private final ElnRecordRepeatValueMapper repeatValueMapper; private final FormulaPreviewService previewService; private final FormulaDependencyGraph graph; @Autowired public ElnRecordFormulaCalculationServiceImpl(ElnTemplateFormulaBindingMapper formulaBindingMapper, ElnFormulaVersionMapper formulaVersionMapper, ElnFormulaParameterSlotMapper parameterMapper, ElnTemplateBindingMapper fieldBindingMapper, ElnFormulaCalculationLogMapper logMapper, ElnRecordRepeatRowMapper repeatRowMapper, ElnRecordRepeatValueMapper repeatValueMapper, FormulaPreviewService previewService) { this(formulaBindingMapper, formulaVersionMapper, parameterMapper, fieldBindingMapper, logMapper, repeatRowMapper, repeatValueMapper, previewService, new FormulaDependencyGraph()); } ElnRecordFormulaCalculationServiceImpl(ElnTemplateFormulaBindingMapper formulaBindingMapper, ElnFormulaVersionMapper formulaVersionMapper, ElnFormulaParameterSlotMapper parameterMapper, ElnTemplateBindingMapper fieldBindingMapper, ElnFormulaCalculationLogMapper logMapper, FormulaPreviewService previewService, FormulaDependencyGraph graph) { this(formulaBindingMapper, formulaVersionMapper, parameterMapper, fieldBindingMapper, logMapper, null, null, previewService, graph); } ElnRecordFormulaCalculationServiceImpl(ElnTemplateFormulaBindingMapper formulaBindingMapper, ElnFormulaVersionMapper formulaVersionMapper, ElnFormulaParameterSlotMapper parameterMapper, ElnTemplateBindingMapper fieldBindingMapper, ElnFormulaCalculationLogMapper logMapper, ElnRecordRepeatRowMapper repeatRowMapper, ElnRecordRepeatValueMapper repeatValueMapper, FormulaPreviewService previewService, FormulaDependencyGraph graph) { this.formulaBindingMapper = formulaBindingMapper; this.formulaVersionMapper = formulaVersionMapper; this.parameterMapper = parameterMapper; this.fieldBindingMapper = fieldBindingMapper; this.logMapper = logMapper; this.repeatRowMapper = repeatRowMapper; this.repeatValueMapper = repeatValueMapper; this.previewService = previewService; this.graph = graph; } @Override public List recalculateAffected( String tenantId, ElnRecordEntity record, ElnRecordRevisionEntity revision, List values, Set changedBindingIds, String operationCode, String operatorUserId) { if (changedBindingIds == null || changedBindingIds.isEmpty()) { return Collections.emptyList(); } return recalculateAffectedWithRepeats(tenantId, record, revision, values, changedBindingIds, operationCode, operatorUserId).getFieldValues(); } @Override public List recalculateAll( String tenantId, ElnRecordEntity record, ElnRecordRevisionEntity revision, List values, String operationCode, String operatorUserId) { return recalculateAllWithRepeats(tenantId, record, revision, values, operationCode, operatorUserId).getFieldValues(); } @Override public ElnRecordFormulaCalculationResult recalculateAffectedWithRepeats( String tenantId, ElnRecordEntity record, ElnRecordRevisionEntity revision, List values, Set changedBindingIds, String operationCode, String operatorUserId) { if (changedBindingIds == null || changedBindingIds.isEmpty()) { return new ElnRecordFormulaCalculationResult(); } return calculate(tenantId, record, revision, values, changedBindingIds, operationCode, operatorUserId, false); } @Override public ElnRecordFormulaCalculationResult recalculateAllWithRepeats( String tenantId, ElnRecordEntity record, ElnRecordRevisionEntity revision, List values, String operationCode, String operatorUserId) { return calculate(tenantId, record, revision, values, Collections.emptySet(), operationCode, operatorUserId, true); } private ElnRecordFormulaCalculationResult calculate( String tenantId, ElnRecordEntity record, ElnRecordRevisionEntity revision, List values, Set changedBindingIds, String operationCode, String operatorUserId, boolean all) { ElnRecordFormulaCalculationResult calculated = new ElnRecordFormulaCalculationResult(); requireContext(tenantId, record, revision, values, operationCode, operatorUserId); List formulas = activeFormulas(tenantId, record.getMubanId()); if (formulas.isEmpty()) return calculated; Map formulaById = byId(formulas); FormulaDependencyGraph.Result topology = graph.validate(formulas); if (!topology.isValid()) throw new DataException("活动公式依赖图无效"); Set affected = all ? new LinkedHashSet<>(formulaById.keySet()) : affectedFormulaIds(formulas, changedBindingIds); if (affected.isEmpty()) return calculated; Map valuesByBinding = valuesByBinding(values); Map bindings = fieldBindings(tenantId, record.getMubanId()); RepeatContext repeats = repeatContext(tenantId, record, revision); calculated.setRepeatRows(repeats.rows()); Map resultOwners = resultOwners(formulas); for (String formulaId : topology.getTopologicalOrder()) { if (!affected.contains(formulaId)) continue; ElnTemplateFormulaBindingEntity formula = formulaById.get(formulaId); ElnTemplateBindingEntity resultBinding = bindings.get(formula.getJieguoBangdingId()); if (resultBinding != null && "REPEAT".equals(resultBinding.getKongjianJuese())) { calculated.getRepeatValues().addAll(evaluateRows( tenantId, record, revision, valuesByBinding, bindings, repeats, resultOwners, formula, operationCode, operatorUserId)); continue; } ElnRecordFieldValueEntity result = evaluateOne( tenantId, record, revision, valuesByBinding, bindings, repeats, resultOwners, formula, operationCode, operatorUserId); if (result != null) calculated.getFieldValues().add(result); } return calculated; } private ElnRecordFieldValueEntity evaluateOne( String tenantId, ElnRecordEntity record, ElnRecordRevisionEntity revision, Map valuesByBinding, Map bindings, RepeatContext repeats, Map resultOwners, ElnTemplateFormulaBindingEntity formula, String operationCode, String operatorUserId) { ElnTemplateBindingEntity resultBinding = bindings.get(formula.getJieguoBangdingId()); ElnRecordFieldValueEntity resultValue = valuesByBinding.get(formula.getJieguoBangdingId()); ElnFormulaVersionEntity version = null; Map snapshot = new LinkedHashMap<>(); ElnFormulaVos.Preview preview; String failure = null; try { validateResult(record, revision, formula, resultBinding, resultValue); version = formulaVersion(tenantId, formula); List slots = slots( tenantId, formula.getGongshiBanbenId()); validateMappings(formula, slots, bindings, valuesByBinding, resultBinding); ElnFormulaForms.Preview request = request( version, slots, formula.getCanshuYingshe(), valuesByBinding, bindings, repeats, null, snapshot); String upstreamFailure = upstreamFailureReason(formula, resultOwners, valuesByBinding, bindings, repeats, null); if (upstreamFailure != null) { preview = new ElnFormulaVos.Preview(); preview.setStatus(MISSING_INPUT); failure = upstreamFailure; } else { preview = previewService.preview(request); } } catch (RuntimeException ex) { preview = new ElnFormulaVos.Preview(); preview.setStatus(ERROR); failure = reason(ex.getMessage()); } String status = normalizeStatus(preview == null ? null : preview.getStatus()); if (failure == null && !SUCCESS.equals(status)) failure = diagnosticReason(preview); if (resultValue != null) applyResult(resultValue, preview, status, failure, operatorUserId); insertLog(tenantId, record, revision, formula, resultBinding, operationCode, operatorUserId, snapshot, preview, status, failure); return resultValue; } private List evaluateRows( String tenantId, ElnRecordEntity record, ElnRecordRevisionEntity revision, Map valuesByBinding, Map bindings, RepeatContext repeats, Map resultOwners, ElnTemplateFormulaBindingEntity formula, String operationCode, String operatorUserId) { ElnTemplateBindingEntity resultBinding = bindings.get(formula.getJieguoBangdingId()); if (!repeats.available || resultBinding == null || !"REPEAT".equals(resultBinding.getKongjianJuese()) || !StringUtils.hasText(resultBinding.getRepeatGroupCode())) { throw new DataException("重复公式结果字段固定绑定无效"); } ElnFormulaVersionEntity version = null; List slots = Collections.emptyList(); String setupFailure = null; try { version = formulaVersion(tenantId, formula); slots = slots(tenantId, formula.getGongshiBanbenId()); validateMappings(formula, slots, bindings, valuesByBinding, resultBinding); } catch (RuntimeException ex) { setupFailure = reason(ex.getMessage()); } List calculated = new ArrayList<>(); for (ElnRecordRepeatRowEntity row : repeats.rows(resultBinding.getRepeatGroupCode())) { ElnRecordRepeatValueEntity resultValue = repeats.value( row.getRepeatGroupCode(), row.getRowId(), resultBinding.getId()); Map snapshot = new LinkedHashMap<>(); snapshot.put("rowId", row.getRowId()); ElnFormulaVos.Preview preview; String failure = null; try { if (setupFailure != null) throw new DataException(setupFailure); validateRepeatResult(record, revision, resultBinding, row, resultValue); ElnFormulaForms.Preview request = request(version, slots, formula.getCanshuYingshe(), valuesByBinding, bindings, repeats, row, snapshot); String upstreamFailure = upstreamFailureReason(formula, resultOwners, valuesByBinding, bindings, repeats, row); if (upstreamFailure != null) { preview = new ElnFormulaVos.Preview(); preview.setStatus(MISSING_INPUT); failure = upstreamFailure; } else { preview = previewService.preview(request); } } catch (RuntimeException ex) { preview = new ElnFormulaVos.Preview(); preview.setStatus(ERROR); failure = reason(ex.getMessage()); } String status = normalizeStatus(preview == null ? null : preview.getStatus()); if (failure == null && !SUCCESS.equals(status)) failure = diagnosticReason(preview); if (resultValue != null) { applyResult(resultValue, preview, status, failure, operatorUserId); calculated.add(resultValue); } insertLog(tenantId, record, revision, formula, resultBinding, operationCode, operatorUserId, snapshot, preview, status, failure); } return calculated; } private List activeFormulas(String tenantId, String templateId) { return formulaBindingMapper.selectList(Wrappers.lambdaQuery() .eq(ElnTemplateFormulaBindingEntity::getFTenantId, tenantId) .eq(ElnTemplateFormulaBindingEntity::getMubanId, templateId) .eq(ElnTemplateFormulaBindingEntity::getBangdingZhuangtai, ACTIVE) .and(q -> q.isNull(ElnTemplateFormulaBindingEntity::getFDeleteMark) .or().eq(ElnTemplateFormulaBindingEntity::getFDeleteMark, 0))); } private Map fieldBindings(String tenantId, String templateId) { Map result = new LinkedHashMap<>(); for (ElnTemplateBindingEntity binding : fieldBindingMapper.selectList( Wrappers.lambdaQuery() .eq(ElnTemplateBindingEntity::getFTenantId, tenantId) .eq(ElnTemplateBindingEntity::getMubanId, templateId) .eq(ElnTemplateBindingEntity::getBangdingZhuangtai, ACTIVE) .and(q -> q.isNull(ElnTemplateBindingEntity::getFDeleteMark) .or().eq(ElnTemplateBindingEntity::getFDeleteMark, 0)))) { if (result.put(binding.getId(), binding) != null) throw new DataException("活动字段绑定重复"); } return result; } private RepeatContext repeatContext(String tenantId, ElnRecordEntity record, ElnRecordRevisionEntity revision) { if (repeatRowMapper == null && repeatValueMapper == null) return RepeatContext.unavailable(); if (repeatRowMapper == null || repeatValueMapper == null) { throw new DataException("重复公式计算服务不可用"); } List selectedRows = repeatRowMapper.selectList( Wrappers.lambdaQuery() .eq(ElnRecordRepeatRowEntity::getFTenantId, tenantId) .eq(ElnRecordRepeatRowEntity::getDianziJiluId, record.getId()) .eq(ElnRecordRepeatRowEntity::getXiudingId, revision.getId()) .eq(ElnRecordRepeatRowEntity::getOperationStatus, ACTIVE) .and(q -> q.isNull(ElnRecordRepeatRowEntity::getFDeleteMark) .or().eq(ElnRecordRepeatRowEntity::getFDeleteMark, 0)) .orderByAsc(ElnRecordRepeatRowEntity::getRowOrder)); List activeRows = new ArrayList<>(); Set rowKeys = new LinkedHashSet<>(); for (ElnRecordRepeatRowEntity row : selectedRows == null ? Collections.emptyList() : selectedRows) { if (row == null || !ACTIVE.equals(row.getOperationStatus()) || Integer.valueOf(1).equals(row.getFDeleteMark())) continue; if (!Objects.equals(record.getId(), row.getDianziJiluId()) || !Objects.equals(revision.getId(), row.getXiudingId()) || !Objects.equals(record.getMubanId(), row.getMubanId()) || !StringUtils.hasText(row.getRepeatGroupCode()) || !StringUtils.hasText(row.getRowId()) || !rowKeys.add(repeatCellKey(row.getRepeatGroupCode(), row.getRowId(), ""))) { throw new DataException("重复公式计算行身份无效"); } activeRows.add(row); } activeRows.sort(Comparator.comparing(ElnRecordRepeatRowEntity::getRowOrder, Comparator.nullsLast(Comparator.naturalOrder())) .thenComparing(ElnRecordRepeatRowEntity::getRowId)); List selectedValues = repeatValueMapper.selectList( Wrappers.lambdaQuery() .eq(ElnRecordRepeatValueEntity::getFTenantId, tenantId) .eq(ElnRecordRepeatValueEntity::getDianziJiluId, record.getId()) .eq(ElnRecordRepeatValueEntity::getXiudingId, revision.getId()) .and(q -> q.isNull(ElnRecordRepeatValueEntity::getFDeleteMark) .or().eq(ElnRecordRepeatValueEntity::getFDeleteMark, 0))); Map values = new LinkedHashMap<>(); for (ElnRecordRepeatValueEntity value : selectedValues == null ? Collections.emptyList() : selectedValues) { if (value == null || Integer.valueOf(1).equals(value.getFDeleteMark())) continue; String rowKey = repeatCellKey(value.getRepeatGroupCode(), value.getRepeatRowId(), ""); if (!rowKeys.contains(rowKey)) continue; String key = repeatCellKey(value.getRepeatGroupCode(), value.getRepeatRowId(), value.getMubanBangdingId()); if (values.put(key, value) != null) { throw new DataException("重复公式计算字段值重复"); } } return new RepeatContext(activeRows, values, true); } private ElnFormulaVersionEntity formulaVersion( String tenantId, ElnTemplateFormulaBindingEntity formula) { ElnFormulaVersionEntity version = formulaVersionMapper.selectOne( Wrappers.lambdaQuery() .eq(ElnFormulaVersionEntity::getFTenantId, tenantId) .eq(ElnFormulaVersionEntity::getId, formula.getGongshiBanbenId()) .and(q -> q.isNull(ElnFormulaVersionEntity::getFDeleteMark) .or().eq(ElnFormulaVersionEntity::getFDeleteMark, 0)).last("limit 1")); if (version == null || !Objects.equals(formula.getGongshiDingyiId(), version.getGongshiDingyiId()) || !"PUBLISHED".equals(version.getBizStatus())) { throw new DataException("固定公式版本无效"); } return version; } private static void validateResult( ElnRecordEntity record, ElnRecordRevisionEntity revision, ElnTemplateFormulaBindingEntity formula, ElnTemplateBindingEntity binding, ElnRecordFieldValueEntity value) { if (binding == null || value == null || !"PRIMARY".equals(binding.getKongjianJuese()) || !Objects.equals(record.getMubanId(), binding.getMubanId()) || !Objects.equals(record.getId(), value.getDianziJiluId()) || !Objects.equals(revision.getId(), value.getXiudingId()) || !Objects.equals(binding.getId(), value.getMubanBangdingId()) || !Objects.equals(binding.getZiduanDingyiId(), value.getZiduanDingyiId()) || !Objects.equals(binding.getZiduanBianma(), value.getZiduanBianma()) || !Objects.equals(binding.getZiduanBanbenId(), value.getZiduanBanbenId()) || !Objects.equals(formula.getJieguoBangdingId(), binding.getId())) { throw new DataException("公式结果字段固定绑定无效"); } } private static void validateRepeatResult( ElnRecordEntity record, ElnRecordRevisionEntity revision, ElnTemplateBindingEntity binding, ElnRecordRepeatRowEntity row, ElnRecordRepeatValueEntity value) { if (binding == null || row == null || value == null || !"REPEAT".equals(binding.getKongjianJuese()) || !Objects.equals(record.getMubanId(), binding.getMubanId()) || !Objects.equals(record.getId(), value.getDianziJiluId()) || !Objects.equals(revision.getId(), value.getXiudingId()) || !Objects.equals(row.getRowId(), value.getRepeatRowId()) || !Objects.equals(row.getRepeatGroupCode(), value.getRepeatGroupCode()) || !Objects.equals(binding.getRepeatGroupCode(), value.getRepeatGroupCode()) || !Objects.equals(binding.getId(), value.getMubanBangdingId()) || !Objects.equals(binding.getZiduanDingyiId(), value.getZiduanDingyiId()) || !Objects.equals(binding.getZiduanBianma(), value.getZiduanBianma()) || !Objects.equals(binding.getZiduanBanbenId(), value.getZiduanBanbenId())) { throw new DataException("重复公式结果字段固定绑定无效"); } } private static void validateMappings( ElnTemplateFormulaBindingEntity formula, List slots, Map bindings, Map values, ElnTemplateBindingEntity resultBinding) { if (resultBinding == null || !("PRIMARY".equals(resultBinding.getKongjianJuese()) || "REPEAT".equals(resultBinding.getKongjianJuese()))) { throw new DataException("公式结果字段固定绑定无效"); } Map mappings = formula.getCanshuYingshe() == null ? Collections.emptyMap() : formula.getCanshuYingshe(); Set slotCodes = new LinkedHashSet<>(); for (ElnFormulaParameterSlotEntity slot : slots) { if (slot == null || !StringUtils.hasText(slot.getCanshuBianma()) || !slotCodes.add(slot.getCanshuBianma())) { throw new DataException("固定公式参数槽无效"); } } if (!slotCodes.equals(mappings.keySet())) { throw new DataException("固定公式参数映射不完整"); } for (ElnFormulaParameterSlotEntity slot : slots) { String bindingId = mappings.get(slot.getCanshuBianma()); ElnTemplateBindingEntity binding = bindings.get(bindingId); if (binding == null) { throw new DataException("固定公式参数字段无效"); } boolean list = "DECIMAL_LIST".equalsIgnoreCase(slot.getShujuLeixing()); if (list && !"REPEAT".equals(binding.getKongjianJuese())) { throw new DataException("集合参数必须映射重复字段"); } if (!list && "PRIMARY".equals(resultBinding.getKongjianJuese()) && !"PRIMARY".equals(binding.getKongjianJuese())) { throw new DataException("普通结果的标量参数必须映射主字段"); } if ("REPEAT".equals(resultBinding.getKongjianJuese())) { if (list) throw new DataException("逐行公式不支持集合参数"); if (!"REPEAT".equals(binding.getKongjianJuese()) || !Objects.equals(resultBinding.getRepeatGroupCode(), binding.getRepeatGroupCode())) { throw new DataException("逐行公式参数与结果必须属于同一重复组"); } } if ("PRIMARY".equals(binding.getKongjianJuese())) { ElnRecordFieldValueEntity value = values.get(bindingId); if (value == null || !Objects.equals(binding.getZiduanBanbenId(), value.getZiduanBanbenId())) { throw new DataException("固定公式参数字段无效"); } } } } private static String upstreamFailureReason( ElnTemplateFormulaBindingEntity formula, Map resultOwners, Map values, Map bindings, RepeatContext repeats, ElnRecordRepeatRowEntity row) { List missing = new ArrayList<>(); for (String bindingId : formula.getCanshuYingshe().values()) { if (!resultOwners.containsKey(bindingId)) continue; ElnTemplateBindingEntity binding = bindings.get(bindingId); if (binding != null && "REPEAT".equals(binding.getKongjianJuese())) { List rows = row == null ? repeats.rows(binding.getRepeatGroupCode()) : Collections.singletonList(row); for (ElnRecordRepeatRowEntity item : rows) { ElnRecordRepeatValueEntity value = repeats.value( item.getRepeatGroupCode(), item.getRowId(), bindingId); if (value == null || !SUCCESS.equals(value.getJisuanZhuangtai())) { missing.add(binding.getZiduanBianma() + "[" + item.getRowId() + "]"); } } } else { ElnRecordFieldValueEntity value = values.get(bindingId); if (value == null || !SUCCESS.equals(value.getJisuanZhuangtai())) { missing.add(value != null && StringUtils.hasText(value.getZiduanBianma()) ? value.getZiduanBianma() : bindingId); } } } return missing.isEmpty() ? null : "缺少上游公式结果: " + String.join(", ", missing); } private List slots(String tenantId, String formulaVersionId) { return parameterMapper.selectList(Wrappers.lambdaQuery() .eq(ElnFormulaParameterSlotEntity::getFTenantId, tenantId) .eq(ElnFormulaParameterSlotEntity::getGongshiBanbenId, formulaVersionId) .and(q -> q.isNull(ElnFormulaParameterSlotEntity::getFDeleteMark) .or().eq(ElnFormulaParameterSlotEntity::getFDeleteMark, 0)) .orderByAsc(ElnFormulaParameterSlotEntity::getPaixu) .orderByAsc(ElnFormulaParameterSlotEntity::getId)); } private ElnFormulaForms.Preview request(ElnFormulaVersionEntity version, List slots, Map mappings, Map values, Map bindings, RepeatContext repeats, ElnRecordRepeatRowEntity row, Map snapshot) { ElnFormulaForms.Preview request = new ElnFormulaForms.Preview(); request.setBiaodashi(version.getBiaodashi()); request.setJieguoLeixing(version.getJieguoLeixing()); request.setXiuyueXiaoshuWei(version.getXiuyueXiaoshuWei()); request.setXiuyueMoshi(version.getXiuyueMoshi()); for (ElnFormulaParameterSlotEntity slot : slots) { ElnFormulaForms.Parameter parameter = new ElnFormulaForms.Parameter(); parameter.setId(slot.getId()); parameter.setCanshuBianma(slot.getCanshuBianma()); parameter.setCanshuMingcheng(slot.getCanshuMingcheng()); parameter.setShujuLeixing(slot.getShujuLeixing()); parameter.setDanwei(slot.getDanwei()); parameter.setShifouBitian(slot.getShifouBitian()); parameter.setPaixu(slot.getPaixu()); request.getParameters().add(parameter); String fieldBindingId = mappings == null ? null : mappings.get(slot.getCanshuBianma()); ElnTemplateBindingEntity binding = bindings.get(fieldBindingId); JsonNode input; if ("DECIMAL_LIST".equalsIgnoreCase(slot.getShujuLeixing())) { AggregateInput aggregate = aggregateInput(repeats, binding, slot); input = aggregate.input; snapshot.put(slot.getCanshuBianma(), aggregate.snapshot); } else if (binding != null && "REPEAT".equals(binding.getKongjianJuese())) { ElnRecordRepeatValueEntity value = row == null ? null : repeats.value(row.getRepeatGroupCode(), row.getRowId(), fieldBindingId); input = input(value, slot.getShujuLeixing()); snapshot.put(slot.getCanshuBianma(), snapshotValue(input)); } else { ElnRecordFieldValueEntity value = fieldBindingId == null ? null : values.get(fieldBindingId); input = input(value, slot.getShujuLeixing()); snapshot.put(slot.getCanshuBianma(), snapshotValue(input)); } request.getInputs().put(slot.getCanshuBianma(), input); } return request; } private static AggregateInput aggregateInput(RepeatContext repeats, ElnTemplateBindingEntity binding, ElnFormulaParameterSlotEntity slot) { if (binding == null || !"REPEAT".equals(binding.getKongjianJuese())) { throw new DataException("集合参数必须映射重复字段"); } ArrayNode values = JsonNodeFactory.instance.arrayNode(); List snapshotValues = new ArrayList<>(); List participating = new ArrayList<>(); List excluded = new ArrayList<>(); for (ElnRecordRepeatRowEntity row : repeats.rows(binding.getRepeatGroupCode())) { ElnRecordRepeatValueEntity value = repeats.value(binding.getRepeatGroupCode(), row.getRowId(), binding.getId()); JsonNode item = input(value, "DECIMAL"); if (item == null || item.isNull()) { excluded.add(row.getRowId()); continue; } values.add(item); snapshotValues.add(snapshotValue(item)); participating.add(row.getRowId()); } Map snapshot = new LinkedHashMap<>(); snapshot.put("values", snapshotValues); snapshot.put("participatingRowIds", participating); snapshot.put("excludedRowIds", excluded); JsonNode input = Integer.valueOf(1).equals(slot.getShifouBitian()) && !excluded.isEmpty() ? NullNode.instance : values; return new AggregateInput(input, snapshot); } private static JsonNode input(ElnRecordFieldValueEntity value, String type) { if (value == null || (!StringUtils.hasText(value.getZhiWenben()) && value.getZhiShuzhi() == null)) { return NullNode.instance; } String upper = type == null ? "" : type.toUpperCase(); if ("DECIMAL".equals(upper)) { if (value.getZhiShuzhi() != null) return JsonNodeFactory.instance.numberNode(value.getZhiShuzhi()); try { return JsonNodeFactory.instance.numberNode(new BigDecimal(value.getZhiWenben())); } catch (NumberFormatException ex) { return JsonNodeFactory.instance.textNode(value.getZhiWenben()); } } if ("BOOLEAN".equals(upper)) { if ("true".equalsIgnoreCase(value.getZhiWenben())) return JsonNodeFactory.instance.booleanNode(true); if ("false".equalsIgnoreCase(value.getZhiWenben())) return JsonNodeFactory.instance.booleanNode(false); return JsonNodeFactory.instance.textNode(value.getZhiWenben()); } return JsonNodeFactory.instance.textNode(value.getZhiWenben()); } private static JsonNode input(ElnRecordRepeatValueEntity value, String type) { if (value == null || (!StringUtils.hasText(value.getZhiWenben()) && value.getZhiShuzhi() == null)) return NullNode.instance; String upper = type == null ? "" : type.toUpperCase(); if ("DECIMAL".equals(upper)) { if (value.getZhiShuzhi() != null) { return JsonNodeFactory.instance.numberNode(value.getZhiShuzhi()); } try { return JsonNodeFactory.instance.numberNode(new BigDecimal(value.getZhiWenben())); } catch (NumberFormatException ex) { return JsonNodeFactory.instance.textNode(value.getZhiWenben()); } } if ("BOOLEAN".equals(upper)) { if ("true".equalsIgnoreCase(value.getZhiWenben())) { return JsonNodeFactory.instance.booleanNode(true); } if ("false".equalsIgnoreCase(value.getZhiWenben())) { return JsonNodeFactory.instance.booleanNode(false); } } return JsonNodeFactory.instance.textNode(value.getZhiWenben()); } private static Object snapshotValue(JsonNode value) { if (value == null || value.isNull()) return null; if (value.isBoolean()) return value.booleanValue(); if (value.isNumber()) return value.decimalValue(); return value.asText(); } private static Set affectedFormulaIds(List formulas, Set changedBindingIds) { Set affected = new LinkedHashSet<>(); Set available = new LinkedHashSet<>(changedBindingIds); boolean changed; do { changed = false; for (ElnTemplateFormulaBindingEntity formula : formulas) { if (affected.contains(formula.getId())) continue; if (intersects(formula.getCanshuYingshe(), available)) { affected.add(formula.getId()); available.add(formula.getJieguoBangdingId()); changed = true; } } } while (changed); return affected; } private static boolean intersects(Map mapping, Set bindings) { if (mapping == null) return false; for (String id : mapping.values()) if (bindings.contains(id)) return true; return false; } private static Map byId(List formulas) { Map result = new LinkedHashMap<>(); for (ElnTemplateFormulaBindingEntity formula : formulas) { if (result.put(formula.getId(), formula) != null) throw new DataException("活动公式绑定重复"); } return result; } private static Map resultOwners( List formulas) { Map result = new LinkedHashMap<>(); for (ElnTemplateFormulaBindingEntity formula : formulas) { if (result.put(formula.getJieguoBangdingId(), formula.getId()) != null) { throw new DataException("公式结果字段被重复写入"); } } return result; } private static Map valuesByBinding(List values) { Map result = new LinkedHashMap<>(); for (ElnRecordFieldValueEntity value : values) { if (result.put(value.getMubanBangdingId(), value) != null) { throw new DataException("电子记录存在重复主字段值"); } } return result; } private static void applyResult(ElnRecordFieldValueEntity target, ElnFormulaVos.Preview preview, String status, String failure, String userId) { target.setZhiWenben(null); target.setZhiShuzhi(null); target.setZhiRiqi(null); target.setZhiLaiyuan("FORMULA"); target.setJisuanZhuangtai(status); target.setJisuanXiaoxi(SUCCESS.equals(status) ? null : reason(failure)); target.setJisuanShijian(new Date()); target.setFLastModifyUserId(userId); if (!SUCCESS.equals(status) || preview == null) return; Object result = preview.getRoundedResult(); if (result instanceof BigDecimal) { target.setZhiShuzhi((BigDecimal) result); target.setZhiWenben(((BigDecimal) result).toPlainString()); } else if (result instanceof Boolean) { target.setZhiWenben(String.valueOf(result)); } else if (result != null) { target.setZhiWenben(String.valueOf(result)); } } private static void applyResult(ElnRecordRepeatValueEntity target, ElnFormulaVos.Preview preview, String status, String failure, String userId) { target.setZhiWenben(null); target.setZhiShuzhi(null); target.setZhiRiqi(null); target.setZhiLaiyuan("FORMULA"); target.setJisuanZhuangtai(status); target.setJisuanXiaoxi(SUCCESS.equals(status) ? null : reason(failure)); target.setJisuanShijian(new Date()); target.setFLastModifyUserId(userId); target.setFLastModifyTime(new Date()); if (!SUCCESS.equals(status) || preview == null) return; Object result = preview.getRoundedResult(); if (result instanceof BigDecimal) { target.setZhiShuzhi((BigDecimal) result); target.setZhiWenben(((BigDecimal) result).toPlainString()); } else if (result != null) { target.setZhiWenben(String.valueOf(result)); } } private void insertLog(String tenantId, ElnRecordEntity record, ElnRecordRevisionEntity revision, ElnTemplateFormulaBindingEntity formula, ElnTemplateBindingEntity resultBinding, String operationCode, String userId, Map snapshot, ElnFormulaVos.Preview preview, String status, String failure) { ElnFormulaCalculationLogEntity log = new ElnFormulaCalculationLogEntity(); log.setId(IdUtil.getSnowflakeNextIdStr()); log.setDianziJiluId(record.getId()); log.setXiudingId(revision.getId()); log.setMubanGongshiBangdingId(formula.getId()); log.setGongshiBanbenId(formula.getGongshiBanbenId()); log.setJieguoMubanBangdingId(formula.getJieguoBangdingId()); log.setJieguoZiduanBanbenId(resultBinding == null ? null : resultBinding.getZiduanBanbenId()); log.setJisuanCaozuoBianma(operationCode); log.setJisuanMoshi("AUTHORITATIVE"); log.setJisuanZhuangtai(status); log.setShuruKuaizhao(snapshot); if (SUCCESS.equals(status) && preview != null) { Object result = preview.getRoundedResult(); log.setJieguoWenben(result == null ? null : String.valueOf(result)); if (result instanceof BigDecimal) log.setJieguoShuzhi((BigDecimal) result); } log.setShibaiYuanyin(SUCCESS.equals(status) ? null : reason(failure)); log.setJisuanShijian(new Date()); log.setFTenantId(tenantId); log.setFVersion(0); log.setFCreatorUserId(userId); log.setFLastModifyUserId(userId); log.setFDeleteMark(0); if (logMapper.insert(log) != 1) throw new DataException("公式计算日志写入失败"); } private static String normalizeStatus(String status) { return SUCCESS.equals(status) || MISSING_INPUT.equals(status) || ERROR.equals(status) ? status : ERROR; } private static String diagnosticReason(ElnFormulaVos.Preview preview) { if (preview != null && preview.getDiagnostics() != null) { for (ElnFormulaVos.Diagnostic item : preview.getDiagnostics()) { if (item != null && "ERROR".equals(item.getSeverity())) return reason(item.getCode() + ": " + item.getMessage()); } } return "公式计算失败"; } private static String reason(String value) { if (!StringUtils.hasText(value)) return "公式计算失败"; return value.length() > 1000 ? value.substring(0, 1000) : value; } private static void requireContext(String tenantId, ElnRecordEntity record, ElnRecordRevisionEntity revision, List values, String operationCode, String operatorUserId) { if (!StringUtils.hasText(tenantId) || record == null || revision == null || values == null || !StringUtils.hasText(operationCode) || !StringUtils.hasText(operatorUserId)) { throw new DataException("公式计算上下文不完整"); } } private static String repeatCellKey(String groupCode, String rowId, String bindingId) { return groupCode + "\n" + rowId + "\n" + bindingId; } private static final class AggregateInput { private final JsonNode input; private final Map snapshot; private AggregateInput(JsonNode input, Map snapshot) { this.input = input; this.snapshot = snapshot; } } private static final class RepeatContext { private final List rows; private final Map values; private final boolean available; private RepeatContext(List rows, Map values, boolean available) { this.rows = rows; this.values = values; this.available = available; } private static RepeatContext unavailable() { return new RepeatContext(Collections.emptyList(), Collections.emptyMap(), false); } private List rows(String groupCode) { if (!available) throw new DataException("重复公式计算服务不可用"); List result = new ArrayList<>(); for (ElnRecordRepeatRowEntity row : rows) { if (Objects.equals(groupCode, row.getRepeatGroupCode())) result.add(row); } return result; } private List rows() { return available ? new ArrayList<>(rows) : Collections.emptyList(); } private ElnRecordRepeatValueEntity value( String groupCode, String rowId, String bindingId) { return values.get(repeatCellKey(groupCode, rowId, bindingId)); } } }