package jnpf.base.service; import cn.hutool.core.util.IdUtil; import cn.hutool.crypto.digest.DigestUtil; import jnpf.base.UserInfo; import jnpf.base.model.uploadedit.*; 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.stereotype.Service; import java.util.*; import java.util.concurrent.TimeUnit; /** 无稳定业务 ID 时仅保存短期打开凭据;回调不写内容、不替换表单附件、不提供恢复。 */ @Service @RequiredArgsConstructor public class UploadEditTemporaryService { private final UploadEditAccess access; private final DmsAuditTemporaryEditService revisionService; private final StringRedisTemplate redis; public void prepareRevisionSave(jnpf.base.model.flow.DataModel model) { revisionService.prepareSave(model); } public Map begin(UploadEditCommand command) { if (command.getFiles() != null && command.getFiles().size() == 1 && command.getFiles().get(0).containsKey("revisionEditReceipt")) return revisionService.begin(command); UploadEditContext context = command.getContext(); if (context == null || (context.getRecordId() != null && !context.getRecordId().isEmpty() && (context.getParentField() == null || context.getParentField().isEmpty() || (context.getRowId() != null && !context.getRowId().isEmpty())))) { throw new DataException("有跟踪 ID 的附件应使用工作稿编辑"); } UserInfo user = UserProvider.getUser(); access.authorizeTemporary(context, user.getUserId()); if (command.getFiles() == null || command.getFiles().size() != 1) throw new DataException("请选择一个新上传的文件"); Map requested = command.getFiles().get(0); String proofJson = requested.get("uploadReceipt") instanceof String ? TicketUtil.parseTicket((String) requested.get("uploadReceipt")) : null; Map proof = proofJson == null ? Collections.emptyMap() : JsonUtil.getJsonToBean(proofJson, Map.class); String tenant = UploadEditDraftService.tenant(user); if (!"upload-edit-source".equals(proof.get("purpose")) || !tenant.equals(proof.get("tenantId")) || !user.getUserId().equals(proof.get("ownerId")) || !Objects.equals(command.getFileId(), proof.get("fileId")) || !jnpf.base.util.UploadEditStateMachine.supportsEditing(proof.get("name"))) { throw new DataException("临时编辑仅支持本次新上传且归属可核验的 DOC、DOCX,请重新上传"); } Map file = new LinkedHashMap<>(); for (String key : Arrays.asList("fileId", "name", "fileSize", "fileExtension", "url", "contentSavedAt")) file.put(key, proof.get(key)); jnpf.base.util.UploadTemplateSource.inherit(file, proof); if (access.isDmsAuditTemporary(context)) return revisionService.beginUpload(command, file); UploadEditLease lease = new UploadEditLease(); lease.setLeaseId(IdUtil.simpleUUID()); lease.setDraftId(lease.getLeaseId()); lease.setTenantId(tenant); lease.setOwnerId(user.getUserId()); lease.setTracked(false); lease.setFile(file); Map state = new LinkedHashMap<>(); state.put("lease", lease); state.put("context", context); redis.opsForValue().set(key(tenant, lease.getLeaseId()), JsonUtil.getObjectToString(state), 4, TimeUnit.HOURS); Map result = new LinkedHashMap<>(); result.put("leaseId", lease.getLeaseId()); result.put("draftId", lease.getDraftId()); result.put("source", file); result.put("files", Collections.singletonList(file)); result.put("revision", 0); result.put("canDiscard", false); return result; } /** 状态轮询不授予文件访问权;保留 Redis 到期/清理和租约归属检查。 */ public boolean canReadStatus(String tenant, String owner, String leaseId) { if (revisionService.canReadStatus(tenant, owner, leaseId)) return true; String json = redis.opsForValue().get(key(tenant, leaseId)); if (json == null) return false; Map state = JsonUtil.getJsonToBean(json, Map.class); UploadEditLease lease = JsonUtil.getJsonToBean(state.get("lease"), UploadEditLease.class); return lease != null && Objects.equals(tenant, lease.getTenantId()) && Objects.equals(owner, lease.getOwnerId()) && Objects.equals(leaseId, lease.getLeaseId()); } public UploadEditLease find(String tenant, String owner, String leaseId) { UploadEditLease revision = revisionService.find(tenant, owner, leaseId); if (revision != null) return revision; String json = redis.opsForValue().get(key(tenant, leaseId)); if (json == null) return null; Map state = JsonUtil.getJsonToBean(json, Map.class); UploadEditLease lease = JsonUtil.getJsonToBean(state.get("lease"), UploadEditLease.class); if (!owner.equals(lease.getOwnerId())) throw new DataException("临时编辑会话不可访问"); UploadEditContext context = JsonUtil.getJsonToBean(state.get("context"), UploadEditContext.class); access.authorizeTemporary(context, owner); lease.setAllowDownload(access.allowDownload(context)); lease.setQianpiyeAnnotatorEnabled(access.qianpiyeAnnotatorEnabled(context)); return lease; } /** 清理接口仅撤销本人的临时会话。无 ID 回调不落文件,因此无需删除源文件或历史文件。 */ public void cleanup(String leaseId) { UserInfo user = UserProvider.getUser(); String tenant = UploadEditDraftService.tenant(user); revisionService.cleanup(tenant, user.getUserId(), leaseId); String key = key(tenant, leaseId); String json = redis.opsForValue().get(key); if (json == null) return; Map state = JsonUtil.getJsonToBean(json, Map.class); UploadEditLease lease = JsonUtil.getJsonToBean(state.get("lease"), UploadEditLease.class); if (!user.getUserId().equals(lease.getOwnerId())) throw new DataException("临时编辑会话不可访问"); redis.delete(key); } public boolean saveRevision(UploadEditSaveCommand command) { return revisionService.save(command); } public Map revisionResult(String leaseId) { return revisionService.result(leaseId); } private String key(String tenant, String leaseId) { if (tenant == null || leaseId == null || !leaseId.matches("[a-f0-9]{32}")) throw new DataException("临时编辑标识无效"); return "upload-edit:temporary:" + DigestUtil.sha256Hex(tenant) + ":" + leaseId; } }