package jnpf.elnService.template;
|
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
import jnpf.audit.model.AuditEventDTO;
|
import jnpf.audit.sdk.AuditClient;
|
import jnpf.elnEntity.field.ElnFieldVersionEntity;
|
import jnpf.elnEntity.formula.ElnTemplateFormulaBindingEntity;
|
import jnpf.elnEntity.template.ElnTemplateBindingEntity;
|
import jnpf.elnEntity.template.ElnTemplateVos;
|
import jnpf.elnMapper.ElnFieldVersionMapper;
|
import jnpf.elnMapper.ElnTemplateFormulaBindingMapper;
|
import jnpf.elnMapper.ElnTemplateBindingMapper;
|
import jnpf.elnService.template.docx.DocxControlConfigMatcher;
|
import jnpf.elnService.template.docx.DocxExpectedControlConfig;
|
import jnpf.elnService.template.docx.DocxControlSnapshot;
|
import jnpf.exception.DataException;
|
import jnpf.util.JsonUtil;
|
import org.springframework.beans.factory.ObjectProvider;
|
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.stereotype.Service;
|
import org.springframework.util.StringUtils;
|
|
import java.util.ArrayList;
|
import java.util.Date;
|
import java.util.LinkedHashMap;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.Objects;
|
import java.util.function.Consumer;
|
|
@Service
|
public class ElnPendingBindingCleanupService {
|
|
private static final String PENDING = "PENDING";
|
private static final String CANCELLED = "CANCELLED";
|
private static final String TIMEOUT_REASON = "PENDING_TIMEOUT_NO_CONTROL";
|
private static final String FORMULA_TIMEOUT_REASON = "PENDING_TIMEOUT_NO_LOCK";
|
|
private final Store store;
|
private final FormulaStore formulaStore;
|
private final Consumer<AuditEventDTO> auditRecorder;
|
|
@Autowired
|
public ElnPendingBindingCleanupService(ElnTemplateBindingMapper bindingMapper,
|
ElnFieldVersionMapper fieldVersionMapper,
|
ElnTemplateFormulaBindingMapper formulaBindingMapper,
|
ObjectProvider<AuditClient> auditClientProvider) {
|
this(new MyBatisStore(bindingMapper, fieldVersionMapper),
|
new MyBatisFormulaStore(formulaBindingMapper, bindingMapper), event -> {
|
AuditClient client = auditClientProvider.getIfAvailable();
|
if (client != null) {
|
client.record(event);
|
}
|
});
|
}
|
|
ElnPendingBindingCleanupService(Store store, Consumer<AuditEventDTO> auditRecorder) {
|
this(store, FormulaStore.none(), auditRecorder);
|
}
|
|
ElnPendingBindingCleanupService(Store store, FormulaStore formulaStore,
|
Consumer<AuditEventDTO> auditRecorder) {
|
this.store = store;
|
this.formulaStore = formulaStore == null ? FormulaStore.none() : formulaStore;
|
this.auditRecorder = auditRecorder;
|
}
|
|
public List<ElnTemplateVos.Diagnostic> reconcileExpired(String tenantId, String templateId,
|
Date now, List<DocxControlSnapshot> controls) {
|
if (!StringUtils.hasText(tenantId) || !StringUtils.hasText(templateId)
|
|| now == null || controls == null) {
|
throw new DataException("PENDING 绑定对账参数无效");
|
}
|
Map<String, List<DocxControlSnapshot>> byTag = new LinkedHashMap<>();
|
for (DocxControlSnapshot control : controls) {
|
if (control != null && StringUtils.hasText(control.getTag())) {
|
byTag.computeIfAbsent(control.getTag(), key -> new ArrayList<>()).add(control);
|
}
|
}
|
List<ElnTemplateVos.Diagnostic> result = new ArrayList<>();
|
for (ElnTemplateBindingEntity binding
|
: store.listExpired(tenantId.trim(), templateId.trim(), now)) {
|
if (binding == null || !tenantId.trim().equals(binding.getFTenantId())
|
|| !templateId.trim().equals(binding.getMubanId())
|
|| !PENDING.equals(binding.getBangdingZhuangtai())
|
|| binding.getDaoqiShijian() == null
|
|| !binding.getDaoqiShijian().before(now)) {
|
continue;
|
}
|
List<DocxControlSnapshot> matches = byTag.get(binding.getKongjianBiaoqian());
|
int count = matches == null ? 0 : matches.size();
|
if (count == 0) {
|
if (binding.getFVersion() != null && store.cancelIfPending(
|
binding, binding.getFVersion(), now, TIMEOUT_REASON)) {
|
result.add(diagnostic("INFO", TIMEOUT_REASON, binding,
|
"超时待确认绑定未在 DOCX 中产生控件,已取消"));
|
auditCancellation(binding);
|
}
|
continue;
|
}
|
if (count > 1) {
|
result.add(diagnostic("ERROR", "DUPLICATE_TAG", binding,
|
"待确认控件 Tag 在 DOCX 中出现多次"));
|
continue;
|
}
|
DocxControlSnapshot control = matches.get(0);
|
if (!Objects.equals(binding.getKongjianJuese(), control.getControlRole())) {
|
result.add(diagnostic("ERROR", "ROLE_MISMATCH", binding,
|
"待确认控件角色与预留绑定不一致"));
|
} else if (!Objects.equals(binding.getKongjianLeixing(), control.getControlType())) {
|
result.add(diagnostic("ERROR", "TYPE_MISMATCH", binding,
|
"待确认控件类型与预留绑定不一致"));
|
} else if (!DocxControlConfigMatcher.matches(binding.getKongjianLeixing(),
|
store.expectedControlConfig(tenantId, binding),
|
control.getControlConfig())) {
|
result.add(diagnostic("ERROR", "CONFIG_MISMATCH", binding,
|
"待确认控件配置与字段版本不一致"));
|
} else {
|
result.add(diagnostic("WARNING", "PENDING_CONFIRMATION_REQUIRED", binding,
|
"DOCX 已存在控件,请插件按原操作码完成确认"));
|
}
|
}
|
reconcileExpiredFormulaBindings(tenantId.trim(), templateId.trim(), now, byTag, result);
|
return result;
|
}
|
|
private void reconcileExpiredFormulaBindings(String tenantId, String templateId, Date now,
|
Map<String, List<DocxControlSnapshot>> controlsByTag,
|
List<ElnTemplateVos.Diagnostic> diagnostics) {
|
for (ElnTemplateFormulaBindingEntity formula
|
: formulaStore.listExpired(tenantId, templateId, now)) {
|
if (formula == null || !tenantId.equals(formula.getFTenantId())
|
|| !templateId.equals(formula.getMubanId())
|
|| !PENDING.equals(formula.getBangdingZhuangtai())
|
|| formula.getDaoqiShijian() == null
|
|| !formula.getDaoqiShijian().before(now)) continue;
|
List<ElnTemplateBindingEntity> resultControls = formulaStore.resultControls(
|
tenantId, templateId, formula.getId());
|
ElnTemplateBindingEntity primary = null;
|
for (ElnTemplateBindingEntity field : safe(resultControls)) {
|
if (formula.getJieguoBangdingId().equals(field.getId())
|
&& "PRIMARY".equals(field.getKongjianJuese())) primary = field;
|
}
|
List<DocxControlSnapshot> primaryMatches = primary == null ? null
|
: controlsByTag.get(primary.getKongjianBiaoqian());
|
if (primary == null || primaryMatches == null || primaryMatches.size() != 1) {
|
diagnostics.add(formulaDiagnostic("ERROR", "FORMULA_RESULT_LOCK_MISMATCH",
|
formula, "公式结果主控件锁状态不完整"));
|
continue;
|
}
|
DocxControlSnapshot primaryControl = primaryMatches.get(0);
|
if (!StringUtils.hasText(primaryControl.getLock())) {
|
boolean allEditable = true;
|
for (ElnTemplateBindingEntity field : safe(resultControls)) {
|
List<DocxControlSnapshot> matches = controlsByTag.get(field.getKongjianBiaoqian());
|
allEditable &= matches != null && matches.size() == 1
|
&& !StringUtils.hasText(matches.get(0).getLock());
|
}
|
if (allEditable) {
|
ElnTemplateFormulaBindingEntity before = copyFormulaBinding(formula);
|
if (formula.getFVersion() != null && formulaStore.cancelIfPending(
|
formula, formula.getFVersion(), now, FORMULA_TIMEOUT_REASON)) {
|
diagnostics.add(formulaDiagnostic("INFO", FORMULA_TIMEOUT_REASON, formula,
|
"公式结果主控件仍可编辑,超时映射已取消"));
|
auditFormulaCancellation(before);
|
}
|
} else {
|
diagnostics.add(formulaDiagnostic("ERROR", "FORMULA_RESULT_LOCK_MISMATCH",
|
formula, "公式结果控件锁状态不完整"));
|
}
|
continue;
|
}
|
boolean completeReadOnly = "sdtContentLocked".equals(primaryControl.getLock());
|
for (ElnTemplateBindingEntity field : safe(resultControls)) {
|
List<DocxControlSnapshot> matches = controlsByTag.get(field.getKongjianBiaoqian());
|
completeReadOnly &= matches != null && matches.size() == 1
|
&& "sdtContentLocked".equals(matches.get(0).getLock());
|
}
|
diagnostics.add(formulaDiagnostic(completeReadOnly ? "WARNING" : "ERROR",
|
completeReadOnly ? "PENDING_CONFIRMATION_REQUIRED"
|
: "FORMULA_RESULT_LOCK_MISMATCH",
|
formula, completeReadOnly ? "公式结果控件已只读,请插件继续确认"
|
: "公式结果控件锁状态不完整"));
|
}
|
}
|
|
private static <T> List<T> safe(List<T> values) {
|
return values == null ? java.util.Collections.emptyList() : values;
|
}
|
|
private void auditFormulaCancellation(ElnTemplateFormulaBindingEntity before) {
|
ElnTemplateFormulaBindingEntity after = copyFormulaBinding(before);
|
after.setBangdingZhuangtai(CANCELLED);
|
after.setZhuangtaiYuanyin(FORMULA_TIMEOUT_REASON);
|
if (after.getFVersion() != null) after.setFVersion(after.getFVersion() + 1);
|
Map<String, Object> snapshot = new LinkedHashMap<>();
|
snapshot.put("templateId", before.getMubanId());
|
snapshot.put("formulaDefinitionId", before.getGongshiDingyiId());
|
snapshot.put("formulaVersionId", before.getGongshiBanbenId());
|
snapshot.put("parameterBindings", before.getCanshuYingshe());
|
snapshot.put("resultBindingId", before.getJieguoBangdingId());
|
snapshot.put("operationCode", before.getCaozuoBianma());
|
snapshot.put("reason", FORMULA_TIMEOUT_REASON);
|
snapshot.put("before", before);
|
snapshot.put("after", after);
|
snapshot.put("beforeStatus", PENDING);
|
snapshot.put("afterStatus", CANCELLED);
|
auditRecorder.accept(AuditEventDTO.builder().tenantId(before.getFTenantId())
|
.eventTime(new Date()).eventType("DATA_CHANGE")
|
.actionCode("ELN_TEMPLATE_FORMULA_CANCEL")
|
.actionLabel("取消超时待确认模板公式映射")
|
.bizModule("eln").bizType("eln_template_formula_binding")
|
.targetTable("eln_template_formula_binding").targetId(before.getId())
|
.reason(FORMULA_TIMEOUT_REASON)
|
.dataSnapshot(JsonUtil.getObjectToString(snapshot)).build());
|
}
|
|
private static ElnTemplateFormulaBindingEntity copyFormulaBinding(
|
ElnTemplateFormulaBindingEntity binding) {
|
return JsonUtil.getJsonToBean(JsonUtil.getObjectToString(binding),
|
ElnTemplateFormulaBindingEntity.class);
|
}
|
|
private static ElnTemplateVos.Diagnostic formulaDiagnostic(String severity, String code,
|
ElnTemplateFormulaBindingEntity binding, String message) {
|
ElnTemplateVos.Diagnostic result = new ElnTemplateVos.Diagnostic();
|
result.setSeverity(severity); result.setCode(code); result.setMessage(message);
|
result.setBindingId(binding.getId());
|
return result;
|
}
|
|
private void auditCancellation(ElnTemplateBindingEntity binding) {
|
Map<String, Object> snapshot = new LinkedHashMap<>();
|
snapshot.put("templateId", binding.getMubanId());
|
snapshot.put("bindingId", binding.getId());
|
snapshot.put("reason", TIMEOUT_REASON);
|
snapshot.put("beforeStatus", PENDING);
|
snapshot.put("afterStatus", CANCELLED);
|
auditRecorder.accept(AuditEventDTO.builder()
|
.tenantId(binding.getFTenantId())
|
.eventTime(new Date())
|
.eventType("DATA_CHANGE")
|
.actionCode("ELN_TEMPLATE_BINDING_CANCEL")
|
.actionLabel("取消超时待确认模板绑定")
|
.bizModule("eln")
|
.bizType("eln_template_binding")
|
.targetTable("eln_template_binding")
|
.targetId(binding.getId())
|
.reason(TIMEOUT_REASON)
|
.dataSnapshot(JsonUtil.getObjectToString(snapshot))
|
.build());
|
}
|
|
private static ElnTemplateVos.Diagnostic diagnostic(String severity, String code,
|
ElnTemplateBindingEntity binding, String message) {
|
ElnTemplateVos.Diagnostic result = new ElnTemplateVos.Diagnostic();
|
result.setSeverity(severity);
|
result.setCode(code);
|
result.setMessage(message);
|
result.setBindingId(binding.getId());
|
result.setControlTag(binding.getKongjianBiaoqian());
|
return result;
|
}
|
|
public interface Store {
|
List<ElnTemplateBindingEntity> listExpired(
|
String tenantId, String templateId, Date now);
|
|
Map<String, Object> expectedControlConfig(
|
String tenantId, ElnTemplateBindingEntity binding);
|
|
boolean cancelIfPending(ElnTemplateBindingEntity binding, int expectedVersion,
|
Date now, String reason);
|
}
|
|
public interface FormulaStore {
|
List<ElnTemplateFormulaBindingEntity> listExpired(
|
String tenantId, String templateId, Date now);
|
List<ElnTemplateBindingEntity> resultControls(
|
String tenantId, String templateId, String formulaBindingId);
|
boolean cancelIfPending(ElnTemplateFormulaBindingEntity binding,
|
int expectedVersion, Date now, String reason);
|
static FormulaStore none() {
|
return new FormulaStore() {
|
@Override public List<ElnTemplateFormulaBindingEntity> listExpired(
|
String tenantId, String templateId, Date now) {
|
return java.util.Collections.emptyList();
|
}
|
@Override public List<ElnTemplateBindingEntity> resultControls(
|
String tenantId, String templateId, String formulaBindingId) {
|
return java.util.Collections.emptyList();
|
}
|
@Override public boolean cancelIfPending(ElnTemplateFormulaBindingEntity binding,
|
int expectedVersion, Date now, String reason) { return false; }
|
};
|
}
|
}
|
|
private static final class MyBatisStore implements Store {
|
private final ElnTemplateBindingMapper bindingMapper;
|
private final ElnFieldVersionMapper fieldVersionMapper;
|
|
private MyBatisStore(ElnTemplateBindingMapper bindingMapper,
|
ElnFieldVersionMapper fieldVersionMapper) {
|
this.bindingMapper = bindingMapper;
|
this.fieldVersionMapper = fieldVersionMapper;
|
}
|
|
@Override
|
public List<ElnTemplateBindingEntity> listExpired(
|
String tenantId, String templateId, Date now) {
|
return bindingMapper.selectList(Wrappers.<ElnTemplateBindingEntity>lambdaQuery()
|
.eq(ElnTemplateBindingEntity::getFTenantId, tenantId)
|
.eq(ElnTemplateBindingEntity::getMubanId, templateId)
|
.eq(ElnTemplateBindingEntity::getBangdingZhuangtai, PENDING)
|
.isNotNull(ElnTemplateBindingEntity::getDaoqiShijian)
|
.lt(ElnTemplateBindingEntity::getDaoqiShijian, now)
|
.and(q -> q.isNull(ElnTemplateBindingEntity::getFDeleteMark)
|
.or().eq(ElnTemplateBindingEntity::getFDeleteMark, 0))
|
.orderByAsc(ElnTemplateBindingEntity::getId));
|
}
|
|
@Override
|
public Map<String, Object> expectedControlConfig(
|
String tenantId, ElnTemplateBindingEntity binding) {
|
ElnFieldVersionEntity version = fieldVersionMapper.selectOne(
|
Wrappers.<ElnFieldVersionEntity>lambdaQuery()
|
.eq(ElnFieldVersionEntity::getFTenantId, tenantId)
|
.eq(ElnFieldVersionEntity::getId, binding.getZiduanBanbenId())
|
.and(q -> q.isNull(ElnFieldVersionEntity::getFDeleteMark)
|
.or().eq(ElnFieldVersionEntity::getFDeleteMark, 0)));
|
if (version == null
|
|| !Objects.equals(binding.getZiduanDingyiId(), version.getZiduanDingyiId())) {
|
throw new DataException("待确认绑定的字段版本不存在或不属于当前租户");
|
}
|
return DocxExpectedControlConfig.from(
|
version.getShujuLeixing(), version.getKongjianPeizhi());
|
}
|
|
@Override
|
public boolean cancelIfPending(ElnTemplateBindingEntity binding, int expectedVersion,
|
Date now, String reason) {
|
return bindingMapper.update(null,
|
Wrappers.<ElnTemplateBindingEntity>lambdaUpdate()
|
.eq(ElnTemplateBindingEntity::getFTenantId, binding.getFTenantId())
|
.eq(ElnTemplateBindingEntity::getMubanId, binding.getMubanId())
|
.eq(ElnTemplateBindingEntity::getId, binding.getId())
|
.eq(ElnTemplateBindingEntity::getFVersion, expectedVersion)
|
.eq(ElnTemplateBindingEntity::getBangdingZhuangtai, PENDING)
|
.isNotNull(ElnTemplateBindingEntity::getDaoqiShijian)
|
.lt(ElnTemplateBindingEntity::getDaoqiShijian, now)
|
.and(q -> q.isNull(ElnTemplateBindingEntity::getFDeleteMark)
|
.or().eq(ElnTemplateBindingEntity::getFDeleteMark, 0))
|
.set(ElnTemplateBindingEntity::getBangdingZhuangtai, CANCELLED)
|
.set(ElnTemplateBindingEntity::getZhuangtaiYuanyin, reason)
|
.set(ElnTemplateBindingEntity::getFVersion, expectedVersion + 1)) == 1;
|
}
|
}
|
|
private static final class MyBatisFormulaStore implements FormulaStore {
|
private final ElnTemplateFormulaBindingMapper formulaMapper;
|
private final ElnTemplateBindingMapper fieldMapper;
|
private MyBatisFormulaStore(ElnTemplateFormulaBindingMapper formulaMapper,
|
ElnTemplateBindingMapper fieldMapper) {
|
this.formulaMapper = formulaMapper; this.fieldMapper = fieldMapper;
|
}
|
@Override public List<ElnTemplateFormulaBindingEntity> listExpired(
|
String tenantId, String templateId, Date now) {
|
return formulaMapper.selectList(Wrappers.<ElnTemplateFormulaBindingEntity>lambdaQuery()
|
.eq(ElnTemplateFormulaBindingEntity::getFTenantId, tenantId)
|
.eq(ElnTemplateFormulaBindingEntity::getMubanId, templateId)
|
.eq(ElnTemplateFormulaBindingEntity::getBangdingZhuangtai, PENDING)
|
.isNotNull(ElnTemplateFormulaBindingEntity::getDaoqiShijian)
|
.lt(ElnTemplateFormulaBindingEntity::getDaoqiShijian, now)
|
.and(q -> q.isNull(ElnTemplateFormulaBindingEntity::getFDeleteMark)
|
.or().eq(ElnTemplateFormulaBindingEntity::getFDeleteMark, 0)));
|
}
|
@Override public List<ElnTemplateBindingEntity> resultControls(
|
String tenantId, String templateId, String formulaBindingId) {
|
ElnTemplateFormulaBindingEntity formula = formulaMapper.selectOne(
|
Wrappers.<ElnTemplateFormulaBindingEntity>lambdaQuery()
|
.eq(ElnTemplateFormulaBindingEntity::getFTenantId, tenantId)
|
.eq(ElnTemplateFormulaBindingEntity::getMubanId, templateId)
|
.eq(ElnTemplateFormulaBindingEntity::getId, formulaBindingId));
|
if (formula == null) return java.util.Collections.emptyList();
|
return fieldMapper.selectList(Wrappers.<ElnTemplateBindingEntity>lambdaQuery()
|
.eq(ElnTemplateBindingEntity::getFTenantId, tenantId)
|
.eq(ElnTemplateBindingEntity::getMubanId, templateId)
|
.eq(ElnTemplateBindingEntity::getBangdingZhuangtai, "ACTIVE")
|
.and(q -> q.eq(ElnTemplateBindingEntity::getId, formula.getJieguoBangdingId())
|
.or().eq(ElnTemplateBindingEntity::getYinyongShiliId,
|
formula.getJieguoBangdingId()))
|
.and(q -> q.isNull(ElnTemplateBindingEntity::getFDeleteMark)
|
.or().eq(ElnTemplateBindingEntity::getFDeleteMark, 0)));
|
}
|
@Override public boolean cancelIfPending(ElnTemplateFormulaBindingEntity binding,
|
int expectedVersion, Date now, String reason) {
|
return formulaMapper.update(null,
|
Wrappers.<ElnTemplateFormulaBindingEntity>lambdaUpdate()
|
.eq(ElnTemplateFormulaBindingEntity::getFTenantId, binding.getFTenantId())
|
.eq(ElnTemplateFormulaBindingEntity::getMubanId, binding.getMubanId())
|
.eq(ElnTemplateFormulaBindingEntity::getId, binding.getId())
|
.eq(ElnTemplateFormulaBindingEntity::getFVersion, expectedVersion)
|
.eq(ElnTemplateFormulaBindingEntity::getBangdingZhuangtai, PENDING)
|
.isNotNull(ElnTemplateFormulaBindingEntity::getDaoqiShijian)
|
.lt(ElnTemplateFormulaBindingEntity::getDaoqiShijian, now)
|
.and(q -> q.isNull(ElnTemplateFormulaBindingEntity::getFDeleteMark)
|
.or().eq(ElnTemplateFormulaBindingEntity::getFDeleteMark, 0))
|
.set(ElnTemplateFormulaBindingEntity::getBangdingZhuangtai, CANCELLED)
|
.set(ElnTemplateFormulaBindingEntity::getZhuangtaiYuanyin, reason)
|
.set(ElnTemplateFormulaBindingEntity::getFVersion, expectedVersion + 1)) == 1;
|
}
|
}
|
}
|