feat: add ONLYOFFICE document preview for contract attachments

DocumentPreviewController provides preview config and file streaming endpoints. ContractDetailView adds 'preview' button to attachment list and opens ONLYOFFICE iframe dialog.

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-05-27 08:37:09 +08:00
parent 8c788ea388
commit 8ee9aa51d8
2 changed files with 104 additions and 2 deletions
@@ -0,0 +1,84 @@
package cn.craftlabs.platform.api.preview;
import cn.craftlabs.platform.api.persistence.attachment.PlatformContractAttachment;
import cn.craftlabs.platform.api.persistence.attachment.PlatformContractAttachmentMapper;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import java.util.Map;
@RestController
@RequestMapping("/api/v1/preview")
public class DocumentPreviewController {
private final PlatformContractAttachmentMapper attachmentMapper;
@Value("${onlyoffice.url:http://craftsupport.cn:8088}")
private String onlyofficeUrl;
public DocumentPreviewController(PlatformContractAttachmentMapper attachmentMapper) {
this.attachmentMapper = attachmentMapper;
}
@GetMapping("/{attachmentId}")
public ResponseEntity<Map<String, Object>> getPreviewConfig(@PathVariable("attachmentId") Long attachmentId) {
PlatformContractAttachment attachment = attachmentMapper.selectById(attachmentId);
if (attachment == null) {
throw new ResponseStatusException(org.springframework.http.HttpStatus.NOT_FOUND, "附件不存在");
}
String ext = "";
String fileName = attachment.getFileName();
if (fileName != null && fileName.contains(".")) {
ext = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase();
}
Map<String, Object> config = new java.util.LinkedHashMap<>();
config.put("document", Map.of(
"fileType", ext,
"key", "attachment_" + attachmentId,
"title", attachment.getFileName(),
"url", getFileUrl(attachmentId),
"permissions", Map.of("download", false, "edit", false, "print", false)
));
config.put("editorConfig", Map.of(
"mode", "view",
"customization", Map.of("autosave", false, "chat", false, "compactHeader", true)
));
config.put("documentServerUrl", onlyofficeUrl);
return ResponseEntity.ok(config);
}
@GetMapping("/{attachmentId}/file")
public ResponseEntity<Resource> getFile(@PathVariable("attachmentId") Long attachmentId) {
PlatformContractAttachment attachment = attachmentMapper.selectById(attachmentId);
if (attachment == null) {
throw new ResponseStatusException(org.springframework.http.HttpStatus.NOT_FOUND, "附件不存在");
}
java.io.File file = new java.io.File(attachment.getFilePath());
if (!file.exists()) {
throw new ResponseStatusException(org.springframework.http.HttpStatus.NOT_FOUND, "文件不存在");
}
FileSystemResource resource = new FileSystemResource(file);
String contentType = attachment.getContentType();
if (contentType == null) contentType = "application/octet-stream";
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(contentType))
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + attachment.getFileName() + "\"")
.body(resource);
}
private String getFileUrl(Long attachmentId) {
return "/api/v1/preview/" + attachmentId + "/file";
}
}