package jnpf.base.util;
|
|
import cn.hutool.core.util.IdUtil;
|
import jnpf.base.model.uploadedit.UploadEditDraft;
|
import jnpf.exception.DataException;
|
import jnpf.util.JsonUtil;
|
import java.util.ArrayList;
|
import java.util.Arrays;
|
import java.util.Date;
|
import java.util.LinkedHashMap;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.Objects;
|
|
/** 状态转换只改变附件引用,从不覆盖/删除物理内容。调用者必须以 revision 做数据库 CAS。 */
|
public final class UploadEditStateMachine {
|
/** Content identity is independent of lease/CAS revisions. Server files are immutable. */
|
public static String contentToken(Object files) {
|
return cn.hutool.crypto.digest.DigestUtil.sha256Hex(jnpf.util.JsonUtil.getObjectToString(
|
files(files).stream().map(file -> file.get("fileId")).toList()));
|
}
|
|
/** Business metadata only: timestamps and lease revisions do not mean content changed. */
|
public static String metadataToken(Object value) {
|
return cn.hutool.crypto.digest.DigestUtil.sha256Hex(JsonUtil.getObjectToString(files(value).stream()
|
.map(f -> Arrays.asList(f.get("fileId"), f.get(UploadTemplateSource.FIELD), f.get("contentSavedAt"))).toList()));
|
}
|
|
private UploadEditStateMachine() { }
|
|
@SuppressWarnings("unchecked")
|
public static List<Map<String, Object>> files(Object value) {
|
if (value == null || "".equals(value)) return new ArrayList<>();
|
Object parsed = value instanceof String ? JsonUtil.getJsonToBean((String) value, List.class) : value;
|
if (!(parsed instanceof List<?>)) throw new DataException("附件值不是有效列表");
|
List<Map<String, Object>> result = new ArrayList<>();
|
for (Object item : (List<?>) parsed) {
|
if (!(item instanceof Map) || !(((Map<?, ?>) item).get("fileId") instanceof String)
|
|| ((String) ((Map<?, ?>) item).get("fileId")).trim().isEmpty()) {
|
throw new DataException("附件缺少文件标识");
|
}
|
Map<?, ?> map = (Map<?, ?>) item;
|
String id = (String) map.get("fileId");
|
if (result.stream().anyMatch(file -> id.equals(file.get("fileId")))) throw new DataException("附件标识重复");
|
result.add(new LinkedHashMap<>((Map<String, Object>) map));
|
}
|
return result;
|
}
|
|
public static boolean sameFiles(Object left, Object right) {
|
return files(left).equals(files(right));
|
}
|
|
public static void checkRevision(UploadEditDraft draft, Long revision) {
|
if (revision == null || !Objects.equals(draft.getRevision(), revision)) {
|
throw new DataException("附件已在其他页面更新,请重新打开表单");
|
}
|
}
|
|
public static void replace(UploadEditDraft draft, Long revision, List<Map<String, Object>> next) {
|
checkRevision(draft, revision);
|
draft.setCurrentJson(JsonUtil.getObjectToString(files(next)));
|
Map<String, String> origins = editOrigins(draft);
|
origins.keySet().removeIf(id -> next.stream().noneMatch(file -> id.equals(file.get("fileId"))));
|
draft.setEditOriginsJson(JsonUtil.getObjectToString(origins));
|
invalidate(draft);
|
advance(draft);
|
}
|
|
public static void discard(UploadEditDraft draft, Long revision) {
|
checkRevision(draft, revision);
|
draft.setCurrentJson(draft.getBaselineJson());
|
draft.setEditOriginsJson(null);
|
invalidate(draft);
|
advance(draft);
|
}
|
|
public static boolean supportsEditing(Object fileName) {
|
String name = String.valueOf(fileName).toLowerCase(java.util.Locale.ROOT);
|
return name.endsWith(".doc") || name.endsWith(".docx");
|
}
|
|
public static Map<String, Object> begin(UploadEditDraft draft, Long revision, String fileId) {
|
checkRevision(draft, revision);
|
Map<String, Object> file = files(draft.getCurrentJson()).stream()
|
.filter(item -> Objects.equals(fileId, item.get("fileId"))).findFirst()
|
.orElseThrow(() -> new DataException("当前附件已被替换,请重新打开"));
|
if (!supportsEditing(file.get("name"))) {
|
throw new DataException("在线编辑目前仅支持 DOC、DOCX");
|
}
|
invalidate(draft);
|
draft.setLeaseId(IdUtil.simpleUUID());
|
draft.setLeaseFileId(fileId);
|
advance(draft);
|
return file;
|
}
|
|
/** 同一回调幂等;替换、放弃或表单采用后的租约不能再次生效。 */
|
public static boolean save(UploadEditDraft draft, String leaseId, String operationId, Map<String, Object> saved) {
|
if (leaseId == null || !leaseId.equals(draft.getLeaseId())) return false;
|
if (operationId == null || operationId.trim().isEmpty()) throw new DataException("保存操作标识不能为空");
|
if (operationId.equals(draft.getLastOperationId())) return true;
|
List<Map<String, Object>> current = files(draft.getCurrentJson());
|
Map<String, Object> replacement = files(java.util.Collections.singletonList(saved)).get(0);
|
String sourceId = draft.getLeaseResultFileId() == null ? draft.getLeaseFileId() : draft.getLeaseResultFileId();
|
for (int i = 0; i < current.size(); i++) {
|
if (!Objects.equals(sourceId, current.get(i).get("fileId"))) continue;
|
Map<String, String> origins = editOrigins(draft);
|
String origin = origins.containsKey(sourceId) ? origins.remove(sourceId) : sourceId;
|
origins.put(String.valueOf(saved.get("fileId")), origin);
|
draft.setEditOriginsJson(JsonUtil.getObjectToString(origins));
|
Object savedAt = replacement.get("contentSavedAt");
|
UploadTemplateSource.inherit(replacement, current.get(i));
|
if (savedAt instanceof Number) replacement.put("contentSavedAt", savedAt);
|
current.set(i, replacement);
|
draft.setCurrentJson(JsonUtil.getObjectToString(current));
|
draft.setLeaseResultFileId(String.valueOf(saved.get("fileId")));
|
draft.setLastOperationId(operationId);
|
advance(draft);
|
return true;
|
}
|
return false;
|
}
|
|
public static void adopt(UploadEditDraft draft, Long revision, Object submitted) {
|
checkRevision(draft, revision);
|
if (!sameFiles(draft.getCurrentJson(), submitted)) {
|
throw new DataException("提交附件不是当前工作文件,请刷新后重试");
|
}
|
draft.setBaselineJson(draft.getCurrentJson());
|
draft.setEditOriginsJson(null);
|
invalidate(draft);
|
advance(draft);
|
}
|
|
/** 仅读取受 CAS 保护的服务器工作稿;客户端附件上的同名属性没有授权含义。 */
|
public static Map<String, String> editOrigins(UploadEditDraft draft) {
|
Map<String, String> result = new LinkedHashMap<>();
|
if (draft.getEditOriginsJson() == null) return result;
|
Map<?, ?> stored = JsonUtil.getJsonToBean(draft.getEditOriginsJson(), Map.class);
|
if (stored == null) throw new DataException("附件编辑来源无效");
|
for (Map.Entry<?, ?> entry : stored.entrySet()) {
|
if (!(entry.getKey() instanceof String) || !(entry.getValue() instanceof String)
|
|| ((String) entry.getKey()).trim().isEmpty() || ((String) entry.getValue()).trim().isEmpty()) {
|
throw new DataException("附件编辑来源无效");
|
}
|
result.put((String) entry.getKey(), (String) entry.getValue());
|
}
|
return result;
|
}
|
|
public static void invalidate(UploadEditDraft draft) {
|
draft.setLeaseId(null);
|
draft.setLeaseFileId(null);
|
draft.setLeaseResultFileId(null);
|
draft.setLastOperationId(null);
|
}
|
|
private static void advance(UploadEditDraft draft) {
|
draft.setRevision(Math.addExact(draft.getRevision(), 1));
|
draft.setUpdatedAt(new Date());
|
}
|
}
|