diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/xzd/config/WebMvcConfig.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/xzd/config/WebMvcConfig.java new file mode 100644 index 00000000..18d6daac --- /dev/null +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/xzd/config/WebMvcConfig.java @@ -0,0 +1,21 @@ +package org.dromara.xzd.config; + +import org.dromara.xzd.interceptor.RequestLogInterceptor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + + @Autowired + private RequestLogInterceptor requestLogInterceptor; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(requestLogInterceptor) + .addPathPatterns("/xzd/**") // 拦截所有路径 + .order(1); + } +} diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/xzd/domain/XzdCustomerSupplier.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/xzd/domain/XzdCustomerSupplier.java index 193a43e7..0263ffc2 100644 --- a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/xzd/domain/XzdCustomerSupplier.java +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/xzd/domain/XzdCustomerSupplier.java @@ -1,8 +1,12 @@ package org.dromara.xzd.domain; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; +import org.dromara.common.mybatis.core.domain.BaseEntity; +import com.baomidou.mybatisplus.annotation.*; import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serial; +import java.io.Serializable; /** * 供应商-客户中间对象 xzd_customer_supplier @@ -12,7 +16,10 @@ import lombok.Data; */ @Data @TableName("xzd_customer_supplier") -public class XzdCustomerSupplier { +public class XzdCustomerSupplier implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; /** * diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/xzd/domain/XzdCustomerSupplierYyb.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/xzd/domain/XzdCustomerSupplierYyb.java index 9981d91a..3a274aca 100644 --- a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/xzd/domain/XzdCustomerSupplierYyb.java +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/xzd/domain/XzdCustomerSupplierYyb.java @@ -1,8 +1,12 @@ package org.dromara.xzd.domain; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; +import org.dromara.common.mybatis.core.domain.BaseEntity; +import com.baomidou.mybatisplus.annotation.*; import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serial; +import java.io.Serializable; /** * 供应商-客户中间对象 xzd_customer_supplier_yyb @@ -12,7 +16,10 @@ import lombok.Data; */ @Data @TableName("xzd_customer_supplier_yyb") -public class XzdCustomerSupplierYyb { +public class XzdCustomerSupplierYyb implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; /** * diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/xzd/handler/AutoIncrementIdHolder.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/xzd/handler/AutoIncrementIdHolder.java new file mode 100644 index 00000000..2a45706f --- /dev/null +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/xzd/handler/AutoIncrementIdHolder.java @@ -0,0 +1,27 @@ +package org.dromara.xzd.handler; + +import org.springframework.stereotype.Component; + +/** + * 存储AOP拦截到的自增ID,供拦截器使用(线程安全) + */ +@Component +public class AutoIncrementIdHolder { + // 存储当前线程新增操作生成的ID + private static final ThreadLocal ID_HOLDER = new ThreadLocal<>(); + + // 设置ID + public static void setId(Long id) { + ID_HOLDER.set(id); + } + + // 获取ID + public static Long getId() { + return ID_HOLDER.get(); + } + + // 清除ID(避免内存泄漏) + public static void clear() { + ID_HOLDER.remove(); + } +} diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/xzd/interceptor/RequestCachingFilter.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/xzd/interceptor/RequestCachingFilter.java new file mode 100644 index 00000000..716e3510 --- /dev/null +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/xzd/interceptor/RequestCachingFilter.java @@ -0,0 +1,36 @@ +package org.dromara.xzd.interceptor; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.ContentCachingRequestWrapper; + + +import java.io.IOException; + +/** + * 提前包装请求,确保POST/PUT请求体能被缓存 + */ +@Component +@Order(Ordered.HIGHEST_PRECEDENCE) // 最高优先级 +public class RequestCachingFilter extends OncePerRequestFilter { + private static final int CACHE_SIZE = 1024 * 1024; // 1MB缓存容量 + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + String method = request.getMethod(); + if ("POST".equals(method) || "PUT".equals(method)) { + // 包装请求,支持大请求体 + ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request, CACHE_SIZE); + filterChain.doFilter(wrappedRequest, response); + } else { + filterChain.doFilter(request, response); + } + } +} diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/xzd/interceptor/RequestLogInterceptor.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/xzd/interceptor/RequestLogInterceptor.java new file mode 100644 index 00000000..25065416 --- /dev/null +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/xzd/interceptor/RequestLogInterceptor.java @@ -0,0 +1,390 @@ +package org.dromara.xzd.interceptor; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.dromara.xzd.domain.XzdCustomerSupplierYyb; +import org.dromara.xzd.handler.AutoIncrementIdHolder; +import org.dromara.xzd.mapper.XzdCustomerSupplierYybMapper; +import org.dromara.xzd.utilS.RequestParamExtractor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.util.ContentCachingRequestWrapper; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.util.*; + +@Slf4j +@Component +public class RequestLogInterceptor implements HandlerInterceptor { + + @Autowired + private XzdCustomerSupplierYybMapper customerSupplierYybMapper; + @Autowired + private AutoIncrementIdHolder autoIncrementIdHolder; + + // 需要拦截的请求方法 + private static final List INTERCEPT_METHODS = Arrays.asList("POST", "PUT", "DELETE"); + + // 需要提取的请求体字段(根据实际需求修改) + private static final List EXTRACT_FIELDS = Arrays.asList("id", + "jointInvestmentEntity", + "constructionUnit", + "biddingUnit", + "bidUnit", + "depositReceivingUnit", + "applicationOrganization", + "applicationUnit", + "partyAUnit", + "partyBUnit", + "invoicingUnit", + "ticketReceivingUnit", + "invoiceIssuingUnit", + "invoiceReceivingUnit", + "signingOrganization", + "partyCUnit", + "buyerName", + "auditUnit", + "supervisionUnit", + "ownerUnit", + "partyA", + "partyB", + "invoiceIssuer", + "invoiceReceiver", + "receiptUnit", + "settlementUnit", + "currentInvoiceUnit", + "owner", + "supervision", + "review", + "fkdwId", + "payer", + "skdwId"); + + + // 缓存POST请求数据(线程安全) + private final ThreadLocal> postRequestCache = new ThreadLocal<>(); + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + String method = request.getMethod(); + if (!Arrays.asList("POST", "PUT", "DELETE").contains(method)) { // 明确拦截的方法 + return true; + } + + // 处理PUT/DELETE(无需等待ID) + if ("PUT".equals(method) || "DELETE".equals(method)) { + handlePutOrDelete(request, method); + } + // 缓存POST请求数据(等待后续处理) + else if ("POST".equals(method)) { + // 注意:需要将request包装为ContentCachingRequestWrapper才能重复读取请求体 + ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request); + request.setAttribute("wrappedRequest", wrappedRequest); // 存入attribute供后续使用 + cachePostRequestData(wrappedRequest); + } + return true; + } + + @Override + public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { + log.info("postHandle开始执行:请求方法={},响应状态码={}", + request.getMethod(), response.getStatus()); + + if ("POST".equals(request.getMethod())) { + handlePostAfterSave(); + } + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { + // 清除线程缓存(必须执行,避免内存泄漏) + postRequestCache.remove(); + AutoIncrementIdHolder.clear(); // 清除ID持有者 + } + + // 缓存POST请求数据(优化:使用包装后的request) + private void cachePostRequestData(ContentCachingRequestWrapper wrappedRequest) throws IOException { + String uri = wrappedRequest.getRequestURI(); + Map extractedParams = RequestParamExtractor.extractParams(wrappedRequest, "POST", EXTRACT_FIELDS); + + postRequestCache.set(Map.of( + "uri", uri, + "extractedParams", extractedParams + )); + } + + // 处理POST请求(核心:从ThreadLocal获取AOP存入的ID) + private void handlePostAfterSave() { + Map cachedData = postRequestCache.get(); + if (cachedData == null) { + return; + } + + String uri = (String) cachedData.get("uri"); + Map extractedParams = (Map) cachedData.get("extractedParams"); + + // 1. 尝试从请求参数获取ID(前端传入的情况,如非自增ID) + Long newId = convertToLong(extractedParams.get("id")); + // 2. 从AOP存入的ThreadLocal获取ID(数据库自增/雪花算法生成的情况) + if (newId == null) { + newId = AutoIncrementIdHolder.getId(); + } + + if (newId == null) { + log.warn("未获取到新增ID,跳过保存操作"); + return; + } + + List list = new ArrayList<>(); +// // 3. 保存到数据库(你的业务逻辑) + for (Map.Entry entry : extractedParams.entrySet()) { + String k = entry.getKey(); + Object v = entry.getValue(); + if ("id".equals(k)){ + continue; + } + XzdCustomerSupplierYyb entity = new XzdCustomerSupplierYyb(); + entity.setUri(uri); + entity.setCSId(convertToLong(v)); + entity.setMainTableId(newId); // 核心:使用AOP获取的新增ID + list.add(entity); + } + if (!list.isEmpty()) { + customerSupplierYybMapper.insertBatch(list); + } + log.info("新增ID:{} 已关联保存至XzdCustomerSupplierYyb", newId); + } + + // 处理PUT/DELETE + private void handlePutOrDelete(HttpServletRequest request, String method) { + String uri = request.getRequestURI(); + Map extractedParams = RequestParamExtractor.extractParams(request, method, EXTRACT_FIELDS); + if ("PUT".equals(method)) { + Long mainTableId = convertToLong(extractedParams.get("id")); + customerSupplierYybMapper.delete(new LambdaQueryWrapper().eq(XzdCustomerSupplierYyb::getMainTableId, mainTableId)); + List list = new ArrayList<>(); +// // 3. 保存到数据库(你的业务逻辑) + for (Map.Entry entry : extractedParams.entrySet()) { + String k = entry.getKey(); + Object v = entry.getValue(); + if ("id".equals(k)){ + continue; + } + XzdCustomerSupplierYyb customerSupplierYyb = new XzdCustomerSupplierYyb(); + customerSupplierYyb.setUri(uri); + customerSupplierYyb.setCSId(convertToLong(v)); + customerSupplierYyb.setMainTableId(mainTableId); // 核心:使用AOP获取的新增ID + list.add(customerSupplierYyb); + } + if (!list.isEmpty()) { + customerSupplierYybMapper.insertBatch(list); + } + } else if ("DELETE".equals(method)) { + Object ids = extractedParams.get("ids"); + if (ids != null) { + List list = JSONUtil.parseArray(ids).toList(Long.class); + customerSupplierYybMapper.delete(new LambdaQueryWrapper().in(XzdCustomerSupplierYyb::getMainTableId, list)); + } + } + } + + // 辅助方法:获取请求体 + private String getRequestBody(ContentCachingRequestWrapper request) { + try { + request.getInputStream(); + } catch (IOException e) { + // 忽略异常 + } + byte[] content = request.getContentAsByteArray(); + return content.length > 0 ? new String(content, StandardCharsets.UTF_8) : StrUtil.EMPTY; + } + + // 辅助方法:转换为Long + private Long convertToLong(Object value) { + if (value == null) { + return null; + } + try { + return Long.parseLong(value.toString()); + } catch (NumberFormatException e) { + return null; + } + } + +// @Override +// public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { +// String method = request.getMethod(); +// // 只处理指定方法,且确保请求已被过滤器包装为ContentCachingRequestWrapper +// if (INTERCEPT_METHODS.contains(method) && request instanceof ContentCachingRequestWrapper) { +// // 直接使用过滤器包装后的请求对象 +// ContentCachingRequestWrapper wrappedRequest = (ContentCachingRequestWrapper) request; +// handleRequest(wrappedRequest); +// } +// return true; +// } + +// private void handleRequest(ContentCachingRequestWrapper request) throws IOException { +// String method = request.getMethod(); +// String uri = request.getRequestURI(); +// // 统一提取参数(POST/PUT从body,DELETE从URL) +// Map extractedParams = extractParams(request, method, EXTRACT_FIELDS); +// String requestBody = getRequestBody(request); +// +//// XzdCustomerSupplierYyb customerSupplierYyb = new XzdCustomerSupplierYyb(); +//// customerSupplierYyb.setUri(uri); +//// customerSupplierYyb.setCSId(); +//// customerSupplierYyb.setMainTableId(); +// +// // 根据请求方法执行不同操作 +//// switch (method) { +//// case "POST": +//// // 新增请求直接保存 +//// customerSupplierYybMapper.insert(customerSupplierYyb); +//// break; +//// case "PUT": +//// // 修改请求先删除后新增 +//// customerSupplierYybMapper.deleteByUri(uri); +//// customerSupplierYybMapper.insert(customerSupplierYyb); +//// break; +//// case "DELETE": +//// // 删除请求直接删除记录 +//// customerSupplierYybMapper.deleteByUri(uri); +//// break; +//// } +// } + +// // 获取请求体内容 +// private String getRequestBody(ContentCachingRequestWrapper request) { +// try { +// // 主动读取一次请求体(触发缓存,即使不使用输入流内容) +// request.getInputStream(); +// } catch (IOException e) { +// // 忽略异常(读取失败时继续执行) +// } +// +// // 此时缓存已生效,获取内容 +// byte[] content = request.getContentAsByteArray(); +// if (content.length > 0) { +// return new String(content, StandardCharsets.UTF_8); +// } +// return StrUtil.EMPTY; +// } +// +// /** +// * 从请求中提取指定字段(根据请求方法自动适配来源) +// * @param request 请求对象 +// * @param method 请求方法(POST/PUT/DELETE) +// * @param extractFields 需要提取的字段 +// * @return 提取的字段键值对 +// */ +// public static Map extractParams(HttpServletRequest request, String method, List extractFields) { +// Map params = new HashMap<>(); +// if ("POST".equals(method) || "PUT".equals(method)) { +// // POST/PUT:从JSON请求体提取 +// String requestBody = getJsonBody(request); +// if (StrUtil.isNotEmpty(requestBody)) { +// JSONObject json = JSONObject.parseObject(requestBody); +// for (String field : extractFields) { +// if (json.containsKey(field)) { +// params.put(field, json.get(field)); +// } +// } +// } +// } else if ("DELETE".equals(method)) { +// // DELETE:从URL提取(路径参数 + 查询参数) +// Map urlParams = extractUrlParams(request); +// for (String field : extractFields) { +// if (urlParams.containsKey(field)) { +// params.put(field, urlParams.get(field)); +// } +// } +// } +// return params; +// } +// +// /** +// * 获取POST/PUT的JSON请求体(支持大请求体,确保完整读取) +// */ +// private static String getJsonBody(HttpServletRequest request) { +// // 仅处理包装后的请求 +// if (!(request instanceof ContentCachingRequestWrapper)) { +// log.warn("请求未被ContentCachingRequestWrapper包装,无法获取请求体"); +// return StrUtil.EMPTY; +// } +// +// ContentCachingRequestWrapper wrappedRequest = (ContentCachingRequestWrapper) request; +// byte[] content = null; +// +// try { +// // 1. 尝试通过输入流完整读取(确保大请求体被全部缓存) +// InputStream inputStream = wrappedRequest.getInputStream(); +// if (inputStream != null) { +// // 读取全部字节(IOUtils确保完整读取,支持大文件) +// byte[] fullContent = IOUtils.toByteArray(inputStream); +// if (fullContent.length > 0) { +// content = fullContent; +// log.debug("通过输入流读取请求体,大小:{}字节", fullContent.length); +// } +// } +// +// // 2. 若输入流读取失败,尝试从缓存获取(兼容小请求体) +// if (content == null || content.length == 0) { +// content = wrappedRequest.getContentAsByteArray(); +// if (content.length > 0) { +// log.debug("通过缓存获取请求体,大小:{}字节", content.length); +// } else { +// log.debug("请求体为空(输入流和缓存均无内容)"); +// return StrUtil.EMPTY; +// } +// } +// +// // 3. 转换为字符串(指定UTF-8编码) +// String jsonBody = new String(content, StandardCharsets.UTF_8); +// // 日志脱敏:只输出前1000字符,避免敏感信息和大内容刷屏 +// if (jsonBody.length() > 1000) { +// log.debug("请求体内容:{}...(省略{}字符)", jsonBody.substring(0, 1000), jsonBody.length() - 1000); +// } else { +// log.debug("请求体内容:{}", jsonBody); +// } +// return jsonBody; +// +// } catch (IOException e) { +// log.error("读取请求体失败", e); // 记录详细异常,便于排查 +// // 异常时最后尝试从缓存获取 +// content = wrappedRequest.getContentAsByteArray(); +// return content.length > 0 ? new String(content, StandardCharsets.UTF_8) : StrUtil.EMPTY; +// } +// } +// +// /** +// * 提取DELETE请求的URL参数(路径参数 + 查询参数) +// */ +// private static Map extractUrlParams(HttpServletRequest request) { +// Map params = new HashMap<>(); +// +// // 1. 提取路径参数(如 /api/resource/{id} 中的id) +// Map pathVariables = (Map) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); +// if (pathVariables != null) { +// params.putAll(pathVariables); +// } +// +// // 2. 提取查询参数(如 /api/resource?id=1&name=test 中的id和name) +// UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(request.getRequestURI()); +// request.getParameterMap().forEach((key, values) -> { +// if (values != null && values.length > 0) { +// params.put(key, values[0]); // 取第一个值 +// } +// }); +// +// return params; +// } + +} diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/xzd/interceptor/SaveMethodAspect.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/xzd/interceptor/SaveMethodAspect.java new file mode 100644 index 00000000..3b7703b0 --- /dev/null +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/xzd/interceptor/SaveMethodAspect.java @@ -0,0 +1,79 @@ +package org.dromara.xzd.interceptor; + +import com.baomidou.mybatisplus.annotation.TableId; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import java.lang.reflect.Field; +import org.dromara.xzd.handler.AutoIncrementIdHolder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * 拦截Service的保存方法,获取数据库自增ID + */ +@Aspect +@Component +public class SaveMethodAspect { + + @Autowired + private AutoIncrementIdHolder autoIncrementIdHolder; + + // 切入点:匹配Service中所有"save"开头的方法(修改为你的Service包路径) + @Pointcut("execution(* org.dromara.xzd.*.mapper.*Mapper.insert*(..)) || " + + "execution(* org.dromara.xzd.*.*.mapper.*Mapper.insert*(..)) " + + "|| execution(* org.dromara.xzd.mapper.*Mapper.insert*(..)) ") + public void insertPointcut() {} + + /** + * 后置通知:方法执行后,从参数实体中提取主键 ID + */ + @AfterReturning(pointcut = "insertPointcut()") + public void afterInsert(JoinPoint joinPoint) { + Object[] args = joinPoint.getArgs(); + if (args == null || args.length == 0) { + return; + } + + // 遍历方法参数,处理实体对象 + for (Object arg : args) { + if (arg == null) { + continue; + } + // 通过反射获取实体中的主键字段值 + Object id = getPrimaryKeyValue(arg); + if (id != null) { + // 处理 ID(日志、关联操作等) + AutoIncrementIdHolder.setId((Long) id); + System.out.println("新增操作生成的 ID:" + id + "(实体类:" + arg.getClass().getSimpleName() + ")"); + } + } + } + + /** + * 反射获取实体中被 @TableId 注解标识的主键值 + */ + private Object getPrimaryKeyValue(Object entity) { + Class clazz = entity.getClass(); + // 遍历所有字段(包括父类字段) + while (clazz != null && clazz != Object.class) { + Field[] fields = clazz.getDeclaredFields(); + for (Field field : fields) { + // 判断字段是否被 @TableId 注解标识(MyBatis-Plus 主键) + if (field.isAnnotationPresent(TableId.class)) { + try { + field.setAccessible(true); // 允许访问私有字段 + return field.get(entity); // 返回主键值 + } catch (IllegalAccessException e) { + System.err.println("获取主键值失败:" + e.getMessage()); + } + } + } + clazz = clazz.getSuperclass(); // 检查父类字段 + } + // 若未找到主键字段,可根据需求返回 null 或抛出异常 + System.out.println("实体类 " + entity.getClass().getSimpleName() + " 未找到 @TableId 注解的主键字段"); + return null; + } +} diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/xzd/utilS/RequestParamExtractor.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/xzd/utilS/RequestParamExtractor.java new file mode 100644 index 00000000..0da26caa --- /dev/null +++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/xzd/utilS/RequestParamExtractor.java @@ -0,0 +1,253 @@ +package org.dromara.xzd.utilS; + +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSONObject; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.apache.poi.util.IOUtils; +import org.dromara.common.web.filter.RepeatedlyRequestWrapper; +import org.springframework.web.util.ContentCachingRequestWrapper; +import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.util.UriComponentsBuilder; + + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +@Slf4j +public class RequestParamExtractor { + private static final int MAX_RECURSION_DEPTH = 3; + + /** + * 从请求中提取指定字段(根据请求方法自动适配来源) + * @param request 请求对象 + * @param method 请求方法(POST/PUT/DELETE) + * @param extractFields 需要提取的字段 + * @return 提取的字段键值对 + */ + public static Map extractParams(HttpServletRequest request, String method, List extractFields) { + Map params = new HashMap<>(); + if ("POST".equals(method) || "PUT".equals(method)) { + // POST/PUT:从JSON请求体提取 + String requestBody = getJsonBody(request); + if (StrUtil.isNotEmpty(requestBody)) { + JSONObject json = JSONObject.parseObject(requestBody); + for (String field : extractFields) { + if (json.containsKey(field)) { + params.put(field, json.get(field)); + } + } + } + } else if ("DELETE".equals(method)) { + // DELETE:从URL提取(路径参数 + 查询参数) + String idsStr = extractUrlParams(request); + if (idsStr != null&& !idsStr.isEmpty()) { + // 按逗号拆分(兼容可能的空格,如"1, 2,3") + String[] idStrArray = idsStr.split("\\s*,\\s*"); + // 转换为Long数组(处理数字格式异常) + List idsArray = new ArrayList<>(); + for (int i = 0; i < idStrArray.length; i++) { + String idStr = idStrArray[i]; + if (isNumeric(idStr)) { // 校验是否为数字 + idsArray.add(Long.parseLong(idStr)); + } else { + // 处理无效ID(根据业务需求:可忽略/抛出异常/记录日志) + throw new IllegalArgumentException("无效的ID格式:" + idStr); + } + } + params.put("ids", idsArray); + } + } + return params; + } + + /** + * 获取POST/PUT的JSON请求体(支持大请求体,确保完整读取) + */ + private static String getJsonBody(HttpServletRequest request) { + + // 递归获取最内层的ContentCachingRequestWrapper + ContentCachingRequestWrapper wrappedRequest = getContentCachingWrapper(request, 0); + if (wrappedRequest == null) { + log.warn("未找到ContentCachingRequestWrapper(可能嵌套层数超过限制或类型不匹配)"); + return StrUtil.EMPTY; + } + byte[] content = null; + + try { + // 1. 尝试通过输入流完整读取 + InputStream inputStream = wrappedRequest.getInputStream(); + if (inputStream != null) { + byte[] fullContent = IOUtils.toByteArray(inputStream); + if (fullContent.length > 0) { + content = fullContent; + log.debug("通过输入流读取请求体,大小:{}字节", fullContent.length); + } + } + + // 2. 若输入流读取失败,尝试从缓存获取 + if (content == null || content.length == 0) { + content = wrappedRequest.getContentAsByteArray(); + if (content.length > 0) { + log.debug("通过缓存获取请求体,大小:{}字节", content.length); + } else { + log.debug("请求体为空"); + return StrUtil.EMPTY; + } + } + + // 3. 转换为字符串 + String jsonBody = new String(content, StandardCharsets.UTF_8); + if (jsonBody.length() > 1000) { + log.debug("请求体内容:{}...(省略{}字符)", jsonBody.substring(0, 1000), jsonBody.length() - 1000); + } else { + log.debug("请求体内容:{}", jsonBody); + } + return jsonBody; + + } catch (IOException e) { + log.error("读取请求体失败", e); + content = wrappedRequest.getContentAsByteArray(); + return content.length > 0 ? new String(content, StandardCharsets.UTF_8) : StrUtil.EMPTY; + } + +// // 仅处理包装后的请求 +// if (!(request instanceof ContentCachingRequestWrapper)) { +// log.warn("请求未被ContentCachingRequestWrapper包装,无法获取请求体"); +// return StrUtil.EMPTY; +// } +// +// ContentCachingRequestWrapper wrappedRequest = (ContentCachingRequestWrapper) request; +// byte[] content = null; +// +// try { +// // 1. 尝试通过输入流完整读取(确保大请求体被全部缓存) +// InputStream inputStream = wrappedRequest.getInputStream(); +// if (inputStream != null) { +// // 读取全部字节(IOUtils确保完整读取,支持大文件) +// byte[] fullContent = IOUtils.toByteArray(inputStream); +// if (fullContent.length > 0) { +// content = fullContent; +// log.debug("通过输入流读取请求体,大小:{}字节", fullContent.length); +// } +// } +// +// // 2. 若输入流读取失败,尝试从缓存获取(兼容小请求体) +// if (content == null || content.length == 0) { +// content = wrappedRequest.getContentAsByteArray(); +// if (content.length > 0) { +// log.debug("通过缓存获取请求体,大小:{}字节", content.length); +// } else { +// log.debug("请求体为空(输入流和缓存均无内容)"); +// return StrUtil.EMPTY; +// } +// } +// +// // 3. 转换为字符串(指定UTF-8编码) +// String jsonBody = new String(content, StandardCharsets.UTF_8); +// // 日志脱敏:只输出前1000字符,避免敏感信息和大内容刷屏 +// if (jsonBody.length() > 1000) { +// log.debug("请求体内容:{}...(省略{}字符)", jsonBody.substring(0, 1000), jsonBody.length() - 1000); +// } else { +// log.debug("请求体内容:{}", jsonBody); +// } +// return jsonBody; +// +// } catch (IOException e) { +// log.error("读取请求体失败", e); // 记录详细异常,便于排查 +// // 异常时最后尝试从缓存获取 +// content = wrappedRequest.getContentAsByteArray(); +// return content.length > 0 ? new String(content, StandardCharsets.UTF_8) : StrUtil.EMPTY; +// } + } + + /** + * 提取DELETE请求的URL参数(路径参数 + 查询参数) + */ + private static String extractUrlParams(HttpServletRequest request) { +// Map params = new HashMap<>(); + + // 1. 提取路径参数(适配 /api/resource/{ids} 中的ids,拆分为Long数组) + Map pathVariables = (Map) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); + // 初始化参数Map(用于存储处理后的参数) + if (pathVariables != null) { + // 单独处理"ids"参数:拆分字符串并转换为Long数组 + return pathVariables.get("ids"); + } + return null; +// // 2. 提取查询参数(如 /api/resource?id=1&name=test 中的id和name) +// UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(request.getRequestURI()); +// request.getParameterMap().forEach((key, values) -> { +// if (values != null && values.length > 0) { +// params.put(key, values[0]); // 取第一个值 +// } +// }); + + } + // 辅助方法:校验字符串是否为纯数字(避免Long.parseLong抛出异常) + private static boolean isNumeric(String str) { + if (str == null || str.isEmpty()) { + return false; + } + // 正则匹配整数(支持正数/负数) + return Pattern.matches("^[-+]?\\d+$", str); + } + + + /** + * 递归解析多层嵌套请求,找到ContentCachingRequestWrapper + * @param request 当前请求对象 + * @param depth 当前递归深度 + * @return 内层的ContentCachingRequestWrapper(未找到则返回null) + */ + private static ContentCachingRequestWrapper getContentCachingWrapper(HttpServletRequest request, int depth) { + // 终止条件1:超过最大递归深度(防止无限循环) + if (depth >= MAX_RECURSION_DEPTH) { + log.warn("递归获取请求体超过最大深度{},终止解析", MAX_RECURSION_DEPTH); + return null; + } + + // 终止条件2:当前请求已是ContentCachingRequestWrapper,直接返回 + if (request instanceof ContentCachingRequestWrapper) { + return (ContentCachingRequestWrapper) request; + } + + // 尝试获取内层请求(通常包装类会提供getRequest()方法) + HttpServletRequest innerRequest = getInnerRequest(request); + if (innerRequest == null) { + log.debug("当前请求{}无内层请求,终止解析", request.getClass().getSimpleName()); + return null; + } + + // 递归解析内层请求(深度+1) + return getContentCachingWrapper(innerRequest, depth + 1); + } + + /** + * 获取包装类中的内层请求(适配常见的包装类,如RepeatedlyRequestWrapper、RequestWrapper等) + * 原理:通过反射或方法调用获取内层request(多数包装类会实现getRequest()方法) + */ + private static HttpServletRequest getInnerRequest(HttpServletRequest request) { + try { + // 尝试调用getRequest()方法(多数包装类的标准实现) + return (HttpServletRequest) request.getClass().getMethod("getRequest").invoke(request); + } catch (Exception e) { + // 若没有getRequest()方法,检查是否有其他获取内层请求的方法(如getOriginalRequest()) + try { + return (HttpServletRequest) request.getClass().getMethod("getOriginalRequest").invoke(request); + } catch (Exception ex) { + log.debug("请求{}无法获取内层请求(无getRequest()或getOriginalRequest()方法)", + request.getClass().getSimpleName(), ex); + return null; + } + } + } + +}