feat(mybatis): 添加通用查询和分页功能

- 新增 AutoQueryUtil 工具类,实现自动查询功能
- 添加 BaseEntity基类,包含通用的创建和更新字段
- 修改 DefaultDBFieldHandler,支持 BaseEntity 的字段填充
- 新增 PageQuery 类,用于构建分页查询对象
- 添加 TableDataInfo 类,用于封装分页查询结果
- 在 ServletUtils 中增加 getHeader 方法,用于获取请求头信息
This commit is contained in:
weike001 2025-02-20 15:01:49 +08:00
parent b62b7c75f5
commit 6bd0b2f0e0
7 changed files with 511 additions and 7 deletions

View File

@ -4,6 +4,7 @@ import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.servlet.ServletUtil;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.MediaType;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
@ -13,6 +14,8 @@ import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.Map;
@ -59,6 +62,28 @@ public class ServletUtils {
return ua != null ? ua : "";
}
public static String getHeader(HttpServletRequest request, String name){
String value = request.getHeader(name);
if (StringUtils.isEmpty(value)) {
return StringUtils.EMPTY;
}
return urlDecode(value);
}
/**
* 内容解码
*
* @param str 内容
* @return 解码后的内容
*/
public static String urlDecode(String str) {
try {
return URLDecoder.decode(str,"UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
/**
* 获得请求
*

View File

@ -0,0 +1,71 @@
package cn.iocoder.yudao.framework.mybatis.core.dataobject;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* Entity基类
*
* @author Lion Li
*/
@Data
public class BaseEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 搜索值
*/
@JsonIgnore
@TableField(exist = false)
private String searchValue;
// /**
// * 创建部门
// */
// @TableField(fill = FieldFill.INSERT)
// private Long deptId;
/**
* 创建者
*/
@TableField(fill = FieldFill.INSERT)
private Long createBy;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
/**
* 更新者
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateBy;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
/**
* 请求参数
*/
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@TableField(exist = false)
private Map<String, Object> params = new HashMap<>();
}

View File

@ -1,6 +1,7 @@
package cn.iocoder.yudao.framework.mybatis.core.handler;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseEntity;
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
@ -41,6 +42,28 @@ public class DefaultDBFieldHandler implements MetaObjectHandler {
if (Objects.nonNull(userId) && Objects.isNull(baseDO.getUpdater())) {
baseDO.setUpdater(userId.toString());
}
}else if (Objects.nonNull(metaObject) && metaObject.getOriginalObject() instanceof BaseEntity) {
BaseEntity baseEntity = (BaseEntity) metaObject.getOriginalObject();
LocalDateTime current = LocalDateTime.now();
// 创建时间为空则以当前时间为插入时间
if (Objects.isNull(baseEntity.getCreateTime())) {
baseEntity.setCreateTime(current);
}
// 更新时间为空则以当前时间为更新时间
if (Objects.isNull(baseEntity.getUpdateTime())) {
baseEntity.setUpdateTime(current);
}
Long userId = WebFrameworkUtils.getLoginUserId();
// 当前登录用户不为空创建人为空则当前登录用户为创建人
if (Objects.nonNull(userId) && Objects.isNull(baseEntity.getCreateBy())) {
baseEntity.setCreateBy(userId);
}
// 当前登录用户不为空更新人为空则当前登录用户为更新人
if (Objects.nonNull(userId) && Objects.isNull(baseEntity.getUpdateBy())) {
baseEntity.setUpdateBy(userId);
}
}
}
@ -51,12 +74,20 @@ public class DefaultDBFieldHandler implements MetaObjectHandler {
if (Objects.isNull(modifyTime)) {
setFieldValByName("updateTime", LocalDateTime.now(), metaObject);
}
// 当前登录用户不为空更新人为空则当前登录用户为更新人
Object modifier = getFieldValByName("updater", metaObject);
Long userId = WebFrameworkUtils.getLoginUserId();
if (Objects.nonNull(userId) && Objects.isNull(modifier)) {
setFieldValByName("updater", userId.toString(), metaObject);
if (Objects.nonNull(metaObject) && metaObject.getOriginalObject() instanceof BaseDO) {
// 当前登录用户不为空更新人为空则当前登录用户为更新人
Object modifier = getFieldValByName("updater", metaObject);
Long userId = WebFrameworkUtils.getLoginUserId();
if (Objects.nonNull(userId) && Objects.isNull(modifier)) {
setFieldValByName("updater", userId.toString(), metaObject);
}
}else if (Objects.nonNull(metaObject) && metaObject.getOriginalObject() instanceof BaseEntity) {
// 当前登录用户不为空更新人为空则当前登录用户为更新人
Object modifier = getFieldValByName("updateBy", metaObject);
Long userId = WebFrameworkUtils.getLoginUserId();
if (Objects.nonNull(userId) && Objects.isNull(modifier)) {
setFieldValByName("updateBy", userId, metaObject);
}
}
}
}

View File

@ -0,0 +1,141 @@
package cn.iocoder.yudao.framework.mybatis.core.page;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* 分页查询实体类
*
* @author Lion Li
*/
@Data
public class PageQuery implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 分页大小
*/
private Integer pageSize;
/**
* 当前页数
*/
private Integer pageNum;
/**
* 排序列
*/
private String orderByColumn;
/**
* 排序的方向desc或者asc
*/
private String isAsc;
/**
* 当前记录起始索引 默认值
*/
public static final int DEFAULT_PAGE_NUM = 1;
/**
* 每页显示记录数 默认值 默认查全部
*/
public static final int DEFAULT_PAGE_SIZE = Integer.MAX_VALUE;
/**
* 仅支持字母数字下划线空格逗号小数点支持多个字段排序
*/
public static final String SQL_PATTERN = "[a-zA-Z0-9_\\ \\,\\.]+";
public static final String SEPARATOR = ",";
/**
* 构建分页对象
*/
public <T> Page<T> build() {
Integer pageNum = ObjectUtil.defaultIfNull(getPageNum(), DEFAULT_PAGE_NUM);
Integer pageSize = ObjectUtil.defaultIfNull(getPageSize(), DEFAULT_PAGE_SIZE);
if (pageNum <= 0) {
pageNum = DEFAULT_PAGE_NUM;
}
Page<T> page = new Page<>(pageNum, pageSize);
List<OrderItem> orderItems = buildOrderItem();
if (CollUtil.isNotEmpty(orderItems)) {
page.addOrder(orderItems);
}
return page;
}
/**
* 构建排序
*
* 支持的用法如下:
* {isAsc:"asc",orderByColumn:"id"} order by id asc
* {isAsc:"asc",orderByColumn:"id,createTime"} order by id asc,create_time asc
* {isAsc:"desc",orderByColumn:"id,createTime"} order by id desc,create_time desc
* {isAsc:"asc,desc",orderByColumn:"id,createTime"} order by id asc,create_time desc
*/
private List<OrderItem> buildOrderItem() {
if (StringUtils.isBlank(orderByColumn) || StringUtils.isBlank(isAsc)) {
return null;
}
String orderBy = escapeOrderBySql(orderByColumn);
orderBy = StrUtil.toUnderlineCase(orderBy);
// 兼容前端排序类型
isAsc = StringUtils.replaceEach(isAsc, new String[]{"ascending", "descending"}, new String[]{"asc", "desc"});
String[] orderByArr = orderBy.split(SEPARATOR);
String[] isAscArr = isAsc.split(SEPARATOR);
if (isAscArr.length != 1 && isAscArr.length != orderByArr.length) {
throw new ServiceException(500,"排序参数有误");
}
List<OrderItem> list = new ArrayList<>();
// 每个字段各自排序
for (int i = 0; i < orderByArr.length; i++) {
String orderByStr = orderByArr[i];
String isAscStr = isAscArr.length == 1 ? isAscArr[0] : isAscArr[i];
if ("asc".equals(isAscStr)) {
list.add(OrderItem.asc(orderByStr));
} else if ("desc".equals(isAscStr)) {
list.add(OrderItem.desc(orderByStr));
} else {
throw new ServiceException(500,"排序参数有误");
}
}
return list;
}
public Integer getFirstNum() {
return (pageNum - 1) * pageSize;
}
/**
* 检查字符防止注入绕过
*/
public static String escapeOrderBySql(String value) {
if (StringUtils.isNotEmpty(value) && !isValidOrderBySql(value)) {
throw new IllegalArgumentException("参数不符合规范,不能进行查询");
}
return value;
}
/**
* 验证 order by 语法是否符合规范
*/
public static boolean isValidOrderBySql(String value) {
return value.matches(SQL_PATTERN);
}
}

