Commit a426cc2f4b8ca83cdf87967c13d68a169822d729

Authored by YunaiV
1 parent e72c0249

fix: 修复 findInSet SQL 注入风险

- 调整 MyBatisUtils.findInSet 使用 MyBatis-Plus 参数绑定
- 增加 columnName 白名单校验,避免列名被注入
- 补充 H2 兼容实现,恢复相关单测覆盖
- 替换各模块动态 FIND_IN_SET 字符串拼接写法
- 补充单参数、多参数绑定场景单测
Showing 17 changed files with 154 additions and 48 deletions
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/enums/DbTypeEnum.java
... ... @@ -20,51 +20,49 @@ public enum DbTypeEnum {
20 20  
21 21 /**
22 22 * H2
23   - *
24   - * 注意:H2 不支持 find_in_set 函数
25 23 */
26   - H2(DbType.H2, "H2", ""),
  24 + H2(DbType.H2, "H2", "POSITION(',' || CAST(#{value} AS VARCHAR) || ',' IN ',' || #{column} || ',') > 0"),
27 25  
28 26 /**
29 27 * MySQL
30 28 */
31   - MY_SQL(DbType.MYSQL, "MySQL", "FIND_IN_SET('#{value}', #{column}) <> 0"),
  29 + MY_SQL(DbType.MYSQL, "MySQL", "FIND_IN_SET(#{value}, #{column}) <> 0"),
32 30  
33 31 /**
34 32 * Oracle
35 33 */
36   - ORACLE(DbType.ORACLE, "Oracle", "FIND_IN_SET('#{value}', #{column}) <> 0"),
  34 + ORACLE(DbType.ORACLE, "Oracle", "INSTR(',' || #{column} || ',', ',' || #{value} || ',') > 0"),
37 35  
38 36 /**
39 37 * PostgreSQL
40 38 *
41 39 * 华为 openGauss 使用 ProductName 与 PostgreSQL 相同
42 40 */
43   - POSTGRE_SQL(DbType.POSTGRE_SQL,"PostgreSQL", "POSITION('#{value}' IN #{column}) <> 0"),
  41 + POSTGRE_SQL(DbType.POSTGRE_SQL, "PostgreSQL", "POSITION(',' || CAST(#{value} AS VARCHAR) || ',' IN ',' || #{column} || ',') > 0"),
44 42  
45 43 /**
46 44 * SQL Server
47 45 */
48   - SQL_SERVER(DbType.SQL_SERVER, "Microsoft SQL Server", "CHARINDEX(',' + #{value} + ',', ',' + #{column} + ',') <> 0"),
  46 + SQL_SERVER(DbType.SQL_SERVER, "Microsoft SQL Server", "CHARINDEX(',' + CAST(#{value} AS varchar(255)) + ',', ',' + #{column} + ',') > 0"),
49 47 /**
50 48 * SQL Server 2005
51 49 */
52   - SQL_SERVER2005(DbType.SQL_SERVER2005, "Microsoft SQL Server 2005", "CHARINDEX(',' + #{value} + ',', ',' + #{column} + ',') <> 0"),
  50 + SQL_SERVER2005(DbType.SQL_SERVER2005, "Microsoft SQL Server 2005", "CHARINDEX(',' + CAST(#{value} AS varchar(255)) + ',', ',' + #{column} + ',') > 0"),
53 51  
54 52 /**
55 53 * 达梦
56 54 */
57   - DM(DbType.DM, "DM DBMS", "FIND_IN_SET('#{value}', #{column}) <> 0"),
  55 + DM(DbType.DM, "DM DBMS", "FIND_IN_SET(#{value}, #{column}) <> 0"),
58 56  
59 57 /**
60 58 * 人大金仓
61 59 */
62   - KINGBASE_ES(DbType.KINGBASE_ES, "KingbaseES", "POSITION('#{value}' IN #{column}) <> 0"),
  60 + KINGBASE_ES(DbType.KINGBASE_ES, "KingbaseES", "POSITION(',' || CAST(#{value} AS VARCHAR) || ',' IN ',' || #{column} || ',') > 0"),
63 61  
64 62 /**
65 63 * OceanBase
66 64 */
67   - OCEAN_BASE(DbType.OCEAN_BASE, "OceanBase", "FIND_IN_SET('#{value}', #{column}) <> 0")
  65 + OCEAN_BASE(DbType.OCEAN_BASE, "OceanBase", "FIND_IN_SET(#{value}, #{column}) <> 0")
68 66  
69 67 ;
70 68  
... ... @@ -95,7 +93,9 @@ public enum DbTypeEnum {
95 93 }
96 94  
97 95 public static String getFindInSetTemplate(DbType dbType) {
98   - return Optional.of(MAP_BY_MP.get(dbType).getFindInSetTemplate())
  96 + return Optional.ofNullable(MAP_BY_MP.get(dbType))
  97 + .map(DbTypeEnum::getFindInSetTemplate)
  98 + .filter(StrUtil::isNotBlank)
99 99 .orElseThrow(() -> new IllegalArgumentException("FIND_IN_SET not supported"));
100 100 }
101 101 }
... ...
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/util/MyBatisUtils.java
... ... @@ -34,6 +34,10 @@ public class MyBatisUtils {
34 34  
35 35 private static final Pattern SAFE_COLUMN_NAME_PATTERN = Pattern.compile("^[a-zA-Z0-9_]+(\\.[a-zA-Z0-9_]+)*$");
36 36  
  37 + private static final String FIND_IN_SET_VALUE_PLACEHOLDER = "#{value}";
  38 +
  39 + private static final String FIND_IN_SET_COLUMN_PLACEHOLDER = "#{column}";
  40 +
37 41 public static <T> Page<T> buildPage(PageParam pageParam) {
38 42 return buildPage(pageParam, null);
39 43 }
... ... @@ -157,15 +161,43 @@ public class MyBatisUtils {
157 161 /**
158 162 * 跨数据库的 find_in_set 实现
159 163 *
160   - * @param column 字段名称
161   - * @param value 查询值(不带单引号)
  164 + * @param columnName 字段名称
  165 + * @return sql
  166 + */
  167 + public static String findInSet(String columnName) {
  168 + return findInSet(columnName, 0);
  169 + }
  170 +
  171 + /**
  172 + * 跨数据库的 find_in_set 实现,适用于同一个 apply 语句中有多个参数的场景
  173 + *
  174 + * @param columnName 字段名称
  175 + * @param paramIndex apply 参数序号
162 176 * @return sql
163 177 */
164   - public static String findInSet(String column, Object value) {
  178 + public static String findInSetWithParamIndex(String columnName, int paramIndex) {
  179 + return findInSet(columnName, paramIndex);
  180 + }
  181 +
  182 + private static String findInSet(String columnName, int paramIndex) {
165 183 DbType dbType = JdbcUtils.getDbType();
  184 + return findInSet(dbType, columnName, paramIndex);
  185 + }
  186 +
  187 + static String findInSet(DbType dbType, String columnName, int paramIndex) {
  188 + if (!isSafeColumnName(columnName)) {
  189 + throw new IllegalArgumentException("Invalid column name: " + columnName);
  190 + }
  191 + if (paramIndex < 0) {
  192 + throw new IllegalArgumentException("Invalid param index: " + paramIndex);
  193 + }
166 194 return DbTypeEnum.getFindInSetTemplate(dbType)
167   - .replace("#{column}", column)
168   - .replace("#{value}", StrUtil.toString(value));
  195 + .replace(FIND_IN_SET_COLUMN_PLACEHOLDER, columnName)
  196 + .replace(FIND_IN_SET_VALUE_PLACEHOLDER, "{" + paramIndex + "}");
  197 + }
  198 +
  199 + private static boolean isSafeColumnName(String columnName) {
  200 + return StrUtil.isNotEmpty(columnName) && SAFE_COLUMN_NAME_PATTERN.matcher(columnName).matches();
169 201 }
170 202  
171 203 /**
... ...
yudao-framework/yudao-spring-boot-starter-mybatis/src/test/java/cn/iocoder/yudao/framework/mybatis/core/util/MyBatisUtilsTest.java
... ... @@ -2,6 +2,7 @@ package cn.iocoder.yudao.framework.mybatis.core.util;
2 2  
3 3 import cn.iocoder.yudao.framework.common.pojo.PageParam;
4 4 import cn.iocoder.yudao.framework.common.pojo.SortingField;
  5 +import com.baomidou.mybatisplus.annotation.DbType;
5 6 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
6 7 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
7 8 import com.baomidou.mybatisplus.core.metadata.OrderItem;
... ... @@ -13,6 +14,7 @@ import java.util.List;
13 14  
14 15 import static org.junit.jupiter.api.Assertions.assertEquals;
15 16 import static org.junit.jupiter.api.Assertions.assertFalse;
  17 +import static org.junit.jupiter.api.Assertions.assertThrows;
16 18 import static org.junit.jupiter.api.Assertions.assertTrue;
17 19  
18 20 /**
... ... @@ -98,6 +100,71 @@ public class MyBatisUtilsTest {
98 100 assertEquals("DESC", MyBatisUtils.getOrderDirection(null));
99 101 }
100 102  
  103 + @Test
  104 + public void testFindInSet() {
  105 + assertEquals("FIND_IN_SET({0}, websites) <> 0",
  106 + MyBatisUtils.findInSet(DbType.MYSQL, "websites", 0));
  107 + assertEquals("POSITION(',' || CAST({0} AS VARCHAR) || ',' IN ',' || websites || ',') > 0",
  108 + MyBatisUtils.findInSet(DbType.H2, "websites", 0));
  109 + assertEquals("INSTR(',' || t.websites || ',', ',' || {0} || ',') > 0",
  110 + MyBatisUtils.findInSet(DbType.ORACLE, "t.websites", 0));
  111 + assertEquals("POSITION(',' || CAST({1} AS VARCHAR) || ',' IN ',' || websites || ',') > 0",
  112 + MyBatisUtils.findInSet(DbType.POSTGRE_SQL, "websites", 1));
  113 + assertEquals("CHARINDEX(',' + CAST({2} AS varchar(255)) + ',', ',' + websites + ',') > 0",
  114 + MyBatisUtils.findInSet(DbType.SQL_SERVER, "websites", 2));
  115 + }
  116 +
  117 + @Test
  118 + public void testFindInSet_invalidColumnName() {
  119 + assertThrows(IllegalArgumentException.class,
  120 + () -> MyBatisUtils.findInSet(DbType.MYSQL, "websites;drop table system_tenant", 0));
  121 + assertThrows(IllegalArgumentException.class,
  122 + () -> MyBatisUtils.findInSet(DbType.MYSQL, "FIND_IN_SET(value, websites)", 0));
  123 + }
  124 +
  125 + @Test
  126 + public void testFindInSet_invalidParamIndex() {
  127 + assertThrows(IllegalArgumentException.class,
  128 + () -> MyBatisUtils.findInSet(DbType.MYSQL, "websites", -1));
  129 + }
  130 +
  131 + @Test
  132 + public void testFindInSet_applyBindsValue() {
  133 + // 准备参数
  134 + QueryWrapper<Object> query = new QueryWrapper<>();
  135 + String value = "test' OR 1 = 1";
  136 +
  137 + // 调用
  138 + query.apply(MyBatisUtils.findInSet(DbType.MYSQL, "to_mails", 0), value);
  139 +
  140 + // 断言:SQL 片段里只有 MyBatis Plus 参数占位,用户输入不会被直接拼接进去
  141 + assertEquals("(FIND_IN_SET(#{ew.paramNameValuePairs.MPGENVAL1}, to_mails) <> 0)",
  142 + query.getSqlSegment());
  143 + assertFalse(query.getSqlSegment().contains(value));
  144 + assertEquals(value, query.getParamNameValuePairs().get("MPGENVAL1"));
  145 + }
  146 +
  147 + @Test
  148 + public void testFindInSet_applyBindsMultipleValues() {
  149 + // 准备参数
  150 + QueryWrapper<Object> query = new QueryWrapper<>();
  151 + String value1 = "1' OR 1 = 1";
  152 + String value2 = "2' OR 1 = 1";
  153 +
  154 + // 调用
  155 + query.apply(MyBatisUtils.findInSet(DbType.MYSQL, "tag_ids", 0)
  156 + + " OR " + MyBatisUtils.findInSet(DbType.MYSQL, "tag_ids", 1), value1, value2);
  157 +
  158 + // 断言:多个参数都由 MyBatis Plus 生成占位符,不拼接用户输入
  159 + assertEquals("(FIND_IN_SET(#{ew.paramNameValuePairs.MPGENVAL1}, tag_ids) <> 0"
  160 + + " OR FIND_IN_SET(#{ew.paramNameValuePairs.MPGENVAL2}, tag_ids) <> 0)",
  161 + query.getSqlSegment());
  162 + assertFalse(query.getSqlSegment().contains(value1));
  163 + assertFalse(query.getSqlSegment().contains(value2));
  164 + assertEquals(value1, query.getParamNameValuePairs().get("MPGENVAL1"));
  165 + assertEquals(value2, query.getParamNameValuePairs().get("MPGENVAL2"));
  166 + }
  167 +
101 168 private void assertOrderItem(OrderItem orderItem, String column, boolean asc) {
102 169 assertEquals(column, orderItem.getColumn());
103 170 assertEquals(asc, orderItem.isAsc());
... ...
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerLimitConfigMapper.java
... ... @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.crm.dal.mysql.customer;
3 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 4 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
5 5 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  6 +import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
6 7 import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.limitconfig.CrmCustomerLimitConfigPageReqVO;
7 8 import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerLimitConfigDO;
8 9 import org.apache.ibatis.annotations.Mapper;
... ... @@ -28,9 +29,9 @@ public interface CrmCustomerLimitConfigMapper extends BaseMapperX&lt;CrmCustomerLim
28 29 LambdaQueryWrapperX<CrmCustomerLimitConfigDO> query = new LambdaQueryWrapperX<CrmCustomerLimitConfigDO>()
29 30 .eq(CrmCustomerLimitConfigDO::getType, type);
30 31 query.and(w -> {
31   - w.apply("FIND_IN_SET({0}, user_ids) > 0", userId);
  32 + w.apply(MyBatisUtils.findInSet("user_ids"), userId);
32 33 if (deptId != null) {
33   - w.or().apply("FIND_IN_SET({0}, dept_ids) > 0", deptId);
  34 + w.or().apply(MyBatisUtils.findInSet("dept_ids"), deptId);
34 35 }
35 36 });
36 37 return selectList(query);
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/message/ImChannelMessageMapper.java
... ... @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.im.dal.mysql.message;
3 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 4 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
5 5 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  6 +import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
6 7 import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.channel.ImChannelMessagePageReqVO;
7 8 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImChannelMessageDO;
8 9 import org.apache.ibatis.annotations.Mapper;
... ... @@ -32,7 +33,7 @@ public interface ImChannelMessageMapper extends BaseMapperX&lt;ImChannelMessageDO&gt;
32 33 .gt(ImChannelMessageDO::getId, minId)
33 34 .and(w -> w.isNull(ImChannelMessageDO::getReceiverUserIds)
34 35 .or().eq(ImChannelMessageDO::getReceiverUserIds, "")
35   - .or().apply("FIND_IN_SET({0}, receiver_user_ids)", userId))
  36 + .or().apply(MyBatisUtils.findInSet("receiver_user_ids"), userId))
36 37 .orderByAsc(ImChannelMessageDO::getId)
37 38 .last("LIMIT " + size));
38 39 }
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/alert/IotAlertConfigMapper.java
... ... @@ -33,7 +33,7 @@ public interface IotAlertConfigMapper extends BaseMapperX&lt;IotAlertConfigDO&gt; {
33 33 default List<IotAlertConfigDO> selectListBySceneRuleIdAndStatus(Long sceneRuleId, Integer status) {
34 34 return selectList(new LambdaQueryWrapperX<IotAlertConfigDO>()
35 35 .eq(IotAlertConfigDO::getStatus, status)
36   - .apply(MyBatisUtils.findInSet("scene_rule_ids", sceneRuleId)));
  36 + .apply(MyBatisUtils.findInSet("scene_rule_ids"), sceneRuleId));
37 37 }
38 38  
39   -}
40 39 \ No newline at end of file
  40 +}
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceMapper.java
... ... @@ -4,6 +4,7 @@ import cn.hutool.core.util.ObjectUtil;
4 4 import cn.iocoder.yudao.framework.common.pojo.PageResult;
5 5 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
6 6 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  7 +import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
7 8 import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.IotDevicePageReqVO;
8 9 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
9 10 import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum;
... ... @@ -34,7 +35,7 @@ public interface IotDeviceMapper extends BaseMapperX&lt;IotDeviceDO&gt; {
34 35 .likeIfPresent(IotDeviceDO::getNickname, reqVO.getNickname())
35 36 .eqIfPresent(IotDeviceDO::getState, reqVO.getStatus())
36 37 .eqIfPresent(IotDeviceDO::getGatewayId, reqVO.getGatewayId())
37   - .apply(ObjectUtil.isNotNull(reqVO.getGroupId()), "FIND_IN_SET(" + reqVO.getGroupId() + ",group_ids) > 0")
  38 + .apply(ObjectUtil.isNotNull(reqVO.getGroupId()), MyBatisUtils.findInSet("group_ids"), reqVO.getGroupId())
38 39 .orderByDesc(IotDeviceDO::getId));
39 40 }
40 41  
... ... @@ -72,7 +73,7 @@ public interface IotDeviceMapper extends BaseMapperX&lt;IotDeviceDO&gt; {
72 73  
73 74 default Long selectCountByGroupId(Long groupId) {
74 75 return selectCount(new LambdaQueryWrapperX<IotDeviceDO>()
75   - .apply("FIND_IN_SET(" + groupId + ",group_ids) > 0"));
  76 + .apply(MyBatisUtils.findInSet("group_ids"), groupId));
76 77 }
77 78  
78 79 default Long selectCountByCreateTime(@Nullable LocalDateTime createTime) {
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/rule/IotDataRuleMapper.java
... ... @@ -28,7 +28,7 @@ public interface IotDataRuleMapper extends BaseMapperX&lt;IotDataRuleDO&gt; {
28 28  
29 29 default List<IotDataRuleDO> selectListBySinkId(Long sinkId) {
30 30 return selectList(new LambdaQueryWrapperX<IotDataRuleDO>()
31   - .apply(MyBatisUtils.findInSet("sink_ids", sinkId)));
  31 + .apply(MyBatisUtils.findInSet("sink_ids"), sinkId));
32 32 }
33 33  
34 34 default List<IotDataRuleDO> selectListByStatus(Integer status) {
... ... @@ -39,4 +39,4 @@ public interface IotDataRuleMapper extends BaseMapperX&lt;IotDataRuleDO&gt; {
39 39 return selectOne(IotDataRuleDO::getName, name);
40 40 }
41 41  
42   -}
43 42 \ No newline at end of file
  43 +}
... ...
yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponTemplateMapper.java
... ... @@ -5,6 +5,7 @@ import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
5 5 import cn.iocoder.yudao.framework.common.pojo.PageResult;
6 6 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
7 7 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  8 +import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
8 9 import cn.iocoder.yudao.module.promotion.controller.admin.coupon.vo.template.CouponTemplatePageReqVO;
9 10 import cn.iocoder.yudao.module.promotion.dal.dataobject.coupon.CouponTemplateDO;
10 11 import cn.iocoder.yudao.module.promotion.enums.coupon.CouponTemplateValidityTypeEnum;
... ... @@ -34,7 +35,7 @@ public interface CouponTemplateMapper extends BaseMapperX&lt;CouponTemplateDO&gt; {
34 35 .eqIfPresent(CouponTemplateDO::getDiscountType, reqVO.getDiscountType())
35 36 .betweenIfPresent(CouponTemplateDO::getCreateTime, reqVO.getCreateTime())
36 37 .eqIfPresent(CouponTemplateDO::getProductScope, reqVO.getProductScope())
37   - .and(reqVO.getProductScopeValue() != null, w -> w.apply("FIND_IN_SET({0}, product_scope_values)",
  38 + .and(reqVO.getProductScopeValue() != null, w -> w.apply(MyBatisUtils.findInSet("product_scope_values"),
38 39 reqVO.getProductScopeValue()))
39 40 .and(canTakeConsumer != null, canTakeConsumer)
40 41 .orderByDesc(CouponTemplateDO::getId));
... ... @@ -61,7 +62,7 @@ public interface CouponTemplateMapper extends BaseMapperX&lt;CouponTemplateDO&gt; {
61 62 Consumer<LambdaQueryWrapper<CouponTemplateDO>> canTakeConsumer = buildCanTakeQueryConsumer(canTakeTypes);
62 63 return selectList(new LambdaQueryWrapperX<CouponTemplateDO>()
63 64 .eqIfPresent(CouponTemplateDO::getProductScope, productScope)
64   - .and(productScopeValue != null, w -> w.apply("FIND_IN_SET({0}, product_scope_values)",
  65 + .and(productScopeValue != null, w -> w.apply(MyBatisUtils.findInSet("product_scope_values"),
65 66 productScopeValue))
66 67 .and(canTakeConsumer != null, canTakeConsumer)
67 68 .last(" LIMIT " + count)
... ...
yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/reward/RewardActivityMapper.java
1 1 package cn.iocoder.yudao.module.promotion.dal.mysql.reward;
2 2  
3   -import cn.hutool.core.util.StrUtil;
4 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
5 4 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
6 5 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  6 +import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
7 7 import cn.iocoder.yudao.module.promotion.controller.admin.reward.vo.RewardActivityPageReqVO;
8 8 import cn.iocoder.yudao.module.promotion.dal.dataobject.reward.RewardActivityDO;
9 9 import cn.iocoder.yudao.module.promotion.enums.common.PromotionProductScopeEnum;
... ... @@ -14,6 +14,7 @@ import java.util.Collection;
14 14 import java.util.List;
15 15 import java.util.function.Function;
16 16 import java.util.stream.Collectors;
  17 +import java.util.stream.IntStream;
17 18  
18 19 /**
19 20 * 满减送活动 Mapper
... ... @@ -34,18 +35,18 @@ public interface RewardActivityMapper extends BaseMapperX&lt;RewardActivityDO&gt; {
34 35 Collection<Long> categoryIds,
35 36 Integer status) {
36 37 LocalDateTime now = LocalDateTime.now();
37   - Function<Collection<Long>, String> productScopeValuesFindInSetFunc = ids -> ids.stream()
38   - .map(id -> StrUtil.format("FIND_IN_SET({}, product_scope_values) ", id))
  38 + Function<Collection<Long>, String> productScopeValuesFindInSetFunc = ids -> IntStream.range(0, ids.size())
  39 + .mapToObj(index -> MyBatisUtils.findInSetWithParamIndex("product_scope_values", index))
39 40 .collect(Collectors.joining(" OR "));
40 41 return selectList(new LambdaQueryWrapperX<RewardActivityDO>()
41 42 .eq(RewardActivityDO::getStatus, status)
42 43 .lt(RewardActivityDO::getStartTime, now)
43 44 .gt(RewardActivityDO::getEndTime, now)
44 45 .and(i -> i.eq(RewardActivityDO::getProductScope, PromotionProductScopeEnum.SPU.getScope())
45   - .and(i1 -> i1.apply(productScopeValuesFindInSetFunc.apply(spuIds)))
  46 + .and(i1 -> i1.apply(productScopeValuesFindInSetFunc.apply(spuIds), spuIds.toArray()))
46 47 .or(i1 -> i1.eq(RewardActivityDO::getProductScope, PromotionProductScopeEnum.ALL.getScope()))
47 48 .or(i1 -> i1.eq(RewardActivityDO::getProductScope, PromotionProductScopeEnum.CATEGORY.getScope())
48   - .and(i2 -> i2.apply(productScopeValuesFindInSetFunc.apply(categoryIds)))))
  49 + .and(i2 -> i2.apply(productScopeValuesFindInSetFunc.apply(categoryIds), categoryIds.toArray()))))
49 50 .orderByDesc(RewardActivityDO::getId)
50 51 );
51 52 }
... ...
yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/seckill/seckillactivity/SeckillActivityMapper.java
... ... @@ -5,6 +5,7 @@ import cn.hutool.core.util.ObjectUtil;
5 5 import cn.iocoder.yudao.framework.common.pojo.PageResult;
6 6 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
7 7 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  8 +import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
8 9 import cn.iocoder.yudao.module.promotion.controller.admin.seckill.vo.activity.SeckillActivityPageReqVO;
9 10 import cn.iocoder.yudao.module.promotion.controller.app.seckill.vo.activity.AppSeckillActivityPageReqVO;
10 11 import cn.iocoder.yudao.module.promotion.dal.dataobject.seckill.SeckillActivityDO;
... ... @@ -27,7 +28,7 @@ public interface SeckillActivityMapper extends BaseMapperX&lt;SeckillActivityDO&gt; {
27 28 .likeIfPresent(SeckillActivityDO::getName, reqVO.getName())
28 29 .eqIfPresent(SeckillActivityDO::getStatus, reqVO.getStatus())
29 30 .betweenIfPresent(SeckillActivityDO::getCreateTime, reqVO.getCreateTime())
30   - .apply(ObjectUtil.isNotNull(reqVO.getConfigId()), "FIND_IN_SET(" + reqVO.getConfigId() + ", config_ids) > 0")
  31 + .apply(ObjectUtil.isNotNull(reqVO.getConfigId()), MyBatisUtils.findInSet("config_ids"), reqVO.getConfigId())
31 32 .orderByDesc(SeckillActivityDO::getId));
32 33 }
33 34  
... ... @@ -70,7 +71,7 @@ public interface SeckillActivityMapper extends BaseMapperX&lt;SeckillActivityDO&gt; {
70 71 .eqIfPresent(SeckillActivityDO::getStatus, status)
71 72 .lt(SeckillActivityDO::getStartTime, dateTime)
72 73 .gt(SeckillActivityDO::getEndTime, dateTime)// 开始时间 < 指定时间 < 结束时间,也就是说获取指定时间段的活动
73   - .apply(ObjectUtil.isNotNull(pageReqVO.getConfigId()), "FIND_IN_SET(" + pageReqVO.getConfigId() + ",config_ids) > 0"));
  74 + .apply(ObjectUtil.isNotNull(pageReqVO.getConfigId()), MyBatisUtils.findInSet("config_ids"), pageReqVO.getConfigId()));
74 75 }
75 76  
76 77 default SeckillActivityDO selectBySpuIdAndStatusAndNow(Long spuId, Integer status) {
... ...
yudao-module-member/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/user/MemberUserMapper.java
... ... @@ -2,10 +2,10 @@ package cn.iocoder.yudao.module.member.dal.mysql.user;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
4 4 import cn.hutool.core.lang.Assert;
5   -import cn.hutool.core.util.StrUtil;
6 5 import cn.iocoder.yudao.framework.common.pojo.PageResult;
7 6 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
8 7 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  8 +import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
9 9 import cn.iocoder.yudao.module.member.controller.admin.user.vo.MemberUserPageReqVO;
10 10 import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO;
11 11 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
... ... @@ -13,6 +13,7 @@ import org.apache.ibatis.annotations.Mapper;
13 13  
14 14 import java.util.List;
15 15 import java.util.stream.Collectors;
  16 +import java.util.stream.IntStream;
16 17  
17 18 /**
18 19 * 会员 User Mapper
... ... @@ -38,10 +39,12 @@ public interface MemberUserMapper extends BaseMapperX&lt;MemberUserDO&gt; {
38 39 default PageResult<MemberUserDO> selectPage(MemberUserPageReqVO reqVO) {
39 40 // 处理 tagIds 过滤条件
40 41 String tagIdSql = "";
  42 + Object[] tagIdParams = new Object[0];
41 43 if (CollUtil.isNotEmpty(reqVO.getTagIds())) {
42   - tagIdSql = reqVO.getTagIds().stream()
43   - .map(tagId -> "FIND_IN_SET(" + tagId + ", tag_ids)")
  44 + tagIdSql = IntStream.range(0, reqVO.getTagIds().size())
  45 + .mapToObj(index -> MyBatisUtils.findInSetWithParamIndex("tag_ids", index))
44 46 .collect(Collectors.joining(" OR "));
  47 + tagIdParams = reqVO.getTagIds().toArray();
45 48 }
46 49 // 分页查询
47 50 return selectPage(reqVO, new LambdaQueryWrapperX<MemberUserDO>()
... ... @@ -52,7 +55,7 @@ public interface MemberUserMapper extends BaseMapperX&lt;MemberUserDO&gt; {
52 55 .betweenIfPresent(MemberUserDO::getCreateTime, reqVO.getCreateTime())
53 56 .eqIfPresent(MemberUserDO::getLevelId, reqVO.getLevelId())
54 57 .eqIfPresent(MemberUserDO::getGroupId, reqVO.getGroupId())
55   - .apply(StrUtil.isNotEmpty(tagIdSql), tagIdSql)
  58 + .apply(CollUtil.isNotEmpty(reqVO.getTagIds()), tagIdSql, tagIdParams)
56 59 .orderByDesc(MemberUserDO::getId));
57 60 }
58 61  
... ... @@ -66,7 +69,7 @@ public interface MemberUserMapper extends BaseMapperX&lt;MemberUserDO&gt; {
66 69  
67 70 default Long selectCountByTagId(Long tagId) {
68 71 return selectCount(new LambdaQueryWrapperX<MemberUserDO>()
69   - .apply("FIND_IN_SET({0}, tag_ids)", tagId));
  72 + .apply(MyBatisUtils.findInSet("tag_ids"), tagId));
70 73 }
71 74  
72 75 /**
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/template/MesQcTemplateMapper.java
... ... @@ -24,7 +24,7 @@ public interface MesQcTemplateMapper extends BaseMapperX&lt;MesQcTemplateDO&gt; {
24 24 .likeIfPresent(MesQcTemplateDO::getName, reqVO.getName())
25 25 .orderByDesc(MesQcTemplateDO::getId);
26 26 if (reqVO.getType() != null) {
27   - query.apply(MyBatisUtils.findInSet("types", reqVO.getType()));
  27 + query.apply(MyBatisUtils.findInSet("types"), reqVO.getType());
28 28 }
29 29 query.eqIfPresent(MesQcTemplateDO::getStatus, reqVO.getStatus());
30 30 return selectPage(reqVO, query);
... ...
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/mail/MailLogMapper.java
... ... @@ -21,7 +21,7 @@ public interface MailLogMapper extends BaseMapperX&lt;MailLogDO&gt; {
21 21 .eqIfPresent(MailLogDO::getSendStatus, reqVO.getSendStatus())
22 22 .betweenIfPresent(MailLogDO::getSendTime, reqVO.getSendTime())
23 23 .apply(StrUtil.isNotBlank(reqVO.getToMail()),
24   - MyBatisUtils.findInSet("to_mails", reqVO.getToMail()))
  24 + MyBatisUtils.findInSet("to_mails"), reqVO.getToMail())
25 25 .orderByDesc(MailLogDO::getId));
26 26 }
27 27  
... ...
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/tenant/TenantMapper.java
... ... @@ -29,7 +29,7 @@ public interface TenantMapper extends BaseMapperX&lt;TenantDO&gt; {
29 29  
30 30 default List<TenantDO> selectListByWebsite(String website) {
31 31 return selectList(new LambdaQueryWrapperX<TenantDO>()
32   - .apply(MyBatisUtils.findInSet("websites", website)));
  32 + .apply(MyBatisUtils.findInSet("websites"), website));
33 33 }
34 34  
35 35 default Long selectCountByPackageId(Long packageId) {
... ...
yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/mail/MailLogServiceImplTest.java
... ... @@ -160,8 +160,8 @@ public class MailLogServiceImplTest extends BaseDbUnitTest {
160 160 mailLogMapper.insert(cloneIgnoreId(dbMailLog, o -> o.setUserId(2L)));
161 161 // 测试 userType 不匹配
162 162 mailLogMapper.insert(cloneIgnoreId(dbMailLog, o -> o.setUserType(UserTypeEnum.MEMBER.getValue())));
163   - // 测试 toMails 不匹配(特殊:find_in_set 无法单测)
164   -// mailLogMapper.insert(cloneIgnoreId(dbMailLog, o -> o.setToMails(Lists.newArrayList("788@qq.com"))));
  163 + // 测试 toMails 不匹配
  164 + mailLogMapper.insert(cloneIgnoreId(dbMailLog, o -> o.setToMails(Lists.newArrayList("788@qq.com"))));
165 165 // 测试 accountId 不匹配
166 166 mailLogMapper.insert(cloneIgnoreId(dbMailLog, o -> o.setAccountId(11L)));
167 167 // 测试 templateId 不匹配
... ... @@ -174,7 +174,7 @@ public class MailLogServiceImplTest extends BaseDbUnitTest {
174 174 MailLogPageReqVO reqVO = new MailLogPageReqVO();
175 175 reqVO.setUserId(1L);
176 176 reqVO.setUserType(UserTypeEnum.ADMIN.getValue());
177   -// reqVO.setToMail("768@qq.com");
  177 + reqVO.setToMail("768@qq.com");
178 178 reqVO.setAccountId(10L);
179 179 reqVO.setTemplateId(100L);
180 180 reqVO.setSendStatus(MailSendStatusEnum.INIT.getStatus());
... ...
yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/tenant/TenantServiceImplTest.java
... ... @@ -22,7 +22,6 @@ import cn.iocoder.yudao.module.system.service.tenant.handler.TenantMenuHandler;
22 22 import cn.iocoder.yudao.module.system.service.user.AdminUserService;
23 23 import jakarta.annotation.Resource;
24 24 import org.junit.jupiter.api.BeforeEach;
25   -import org.junit.jupiter.api.Disabled;
26 25 import org.junit.jupiter.api.Test;
27 26 import org.springframework.context.annotation.Import;
28 27 import org.springframework.test.context.bean.override.mockito.MockitoBean;
... ... @@ -334,7 +333,6 @@ public class TenantServiceImplTest extends BaseDbUnitTest {
334 333 }
335 334  
336 335 @Test
337   - @Disabled // H2 不支持 find_in_set 函数
338 336 public void testGetTenantByWebsite() {
339 337 // mock 数据
340 338 TenantDO dbTenant = randomPojo(TenantDO.class, o -> o.setWebsites(singletonList("https://www.iocoder.cn")));
... ...