package jnpf.dmsService; import jnpf.base.UserInfo; import jnpf.dmsEntity.permission.entity.DmsDocumentEntity; import jnpf.dmsEntity.permission.entity.DmsFileVersionEntity; import jnpf.dmsEntity.permission.entity.DmsPermissionResourceEntity; import jnpf.dmsEntity.permission.enums.DmsPermissionAction; import jnpf.dmsEntity.permission.enums.DmsResourceType; import jnpf.dmsEntity.storage.entity.DmsFileObjectEntity; import jnpf.dmsEntity.storage.form.DmsFileRevisionRegisterForm; import jnpf.dmsEntity.storage.vo.DmsFileUploadVO; import jnpf.dmsIntegration.audit.DmsAuditAdapter; import jnpf.dmsMapper.DmsDocumentMapper; import jnpf.dmsMapper.DmsFileObjectMapper; import jnpf.dmsMapper.DmsFileVersionMapper; import jnpf.dmsMapper.DmsPermissionResourceMapper; import jnpf.dmsMapper.DmsVersionStateMapper; import jnpf.dmsPermission.DmsCurrentIdentity; import jnpf.dmsPermission.DmsPermissionError; import jnpf.dmsPermission.DmsPermissionException; import lombok.RequiredArgsConstructor; import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.Locale; import java.util.Objects; @Service @RequiredArgsConstructor public class DmsFileRevisionService { private static final String SOURCE_PREFIX = "revision:"; private final DmsCurrentIdentity currentIdentity; private final DmsPermissionDecisionService decisionService; private final DmsDocumentMapper documentMapper; private final DmsFileVersionMapper fileVersionMapper; private final DmsPermissionResourceMapper resourceMapper; private final DmsFileObjectMapper fileObjectMapper; private final DmsVersionStateMapper versionStateMapper; private final DmsAuditAdapter auditAdapter; @Transactional public DmsFileUploadVO register(DmsFileRevisionRegisterForm form, String serviceId) { UserInfo user = currentIdentity.requireUser(); String tenantId = currentIdentity.tenantId(user); String sourceId = form.getSourceId().trim(); decisionService.requireAllowed(DmsResourceType.DOCUMENT, form.getDocumentId(), DmsPermissionAction.UPDATE); fileObjectMapper.lockSource(tenantId, serviceId, sourceId); DmsFileUploadVO existing = fileObjectMapper.selectUploadBySource(tenantId, serviceId, sourceId); if (existing != null) { validateRetry(existing, form); return existing; } DmsDocumentEntity document = documentMapper.selectByTenantAndIdForUpdate(tenantId, form.getDocumentId()); if (document == null) { throw DmsPermissionException.of(DmsPermissionError.RESOURCE_NOT_FOUND, "待修订文件不存在"); } DmsFileVersionEntity current = fileVersionMapper.selectCurrentEffective(tenantId, form.getDocumentId()); if (current == null || !Objects.equals(current.getId(), form.getBaseFileVersionId())) { throw DmsPermissionException.of(DmsPermissionError.REVISION_CONFLICT, "待修订版本已不是现行有效版本,请刷新后重试"); } long nextVersionNo = fileVersionMapper.selectNextVersionNo(tenantId, form.getDocumentId()); try { insertFileVersion(form, tenantId, user.getUserId(), nextVersionNo); insertRegistry(form, tenantId, serviceId, sourceId); insertFileObject(form, tenantId, user.getUserId(), serviceId, sourceId); int superseded = versionStateMapper.supersedeCurrent(tenantId, form.getDocumentId(), form.getBaseFileVersionId(), form.getVersionLabel().trim(), user.getUserId()); if (superseded != 1) { throw DmsPermissionException.of(DmsPermissionError.REVISION_CONFLICT, "现行有效版本已发生变化,请刷新后重试"); } versionStateMapper.insertRevisionEffective(tenantId, form.getDocumentId(), form.getBaseFileVersionId(), form.getFileVersionId(), sourceId, form.getRevisionReason().trim(), user.getUserId()); } catch (DuplicateKeyException exception) { throw conflict("修订登记的资源 ID、source_id、版本号、版本标签或对象键已被占用", exception); } auditAdapter.dataChange("UPDATE", "模拟修订 DMS 文件", "dms_documents", form.getDocumentId().toString(), "{\"fileVersionId\":\"" + form.getBaseFileVersionId() + "\"}", "{\"fileVersionId\":\"" + form.getFileVersionId() + "\",\"versionLabel\":\"" + escapeJson(form.getVersionLabel().trim()) + "\"}"); return fileObjectMapper.selectUploadBySource(tenantId, serviceId, sourceId); } private void insertFileVersion(DmsFileRevisionRegisterForm form, String tenantId, String userId, long versionNo) { DmsFileVersionEntity version = new DmsFileVersionEntity(); version.setId(form.getFileVersionId()); version.setTenantId(tenantId); version.setDocumentId(form.getDocumentId()); version.setVersionNo(versionNo); version.setVersionLabel(form.getVersionLabel().trim()); version.setFileName(form.getFileName().trim()); version.setMimeType(trimToNull(form.getMimeType())); version.setSizeBytes(form.getSizeBytes()); version.setChecksumSha256(form.getChecksumSha256().toLowerCase(Locale.ROOT)); version.setPreparedBy(form.getPreparedBy().trim()); version.setPreparedOn(form.getPreparedOn()); version.setCreatedBy(userId); fileVersionMapper.insert(version); } private void insertRegistry(DmsFileRevisionRegisterForm form, String tenantId, String serviceId, String sourceId) { DmsPermissionResourceEntity registry = new DmsPermissionResourceEntity(); registry.setId(form.getFileVersionId()); registry.setTenantId(tenantId); registry.setResourceType(DmsResourceType.FILE_VERSION.name()); registry.setSourceType("SYSTEM"); registry.setSourceId(SOURCE_PREFIX + sourceId + ":version"); registry.setRegisteredBy(serviceId); resourceMapper.insert(registry); } private void insertFileObject(DmsFileRevisionRegisterForm form, String tenantId, String userId, String serviceId, String sourceId) { DmsFileObjectEntity object = new DmsFileObjectEntity(); object.setId(form.getFileObjectId()); object.setTenantId(tenantId); object.setFileVersionId(form.getFileVersionId()); object.setStoragePlatform(form.getStoragePlatform().trim()); object.setBucketName(form.getBucketName().trim()); object.setBasePath(trimToEmpty(form.getBasePath())); object.setObjectPath(form.getObjectPath().trim()); object.setObjectName(form.getObjectName().trim()); object.setSourceServiceId(serviceId); object.setSourceId(sourceId); object.setCreatedBy(userId); fileObjectMapper.insert(object); } private void validateRetry(DmsFileUploadVO existing, DmsFileRevisionRegisterForm form) { if (!Objects.equals(existing.getDocumentId(), form.getDocumentId()) || !Objects.equals(existing.getBaseFileVersionId(), form.getBaseFileVersionId()) || !Objects.equals(existing.getVersionLabel(), form.getVersionLabel().trim()) || !Objects.equals(existing.getFileName(), form.getFileName().trim()) || !Objects.equals(existing.getMimeType(), trimToNull(form.getMimeType())) || !Objects.equals(existing.getSizeBytes(), form.getSizeBytes()) || !equalsIgnoreCase(existing.getChecksumSha256(), form.getChecksumSha256()) || !Objects.equals(existing.getPreparedBy(), form.getPreparedBy().trim()) || !Objects.equals(existing.getPreparedOn(), form.getPreparedOn()) || !Objects.equals(existing.getRevisionReason(), form.getRevisionReason().trim())) { throw DmsPermissionException.of(DmsPermissionError.IDEMPOTENCY_CONFLICT, "source_id 对应的修订内容与本次请求不一致"); } } private String trimToNull(String value) { return value == null || value.trim().isEmpty() ? null : value.trim(); } private String trimToEmpty(String value) { return value == null ? "" : value.trim(); } private boolean equalsIgnoreCase(String left, String right) { return left == null ? right == null : right != null && left.equalsIgnoreCase(right); } private String escapeJson(String value) { return value.replace("\\", "\\\\").replace("\"", "\\\""); } private DmsPermissionException conflict(String message, Throwable cause) { DmsPermissionException exception = DmsPermissionException.of(DmsPermissionError.SOURCE_CONFLICT, message); exception.initCause(cause); return exception; } }