新增管理的发布时间字段

This commit is contained in:
潘荣晟
2025-09-25 18:08:09 +08:00
parent 327fd9bdcb
commit 7f940ff257
12 changed files with 574 additions and 2 deletions

View File

@@ -5,6 +5,7 @@ import com.zt.plat.module.base.controller.admin.templtp.vo.*;
import com.zt.plat.module.base.dal.dataobject.tmpltp.TemplateInstanceDO; import com.zt.plat.module.base.dal.dataobject.tmpltp.TemplateInstanceDO;
import com.zt.plat.module.base.service.tmpltp.TemplateInstanceService; import com.zt.plat.module.base.service.tmpltp.TemplateInstanceService;
import com.zt.plat.module.infra.api.file.FileApi; import com.zt.plat.module.infra.api.file.FileApi;
import com.zt.plat.module.infra.api.file.dto.FileCreateReqDTO;
import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotEmpty;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
@@ -35,6 +36,7 @@ import static com.zt.plat.framework.common.pojo.CommonResult.success;
import com.zt.plat.framework.excel.core.util.ExcelUtils; import com.zt.plat.framework.excel.core.util.ExcelUtils;
import com.zt.plat.framework.apilog.core.annotation.ApiAccessLog; import com.zt.plat.framework.apilog.core.annotation.ApiAccessLog;
import org.springframework.web.multipart.MultipartFile;
import static com.zt.plat.framework.apilog.core.enums.OperateTypeEnum.*; import static com.zt.plat.framework.apilog.core.enums.OperateTypeEnum.*;
@@ -53,10 +55,15 @@ public class TemplateInstanceController extends AbstractFileUploadController {
setFileUploadInfo(annotation); setFileUploadInfo(annotation);
} }
} }
@Resource @Resource
private TemplateInstanceService templateInstanceService; private TemplateInstanceService templateInstanceService;
//上传文件
@PostMapping("/save-file")
@Operation(summary = "上传文件", description = "上传文件,传入文件对象和模版实例id")
public CommonResult<Map<String, Object>> uploadFile(@NotEmpty(message = "文件不能为空") @RequestParam("file") MultipartFile file,@RequestParam("id") String id) {
return success(templateInstanceService.saveFile(file,id));
}
@PostMapping("/create") @PostMapping("/create")
@Operation(summary = "创建模板实例") @Operation(summary = "创建模板实例")

View File

@@ -0,0 +1,14 @@
//package com.zt.plat.module.base.controller.admin.templtp.onlyoffice.config;
//
//import org.springframework.context.annotation.Bean;
//import org.springframework.context.annotation.Configuration;
//import org.springframework.web.client.RestTemplate;
//
//@Configuration
//public class OnlyOfficeConfig {
//
// @Bean
// public RestTemplate restTemplate() {
// return new RestTemplate();
// }
//}

View File

@@ -0,0 +1,42 @@
package com.zt.plat.module.base.controller.admin.templtp.onlyoffice.controller;
import com.zt.plat.framework.tenant.core.aop.TenantIgnore;
import com.zt.plat.module.base.controller.admin.templtp.onlyoffice.pojo.OnlyOfficeCallback;
import com.zt.plat.module.base.controller.admin.templtp.onlyoffice.service.OnlyOfficeCallbackService;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.security.PermitAll;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/base/onlyoffice")
@Tag(name = "管理后台 - onlyOffice回调")
public class OnlyOfficeCallbackController {
private final OnlyOfficeCallbackService callbackService;
public OnlyOfficeCallbackController(OnlyOfficeCallbackService callbackService) {
this.callbackService = callbackService;
}
/**
* 处理OnlyOffice文档编辑服务发送的回调
*/
@PostMapping("/callback/{id}")
@PermitAll
@TenantIgnore
public ResponseEntity<Map<String, Integer>> handleCallback(@RequestBody OnlyOfficeCallback callback, @PathVariable String id) {
// 处理回调逻辑
callbackService.processCallback(callback,id);
// 返回必须的响应否则OnlyOffice会显示错误
Map<String, Integer> response = new HashMap<>();
response.put("error", 0);
return new ResponseEntity<>(response, HttpStatus.OK);
}
}

View File

