1. 统一包名修改

This commit is contained in:
chenbowen
2025-09-22 11:55:27 +08:00
parent a001fc8f16
commit 0d46897482
2739 changed files with 512 additions and 512 deletions

View File

@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.zt.plat</groupId>
<artifactId>zt-framework</artifactId>
<version>${revision}</version>
</parent>
<artifactId>zt-spring-boot-starter-biz-business</artifactId>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>com.zt.plat</groupId>
<artifactId>zt-module-system-api</artifactId>
<version>${revision}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.zt.plat</groupId>
<artifactId>zt-spring-boot-starter-security</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.zt.plat</groupId>
<artifactId>zt-common</artifactId>
</dependency>
<dependency>
<groupId>com.zt.plat</groupId>
<artifactId>zt-spring-boot-starter-biz-data-permission</artifactId>
<version>${revision}</version>
</dependency>
<!-- Test 测试相关 -->
<dependency>
<groupId>com.zt.plat</groupId>
<artifactId>zt-spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.zt.plat</groupId>
<artifactId>zt-module-infra-api</artifactId>
<version>${revision}</version>
</dependency>
<!-- RPC 远程调用相关 -->
<dependency>
<groupId>com.zt.plat</groupId>
<artifactId>zt-spring-boot-starter-rpc</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,49 @@
package com.zt.plat.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";
}

View File

@@ -0,0 +1,33 @@
package com.zt.plat.framework.business.config;
import com.zt.plat.framework.business.filter.FileUploadFilter;
import com.zt.plat.framework.business.interceptor.BusinessHeaderInterceptor;
import com.zt.plat.framework.business.interceptor.FileUploadHeaderInterceptor;
import com.zt.plat.framework.web.config.CloudWebAutoConfiguration;
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;
/**
* @author chenbowen
*/
@AutoConfiguration(after = CloudWebAutoConfiguration.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
public class CloudBusinessAutoConfiguration 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());
}
}

View File

@@ -0,0 +1,28 @@
package com.zt.plat.framework.business.controller;
import com.zt.plat.framework.business.annotation.FileUploadController;
import com.zt.plat.framework.business.vo.FileUploadInfoVO;
import com.zt.plat.framework.common.pojo.CommonResult;
import io.swagger.v3.oas.annotations.Operation;
import org.springframework.web.bind.annotation.GetMapping;
import static com.zt.plat.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();
}
}

View File

@@ -0,0 +1,64 @@
package com.zt.plat.framework.business.core.util;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.zt.plat.framework.common.pojo.CompanyDeptInfo;
import com.zt.plat.framework.security.core.LoginUser;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import static com.zt.plat.framework.common.util.collection.CollectionUtils.singleton;
import static com.zt.plat.framework.security.core.util.SecurityFrameworkUtils.getLoginUser;
/**
* @author chenbowen
*/
public class BusinessDeptHandleUtil {
public static Set<CompanyDeptInfo> getBelongCompanyAndDept(HttpServletRequest request, HttpServletResponse response) throws Exception {
response.setContentType("application/json;charset=UTF-8");
String companyId = request.getHeader("visit-company-id");
String deptId = request.getHeader("visit-dept-id");
LoginUser loginUser = Optional.ofNullable(getLoginUser()).orElse(new LoginUser().setInfo(new HashMap<>()));
Set<CompanyDeptInfo> companyDeptSet = JSONUtil.parseArray(loginUser.getInfo().getOrDefault(LoginUser.INFO_KEY_COMPANY_DEPT_SET, "[]")).stream()
.map(obj -> JSONUtil.toBean((JSONObject) obj, CompanyDeptInfo.class))
.collect(Collectors.toSet());
// 1. 有 companyId
if (companyId != null && !companyId.isBlank()) {
// 根据请求头中的公司 ID 过滤出当前用户的公司部门信息
Set<CompanyDeptInfo> companyDeptSetByCompanyId = companyDeptSet.stream().filter(companyDeptInfo -> companyDeptInfo.getCompanyId().toString().equals(companyId)).collect(Collectors.toSet());
if (companyDeptSetByCompanyId.isEmpty()) {
// 当前公司下没有部门
CompanyDeptInfo data = new CompanyDeptInfo();
data.setCompanyId(Long.valueOf(companyId));
data.setDeptId(0L);
return new HashSet<>(singleton(data));
}
// 如果有 deptId校验其是否属于该 companyId
if (deptId != null) {
boolean valid = companyDeptSetByCompanyId.stream().anyMatch(info -> String.valueOf(info.getDeptId()).equals(deptId));
if (!valid) {
return null;
}else{
// 部门存在,放行
return new HashSet<>();
}
}
return companyDeptSetByCompanyId;
}
// 2. 没有公司信息,尝试唯一性自动推断
// 如果当前用户下只有一个公司和部门的对于关系
if (companyDeptSet.size() == 1) {
CompanyDeptInfo companyDeptInfo = companyDeptSet.iterator().next();
return new HashSet<>(singleton(companyDeptInfo));
} else {
return companyDeptSet;
}
}
}

