刘光辉
8 小时以前 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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
package jnpf.base.service;
 
import cn.hutool.core.util.IdUtil;
import cn.hutool.crypto.digest.DigestUtil;
import jnpf.base.ActionResult;
import jnpf.base.ActionResultCode;
import jnpf.base.model.uploadedit.*;
import jnpf.dms.DmsRevisionEditApi;
import jnpf.dmsEntity.storage.DmsRevisionEditRequest;
import jnpf.exception.DataException;
import jnpf.util.JsonUtil;
import jnpf.util.TicketUtil;
import jnpf.util.UserProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.concurrent.TimeUnit;
 
/** DMS新增/修订审核共用临时副本;只保存附件结果,主子表仍由原生表单持久化。 */
@Service
@RequiredArgsConstructor
public class DmsAuditTemporaryEditService {
    private final UploadEditAccess access;
    private final DmsRevisionEditApi sources;
    private final StringRedisTemplate redis;
    private static final DefaultRedisScript<Long> CAS = new DefaultRedisScript<>(
        "if redis.call('get',KEYS[1]) == ARGV[1] then redis.call('set',KEYS[1],ARGV[2],'KEEPTTL'); return 1 else return 0 end",Long.class);
 
    /** 上传/模板凭据已经由原临时入口核验,转换成同一审核副本会话。 */
    public Map<String,Object> beginUpload(UploadEditCommand command, Map<String,Object> file) {
        UploadEditContext context = command.getContext();
        if (!access.isDmsAuditTemporary(context)) throw denied();
        Map<String,Object> proof = new LinkedHashMap<>();
        proof.put("purpose", "dms-audit-upload");
        proof.put("tenantId", UploadEditDraftService.tenant(UserProvider.getUser()));
        proof.put("ownerId", UserProvider.getLoginUserId()); proof.put("modelId", context.getModelId());
        proof.put("parentField", Objects.toString(context.getParentField(), "")); proof.put("field", context.getField());
        proof.put("file", new LinkedHashMap<>(file)); proof.put("expiresAt", System.currentTimeMillis()+14400000L);
        String receipt = TicketUtil.createTicket(JsonUtil.getObjectToString(proof), 14400L);
        Map<String,Object> source = new LinkedHashMap<>(file); source.put("revisionEditReceipt", receipt);
        UploadEditCommand issued = new UploadEditCommand(); issued.setContext(context);
        issued.setFileId(command.getFileId()); issued.setFiles(List.of(source));
        return begin(issued);
    }
 
