package jnpf.dmsService; import jnpf.dmsEntity.DmsWenjianBanbenEntity; import jnpf.dmsEntity.DmsWenjianGuanxiEntity; import jnpf.dmsEntity.DmsWenjianZiyuanEntity; import jnpf.dmsEntity.banben.DmsWenjianBanbenJiesuanModel; import jnpf.dmsMapper.DmsWenjianBanbenMapper; import jnpf.dmsMapper.DmsWenjianGuanxiMapper; import jnpf.dmsMapper.DmsWenjianZiyuanMapper; import jnpf.exception.DataException; import jnpf.util.RandomUtil; import lombok.RequiredArgsConstructor; import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.Date; import java.util.Objects; @Service @RequiredArgsConstructor public class DmsWenjianYewuBanbenService { public static final String MAIN_FILE = "zhuwenjian"; public static final String RECORD_FILE = "jilu"; public static final String EXTERNAL_FILE = "wailaiWenjian"; public static final String RELATION_RECORD = "jilu"; private static final String EFFECTIVE = "effective"; private final DmsWenjianZiyuanMapper resourceMapper; private final DmsWenjianBanbenMapper versionMapper; private final DmsWenjianGuanxiMapper relationMapper; @Transactional(rollbackFor = Exception.class) public DmsWenjianBanbenEntity createInitialVersion(DmsWenjianBanbenJiesuanModel model) throws DataException { validateCommon(model); validateInitial(model); DmsWenjianBanbenEntity repeated = versionMapper.selectBySourceBusinessKey( model.getTenantId(), model.getLaiyuanYewuJian()); if (repeated != null) { validateRepeated(repeated, model, null); return repeated; } if (resourceMapper.selectByNumberForUpdate( model.getTenantId(), model.getWenjianBianhao().trim()) != null || (!blank(model.getWenjianBianhaoId()) && resourceMapper.selectByNumberDetailForUpdate( model.getTenantId(), model.getWenjianBianhaoId().trim()) != null)) { throw new DataException("文件编号已被占用,历史编号不可复用"); } Date now = new Date(); String fileId = blank(model.getWenjianId()) ? RandomUtil.uuId() : model.getWenjianId(); DmsWenjianZiyuanEntity resource = new DmsWenjianZiyuanEntity(); resource.setId(fileId); resource.setTenantId(model.getTenantId()); resource.setWenjianLeixing(model.getWenjianLeixing()); resource.setWenjianBianhaoId(trimToNull(model.getWenjianBianhaoId())); resource.setWenjianBianhao(model.getWenjianBianhao().trim()); resource.setWenjianMingcheng(model.getWenjianMingcheng().trim()); resource.setJiluLeixing(trimToNull(model.getJiluLeixing())); resource.setDengji(trimToNull(model.getDengji())); resource.setWenjianMuluId(model.getWenjianMuluId()); resource.setShifouShoukong(model.getShifouShoukong()); resource.setJiluDengjiGuanliId(model.getJiluDengjiGuanliId()); resource.setJiluDengjiGuanliBanben(model.getJiluDengjiGuanliBanben()); resource.setGuishuBumenId(trimToNull(model.getGuishuBumenId())); resource.setWenjianZhongxinWenjianId(model.getCenterDocumentId()); applyCenterValidity(resource, model); applyProjectionMetadata(resource, model); resource.setShouciShengxiaoShijian(model.getEffectiveAt() == null ? now : model.getEffectiveAt()); resource.setBizStatus(EFFECTIVE); resource.setCreatorUserId(model.getUserId()); resource.setCreatorTime(now); try { requireSingle(resourceMapper.insertResource(resource), "创建业务文件失败"); } catch (DuplicateKeyException exception) { throw new DataException("文件资源或编号已被占用,历史编号不可复用"); } DmsWenjianBanbenEntity version = buildVersion(model, fileId, null, 0, now); requireSingle(versionMapper.insertVersion(version), "创建文件初始版本失败"); resource.setDangqianBanbenId(version.getId()); resource.setDangqianBanbenhao(version.getBanbenhao()); resource.setDangqianBanbenXuhao(version.getBanbenXuhao()); resource.setLastModifyUserId(model.getUserId()); resource.setLastModifyTime(now); requireSingle(resourceMapper.setInitialCurrentVersion(resource), "设置文件初始有效版本失败"); return version; } @Transactional(rollbackFor = Exception.class) public DmsWenjianBanbenEntity createRevisionVersion(DmsWenjianBanbenJiesuanModel model) throws DataException { validateCommon(model); requireText(model.getWenjianId(), "待升级文件ID不能为空"); String sourceVersionId = requireText( model.getYuanWenjianBanbenId(), "来源文件版本ID不能为空"); DmsWenjianBanbenEntity repeated = versionMapper.selectBySourceBusinessKey( model.getTenantId(), model.getLaiyuanYewuJian()); if (repeated != null) { validateRepeated(repeated, model, sourceVersionId); return repeated; } DmsWenjianZiyuanEntity resource = requireCurrentResource(model, sourceVersionId); DmsWenjianBanbenEntity current = versionMapper.selectById( model.getTenantId(), sourceVersionId); if (current == null || !EFFECTIVE.equals(current.getBizStatus())) { throw new DataException("来源文件版本不是当前有效版本"); } int nextSequence; try { nextSequence = Math.addExact(current.getBanbenXuhao(), 1); } catch (ArithmeticException exception) { throw new DataException("文件版本序号已达到上限"); } if (!Objects.equals(resource.getWenjianMuluId(), model.getWenjianMuluId())) { throw new DataException("普通修订不能改变文件目录"); } model.setWenjianBianhaoId(resource.getWenjianBianhaoId()); model.setWenjianBianhao(resource.getWenjianBianhao()); model.setBianhaoShiyongRenId(resource.getBianhaoShiyongRenId()); model.setWenjianFenlei(resource.getWenjianFenlei()); if (blank(model.getWenjianMingcheng())) model.setWenjianMingcheng(resource.getWenjianMingcheng()); if (blank(model.getGuishuBumenId())) model.setGuishuBumenId(resource.getGuishuBumenId()); Date now = new Date(); DmsWenjianBanbenEntity version = buildVersion( model, resource.getId(), sourceVersionId, nextSequence, now); requireSingle(versionMapper.markSuperseded( model.getTenantId(), sourceVersionId, model.getUserId(), "修订升版,由 " + version.getBanbenhao() + " 替代"), "来源文件版本已发生变化"); requireSingle(versionMapper.insertVersion(version), "创建文件升级版本失败"); applyMetadata(resource, model); resource.setWenjianZhongxinWenjianId(model.getCenterDocumentId()); applyCenterValidity(resource, model); applyProjectionMetadata(resource, model); resource.setDangqianBanbenId(version.getId()); resource.setDangqianBanbenhao(version.getBanbenhao()); resource.setDangqianBanbenXuhao(version.getBanbenXuhao()); resource.setLastModifyUserId(model.getUserId()); resource.setLastModifyTime(now); requireSingle(resourceMapper.advanceCurrentVersion( resource, sourceVersionId), "更新文件当前有效版本失败"); return version; } @Transactional(rollbackFor = Exception.class) public DmsWenjianBanbenEntity reuseCurrentVersion(String tenantId, String fileId, String expectedVersionId) throws DataException { DmsWenjianBanbenJiesuanModel model = new DmsWenjianBanbenJiesuanModel(); model.setTenantId(requireText(tenantId, "租户ID不能为空")); model.setWenjianId(requireText(fileId, "文件ID不能为空")); DmsWenjianZiyuanEntity resource = requireCurrentResource( model, requireText(expectedVersionId, "来源文件版本ID不能为空")); DmsWenjianBanbenEntity current = versionMapper.selectById(tenantId, resource.getDangqianBanbenId()); if (current == null || !EFFECTIVE.equals(current.getBizStatus())) { throw new DataException("来源文件版本不是当前有效版本"); } return current; } /** REUSE keeps the attachment identity; approved record metadata can still change. */ @Transactional(rollbackFor = Exception.class) public DmsWenjianBanbenEntity updateReusedRecordMetadata(DmsWenjianBanbenJiesuanModel model) { if (!"xiuding_shenhe".equals(model.getLaiyuanDanjuLeixing()) || !RECORD_FILE.equals(model.getWenjianLeixing())) throw new DataException("仅修订审核记录允许沿用附件更新信息"); if (!"yes".equals(model.getShifouShoukong()) && !"no".equals(model.getShifouShoukong())) throw new DataException("记录方式缺失或无效"); requireText(model.getWenjianMingcheng(), "记录名称不能为空"); requireText(model.getGuishuBumenId(), "记录归属部门不能为空"); DmsWenjianBanbenEntity current = reuseCurrentVersion(model.getTenantId(), model.getWenjianId(), model.getYuanWenjianBanbenId()); if (!RECORD_FILE.equals(current.getWenjianLeixing()) || !Objects.equals(current.getWenjianMuluId(), model.getWenjianMuluId()) || !Objects.equals(current.getWenjianZhongxinBanbenId(), model.getCenterVersionId())) throw new DataException("沿用记录目录或附件版本已变化"); requireSingle(versionMapper.updateReusedRecordMetadata(model), "更新沿用记录版本信息失败"); requireSingle(resourceMapper.updateReusedRecordMetadata(model), "更新沿用记录资源信息失败"); current.setShifouShoukong(model.getShifouShoukong()); current.setWenjianMingcheng(model.getWenjianMingcheng()); current.setGuishuBumenId(model.getGuishuBumenId()); return current; } /** Called only after the unified revision batch has completed its center transaction. */ @Transactional(rollbackFor = Exception.class) public void retireRecord(String tenantId, String mainFileId, String fileId, String versionId, String userId) { DmsWenjianZiyuanEntity resource = resourceMapper.selectByIdForUpdate(tenantId, fileId); if (resource == null || !RECORD_FILE.equals(resource.getWenjianLeixing()) || !EFFECTIVE.equals(resource.getBizStatus()) || !Objects.equals(versionId, resource.getDangqianBanbenId()) || relationMapper.selectEffective(tenantId, mainFileId, fileId, RELATION_RECORD) == null) throw new DataException("移除记录的当前版本或归属已改变"); requireSingle(versionMapper.markSuperseded(tenantId, versionId, userId, "修订移除记录,保留历史"), "移除记录的业务版本已改变"); requireSingle(resourceMapper.retireRecord(tenantId, fileId, versionId, userId), "回写移除记录失败"); requireSingle(relationMapper.retireRecordRelation(tenantId, mainFileId, fileId, userId), "回写记录关系失败"); } public DmsWenjianBanbenEntity resolveCurrentVersion(String tenantId, String fileId) throws DataException { String resolvedTenantId = requireText(tenantId, "租户ID不能为空"); String resolvedFileId = requireText(fileId, "文件ID不能为空"); DmsWenjianBanbenEntity current = versionMapper.selectCurrentByFileId( resolvedTenantId, resolvedFileId); if (current == null || !EFFECTIVE.equals(current.getBizStatus())) { throw new DataException("文件不存在当前有效版本"); } return current; } @Transactional(rollbackFor = Exception.class) public DmsWenjianGuanxiEntity ensureRelation(String tenantId, String mainFileId, String relatedFileId, String relationType, String sourceAuditId, String userId) throws DataException { requireText(tenantId, "租户ID不能为空"); requireText(mainFileId, "主文件ID不能为空"); requireText(relatedFileId, "关联文件ID不能为空"); if (mainFileId.equals(relatedFileId)) { throw new DataException("文件不能关联自身"); } if (!RELATION_RECORD.equals(relationType) && !"guanlian_wenjian".equals(relationType)) { throw new DataException("文件关系类型无效"); } DmsWenjianZiyuanEntity main = resourceMapper.selectByIdForUpdate(tenantId, mainFileId); DmsWenjianZiyuanEntity related = resourceMapper.selectByIdForUpdate(tenantId, relatedFileId); if (main == null || !EFFECTIVE.equals(main.getBizStatus()) || !MAIN_FILE.equals(main.getWenjianLeixing())) { throw new DataException("主文件不存在或不是有效主文件"); } if (related == null || !EFFECTIVE.equals(related.getBizStatus())) { throw new DataException("关联文件不存在或不是有效文件"); } if (RELATION_RECORD.equals(relationType) && !RECORD_FILE.equals(related.getWenjianLeixing())) { throw new DataException("记录关系只能关联有效记录文件"); } if (RELATION_RECORD.equals(relationType) && (blank(main.getWenjianMuluId()) || !Objects.equals(main.getWenjianMuluId(), related.getWenjianMuluId()))) { throw new DataException("记录必须与主文件在同一目录,不能跨目录复用"); } DmsWenjianGuanxiEntity existing = relationMapper.selectEffective( tenantId, mainFileId, relatedFileId, relationType); if (existing != null) { return existing; } Date now = new Date(); DmsWenjianGuanxiEntity relation = new DmsWenjianGuanxiEntity(); relation.setId(RandomUtil.uuId()); relation.setTenantId(tenantId); relation.setZhuWenjianId(mainFileId); relation.setGuanlianWenjianId(relatedFileId); relation.setGuanxiLeixing(relationType); relation.setLaiyuanDanjuId(requireText(sourceAuditId, "来源审核单ID不能为空")); relation.setBizStatus(EFFECTIVE); relation.setCreatorUserId(requireText(userId, "操作人ID不能为空")); relation.setCreatorTime(now); requireSingle(relationMapper.insertRelation(relation), "创建文件关系失败"); return relation; } private DmsWenjianZiyuanEntity requireCurrentResource( DmsWenjianBanbenJiesuanModel model, String expectedVersionId) throws DataException { DmsWenjianZiyuanEntity resource = resourceMapper.selectByIdForUpdate( model.getTenantId(), model.getWenjianId()); if (resource == null || !EFFECTIVE.equals(resource.getBizStatus())) { throw new DataException("待处理文件不存在或不是有效文件"); } if (!Objects.equals(resource.getDangqianBanbenId(), expectedVersionId)) { throw new DataException("来源文件版本已不是当前有效版本,请重新发起申请"); } if (!blank(model.getWenjianLeixing()) && !Objects.equals(resource.getWenjianLeixing(), model.getWenjianLeixing())) { throw new DataException("待处理文件类型与审核内容不一致"); } return resource; } private DmsWenjianBanbenEntity buildVersion(DmsWenjianBanbenJiesuanModel model, String fileId, String sourceVersionId, int sequence, Date now) { DmsWenjianBanbenEntity version = new DmsWenjianBanbenEntity(); version.setId(RandomUtil.uuId()); version.setTenantId(model.getTenantId()); version.setWenjianId(fileId); version.setWenjianMuluId(model.getWenjianMuluId()); version.setShifouShoukong(model.getShifouShoukong()); version.setJiluDengjiGuanliId(model.getJiluDengjiGuanliId()); version.setJiluDengjiGuanliBanben(model.getJiluDengjiGuanliBanben()); version.setWenjianLeixing(model.getWenjianLeixing()); version.setJiluLeixing(model.getJiluLeixing()); version.setDengji(model.getDengji()); version.setYuanWenjianBanbenId(sourceVersionId); version.setBanbenXuhao(sequence); if (model.getCenterVersionLabel() != null && !"wailai_wenjian_xinzeng_shenhe".equals(model.getLaiyuanDanjuLeixing()) && !formatVersion(sequence).equals(model.getCenterVersionLabel())) throw new DataException("中心版本号与批准业务版本序号不一致"); version.setBanbenhao(model.getCenterVersionLabel() == null ? formatVersion(sequence) : model.getCenterVersionLabel()); version.setWenjianZhongxinBanbenId(model.getCenterVersionId()); version.setWenjianNeirong(model.getWenjianNeirong().trim()); version.setLaiyuanDanjuLeixing(model.getLaiyuanDanjuLeixing()); version.setLaiyuanDanjuId(model.getLaiyuanDanjuId()); version.setLaiyuanZixiangId(trimToNull(model.getLaiyuanZixiangId())); version.setLaiyuanYewuJian(model.getLaiyuanYewuJian()); version.setShengxiaoShijian(model.getEffectiveAt() == null ? now : model.getEffectiveAt()); version.setWenjianBianhaoId(model.getWenjianBianhaoId()); version.setWenjianBianhao(model.getWenjianBianhao()); version.setWenjianMingcheng(model.getWenjianMingcheng()); version.setGuishuBumenId(model.getGuishuBumenId()); version.setBianhaoShiyongRenId(model.getBianhaoShiyongRenId()); version.setZhidingRenId(model.getZhidingRenId()); version.setZhidingrenBumenId(model.getZhidingrenBumenId()); version.setZhidingRiqi(model.getZhidingRiqi()); version.setWenjianFenlei(model.getWenjianFenlei()); version.setWenjianDuixiangId(model.getWenjianDuixiangId()); version.setShenheLiuchengId(model.getShenheLiuchengId()); version.setShenheLiuchengRenwuId(model.getShenheLiuchengRenwuId()); version.setWenjianZhongxinWenjianId(model.getCenterDocumentId()); version.setYouxiaoQiZhi(model.getEffectiveTo()); if (model.getEffectiveAt() != null) { version.setYouxiaoqiFangshi(model.getEffectiveTo() == null ? "changqi" : "guding_riqi"); version.setShengxiaoNianxian(null); } version.setBizStatus(EFFECTIVE); version.setCreatorUserId(model.getUserId()); version.setCreatorTime(now); return version; } private void applyProjectionMetadata(DmsWenjianZiyuanEntity resource, DmsWenjianBanbenJiesuanModel model) { resource.setBianhaoShiyongRenId(model.getBianhaoShiyongRenId()); resource.setZhidingRenId(model.getZhidingRenId()); resource.setZhidingrenBumenId(model.getZhidingrenBumenId()); resource.setZhidingRiqi(model.getZhidingRiqi()); resource.setWenjianFenlei(model.getWenjianFenlei()); resource.setWenjianDuixiangId(model.getWenjianDuixiangId()); resource.setShenheLiuchengId(model.getShenheLiuchengId()); resource.setShenheLiuchengRenwuId(model.getShenheLiuchengRenwuId()); resource.setLaiyuanDanjuLeixing(model.getLaiyuanDanjuLeixing()); resource.setLaiyuanDanjuId(model.getLaiyuanDanjuId()); resource.setLaiyuanZixiangId(model.getLaiyuanZixiangId()); resource.setWenjianZhongxinBanbenId(model.getCenterVersionId()); } private void applyCenterValidity(DmsWenjianZiyuanEntity resource, DmsWenjianBanbenJiesuanModel model) { resource.setYouxiaoQiZhi(model.getEffectiveTo()); if (model.getEffectiveAt() != null) { resource.setShengxiaoShijian(model.getEffectiveAt()); // The center returns an explicit expiry boundary, never an inferred year duration. resource.setYouxiaoqiFangshi(model.getEffectiveTo() == null ? "changqi" : "guding_riqi"); resource.setShengxiaoNianxian(null); } } private void validateCommon(DmsWenjianBanbenJiesuanModel model) throws DataException { if (model == null) { throw new DataException("文件版本结算参数不能为空"); } requireText(model.getTenantId(), "租户ID不能为空"); requireText(model.getWenjianNeirong(), "文件内容不能为空"); requireText(model.getWenjianMuluId(), "文件目录不能为空,请在表单中选择有效来源"); if (RECORD_FILE.equals(model.getWenjianLeixing())) { // This service only settles new/revision audits, whose control flag is selected directly. if (!"yes".equals(model.getShifouShoukong()) && !"no".equals(model.getShifouShoukong())) { throw new DataException("记录是否受控快照缺失或无效,请完善表单"); } } requireText(model.getLaiyuanDanjuLeixing(), "来源单据类型不能为空"); if (!"xinzeng_shenhe".equals(model.getLaiyuanDanjuLeixing()) && !"xiuding_shenhe".equals(model.getLaiyuanDanjuLeixing()) && !"wailai_wenjian_xinzeng_shenhe".equals(model.getLaiyuanDanjuLeixing())) { throw new DataException("来源单据类型无效"); } requireText(model.getLaiyuanDanjuId(), "来源审核单ID不能为空"); requireText(model.getLaiyuanYewuJian(), "来源业务键不能为空"); requireText(model.getUserId(), "操作人ID不能为空"); } private void validateInitial(DmsWenjianBanbenJiesuanModel model) throws DataException { boolean externalSource = "wailai_wenjian_xinzeng_shenhe".equals(model.getLaiyuanDanjuLeixing()); if (externalSource != EXTERNAL_FILE.equals(model.getWenjianLeixing()) || (!externalSource && !MAIN_FILE.equals(model.getWenjianLeixing()) && !RECORD_FILE.equals(model.getWenjianLeixing()))) { throw new DataException("文件类型无效"); } requireText(model.getWenjianBianhao(), "文件编号不能为空"); requireText(model.getWenjianMingcheng(), "文件名称不能为空"); if (MAIN_FILE.equals(model.getWenjianLeixing())) { requireText(model.getWenjianBianhaoId(), "主文件编号申请明细ID不能为空"); } else if (RECORD_FILE.equals(model.getWenjianLeixing())) { requireText(model.getJiluLeixing(), "记录类型不能为空"); requireText(model.getDengji(), "记录等级不能为空"); } if (!blank(model.getYuanWenjianBanbenId())) { throw new DataException("初始版本不能包含来源版本ID"); } } private void validateRepeated(DmsWenjianBanbenEntity repeated, DmsWenjianBanbenJiesuanModel model, String sourceVersionId) throws DataException { if ((!blank(model.getWenjianId()) && !Objects.equals(repeated.getWenjianId(), model.getWenjianId())) || !Objects.equals(repeated.getYuanWenjianBanbenId(), sourceVersionId) || !Objects.equals(repeated.getWenjianNeirong(), model.getWenjianNeirong().trim()) || !Objects.equals(repeated.getWenjianMuluId(), model.getWenjianMuluId()) || !Objects.equals(repeated.getShifouShoukong(), model.getShifouShoukong()) || !Objects.equals(repeated.getJiluDengjiGuanliId(), model.getJiluDengjiGuanliId()) || !Objects.equals(repeated.getJiluDengjiGuanliBanben(), model.getJiluDengjiGuanliBanben()) || !Objects.equals(repeated.getWenjianLeixing(), model.getWenjianLeixing()) || !Objects.equals(repeated.getJiluLeixing(), model.getJiluLeixing()) || !Objects.equals(repeated.getDengji(), model.getDengji())) { throw new DataException("来源业务键对应的文件版本与本次结算内容不一致"); } } private void applyMetadata(DmsWenjianZiyuanEntity resource, DmsWenjianBanbenJiesuanModel model) { resource.setWenjianMuluId(model.getWenjianMuluId()); resource.setShifouShoukong(model.getShifouShoukong()); resource.setJiluDengjiGuanliId(model.getJiluDengjiGuanliId()); resource.setJiluDengjiGuanliBanben(model.getJiluDengjiGuanliBanben()); if (!blank(model.getWenjianMingcheng())) { resource.setWenjianMingcheng(model.getWenjianMingcheng().trim()); } if (!blank(model.getJiluLeixing())) { resource.setJiluLeixing(model.getJiluLeixing().trim()); } if (!blank(model.getDengji())) { resource.setDengji(model.getDengji().trim()); } if (!blank(model.getGuishuBumenId())) { resource.setGuishuBumenId(model.getGuishuBumenId().trim()); } } public static String formatVersion(int sequence) { return String.format("D(%02d)", sequence); } private String requireText(String value, String message) throws DataException { if (blank(value)) { throw new DataException(message); } return value.trim(); } private String trimToNull(String value) { return blank(value) ? null : value.trim(); } private boolean blank(String value) { return value == null || value.trim().isEmpty(); } private void requireSingle(int affected, String message) throws DataException { if (affected != 1) { throw new DataException(message); } } }