package jnpf.bizcommon.onlyoffice.service.impl; import cn.hutool.crypto.digest.DigestUtil; import cn.hutool.jwt.JWTUtil; import jnpf.base.ActionResult; import jnpf.base.ActionResultCode; import jnpf.base.model.uploadedit.UploadEditLease; import jnpf.base.model.uploadedit.UploadEditSaveCommand; import jnpf.bizcommon.onlyoffice.entity.OnlyOfficeCallbackParam; import jnpf.bizcommon.onlyoffice.entity.OnlyOfficeSessionEntity; import jnpf.exception.DataException; import jnpf.onlinedev.UploadEditApi; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import java.util.Map; import java.util.Objects; @Component @RequiredArgsConstructor public class UploadEditSessionSupport { private final UploadEditApi api; private final UploadEditStorage storage; public static boolean matches(OnlyOfficeSessionEntity session) { return session != null && UploadEditSceneConfigBuilder.SCENE.equals(session.getBizScene()) && UploadEditSceneConfigBuilder.MODULE.equals(session.getBizModule()); } public void requireStatusAccess(String tenantId, String ownerId, String leaseId) { if (!Boolean.TRUE.equals(require(api.canReadStatus(tenantId, ownerId, leaseId)))) { throw new DataException("编辑租约已失效或不可访问"); } } public UploadEditLease lease(String tenantId, String ownerId, String leaseId) { UploadEditLease lease = require(api.lease(tenantId, ownerId, leaseId)); if (lease == null || lease.getFile() == null) throw new DataException("附件编辑租约不可用"); return lease; } public void save(OnlyOfficeSessionEntity session, OnlyOfficeCallbackParam callback, byte[] candidate) { // 回调到达时重新检查当前流程权限,不能复用打开时的权限结论。 UploadEditLease lease = lease(session.getTenantId(), session.getLockUserId(), session.getFileId()); // 普通未保存附件维持原行为;只有服务端核验的DMS修订来源可另存临时副本。 if (!lease.isTracked() && !lease.isRevisionTemporary()) return; String operationId = DigestUtil.sha256Hex(callback.getKey() + "\n" + callback.getStatus() + "\n" + callback.getUrl()); if (operationId.equals(lease.getLastOperationId())) return; Map file = storage.save(session.getTenantId(), session.getFileId(), savedFileName(session.getFileName(), callback.getFiletype()), candidate); UploadEditSaveCommand command = new UploadEditSaveCommand(); command.setTenantId(session.getTenantId()); command.setOwnerId(lease.getOwnerId()); command.setLeaseId(lease.getLeaseId()); command.setOperationId(operationId); command.setFile(file); if (!Boolean.TRUE.equals(require(api.save(command)))) throw new DataException("附件已替换、放弃或采用,本次编辑产物不再生效"); } static String savedFileName(String sourceName, String outputType) { boolean sourceDoc = sourceName != null && sourceName.toLowerCase(java.util.Locale.ROOT).endsWith(".doc"); // DOC 可能由 Document Server 转成 DOCX;缺少实际输出类型时不能冒用源后缀。 if (outputType == null || outputType.isBlank()) { if (sourceDoc) throw new DataException("DOC 编辑产物缺少输出格式,无法安全保存"); return sourceName; } String extension = outputType.toLowerCase(java.util.Locale.ROOT); if ((!"docx".equals(extension) && !(sourceDoc && "doc".equals(extension))) || sourceName == null) { throw new DataException("附件编辑产物格式不受支持"); } if (sourceName.toLowerCase(java.util.Locale.ROOT).endsWith("." + extension)) return sourceName; return sourceName.substring(0, sourceName.lastIndexOf('.')) + "." + extension; } /** 已经过签名验证;进一步绑定请求体,拒绝拿编辑器配置 JWT 冒充保存回调。 */ public static void requireBoundCallback(OnlyOfficeCallbackParam callback, String authorization) { String token = authorization == null || authorization.isBlank() ? callback.getToken() : authorization.startsWith("Bearer ") ? authorization.substring(7).trim() : authorization.trim(); Object payload = JWTUtil.parseToken(token).getPayload("payload"); Map claims = payload instanceof Map ? (Map) payload : JWTUtil.parseToken(token).getPayloads(); if (!Objects.equals(callback.getKey(), claims.get("key")) || !(claims.get("status") instanceof Number) || !Objects.equals(callback.getStatus(), ((Number) claims.get("status")).intValue()) || !Objects.equals(callback.getUrl(), claims.get("url")) || !Objects.equals(callback.getFiletype(), claims.get("filetype"))) { throw new DataException("保存回调与已签名内容不一致"); } } private static T require(ActionResult result) { if (result == null || !ActionResultCode.SUCCESS.getCode().equals(result.getCode())) { throw new DataException(result == null ? "附件编辑服务不可用" : result.getMsg()); } return result.getData(); } }