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("附件编辑授权已失效,请重新选择来源或上传附件"); }
|
}
|