package jnpf.elnService.impl; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import jnpf.audit.model.AuditEventDTO; import jnpf.audit.sdk.AuditClient; import jnpf.database.util.TenantDataSourceUtil; import jnpf.elnEntity.runtime.ElnRecordEntity; import jnpf.elnEntity.runtime.ElnRecordForms; import jnpf.elnEntity.runtime.ElnRecordRepeatRowEntity; import jnpf.elnEntity.runtime.ElnRecordRevisionEntity; import jnpf.elnEntity.runtime.ElnRecordVos; import jnpf.elnEntity.template.ElnTemplateVos; import jnpf.elnMapper.ElnRecordMapper; import jnpf.elnMapper.ElnRecordRepeatRowMapper; import jnpf.elnMapper.ElnRecordRevisionMapper; import jnpf.elnService.ElnRecordFillAccessService; import jnpf.elnService.ElnRecordRepeatService; import jnpf.elnService.ElnRecordFillTicketService; import jnpf.elnService.template.ElnTemplateRepeatGroupService; import jnpf.exception.DataException; import jnpf.util.RandomUtil; import jnpf.util.JsonUtil; import org.springframework.dao.DuplicateKeyException; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.support.TransactionTemplate; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; 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 ElnRecordRepeatServiceImpl implements ElnRecordRepeatService { private static final String PREPARED = "PREPARED"; private static final String ACTIVE = "ACTIVE"; private static final String DELETE_PREPARED = "DELETE_PREPARED"; private static final int MAX_OPERATION_CODE_LENGTH = 64; private static final int MAX_DELETE_REASON_LENGTH = 500; private final ElnRecordMapper recordMapper; private final ElnRecordRevisionMapper revisionMapper; private final ElnRecordRepeatRowMapper rowMapper; private final ElnTemplateRepeatGroupService repeatGroupService; private final ElnRecordFillAccessService accessService; private final Consumer auditRecorder; private final TransactionTemplate transactionTemplate; private final TenantRoute tenantRoute; @Autowired public ElnRecordRepeatServiceImpl(ElnRecordMapper recordMapper, ElnRecordRevisionMapper revisionMapper, ElnRecordRepeatRowMapper rowMapper, ElnTemplateRepeatGroupService repeatGroupService, ElnRecordFillAccessService accessService, PlatformTransactionManager transactionManager, ObjectProvider auditClientProvider) { this(recordMapper, revisionMapper, rowMapper, repeatGroupService, accessService, transactionManager, event -> { AuditClient client = auditClientProvider.getIfAvailable(); if (client != null) client.record(event); }, new TenantRoute() { @Override public void switchTo(String tenantId) { TenantDataSourceUtil.switchTenant(tenantId); } @Override public void clear() { TenantDataSourceUtil.clearLocalTenantInfo(); } }); } ElnRecordRepeatServiceImpl(ElnRecordMapper recordMapper, ElnRecordRevisionMapper revisionMapper, ElnRecordRepeatRowMapper rowMapper, ElnTemplateRepeatGroupService repeatGroupService, ElnRecordFillAccessService accessService, PlatformTransactionManager transactionManager, Consumer auditRecorder, TenantRoute tenantRoute) { this.recordMapper = recordMapper; this.revisionMapper = revisionMapper; this.rowMapper = rowMapper; this.repeatGroupService = repeatGroupService; this.accessService = accessService; this.auditRecorder = auditRecorder; this.transactionTemplate = new TransactionTemplate(transactionManager); this.tenantRoute = tenantRoute; } @Override public ElnRecordVos.RepeatRow append(ElnRecordFillTicketService.FillTicketContext ticket, ElnRecordForms.RepeatAppend form) { requireRequest(ticket, form); if (!accessService.canFill(ticket.getTenantId(), ticket.getUserId(), ticket.getBizDataId(), ticket.getFileId())) { throw new DataException("当前用户或记录状态不允许新增重复行"); } try { tenantRoute.switchTo(ticket.getTenantId()); return transactionTemplate.execute(status -> appendInTenant(ticket, form)); } finally { tenantRoute.clear(); } } private ElnRecordVos.RepeatRow appendInTenant( ElnRecordFillTicketService.FillTicketContext ticket, ElnRecordForms.RepeatAppend form) { String operationCode = form.getOperationCode().trim(); String groupCode = form.getGroupCode().trim(); Current current = lockCurrent(ticket); ElnRecordEntity record = current.record; ElnRecordRevisionEntity revision = current.revision; ElnTemplateVos.RepeatGroup group = requireTemplateGroup( ticket.getTenantId(), record.getMubanId(), groupCode); ElnRecordRepeatRowEntity retried = findByOperation( ticket.getTenantId(), revision.getId(), operationCode); if (retried != null) { if (!Objects.equals(groupCode, retried.getRepeatGroupCode())) { throw new DataException("重复行操作码已用于其他请求"); } return toRow(retried); } Long allocated = rowMapper.selectCount( Wrappers.lambdaQuery() .eq(ElnRecordRepeatRowEntity::getFTenantId, ticket.getTenantId()) .eq(ElnRecordRepeatRowEntity::getDianziJiluId, record.getId()) .eq(ElnRecordRepeatRowEntity::getXiudingId, revision.getId()) .eq(ElnRecordRepeatRowEntity::getRepeatGroupCode, groupCode) .in(ElnRecordRepeatRowEntity::getOperationStatus, ACTIVE, PREPARED, DELETE_PREPARED) .and(q -> q.isNull(ElnRecordRepeatRowEntity::getFDeleteMark) .or().eq(ElnRecordRepeatRowEntity::getFDeleteMark, 0))); if (allocated != null && allocated >= group.getMaxRows()) { throw new DataException("重复组已达到最大行数 " + group.getMaxRows()); } ElnRecordRepeatRowEntity last = rowMapper.selectOne( Wrappers.lambdaQuery() .eq(ElnRecordRepeatRowEntity::getFTenantId, ticket.getTenantId()) .eq(ElnRecordRepeatRowEntity::getXiudingId, revision.getId()) .eq(ElnRecordRepeatRowEntity::getRepeatGroupCode, groupCode) .orderByDesc(ElnRecordRepeatRowEntity::getRowOrder) .last("limit 1")); String rowId = RandomUtil.uuId(); ElnRecordRepeatRowEntity created = new ElnRecordRepeatRowEntity(); created.setId(RandomUtil.uuId()); created.setRowId(rowId); created.setDianziJiluId(record.getId()); created.setXiudingId(revision.getId()); created.setMubanId(record.getMubanId()); created.setRepeatGroupCode(groupCode); created.setRowOrder(last == null ? 1 : last.getRowOrder() + 1); created.setOperationCode(operationCode); created.setOperationStatus(PREPARED); created.setFTenantId(ticket.getTenantId()); created.setFVersion(0); created.setFCreatorUserId(ticket.getUserId()); created.setFCreatorTime(new Date()); created.setFLastModifyUserId(ticket.getUserId()); created.setFLastModifyTime(new Date()); created.setFDeleteMark(0); try { if (rowMapper.insert(created) != 1) { throw new DataException("重复行身份创建失败"); } } catch (DuplicateKeyException e) { ElnRecordRepeatRowEntity concurrent = findByOperation( ticket.getTenantId(), revision.getId(), operationCode); if (concurrent == null || !Objects.equals(groupCode, concurrent.getRepeatGroupCode())) { throw new DataException("重复行新增发生并发冲突"); } return toRow(concurrent); } audit(ticket, record, created, created.getOperationCode(), "ELN_RECORD_REPEAT_APPEND_PREPARE", "分配电子记录重复行"); return toRow(created); } @Override public ElnRecordVos.RepeatRow requestDelete( ElnRecordFillTicketService.FillTicketContext ticket, ElnRecordForms.RepeatDelete form) { requireDeleteRequest(ticket, form); if (!accessService.canFill(ticket.getTenantId(), ticket.getUserId(), ticket.getBizDataId(), ticket.getFileId())) { throw new DataException("当前用户或记录状态不允许删除重复行"); } try { tenantRoute.switchTo(ticket.getTenantId()); return transactionTemplate.execute(status -> requestDeleteInTenant(ticket, form)); } finally { tenantRoute.clear(); } } private ElnRecordVos.RepeatRow requestDeleteInTenant( ElnRecordFillTicketService.FillTicketContext ticket, ElnRecordForms.RepeatDelete form) { String groupCode = form.getGroupCode().trim(); String rowId = form.getRowId().trim(); String operationCode = form.getOperationCode().trim(); String reason = form.getReason().trim(); Current current = lockCurrent(ticket); ElnTemplateVos.RepeatGroup group = requireTemplateGroup(ticket.getTenantId(), current.record.getMubanId(), groupCode); ElnRecordRepeatRowEntity replay = findByDeleteOperation(ticket.getTenantId(), current.revision.getId(), operationCode); if (replay != null) { if (!Objects.equals(groupCode, replay.getRepeatGroupCode()) || !Objects.equals(rowId, replay.getRowId()) || !Objects.equals(reason, replay.getDeleteReason())) { throw new DataException("重复行删除操作码已用于其他请求"); } return toRow(replay); } ElnRecordRepeatRowEntity row = findRow(ticket.getTenantId(), current.revision.getId(), groupCode, rowId); if (row == null || !Objects.equals(current.record.getId(), row.getDianziJiluId()) || !Objects.equals(current.record.getMubanId(), row.getMubanId())) { throw new DataException("目标重复行不存在"); } if (DELETE_PREPARED.equals(row.getOperationStatus())) { throw new DataException("重复行已有待完成的删除意图"); } if (!ACTIVE.equals(row.getOperationStatus())) { throw new DataException("只有已激活的重复行可以删除"); } Long activeRows = rowMapper.selectCount( Wrappers.lambdaQuery() .eq(ElnRecordRepeatRowEntity::getFTenantId, ticket.getTenantId()) .eq(ElnRecordRepeatRowEntity::getDianziJiluId, current.record.getId()) .eq(ElnRecordRepeatRowEntity::getXiudingId, current.revision.getId()) .eq(ElnRecordRepeatRowEntity::getRepeatGroupCode, groupCode) .eq(ElnRecordRepeatRowEntity::getOperationStatus, ACTIVE) .and(q -> q.isNull(ElnRecordRepeatRowEntity::getFDeleteMark) .or().eq(ElnRecordRepeatRowEntity::getFDeleteMark, 0))); if (activeRows == null || activeRows - 1 < group.getMinRows()) { throw new DataException("重复组至少保留 " + group.getMinRows() + " 行"); } Date now = new Date(); row.setOperationStatus(DELETE_PREPARED); row.setDeleteOperationCode(operationCode); row.setDeleteReason(reason); row.setDeleteRequestedUserId(ticket.getUserId()); row.setDeleteRequestedTime(now); row.setFLastModifyUserId(ticket.getUserId()); row.setFLastModifyTime(now); if (rowMapper.updateById(row) != 1) { throw new DataException("重复行删除意图登记冲突"); } audit(ticket, current.record, row, operationCode, "ELN_RECORD_REPEAT_DELETE_PREPARE", "登记电子记录重复行删除意图"); return toRow(row); } @Override public ElnRecordVos.RepeatRow restoreDelete( ElnRecordFillTicketService.FillTicketContext ticket, ElnRecordForms.RepeatRestore form) { requireRestoreRequest(ticket, form); if (!accessService.canFill(ticket.getTenantId(), ticket.getUserId(), ticket.getBizDataId(), ticket.getFileId())) { throw new DataException("当前用户或记录状态不允许恢复重复行"); } try { tenantRoute.switchTo(ticket.getTenantId()); return transactionTemplate.execute(status -> restoreDeleteInTenant(ticket, form)); } finally { tenantRoute.clear(); } } private ElnRecordVos.RepeatRow restoreDeleteInTenant( ElnRecordFillTicketService.FillTicketContext ticket, ElnRecordForms.RepeatRestore form) { String groupCode = form.getGroupCode().trim(); String rowId = form.getRowId().trim(); String operationCode = form.getOperationCode().trim(); Current current = lockCurrent(ticket); requireTemplateGroup(ticket.getTenantId(), current.record.getMubanId(), groupCode); ElnRecordRepeatRowEntity row = findRow(ticket.getTenantId(), current.revision.getId(), groupCode, rowId); if (row == null || !Objects.equals(current.record.getId(), row.getDianziJiluId()) || !Objects.equals(current.record.getMubanId(), row.getMubanId())) { throw new DataException("目标重复行不存在"); } if (ACTIVE.equals(row.getOperationStatus())) return toRow(row); if (!DELETE_PREPARED.equals(row.getOperationStatus()) || !Objects.equals(operationCode, row.getDeleteOperationCode())) { throw new DataException("重复行删除恢复操作码不匹配"); } row.setOperationStatus(ACTIVE); row.setDeleteOperationCode(null); row.setDeleteReason(null); row.setDeleteRequestedUserId(null); row.setDeleteRequestedTime(null); row.setFLastModifyUserId(ticket.getUserId()); row.setFLastModifyTime(new Date()); if (rowMapper.updateById(row) != 1) { throw new DataException("重复行删除恢复冲突"); } audit(ticket, current.record, row, operationCode, "ELN_RECORD_REPEAT_DELETE_RESTORE", "恢复电子记录重复行删除意图"); return toRow(row); } private Current lockCurrent(ElnRecordFillTicketService.FillTicketContext ticket) { ElnRecordEntity record = recordMapper.selectOne( Wrappers.lambdaQuery() .eq(ElnRecordEntity::getFTenantId, ticket.getTenantId()) .eq(ElnRecordEntity::getId, ticket.getBizDataId()) .and(q -> q.isNull(ElnRecordEntity::getFDeleteMark) .or().eq(ElnRecordEntity::getFDeleteMark, 0)) .last("for update")); if (record == null || !StringUtils.hasText(record.getDangqianXiudingId())) { throw new DataException("电子记录不存在或当前修订无效"); } ElnRecordRevisionEntity revision = revisionMapper.selectOne( Wrappers.lambdaQuery() .eq(ElnRecordRevisionEntity::getFTenantId, ticket.getTenantId()) .eq(ElnRecordRevisionEntity::getId, record.getDangqianXiudingId()) .eq(ElnRecordRevisionEntity::getDianziJiluId, record.getId()) .eq(ElnRecordRevisionEntity::getJiluWendang, ticket.getFileId()) .eq(ElnRecordRevisionEntity::getBizStatus, "DRAFT") .and(q -> q.isNull(ElnRecordRevisionEntity::getFDeleteMark) .or().eq(ElnRecordRevisionEntity::getFDeleteMark, 0)) .last("limit 1")); if (revision == null) { throw new DataException("电子记录当前修订与文件不匹配"); } return new Current(record, revision); } private ElnTemplateVos.RepeatGroup requireTemplateGroup(String tenantId, String templateId, String requestedGroup) { List groups = repeatGroupService.list(tenantId, templateId); for (ElnTemplateVos.RepeatGroup group : groups) { if (Objects.equals(requestedGroup, group.getGroupCode())) return group; } throw new DataException("模板没有目标重复组配置"); } private void audit(ElnRecordFillTicketService.FillTicketContext ticket, ElnRecordEntity record, ElnRecordRepeatRowEntity row, String operationCode, String actionCode, String actionLabel) { Map snapshot = new LinkedHashMap<>(); snapshot.put("groupCode", row.getRepeatGroupCode()); snapshot.put("rowId", row.getRowId()); snapshot.put("rowOrder", row.getRowOrder()); snapshot.put("status", row.getOperationStatus()); snapshot.put("deleteReason", row.getDeleteReason()); auditRecorder.accept(AuditEventDTO.builder() .clientEventId(ElnRepeatAuditEventIds.of(operationCode, actionCode)) .operationId(operationCode) .eventTime(new Date()) .tenantId(ticket.getTenantId()) .operatorId(ticket.getUserId()) .eventType("DATA_CHANGE") .actionCode(actionCode) .actionLabel(actionLabel) .bizModule("eln") .bizType("eln_record_repeat_row") .bizCode(record.getJiluBianhao()) .targetTable("eln_record_repeat_row") .targetId(row.getId()) .dataSnapshot(JsonUtil.getObjectToString(snapshot)) .build()); } private ElnRecordRepeatRowEntity findByOperation( String tenantId, String revisionId, String operationCode) { return rowMapper.selectOne(Wrappers.lambdaQuery() .eq(ElnRecordRepeatRowEntity::getFTenantId, tenantId) .eq(ElnRecordRepeatRowEntity::getXiudingId, revisionId) .eq(ElnRecordRepeatRowEntity::getOperationCode, operationCode) .and(q -> q.isNull(ElnRecordRepeatRowEntity::getFDeleteMark) .or().eq(ElnRecordRepeatRowEntity::getFDeleteMark, 0)) .last("limit 1")); } private ElnRecordRepeatRowEntity findByDeleteOperation( String tenantId, String revisionId, String operationCode) { return rowMapper.selectOne(Wrappers.lambdaQuery() .eq(ElnRecordRepeatRowEntity::getFTenantId, tenantId) .eq(ElnRecordRepeatRowEntity::getXiudingId, revisionId) .eq(ElnRecordRepeatRowEntity::getDeleteOperationCode, operationCode) .last("limit 1")); } private ElnRecordRepeatRowEntity findRow(String tenantId, String revisionId, String groupCode, String rowId) { return rowMapper.selectOne(Wrappers.lambdaQuery() .eq(ElnRecordRepeatRowEntity::getFTenantId, tenantId) .eq(ElnRecordRepeatRowEntity::getXiudingId, revisionId) .eq(ElnRecordRepeatRowEntity::getRepeatGroupCode, groupCode) .eq(ElnRecordRepeatRowEntity::getRowId, rowId) .and(q -> q.isNull(ElnRecordRepeatRowEntity::getFDeleteMark) .or().eq(ElnRecordRepeatRowEntity::getFDeleteMark, 0)) .last("limit 1")); } private static void requireRequest(ElnRecordFillTicketService.FillTicketContext ticket, ElnRecordForms.RepeatAppend form) { if (ticket == null || !ElnRecordFillTicketService.BIZ_MODULE.equals(ticket.getBizModule()) || form == null || !StringUtils.hasText(form.getOperationCode()) || !StringUtils.hasText(form.getGroupCode())) { throw new DataException("重复行新增请求无效"); } if (form.getOperationCode().trim().length() > MAX_OPERATION_CODE_LENGTH) { throw new DataException("重复行操作码超过 64 个字符"); } if (!form.getGroupCode().trim().matches("[A-Za-z0-9_-]{1,80}")) { throw new DataException("重复组编码无效"); } } private static void requireDeleteRequest( ElnRecordFillTicketService.FillTicketContext ticket, ElnRecordForms.RepeatDelete form) { if (form == null || !StringUtils.hasText(form.getReason())) { throw new DataException("重复行删除请求无效"); } requireRowRequest(ticket, form.getOperationCode(), form.getGroupCode(), form.getRowId(), "重复行删除请求无效"); if (form.getReason().trim().length() > MAX_DELETE_REASON_LENGTH) { throw new DataException("重复行删除原因超过 500 个字符"); } } private static void requireRestoreRequest( ElnRecordFillTicketService.FillTicketContext ticket, ElnRecordForms.RepeatRestore form) { if (form == null) throw new DataException("重复行删除恢复请求无效"); requireRowRequest(ticket, form.getOperationCode(), form.getGroupCode(), form.getRowId(), "重复行删除恢复请求无效"); } private static void requireRowRequest( ElnRecordFillTicketService.FillTicketContext ticket, String operationCode, String groupCode, String rowId, String invalidMessage) { if (ticket == null || !ElnRecordFillTicketService.BIZ_MODULE.equals(ticket.getBizModule()) || !StringUtils.hasText(operationCode) || !StringUtils.hasText(groupCode) || !StringUtils.hasText(rowId)) { throw new DataException(invalidMessage); } if (operationCode.trim().length() > MAX_OPERATION_CODE_LENGTH) { throw new DataException("重复行操作码超过 64 个字符"); } if (!groupCode.trim().matches("[A-Za-z0-9_-]{1,80}") || !rowId.trim().matches("[A-Za-z0-9_-]{1,80}") || "prototype".equals(rowId.trim())) { throw new DataException("重复行身份无效"); } } private static ElnRecordVos.RepeatRow toRow(ElnRecordRepeatRowEntity source) { ElnRecordVos.RepeatRow result = new ElnRecordVos.RepeatRow(); result.setRowId(source.getRowId()); result.setGroupCode(source.getRepeatGroupCode()); result.setRowOrder(source.getRowOrder()); result.setStatus(source.getOperationStatus()); result.setDeleteOperationCode(source.getDeleteOperationCode()); result.setDeleteReason(source.getDeleteReason()); return result; } private static final class Current { private final ElnRecordEntity record; private final ElnRecordRevisionEntity revision; private Current(ElnRecordEntity record, ElnRecordRevisionEntity revision) { this.record = record; this.revision = revision; } } interface TenantRoute { void switchTo(String tenantId); void clear(); } }