fix: 修复 findInSet SQL 注入风险

- 调整 MyBatisUtils.findInSet 使用 MyBatis-Plus 参数绑定
- 增加 columnName 白名单校验,避免列名被注入
- 补充 H2 兼容实现,恢复相关单测覆盖
- 替换各模块动态 FIND_IN_SET 字符串拼接写法
- 补充单参数、多参数绑定场景单测
This commit is contained in:
YunaiV
2026-06-07 02:40:46 +08:00
parent e72c02497f
commit a426cc2f4b
17 changed files with 154 additions and 48 deletions
@@ -20,51 +20,49 @@ public enum DbTypeEnum {
/**
* H2
*
* 注意:H2 不支持 find_in_set 函数
*/
H2(DbType.H2, "H2", ""),
H2(DbType.H2, "H2", "POSITION(',' || CAST(#{value} AS VARCHAR) || ',' IN ',' || #{column} || ',') > 0"),
/**
* MySQL
*/
MY_SQL(DbType.MYSQL, "MySQL", "FIND_IN_SET('#{value}', #{column}) <> 0"),
MY_SQL(DbType.MYSQL, "MySQL", "FIND_IN_SET(#{value}, #{column}) <> 0"),
/**
* Oracle
*/
ORACLE(DbType.ORACLE, "Oracle", "FIND_IN_SET('#{value}', #{column}) <> 0"),
ORACLE(DbType.ORACLE, "Oracle", "INSTR(',' || #{column} || ',', ',' || #{value} || ',') > 0"),
/**
* PostgreSQL
*
* 华为 openGauss 使用 ProductName 与 PostgreSQL 相同
*/
POSTGRE_SQL(DbType.POSTGRE_SQL,"PostgreSQL", "POSITION('#{value}' IN #{column}) <> 0"),
POSTGRE_SQL(DbType.POSTGRE_SQL, "PostgreSQL", "POSITION(',' || CAST(#{value} AS VARCHAR) || ',' IN ',' || #{column} || ',') > 0"),
/**
* SQL Server
*/
SQL_SERVER(DbType.SQL_SERVER, "Microsoft SQL Server", "CHARINDEX(',' + #{value} + ',', ',' + #{column} + ',') <> 0"),
SQL_SERVER(DbType.SQL_SERVER, "Microsoft SQL Server", "CHARINDEX(',' + CAST(#{value} AS varchar(255)) + ',', ',' + #{column} + ',') > 0"),
/**
* SQL Server 2005
*/
SQL_SERVER2005(DbType.SQL_SERVER2005, "Microsoft SQL Server 2005", "CHARINDEX(',' + #{value} + ',', ',' + #{column} + ',') <> 0"),
SQL_SERVER2005(DbType.SQL_SERVER2005, "Microsoft SQL Server 2005", "CHARINDEX(',' + CAST(#{value} AS varchar(255)) + ',', ',' + #{column} + ',') > 0"),
/**
* 达梦
*/
DM(DbType.DM, "DM DBMS", "FIND_IN_SET('#{value}', #{column}) <> 0"),
DM(DbType.DM, "DM DBMS", "FIND_IN_SET(#{value}, #{column}) <> 0"),
/**
* 人大金仓
*/
KINGBASE_ES(DbType.KINGBASE_ES, "KingbaseES", "POSITION('#{value}' IN #{column}) <> 0"),
KINGBASE_ES(DbType.KINGBASE_ES, "KingbaseES", "POSITION(',' || CAST(#{value} AS VARCHAR) || ',' IN ',' || #{column} || ',') > 0"),
/**
* OceanBase
*/
OCEAN_BASE(DbType.OCEAN_BASE, "OceanBase", "FIND_IN_SET('#{value}', #{column}) <> 0")
OCEAN_BASE(DbType.OCEAN_BASE, "OceanBase", "FIND_IN_SET(#{value}, #{column}) <> 0")
;
@@ -95,7 +93,9 @@ public enum DbTypeEnum {
}
public static String getFindInSetTemplate(DbType dbType) {
return Optional.of(MAP_BY_MP.get(dbType).getFindInSetTemplate())
return Optional.ofNullable(MAP_BY_MP.get(dbType))
.map(DbTypeEnum::getFindInSetTemplate)
.filter(StrUtil::isNotBlank)
.orElseThrow(() -> new IllegalArgumentException("FIND_IN_SET not supported"));
}
}
@@ -34,6 +34,10 @@ public class MyBatisUtils {
private static final Pattern SAFE_COLUMN_NAME_PATTERN = Pattern.compile("^[a-zA-Z0-9_]+(\\.[a-zA-Z0-9_]+)*$");
private static final String FIND_IN_SET_VALUE_PLACEHOLDER = "#{value}";
private static final String FIND_IN_SET_COLUMN_PLACEHOLDER = "#{column}";
public static <T> Page<T> buildPage(PageParam pageParam) {
return buildPage(pageParam, null);
}
@@ -157,15 +161,43 @@ public class MyBatisUtils {
/**
* 跨数据库的 find_in_set 实现
*
* @param column 字段名称
* @param value 查询值(不带单引号)
* @param columnName 字段名称
* @return sql
*/
public static String findInSet(String column, Object value) {
public static String findInSet(String columnName) {
return findInSet(columnName, 0);
}
/**
* 跨数据库的 find_in_set 实现,适用于同一个 apply 语句中有多个参数的场景
*
* @param columnName 字段名称
* @param paramIndex apply 参数序号
* @return sql
*/
public static String findInSetWithParamIndex(String columnName, int paramIndex) {
return findInSet(columnName, paramIndex);
}
private static String findInSet(String columnName, int paramIndex) {
DbType dbType = JdbcUtils.getDbType();
return findInSet(dbType, columnName, paramIndex);
}
static String findInSet(DbType dbType, String columnName, int paramIndex) {
if (!isSafeColumnName(columnName)) {
throw new IllegalArgumentException("Invalid column name: " + columnName);
}
if (paramIndex < 0) {
throw new IllegalArgumentException("Invalid param index: " + paramIndex);
}
return DbTypeEnum.getFindInSetTemplate(dbType)
.replace("#{column}", column)
.replace("#{value}", StrUtil.toString(value));
.replace(FIND_IN_SET_COLUMN_PLACEHOLDER, columnName)
.replace(FIND_IN_SET_VALUE_PLACEHOLDER, "{" + paramIndex + "}");
}
private static boolean isSafeColumnName(String columnName) {
return StrUtil.isNotEmpty(columnName) && SAFE_COLUMN_NAME_PATTERN.matcher(columnName).matches();
}
/**
@@ -2,6 +2,7 @@ package cn.iocoder.yudao.framework.mybatis.core.util;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.SortingField;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
@@ -13,6 +14,7 @@ import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
@@ -98,6 +100,71 @@ public class MyBatisUtilsTest {
assertEquals("DESC", MyBatisUtils.getOrderDirection(null));
}
@Test
public void testFindInSet() {
assertEquals("FIND_IN_SET({0}, websites) <> 0",
MyBatisUtils.findInSet(DbType.MYSQL, "websites", 0));
assertEquals("POSITION(',' || CAST({0} AS VARCHAR) || ',' IN ',' || websites || ',') > 0",
MyBatisUtils.findInSet(DbType.H2, "websites", 0));
assertEquals("INSTR(',' || t.websites || ',', ',' || {0} || ',') > 0",
MyBatisUtils.findInSet(DbType.ORACLE, "t.websites", 0));
assertEquals("POSITION(',' || CAST({1} AS VARCHAR) || ',' IN ',' || websites || ',') > 0",
MyBatisUtils.findInSet(DbType.POSTGRE_SQL, "websites", 1));
assertEquals("CHARINDEX(',' + CAST({2} AS varchar(255)) + ',', ',' + websites + ',') > 0",
MyBatisUtils.findInSet(DbType.SQL_SERVER, "websites", 2));
}
@Test
public void testFindInSet_invalidColumnName() {
assertThrows(IllegalArgumentException.class,
() -> MyBatisUtils.findInSet(DbType.MYSQL, "websites;drop table system_tenant", 0));
assertThrows(IllegalArgumentException.class,
() -> MyBatisUtils.findInSet(DbType.MYSQL, "FIND_IN_SET(value, websites)", 0));
}
@Test
public void testFindInSet_invalidParamIndex() {
assertThrows(IllegalArgumentException.class,
() -> MyBatisUtils.findInSet(DbType.MYSQL, "websites", -1));
}
@Test
public void testFindInSet_applyBindsValue() {
// 准备参数
QueryWrapper<Object> query = new QueryWrapper<>();
String value = "test' OR 1 = 1";
// 调用
query.apply(MyBatisUtils.findInSet(DbType.MYSQL, "to_mails", 0), value);
// 断言:SQL 片段里只有 MyBatis Plus 参数占位,用户输入不会被直接拼接进去
assertEquals("(FIND_IN_SET(#{ew.paramNameValuePairs.MPGENVAL1}, to_mails) <> 0)",
query.getSqlSegment());
assertFalse(query.getSqlSegment().contains(value));
assertEquals(value, query.getParamNameValuePairs().get("MPGENVAL1"));
}
@Test
public void testFindInSet_applyBindsMultipleValues() {
// 准备参数
QueryWrapper<Object> query = new QueryWrapper<>();
String value1 = "1' OR 1 = 1";
String value2 = "2' OR 1 = 1";
// 调用
query.apply(MyBatisUtils.findInSet(DbType.MYSQL, "tag_ids", 0)
+ " OR " + MyBatisUtils.findInSet(DbType.MYSQL, "tag_ids", 1), value1, value2);
// 断言:多个参数都由 MyBatis Plus 生成占位符,不拼接用户输入
assertEquals("(FIND_IN_SET(#{ew.paramNameValuePairs.MPGENVAL1}, tag_ids) <> 0"
+ " OR FIND_IN_SET(#{ew.paramNameValuePairs.MPGENVAL2}, tag_ids) <> 0)",
query.getSqlSegment());
assertFalse(query.getSqlSegment().contains(value1));
assertFalse(query.getSqlSegment().contains(value2));
assertEquals(value1, query.getParamNameValuePairs().get("MPGENVAL1"));
assertEquals(value2, query.getParamNameValuePairs().get("MPGENVAL2"));
}
private void assertOrderItem(OrderItem orderItem, String column, boolean asc) {
assertEquals(column, orderItem.getColumn());
assertEquals(asc, orderItem.isAsc());
@@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.crm.dal.mysql.customer;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.limitconfig.CrmCustomerLimitConfigPageReqVO;
import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerLimitConfigDO;
import org.apache.ibatis.annotations.Mapper;
@@ -28,9 +29,9 @@ public interface CrmCustomerLimitConfigMapper extends BaseMapperX<CrmCustomerLim
LambdaQueryWrapperX<CrmCustomerLimitConfigDO> query = new LambdaQueryWrapperX<CrmCustomerLimitConfigDO>()
.eq(CrmCustomerLimitConfigDO::getType, type);
query.and(w -> {
w.apply("FIND_IN_SET({0}, user_ids) > 0", userId);
w.apply(MyBatisUtils.findInSet("user_ids"), userId);
if (deptId != null) {
w.or().apply("FIND_IN_SET({0}, dept_ids) > 0", deptId);
w.or().apply(MyBatisUtils.findInSet("dept_ids"), deptId);
}
});
return selectList(query);
@@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.im.dal.mysql.message;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.channel.ImChannelMessagePageReqVO;
import cn.iocoder.yudao.module.im.dal.dataobject.message.ImChannelMessageDO;
import org.apache.ibatis.annotations.Mapper;
@@ -32,7 +33,7 @@ public interface ImChannelMessageMapper extends BaseMapperX<ImChannelMessageDO>
.gt(ImChannelMessageDO::getId, minId)
.and(w -> w.isNull(ImChannelMessageDO::getReceiverUserIds)
.or().eq(ImChannelMessageDO::getReceiverUserIds, "")
.or().apply("FIND_IN_SET({0}, receiver_user_ids)", userId))
.or().apply(MyBatisUtils.findInSet("receiver_user_ids"), userId))
.orderByAsc(ImChannelMessageDO::getId)
.last("LIMIT " + size));
}
@@ -33,7 +33,7 @@ public interface IotAlertConfigMapper extends BaseMapperX<IotAlertConfigDO> {
default List<IotAlertConfigDO> selectListBySceneRuleIdAndStatus(Long sceneRuleId, Integer status) {
return selectList(new LambdaQueryWrapperX<IotAlertConfigDO>()
.eq(IotAlertConfigDO::getStatus, status)
.apply(MyBatisUtils.findInSet("scene_rule_ids", sceneRuleId)));
.apply(MyBatisUtils.findInSet("scene_rule_ids"), sceneRuleId));
}
}
}
@@ -4,6 +4,7 @@ import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.IotDevicePageReqVO;
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum;
@@ -34,7 +35,7 @@ public interface IotDeviceMapper extends BaseMapperX<IotDeviceDO> {
.likeIfPresent(IotDeviceDO::getNickname, reqVO.getNickname())
.eqIfPresent(IotDeviceDO::getState, reqVO.getStatus())
.eqIfPresent(IotDeviceDO::getGatewayId, reqVO.getGatewayId())
.apply(ObjectUtil.isNotNull(reqVO.getGroupId()), "FIND_IN_SET(" + reqVO.getGroupId() + ",group_ids) > 0")
.apply(ObjectUtil.isNotNull(reqVO.getGroupId()), MyBatisUtils.findInSet("group_ids"), reqVO.getGroupId())
.orderByDesc(IotDeviceDO::getId));
}
@@ -72,7 +73,7 @@ public interface IotDeviceMapper extends BaseMapperX<IotDeviceDO> {
default Long selectCountByGroupId(Long groupId) {
return selectCount(new LambdaQueryWrapperX<IotDeviceDO>()
.apply("FIND_IN_SET(" + groupId + ",group_ids) > 0"));
.apply(MyBatisUtils.findInSet("group_ids"), groupId));
}
default Long selectCountByCreateTime(@Nullable LocalDateTime createTime) {
@@ -28,7 +28,7 @@ public interface IotDataRuleMapper extends BaseMapperX<IotDataRuleDO> {
default List<IotDataRuleDO> selectListBySinkId(Long sinkId) {
return selectList(new LambdaQueryWrapperX<IotDataRuleDO>()
.apply(MyBatisUtils.findInSet("sink_ids", sinkId)));
.apply(MyBatisUtils.findInSet("sink_ids"), sinkId));
}
default List<IotDataRuleDO> selectListByStatus(Integer status) {
@@ -39,4 +39,4 @@ public interface IotDataRuleMapper extends BaseMapperX<IotDataRuleDO> {
return selectOne(IotDataRuleDO::getName, name);
}
}
}
@@ -5,6 +5,7 @@ import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
import cn.iocoder.yudao.module.promotion.controller.admin.coupon.vo.template.CouponTemplatePageReqVO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponTemplateDO;
import cn.iocoder.yudao.module.promotion.enums.coupon.CouponTemplateValidityTypeEnum;
@@ -34,7 +35,7 @@ public interface CouponTemplateMapper extends BaseMapperX<CouponTemplateDO> {
.eqIfPresent(CouponTemplateDO::getDiscountType, reqVO.getDiscountType())
.betweenIfPresent(CouponTemplateDO::getCreateTime, reqVO.getCreateTime())
.eqIfPresent(CouponTemplateDO::getProductScope, reqVO.getProductScope())
.and(reqVO.getProductScopeValue() != null, w -> w.apply("FIND_IN_SET({0}, product_scope_values)",
.and(reqVO.getProductScopeValue() != null, w -> w.apply(MyBatisUtils.findInSet("product_scope_values"),
reqVO.getProductScopeValue()))
.and(canTakeConsumer != null, canTakeConsumer)
.orderByDesc(CouponTemplateDO::getId));
@@ -61,7 +62,7 @@ public interface CouponTemplateMapper extends BaseMapperX<CouponTemplateDO> {
Consumer<LambdaQueryWrapper<CouponTemplateDO>> canTakeConsumer = buildCanTakeQueryConsumer(canTakeTypes);
return selectList(new LambdaQueryWrapperX<CouponTemplateDO>()
.eqIfPresent(CouponTemplateDO::getProductScope, productScope)
.and(productScopeValue != null, w -> w.apply("FIND_IN_SET({0}, product_scope_values)",
.and(productScopeValue != null, w -> w.apply(MyBatisUtils.findInSet("product_scope_values"),
productScopeValue))
.and(canTakeConsumer != null, canTakeConsumer)
.last(" LIMIT " + count)
@@ -1,9 +1,9 @@
package cn.iocoder.yudao.module.promotion.dal.mysql.reward;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityPageReqVO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO;
import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
@@ -14,6 +14,7 @@ import java.util.Collection;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
/**
* 满减送活动 Mapper
@@ -34,18 +35,18 @@ public interface RewardActivityMapper extends BaseMapperX<RewardActivityDO> {
Collection<Long> categoryIds,
Integer status) {
LocalDateTime now = LocalDateTime.now();
Function<Collection<Long>, String> productScopeValuesFindInSetFunc = ids -> ids.stream()
.map(id -> StrUtil.format("FIND_IN_SET({}, product_scope_values) ", id))
Function<Collection<Long>, String> productScopeValuesFindInSetFunc = ids -> IntStream.range(0, ids.size())
.mapToObj(index -> MyBatisUtils.findInSetWithParamIndex("product_scope_values", index))
.collect(Collectors.joining(" OR "));
return selectList(new LambdaQueryWrapperX<RewardActivityDO>()
.eq(RewardActivityDO::getStatus, status)
.lt(RewardActivityDO::getStartTime, now)
.gt(RewardActivityDO::getEndTime, now)
.and(i -> i.eq(RewardActivityDO::getProductScope, PromotionProductScopeEnum.SPU.getScope())
.and(i1 -> i1.apply(productScopeValuesFindInSetFunc.apply(spuIds)))
.and(i1 -> i1.apply(productScopeValuesFindInSetFunc.apply(spuIds), spuIds.toArray()))
.or(i1 -> i1.eq(RewardActivityDO::getProductScope, PromotionProductScopeEnum.ALL.getScope()))
.or(i1 -> i1.eq(RewardActivityDO::getProductScope, PromotionProductScopeEnum.CATEGORY.getScope())
.and(i2 -> i2.apply(productScopeValuesFindInSetFunc.apply(categoryIds)))))
.and(i2 -> i2.apply(productScopeValuesFindInSetFunc.apply(categoryIds), categoryIds.toArray()))))
.orderByDesc(RewardActivityDO::getId)
);
}
@@ -5,6 +5,7 @@ import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
import cn.iocoder.yudao.module.promotion.controller.admin.seckill.vo.activity.SeckillActivityPageReqVO;
import cn.iocoder.yudao.module.promotion.controller.app.seckill.vo.activity.AppSeckillActivityPageReqVO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.seckill.SeckillActivityDO;
@@ -27,7 +28,7 @@ public interface SeckillActivityMapper extends BaseMapperX<SeckillActivityDO> {
.likeIfPresent(SeckillActivityDO::getName, reqVO.getName())
.eqIfPresent(SeckillActivityDO::getStatus, reqVO.getStatus())
.betweenIfPresent(SeckillActivityDO::getCreateTime, reqVO.getCreateTime())
.apply(ObjectUtil.isNotNull(reqVO.getConfigId()), "FIND_IN_SET(" + reqVO.getConfigId() + ", config_ids) > 0")
.apply(ObjectUtil.isNotNull(reqVO.getConfigId()), MyBatisUtils.findInSet("config_ids"), reqVO.getConfigId())
.orderByDesc(SeckillActivityDO::getId));
}
@@ -70,7 +71,7 @@ public interface SeckillActivityMapper extends BaseMapperX<SeckillActivityDO> {
.eqIfPresent(SeckillActivityDO::getStatus, status)
.lt(SeckillActivityDO::getStartTime, dateTime)
.gt(SeckillActivityDO::getEndTime, dateTime)// 开始时间 < 指定时间 < 结束时间,也就是说获取指定时间段的活动
.apply(ObjectUtil.isNotNull(pageReqVO.getConfigId()), "FIND_IN_SET(" + pageReqVO.getConfigId() + ",config_ids) > 0"));
.apply(ObjectUtil.isNotNull(pageReqVO.getConfigId()), MyBatisUtils.findInSet("config_ids"), pageReqVO.getConfigId()));
}
default SeckillActivityDO selectBySpuIdAndStatusAndNow(Long spuId, Integer status) {
@@ -2,10 +2,10 @@ package cn.iocoder.yudao.module.member.dal.mysql.user;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
import cn.iocoder.yudao.module.member.controller.admin.user.vo.MemberUserPageReqVO;
import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
@@ -13,6 +13,7 @@ import org.apache.ibatis.annotations.Mapper;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
/**
* 会员 User Mapper
@@ -38,10 +39,12 @@ public interface MemberUserMapper extends BaseMapperX<MemberUserDO> {
default PageResult<MemberUserDO> selectPage(MemberUserPageReqVO reqVO) {
// 处理 tagIds 过滤条件
String tagIdSql = "";
Object[] tagIdParams = new Object[0];
if (CollUtil.isNotEmpty(reqVO.getTagIds())) {
tagIdSql = reqVO.getTagIds().stream()
.map(tagId -> "FIND_IN_SET(" + tagId + ", tag_ids)")
tagIdSql = IntStream.range(0, reqVO.getTagIds().size())
.mapToObj(index -> MyBatisUtils.findInSetWithParamIndex("tag_ids", index))
.collect(Collectors.joining(" OR "));
tagIdParams = reqVO.getTagIds().toArray();
}
// 分页查询
return selectPage(reqVO, new LambdaQueryWrapperX<MemberUserDO>()
@@ -52,7 +55,7 @@ public interface MemberUserMapper extends BaseMapperX<MemberUserDO> {
.betweenIfPresent(MemberUserDO::getCreateTime, reqVO.getCreateTime())
.eqIfPresent(MemberUserDO::getLevelId, reqVO.getLevelId())
.eqIfPresent(MemberUserDO::getGroupId, reqVO.getGroupId())
.apply(StrUtil.isNotEmpty(tagIdSql), tagIdSql)
.apply(CollUtil.isNotEmpty(reqVO.getTagIds()), tagIdSql, tagIdParams)
.orderByDesc(MemberUserDO::getId));
}
@@ -66,7 +69,7 @@ public interface MemberUserMapper extends BaseMapperX<MemberUserDO> {
default Long selectCountByTagId(Long tagId) {
return selectCount(new LambdaQueryWrapperX<MemberUserDO>()
.apply("FIND_IN_SET({0}, tag_ids)", tagId));
.apply(MyBatisUtils.findInSet("tag_ids"), tagId));
}
/**
@@ -24,7 +24,7 @@ public interface MesQcTemplateMapper extends BaseMapperX<MesQcTemplateDO> {
.likeIfPresent(MesQcTemplateDO::getName, reqVO.getName())
.orderByDesc(MesQcTemplateDO::getId);
if (reqVO.getType() != null) {
query.apply(MyBatisUtils.findInSet("types", reqVO.getType()));
query.apply(MyBatisUtils.findInSet("types"), reqVO.getType());
}
query.eqIfPresent(MesQcTemplateDO::getStatus, reqVO.getStatus());
return selectPage(reqVO, query);
@@ -21,7 +21,7 @@ public interface MailLogMapper extends BaseMapperX<MailLogDO> {
.eqIfPresent(MailLogDO::getSendStatus, reqVO.getSendStatus())
.betweenIfPresent(MailLogDO::getSendTime, reqVO.getSendTime())
.apply(StrUtil.isNotBlank(reqVO.getToMail()),
MyBatisUtils.findInSet("to_mails", reqVO.getToMail()))
MyBatisUtils.findInSet("to_mails"), reqVO.getToMail())
.orderByDesc(MailLogDO::getId));
}
@@ -29,7 +29,7 @@ public interface TenantMapper extends BaseMapperX<TenantDO> {
default List<TenantDO> selectListByWebsite(String website) {
return selectList(new LambdaQueryWrapperX<TenantDO>()
.apply(MyBatisUtils.findInSet("websites", website)));
.apply(MyBatisUtils.findInSet("websites"), website));
}
default Long selectCountByPackageId(Long packageId) {
@@ -160,8 +160,8 @@ public class MailLogServiceImplTest extends BaseDbUnitTest {
mailLogMapper.insert(cloneIgnoreId(dbMailLog, o -> o.setUserId(2L)));
// 测试 userType 不匹配
mailLogMapper.insert(cloneIgnoreId(dbMailLog, o -> o.setUserType(UserTypeEnum.MEMBER.getValue())));
// 测试 toMails 不匹配(特殊:find_in_set 无法单测)
// mailLogMapper.insert(cloneIgnoreId(dbMailLog, o -> o.setToMails(Lists.newArrayList("788@qq.com"))));
// 测试 toMails 不匹配
mailLogMapper.insert(cloneIgnoreId(dbMailLog, o -> o.setToMails(Lists.newArrayList("788@qq.com"))));
// 测试 accountId 不匹配
mailLogMapper.insert(cloneIgnoreId(dbMailLog, o -> o.setAccountId(11L)));
// 测试 templateId 不匹配
@@ -174,7 +174,7 @@ public class MailLogServiceImplTest extends BaseDbUnitTest {
MailLogPageReqVO reqVO = new MailLogPageReqVO();
reqVO.setUserId(1L);
reqVO.setUserType(UserTypeEnum.ADMIN.getValue());
// reqVO.setToMail("768@qq.com");
reqVO.setToMail("768@qq.com");
reqVO.setAccountId(10L);
reqVO.setTemplateId(100L);
reqVO.setSendStatus(MailSendStatusEnum.INIT.getStatus());
@@ -22,7 +22,6 @@ import cn.iocoder.yudao.module.system.service.tenant.handler.TenantMenuHandler;
import cn.iocoder.yudao.module.system.service.user.AdminUserService;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
@@ -334,7 +333,6 @@ public class TenantServiceImplTest extends BaseDbUnitTest {
}
@Test
@Disabled // H2 不支持 find_in_set 函数
public void testGetTenantByWebsite() {
// mock 数据
TenantDO dbTenant = randomPojo(TenantDO.class, o -> o.setWebsites(singletonList("https://www.iocoder.cn")));