    public Map<String,Object> begin(UploadEditCommand command) {
        UploadEditContext context=command.getContext();
        // 不把已保存记录、流程实例或可跟踪子行降级为临时副本。
        if (context==null || text(context.getRecordId()) || text(context.getTaskId())) throw denied();
        String owner=UserProvider.getLoginUserId(), tenant=UploadEditDraftService.tenant(UserProvider.getUser());
        access.authorizeTemporary(context,owner);
        if (command.getFiles()==null || command.getFiles().size()!=1) throw denied();
        String receipt=Objects.toString(command.getFiles().get(0).get("revisionEditReceipt"),"");
        Map<String,Object> proof=verify(receipt,context,tenant,owner);
        Map<String,Object> file=(Map<String,Object>)proof.get("file");
        if (!Objects.equals(command.getFileId(),file.get("fileId"))
                || !jnpf.base.util.UploadEditStateMachine.supportsEditing(file.get("name"))) throw denied();
        UploadEditLease lease=new UploadEditLease();
        lease.setLeaseId(IdUtil.simpleUUID()); lease.setDraftId(lease.getLeaseId());
        lease.setTenantId(tenant); lease.setOwnerId(owner); lease.setRevisionTemporary(true);
        Map<String,Object> issuedFile=new LinkedHashMap<>(file); issuedFile.put("revisionEditReceipt",receipt); lease.setFile(issuedFile);
        Map<String,Object> state=new LinkedHashMap<>(); state.put("lease",lease); state.put("context",context); state.put("receipt",receipt);
        long remaining=((Number)proof.get("expiresAt")).longValue()-System.currentTimeMillis();
        if (remaining<=0) throw denied();
        redis.opsForValue().set(key(tenant,lease.getLeaseId()),JsonUtil.getObjectToString(state),remaining,TimeUnit.MILLISECONDS);
        return response(lease);
    }
    public UploadEditLease find(String tenant,String owner,String id) {
        String raw=redis.opsForValue().get(key(tenant,id));
        if (raw==null) return null;
        return checked(raw,tenant,owner,id);
    }
    public boolean canReadStatus(String tenant,String owner,String id) {
        String raw=redis.opsForValue().get(key(tenant,id));
        if (raw==null) return false;
        Map<?,?> state=JsonUtil.getJsonToBean(raw,Map.class);
        UploadEditLease lease=JsonUtil.getJsonToBean(state.get("lease"),UploadEditLease.class);
        return Objects.equals(owner,lease.getOwnerId()) && Objects.equals(tenant,lease.getTenantId()) && Objects.equals(id,lease.getLeaseId());
    }
    private UploadEditLease checked(String raw,String tenant,String owner,String id) {
        Map<?,?> state=JsonUtil.getJsonToBean(raw,Map.class);
        UploadEditLease lease=JsonUtil.getJsonToBean(state.get("lease"),UploadEditLease.class);
        if (!lease.isRevisionTemporary() || !Objects.equals(tenant,lease.getTenantId()) || !Objects.equals(owner,lease.getOwnerId())
                || !Objects.equals(id,lease.getLeaseId())) throw denied();
        UploadEditContext context=JsonUtil.getJsonToBean(state.get("context"),UploadEditContext.class);
        access.authorizeTemporary(context,owner);
        verify(Objects.toString(state.get("receipt"),""),context,tenant,owner);
        lease.setAllowDownload(access.allowDownload(context));
        lease.setQianpiyeAnnotatorEnabled(access.qianpiyeAnnotatorEnabled(context));
        return lease;
    }
    public boolean save(UploadEditSaveCommand command) {
        String key=key(command.getTenantId(),command.getLeaseId());
        String raw=redis.opsForValue().get(key);
        if (raw==null) return false;
        UploadEditLease lease=checked(raw,command.getTenantId(),command.getOwnerId(),command.getLeaseId());
        if (Objects.equals(command.getOperationId(),lease.getLastOperationId())) return true;
        if (!text(command.getOperationId()) || command.getFile()==null
                || !Objects.toString(command.getFile().get("fileId"),"").contains(","+lease.getLeaseId()+",")) throw denied();
        Map<String,Object> state=JsonUtil.getJsonToBean(raw,Map.class);
        UploadEditContext context=JsonUtil.getJsonToBean(state.get("context"),UploadEditContext.class);
        Map<String,Object> proof=new LinkedHashMap<>(verify(Objects.toString(state.get("receipt"),""),context,lease.getTenantId(),lease.getOwnerId()));
        Map<String,Object> file=new LinkedHashMap<>();
        for(String field:List.of("fileId","name","fileSize","fileExtension","url")) file.put(field,command.getFile().get(field));
        if (!jnpf.base.util.UploadEditStateMachine.supportsEditing(file.get("name"))) throw denied();
        Object savedAt = command.getFile().get("contentSavedAt");
        jnpf.base.util.UploadTemplateSource.inherit(file, lease.getFile());
        if (savedAt instanceof Number) file.put("contentSavedAt", savedAt);
        proof.put("file",new LinkedHashMap<>(file));
        long remaining=(((Number)proof.get("expiresAt")).longValue()-System.currentTimeMillis())/1000;
        if (remaining<=0) throw denied();
        String receipt=TicketUtil.createTicket(JsonUtil.getObjectToString(proof),remaining);
        file.put("revisionEditReceipt",receipt); lease.setFile(file); lease.setLastOperationId(command.getOperationId());
        state.put("lease",lease); state.put("receipt",receipt);
        if (!Long.valueOf(1).equals(redis.execute(CAS,List.of(key),raw,JsonUtil.getObjectToString(state)))) {
            TicketUtil.deleteTicket(receipt); return false;
        }
        return true;
    }
    /** 原生落库之前校验带入来源并移除仅供未保存页面使用的凭据。 */
    public void prepareSave(jnpf.base.model.flow.DataModel model) {
        Map<String,Object> data = model.getDataNewMap();
        String tenant = UploadEditDraftService.tenant(UserProvider.getUser());
        String owner = UserProvider.getLoginUserId();
        UploadEditContext mainContext = new UploadEditContext();
        mainContext.setModelId(model.getVisualId()); mainContext.setField("wenjian");
        if (data.containsKey("wenjian") && access.isDmsAuditTemporary(mainContext)) {
            Object original = data.get("wenjian");
            List<Map<String,Object>> mainFiles = jnpf.base.util.UploadEditStateMachine.files(original);
            // 修订凭据由下面的原生保存校验核实;普通上传不能携带任意模板标记。
            List<Map<String,Object>> ordinary = mainFiles.stream()
                    .filter(file -> !file.containsKey("revisionEditReceipt")).toList();
            jnpf.base.util.UploadTemplateSource.normalize(ordinary, List.of(), tenant, owner);
            data.put("wenjian", original instanceof String ? JsonUtil.getObjectToString(mainFiles) : mainFiles);
        }
        sanitizeFiles(data, "wenjian", "", data, model.getVisualId(), tenant, owner);
        Object rows = data.get("jilu_mingxi");
        if (rows instanceof List<?>) for (Object row : (List<?>) rows) {
            if (row instanceof Map<?,?>) sanitizeFiles((Map<String,Object>) row, "jilu_wenjian",
                    "jilu_mingxi", data, model.getVisualId(), tenant, owner);
        }
    }
 
