Commit a426cc2f4b8ca83cdf87967c13d68a169822d729
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<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<ImChannelMessageDO> |
| 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<IotAlertConfigDO> { |
| 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<IotDeviceDO> { |
| 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<IotDeviceDO> { |
| 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<IotDataRuleDO> { |
| 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<IotDataRuleDO> { |
| 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<CouponTemplateDO> { |
| 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<CouponTemplateDO> { |
| 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<RewardActivityDO> { |
| 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<SeckillActivityDO> { |
| 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<SeckillActivityDO> { |
| 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<MemberUserDO> { |
| 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<MemberUserDO> { |
| 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<MemberUserDO> { |
| 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<MesQcTemplateDO> { |
| 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<MailLogDO> { |
| 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<TenantDO> { |
| 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"))); | ... | ... |