View File

@ -0,0 +1,70 @@
package cn.iocoder.yudao.framework.mybatis.core.page;
import com.baomidou.mybatisplus.core.metadata.IPage;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.List;
/**
* 表格分页数据对象
*
* @author Lion Li
*/
@Data
@NoArgsConstructor
public class TableDataInfo<T> implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 总记录数
*/
private long total;
/**
* 列表数据
*/
private List<T> list;
/**
* 分页
*
* @param list 列表数据
* @param total 总记录数
*/
public TableDataInfo(List<T> list, long total) {
this.list = list;
this.total = total;
}
/**
* 根据分页对象构建表格分页数据对象
*/
public static <T> TableDataInfo<T> build(IPage<T> page) {
TableDataInfo<T> rspData = new TableDataInfo<>();
rspData.setList(page.getRecords());
rspData.setTotal(page.getTotal());
return rspData;
}
/**
* 根据数据列表构建表格分页数据对象
*/
public static <T> TableDataInfo<T> build(List<T> list) {
TableDataInfo<T> rspData = new TableDataInfo<>();
rspData.setList(list);
rspData.setTotal(list.size());
return rspData;
}
/**
* 构建表格分页数据对象
*/
public static <T> TableDataInfo<T> build() {
TableDataInfo<T> rspData = new TableDataInfo<>();
return rspData;
}
}

