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<ElnRecordFieldValueEntity> recalculateAffected(
|
String tenantId, ElnRecordEntity record,
|
ElnRecordRevisionEntity revision,
|
List<ElnRecordFieldValueEntity> values,
|
Set<String> 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<ElnRecordFieldValueEntity> recalculateAll(
|
String tenantId, ElnRecordEntity record,
|
ElnRecordRevisionEntity revision,
|
List<ElnRecordFieldValueEntity> 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<ElnRecordFieldValueEntity> values,
|
Set<String> 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<ElnRecordFieldValueEntity> values,
|
String operationCode, String operatorUserId) {
|
return calculate(tenantId, record, revision, values, Collections.<String>emptySet(),
|
operationCode, operatorUserId, true);
|
}
|
|
private ElnRecordFormulaCalculationResult calculate(
|
String tenantId, ElnRecordEntity record, ElnRecordRevisionEntity revision,
|
List<ElnRecordFieldValueEntity> values, Set<String> changedBindingIds,
|
String operationCode, String operatorUserId, boolean all) {
|
ElnRecordFormulaCalculationResult calculated = new ElnRecordFormulaCalculationResult();
|
requireContext(tenantId, record, revision, values, operationCode, operatorUserId);
|
List<ElnTemplateFormulaBindingEntity> formulas = activeFormulas(tenantId, record.getMubanId());
|
if (formulas.isEmpty()) return calculated;
|
Map<String, ElnTemplateFormulaBindingEntity> formulaById = byId(formulas);
|
FormulaDependencyGraph.Result topology = graph.validate(formulas);
|
if (!topology.isValid()) throw new DataException("活动公式依赖图无效");
|
Set<String> affected = all ? new LinkedHashSet<>(formulaById.keySet())
|
: affectedFormulaIds(formulas, changedBindingIds);
|
if (affected.isEmpty()) return calculated;
|
Map<String, ElnRecordFieldValueEntity> valuesByBinding = valuesByBinding(values);
|
Map<String, ElnTemplateBindingEntity> bindings = fieldBindings(tenantId, record.getMubanId());
|
RepeatContext repeats = repeatContext(tenantId, record, revision);
|
calculated.setRepeatRows(repeats.rows());
|
Map<String, String> 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<String, ElnRecordFieldValueEntity> valuesByBinding,
|
Map<String, ElnTemplateBindingEntity> bindings,
|
RepeatContext repeats,
|
Map<String, String> resultOwners,
|
ElnTemplateFormulaBindingEntity formula, String operationCode,
|
String operatorUserId) {
|
ElnTemplateBindingEntity resultBinding = bindings.get(formula.getJieguoBangdingId());
|
ElnRecordFieldValueEntity resultValue = valuesByBinding.get(formula.getJieguoBangdingId());
|
ElnFormulaVersionEntity version = null;
|
Map<String, Object> snapshot = new LinkedHashMap<>();
|
ElnFormulaVos.Preview preview;
|
String failure = null;
|
try {
|
validateResult(record, revision, formula, resultBinding, resultValue);
|
version = formulaVersion(tenantId, formula);
|
List<ElnFormulaParameterSlotEntity> 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<ElnRecordRepeatValueEntity> evaluateRows(
|
String tenantId, ElnRecordEntity record, ElnRecordRevisionEntity revision,
|
Map<String, ElnRecordFieldValueEntity> valuesByBinding,
|
Map<String, ElnTemplateBindingEntity> bindings, RepeatContext repeats,
|
Map<String, String> 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<ElnFormulaParameterSlotEntity> 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<ElnRecordRepeatValueEntity> calculated = new ArrayList<>();
|
for (ElnRecordRepeatRowEntity row : repeats.rows(resultBinding.getRepeatGroupCode())) {
|
ElnRecordRepeatValueEntity resultValue = repeats.value(
|
row.getRepeatGroupCode(), row.getRowId(), resultBinding.getId());
|
Map<String, Object> 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<ElnTemplateFormulaBindingEntity> activeFormulas(String tenantId, String templateId) {
|
return formulaBindingMapper.selectList(Wrappers.<ElnTemplateFormulaBindingEntity>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<String, ElnTemplateBindingEntity> fieldBindings(String tenantId, String templateId) {
|
Map<String, ElnTemplateBindingEntity> result = new LinkedHashMap<>();
|
for (ElnTemplateBindingEntity binding : fieldBindingMapper.selectList(
|
Wrappers.<ElnTemplateBindingEntity>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<ElnRecordRepeatRowEntity> selectedRows = repeatRowMapper.selectList(
|
Wrappers.<ElnRecordRepeatRowEntity>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<ElnRecordRepeatRowEntity> activeRows = new ArrayList<>();
|
Set<String> rowKeys = new LinkedHashSet<>();
|
for (ElnRecordRepeatRowEntity row : selectedRows == null
|
? Collections.<ElnRecordRepeatRowEntity>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<ElnRecordRepeatValueEntity> selectedValues = repeatValueMapper.selectList(
|
Wrappers.<ElnRecordRepeatValueEntity>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<String, ElnRecordRepeatValueEntity> values = new LinkedHashMap<>();
|
for (ElnRecordRepeatValueEntity value : selectedValues == null
|
? Collections.<ElnRecordRepeatValueEntity>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.<ElnFormulaVersionEntity>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<ElnFormulaParameterSlotEntity> slots,
|
Map<String, ElnTemplateBindingEntity> bindings,
|
Map<String, ElnRecordFieldValueEntity> values,
|
ElnTemplateBindingEntity resultBinding) {
|
if (resultBinding == null || !("PRIMARY".equals(resultBinding.getKongjianJuese())
|
|| "REPEAT".equals(resultBinding.getKongjianJuese()))) {
|
throw new DataException("公式结果字段固定绑定无效");
|
}
|
Map<String, String> mappings = formula.getCanshuYingshe() == null
|
? Collections.emptyMap() : formula.getCanshuYingshe();
|
Set<String> 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<String, String> resultOwners,
|
Map<String, ElnRecordFieldValueEntity> values,
|
Map<String, ElnTemplateBindingEntity> bindings,
|
RepeatContext repeats, ElnRecordRepeatRowEntity row) {
|
List<String> 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<ElnRecordRepeatRowEntity> 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<ElnFormulaParameterSlotEntity> slots(String tenantId, String formulaVersionId) {
|
return parameterMapper.selectList(Wrappers.<ElnFormulaParameterSlotEntity>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<ElnFormulaParameterSlotEntity> slots,
|
Map<String, String> mappings,
|
Map<String, ElnRecordFieldValueEntity> values,
|
Map<String, ElnTemplateBindingEntity> bindings,
|
RepeatContext repeats,
|
ElnRecordRepeatRowEntity row,
|
Map<String, Object> 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<Object> snapshotValues = new ArrayList<>();
|
List<String> participating = new ArrayList<>();
|
List<String> 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<String, Object> 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<String> affectedFormulaIds(List<ElnTemplateFormulaBindingEntity> formulas,
|
Set<String> changedBindingIds) {
|
Set<String> affected = new LinkedHashSet<>();
|
Set<String> 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<String, String> mapping, Set<String> bindings) {
|
if (mapping == null) return false;
|
for (String id : mapping.values()) if (bindings.contains(id)) return true;
|
return false;
|
}
|
|
private static Map<String, ElnTemplateFormulaBindingEntity> byId(List<ElnTemplateFormulaBindingEntity> formulas) {
|
Map<String, ElnTemplateFormulaBindingEntity> result = new LinkedHashMap<>();
|
for (ElnTemplateFormulaBindingEntity formula : formulas) {
|
if (result.put(formula.getId(), formula) != null) throw new DataException("活动公式绑定重复");
|
}
|
return result;
|
}
|
|
private static Map<String, String> resultOwners(
|
List<ElnTemplateFormulaBindingEntity> formulas) {
|
Map<String, String> result = new LinkedHashMap<>();
|
for (ElnTemplateFormulaBindingEntity formula : formulas) {
|
if (result.put(formula.getJieguoBangdingId(), formula.getId()) != null) {
|
throw new DataException("公式结果字段被重复写入");
|
}
|
}
|
return result;
|
}
|
|
private static Map<String, ElnRecordFieldValueEntity> valuesByBinding(List<ElnRecordFieldValueEntity> values) {
|
Map<String, ElnRecordFieldValueEntity> 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<String, Object> 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<ElnRecordFieldValueEntity> 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<String, Object> snapshot;
|
|
private AggregateInput(JsonNode input, Map<String, Object> snapshot) {
|
this.input = input;
|
this.snapshot = snapshot;
|
}
|
}
|
|
private static final class RepeatContext {
|
private final List<ElnRecordRepeatRowEntity> rows;
|
private final Map<String, ElnRecordRepeatValueEntity> values;
|
private final boolean available;
|
|
private RepeatContext(List<ElnRecordRepeatRowEntity> rows,
|
Map<String, ElnRecordRepeatValueEntity> values,
|
boolean available) {
|
this.rows = rows;
|
this.values = values;
|
this.available = available;
|
}
|
|
private static RepeatContext unavailable() {
|
return new RepeatContext(Collections.<ElnRecordRepeatRowEntity>emptyList(),
|
Collections.<String, ElnRecordRepeatValueEntity>emptyMap(), false);
|
}
|
|
private List<ElnRecordRepeatRowEntity> rows(String groupCode) {
|
if (!available) throw new DataException("重复公式计算服务不可用");
|
List<ElnRecordRepeatRowEntity> result = new ArrayList<>();
|
for (ElnRecordRepeatRowEntity row : rows) {
|
if (Objects.equals(groupCode, row.getRepeatGroupCode())) result.add(row);
|
}
|
return result;
|
}
|
|
private List<ElnRecordRepeatRowEntity> rows() {
|
return available ? new ArrayList<>(rows) : Collections.emptyList();
|
}
|
|
private ElnRecordRepeatValueEntity value(
|
String groupCode, String rowId, String bindingId) {
|
return values.get(repeatCellKey(groupCode, rowId, bindingId));
|
}
|
}
|
}
|