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 auditRecorder; @Autowired public ElnPendingBindingCleanupService(ElnTemplateBindingMapper bindingMapper, ElnFieldVersionMapper fieldVersionMapper, ElnTemplateFormulaBindingMapper formulaBindingMapper, ObjectProvider 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 auditRecorder) { this(store, FormulaStore.none(), auditRecorder); } ElnPendingBindingCleanupService(Store store, FormulaStore formulaStore, Consumer auditRecorder) { this.store = store; this.formulaStore = formulaStore == null ? FormulaStore.none() : formulaStore; this.auditRecorder = auditRecorder; } public List reconcileExpired(String tenantId, String templateId, Date now, List controls) { if (!StringUtils.hasText(tenantId) || !StringUtils.hasText(templateId) || now == null || controls == null) { throw new DataException("PENDING 绑定对账参数无效"); } Map> byTag = new LinkedHashMap<>(); for (DocxControlSnapshot control : controls) { if (control != null && StringUtils.hasText(control.getTag())) { byTag.computeIfAbsent(control.getTag(), key -> new ArrayList<>()).add(control); } } List 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 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> controlsByTag, List 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 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 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 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 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 List safe(List 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 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 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 listExpired( String tenantId, String templateId, Date now); Map expectedControlConfig( String tenantId, ElnTemplateBindingEntity binding); boolean cancelIfPending(ElnTemplateBindingEntity binding, int expectedVersion, Date now, String reason); } public interface FormulaStore { List listExpired( String tenantId, String templateId, Date now); List 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 listExpired( String tenantId, String templateId, Date now) { return java.util.Collections.emptyList(); } @Override public List 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 listExpired( String tenantId, String templateId, Date now) { return bindingMapper.selectList(Wrappers.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 expectedControlConfig( String tenantId, ElnTemplateBindingEntity binding) { ElnFieldVersionEntity version = fieldVersionMapper.selectOne( Wrappers.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.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 listExpired( String tenantId, String templateId, Date now) { return formulaMapper.selectList(Wrappers.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 resultControls( String tenantId, String templateId, String formulaBindingId) { ElnTemplateFormulaBindingEntity formula = formulaMapper.selectOne( Wrappers.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.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.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; } } }