    private void sanitizeFiles(Map<String,Object> row, String field, String parent, Map<String,Object> data,
                               String modelId, String tenant, String owner) {
        Object original = row.get(field);
        if (original == null || !(original instanceof List<?> || original instanceof String)) return;
        boolean hasReceipt = original instanceof String
                ? ((String) original).contains("\"revisionEditReceipt\"")
                : ((List<?>) original).stream().anyMatch(value -> value instanceof Map<?,?>
                    && ((Map<?,?>) value).containsKey("revisionEditReceipt"));
        if (!hasReceipt) return;
        // 无修订凭据的普通业务字段保持原值与原生保存路径。
        List<Map<String,Object>> files = jnpf.base.util.UploadEditStateMachine.files(original);
        if (files.stream().noneMatch(file -> file.containsKey("revisionEditReceipt"))) return;
        List<Map<String,Object>> clean = new ArrayList<>();
        for (Map<String,Object> file : files) {
            if (!file.containsKey("revisionEditReceipt")) { clean.add(file); continue; }
            UploadEditContext context = new UploadEditContext();
            context.setModelId(modelId); context.setField(field); context.setParentField(parent);
            Map<String,Object> proof = verify(Objects.toString(file.get("revisionEditReceipt"), ""), context, tenant, owner);
            Map<String,Object> canonical = (Map<String,Object>) proof.get("file");
            if (!"dms-audit-upload".equals(proof.get("purpose"))
                    && (!Objects.equals(proof.get("sourceItemId"), data.get("xiuding_shenqing_zixiang_id"))
                    || !Objects.equals(proof.get("versionId"), row.get("yuan_wenjian_banben_id")))) throw denied();
            if (!Objects.equals(canonical.get("fileId"), file.get("fileId"))) throw denied();
            // 仅内部来源核验服务可返回原版本完整附件;新上传和编辑副本仍采用原规范值。
            if ("dms-revision-edit".equals(proof.get("purpose")) && proof.get("sourceFileJson") instanceof String) {
                canonical = JsonUtil.getJsonToBean((String)proof.get("sourceFileJson"), Map.class);
                if (!Objects.equals(canonical.get("fileId"), file.get("fileId"))
                        || !Objects.equals(canonical.get("fileId"), proof.get("originFileId"))) throw denied();
            }
            clean.add(new LinkedHashMap<>(canonical));
        }
        row.put(field, original instanceof String ? JsonUtil.getObjectToString(clean) : clean);
    }
 
