package jnpf.elnService.impl; import cn.hutool.core.util.IdUtil; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import jnpf.base.ActionResult; import jnpf.base.ActionResultCode; import jnpf.base.UserInfo; import jnpf.database.util.TenantDataSourceUtil; import jnpf.elnEntity.ElnDocumentFileVo; import jnpf.elnEntity.field.ElnFieldForms; import jnpf.elnEntity.field.ElnFieldVersionEntity; import jnpf.elnEntity.runtime.ElnRecordEntity; import jnpf.elnEntity.runtime.ElnRecordFieldValueEntity; import jnpf.elnEntity.runtime.ElnRecordFormulaCalculationResult; import jnpf.elnEntity.runtime.ElnRecordForms; import jnpf.elnEntity.runtime.ElnRecordRepeatRowEntity; import jnpf.elnEntity.runtime.ElnRecordRepeatValueEntity; import jnpf.elnEntity.runtime.ElnRecordRevisionEntity; import jnpf.elnEntity.runtime.ElnRecordVos; import jnpf.elnMapper.ElnFieldVersionMapper; import jnpf.elnMapper.ElnRecordFieldValueMapper; import jnpf.elnMapper.ElnRecordMapper; import jnpf.elnMapper.ElnRecordRepeatRowMapper; import jnpf.elnMapper.ElnRecordRepeatValueMapper; import jnpf.elnMapper.ElnRecordRevisionMapper; import jnpf.elnService.ElnTestWorkItemSnapshotProvider; import jnpf.elnService.ElnRecordFormulaCalculationService; import jnpf.elnService.ElnRecordRevisionService; import jnpf.elnService.template.ElnTemplateFileService; import jnpf.exception.DataException; import jnpf.lims.api.model.TestWorkItemSnapshotBatch; import jnpf.lims.api.model.TestWorkItemSnapshot; import jnpf.lims.api.model.TestWorkItemSnapshotProperty; import jnpf.model.tenant.TenantVO; import jnpf.onlyoffice.RecordRevisionFenceApi; import jnpf.util.JsonUtil; import jnpf.util.UserProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionSynchronization; import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.util.StringUtils; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; 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; @Service public class ElnRecordRevisionServiceImpl implements ElnRecordRevisionService { private static final Logger log = LoggerFactory.getLogger(ElnRecordRevisionServiceImpl.class); private static final String ACTIVE = "ACTIVE"; private static final String PRIMARY = "PRIMARY"; private static final String DRAFT = "DRAFT"; private static final String HISTORICAL = "HISTORICAL"; private static final String PENDING_TEST = "pending_test"; private static final String AUDIT_REJECTED = "audit_rejected"; private static final String CANCELLED = "cancelled"; private static final String CROSS_TEMPLATE = "CROSS_TEMPLATE"; private static final String LIMS_TASK = "LIMS_TASK"; private static final String SYSTEM = "SYSTEM"; private final ElnTestWorkItemSnapshotProvider taskProvider; private final ElnRecordMapper recordMapper; private final ElnRecordRevisionMapper revisionMapper; private final ElnRecordFieldValueMapper valueMapper; private final ElnFieldVersionMapper fieldVersionMapper; private final ElnRecordRepeatRowMapper repeatRowMapper; private final ElnRecordRepeatValueMapper repeatValueMapper; private final ElnTemplateFileService fileService; private final ElnRecordFormulaCalculationService calculationService; private final RecordRevisionFence revisionFence; private final CurrentUserProvider currentUserProvider; @Autowired public ElnRecordRevisionServiceImpl( ElnTestWorkItemSnapshotProvider taskProvider, ElnRecordMapper recordMapper, ElnRecordRevisionMapper revisionMapper, ElnRecordFieldValueMapper valueMapper, ElnFieldVersionMapper fieldVersionMapper, ElnRecordRepeatRowMapper repeatRowMapper, ElnRecordRepeatValueMapper repeatValueMapper, ElnTemplateFileService fileService, ElnRecordFormulaCalculationService calculationService, RecordRevisionFenceApi revisionFenceApi) { this(taskProvider, recordMapper, revisionMapper, valueMapper, fieldVersionMapper, repeatRowMapper, repeatValueMapper, fileService, calculationService, remoteFence(revisionFenceApi), ElnRecordRevisionServiceImpl::currentUser); } ElnRecordRevisionServiceImpl( ElnTestWorkItemSnapshotProvider taskProvider, ElnRecordMapper recordMapper, ElnRecordRevisionMapper revisionMapper, ElnRecordFieldValueMapper valueMapper, ElnFieldVersionMapper fieldVersionMapper, ElnRecordRepeatRowMapper repeatRowMapper, ElnRecordRepeatValueMapper repeatValueMapper, ElnTemplateFileService fileService, ElnRecordFormulaCalculationService calculationService, RecordRevisionFence revisionFence, CurrentUserProvider currentUserProvider) { this.taskProvider = taskProvider; this.recordMapper = recordMapper; this.revisionMapper = revisionMapper; this.valueMapper = valueMapper; this.fieldVersionMapper = fieldVersionMapper; this.repeatRowMapper = repeatRowMapper; this.repeatValueMapper = repeatValueMapper; this.fileService = fileService; this.calculationService = calculationService; this.revisionFence = revisionFence; this.currentUserProvider = currentUserProvider; } @Override @Transactional(rollbackFor = Exception.class) public ElnRecordVos.Created open(ElnRecordForms.Create command) { String testWorkItemId = required( command == null ? null : command.getTestWorkItemId(), "检验项不能为空"); String operationCode = required( command == null ? null : command.getOperationCode(), "操作码不能为空"); UserContext user = currentUserProvider.get(); TestWorkItemSnapshot snapshot = snapshot(user, testWorkItemId); ElnRecordEntity observed = recordMapper.selectLiveByTestWorkItem( user.tenantId, testWorkItemId); if (observed == null) { throw new DataException("电子记录不存在"); } ElnRecordEntity record = recordMapper.selectLiveForUpdate(user.tenantId, observed.getId()); if (record == null || !testWorkItemId.equals(record.getJianyanXiangId()) || !StringUtils.hasText(record.getDangqianXiudingId())) { throw new DataException("电子记录不存在或当前修订无效"); } ElnRecordRevisionEntity current = currentRevision(user.tenantId, record); ElnRecordRevisionEntity idempotent = revisionByOperation( user.tenantId, record.getId(), operationCode); if (idempotent != null) { if (!Objects.equals(current.getId(), idempotent.getId())) { throw new DataException("操作码已用于其他电子记录修订"); } return created(record, idempotent, true); } if (PENDING_TEST.equals(snapshot.getTaskStatus())) { if (!DRAFT.equals(current.getBizStatus())) { throw new DataException("电子记录当前修订不是可编辑草稿"); } return created(record, current, true); } if (!AUDIT_REJECTED.equals(snapshot.getTaskStatus()) && !CANCELLED.equals(snapshot.getTaskStatus())) { throw new DataException("检验项当前状态不允许创建电子记录修订"); } if (DRAFT.equals(current.getBizStatus()) && StringUtils.hasText(current.getLaiyuanXiudingId())) { return created(record, current, true); } return createRevision(record, current, snapshot, operationCode, user); } private ElnRecordVos.Created createRevision( ElnRecordEntity record, ElnRecordRevisionEntity source, TestWorkItemSnapshot snapshot, String operationCode, UserContext user) { if (!DRAFT.equals(source.getBizStatus()) && !HISTORICAL.equals(source.getBizStatus())) { throw new DataException("电子记录来源修订状态无效"); } if (source.getXiudingHao() == null || source.getXiudingHao() < 1) { throw new DataException("电子记录来源修订号无效"); } String sourceFileId = required(source.getJiluWendang(), "电子记录来源修订缺少文档"); FenceLease lease = acquireFence(user.tenantId, record.getId(), sourceFileId); boolean completionRegistered = registerFenceCompletion(lease); try { List sourceValues = sourceValues( user.tenantId, record, source); RepeatSnapshot repeatSnapshot = sourceRepeatSnapshot( user.tenantId, record, source); if (DRAFT.equals(source.getBizStatus()) && revisionMapper.markHistorical(user.tenantId, record.getId(), source.getId(), source.getFVersion(), user.userId) != 1) { throw new DataException("电子记录来源修订状态冲突"); } String revisionId = IdUtil.getSnowflakeNextIdStr(); Date now = new Date(); ElnDocumentFileVo document = copiedDocument(fileService.copyDocx( sourceDocument(sourceFileId), revisionId)); ElnRecordRevisionEntity revision = revision( revisionId, record.getId(), source, operationCode, document.getFileId(), user, now); if (revisionMapper.insert(revision) != 1) { throw new DataException("电子记录新修订创建冲突"); } List values = new ArrayList<>(sourceValues.size()); for (ElnRecordFieldValueEntity sourceValue : sourceValues) { ElnRecordFieldValueEntity value = copyValue( sourceValue, revisionId, snapshot, user, now); if (valueMapper.insert(value) != 1) { throw new DataException("电子记录新修订字段值复制失败"); } values.add(value); } copyRepeatSnapshot(repeatSnapshot, revisionId, user, now); if (recordMapper.switchCurrentRevision( user.tenantId, record.getId(), source.getId(), revisionId, record.getFVersion(), user.userId) != 1) { throw new DataException("电子记录当前修订切换冲突"); } record.setDangqianXiudingId(revisionId); record.setFVersion(record.getFVersion() + 1); ElnRecordFormulaCalculationResult calculated = calculationService.recalculateAllWithRepeats( user.tenantId, record, revision, values, operationCode, user.userId); for (ElnRecordFieldValueEntity value : calculated.getFieldValues()) { if (valueMapper.updateById(value) != 1) { throw new DataException("电子记录新修订公式结果保存失败"); } } for (ElnRecordRepeatValueEntity value : calculated.getRepeatValues()) { if (repeatValueMapper.updateById(value) != 1) { throw new DataException("电子记录新修订重复公式结果保存失败"); } } if (!completionRegistered) { revisionFence.complete(lease); } return created(record, revision, false); } catch (RuntimeException e) { if (!completionRegistered) { releaseFence(lease, e); } throw e; } } private FenceLease acquireFence(String tenantId, String recordId, String fileId) { String fileType = "annex"; String token = revisionFence.acquire(tenantId, recordId, fileId, fileType); if (!StringUtils.hasText(token)) { throw new DataException("无法锁定 OnlyOffice 历史文档,请稍后重试"); } return new FenceLease(tenantId, recordId, fileId, fileType, token); } private boolean registerFenceCompletion(FenceLease lease) { if (!TransactionSynchronizationManager.isSynchronizationActive()) { return false; } TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { @Override public void afterCompletion(int status) { try { if (status == TransactionSynchronization.STATUS_COMMITTED) { revisionFence.complete(lease); } else { revisionFence.release(lease); } } catch (RuntimeException e) { log.error("OnlyOffice 记录修订 fence 收尾失败,record={} file={} committed={}", lease.recordId, lease.fileId, status == TransactionSynchronization.STATUS_COMMITTED, e); } } }); return true; } private void releaseFence(FenceLease lease, RuntimeException original) { try { revisionFence.release(lease); } catch (RuntimeException releaseError) { original.addSuppressed(releaseError); } } private List sourceValues( String tenantId, ElnRecordEntity record, ElnRecordRevisionEntity revision) { List values = valueMapper.selectList( Wrappers.lambdaQuery() .eq(ElnRecordFieldValueEntity::getFTenantId, tenantId) .eq(ElnRecordFieldValueEntity::getDianziJiluId, record.getId()) .eq(ElnRecordFieldValueEntity::getXiudingId, revision.getId()) .and(q -> q.isNull(ElnRecordFieldValueEntity::getFDeleteMark) .or().eq(ElnRecordFieldValueEntity::getFDeleteMark, 0)) .orderByAsc(ElnRecordFieldValueEntity::getId)); if (values == null || values.isEmpty()) { throw new DataException("电子记录来源修订没有字段值"); } Set bindingIds = new LinkedHashSet<>(); for (ElnRecordFieldValueEntity value : values) { if (!Objects.equals(record.getMubanId(), value.getMubanId()) || !Objects.equals(revision.getId(), value.getXiudingId()) || !PRIMARY.equals(value.getBangdingJuese()) || !ACTIVE.equals(value.getBangdingZhuangtai()) || !bindingIds.add(value.getMubanBangdingId())) { throw new DataException("电子记录来源修订字段值与固定模板不一致"); } } return values; } private RepeatSnapshot sourceRepeatSnapshot( String tenantId, ElnRecordEntity record, ElnRecordRevisionEntity revision) { List rows = repeatRowMapper.selectList( Wrappers.lambdaQuery() .eq(ElnRecordRepeatRowEntity::getFTenantId, tenantId) .eq(ElnRecordRepeatRowEntity::getDianziJiluId, record.getId()) .eq(ElnRecordRepeatRowEntity::getXiudingId, revision.getId()) .orderByAsc(ElnRecordRepeatRowEntity::getRepeatGroupCode) .orderByAsc(ElnRecordRepeatRowEntity::getRowOrder) .orderByAsc(ElnRecordRepeatRowEntity::getId)); List values = repeatValueMapper.selectList( Wrappers.lambdaQuery() .eq(ElnRecordRepeatValueEntity::getFTenantId, tenantId) .eq(ElnRecordRepeatValueEntity::getDianziJiluId, record.getId()) .eq(ElnRecordRepeatValueEntity::getXiudingId, revision.getId()) .orderByAsc(ElnRecordRepeatValueEntity::getRepeatGroupCode) .orderByAsc(ElnRecordRepeatValueEntity::getRepeatRowId) .orderByAsc(ElnRecordRepeatValueEntity::getId)); rows = rows == null ? Collections.emptyList() : rows; values = values == null ? Collections.emptyList() : values; Map rowsByIdentity = new LinkedHashMap<>(); Set operationCodes = new LinkedHashSet<>(); for (ElnRecordRepeatRowEntity row : rows) { String key = repeatRowKey(row == null ? null : row.getRepeatGroupCode(), row == null ? null : row.getRowId()); if (row == null || !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()) || row.getRowOrder() == null || row.getRowOrder() < 1 || !StringUtils.hasText(row.getOperationCode()) || !validRepeatRowLifecycle(row) || rowsByIdentity.put(key, row) != null || !operationCodes.add(row.getOperationCode())) { throw new DataException("电子记录来源修订重复行快照无效"); } } Set cells = new LinkedHashSet<>(); for (ElnRecordRepeatValueEntity value : values) { String rowKey = repeatRowKey(value == null ? null : value.getRepeatGroupCode(), value == null ? null : value.getRepeatRowId()); ElnRecordRepeatRowEntity row = rowsByIdentity.get(rowKey); String cellKey = rowKey + "\n" + (value == null ? null : value.getMubanBangdingId()); if (value == null || row == null || !Objects.equals(record.getId(), value.getDianziJiluId()) || !Objects.equals(revision.getId(), value.getXiudingId()) || !Objects.equals(record.getMubanId(), value.getMubanId()) || !Objects.equals("REPEAT", value.getBangdingJuese()) || !Objects.equals(ACTIVE, value.getBangdingZhuangtai()) || !StringUtils.hasText(value.getMubanBangdingId()) || !Objects.equals(deleted(row), deleted(value)) || !cells.add(cellKey)) { throw new DataException("电子记录来源修订重复字段值快照无效"); } } return new RepeatSnapshot(rows, values); } private void copyRepeatSnapshot( RepeatSnapshot snapshot, String revisionId, UserContext user, Date now) { for (ElnRecordRepeatRowEntity source : snapshot.rows) { ElnRecordRepeatRowEntity row = copyRepeatRow(source, revisionId, user, now); if (repeatRowMapper.insert(row) != 1) { throw new DataException("电子记录新修订重复行复制失败"); } } for (ElnRecordRepeatValueEntity source : snapshot.values) { ElnRecordRepeatValueEntity value = copyRepeatValue(source, revisionId, user, now); if (repeatValueMapper.insert(value) != 1) { throw new DataException("电子记录新修订重复字段值复制失败"); } } } private static ElnRecordRepeatRowEntity copyRepeatRow( ElnRecordRepeatRowEntity source, String revisionId, UserContext user, Date now) { ElnRecordRepeatRowEntity row = new ElnRecordRepeatRowEntity(); row.setId(IdUtil.getSnowflakeNextIdStr()); row.setDianziJiluId(source.getDianziJiluId()); row.setXiudingId(revisionId); row.setMubanId(source.getMubanId()); row.setRepeatGroupCode(source.getRepeatGroupCode()); row.setRowId(source.getRowId()); row.setRowOrder(source.getRowOrder()); row.setOperationCode(source.getOperationCode()); row.setOperationStatus(source.getOperationStatus()); row.setDeleteOperationCode(source.getDeleteOperationCode()); row.setDeleteReason(source.getDeleteReason()); row.setDeleteRequestedUserId(source.getDeleteRequestedUserId()); row.setDeleteRequestedTime(source.getDeleteRequestedTime()); row.setFTenantId(user.tenantId); row.setFVersion(0); row.setFCreatorUserId(user.userId); row.setFCreatorTime(now); row.setFLastModifyUserId(user.userId); row.setFLastModifyTime(now); row.setFDeleteUserId(source.getFDeleteUserId()); row.setFDeleteTime(source.getFDeleteTime()); row.setFDeleteMark(source.getFDeleteMark()); return row; } private static ElnRecordRepeatValueEntity copyRepeatValue( ElnRecordRepeatValueEntity source, String revisionId, UserContext user, Date now) { ElnRecordRepeatValueEntity value = new ElnRecordRepeatValueEntity(); value.setId(IdUtil.getSnowflakeNextIdStr()); value.setDianziJiluId(source.getDianziJiluId()); value.setXiudingId(revisionId); value.setMubanId(source.getMubanId()); value.setRepeatRowId(source.getRepeatRowId()); value.setRepeatGroupCode(source.getRepeatGroupCode()); value.setMubanBangdingId(source.getMubanBangdingId()); value.setZiduanDingyiId(source.getZiduanDingyiId()); value.setZiduanBianma(source.getZiduanBianma()); value.setZiduanBanbenId(source.getZiduanBanbenId()); value.setBangdingJuese(source.getBangdingJuese()); value.setBangdingZhuangtai(source.getBangdingZhuangtai()); value.setZhiWenben(source.getZhiWenben()); value.setZhiShuzhi(source.getZhiShuzhi()); value.setZhiRiqi(source.getZhiRiqi()); value.setZhiLaiyuan(source.getZhiLaiyuan()); value.setJisuanZhuangtai(source.getJisuanZhuangtai()); value.setJisuanXiaoxi(source.getJisuanXiaoxi()); value.setJisuanShijian(source.getJisuanShijian()); value.setFTenantId(user.tenantId); value.setFVersion(0); value.setFCreatorUserId(user.userId); value.setFCreatorTime(now); value.setFLastModifyUserId(user.userId); value.setFLastModifyTime(now); value.setFDeleteUserId(source.getFDeleteUserId()); value.setFDeleteTime(source.getFDeleteTime()); value.setFDeleteMark(source.getFDeleteMark()); return value; } private static boolean validRepeatRowLifecycle(ElnRecordRepeatRowEntity row) { String status = row.getOperationStatus(); boolean deleted = deleted(row); if ("DELETED".equals(status)) { return deleted && hasDeleteIntent(row) && StringUtils.hasText(row.getFDeleteUserId()) && row.getFDeleteTime() != null; } if ("DELETE_PREPARED".equals(status)) { return !deleted && hasDeleteIntent(row); } if (ACTIVE.equals(status) || "PREPARED".equals(status) || "CANCELLED".equals(status)) { return !deleted && !hasAnyDeleteIntent(row); } return false; } private static boolean hasDeleteIntent(ElnRecordRepeatRowEntity row) { return StringUtils.hasText(row.getDeleteOperationCode()) && StringUtils.hasText(row.getDeleteReason()) && StringUtils.hasText(row.getDeleteRequestedUserId()) && row.getDeleteRequestedTime() != null; } private static boolean hasAnyDeleteIntent(ElnRecordRepeatRowEntity row) { return row.getDeleteOperationCode() != null || row.getDeleteReason() != null || row.getDeleteRequestedUserId() != null || row.getDeleteRequestedTime() != null; } private static boolean deleted(ElnRecordRepeatRowEntity row) { return Objects.equals(row.getFDeleteMark(), 1); } private static boolean deleted(ElnRecordRepeatValueEntity value) { return Objects.equals(value.getFDeleteMark(), 1); } private static String repeatRowKey(String groupCode, String rowId) { return groupCode + "\n" + rowId; } private ElnRecordFieldValueEntity copyValue( ElnRecordFieldValueEntity source, String revisionId, TestWorkItemSnapshot snapshot, UserContext user, Date now) { ElnRecordFieldValueEntity value = new ElnRecordFieldValueEntity(); value.setId(IdUtil.getSnowflakeNextIdStr()); value.setDianziJiluId(source.getDianziJiluId()); value.setXiudingId(revisionId); value.setMubanId(source.getMubanId()); value.setMubanBangdingId(source.getMubanBangdingId()); value.setZiduanDingyiId(source.getZiduanDingyiId()); value.setZiduanBianma(source.getZiduanBianma()); value.setZiduanBanbenId(source.getZiduanBanbenId()); value.setBangdingJuese(source.getBangdingJuese()); value.setBangdingZhuangtai(source.getBangdingZhuangtai()); value.setZhiWenben(source.getZhiWenben()); value.setZhiShuzhi(source.getZhiShuzhi()); value.setZhiRiqi(source.getZhiRiqi()); value.setZhiLaiyuan(source.getZhiLaiyuan()); value.setYuanShuxing(source.getYuanShuxing()); value.setYuanShuxingBanben(source.getYuanShuxingBanben()); value.setYuanYinyongLeixing(source.getYuanYinyongLeixing()); value.setYuanYinyongId(source.getYuanYinyongId()); value.setYuanHuoquShijian(source.getYuanHuoquShijian()); value.setYuanQuzhiZhuangtai(source.getYuanQuzhiZhuangtai()); value.setYuanQuzhiYuanyin(source.getYuanQuzhiYuanyin()); value.setYuanXueyuan(source.getYuanXueyuan()); value.setKuamoLaiyuanJiluId(source.getKuamoLaiyuanJiluId()); value.setKuamoLaiyuanXiudingId(source.getKuamoLaiyuanXiudingId()); value.setKuamoLaiyuanZhiId(source.getKuamoLaiyuanZhiId()); value.setKuamoLaiyuanZiduanBanbenId(source.getKuamoLaiyuanZiduanBanbenId()); value.setKuamoHuoquShijian(source.getKuamoHuoquShijian()); value.setJisuanZhuangtai(source.getJisuanZhuangtai()); value.setJisuanXiaoxi(source.getJisuanXiaoxi()); value.setJisuanShijian(source.getJisuanShijian()); value.setFTenantId(user.tenantId); value.setFVersion(0); value.setFDeleteMark(0); value.setFCreatorUserId(user.userId); value.setFLastModifyUserId(user.userId); value.setFCreatorTime(now); value.setFLastModifyTime(now); if (CROSS_TEMPLATE.equals(source.getZhiLaiyuan())) { return value; } if (source.getYuanShuxingBanben() != null) { return value; } ElnFieldVersionEntity version = fixedVersion(user.tenantId, source); ElnFieldForms.Binding binding = parseBinding(version.getBangdingPeizhi()); String provider = binding == null || binding.getRead() == null ? null : binding.getRead().getSource(); String property = binding == null || binding.getRead() == null ? null : binding.getRead().getProperty(); if (LIMS_TASK.equals(provider)) { TestWorkItemSnapshotProperty taskValue = taskProperty(snapshot, property); if (!Objects.equals(version.getShujuLeixing(), taskValue.getDataType())) { throw new DataException("LIMS 任务快照字段类型与固定字段版本不一致"); } value.setZhiWenben(taskValue.getValue()); value.setZhiShuzhi(null); value.setZhiRiqi(null); value.setZhiLaiyuan(LIMS_TASK); value.setYuanShuxing(property); value.setYuanYinyongLeixing(taskValue.getSource()); value.setYuanYinyongId(taskValue.getSourceRef()); value.setYuanHuoquShijian(now); } else if (SYSTEM.equals(provider)) { applySystemValue(value, property, user, now); } return value; } private ElnFieldVersionEntity fixedVersion( String tenantId, ElnRecordFieldValueEntity value) { ElnFieldVersionEntity version = fieldVersionMapper.selectOne( Wrappers.lambdaQuery() .eq(ElnFieldVersionEntity::getFTenantId, tenantId) .eq(ElnFieldVersionEntity::getId, value.getZiduanBanbenId()) .and(q -> q.isNull(ElnFieldVersionEntity::getFDeleteMark) .or().eq(ElnFieldVersionEntity::getFDeleteMark, 0)) .last("limit 1")); if (version == null || !Objects.equals( value.getZiduanDingyiId(), version.getZiduanDingyiId())) { throw new DataException("电子记录固定字段版本无效"); } return version; } private TestWorkItemSnapshot snapshot(UserContext user, String testWorkItemId) { TestWorkItemSnapshotBatch batch = taskProvider.resolveAsActor( user.tenantId, user.userId, Collections.singletonList(testWorkItemId), Collections.singletonList("test.basis")); if (batch == null || batch.getItems() == null || batch.getItems().size() != 1) { throw new DataException("LIMS 检验任务快照返回无效"); } TestWorkItemSnapshot item = batch.getItems().get(0); if (item == null || !testWorkItemId.equals(item.getTestWorkItemId()) || !"SUCCESS".equals(item.getOutcomeCode())) { throw new DataException(item != null && StringUtils.hasText(item.getReason()) ? item.getReason() : "当前用户或检验项状态不允许打开电子记录"); } return item; } private ElnRecordRevisionEntity currentRevision(String tenantId, ElnRecordEntity record) { ElnRecordRevisionEntity revision = revisionMapper.selectOne( Wrappers.lambdaQuery() .eq(ElnRecordRevisionEntity::getFTenantId, tenantId) .eq(ElnRecordRevisionEntity::getId, record.getDangqianXiudingId()) .eq(ElnRecordRevisionEntity::getDianziJiluId, record.getId()) .and(q -> q.isNull(ElnRecordRevisionEntity::getFDeleteMark) .or().eq(ElnRecordRevisionEntity::getFDeleteMark, 0)) .last("limit 1")); if (revision == null) { throw new DataException("电子记录当前修订不存在"); } return revision; } private ElnRecordRevisionEntity revisionByOperation( String tenantId, String recordId, String operationCode) { return revisionMapper.selectOne(Wrappers.lambdaQuery() .eq(ElnRecordRevisionEntity::getFTenantId, tenantId) .eq(ElnRecordRevisionEntity::getDianziJiluId, recordId) .eq(ElnRecordRevisionEntity::getXiudingCaozuoBianma, operationCode) .and(q -> q.isNull(ElnRecordRevisionEntity::getFDeleteMark) .or().eq(ElnRecordRevisionEntity::getFDeleteMark, 0)) .last("limit 1")); } private static ElnRecordRevisionEntity revision( String revisionId, String recordId, ElnRecordRevisionEntity source, String operationCode, String document, UserContext user, Date now) { ElnRecordRevisionEntity revision = new ElnRecordRevisionEntity(); revision.setId(revisionId); revision.setDianziJiluId(recordId); revision.setXiudingHao(source.getXiudingHao() + 1); revision.setLaiyuanXiudingId(source.getId()); revision.setXiudingCaozuoBianma(operationCode); revision.setJiluWendang(document); revision.setBizStatus(DRAFT); revision.setFTenantId(user.tenantId); revision.setFVersion(0); revision.setFDeleteMark(0); revision.setFCreatorUserId(user.userId); revision.setFLastModifyUserId(user.userId); revision.setFCreatorTime(now); revision.setFLastModifyTime(now); return revision; } private static void applySystemValue( ElnRecordFieldValueEntity value, String property, UserContext user, Date now) { value.setZhiWenben(null); value.setZhiShuzhi(null); value.setZhiRiqi(null); value.setZhiLaiyuan(SYSTEM); value.setYuanShuxing(property); value.setYuanYinyongLeixing(SYSTEM); value.setYuanYinyongId(user.userId); value.setYuanHuoquShijian(now); if ("current_user.id".equals(property)) { value.setZhiWenben(user.userId); } else if ("current_user.real_name".equals(property)) { value.setZhiWenben(user.userName); } else if ("current_date".equals(property)) { value.setZhiWenben(new SimpleDateFormat("yyyy-MM-dd").format(now)); value.setZhiRiqi(now); } else if ("current_datetime".equals(property)) { value.setZhiWenben(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(now)); value.setZhiRiqi(now); } else { throw new DataException("未支持的 SYSTEM property:" + property); } } private static TestWorkItemSnapshotProperty taskProperty( TestWorkItemSnapshot item, String property) { if (item.getProperties() != null) { for (TestWorkItemSnapshotProperty candidate : item.getProperties()) { if (candidate != null && Objects.equals(property, candidate.getProperty())) { if (!"SUCCESS".equals(candidate.getOutcomeCode())) { throw new DataException(StringUtils.hasText(candidate.getReason()) ? candidate.getReason() : "LIMS 任务快照字段不可用"); } return candidate; } } } throw new DataException("LIMS 任务快照缺少字段:" + property); } private static ElnFieldForms.Binding parseBinding(String json) { try { return StringUtils.hasText(json) ? JsonUtil.getJsonToBean(json, ElnFieldForms.Binding.class) : null; } catch (RuntimeException e) { throw new DataException("字段绑定配置无效"); } } private static String sourceDocument(String fileId) { if (!StringUtils.hasText(fileId)) { throw new DataException("电子记录来源修订缺少文档"); } ElnDocumentFileVo file = new ElnDocumentFileVo(); file.setFileId(fileId); file.setName(fileId); file.setFileExtension("docx"); return JsonUtil.getObjectToString(Collections.singletonList(file)); } private static ElnDocumentFileVo copiedDocument(String attachments) { try { List files = JsonUtil.getJsonToList(attachments, ElnDocumentFileVo.class); if (files == null || files.size() != 1 || files.get(0) == null || !StringUtils.hasText(files.get(0).getFileId())) { throw new DataException("独占电子记录文档复制结果无效"); } return files.get(0); } catch (DataException e) { throw e; } catch (RuntimeException e) { throw new DataException("独占电子记录文档复制结果无效"); } } private static ElnRecordVos.Created created( ElnRecordEntity record, ElnRecordRevisionEntity revision, boolean existing) { ElnRecordVos.Created result = new ElnRecordVos.Created(); result.setRecordId(record.getId()); result.setRevisionId(revision.getId()); result.setDocument(document(revision.getJiluWendang())); result.setExisting(existing); return result; } private static ElnDocumentFileVo document(String fileId) { if (!StringUtils.hasText(fileId)) { throw new DataException("电子记录当前修订缺少文档"); } ElnDocumentFileVo document = new ElnDocumentFileVo(); document.setFileId(fileId); document.setName(fileId); document.setFileExtension("docx"); return document; } private static String required(String value, String message) { if (!StringUtils.hasText(value)) { throw new DataException(message); } return value.trim(); } private static UserContext currentUser() { UserInfo user = UserProvider.getUser(); if (user == null || !StringUtils.hasText(user.getUserId())) { throw new DataException("当前登录用户无效"); } TenantVO routed = TenantDataSourceUtil.getTenantInfo(); String tenantId = routed != null && StringUtils.hasText(routed.getEnCode()) ? routed.getEnCode().trim() : user.getTenantId(); return new UserContext( StringUtils.hasText(tenantId) ? tenantId.trim() : "0", user.getUserId().trim(), user.getUserName()); } private static RecordRevisionFence remoteFence(RecordRevisionFenceApi api) { return new RecordRevisionFence() { @Override public String acquire(String tenantId, String recordId, String fileId, String fileType) { ActionResult result = api.acquire(tenantId, recordId, fileId, fileType); if (result == null || !ActionResultCode.SUCCESS.getCode().equals(result.getCode()) || !StringUtils.hasText(result.getData())) { throw new DataException(result == null || !StringUtils.hasText(result.getMsg()) ? "无法锁定 OnlyOffice 历史文档,请稍后重试" : result.getMsg()); } return result.getData(); } @Override public void complete(FenceLease lease) { requireFenceResult(api.complete(lease.tenantId, lease.recordId, lease.fileId, lease.fileType, lease.token), "OnlyOffice 历史文档标记失败,需人工恢复"); } @Override public void release(FenceLease lease) { requireFenceResult(api.release(lease.tenantId, lease.recordId, lease.fileId, lease.fileType, lease.token), "OnlyOffice 记录修订锁释放失败,需人工恢复"); } }; } private static void requireFenceResult(ActionResult result, String fallback) { if (result == null || !ActionResultCode.SUCCESS.getCode().equals(result.getCode()) || !Boolean.TRUE.equals(result.getData())) { throw new DataException(result == null || !StringUtils.hasText(result.getMsg()) ? fallback : result.getMsg()); } } private static final class RepeatSnapshot { private final List rows; private final List values; private RepeatSnapshot(List rows, List values) { this.rows = rows; this.values = values; } } @FunctionalInterface interface CurrentUserProvider { UserContext get(); } interface RecordRevisionFence { String acquire(String tenantId, String recordId, String fileId, String fileType); void complete(FenceLease lease); void release(FenceLease lease); } static final class FenceLease { private final String tenantId; private final String recordId; private final String fileId; private final String fileType; private final String token; FenceLease(String tenantId, String recordId, String fileId, String fileType, String token) { this.tenantId = tenantId; this.recordId = recordId; this.fileId = fileId; this.fileType = fileType; this.token = token; } } static final class UserContext { private final String tenantId; private final String userId; private final String userName; UserContext(String tenantId, String userId, String userName) { this.tenantId = tenantId; this.userId = userId; this.userName = userName; } } }