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");
|
}
|
}
|
}
|