package cn.iocoder.yudao.module.system.service.storage; import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; import cn.iocoder.yudao.module.system.controller.admin.storage.vo.SystemStorageAttachmentSaveReqVO; import cn.iocoder.yudao.module.system.dal.dataobject.storage.SystemAgentImportSessionDO; import cn.iocoder.yudao.module.system.dal.dataobject.storage.SystemStorageAttachmentDO; import cn.iocoder.yudao.module.system.dal.dataobject.storage.SystemStorageBlobDO; import cn.iocoder.yudao.module.system.dal.mysql.storage.SystemAgentImportSessionMapper; import cn.iocoder.yudao.module.system.dal.mysql.storage.SystemStorageAttachmentMapper; import cn.iocoder.yudao.module.system.framework.agent.AgentBridgeException; import cn.iocoder.yudao.module.system.framework.storage.config.StorageProperties; import jakarta.annotation.Resource; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.HexFormat; import java.util.List; import java.util.Objects; @Service public class SystemAgentImportBridgeService { public static final long MAX_FILE_BYTES = 8L * 1024 * 1024; public static final String RECORD_TYPE = "agent_import_session"; @Resource private SystemAgentImportSessionMapper sessionMapper; @Resource private SystemStorageAttachmentMapper attachmentMapper; @Resource private SystemStorageBlobService blobService; @Resource private SystemStorageAttachmentService attachmentService; @Resource private StorageProperties storageProperties; @Transactional(rollbackFor = Exception.class) public Long createSession() { Long userId = SecurityFrameworkUtils.getLoginUserId(); if (userId == null) { throw new AgentBridgeException(HttpStatus.UNAUTHORIZED, "UNAUTHORIZED", "未登录或登录已过期"); } SystemAgentImportSessionDO session = new SystemAgentImportSessionDO(); session.setOwnerUserId(userId); session.setStatus("active"); sessionMapper.insert(session); return session.getId(); } @Transactional(rollbackFor = Exception.class) public Long bindAttachment(Long sessionId, Long blobId) { requireOwnedSession(sessionId); SystemStorageBlobDO blob = blobService.getStorageBlob(blobId); requireOwnedBlob(blob); SystemStorageAttachmentSaveReqVO req = new SystemStorageAttachmentSaveReqVO(); req.setApplication("file"); req.setRecordType(RECORD_TYPE); req.setRecordId(sessionId); SystemStorageAttachmentSaveReqVO.BlobItem item = new SystemStorageAttachmentSaveReqVO.BlobItem(); item.setBlobId(blobId); req.setBlobItems(List.of(item)); attachmentService.bindAttachments(req); // 通用 bind 对同一会话是替换语义:重复绑定时旧附件行会被删除。 return attachmentMapper.selectListByApplicationAndRecordTypeAndRecordId("file", RECORD_TYPE, sessionId) .stream().filter(row -> blobId.equals(row.getStorageBlobId())) .map(SystemStorageAttachmentDO::getId).findFirst() .orElseThrow(() -> new AgentBridgeException(HttpStatus.CONFLICT, "ATTACHMENT_BIND_FAILED", "附件绑定失败,请重新上传后再试", true, null)); } public SystemAgentAttachmentRef requireOwnedAttachmentRef(Long attachmentId) { SystemStorageAttachmentDO attachment = attachmentMapper.selectById(attachmentId); if (attachment == null) { throw new AgentBridgeException(HttpStatus.CONFLICT, "ATTACHMENT_REPLACED", "附件已失效(可能被重新上传替换),请重新上传后再试", true, null); } if (!RECORD_TYPE.equals(attachment.getRecordType())) { throw new AgentBridgeException(HttpStatus.FORBIDDEN, "ATTACHMENT_FORBIDDEN", "附件不可访问"); } requireOwnedSession(attachment.getRecordId()); SystemStorageBlobDO blob = blobService.getStorageBlob(attachment.getStorageBlobId()); requireOwnedBlob(blob); Path file = resolveBlobFile(blob); String filename = blob.getOriginalFilename(); if (filename == null || filename.isBlank()) { filename = "attachment"; } return new SystemAgentAttachmentRef(attachmentId, attachment.getRecordId(), filename, blob.getByteSize(), file); } public String sha256OfAttachment(Long attachmentId) { SystemAgentAttachmentRef ref = requireOwnedAttachmentRef(attachmentId); try (InputStream in = Files.newInputStream(ref.path())) { MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] buffer = new byte[8192]; long total = 0; int read; while ((read = in.read(buffer)) != -1) { total += read; if (total > MAX_FILE_BYTES) { throw new AgentBridgeException(HttpStatus.FORBIDDEN, "FILE_TOO_LARGE", "附件超过 8 MiB"); } digest.update(buffer, 0, read); } return HexFormat.of().formatHex(digest.digest()); } catch (IOException ex) { throw new AgentBridgeException(HttpStatus.FORBIDDEN, "ATTACHMENT_UNREADABLE", "附件不可访问"); } catch (NoSuchAlgorithmException ex) { throw new IllegalStateException("SHA-256 不可用", ex); } } private Path resolveBlobFile(SystemStorageBlobDO blob) { try { Path root = Path.of(storageProperties.getPath()).toRealPath(); Path file = root.resolve(blob.getPath() == null ? "" : blob.getPath()) .resolve(blob.getUidFilename()).toRealPath(); if (!file.startsWith(root) || !Files.isRegularFile(file) || Files.size(file) > MAX_FILE_BYTES) { throw new AgentBridgeException(HttpStatus.FORBIDDEN, "ATTACHMENT_UNREADABLE", "附件不可访问"); } return file; } catch (IOException ex) { throw new AgentBridgeException(HttpStatus.FORBIDDEN, "ATTACHMENT_UNREADABLE", "附件不可访问"); } } private void requireOwnedSession(Long sessionId) { SystemAgentImportSessionDO session = sessionMapper.selectById(sessionId); Long userId = SecurityFrameworkUtils.getLoginUserId(); if (session == null || userId == null || !Objects.equals(session.getOwnerUserId(), userId) || !"active".equals(session.getStatus())) { throw new AgentBridgeException(HttpStatus.FORBIDDEN, "SESSION_FORBIDDEN", "补录会话不可访问"); } } private void requireOwnedBlob(SystemStorageBlobDO blob) { Long userId = SecurityFrameworkUtils.getLoginUserId(); if (blob == null || userId == null || !String.valueOf(userId).equals(blob.getCreator()) || blob.getByteSize() == null || blob.getByteSize() > MAX_FILE_BYTES) { throw new AgentBridgeException(HttpStatus.FORBIDDEN, "BLOB_FORBIDDEN", "文件不可访问或超过 8 MiB"); } } }