    public Map<String,Object> result(String id) {
        String tenant=UploadEditDraftService.tenant(UserProvider.getUser()),owner=UserProvider.getLoginUserId();
        UploadEditLease lease=find(tenant,owner,id);
        if (lease==null) throw denied();
        return response(lease);
    }
    public void cleanup(String tenant,String owner,String id) {
        String key=key(tenant,id),raw=redis.opsForValue().get(key);
        if (raw==null) return;
        Map<?,?> state=JsonUtil.getJsonToBean(raw,Map.class);
        UploadEditLease lease=JsonUtil.getJsonToBean(state.get("lease"),UploadEditLease.class);
        if (!Objects.equals(owner,lease.getOwnerId())) throw denied();
        // 不删除附件实体:原生表单可能已经采用。删除会话阻止后续回调重新写入。
        redis.delete(key);
    }
    private Map<String,Object> verify(String receipt,UploadEditContext context,String tenant,String owner) {
        String raw = text(receipt) ? TicketUtil.parseTicket(receipt) : null;
        Map<String,Object> upload = raw == null ? Collections.emptyMap() : JsonUtil.getJsonToBean(raw, Map.class);
        if ("dms-audit-upload".equals(upload.get("purpose"))) {
            if (!Objects.equals(tenant, upload.get("tenantId")) || !Objects.equals(owner, upload.get("ownerId"))
                    || !Objects.equals(context.getModelId(), upload.get("modelId"))
                    || !Objects.equals(context.getField(), upload.get("field"))
                    || !Objects.toString(context.getParentField(), "").equals(upload.get("parentField"))
                    || !(upload.get("expiresAt") instanceof Number)
                    || ((Number)upload.get("expiresAt")).longValue() <= System.currentTimeMillis()
                    || !(upload.get("file") instanceof Map<?,?>) || !access.isDmsAuditTemporary(context)) throw denied();
            return upload;
        }
        DmsRevisionEditRequest request=new DmsRevisionEditRequest();
        request.setReceipt(receipt);request.setTenantId(tenant);request.setOwnerId(owner);request.setModelId(context.getModelId());
        request.setField(context.getField());request.setParentField(context.getParentField());
        ActionResult<Map<String,Object>> result=sources.verify(request);
        if (result==null || !ActionResultCode.SUCCESS.getCode().equals(result.getCode()) || result.getData()==null) throw denied();
        return result.getData();
    }
    private static Map<String,Object> response(UploadEditLease lease) {
        Map<String,Object> result=new LinkedHashMap<>();result.put("leaseId",lease.getLeaseId());result.put("draftId",lease.getDraftId());
        result.put("source",lease.getFile());result.put("files",List.of(lease.getFile()));result.put("revision",0);
        result.put("canDiscard",false);result.put("revisionTemporary",true);return result;
    }
    private static String key(String tenant,String id) {
        if (!text(tenant) || id==null || !id.matches("[a-f0-9]{32}")) throw denied();
        return "upload-edit:dms-revision:"+DigestUtil.sha256Hex(tenant)+":"+id;
    }
    private static boolean text(String value) { return value!=null && !value.isBlank(); }
    private static DataException denied() { return new DataException("附件编辑授权已失效,请重新选择来源或上传附件"); }
}