package jnpf.base.service; import cn.hutool.crypto.digest.DigestUtil; import jnpf.base.UserInfo; import jnpf.base.mapper.UploadEditDraftMapper; import jnpf.base.model.flow.DataModel; import jnpf.base.model.uploadedit.*; import jnpf.base.util.UploadEditStateMachine; import jnpf.base.util.UploadEditWorkingCopy; import jnpf.exception.DataException; import jnpf.util.JsonUtil; import jnpf.util.TicketUtil; import jnpf.util.UserProvider; import lombok.RequiredArgsConstructor; import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import java.util.*; @Service @RequiredArgsConstructor public class UploadEditDraftService { public static final String SUBMIT_STATE = "__jnpfUploadEdit"; private final UploadEditDraftMapper mapper; private final UploadEditAccess access; private final UploadEditTemporaryService temporaryService; private final UploadFilePermissionService filePermissions; @org.springframework.beans.factory.annotation.Autowired private UploadEditNotifications notifications; public org.springframework.web.context.request.async.DeferredResult> watchShared(String token) { return notifications.watch(token); } public void prepareRevisionSave(DataModel model) { temporaryService.prepareRevisionSave(model); } public Map load(UploadEditContext context) { if (context == null || !StringUtils.hasText(context.getRecordId())) throw new DataException("无跟踪 ID 不建立工作稿"); UserInfo user = UserProvider.getUser(); UploadEditAccess.CheckedBaseline checked = access.checkedBaseline(context, user.getUserId()); if (!checked.isControlled() && access.isDmsAudit(context)) throw new DataException(UploadEditAccess.DMS_INCOMPATIBLE); return load(context, checked.getFiles(), checked.isControlled()); } public Map loadShared(UploadEditContext context) { UploadEditAccess.CheckedBaseline checked = access.sharedBaseline(context, UserProvider.getLoginUserId()); // 复用本次准入已读出的表单类型,避免加载与组装结果再次读取完整发布定义。 return load(context, checked.getFiles(), true, !checked.isDmsAudit(), checked.isDmsAudit()); } public List> loadSharedBatch(List contexts) { List baselines = access.sharedBaselines(contexts, UserProvider.getLoginUserId()); List> result = new ArrayList<>(); Map observed = new LinkedHashMap<>(); for (int i = 0; i < contexts.size(); i++) { UploadEditAccess.CheckedBaseline checked = baselines.get(i); if (!checked.isDmsAudit()) throw new DataException("批量共享状态仅用于DMS审核附件"); String id = UploadEditWorkingCopy.id(tenant(UserProvider.getUser()), contexts.get(i)); if (notifications != null) observed.put(id, notifications.version(tenant(UserProvider.getUser()), id)); Map value = load(contexts.get(i), checked.getFiles(), true, false, true, true); UploadEditDraft draft = mapper.find(tenant(UserProvider.getUser()), (String) value.get("draftId")); value.put("canDiscard", draft != null && !UploadEditWorkingCopy.active(draft) && UploadEditStateMachine.sameFiles(draft.getBaselineJson(), checked.getFiles()) && filePermissions.canDiscardDraft(draft, checked.getFilePermissionView())); result.add(value); } if (notifications != null && !result.isEmpty()) result.get(0).put("watchToken", notifications.ticket(observed)); return result; } private Map load(UploadEditContext context, List> baseline, boolean shared) { return load(context, baseline, shared, !shared || !access.isDmsAudit(context)); } private Map load(UploadEditContext context, List> baseline, boolean shared, boolean create) { return load(context, baseline, shared, create, null); } private Map load(UploadEditContext context, List> baseline, boolean shared, boolean create, Boolean dmsAudit) { return load(context, baseline, shared, create, dmsAudit, false); } private Map load(UploadEditContext context, List> baseline, boolean shared, boolean create, Boolean dmsAudit, boolean lightweight) { UserInfo user = UserProvider.getUser(); String id = shared ? UploadEditWorkingCopy.id(tenant(user), context) : DigestUtil.sha256Hex(JsonUtil.getObjectToString(Arrays.asList(tenant(user), user.getUserId(), context.getModelId(), context.getRecordId(), context.getParentField(), context.getRowId(), context.getField()))); UploadEditDraft draft = mapper.find(tenant(user), id); if (draft == null || !create && !UploadEditStateMachine.sameFiles(draft.getBaselineJson(), baseline)) { draft = new UploadEditDraft(); draft.setId(id); draft.setTenantId(tenant(user)); draft.setOwnerId(user.getUserId()); draft.setModelId(context.getModelId()); draft.setRecordId(context.getRecordId()); draft.setField(context.getField()); draft.setParentField(context.getParentField()); draft.setRowId(context.getRowId()); draft.setMenuId(context.getMenuId()); draft.setTaskId(context.getTaskId()); draft.setBaselineJson(JsonUtil.getObjectToString(baseline)); draft.setCurrentJson(draft.getBaselineJson()); draft.setRevision(0L); draft.setUpdatedAt(new Date()); if (!create) return view(draft, dmsAudit, lightweight); try { mapper.insert(draft); } catch (DuplicateKeyException concurrentOpen) { draft = mapper.find(tenant(user), id); } } if (!UploadEditStateMachine.sameFiles(draft.getBaselineJson(), baseline)) { // 正式附件已经由其他会话保存,旧工作稿不再以过时基准恢复。 long revision = draft.getRevision(); draft.setBaselineJson(JsonUtil.getObjectToString(baseline)); UploadEditStateMachine.discard(draft, revision); persist(draft, revision); } return view(draft, dmsAudit, lightweight); } public boolean lockDmsRecord(String modelId, String recordId) { String table = access.dmsAuditTable(modelId); if (table != null && StringUtils.hasText(recordId) && mapper.lockDmsRecord(tenant(UserProvider.getUser()), table, recordId) == null) throw new DataException("审核单已移除,请重新打开"); return table != null; } private UploadEditDraft commandDraft(UploadEditCommand command) { UploadEditContext context = command.getContext(); if (context != null && access.isDmsAudit(context)) { access.sharedBaseline(context, UserProvider.getLoginUserId()); lockDmsRecord(context.getModelId(), context.getRecordId()); UploadEditAccess.CheckedBaseline checked = access.sharedBaseline(context, UserProvider.getLoginUserId()); String id = UploadEditWorkingCopy.id(tenant(UserProvider.getUser()), context); if (!Objects.equals(id, command.getDraftId())) throw new DataException("附件位置已变化,请重新打开"); Map current = load(context, checked.getFiles(), true, false); if (!Objects.equals(command.getContentToken(), current.get("contentToken"))) throw new DataException("共享附件内容已变化,请核对后重试"); if (command.getMetadataToken() != null && !Objects.equals(command.getMetadataToken(), current.get("metadataToken"))) throw new DataException("附件模板来源已变化,请核对后重试"); load(context, checked.getFiles(), true, true); UploadEditDraft draft = own(id); command.setRevision(draft.getRevision()); return draft; } UploadEditDraft draft = own(command.getDraftId()); // DMS shared mutations must participate in the parent-row lock protocol. if (access.isDmsAudit(context(draft))) throw new DataException("共享附件请求缺少当前协议上下文,请刷新页面后重试"); return draft; } @com.baomidou.dynamic.datasource.annotation.DSTransactional(rollbackFor = Exception.class) public Map replace(UploadEditCommand command) { UploadEditDraft draft = commandDraft(command); requireIdle(draft); requireCurrentBaseline(draft); draft.setOwnerId(UserProvider.getLoginUserId()); List> accepted = new ArrayList<>(); List> known = UploadEditStateMachine.files(draft.getCurrentJson()); known.addAll(UploadEditStateMachine.files(draft.getBaselineJson())); for (Map file : UploadEditStateMachine.files(command.getFiles())) { Map existing = known.stream().filter(item -> Objects.equals(file.get("fileId"), item.get("fileId"))) .findFirst().orElse(null); boolean newReceipt = file.containsKey("uploadReceipt") && (existing == null || !Objects.equals(file.get("uploadReceipt"), existing.get("uploadReceipt"))); accepted.add(existing == null || newReceipt ? uploadedFile(file, draft) : existing); } filePermissions.validateDraftChange(draft, accepted); long revision = draft.getRevision(); UploadEditStateMachine.replace(draft, command.getRevision(), accepted); persist(draft, revision); return view(draft); } @com.baomidou.dynamic.datasource.annotation.DSTransactional(rollbackFor = Exception.class) public Map discard(UploadEditCommand command) { UploadEditDraft draft = commandDraft(command); requireIdle(draft); List> baseline = access.baseline(context(draft), UserProvider.getLoginUserId()); if (!UploadEditStateMachine.sameFiles(draft.getBaselineJson(), baseline)) throw new DataException("表单已更新,请重新打开后放弃修改"); filePermissions.validateDraftDiscard(draft); long revision = draft.getRevision(); UploadEditStateMachine.discard(draft, command.getRevision()); persist(draft, revision); return view(draft); } @com.baomidou.dynamic.datasource.annotation.DSTransactional(rollbackFor = Exception.class) public Map begin(UploadEditCommand command) { UploadEditDraft draft = commandDraft(command); requireCurrentBaseline(draft); requireIdle(draft); draft.setOwnerId(UserProvider.getLoginUserId()); long revision = draft.getRevision(); String allowed = access.checkedBaseline(context(draft), UserProvider.getLoginUserId()).getMode(); if (command.getMode() != null && !"edit".equals(command.getMode()) && !"review".equals(command.getMode())) throw new DataException("无效的附件编辑模式"); String mode = "review".equals(allowed) || "review".equals(command.getMode()) ? "review" : "edit"; Map file = UploadEditStateMachine.begin(draft, command.getRevision(), command.getFileId()); if ("review".equals(mode)) draft.setLeaseId(jnpf.base.model.uploadedit.UploadEditMode.REVIEW_PREFIX + draft.getLeaseId()); persist(draft, revision); Map result = view(draft); result.put("leaseId", draft.getLeaseId()); result.put("mode", jnpf.base.model.uploadedit.UploadEditMode.ofLease(draft.getLeaseId())); result.put("source", file); return result; } /** 仅允许轮询本人有效租约的保存状态;不返回附件或下载凭据,不替代打开/保存时的业务权限检查。 */ public boolean canReadStatus(String tenantId, String ownerId, String leaseId) { if (!StringUtils.hasText(tenantId) || !StringUtils.hasText(ownerId) || !StringUtils.hasText(leaseId)) return false; UploadEditDraft draft = mapper.findLease(tenantId, leaseId); if (draft != null) { return Objects.equals(tenantId, draft.getTenantId()) && Objects.equals(ownerId, draft.getOwnerId()) && Objects.equals(leaseId, draft.getLeaseId()); } if ("review".equals(UploadEditMode.ofLease(leaseId))) return false; return temporaryService.canReadStatus(tenantId, ownerId, leaseId); } public UploadEditLease lease(String tenantId, String ownerId, String leaseId) { // Review leases are tracked drafts and cannot belong to the temporary UUID namespace. if (!"review".equals(UploadEditMode.ofLease(leaseId))) { UploadEditLease temporary = temporaryService.find(tenantId, ownerId, leaseId); if (temporary != null) return temporary; } UploadEditDraft draft = mapper.findLease(tenantId, leaseId); if (draft == null || !Objects.equals(ownerId, draft.getOwnerId()) || !Objects.equals(tenantId, draft.getTenantId()) || !Objects.equals(leaseId, draft.getLeaseId())) throw new DataException("编辑租约已失效"); requireSupportedDraft(draft); if (UploadEditWorkingCopy.shared(draft) && !UploadEditWorkingCopy.active(draft)) throw new DataException("编辑会话已过期,请重新打开"); UploadEditContext checkedContext = context(draft); UploadEditAccess.CheckedBaseline checked = access.checkedBaseline(checkedContext, ownerId); if (!UploadEditStateMachine.sameFiles(draft.getBaselineJson(), checked.getFiles())) { throw new DataException("表单基准已变化,旧编辑会话失效,请重新打开表单"); } String fileId = draft.getLeaseResultFileId() == null ? draft.getLeaseFileId() : draft.getLeaseResultFileId(); Map file = UploadEditStateMachine.files(draft.getCurrentJson()).stream() .filter(item -> Objects.equals(fileId, item.get("fileId"))).findFirst() .orElseThrow(() -> new DataException("工作附件已被替换")); UploadEditLease lease = new UploadEditLease(); String issuedMode = jnpf.base.model.uploadedit.UploadEditMode.ofLease(leaseId); if (!jnpf.base.model.uploadedit.UploadEditMode.permits(checked.getMode(), issuedMode)) throw new DataException("附件编辑权限已降低,请关闭旧会话并重新打开"); lease.setMode(issuedMode); lease.setLeaseId(leaseId); lease.setLastOperationId(draft.getLastOperationId()); lease.setDraftId(draft.getId()); lease.setTenantId(tenantId); lease.setOwnerId(ownerId); lease.setTracked(true); lease.setAllowDownload(access.allowDownload(context(draft))); lease.setQianpiyeAnnotatorEnabled(access.qianpiyeAnnotatorEnabled(checkedContext)); lease.setFile(file); return lease; } @com.baomidou.dynamic.datasource.annotation.DSTransactional(rollbackFor = Exception.class) public boolean save(UploadEditSaveCommand command) { UploadEditDraft draft = mapper.findLease(command.getTenantId(), command.getLeaseId()); if (draft == null) return temporaryService.saveRevision(command); if (UploadEditWorkingCopy.shared(draft) && access.isDmsAudit(context(draft))) { lockDmsRecord(draft.getModelId(), draft.getRecordId()); draft = mapper.findLease(command.getTenantId(), command.getLeaseId()); if (draft == null) return false; } if (!Objects.equals(command.getOwnerId(), draft.getOwnerId())) return false; requireSupportedDraft(draft); if (UploadEditWorkingCopy.shared(draft) && !UploadEditWorkingCopy.active(draft)) return false; requireCurrentBaseline(draft); lease(command.getTenantId(), command.getOwnerId(), command.getLeaseId()); if (Objects.equals(command.getOperationId(), draft.getLastOperationId())) return true; String id = String.valueOf(command.getFile().get("fileId")); if (!id.contains("," + command.getLeaseId() + ",")) throw new DataException("编辑产物与租约不匹配"); long revision = draft.getRevision(); if (!UploadEditStateMachine.save(draft, command.getLeaseId(), command.getOperationId(), command.getFile())) return false; persist(draft, revision); return true; } /** 必须从原生 @DSTransactional 保存入口调用,锁一直持有至业务保存提交或回滚。 */ public List prepareSave(DataModel model, java.util.function.Predicate permissionDenied) { return prepareSave(model, permissionDenied, null); } public List prepareSave(DataModel model, java.util.function.Predicate permissionDenied, UploadFilePermissionService.SaveContext permissions) { UploadEditContext savedContext = new UploadEditContext(); savedContext.setModelId(model.getVisualId()); savedContext.setRecordId(model.getMainId()); savedContext.setField("wenjian"); if (StringUtils.hasText(model.getMainId()) && access.isDmsAudit(savedContext) && permissions == null) throw new DataException(UploadEditAccess.DMS_INCOMPATIBLE); Object supplied = model.getDataNewMap().remove(SUBMIT_STATE); if (!access.hasEnabledFields(model.getVisualId())) { normalizeTemplateSource(model, permissions, Collections.emptyList(), permissionDenied); return Collections.emptyList(); } List drafts = mapper.lockRecord(tenant(UserProvider.getUser()), model.getVisualId(), model.getMainId()); for (UploadEditDraft draft : drafts) requireSupportedDraft(draft); normalizeTemplateSource(model, permissions, drafts, permissionDenied); Map contents = new HashMap<>(); Map metadata = new HashMap<>(); Map versions = new HashMap<>(); if (supplied instanceof List) for (Object value : (List) supplied) { Map item = value instanceof Map ? (Map) value : Collections.emptyMap(); if (item.get("contentToken") instanceof String token) contents.put(String.valueOf(item.get("draftId")), token); if (item.get("metadataToken") instanceof String token) metadata.put(String.valueOf(item.get("draftId")), token); if (item.get("revision") instanceof Number) { Number revision = (Number) item.get("revision"); versions.put(String.valueOf(item.get("draftId")), revision.longValue()); } } List adopted = new ArrayList<>(); // 原生字段权限在本次采用期间固定,不能因第一子行已授权而放开其余行。 Map nativeDenied = new HashMap<>(); for (UploadEditDraft draft : drafts) { String key = StringUtils.hasText(draft.getParentField()) ? draft.getParentField() + "-" + draft.getField() : draft.getField(); nativeDenied.putIfAbsent(key, permissionDenied.test(key)); } for (UploadEditDraft draft : drafts) { UploadEditContext context = context(draft); String root = StringUtils.hasText(context.getParentField()) ? context.getParentField() : context.getField(); if (!model.getDataNewMap().containsKey(root) && permissions == null) continue; String field = StringUtils.hasText(context.getParentField()) ? context.getParentField() + "-" + context.getField() : context.getField(); if (UploadEditWorkingCopy.shared(draft)) { if (permissions == null || !Objects.equals(permissions.permissions.getTaskId(), draft.getTaskId())) throw new DataException("共享工作文件的流程权限不可用"); if (!permissions.permissions.allows(field, jnpf.flowable.model.filepermission.FilePermissionPolicy.Action.VIEW)) continue; boolean disabledChild = StringUtils.hasText(context.getParentField()) && model.getFormAllModel() != null && model.getFormAllModel().stream().anyMatch(item -> item.getChildList() != null && context.getParentField().equals(item.getChildList().getTableModel()) && Boolean.TRUE.equals(item.getChildList().getDisabled())); if (disabledChild) continue; requireIdle(draft); Object baseline = UploadEditAccess.fieldValue(permissions.baseline, context); if (!UploadEditStateMachine.sameFiles(draft.getBaselineJson(), baseline)) throw new DataException("共享文件基准已变化,请重新打开表单"); boolean dms = access.isDmsAudit(context); if (dms) { String metadataToken = metadata.get(draft.getId()); if (metadataToken != null && !metadataToken.equals(UploadEditStateMachine.metadataToken(draft.getCurrentJson()))) throw new DataException("附件模板来源或保存信息已变化,请刷新后提交"); String token = contents.get(draft.getId()); if (token != null && !token.equals(UploadEditStateMachine.contentToken(draft.getCurrentJson()))) throw new DataException("共享附件内容已变化,请重新预览确认后提交"); if (token == null && !UploadEditStateMachine.sameFiles(baseline, draft.getCurrentJson())) throw new DataException("共享附件尚未确认,请刷新并预览后提交"); } else if (versions.containsKey(draft.getId())) UploadEditStateMachine.checkRevision(draft, versions.get(draft.getId())); boolean changed = !UploadEditStateMachine.sameFiles(baseline, draft.getCurrentJson()); // 只读字段只采用服务器已核验的正文编辑,绝不信任客户端附件值。 if (model.isWorkflowFileCheckpoint()) { if (!changed) continue; setSharedValue(model.getDataNewMap(), permissions.baseline, context, draft.getCurrentJson()); model.getWorkflowWorkingFields().add(field); } else if (Boolean.TRUE.equals(nativeDenied.get(field))) { if (!changed) continue; if (!versions.containsKey(draft.getId())) throw new DataException("共享附件版本缺失,请刷新后保存"); Map origins = UploadEditStateMachine.editOrigins(draft); List beforeIds = new ArrayList<>(); for (Map file : UploadEditStateMachine.files(baseline)) beforeIds.add((String) file.get("fileId")); List afterIds = new ArrayList<>(); for (Map file : UploadEditStateMachine.files(draft.getCurrentJson())) { String id = (String) file.get("fileId"); afterIds.add(origins.getOrDefault(id, id)); } if (!beforeIds.equals(afterIds)) throw new DataException("只读附件只能采用已授权的在线编辑结果"); setSharedValue(model.getDataNewMap(), permissions.baseline, context, draft.getCurrentJson()); model.getWorkflowWorkingFields().add(field); } else { Object submitted = UploadEditAccess.fieldValue(model.getDataNewMap(), context); if (changed && !versions.containsKey(draft.getId())) throw new DataException("共享附件版本缺失,请重新打开表单后保存"); if (!UploadEditStateMachine.sameFiles(submitted, draft.getCurrentJson())) { // 原生上传/替换/移除仍由实际动作校验处理,不把它解释成编辑。 permissions.nativeDraftIds.add(draft.getId()); draft.setCurrentJson(JsonUtil.getObjectToString(UploadEditStateMachine.files(submitted))); draft.setEditOriginsJson(null); } } permissions.sharedDraftIds.add(draft.getId()); adopted.add(draft); continue; } if (model.isWorkflowFileCheckpoint() || permissions != null && drafts.stream().anyMatch(shared -> UploadEditWorkingCopy.shared(shared) && Objects.equals(shared.getTaskId(), permissions.permissions.getTaskId()) && Objects.equals(shared.getField(), draft.getField()) && Objects.equals(shared.getParentField(), draft.getParentField()) && Objects.equals(shared.getRowId(), draft.getRowId()))) continue; // 与本次原生保存使用同一权限策略(含平台豁免),避免采用未写入的字段或遗漏已写入字段。 if (Boolean.TRUE.equals(nativeDenied.get(field))) continue; if (StringUtils.hasText(context.getParentField())) { boolean disabled = model.getFormAllModel() != null && model.getFormAllModel().stream().anyMatch(item -> item.getChildList() != null && context.getParentField().equals(item.getChildList().getTableModel()) && Boolean.TRUE.equals(item.getChildList().getDisabled())); if (disabled) continue; Object rows = model.getDataNewMap().get(context.getParentField()); if (rows == null && permissions != null) rows = Collections.emptyList(); if (!(rows instanceof List)) throw new DataException("子表数据不是有效列表"); Map row = ((List) rows).stream().filter(item -> item instanceof Map) .map(item -> (Map) item).filter(item -> context.getRowId().equals(UploadEditAccess.rowId(item))) .findFirst().orElse(null); if (row == null) { if (Boolean.TRUE.equals(model.getOnlyUpdate())) continue; // 原生保存将删除本行;同步撤销其稿件,保留所有物理文件和原生历史。 draft.setCurrentJson("[]"); draft.setEditOriginsJson(null); adopted.add(draft); continue; } if (!row.containsKey(context.getField()) && permissions == null) continue; } Object submitted = UploadEditAccess.fieldValue(model.getDataNewMap(), context); if (!Objects.equals(draft.getOwnerId(), UserProvider.getUser().getUserId())) { // 其他人的正式提交使本草稿失效;不把其工作内容混入当前提交。 draft.setCurrentJson(JsonUtil.getObjectToString(UploadEditStateMachine.files(submitted))); draft.setEditOriginsJson(null); } else { Long revision = versions.get(draft.getId()); boolean unchanged = UploadEditStateMachine.sameFiles(draft.getCurrentJson(), draft.getBaselineJson()) && UploadEditStateMachine.sameFiles(submitted, draft.getBaselineJson()); if (revision == null && !unchanged) { if (permissions == null || UploadEditStateMachine.sameFiles(submitted, draft.getCurrentJson())) { throw new DataException("附件工作稿版本缺失,请重新打开表单后保存"); } // 受控实例的普通附件提交由随后原生差异/上传来源校验负责,不借旧稿豁免。 permissions.nativeDraftIds.add(draft.getId()); draft.setCurrentJson(JsonUtil.getObjectToString(UploadEditStateMachine.files(submitted))); draft.setEditOriginsJson(null); } if (revision != null) UploadEditStateMachine.checkRevision(draft, revision); if (!UploadEditStateMachine.sameFiles(draft.getCurrentJson(), submitted)) throw new DataException("附件已更新,请刷新后保存表单"); } adopted.add(draft); } return adopted; } private void normalizeTemplateSource(DataModel model, UploadFilePermissionService.SaveContext permissions, List drafts, java.util.function.Predicate denied) { if (!model.getDataNewMap().containsKey("wenjian") || denied.test("wenjian")) return; UploadEditContext context = new UploadEditContext(); context.setModelId(model.getVisualId()); context.setField("wenjian"); if (!access.isDmsAuditTemporary(context)) return; context.setRecordId(model.getMainId()); List> trusted = new ArrayList<>(); // Only server state read before prepareSave can serve as provenance. for (UploadEditDraft draft : drafts) { if ("wenjian".equals(draft.getField()) && !StringUtils.hasText(draft.getParentField()) && (UploadEditWorkingCopy.shared(draft) || UserProvider.getLoginUserId().equals(draft.getOwnerId()))) trusted.addAll(UploadEditStateMachine.files(draft.getCurrentJson())); } trusted.addAll(permissions == null ? filePermissions.dmsMainAttachmentBaseline(model) : UploadEditStateMachine.files(permissions.baseline.get("wenjian"))); Object original = model.getDataNewMap().get("wenjian"); List> files = UploadEditStateMachine.files(original); jnpf.base.util.UploadTemplateSource.normalize(files, trusted, tenant(UserProvider.getUser()), UserProvider.getLoginUserId()); model.getDataNewMap().put("wenjian", original instanceof String ? JsonUtil.getObjectToString(files) : files); } public void completeSave(List drafts) { for (UploadEditDraft draft : drafts) { long revision = draft.getRevision(); UploadEditStateMachine.adopt(draft, revision, draft.getCurrentJson()); persist(draft, revision); } } @SuppressWarnings("unchecked") private static void setSharedValue(Map target, Map baseline, UploadEditContext c, String files) { if (!StringUtils.hasText(c.getParentField())) { target.put(c.getField(), UploadEditStateMachine.files(files)); return; } Object rows = target.get(c.getParentField()); if (rows instanceof List) for (Object row : (List) rows) { if (row instanceof Map && c.getRowId().equals(UploadEditAccess.rowId((Map) row))) { ((Map) row).put(c.getField(), UploadEditStateMachine.files(files)); return; } } throw new DataException("共享附件所属子表行已移除,请重新打开表单"); } private Map uploadedFile(Map file, UploadEditDraft draft) { return jnpf.base.util.UploadTemplateSource.uploaded(file, draft.getTenantId(), draft.getOwnerId()); } private void requireCurrentBaseline(UploadEditDraft draft) { if (!UploadEditStateMachine.sameFiles(draft.getBaselineJson(), access.baseline(context(draft), UploadEditWorkingCopy.shared(draft) ? UserProvider.getLoginUserId() : draft.getOwnerId()))) { throw new DataException("表单基准已变化,旧编辑会话失效,请重新打开表单"); } } private UploadEditDraft own(String id) { UserInfo user = UserProvider.getUser(); UploadEditDraft draft = mapper.find(tenant(user), id); if (draft == null || !UploadEditWorkingCopy.shared(draft) && !Objects.equals(user.getUserId(), draft.getOwnerId())) throw new DataException("附件工作稿不存在或不可访问"); requireSupportedDraft(draft); return draft; } private void requireSupportedDraft(UploadEditDraft draft) { if (!UploadEditWorkingCopy.shared(draft) && access.isDmsAudit(context(draft))) throw new DataException(UploadEditAccess.DMS_INCOMPATIBLE); } private void requireIdle(UploadEditDraft draft) { if (UploadEditWorkingCopy.shared(draft) && UploadEditWorkingCopy.active(draft)) throw new DataException("该流程附件正在编辑,请关闭编辑器后重试;异常退出的会话将在两分钟后释放"); } /** 心跳只维护租约,不改变文件版本;使用租约、人员、租户和截止时间进行条件更新。 */ public void heartbeat(UploadEditCommand command) { UploadEditDraft draft = own(command.getDraftId()); if (!UploadEditWorkingCopy.shared(draft) || !Objects.equals(draft.getOwnerId(), UserProvider.getLoginUserId()) || !Objects.equals(draft.getLeaseId(), command.getFileId()) || !UploadEditWorkingCopy.active(draft)) throw new DataException("编辑会话已失效"); lease(draft.getTenantId(), draft.getOwnerId(), draft.getLeaseId()); if (mapper.touchLease(draft.getTenantId(), draft.getId(), draft.getOwnerId(), draft.getLeaseId(), new Date(System.currentTimeMillis() - 120_000L), new Date()) != 1) throw new DataException("编辑会话已失效"); } /** 前端已确认保存后释放;撤权后仍允许本人结束自己的租约。迟到回调不能再次写入。 */ public void close(UploadEditCommand command) { UploadEditDraft draft = own(command.getDraftId()); if (!UploadEditWorkingCopy.shared(draft) || draft.getLeaseId() == null) return; if (!Objects.equals(draft.getOwnerId(), UserProvider.getLoginUserId()) || !Objects.equals(draft.getLeaseId(), command.getFileId())) throw new DataException("编辑会话已被替换"); long revision = draft.getRevision(); UploadEditStateMachine.invalidate(draft); draft.setRevision(revision + 1); draft.setUpdatedAt(new Date()); persist(draft, revision); } private void persist(UploadEditDraft draft, long revision) { if (mapper.compareAndSet(draft, revision) != 1) throw new DataException("附件状态已变化,请重新打开表单"); if (notifications != null && UploadEditWorkingCopy.shared(draft)) notifications.changedAfterCommit(draft.getTenantId(), draft.getId()); } private Map view(UploadEditDraft draft) { return view(draft, null); } private Map view(UploadEditDraft draft, Boolean dmsAudit) { return view(draft, dmsAudit, false); } private Map view(UploadEditDraft draft, Boolean dmsAudit, boolean lightweight) { Map result = new LinkedHashMap<>(); result.put("shared", UploadEditWorkingCopy.shared(draft)); result.put("draftId", draft.getId()); result.put("revision", draft.getRevision()); if (UploadEditWorkingCopy.shared(draft) && (dmsAudit != null ? dmsAudit : access.isDmsAudit(context(draft)))) { result.put("dmsAudit", true); result.put("contentToken", UploadEditStateMachine.contentToken(draft.getCurrentJson())); result.put("metadataToken", UploadEditStateMachine.metadataToken(draft.getCurrentJson())); result.put("editing", UploadEditWorkingCopy.active(draft)); result.put("ownEditing", UploadEditWorkingCopy.active(draft) && Objects.equals(draft.getOwnerId(), UserProvider.getLoginUserId())); } result.put("files", UploadEditStateMachine.files(draft.getCurrentJson())); result.put("baselineFiles", UploadEditStateMachine.files(draft.getBaselineJson())); result.put("editOrigins", UploadEditStateMachine.editOrigins(draft)); // Poll/preflight needs no discard authorization; that action is checked when explicitly requested. if (!lightweight) result.put("canDiscard", (!UploadEditWorkingCopy.shared(draft) || !UploadEditWorkingCopy.active(draft)) && filePermissions.canDiscardDraft(draft)); return result; } public static UploadEditContext context(UploadEditDraft draft) { UploadEditContext context = new UploadEditContext(); context.setModelId(draft.getModelId()); context.setRecordId(draft.getRecordId()); context.setField(draft.getField()); context.setParentField(draft.getParentField()); context.setRowId(draft.getRowId()); context.setMenuId(draft.getMenuId()); context.setTaskId(draft.getTaskId()); return context; } public static String tenant(UserInfo user) { return StringUtils.hasText(user.getTenantId()) ? user.getTenantId() : "0"; } }