1. 实现集中式的附件统一管理,统一上传统一预览(代码生成器,公共组件,公共附件元数据定义)
2. 实现统一的 DB 字段数据库定义(代码生成器,共用规范检查)
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
package cn.iocoder.yudao.framework.business.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* @author chenbowen
|
||||
*
|
||||
* 附件上传 Controller 注解,
|
||||
* 1. 标记附件列表上传在 requestBody 中的 Key 值,
|
||||
* 2. 标记当前 controller 中业务创建请求后返回的业务主键 Key 值,
|
||||
* 2. 标记会开启业务提交时,前置校验操作用户唯一部门
|
||||
* 3. 标记当前 controller 所属的业务来源
|
||||
*/
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.TYPE)
|
||||
@Inherited
|
||||
public @interface FileUploadController {
|
||||
/**
|
||||
* 附件列表上传在 requestBody 中的 Key 值
|
||||
*/
|
||||
String filesKey() default "files";
|
||||
|
||||
/**
|
||||
* 附件名称在 requestBody 中的 Key 值
|
||||
*/
|
||||
String fileNameKey() default "name";
|
||||
|
||||
/**
|
||||
* 附件 ID 在 requestBody 中的 Key 值
|
||||
*/
|
||||
String fileIdKey() default "id";
|
||||
|
||||
/**
|
||||
* 业务来源
|
||||
* 例如:bpm、oa、hr 等
|
||||
*/
|
||||
String source() default "default";
|
||||
|
||||
/**
|
||||
* 业务创建请求后返回的业务主键 Key 值
|
||||
*/
|
||||
String primaryKey() default "data.id";
|
||||
|
||||
/**
|
||||
* 业务创建请求后返回的业务编码 Key 值
|
||||
*/
|
||||
String codeKey() default "data.code";
|
||||
}
|
||||
@@ -1,8 +1,13 @@
|
||||
package cn.iocoder.yudao.framework.business.config;
|
||||
|
||||
import cn.iocoder.yudao.framework.business.filter.FileUploadFilter;
|
||||
import cn.iocoder.yudao.framework.business.interceptor.BusinessHeaderInterceptor;
|
||||
import cn.iocoder.yudao.framework.business.interceptor.FileUploadHeaderInterceptor;
|
||||
import cn.iocoder.yudao.framework.web.config.YudaoWebAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@@ -10,11 +15,19 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
* @author chenbowen
|
||||
*/
|
||||
@AutoConfiguration(after = YudaoWebAutoConfiguration.class)
|
||||
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
|
||||
public class YudaoBusinessAutoConfiguration implements WebMvcConfigurer {
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
// 只拦截增删改和 set 相关的 url
|
||||
registry.addInterceptor(new BusinessHeaderInterceptor())
|
||||
.addPathPatterns("/**/add**", "/**/create**", "/**/update**", "/**/edit**", "/**/set**");
|
||||
registry.addInterceptor(new FileUploadHeaderInterceptor())
|
||||
.addPathPatterns("/**/add**", "/**/create**", "/**/update**", "/**/edit**", "/**/set**");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public FilterRegistrationBean<FileUploadFilter> businessHeaderFilter() {
|
||||
return new FilterRegistrationBean<>(new FileUploadFilter());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package cn.iocoder.yudao.framework.business.controller;
|
||||
|
||||
import cn.iocoder.yudao.framework.business.annotation.FileUploadController;
|
||||
import cn.iocoder.yudao.framework.business.vo.FileUploadInfoVO;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
|
||||
/**
|
||||
* @author chenbowen
|
||||
*/
|
||||
|
||||
public abstract class AbstractFileUploadController {
|
||||
protected static String FILE_UPLOAD_SOURCE = "";
|
||||
@GetMapping("/upload-info")
|
||||
@Operation(summary = "获取文件上传 source 配置值")
|
||||
public CommonResult<FileUploadInfoVO> getFileUploadSource() {
|
||||
FileUploadInfoVO vo = new FileUploadInfoVO();
|
||||
vo.setSource(FILE_UPLOAD_SOURCE);
|
||||
return success(vo);
|
||||
}
|
||||
|
||||
protected static void setFileUploadInfo(FileUploadController annotation) {
|
||||
FILE_UPLOAD_SOURCE = annotation.source();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package cn.iocoder.yudao.framework.business.enums;
|
||||
/**
|
||||
* @author chenbowen
|
||||
*/
|
||||
public interface FileUploadConstants {
|
||||
// 是否业务请求
|
||||
String IS_UPLOAD_REQUEST = "isUploadRequest";
|
||||
// request 中附件列表的 Key 值,支持多层级引用 如 data.files
|
||||
String FILES_KEY = "fileKey";
|
||||
String FILE_NAME_KEY = "fileNameKey";
|
||||
String FILE_ID_KEY = "fileIdKey";
|
||||
|
||||
// request 中业务来源的
|
||||
String FILE_SOURCE = "fileSource";
|
||||
// request 中业务创建请求后返回的业务主键 Key 值,支持多层级引用 如 data.businessPrimaryKey
|
||||
String FILE_REL_PRIMARY_KEY = "fileRelPrimaryKey";
|
||||
String FILE_REL_CODE_KEY = "fileRelCode";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
package cn.iocoder.yudao.framework.business.filter;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.util.spring.SpringUtils;
|
||||
import cn.iocoder.yudao.module.infra.api.businessfile.BusinessFileApi;
|
||||
import cn.iocoder.yudao.module.infra.api.businessfile.dto.BusinessFileSaveReqDTO;
|
||||
import com.esotericsoftware.minlog.Log;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.servlet.*;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.util.ContentCachingResponseWrapper;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static cn.iocoder.yudao.framework.business.enums.FileUploadConstants.*;
|
||||
|
||||
/**
|
||||
* @author chenbowen
|
||||
*/
|
||||
@Slf4j
|
||||
public class FileUploadFilter implements Filter {
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
|
||||
HttpServletRequest httpRequest = (HttpServletRequest) request;
|
||||
ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper((HttpServletResponse) response);
|
||||
chain.doFilter(request, wrappedResponse);
|
||||
|
||||
// 仅当 request 包含 isUploadRequest 时,且只处理 200 响应
|
||||
if (!isUploadRequest(httpRequest) || wrappedResponse.getStatus() != HttpServletResponse.SC_OK) {
|
||||
wrappedResponse.copyBodyToResponse();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
String requestBody = getRequestBody(httpRequest);
|
||||
if (requestBody == null || requestBody.isEmpty()) {
|
||||
wrappedResponse.copyBodyToResponse();
|
||||
return;
|
||||
}
|
||||
// 从 request 获取 fileKey、业务主键 key、系统来源
|
||||
String fileKey = httpRequest.getAttribute(FILES_KEY).toString();
|
||||
String businessPrimaryKey = httpRequest.getAttribute(FILE_REL_PRIMARY_KEY).toString();
|
||||
String businessCodeKey = httpRequest.getAttribute(FILE_REL_CODE_KEY).toString();
|
||||
String businessSource = httpRequest.getAttribute(FILE_SOURCE).toString();
|
||||
String fileNameKey = httpRequest.getAttribute(FILE_NAME_KEY).toString();
|
||||
String fileIdKey = httpRequest.getAttribute(FILE_ID_KEY).toString();
|
||||
|
||||
JsonNode filesNode = getJsonNodeByPath(objectMapper.readTree(requestBody), fileKey);
|
||||
if (filesNode == null || !filesNode.isArray()) {
|
||||
wrappedResponse.copyBodyToResponse();
|
||||
return;
|
||||
}
|
||||
String responseBody = new String(wrappedResponse.getContentAsByteArray(), wrappedResponse.getCharacterEncoding());
|
||||
// code 如果不等于 200,则不处理
|
||||
JsonNode responseJson = objectMapper.readTree(responseBody);
|
||||
if (responseJson.path("code").asInt(-1) != 0) {
|
||||
wrappedResponse.copyBodyToResponse();
|
||||
return;
|
||||
}
|
||||
// 业务主键支持多层级,如 data.businessPrimaryKey
|
||||
String businessId = getJsonValueByPath(responseJson, businessPrimaryKey);
|
||||
if (businessId == null) {
|
||||
wrappedResponse.copyBodyToResponse();
|
||||
return;
|
||||
}
|
||||
// 业务编码支持多层级,如 data.businessCodeKey
|
||||
String businessCode = getJsonValueByPath(responseJson, businessCodeKey);
|
||||
|
||||
List<JsonNode> fileNodeList = new ArrayList<>();
|
||||
filesNode.forEach(fileNodeList::add);
|
||||
saveBusinessAttachmentBatch(fileNodeList, businessId, businessCode, businessSource, fileNameKey, fileIdKey);
|
||||
} catch (Exception e) {
|
||||
// 记录日志
|
||||
Log.error(e.getMessage(), e);
|
||||
}
|
||||
wrappedResponse.copyBodyToResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* 按路径获取 JsonNode 的值,支持多层级,如 data.businessPrimaryKey
|
||||
*/
|
||||
private String getJsonValueByPath(JsonNode node, String path) {
|
||||
if (node == null || path == null || path.isEmpty()) return null;
|
||||
JsonNode current = node;
|
||||
for (String key : path.split("\\.")) {
|
||||
current = current.path(key);
|
||||
}
|
||||
return current.isMissingNode() ? null : current.asText(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按路径获取 JsonNode 节点,支持多层级,如 data.files
|
||||
*/
|
||||
private JsonNode getJsonNodeByPath(JsonNode node, String path) {
|
||||
if (node == null || path == null || path.isEmpty()) return null;
|
||||
JsonNode current = node;
|
||||
for (String key : path.split("\\.")) {
|
||||
current = current.path(key);
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
private boolean isUploadRequest(HttpServletRequest request) {
|
||||
// 判断 request 是否包含 isUploadRequest 参数(可根据实际业务调整判断方式)
|
||||
String isUpload = Optional.ofNullable(request.getAttribute(IS_UPLOAD_REQUEST)).orElse("").toString();
|
||||
return "true".equalsIgnoreCase(isUpload);
|
||||
}
|
||||
|
||||
private String getRequestBody(HttpServletRequest request) {
|
||||
try {
|
||||
BufferedReader reader = request.getReader();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
sb.append(line);
|
||||
}
|
||||
return sb.toString();
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void saveBusinessAttachmentBatch(List<JsonNode> fileNodes, String businessId, String businessCode, String businessSource, String name, String id) {
|
||||
BusinessFileApi businessFileApi = SpringUtils.getBean(BusinessFileApi.class);
|
||||
List<BusinessFileSaveReqDTO> reqDTOList = new ArrayList<>();
|
||||
for (JsonNode fileNode : fileNodes) {
|
||||
String fileId = fileNode.path(id).asText(null);
|
||||
String fileName = fileNode.path(name).asText(null);
|
||||
if (fileId != null) {
|
||||
BusinessFileSaveReqDTO createReqDTO = new BusinessFileSaveReqDTO();
|
||||
createReqDTO.setBusinessId(Long.parseLong(businessId));
|
||||
createReqDTO.setBusinessCode(businessCode);
|
||||
createReqDTO.setFileId(Long.parseLong(fileId));
|
||||
createReqDTO.setFileName(fileName);
|
||||
createReqDTO.setSource(businessSource);
|
||||
reqDTOList.add(createReqDTO);
|
||||
}
|
||||
}
|
||||
if (!reqDTOList.isEmpty()) {
|
||||
businessFileApi.batchCreateBusinessFile(reqDTOList);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package cn.iocoder.yudao.framework.business.framework.rpc;
|
||||
|
||||
import cn.iocoder.yudao.module.infra.api.businessfile.BusinessFileApi;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
|
||||
/**
|
||||
* @author chenbowen
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@EnableFeignClients(clients = BusinessFileApi.class)
|
||||
public class YudaoBusinessRpcAutoConfiguration {
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package cn.iocoder.yudao.framework.business.interceptor;
|
||||
|
||||
import cn.iocoder.yudao.framework.business.annotation.FileUploadController;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResultCodeEnum;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CompanyDeptInfo;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import static cn.iocoder.yudao.framework.business.core.util.BusinessDeptHandleUtil.getBelongCompanyAndDept;
|
||||
import static cn.iocoder.yudao.framework.business.enums.FileUploadConstants.*;
|
||||
|
||||
/**
|
||||
* @author chenbowen
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
public class FileUploadHeaderInterceptor implements HandlerInterceptor {
|
||||
|
||||
@Override
|
||||
public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) throws Exception {
|
||||
if (!(handler instanceof HandlerMethod handlerMethod)) {
|
||||
return true;
|
||||
}
|
||||
Object bean = handlerMethod.getBean();
|
||||
FileUploadController annotation = bean.getClass().getAnnotation(FileUploadController.class);
|
||||
if (annotation == null) {
|
||||
return true;
|
||||
}
|
||||
// 设置请求属性,标记当前请求为业务控制器
|
||||
request.setAttribute(IS_UPLOAD_REQUEST, true);
|
||||
// 从注解中获取所有的 value 值,如果没有被 注解,获取注解默认值
|
||||
// 将这些值设置到 request
|
||||
request.setAttribute(FILES_KEY, annotation.filesKey());
|
||||
request.setAttribute(FILE_NAME_KEY, annotation.fileNameKey());
|
||||
request.setAttribute(FILE_ID_KEY, annotation.fileIdKey());
|
||||
request.setAttribute(FILE_SOURCE, annotation.source());
|
||||
request.setAttribute(FILE_REL_PRIMARY_KEY, annotation.primaryKey());
|
||||
request.setAttribute(FILE_REL_CODE_KEY, annotation.codeKey());
|
||||
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
Set<CompanyDeptInfo> companyDeptInfos = getBelongCompanyAndDept(request, response);
|
||||
// 无法获取到有效的用户归属公司与部门信息,提示错误
|
||||
if (companyDeptInfos == null) {
|
||||
return writeResponse(response, HttpStatus.BAD_REQUEST.value(), CommonResult.customize(null, CommonResultCodeEnum.ERROR.getCode(), "当前用户匹配部门不属于此公司"), objectMapper);
|
||||
}
|
||||
// 获取到了有效的一组或多组公司与部门信息,需要返回前端进行补充请求头后的二次请求
|
||||
if (!companyDeptInfos.isEmpty()) {
|
||||
return writeResponse(response, HttpStatus.OK.value(), CommonResult.customize(companyDeptInfos, CommonResultCodeEnum.NEED_ADJUST), objectMapper);
|
||||
}
|
||||
else{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private boolean writeResponse(HttpServletResponse response, int status, CommonResult<?> result, ObjectMapper objectMapper) throws Exception {
|
||||
response.setStatus(status);
|
||||
response.getWriter().write(objectMapper.writeValueAsString(result));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package cn.iocoder.yudao.framework.business.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 文件上传信息的值对象
|
||||
* 用于封装文件上传的相关信息
|
||||
*
|
||||
* @author chenbowen
|
||||
*/
|
||||
@Data
|
||||
public class FileUploadInfoVO {
|
||||
// 文件来源标识
|
||||
private String source;
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
cn.iocoder.yudao.framework.business.config.YudaoBusinessAutoConfiguration
|
||||
cn.iocoder.yudao.framework.business.framework.BusinessDataPermissionConfiguration
|
||||
cn.iocoder.yudao.framework.business.framework.BusinessDataPermissionConfiguration
|
||||
cn.iocoder.yudao.framework.business.framework.rpc.YudaoBusinessRpcAutoConfiguration
|
||||
Reference in New Issue
Block a user