View File

@@ -0,0 +1,21 @@
package com.zt.plat.framework.business.core.util;
import com.zt.plat.module.system.api.user.dto.AdminUserRespDTO;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
/**
* 用户与部门一对多改动,此处统一处理用户与部门关系
* @author chenbowen
*/
public class DeptUtil {
/**
* 从用户信息中获取唯一 deptId (现阶段取第一个,后续如有特殊规则统一调整此处即可)
*/
public static Long getDeptId(AdminUserRespDTO adminUserRespDTO) {
List<Long> deptIds = Optional.ofNullable(adminUserRespDTO.getDeptIds()).orElse(new ArrayList<>());
return deptIds.stream().findFirst().orElse(0L);
}
}

View File

@@ -0,0 +1,19 @@
package com.zt.plat.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";
}

View File

@@ -0,0 +1,149 @@
package com.zt.plat.framework.business.filter;
import com.zt.plat.framework.common.util.spring.SpringUtils;
import com.zt.plat.module.infra.api.businessfile.BusinessFileApi;
import com.zt.plat.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 com.zt.plat.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);
}
}
}

View File

@@ -0,0 +1,28 @@
package com.zt.plat.framework.business.framework;
import com.zt.plat.framework.datapermission.core.rule.company.CompanyDataPermissionRuleCustomizer;
import com.zt.plat.framework.datapermission.core.rule.dept.DeptDataPermissionRuleCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author chenbowen
*/
@Configuration(proxyBeanMethods = false)
public class BusinessDataPermissionConfiguration {
@Bean
public CompanyDataPermissionRuleCustomizer sysCompanyDataPermissionRuleCustomizer() {
return rule -> {
// companyId
rule.addCompanyColumn("demo_contract", "company_id");
};
}
@Bean
public DeptDataPermissionRuleCustomizer businessDeptDataPermissionRuleCustomizer() {
return rule -> {
// dept
rule.addDeptColumn("demo_contract", "dept_id");
};
}
}

View File

@@ -0,0 +1,13 @@
package com.zt.plat.framework.business.framework.rpc;
import com.zt.plat.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 CloudBusinessRpcAutoConfiguration {
}

View File

@@ -0,0 +1,9 @@
package com.zt.plat.framework.business.interceptor;
/**
* @author chenbowen
* 标记是否业务接口,如果是业务接口需要确定唯一的公司和部门信息才能放行
*/
public interface BusinessControllerMarker {
}

View File

@@ -0,0 +1,55 @@
package com.zt.plat.framework.business.interceptor;
import com.zt.plat.framework.common.pojo.CommonResult;
import com.zt.plat.framework.common.pojo.CommonResultCodeEnum;
import com.zt.plat.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 com.zt.plat.framework.business.core.util.BusinessDeptHandleUtil.getBelongCompanyAndDept;
/**
* @author chenbowen
*/
@RequiredArgsConstructor
public class BusinessHeaderInterceptor 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();
if (!(bean instanceof BusinessControllerMarker)) {
return true;
}
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;
}
}

