刘光辉
9 小时以前 06f314545ac3514d7ce28939920866ae8188d31c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
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());
    }
}