@@ -0,0 +1,23 @@
package com.zt.plat.module.base.controller.admin.templtp.onlyoffice.pojo;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
public class Action {
/**
* 操作类型:
* 0 - 用户断开与文档共同编辑的连接
* 1 - 新用户连接到文档共同编辑
* 2 - 用户单击强制保存按钮
*/
@JsonProperty("type")
private int type;
/**
* 用户标识符
*/
@JsonProperty("userid")
private String userId;
}

View File

@@ -0,0 +1,20 @@
package com.zt.plat.module.base.controller.admin.templtp.onlyoffice.pojo;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
public class History {
/**
* 更改内容
*/
@JsonProperty("changes")
private Object changes;
/**
* 服务器版本
*/
@JsonProperty("serverVersion")
private String serverVersion;
}

View File

@@ -0,0 +1,83 @@
package com.zt.plat.module.base.controller.admin.templtp.onlyoffice.pojo;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
@Data
public class OnlyOfficeCallback {
/**
* 编辑的文档标识符(必需)
*/
@JsonProperty("key")
private String key;
/**
* 文档的状态(必需)
*/
@JsonProperty("status")
private int status;
/**
* 用户对文档执行的操作数组
*/
@JsonProperty("actions")
private List<Action> actions;
/**
* 文档更改历史的对象数组已删除使用history替代
*/
@JsonProperty("changeshistory")
@Deprecated
private List<Object> changesHistory;
/**
* 文档更改历史记录的文件链接
*/
@JsonProperty("changesurl")
private String changesUrl;
/**
* 文档的扩展名
*/
@JsonProperty("filetype")
private String fileType;
/**
* 强制保存请求的启动器类型
*/
@JsonProperty("forcesavetype")
private Integer forceSaveType;
/**
* 提交的表单数据的JSON文件URL
*/
@JsonProperty("formsdataurl")
private String formsDataUrl;
/**
* 文档更改历史的对象
*/
@JsonProperty("history")
private History history;
/**
* 已编辑的要保存的文档的链接
*/
@JsonProperty("url")
private String url;
/**
* 发送到命令服务的自定义信息
*/
@JsonProperty("userdata")
private String userData;
/**
* 打开文档进行编辑的用户标识符列表
*/
@JsonProperty("users")
private List<String> users;
}

View File

@@ -0,0 +1,12 @@
package com.zt.plat.module.base.controller.admin.templtp.onlyoffice.service;
import com.zt.plat.module.base.controller.admin.templtp.onlyoffice.pojo.OnlyOfficeCallback;
public interface OnlyOfficeCallbackService {
/**
* 处理OnlyOffice回调
* @param callback 回调数据
*/
void processCallback(OnlyOfficeCallback callback,String id);
}

View File

