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<jnpf.base.ActionResult<Boolean>> watchShared(String token) {
|
return notifications.watch(token);
|
}
|
|
public void prepareRevisionSave(DataModel model) {
|
temporaryService.prepareRevisionSave(model);
|
}
|
|
public Map<String, Object> 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<String, Object> loadShared(UploadEditContext context) {
|
UploadEditAccess.CheckedBaseline checked = access.sharedBaseline(context, UserProvider.getLoginUserId());
|
// 复用本次准入已读出的表单类型,避免加载与组装结果再次读取完整发布定义。
|
return load(context, checked.getFiles(), true, !checked.isDmsAudit(), checked.isDmsAudit());
|
}
|
|
public List<Map<String, Object>> loadSharedBatch(List<UploadEditContext> contexts) {
|
List<UploadEditAccess.CheckedBaseline> baselines = access.sharedBaselines(contexts, UserProvider.getLoginUserId());
|
List<Map<String, Object>> result = new ArrayList<>();
|
Map<String, Long> 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<String, Object> 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<String, Object> load(UploadEditContext context, List<Map<String, Object>> baseline, boolean shared) {
|
return load(context, baseline, shared, !shared || !access.isDmsAudit(context));
|
}
|
|
private Map<String, Object> load(UploadEditContext context, List<Map<String, Object>> baseline, boolean shared, boolean create) {
|
return load(context, baseline, shared, create, null);
|
}
|
|
private Map<String, Object> load(UploadEditContext context, List<Map<String, Object>> baseline, boolean shared, boolean create, Boolean dmsAudit) {
|
return load(context, baseline, shared, create, dmsAudit, false);
|
}
|
private Map<String, Object> load(UploadEditContext context, List<Map<String, Object>> 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<String,Object> 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<String, Object> replace(UploadEditCommand command) {
|
UploadEditDraft draft = commandDraft(command);
|
requireIdle(draft);
|
requireCurrentBaseline(draft);
|
draft.setOwnerId(UserProvider.getLoginUserId());
|
List<Map<String, Object>> accepted = new ArrayList<>();
|
List<Map<String, Object>> known = UploadEditStateMachine.files(draft.getCurrentJson());
|
known.addAll(UploadEditStateMachine.files(draft.getBaselineJson()));
|
for (Map<String, Object> file : UploadEditStateMachine.files(command.getFiles())) {
|
Map<String, Object> 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<String, Object> discard(UploadEditCommand command) {
|
UploadEditDraft draft = commandDraft(command);
|
requireIdle(draft);
|
List<Map<String, Object>> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<UploadEditDraft> prepareSave(DataModel model, java.util.function.Predicate<String> permissionDenied) {
|
return prepareSave(model, permissionDenied, null);
|
}
|
|
public List<UploadEditDraft> prepareSave(DataModel model, java.util.function.Predicate<String> 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<UploadEditDraft> drafts = mapper.lockRecord(tenant(UserProvider.getUser()), model.getVisualId(), model.getMainId());
|
for (UploadEditDraft draft : drafts) requireSupportedDraft(draft);
|
normalizeTemplateSource(model, permissions, drafts, permissionDenied);
|
Map<String, String> contents = new HashMap<>();
|
Map<String, String> metadata = new HashMap<>();
|
Map<String, Long> 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<UploadEditDraft> adopted = new ArrayList<>();
|
// 原生字段权限在本次采用期间固定,不能因第一子行已授权而放开其余行。
|
Map<String, Boolean> 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<String, String> origins = UploadEditStateMachine.editOrigins(draft);
|
List<String> beforeIds = new ArrayList<>();
|
for (Map<String, Object> file : UploadEditStateMachine.files(baseline)) beforeIds.add((String) file.get("fileId"));
|
List<String> afterIds = new ArrayList<>();
|
for (Map<String, Object> 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<UploadEditDraft> drafts, java.util.function.Predicate<String> 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<Map<String, Object>> 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<Map<String, Object>> 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<UploadEditDraft> 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<String, Object> target, Map<String, Object> 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<String, Object>) row).put(c.getField(), UploadEditStateMachine.files(files)); return;
|
}
|
}
|
throw new DataException("共享附件所属子表行已移除,请重新打开表单");
|
}
|
|
private Map<String, Object> uploadedFile(Map<String, Object> 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<String, Object> view(UploadEditDraft draft) {
|
return view(draft, null);
|
}
|
private Map<String, Object> view(UploadEditDraft draft, Boolean dmsAudit) {
|
return view(draft, dmsAudit, false);
|
}
|
private Map<String, Object> view(UploadEditDraft draft, Boolean dmsAudit, boolean lightweight) {
|
Map<String, Object> 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"; }
|
}
|