集成onlyoffice预览和在线编辑

This commit is contained in:
cwchen 2025-11-10 09:25:19 +08:00
parent a04e5a7c4a
commit 818b4e7566
17 changed files with 994 additions and 60 deletions

View File

@ -1,44 +1,36 @@
package com.bonus.web.controller.common;
import com.bonus.common.core.domain.AjaxResult;
import com.bonus.common.domain.file.vo.OnlyOfficeCallback;
import com.bonus.file.service.OnlyOfficeService;
import com.bonus.common.domain.file.vo.Callback;
import com.bonus.web.service.common.DocumentService;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@RestController
@RequestMapping("/documents")
@CrossOrigin(origins = "*")
public class DocumentController {
@Resource(name = "OnlyOfficeService")
private OnlyOfficeService onlyOfficeService;
@Resource
private DocumentService documentService;
@ApiOperation(value = "获取onlyoffice配置", notes = "获取onlyoffice配置")
@GetMapping("/config")
public AjaxResult getEditorConfig(
@RequestParam String fileId,
@RequestParam String fileName,
@RequestParam String type,
@RequestParam(defaultValue = "view") String mode) {
try {
Map<String, Object> config = onlyOfficeService.getConfigWithToken(fileId, fileName, mode);
return AjaxResult.success(config);
} catch (Exception e) {
return AjaxResult.error();
}
return documentService.getEditorConfig(fileId,type,mode);
}
@ApiOperation(value = "onlyoffice文档编辑回调", notes = "onlyoffice文档编辑回调")
@PostMapping("/callback")
public AjaxResult handleCallback(@RequestBody OnlyOfficeCallback callback) {
try {
onlyOfficeService.handleCallback(callback);
return AjaxResult.success();
} catch (Exception e) {
return AjaxResult.error();
}
public String handleCallback(@RequestBody Callback callback, @RequestParam String fileUrl, HttpServletRequest request, HttpServletResponse response) {
return documentService.handleCallback(callback,fileUrl,request,response);
}
}

View File

@ -0,0 +1,85 @@
package com.bonus.web.service.common;
import com.bonus.common.core.domain.AjaxResult;
import com.bonus.common.domain.file.po.ResourceFilePo;
import com.bonus.common.domain.file.vo.Callback;
import com.bonus.common.domain.file.vo.ResourceFileVo;
import com.bonus.file.service.OnlyOfficeService2;
import com.bonus.file.service.SourceFileService;
import com.bonus.framework.web.service.TokenService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Objects;
/**
* @className:DocumentService
* @author:cwchen
* @date:2025-11-06-15:09
* @version:1.0
* @description:onlyoffice文档业务层
*/
@Service(value = "DocumentService")
@Slf4j
public class DocumentService {
@Resource(name = "OnlyOfficeService2")
private OnlyOfficeService2 onlyOfficeService2;
@Resource
private TokenService tokenService;
@Resource(name = "SourceFileService")
private SourceFileService sourceFileService;
/**
* 获取onlyoffice配置
*
* @param fileId
* @param fileName
* @param type
* @param mode
* @return AjaxResult
* @author cwchen
* @date 2025/11/6 15:15
*/
public AjaxResult getEditorConfig(String fileId, String type, String mode) {
try {
ResourceFilePo resourceFilePo = new ResourceFilePo();
resourceFilePo.setSourceId(Long.parseLong(fileId));
ResourceFileVo fileVo = sourceFileService.getFileById(resourceFilePo);
if(Objects.isNull(fileVo)) {
return AjaxResult.error("文件不存在");
}
// config = onlyOfficeService.getConfigWithToken(fileId, fileName, type, mode);
return onlyOfficeService2.getOnlyOfficeConfig(fileVo,type,mode);
} catch (Exception e) {
log.error(e.toString(),e);
return AjaxResult.error();
}
}
/**
* onlyoffice文档编辑回调
*
* @param callback
* @return AjaxResult
* @author cwchen
* @date 2025/11/6 15:16
*/
public String handleCallback(Callback callback, String fileUrl, HttpServletRequest request, HttpServletResponse response) {
try {
log.info("收到 OnlyOffice 回调, 状态: {}, Key: {}", callback.getStatus(), callback.getKey());
log.info("callback:{}",callback);
// onlyOfficeService.handleCallback(callback,response);
return onlyOfficeService2.handleCallback(callback,fileUrl,request,response);
} catch (Exception e) {
log.error(e.toString(),e);
return "{\"error\":\"1\"}";
}
}
}

View File

@ -203,6 +203,25 @@
<artifactId>jjwt</artifactId>
<version>${io.jsonwebtoken.version}</version>
</dependency>
<!--onlyoffice集成的sdk-->
<!--<dependency>
<groupId>com.onlyoffice</groupId>
<artifactId>docs-integration-sdk</artifactId>
<version>1.0.0</version>
</dependency>-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.19.2</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.11</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,73 @@
package com.bonus.common.domain.file.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
* @className:Callback
* @author:cwchen
* @date:2025-11-08-1:54
* @version:1.0
* @description:onlyoffice回调参数
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
public class Callback {
private String key;
private int status;
private String url;
@JsonProperty("changesurl")
private String changesUrl;
private History history;
private List<String> users;
private List<Map<String, Object>> actions;
@JsonProperty("lastsave")
private String lastSave;
@JsonProperty("forcesavetype")
private int forceSaveType;
private String token;
private String filetype;
// History 内部类
@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
public static class History {
private String serverVersion;
private List<Change> changes;
// Change 内部类
@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
public static class Change {
private String created;
private User user;
// User 内部类
@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
public static class User {
private String id;
private String name;
}
}
}
}

View File

@ -14,12 +14,19 @@ import java.util.Map;
*/
@Data
public class OnlyOfficeCallback {
/**
* 0 - 为命令服务执行强制保存请求
* 1 - 每次保存完成时都会执行强制保存请求例如单击 保存 按钮这仅在 forcesave 选项设置为 true时可用
* 2 - 强制保存请求由计时器按服务器配置中的设置执行
* 3 - 每次提交表单时都会执行强制保存请求 Complete & Submit 按钮被点击 )
*/
private Integer forcesavetype;
private Integer status;
private String key;
private String url;
private List<Map<String, Object>> actions;
private String lastsave;
private Boolean users;
// private Boolean users;
private Boolean notmodified;
// 状态常量

View File

@ -0,0 +1,127 @@
package com.bonus.common.domain.file.vo;
import com.bonus.common.utils.StringUtils;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.io.Serializable;
/**
* onlyOffice配置
* 这里的配置会从 application.yml或application.properties 中读取
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Component(value = "OnlyOfficeConfig")
@ConfigurationProperties(prefix = "only-office")
public class OnlyOfficeConfig implements Serializable {
private static final long serialVersionUID = 1L;
private String secret;
private Config config;
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Config implements Serializable {
private static final long serialVersionUID = 1L;
private Document document;
private EditorConfig editorConfig;
private String type;
private String token;
private String documentType;
private String height = "100%";
private String width = "100%";
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Document implements Serializable {
private static final long serialVersionUID = 1L;
private String title;
private String fileType;
private String key;
private String url;
private Permissions permissions;
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Permissions implements Serializable {
private static final long serialVersionUID = 1L;
private Boolean edit;
private Boolean print;
private Boolean download;
private Boolean fillForms;
private Boolean review;
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class EditorConfig implements Serializable {
private static final long serialVersionUID = 1L;
private String callbackUrl;
private String lang;
private CoEditing coEditing;
private Customization customization;
private String region;
private User user;
public User getUser(){
return StringUtils.isNull(user)?new User():user;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class CoEditing implements Serializable {
private static final long serialVersionUID = 1L;
private String mode;
private Boolean change;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Customization implements Serializable {
private static final long serialVersionUID = 1L;
private Boolean forcesave;
private Boolean autosave;
private Boolean comments;
private Boolean compactHeader;
private Boolean compactToolbar;
private Boolean compatibleFeatures;
private Features features;
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Features implements Serializable {
private static final long serialVersionUID = 1L;
private Spellcheck spellcheck;
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Spellcheck implements Serializable {
private static final long serialVersionUID = 1L;
private Boolean mode;
private Boolean change;
}
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class User implements Serializable {
private static final long serialVersionUID = 1L;
private String id;
private String name;
private String image;
private String group;
}
}
}
}

View File

@ -0,0 +1,62 @@
package com.bonus.common.enums;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import java.util.HashMap;
import java.util.Map;
/**
* @className:OnlyOfficeStatus枚举
* @author:cwchen
* @date:2025-11-08-2:06
* @version:1.0
* @description:
*/
public enum OnlyOfficeStatus {
EDITING(1),
SAVE(2),
SAVE_CORRUPTED(3),
CLOSED(4),
FORCESAVE(6),
FORCESAVE_CORRUPTED(7);
private final int id;
private static final Map<Integer, OnlyOfficeStatus> BY_ID = new HashMap<>();
static {
for (OnlyOfficeStatus status : values()) {
BY_ID.put(status.id, status);
}
}
OnlyOfficeStatus(int id) {
this.id = id;
}
@JsonValue
public int getId() {
return this.id;
}
@JsonCreator
public static OnlyOfficeStatus valueOfId(Integer code) {
if (code == null) {
return null;
}
return BY_ID.get(code);
}
// 添加一些实用方法
public static OnlyOfficeStatus fromId(int id) {
return BY_ID.get(id);
}
public boolean isSaveStatus() {
return this == SAVE || this == FORCESAVE;
}
public boolean isErrorStatus() {
return this == SAVE_CORRUPTED || this == FORCESAVE_CORRUPTED;
}
}

View File

@ -0,0 +1,80 @@
package com.bonus.common.utils;
/**
* @className:DocumentTypeUtils
* @author:cwchen
* @date:2025-11-08-3:24
* @version:1.0
* @description:
*/
import java.util.HashMap;
import java.util.Map;
public class DocumentTypeUtils {
private static final Map<String, String> EXTENSION_TO_TYPE = new HashMap<>();
static {
EXTENSION_TO_TYPE.put("docx", "word");
EXTENSION_TO_TYPE.put("doc", "word");
EXTENSION_TO_TYPE.put("pdf", "word");
EXTENSION_TO_TYPE.put("pptx", "slide");
EXTENSION_TO_TYPE.put("ppt", "slide");
EXTENSION_TO_TYPE.put("xlsx", "cell");
EXTENSION_TO_TYPE.put("xls", "cell");
// 可以继续添加其他格式
}
public static String getDocumentType(String fileUrl) {
if (fileUrl == null || fileUrl.isEmpty()) {
return "word";
}
String extension = getFileExtension(fileUrl);
return EXTENSION_TO_TYPE.getOrDefault(extension, "word");
}
public static String getFileExtension(String fileUrl) {
if (fileUrl == null || fileUrl.isEmpty()) {
return "";
}
String normalizedUrl = fileUrl.toLowerCase().trim();
int lastDotIndex = normalizedUrl.lastIndexOf(".");
int lastSlashIndex = Math.max(normalizedUrl.lastIndexOf("/"), normalizedUrl.lastIndexOf("\\"));
// 确保点号在最后一个斜杠之后
if (lastDotIndex > lastSlashIndex && lastDotIndex < normalizedUrl.length() - 1) {
return normalizedUrl.substring(lastDotIndex + 1);
}
return "";
}
// 额外工具方法
public static boolean isSupportedDocument(String fileUrl) {
String extension = getFileExtension(fileUrl);
return EXTENSION_TO_TYPE.containsKey(extension);
}
public static String getFileName(String fileUrl) {
if (fileUrl == null || fileUrl.isEmpty()) {
return "";
}
String normalizedUrl = fileUrl.replace('\\', '/');
int lastSlashIndex = normalizedUrl.lastIndexOf("/");
return lastSlashIndex == -1 ? normalizedUrl : normalizedUrl.substring(lastSlashIndex + 1);
}
// 添加更多实用方法
public static boolean isWordDocument(String fileUrl) {
return "word".equals(getDocumentType(fileUrl));
}
public static boolean isSlideDocument(String fileUrl) {
return "slide".equals(getDocumentType(fileUrl));
}
public static boolean isCellDocument(String fileUrl) {
return "cell".equals(getDocumentType(fileUrl));
}
}

View File

@ -11,7 +11,7 @@ import org.springframework.stereotype.Component;
* @version: 1.0
* @description: OnlyOffice 配置
*/
@Component(value = "OnlyOfficeConfig")
/*@Component(value = "OnlyOfficeConfig")
@Data
public class OnlyOfficeConfig {
@ -27,4 +27,4 @@ public class OnlyOfficeConfig {
@Value("${server.port}")
private String serverPort;
}
}*/

View File

@ -0,0 +1,165 @@
package com.bonus.file.service;
import com.bonus.common.domain.file.vo.Callback;
import com.bonus.common.enums.OnlyOfficeStatus;
import com.bonus.common.utils.FileUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang3.StringUtils;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.ByteArrayInputStream;
import java.util.List;
/**
* @className:CallBackService
* @author:cwchen
* @date:2025-11-08-0:28
* @version:1.0
* @description:onlyoffice回调业务层
*/
@Service(value = "CallBackService")
public class CallBackService {
@Value("${only-office.jwt-enabled}")
public Boolean jwtEnabled;
@Value("${only-office.secret}")
public String secret;
@Resource(name = "FileUploadService")
private FileUploadService fileUploadService;
@Resource(name = "OnlyOfficeJwtService")
private OnlyOfficeJwtService onlyOfficeJwtService;
private final ObjectMapper objectMapper = new ObjectMapper();
public Callback verifyCallback(Callback callback, String authorization) throws JsonProcessingException {
// 判断是否开启jwt校验
if (!Boolean.TRUE.equals(jwtEnabled)) {
// 未开启jwt校验
return callback;
}
String token = callback.getToken();
boolean fromHeader = false;
if (StringUtils.isEmpty(token) && StringUtils.isNotEmpty(authorization)) {
token = authorization.replace("Bearer ", "");
fromHeader = true;
}
if (StringUtils.isEmpty(token)) {
throw new RuntimeException("token不存在");
}
String payload = onlyOfficeJwtService.verify(token);
if (fromHeader) {
JSONObject data = new JSONObject(payload);
JSONObject jsonObject = data.getJSONObject("payload");
return objectMapper.readValue(jsonObject.toString(), Callback.class);
} else {
return objectMapper.readValue(payload, Callback.class);
}
}
public void processCallback(Callback callback, String fileUrl) throws Exception {
OnlyOfficeStatus status = OnlyOfficeStatus.fromId(callback.getStatus());
switch (status) {
case EDITING:
this.handlerEditing(callback, fileUrl);
break;
case SAVE:
this.handlerSave(callback, fileUrl);
break;
case SAVE_CORRUPTED:
this.handlerSaveCorrupted(callback, fileUrl);
break;
case CLOSED:
this.handlerClosed(callback, fileUrl);
break;
case FORCESAVE:
this.handlerForcesave(callback, fileUrl);
break;
case FORCESAVE_CORRUPTED:
this.handlerForcesaveCurrupted(callback, fileUrl);
break;
default:
throw new RuntimeException("Callback has no status");
}
}
public void handlerEditing(Callback callback, String fileUrl) throws Exception {
// 获取用户id
List<String> users = callback.getUsers();
}
public void handlerSave(Callback callback, String fileUrl) throws Exception {
// onlyoffice文档保存
String downloadUrl = callback.getUrl();
if (StringUtils.isEmpty(downloadUrl)) {
return;
}
// 获取文件字节数组
byte[] fileContent = downloadFileFromUrl(downloadUrl);
// 保存到 MinIO
saveToMinio(fileUrl, fileContent);
}
public void handlerSaveCorrupted(Callback callback, String fileUrl) throws Exception {
handlerSave(callback, fileUrl);
}
public void handlerClosed(Callback callback, String fileUrl) throws Exception {
// 无需处理
}
public void handlerForcesave(Callback callback, String fileUrl) throws Exception {
handlerSave(callback, fileUrl);
}
public void handlerForcesaveCurrupted(Callback callback, String fileUrl) throws Exception {
handlerSave(callback, fileUrl);
}
/**
* 下载最新文件
* @param url
* @return byte
* @author cwchen
* @date 2025/11/8 2:49
*/
private byte[] downloadFileFromUrl(String url) throws Exception {
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<byte[]> response = restTemplate.getForEntity(url, byte[].class);
return response.getBody();
}
/**
* 保存最新文件到minio
* @param filePath
* @param content
* @return void
* @author cwchen
* @date 2025/11/8 2:48
*/
private void saveToMinio(String filePath, byte[] content) throws Exception {
String fileName = filePath.substring(filePath.lastIndexOf("/") + 1);
ByteArrayInputStream inputStream = new ByteArrayInputStream(content);
MultipartFile multipartFile = FileUtil.createMultipartFile(fileName, fileName,
"application/octet-stream", inputStream);
long size = multipartFile.getSize();
long fiveMB = 5 * 1024 * 1024; // 5MB in bytes
if (size > fiveMB) {
// 处理大文件逻辑
fileUploadService.uploadLargeFile(multipartFile, fileName);
} else {
// 处理正常文件逻辑
fileUploadService.uploadFile(multipartFile, filePath);
}
}
}

View File

@ -1,10 +1,15 @@
package com.bonus.file.service;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Base64;
import java.util.Date;
import java.util.Map;
@ -18,7 +23,7 @@ import java.util.Map;
@Component(value = "OnlyOfficeJwtService")
public class OnlyOfficeJwtService {
@Value("${onlyoffice.secret}")
@Value("${only-office.secret}")
private String jwtSecret;
// 生成 JWT Token
@ -27,7 +32,7 @@ public class OnlyOfficeJwtService {
return Jwts.builder()
.setClaims(payload)
.signWith(SignatureAlgorithm.HS256, jwtSecret.getBytes())
.setExpiration(new Date(System.currentTimeMillis() + 3600000))
.setExpiration(new Date(System.currentTimeMillis() + 36000000))
.compact();
}
@ -41,5 +46,33 @@ public class OnlyOfficeJwtService {
}
}
private final ObjectMapper objectMapper = new ObjectMapper();
public String createToken(Object object) {
Map<String, ?> payloadMap = (Map)this.objectMapper.convertValue(object, Map.class);
return this.createToken(payloadMap, jwtSecret);
}
public String createToken(Object object, String key) {
Map<String, ?> payloadMap = (Map)this.objectMapper.convertValue(object, Map.class);
return this.createToken(payloadMap, key);
}
public String createToken(Map<String, ?> payloadMap, String key) {
Algorithm algorithm = Algorithm.HMAC256(key);
String token = JWT.create().withPayload(payloadMap).sign(algorithm);
return token;
}
public String verify(String token) {
return this.verifyToken(token, jwtSecret);
}
public String verifyToken(String token, String key) {
Algorithm algorithm = Algorithm.HMAC256(key);
Base64.Decoder decoder = Base64.getUrlDecoder();
DecodedJWT jwt = JWT.require(algorithm).build().verify(token);
return new String(decoder.decode(jwt.getPayload()));
}
}

View File

@ -1,3 +1,4 @@
/*
package com.bonus.file.service;
import com.bonus.common.core.domain.model.LoginUser;
@ -14,20 +15,23 @@ import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.net.URLEncoder;
import java.io.PrintWriter;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
*/
/**
* @className:OnlyOfficeService
* @author:cwchen
* @date:2025-11-03-16:14
* @version:1.0
* @description:OnlyOffice 服务类
*/
*//*
@Service(value = "OnlyOfficeService")
@Slf4j
@CrossOrigin(origins = "*")
@ -47,20 +51,14 @@ public class OnlyOfficeService {
private static final String DOCUMENT_CACHE_KEY = "document:";
public Map<String, Object> getConfigWithToken(String fileKey, String fileName, String mode) throws Exception {
Map<String, Object> map = buildEditorConfig(fileKey, fileName, mode);
public Map<String, Object> getConfigWithToken(String fileKey, String fileName, String type, String mode) throws Exception {
Map<String, Object> map = buildEditorConfig(fileKey, fileName, type, mode);
String token = generateJwtToken(map);
@SuppressWarnings("unchecked")
Map<String, Object> editorConfig = (Map<String, Object>) map.getOrDefault("editorConfig", new HashMap<>());
// 添加token
editorConfig.put("token", token);
// 确保editorConfig被放回map中
map.put("editorConfig", editorConfig);
map.put("token", token);
return map;
}
public Map<String, Object> buildEditorConfig(String fileKey, String fileName, String mode) throws Exception {
public Map<String, Object> buildEditorConfig(String fileKey, String fileName, String type, String mode) throws Exception {
// 清理文件名只保留最终文件名
String cleanFileName = extractFileName(fileName);
@ -83,14 +81,14 @@ public class OnlyOfficeService {
// 用户信息
Map<String, Object> user = new HashMap<>();
user.put("id", getCurrentUserId());
user.put("id", getCurrentUserId() + "");
user.put("name", getCurrentUserName());
editorConfig.put("user", user);
// 自定义配置
Map<String, Object> customization = new HashMap<>();
customization.put("forcesave", true);
customization.put("forcesave", false);
customization.put("about", true);
customization.put("feedback", true);
customization.put("hideRightMenu", true);
@ -100,18 +98,17 @@ public class OnlyOfficeService {
config.put("document", document);
config.put("documentType", getDocumentType(fileName));
config.put("editorConfig", editorConfig);
config.put("type", "embedded");
config.put("type", type);
config.put("width", "100%");
config.put("height", "100%");
// 缓存文档信息
cacheDocumentInfo(fileKey, fileName);
// cacheDocumentInfo(fileKey, fileName);
return config;
}
public void handleCallback(OnlyOfficeCallback callback) {
public void handleCallback(OnlyOfficeCallback callback, HttpServletResponse response) throws Exception {
PrintWriter writer = response.getWriter();
log.info("收到 OnlyOffice 回调, 状态: {}, Key: {}", callback.getStatus(), callback.getKey());
switch (callback.getStatus()) {
case OnlyOfficeCallback.STATUS_READY_FOR_SAVE:
case OnlyOfficeCallback.STATUS_FORCE_SAVE:
@ -126,6 +123,7 @@ public class OnlyOfficeService {
default:
log.info("文档状态: {}", callback.getStatus());
}
writer.write("{\"error\":0}");
}
private void saveDocumentFromCallback(OnlyOfficeCallback callback) {
@ -171,7 +169,9 @@ public class OnlyOfficeService {
}
private String buildCallbackUrl() {
return "http://" + IpUtils.getHostIp() +":" + onlyOfficeConfig.getServerPort() + "/smartBid/documents/callback";
String hostIp = IpUtils.getHostIp();
// log.info("服务器ip:{}", hostIp);
return "http://" + hostIp + ":" + onlyOfficeConfig.getServerPort() + "/smartBid/documents/callback";
}
@ -198,20 +198,47 @@ public class OnlyOfficeService {
case "odt":
case "rtf":
case "txt":
return "text";
case "docm":
case "dot":
case "dotx":
case "dotm":
case "fodt":
case "ott":
case "html":
case "htm":
case "mht":
case "xml":
return "word"; // 修改: text word
case "xlsx":
case "xls":
case "ods":
case "csv":
return "spreadsheet";
case "xlsm":
case "xlt":
case "xltx":
case "xltm":
case "fods":
case "ots":
return "cell"; // 修改: spreadsheet cell
case "pptx":
case "ppt":
case "odp":
return "presentation";
case "pptm":
case "pot":
case "potx":
case "potm":
case "fodp":
case "otp":
return "slide"; // 修改: presentation slide
case "pdf":
return "pdf";
return "pdf"; // 保持不变
case "vsdx":
case "vssx":
case "vsd":
case "vss":
return "diagram"; // 新增图表类型
default:
return "text";
return "word"; // 修改: text word
}
}
@ -237,9 +264,12 @@ public class OnlyOfficeService {
int lastIndex = Math.max(lastSlash, lastBackslash);
return lastIndex >= 0 ? filePath.substring(lastIndex + 1) : filePath;
}
/**
*/
/**
* 生成 JWT TokenOnlyOffice 专用格式
*/
*//*
private String generateJwtToken(Map<String, Object> config) {
try {
// OnlyOffice 需要的 payload 结构
@ -266,3 +296,4 @@ public class OnlyOfficeService {
}
}
}
*/

View File

@ -0,0 +1,191 @@
package com.bonus.file.service;
import cn.hutool.crypto.digest.DigestUtil;
import com.bonus.common.core.domain.AjaxResult;
import com.bonus.common.core.domain.model.LoginUser;
import com.bonus.common.domain.file.vo.Callback;
import com.bonus.common.domain.file.vo.OnlyOfficeConfig;
import com.bonus.common.domain.file.vo.ResourceFileVo;
import com.bonus.common.utils.DocumentTypeUtils;
import com.bonus.common.utils.SecurityUtils;
import com.bonus.common.utils.StringUtils;
import com.bonus.common.utils.ip.IpUtils;
import com.bonus.file.config.MinioConfig;
import io.minio.MinioClient;
import io.minio.StatObjectArgs;
import io.minio.StatObjectResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.CrossOrigin;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.net.URLEncoder;
import java.time.ZonedDateTime;
import java.util.*;
/**
* @className:OnlyOfficeService2
* @author:cwchen
* @date:2025-11-03-16:14
* @version:1.0
* @description:OnlyOffice 服务类
*/
@Service(value = "OnlyOfficeService2")
@Slf4j
@CrossOrigin(origins = "*")
public class OnlyOfficeService2 {
@Resource(name = "FileUploadService")
private FileUploadService fileUploadService;
@Resource(name = "OnlyOfficeJwtService")
private OnlyOfficeJwtService onlyOfficeJwtService;
@Resource(name = "OnlyOfficeConfig")
private OnlyOfficeConfig onlyOfficeConfig;
@Resource(name = "CallBackService")
private CallBackService callBackService;
@Autowired
private MinioClient minioClient;
@Resource
private MinioConfig minioConfig;
@Value("${server.port}")
private String serverPort;
/**
* 获取onlyoffice-config配置
*
* @param fileVo
* @param type
* @param mode
* @return Map<String, Object>
* @author cwchen
* @date 2025/11/7 22:28
*/
public AjaxResult getOnlyOfficeConfig(ResourceFileVo fileVo, String type, String mode) throws Exception {
OnlyOfficeConfig.Config config = onlyOfficeConfig.getConfig();
OnlyOfficeConfig.Config configuration = new OnlyOfficeConfig.Config();
OnlyOfficeConfig.Config.Document documentConfig = new OnlyOfficeConfig.Config.Document();
// 生成唯一文件名
String key = null;
try {
StatObjectResponse statted = minioClient.statObject(StatObjectArgs.builder()
.bucket(minioConfig.getBucketName())
.object(fileVo.getFilePath()).build());
ZonedDateTime zonedDateTime = statted.lastModified();
long timestamp = zonedDateTime.toInstant().toEpochMilli();
key = DigestUtil.sha256Hex(fileVo.getFileName() + timestamp);
} catch (Exception e) {
key = UUID.randomUUID().toString();
}
documentConfig.setKey(key);
// 编辑模式
if (Objects.equals(mode, "edit")) {
documentConfig.setTitle(fileVo.getFileName());
} else { // 预览模式
documentConfig.setTitle(StringUtils.format("{}({})", fileVo.getFileName(), "预览模式"));
}
documentConfig.setFileType(getFileExtension(fileVo.getFileName()));
OnlyOfficeConfig.Config.Document.Permissions permissions = config.getDocument().getPermissions();
if (Objects.equals(mode, "edit")) {
permissions.setEdit(true);
permissions.setReview(false);
}
documentConfig.setPermissions(permissions);
String fileUrl = fileUploadService.getFile(fileVo.getFilePath()).getFilePath();
documentConfig.setUrl(fileUrl);
OnlyOfficeConfig.Config.EditorConfig editorConfig = config.getEditorConfig();
// 生成回调函数地址
String callbackUrl = buildCallbackUrl();
String encodeFileUrl = URLEncoder.encode(fileVo.getFilePath(), "UTF-8");
String newCallbackUrl = StringUtils.format("{}?fileUrl={}", callbackUrl, encodeFileUrl);
editorConfig.setCallbackUrl(newCallbackUrl);
OnlyOfficeConfig.Config.EditorConfig.User user = editorConfig.getUser();
user.setId(String.valueOf(getCurrentUserId()));
user.setName(getCurrentUserName());
editorConfig.setUser(user);
configuration.setEditorConfig(editorConfig);
configuration.setDocumentType(DocumentTypeUtils.getDocumentType(fileUrl));
configuration.setDocument(documentConfig);
// 生成token
HashMap<String, Object> claims = new HashMap<>();
claims.put("document", documentConfig);
claims.put("editorConfig", editorConfig);
claims.put("documentType", DocumentTypeUtils.getDocumentType(fileUrl));
claims.put("type", config.getType());
String token = onlyOfficeJwtService.generateToken(claims);
configuration.setToken(token);
configuration.setType(config.getType());
return AjaxResult.success(configuration);
}
/**
* onlyoffice回调函数
* @param callback
* @param fileUrl
* @param request
* @param response
* @return String
* @author cwchen
* @date 2025/11/8 3:36
*/
public String handleCallback(Callback callback, String fileUrl, HttpServletRequest request, HttpServletResponse response) throws Exception {
// 1.校验token
String authorization = request.getHeader("Authorization");
if (StringUtils.isEmpty(authorization)) {
throw new RuntimeException("token校验失败");
}
// callback = callBackService.verifyCallback(callback, authorization);
// 处理文件回调
callBackService.processCallback(callback, fileUrl);
// 校验callback参数
return "{\"error\":\"0\"}";
}
/**
* 构建回调url
* @return String
* @author cwchen
* @date 2025/11/8 4:07
*/
private String buildCallbackUrl() {
String hostIp = IpUtils.getHostIp();
// log.info("服务器ip:{}", hostIp);
return "http://" + hostIp + ":" + serverPort + "/smartBid/documents/callback";
}
private Long getCurrentUserId() {
// 获取用户ID
return Optional.ofNullable(SecurityUtils.getLoginUser())
.map(LoginUser::getUserId)
.orElse(null);
}
private String getCurrentUserName() {
// 获取用户名
return Optional.ofNullable(SecurityUtils.getLoginUser())
.map(LoginUser::getUsername)
.orElse(null);
}
private String getFileExtension(String fileName) {
return fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
}
}

View File

@ -90,4 +90,15 @@ public class SourceFileService {
public void delResourceFileBybusinessId(List<ResourceFilePo> delFiles) {
mapper.delResourceFileBybusinessId(delFiles);
}
/**
* 根据文件id获取文件对象
* @param po
* @return ResourceFileVo
* @author cwchen
* @date 2025/11/7 22:15
*/
public ResourceFileVo getFileById(ResourceFilePo po) {
return mapper.getFileById(po);
}
}

View File

@ -84,4 +84,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
AND sys_resource_file.business_type = t.business_type
)
</update>
<!--根据文件id获取文件对象-->
<select id="getFileById" resultType="com.bonus.common.domain.file.vo.ResourceFileVo">
SELECT source_id AS sourceId,
file_path AS filePath,
source_file_name AS fileName
FROM sys_resource_file
WHERE source_id = #{sourceId} AND del_flag = '0'
</select>
</mapper>

View File

@ -108,7 +108,7 @@ public class XssRequestWrapper extends HttpServletRequestWrapper {
XSS_PATTERNS.add(Pattern.compile("cookie", Pattern.CASE_INSENSITIVE));
// 1. SQL注释模式
XSS_PATTERNS.add(Pattern.compile("--", Pattern.CASE_INSENSITIVE)); // 单行注释
// XSS_PATTERNS.add(Pattern.compile("--", Pattern.CASE_INSENSITIVE)); // 单行注释
// XSS_PATTERNS.add(Pattern.compile("/\\*.*?\\*/", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL)); // 多行注释
// XSS_PATTERNS.add(Pattern.compile("#", Pattern.CASE_INSENSITIVE)); // MySQL注释

View File

@ -53,6 +53,10 @@ public class TokenService
@Value("${token.expireTime}")
private int expireTime;
// onlyoffice忽略的回调函数访问路径
@Value("${ignoreUrl.callBackUrl}")
private String callBackUrl;
protected static final long MILLIS_SECOND = 1000;
protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;
@ -76,6 +80,9 @@ public class TokenService
*/
public LoginUser getLoginUser(HttpServletRequest request)
{
if(request.getRequestURI().contains(callBackUrl)){
return null;
}
// 获取请求携带的令牌
String token = getToken(request);
if (StringUtils.isNotEmpty(token))
@ -283,21 +290,35 @@ public class TokenService
*/
private String getToken(HttpServletRequest request)
{
// 1. 优先从请求头获取 token
String token = request.getHeader(header);
if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX))
{
token = token.replace(Constants.TOKEN_PREFIX, "");
}
// 2. 如果请求头中没有 token从参数中获取
if (StringUtils.isEmpty(token)) {
token = request.getParameter("token");
// 如果参数中的 token 也有前缀同样处理
if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX))
{
token = token.replace(Constants.TOKEN_PREFIX, "");
}
}
// 3. 如果 token 为空直接返回
if(StringUtils.isEmpty(token)){
return token;
}
// 4. 根据系统配置决定是否解密
boolean systemConfigStatus = getSystemConfigStatus(CacheConstants.AUTH);
String decryptToken = Sm4Utils.decrypt(token);
if(!systemConfigStatus && Objects.equals(decryptToken, token)){
return token;
}else{
boolean systemConfigStatus = getSystemConfigStatus(CacheConstants.AUTH);
String decryptToken = Sm4Utils.decrypt(token);
if(!systemConfigStatus && Objects.equals(decryptToken, token)){
return token;
}else{
return decryptToken;
}
return decryptToken;
}
}
@ -426,6 +447,34 @@ public class TokenService
return false;
}
}
/**
* 根据用户ID获取LoginUser对象高效版本
*/
public LoginUser getLoginUserByUuid(String uuid) {
if (uuid == null) {
return null;
}
try {
// 通过用户ID映射直接获取token
String userIdTokenKey = getTokenKey(uuid);
String token = redisCache.getCacheObject(userIdTokenKey);
if (StringUtils.isEmpty(token)) {
return null;
}
// 根据token获取LoginUser
return getLoginUser(token);
} catch (Exception e) {
log.error("根据用户uuid获取LoginUser异常uuid: {}", uuid, e);
return null;
}
}
/**
* 用户ID到token的映射key
*/
private String getUserIdTokenKey(Long userId) {
return CacheConstants.LOGIN_TOKEN_KEY + userId;
}
/**
* 获取系统配置