diff --git a/bonus-admin/src/main/java/com/bonus/web/controller/common/DocumentController.java b/bonus-admin/src/main/java/com/bonus/web/controller/common/DocumentController.java index 8e4d78c..0748b79 100644 --- a/bonus-admin/src/main/java/com/bonus/web/controller/common/DocumentController.java +++ b/bonus-admin/src/main/java/com/bonus/web/controller/common/DocumentController.java @@ -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 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); } } \ No newline at end of file diff --git a/bonus-admin/src/main/java/com/bonus/web/service/common/DocumentService.java b/bonus-admin/src/main/java/com/bonus/web/service/common/DocumentService.java new file mode 100644 index 0000000..58058f2 --- /dev/null +++ b/bonus-admin/src/main/java/com/bonus/web/service/common/DocumentService.java @@ -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\"}"; + } + } +} diff --git a/bonus-common/pom.xml b/bonus-common/pom.xml index 2994bb0..ca97228 100644 --- a/bonus-common/pom.xml +++ b/bonus-common/pom.xml @@ -203,6 +203,25 @@ jjwt ${io.jsonwebtoken.version} + + + + + + com.auth0 + java-jwt + 3.19.2 + + + + cn.hutool + hutool-all + 5.8.11 + \ No newline at end of file diff --git a/bonus-common/src/main/java/com/bonus/common/domain/file/vo/Callback.java b/bonus-common/src/main/java/com/bonus/common/domain/file/vo/Callback.java new file mode 100644 index 0000000..c68d61e --- /dev/null +++ b/bonus-common/src/main/java/com/bonus/common/domain/file/vo/Callback.java @@ -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 users; + private List> 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 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; + } + } + } + + +} diff --git a/bonus-common/src/main/java/com/bonus/common/domain/file/vo/OnlyOfficeCallback.java b/bonus-common/src/main/java/com/bonus/common/domain/file/vo/OnlyOfficeCallback.java index d79230c..3b802df 100644 --- a/bonus-common/src/main/java/com/bonus/common/domain/file/vo/OnlyOfficeCallback.java +++ b/bonus-common/src/main/java/com/bonus/common/domain/file/vo/OnlyOfficeCallback.java @@ -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> actions; private String lastsave; - private Boolean users; + // private Boolean users; private Boolean notmodified; // 状态常量 diff --git a/bonus-common/src/main/java/com/bonus/common/domain/file/vo/OnlyOfficeConfig.java b/bonus-common/src/main/java/com/bonus/common/domain/file/vo/OnlyOfficeConfig.java new file mode 100644 index 0000000..b18a7d9 --- /dev/null +++ b/bonus-common/src/main/java/com/bonus/common/domain/file/vo/OnlyOfficeConfig.java @@ -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; + } + } + } +} diff --git a/bonus-common/src/main/java/com/bonus/common/enums/OnlyOfficeStatus.java b/bonus-common/src/main/java/com/bonus/common/enums/OnlyOfficeStatus.java new file mode 100644 index 0000000..a584d70 --- /dev/null +++ b/bonus-common/src/main/java/com/bonus/common/enums/OnlyOfficeStatus.java @@ -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 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; + } +} diff --git a/bonus-common/src/main/java/com/bonus/common/utils/DocumentTypeUtils.java b/bonus-common/src/main/java/com/bonus/common/utils/DocumentTypeUtils.java new file mode 100644 index 0000000..3f303dd --- /dev/null +++ b/bonus-common/src/main/java/com/bonus/common/utils/DocumentTypeUtils.java @@ -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 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)); + } +} \ No newline at end of file diff --git a/bonus-file/src/main/java/com/bonus/file/config/OnlyOfficeConfig.java b/bonus-file/src/main/java/com/bonus/file/config/OnlyOfficeConfig.java index 1474acf..58fcbd2 100644 --- a/bonus-file/src/main/java/com/bonus/file/config/OnlyOfficeConfig.java +++ b/bonus-file/src/main/java/com/bonus/file/config/OnlyOfficeConfig.java @@ -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; -} +}*/ diff --git a/bonus-file/src/main/java/com/bonus/file/service/CallBackService.java b/bonus-file/src/main/java/com/bonus/file/service/CallBackService.java new file mode 100644 index 0000000..10810bb --- /dev/null +++ b/bonus-file/src/main/java/com/bonus/file/service/CallBackService.java @@ -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 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 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); + } + } +} diff --git a/bonus-file/src/main/java/com/bonus/file/service/OnlyOfficeJwtService.java b/bonus-file/src/main/java/com/bonus/file/service/OnlyOfficeJwtService.java index 37b2514..8146785 100644 --- a/bonus-file/src/main/java/com/bonus/file/service/OnlyOfficeJwtService.java +++ b/bonus-file/src/main/java/com/bonus/file/service/OnlyOfficeJwtService.java @@ -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 payloadMap = (Map)this.objectMapper.convertValue(object, Map.class); + return this.createToken(payloadMap, jwtSecret); + } + + public String createToken(Object object, String key) { + Map payloadMap = (Map)this.objectMapper.convertValue(object, Map.class); + return this.createToken(payloadMap, key); + } + + public String createToken(Map 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())); + } } diff --git a/bonus-file/src/main/java/com/bonus/file/service/OnlyOfficeService.java b/bonus-file/src/main/java/com/bonus/file/service/OnlyOfficeService.java index 2fc4074..a243134 100644 --- a/bonus-file/src/main/java/com/bonus/file/service/OnlyOfficeService.java +++ b/bonus-file/src/main/java/com/bonus/file/service/OnlyOfficeService.java @@ -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 getConfigWithToken(String fileKey, String fileName, String mode) throws Exception { - Map map = buildEditorConfig(fileKey, fileName, mode); + public Map getConfigWithToken(String fileKey, String fileName, String type, String mode) throws Exception { + Map map = buildEditorConfig(fileKey, fileName, type, mode); String token = generateJwtToken(map); - @SuppressWarnings("unchecked") - Map editorConfig = (Map) map.getOrDefault("editorConfig", new HashMap<>()); - // 添加token - editorConfig.put("token", token); - // 确保editorConfig被放回map中 - map.put("editorConfig", editorConfig); map.put("token", token); return map; } - public Map buildEditorConfig(String fileKey, String fileName, String mode) throws Exception { + public Map buildEditorConfig(String fileKey, String fileName, String type, String mode) throws Exception { // 清理文件名,只保留最终文件名 String cleanFileName = extractFileName(fileName); @@ -83,14 +81,14 @@ public class OnlyOfficeService { // 用户信息 Map user = new HashMap<>(); - user.put("id", getCurrentUserId()); + user.put("id", getCurrentUserId() + ""); user.put("name", getCurrentUserName()); editorConfig.put("user", user); // 自定义配置 Map 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 Token(OnlyOffice 专用格式) - */ + *//* + private String generateJwtToken(Map config) { try { // OnlyOffice 需要的 payload 结构 @@ -266,3 +296,4 @@ public class OnlyOfficeService { } } } +*/ diff --git a/bonus-file/src/main/java/com/bonus/file/service/OnlyOfficeService2.java b/bonus-file/src/main/java/com/bonus/file/service/OnlyOfficeService2.java new file mode 100644 index 0000000..dc31165 --- /dev/null +++ b/bonus-file/src/main/java/com/bonus/file/service/OnlyOfficeService2.java @@ -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 + * @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 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(); + } + +} diff --git a/bonus-file/src/main/java/com/bonus/file/service/SourceFileService.java b/bonus-file/src/main/java/com/bonus/file/service/SourceFileService.java index a0171ae..53cb2c6 100644 --- a/bonus-file/src/main/java/com/bonus/file/service/SourceFileService.java +++ b/bonus-file/src/main/java/com/bonus/file/service/SourceFileService.java @@ -90,4 +90,15 @@ public class SourceFileService { public void delResourceFileBybusinessId(List 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); + } } diff --git a/bonus-file/src/main/resources/mapper/SourceFileMapper.xml b/bonus-file/src/main/resources/mapper/SourceFileMapper.xml index 1205b85..74ee125 100644 --- a/bonus-file/src/main/resources/mapper/SourceFileMapper.xml +++ b/bonus-file/src/main/resources/mapper/SourceFileMapper.xml @@ -84,4 +84,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" AND sys_resource_file.business_type = t.business_type ) + + + diff --git a/bonus-framework/src/main/java/com/bonus/framework/interceptor/XssRequestWrapper.java b/bonus-framework/src/main/java/com/bonus/framework/interceptor/XssRequestWrapper.java index 77d0446..058a56c 100644 --- a/bonus-framework/src/main/java/com/bonus/framework/interceptor/XssRequestWrapper.java +++ b/bonus-framework/src/main/java/com/bonus/framework/interceptor/XssRequestWrapper.java @@ -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注释 diff --git a/bonus-framework/src/main/java/com/bonus/framework/web/service/TokenService.java b/bonus-framework/src/main/java/com/bonus/framework/web/service/TokenService.java index 86cc658..0f374fc 100644 --- a/bonus-framework/src/main/java/com/bonus/framework/web/service/TokenService.java +++ b/bonus-framework/src/main/java/com/bonus/framework/web/service/TokenService.java @@ -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; + } /** * 获取系统配置