View File

@@ -0,0 +1,69 @@
package com.zt.plat.framework.business.interceptor;
import com.zt.plat.framework.business.annotation.FileUploadController;
import com.zt.plat.framework.common.pojo.CommonResult;
import com.zt.plat.framework.common.pojo.CommonResultCodeEnum;
import com.zt.plat.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 com.zt.plat.framework.business.core.util.BusinessDeptHandleUtil.getBelongCompanyAndDept;
import static com.zt.plat.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;
}
}

View File

@@ -0,0 +1,15 @@
package com.zt.plat.framework.business.vo;
import lombok.Data;
/**
* 文件上传信息的值对象
* 用于封装文件上传的相关信息
*
* @author chenbowen
*/
@Data
public class FileUploadInfoVO {
// 文件来源标识
private String source;
}

View File

@@ -0,0 +1,3 @@
com.zt.plat.framework.business.config.CloudBusinessAutoConfiguration
com.zt.plat.framework.business.framework.BusinessDataPermissionConfiguration
com.zt.plat.framework.business.framework.rpc.CloudBusinessRpcAutoConfiguration

View File

@@ -0,0 +1,238 @@
package com.zt.plat.framework.business.interceptor;
import com.zt.plat.framework.common.enums.UserTypeEnum;
import com.zt.plat.framework.common.pojo.CompanyDeptInfo;
import com.zt.plat.framework.security.core.LoginUser;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.web.method.HandlerMethod;
import java.io.PrintWriter;
import java.util.*;
import static com.zt.plat.framework.test.core.util.RandomUtils.randomPojo;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import static org.springframework.security.core.context.SecurityContextHolder.getContext;
class BusinessHeaderInterceptorTest {
private BusinessHeaderInterceptor interceptor;
private HttpServletRequest request;
private HttpServletResponse response;
private HandlerMethod handlerMethod;
private PrintWriter writer;
@BeforeEach
void setUp() throws Exception {
interceptor = new BusinessHeaderInterceptor();
request = mock(HttpServletRequest.class);
response = mock(HttpServletResponse.class);
handlerMethod = mock(HandlerMethod.class);
writer = mock(PrintWriter.class);
when(response.getWriter()).thenReturn(writer);
}
/**
* 用例:传入的 handler 不是 HandlerMethod应该直接返回 true
*/
@Test
void testPreHandle_NotHandlerMethod() throws Exception {
boolean result = interceptor.preHandle(request, response, new Object());
assertTrue(result);
}
/**
* 用例handlerMethod.getBean() 不是 BusinessControllerMarker应该直接返回 true
*/
@Test
void testPreHandle_NotBusinessControllerMarker() throws Exception {
when(handlerMethod.getBean()).thenReturn(new Object());
boolean result = interceptor.preHandle(request, response, handlerMethod);
assertTrue(result);
}
/**
* 用例handlerMethod.getBean() 是普通 Controller未实现 marker 接口),应直接返回 true
*/
@Test
void testPreHandle_NormalController() throws Exception {
class NormalController {}
when(handlerMethod.getBean()).thenReturn(new NormalController());
boolean result = interceptor.preHandle(request, response, handlerMethod);
assertTrue(result);
}
/**
* 用例marker controller且 header 无 companyId/deptIdloginUser 有多个公司部门,应该返回 false 并提示 NEED_ADJUST
*/
@Test
void testPreHandle_NoCompanyId_MultiCompanyDept() throws Exception {
class TestBusinessController implements BusinessControllerMarker {}
when(handlerMethod.getBean()).thenReturn(new TestBusinessController());
when(request.getHeader("visit-company-id")).thenReturn(null);
when(request.getHeader("visit-dept-id")).thenReturn(null);
// 构造 loginUser包含多个公司部门
LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)
.setUserType(UserTypeEnum.ADMIN.getValue()));
Map<String, String> infoMap = new HashMap<>();
infoMap.put(LoginUser.INFO_KEY_COMPANY_DEPT_SET, "[{\"companyId\":1,\"deptId\":2},{\"companyId\":2,\"deptId\":3}]");
loginUser.setInfo(infoMap);
// 通过反射或包可见性设置 getLoginUser 返回
setLoginUserForTest(loginUser);
boolean result = interceptor.preHandle(request, response, handlerMethod);
assertFalse(result);
verify(writer).write(contains("400"));
}
/**
* 用例header 有 companyId/deptIdloginUser 有多个公司部门,应该正常通过
*/
@Test
void testPreHandle_WithCompanyIdDeptId_MultiCompanyDept() throws Exception {
class TestBusinessController implements BusinessControllerMarker {}
when(handlerMethod.getBean()).thenReturn(new TestBusinessController());
when(request.getHeader("visit-company-id")).thenReturn("1");
when(request.getHeader("visit-dept-id")).thenReturn("2");
// 构造 loginUser包含多个公司部门
CompanyDeptInfo deptInfo1 = new CompanyDeptInfo();
deptInfo1.setCompanyId(1L);
deptInfo1.setDeptId(2L);
CompanyDeptInfo deptInfo2 = new CompanyDeptInfo();
deptInfo2.setCompanyId(2L);
deptInfo2.setDeptId(3L);
Set<CompanyDeptInfo> deptSet = new HashSet<>();
deptSet.add(deptInfo1);
deptSet.add(deptInfo2);
LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)
.setUserType(UserTypeEnum.ADMIN.getValue()));
String deptSetJson = "[" +
"{\"companyId\":" + deptInfo1.getCompanyId() + ",\"deptId\":" + deptInfo1.getDeptId() + "}," +
"{\"companyId\":" + deptInfo2.getCompanyId() + ",\"deptId\":" + deptInfo2.getDeptId() + "}]";
Map<String, String> infoMap = new HashMap<>();
infoMap.put(LoginUser.INFO_KEY_COMPANY_DEPT_SET, deptSetJson);
loginUser.setInfo(infoMap);
setLoginUserForTest(loginUser);
boolean result = interceptor.preHandle(request, response, handlerMethod);
assertTrue(result);
}
/**
* 用例header 无 companyId/deptIdloginUser 只有一个公司部门,应该自动填充 header 并通过
*/
@Test
void testPreHandle_NoHeader_SingleCompanyDept() throws Exception {
class TestBusinessController implements BusinessControllerMarker {}
when(handlerMethod.getBean()).thenReturn(new TestBusinessController());
when(request.getHeader("visit-company-id")).thenReturn(null);
when(request.getHeader("visit-dept-id")).thenReturn(null);
// 构造 loginUser只有一个公司且公司下只有一个部门
CompanyDeptInfo deptInfo = new CompanyDeptInfo();
deptInfo.setCompanyId(100L);
deptInfo.setDeptId(200L);
Set<CompanyDeptInfo> deptSet = new HashSet<>();
deptSet.add(deptInfo);
LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)
.setUserType(UserTypeEnum.ADMIN.getValue()));
// 只放一个公司部门
String deptSetJson = "[{\"companyId\":" + deptInfo.getCompanyId() + ",\"deptId\":" + deptInfo.getDeptId() + "}]";
Map<String, String> infoMap = new HashMap<>();
infoMap.put(LoginUser.INFO_KEY_COMPANY_DEPT_SET, deptSetJson);
loginUser.setInfo(infoMap);
setLoginUserForTest(loginUser);
boolean result = interceptor.preHandle(request, response, handlerMethod);
assertFalse(result);
// 可选verify(request).setAttribute("visit-company-id", String.valueOf(deptInfo.getCompanyId()));
// 可选verify(request).setAttribute("visit-dept-id", String.valueOf(deptInfo.getDeptId()));
}
/**
* 用例header 无 companyId/deptIdloginUser 有多个公司部门,应该返回 false 并提示 400
*/
@Test
void testPreHandle_NoHeader_MultiCompanyDept() throws Exception {
class TestBusinessController implements BusinessControllerMarker {}
when(handlerMethod.getBean()).thenReturn(new TestBusinessController());
when(request.getHeader("visit-company-id")).thenReturn(null);
when(request.getHeader("visit-dept-id")).thenReturn(null);
// 构造 loginUser多个公司部门
CompanyDeptInfo deptInfo1 = new CompanyDeptInfo();
deptInfo1.setCompanyId(1L);
deptInfo1.setDeptId(2L);
CompanyDeptInfo deptInfo2 = new CompanyDeptInfo();
deptInfo2.setCompanyId(2L);
deptInfo2.setDeptId(3L);
Set<CompanyDeptInfo> deptSet = new HashSet<>();
deptSet.add(deptInfo1);
deptSet.add(deptInfo2);
LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)
.setUserType(UserTypeEnum.ADMIN.getValue()));
String deptSetJson = "[" +
"{\"companyId\":" + deptInfo1.getCompanyId() + ",\"deptId\":" + deptInfo1.getDeptId() + "}," +
"{\"companyId\":" + deptInfo2.getCompanyId() + ",\"deptId\":" + deptInfo2.getDeptId() + "}]";
Map<String, String> infoMap = new HashMap<>();
infoMap.put(LoginUser.INFO_KEY_COMPANY_DEPT_SET, deptSetJson);
loginUser.setInfo(infoMap);
setLoginUserForTest(loginUser);
boolean result = interceptor.preHandle(request, response, handlerMethod);
assertFalse(result);
verify(writer).write(contains("400"));
}
/**
* 用例header 有错误的 companyId/deptIdloginUser 不包含该公司部门,应该返回 false 并提示 400
*/
@Test
void testPreHandle_HeaderNotMatchUserCompanyDept() throws Exception {
class TestBusinessController implements BusinessControllerMarker {}
when(handlerMethod.getBean()).thenReturn(new TestBusinessController());
when(request.getHeader("visit-company-id")).thenReturn("999");
when(request.getHeader("visit-dept-id")).thenReturn("888");
// 构造 loginUser只有其他公司部门
CompanyDeptInfo deptInfo1 = new CompanyDeptInfo();
deptInfo1.setCompanyId(1L);
deptInfo1.setDeptId(2L);
CompanyDeptInfo deptInfo2 = new CompanyDeptInfo();
deptInfo2.setCompanyId(2L);
deptInfo2.setDeptId(3L);
Set<CompanyDeptInfo> deptSet = new HashSet<>();
deptSet.add(deptInfo1);
deptSet.add(deptInfo2);
LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)
.setUserType(UserTypeEnum.ADMIN.getValue()));
String deptSetJson = "[" +
"{\"companyId\":" + deptInfo1.getCompanyId() + ",\"deptId\":" + deptInfo1.getDeptId() + "}," +
"{\"companyId\":" + deptInfo2.getCompanyId() + ",\"deptId\":" + deptInfo2.getDeptId() + "}]";
Map<String, String> infoMap = new HashMap<>();
infoMap.put(LoginUser.INFO_KEY_COMPANY_DEPT_SET, deptSetJson);
loginUser.setInfo(infoMap);
setLoginUserForTest(loginUser);
boolean result = interceptor.preHandle(request, response, handlerMethod);
assertFalse(result);
verify(writer).write(contains("400"));
}
// 工具方法:通过 Spring Security 设置当前登录用户,仅测试环境使用
private void setLoginUserForTest(LoginUser loginUser) {
// 使用 Spring Security 的 SecurityContextHolder 设置 Authentication
getContext()
.setAuthentication(new UsernamePasswordAuthenticationToken(
loginUser, null, null
));
}
}