@@ -0,0 +1,190 @@
package com.zt.plat.module.base.controller.admin.templtp.onlyoffice.service;
import com.zt.plat.framework.common.pojo.CommonResult;
import com.zt.plat.module.base.controller.admin.templtp.onlyoffice.pojo.Action;
import com.zt.plat.module.base.controller.admin.templtp.onlyoffice.pojo.History;
import com.zt.plat.module.base.controller.admin.templtp.onlyoffice.pojo.OnlyOfficeCallback;
import com.zt.plat.module.infra.api.file.FileApi;
import com.zt.plat.module.infra.api.file.dto.FileCreateReqDTO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import static com.zt.plat.module.base.controller.admin.templtp.onlyoffice.util.UrlFileDownloader.downloadFileAsMultipart;
@Service
@RequiredArgsConstructor
@Slf4j
public class OnlyOfficeCallbackServiceImpl implements OnlyOfficeCallbackService {
private final FileApi fileApi;
@Override
public void processCallback(OnlyOfficeCallback callback,String id) {
log.info("Received OnlyOffice callback for document: {}", callback.getKey());
log.info("Callback status: {}", callback.getStatus());
// 根据不同的状态处理回调
switch (callback.getStatus()) {
case 1:
handleEditingStatus(callback,id);
break;
case 2:
handleDocumentSaved(callback,id);
break;
case 3:
handleSaveError(callback,id);
break;
case 4:
handleDocumentClosedWithoutChanges(callback,id);
break;
case 6:
handleForcedSave(callback,id);
break;
case 7:
handleForcedSaveError(callback,id);
break;
default:
log.warn("Received unknown callback status: {}", callback.getStatus());
}
}
/**
* 处理文档正在编辑的状态
*/
private void handleEditingStatus(OnlyOfficeCallback callback,String id) {
log.info("Document {} is being edited by users: {}",
callback.getKey(), callback.getUsers());
// 处理用户操作(连接或断开连接)
if (callback.getActions() != null) {
for (Action action : callback.getActions()) {
String actionType = switch (action.getType()) {
case 0 -> "disconnected from";
case 1 -> "connected to";
case 2 -> "clicked force save in";
default -> "performed unknown action in";
};
log.info("User {} {}", action.getUserId(), actionType);
}
}
}
/**
* 处理文档已保存的状态
*/
private void handleDocumentSaved(OnlyOfficeCallback callback,String id) {
log.info("Document {} is ready to be saved", callback.getKey());
saveDocument(callback,id);
// 处理历史记录
// handleHistoryChanges(callback,id);
}
/**
* 处理保存错误的状态
*/
private void handleSaveError(OnlyOfficeCallback callback,String id) {
log.error("Error saving document {}", callback.getKey());
// 可以在这里添加错误处理逻辑,如发送通知等
}
/**
* 处理文档关闭且无更改的状态
*/
private void handleDocumentClosedWithoutChanges(OnlyOfficeCallback callback,String id) {
log.info("Document {} closed without changes", callback.getKey());
// 可以在这里添加清理资源等逻辑
}
/**
* 处理强制保存的状态
*/
private void handleForcedSave(OnlyOfficeCallback callback,String id) {
log.info("Document {} forced save. Type: {}",
callback.getKey(), callback.getForceSaveType());
saveDocument(callback,id);
// 处理历史记录
handleHistoryChanges(callback,id);
// 如果是表单提交,处理表单数据
if (callback.getForceSaveType() == 3 && callback.getFormsDataUrl() != null) {
handleFormSubmission(callback,id);
}
}
/**
* 处理强制保存错误的状态
*/
private void handleForcedSaveError(OnlyOfficeCallback callback,String id) {
log.error("Error during forced save of document {}", callback.getKey());
// 可以在这里添加错误处理逻辑
}
/**
* 保存文档到存储
*/
private void saveDocument(OnlyOfficeCallback callback,String id) {
if (callback.getUrl() == null) {
log.error("文件路径为空");
return;
}
try {
MultipartFile file = downloadFileAsMultipart(callback.getUrl());
// 1. 验证文件是否为空
// 2. 获取并验证文件名
String fileName = file.getOriginalFilename();
String directory = "template-instance";
FileCreateReqDTO fileCreateReqDTO = new FileCreateReqDTO();
fileCreateReqDTO.setName(fileName);
fileCreateReqDTO.setContent(file.getBytes());
fileCreateReqDTO.setType(file.getContentType()); // 使用真实的MIME类型
fileCreateReqDTO.setDirectory(directory); // 设置文件存储目录
// 7. 调用文件服务创建文件
CommonResult<String> result = fileApi.createFile(fileCreateReqDTO);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 处理文档历史记录变更
*/
private void handleHistoryChanges(OnlyOfficeCallback callback,String id) {
History history = callback.getHistory();
if (history != null) {
log.info("Processing document history changes for {}", callback.getKey());
// 这里可以实现处理历史记录的逻辑
// 例如调用refreshHistory方法更新历史记录
}
// 处理变更历史URL
if (callback.getChangesUrl() != null) {
log.info("Changes URL for document {}: {}", callback.getKey(), callback.getChangesUrl());
// 这里可以实现保存变更历史的逻辑
// 例如下载变更历史并使用setHistoryData方法存储
}
}
/**
* 处理表单提交数据
*/
private void handleFormSubmission(OnlyOfficeCallback callback,String id) {
log.info("Processing form submission for document {}", callback.getKey());
// 这里可以实现处理表单数据的逻辑
// 例如从formsDataUrl下载并解析表单数据
}
}

View File

@@ -0,0 +1,167 @@
package com.zt.plat.module.base.controller.admin.templtp.onlyoffice.util;
import org.jetbrains.annotations.NotNull;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
/**
* 网络文件下载工具类用于从URL下载文件并转换为MultipartFile
*/
public class UrlFileDownloader {
// 连接超时时间(毫秒)
private static final int CONNECT_TIMEOUT = 5000;
// 读取超时时间(毫秒)
private static final int READ_TIMEOUT = 10000;
// 缓冲区大小
private static final int BUFFER_SIZE = 4096;
/**
* 从URL下载文件并转换为MultipartFile
*
* @param fileUrl 文件的网络地址
* @return 转换后的MultipartFile对象
* @throws IOException 下载过程中发生IO异常时抛出
*/
public static MultipartFile downloadFileAsMultipart(String fileUrl) throws IOException {
if (fileUrl == null || fileUrl.trim().isEmpty()) {
throw new IllegalArgumentException("文件URL不能为空");
}
URL url = new URL(fileUrl);
URLConnection connection = url.openConnection();
// 设置连接和读取超时
connection.setConnectTimeout(CONNECT_TIMEOUT);
connection.setReadTimeout(READ_TIMEOUT);
// 设置请求头,模拟浏览器行为(避免部分服务器拒绝非浏览器请求)
connection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36");
// 1. 获取文件名从响应头或URL提取
String fileName = getFileNameFromUrl(fileUrl, connection);
// 2. 获取文件MIME类型响应头中获取默认二进制流
String contentType = connection.getContentType() == null ? "application/octet-stream" : connection.getContentType();
// 3. 读取文件字节内容
byte[] fileBytes = readFileBytes(connection.getInputStream());
// 4. 使用自定义MultipartFile实现类封装不依赖Spring Test
return new CustomMultipartFile(
"file", // 表单字段名(与上传接口的@RequestParam("file")对应)
fileName, // 文件名
contentType, // 文件MIME类型
fileBytes // 文件字节内容
);
}
/**
* 从输入流读取文件字节数组
*/
private static byte[] readFileBytes(InputStream inputStream) throws IOException {
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
byte[] buffer = new byte[BUFFER_SIZE];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
return outputStream.toByteArray();
} finally {
if (inputStream != null) {
inputStream.close();
}
}
}
/**
* 从URL和响应头中提取文件名
*/
private static String getFileNameFromUrl(String fileUrl, URLConnection connection) {
// 优先从响应头的Content-Disposition获取attachment;filename="test.pdf"
String disposition = connection.getHeaderField("Content-Disposition");
if (disposition != null && disposition.contains("filename=")) {
String fileName = disposition.substring(disposition.indexOf("filename=") + 9);
// 处理文件名中的引号(如去掉"test.pdf"的双引号)
return fileName.replaceAll("^[\"']|[\"']$", "");
}
// 从URL路径提取https://xxx.com/abc/test.pdf → test.pdf
String urlPath = fileUrl.split("\\?")[0]; // 去掉URL参数部分
if (urlPath.contains("/")) {
String fileName = urlPath.substring(urlPath.lastIndexOf("/") + 1);
if (!fileName.isEmpty()) {
return fileName;
}
}
// 兜底:生成默认文件名(避免空文件名)
return "downloaded_file_" + System.currentTimeMillis();
}
/**
* 自定义MultipartFile实现类不依赖Spring Test适用于生产环境
* 覆盖核心方法,满足文件上传场景需求
*/
private static class CustomMultipartFile implements MultipartFile {
private final String fieldName; // 表单字段名
private final String originalFilename; // 原始文件名
private final String contentType; // MIME类型
private final byte[] content; // 文件字节内容
public CustomMultipartFile(String fieldName, String originalFilename, String contentType, byte[] content) {
this.fieldName = fieldName;
this.originalFilename = originalFilename;
this.contentType = contentType;
this.content = content != null ? content : new byte[0];
}
@NotNull
@Override
public String getName() {
return fieldName; // 表单字段名(如"file"
}
@Override
public String getOriginalFilename() {
return originalFilename; // 原始文件名(如"test.pdf"
}
@Override
public String getContentType() {
return contentType; // MIME类型如"application/pdf"
}
@Override
public boolean isEmpty() {
return content.length == 0; // 判断文件是否为空
}
@Override
public long getSize() {
return content.length; // 文件大小(字节数)
}
@NotNull
@Override
public byte[] getBytes() throws IOException {
return content.clone(); // 返回文件字节数组(克隆避免外部修改)
}
@NotNull
@Override
public InputStream getInputStream() throws IOException {
return new ByteArrayInputStream(content); // 字节流(用于读取文件内容)
}
// 以下方法在文件上传场景中极少用到,默认实现即可
@Override
public void transferTo(@NotNull java.io.File dest) throws IOException, IllegalStateException {
throw new UnsupportedOperationException("暂不支持transferTo方法");
}
}
}

View File

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.zt.plat.framework.mybatis.core.dataobject.BusinessBaseDO; import com.zt.plat.framework.mybatis.core.dataobject.BusinessBaseDO;
import lombok.*; import lombok.*;
@@ -75,6 +76,7 @@ public class TemplateInstanceDO extends BusinessBaseDO {
/** /**
* 发布时间 * 发布时间
*/ */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime publishTime; private LocalDateTime publishTime;
// /** // /**
// * 公司编号 // * 公司编号

View File

@@ -9,6 +9,7 @@ import jakarta.validation.*;
import com.zt.plat.framework.common.pojo.PageResult; import com.zt.plat.framework.common.pojo.PageResult;
import com.zt.plat.framework.common.pojo.PageParam; import com.zt.plat.framework.common.pojo.PageParam;
import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotEmpty;
import org.springframework.web.multipart.MultipartFile;
/** /**
* 模板实例 Service 接口 * 模板实例 Service 接口
@@ -82,4 +83,6 @@ public interface TemplateInstanceService {
Map<String, Object> getVersion(String id); Map<String, Object> getVersion(String id);
List<TemplateInstanceRespVO> listByCdg(String cdg); List<TemplateInstanceRespVO> listByCdg(String cdg);
Map<String, Object> saveFile(MultipartFile file, String id);
} }

View File

@@ -13,6 +13,7 @@ import com.zt.plat.framework.tenant.core.context.CompanyContextHolder;
import com.zt.plat.module.base.controller.admin.templtp.vo.*; import com.zt.plat.module.base.controller.admin.templtp.vo.*;
import com.zt.plat.module.base.dal.dataobject.tmpltp.*; import com.zt.plat.module.base.dal.dataobject.tmpltp.*;
import com.zt.plat.module.base.dal.mysql.tmpltp.*; import com.zt.plat.module.base.dal.mysql.tmpltp.*;
import com.zt.plat.module.infra.api.file.FileApi;
import com.zt.plat.module.tmpltp.enums.DeleteStatusEnum; import com.zt.plat.module.tmpltp.enums.DeleteStatusEnum;
import com.zt.plat.module.tmpltp.enums.PublishStatusEnum; import com.zt.plat.module.tmpltp.enums.PublishStatusEnum;
import com.zt.plat.module.tmpltp.enums.TmplStsEnum; import com.zt.plat.module.tmpltp.enums.TmplStsEnum;
@@ -22,6 +23,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.multipart.MultipartFile;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.*; import java.util.*;
@@ -42,7 +44,8 @@ import static com.zt.plat.module.tmpltp.enums.ErrorCodeConstants.*;
@Service @Service
@Validated @Validated
public class TemplateInstanceServiceImpl implements TemplateInstanceService { public class TemplateInstanceServiceImpl implements TemplateInstanceService {
@Resource
private FileApi fileApi;
@Resource @Resource
private TemplateInstanceMapper templateInstanceMapper; private TemplateInstanceMapper templateInstanceMapper;
@Resource @Resource
@@ -373,6 +376,12 @@ private String incrementVersion(String currentVersion) {
return templateInstanceRespVOS; return templateInstanceRespVOS;
} }
@Override
public Map<String, Object> saveFile(MultipartFile file, String id) {
return Map.of();
}
@Override @Override
public void getDetailedInfo(TemplateInstanceRespVO templateInstanceRespVO) { public void getDetailedInfo(TemplateInstanceRespVO templateInstanceRespVO) {