View File

@ -0,0 +1,166 @@
package cn.iocoder.yudao.framework.mybatis.core.util;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.reflect.MethodUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.time.*;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class AutoQueryUtil {
private static final Logger log = LoggerFactory.getLogger(AutoQueryUtil.class);
/**
* 根据实体类的属性值是否为空自动创建LambdaQueryWrapper
*
* @param entity 实体对象
* @param <T> 实体类型
* @return LambdaQueryWrapper
*/
public static <T> QueryWrapper<T> autoQuery(T entity) {
QueryWrapper<T> queryWrapper = new QueryWrapper<>();
if (entity == null) {
return queryWrapper;
}
Class<?> clazz = entity.getClass();
// 遍历当前类和所有父类的字段
while (clazz != null) {
Map<String, Method> getterMethods = new HashMap<>();
for (Field field : clazz.getDeclaredFields()) {
field.setAccessible(true);
String fieldName = field.getName();
Class<?> finalClazz = clazz;
Method getterMethod = getterMethods.computeIfAbsent(
"get" + capitalize(fieldName),
k -> {
try {
return MethodUtils.getAccessibleMethod(finalClazz, k, (Class<?>[]) null);
} catch (Exception e) {
log.error("No such getter method: {}", k, e);
return null;
}
}
);
if (getterMethod != null) {
try {
Object value = getterMethod.invoke(entity);
if (value != null && !"".equals(value)) {
handleFieldValue(queryWrapper, field, value);
}
} catch (Exception e) {
log.error("Error invoking getter method: {}", getterMethod.getName(), e);
}
}
}
// 获取父类继续遍历父类的字段
clazz = clazz.getSuperclass();
}
return queryWrapper;
}
public static String getTenantId(){
HttpServletRequest request = ServletUtils.getRequest();
if (null != request) {
// 获取request的header中的x-tenant-id
String tenantId = ServletUtils.getHeader(request, "x-tenant-id");
if (StringUtils.isNotBlank(tenantId)) {
return tenantId;
}
}
return null;
}
private static void handleFieldValue(QueryWrapper<?> queryWrapper, Field field, Object value) {
String fieldName = field.getName();
if (String.class.isAssignableFrom(field.getType())) {
queryWrapper.like(StrUtil.toUnderlineCase(fieldName), value);
} else if (Integer.class.isAssignableFrom(field.getType()) ||
Long.class.isAssignableFrom(field.getType()) ||
Double.class.isAssignableFrom(field.getType()) ||
Float.class.isAssignableFrom(field.getType()) ||
Boolean.class.isAssignableFrom(field.getType()) ||
Short.class.isAssignableFrom(field.getType()) ||
Byte.class.isAssignableFrom(field.getType()) ||
Character.class.isAssignableFrom(field.getType()) ||
Date.class.isAssignableFrom(field.getType()) ||
LocalDateTime.class.isAssignableFrom(field.getType()) ||
LocalDate.class.isAssignableFrom(field.getType()) ||
LocalTime.class.isAssignableFrom(field.getType()) ||
ZonedDateTime.class.isAssignableFrom(field.getType()) ||
Instant.class.isAssignableFrom(field.getType())) {
queryWrapper.eq(StrUtil.toUnderlineCase(fieldName), String.valueOf(value));
} else {
// 如果是其他类型可以添加更多条件或者忽略
log.warn("Unsupported field type: {}", field.getType().getName());
}
}
/**
* 获取 pageReqVO 中所有 LocalDate[]LocalDateTime[] Date[] 类型字段的名称
*/
public static String[] getDateRangeFieldNames(Object pageReqVO) {
return Arrays.stream(pageReqVO.getClass().getDeclaredFields())
.filter(field -> field.getType().equals(LocalDate[].class)
|| field.getType().equals(LocalDateTime[].class)
|| field.getType().equals(Date[].class))
.map(Field::getName)
.toArray(String[]::new);
}
/**
* 动态处理 pageReqVO 中所有 LocalDate[]LocalDateTime[] Date[] 类型字段设置时间段查询条件
*/
public static void handleDateRangeFields(Object pageReqVO, QueryWrapper<?> queryWrapper) {
for (Field field : pageReqVO.getClass().getDeclaredFields()) {
field.setAccessible(true);
try {
Object fieldValue = field.get(pageReqVO);
if (fieldValue == null) {
continue; // 跳过值为 null 的字段
}
if (field.getType().equals(LocalDate[].class)) {
LocalDate[] dateRange = (LocalDate[]) fieldValue;
if (dateRange.length == 2) {
String columnName = StrUtil.toUnderlineCase(field.getName()); // 字段名转换为数据库列名
queryWrapper.between(columnName, dateRange[0], dateRange[1]);
}
} else if (field.getType().equals(LocalDateTime[].class)) {
LocalDateTime[] dateTimeRange = (LocalDateTime[]) fieldValue;
if (dateTimeRange.length == 2) {
String columnName = StrUtil.toUnderlineCase(field.getName()); // 字段名转换为数据库列名
queryWrapper.between(columnName, dateTimeRange[0], dateTimeRange[1]);
}
} else if (field.getType().equals(Date[].class)) {
Date[] range = (Date[]) fieldValue;
if (range.length == 2) {
String columnName = StrUtil.toUnderlineCase(field.getName()); // 字段名转换为数据库列名
queryWrapper.between(columnName, range[0], range[1]);
}
}
} catch (IllegalAccessException e) {
throw new ServiceException(500,"Failed to access field " + field.getName() + ": " + e.getMessage());
}
}
}
private static String capitalize(String str) {
return str.substring(0, 1).toUpperCase() + str.substring(1);
}
}

View File

@ -163,7 +163,7 @@ yudao:
description: 提供管理员管理的所有功能
version: ${yudao.info.version}
tenant: # 多租户相关配置项
enable: true
enable: false
ignore-urls:
- /admin-api/system/tenant/get-id-by-name # 基于名字获取租户,不许带租户编号
- /admin-api/system/tenant/get-by-website # 基于域名获取租户,不许带租户编号