Commit 9b12e80291b69b301f7c6a03605dc50afd6eb894

Authored by YunaiV
1 parent 8e4d1e70

fix: 适配 IoT 外部数据库时间函数并整理 SQL 工具

yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/injector/SelectForUpdateMethod.java
@@ -47,14 +47,17 @@ public class SelectForUpdateMethod extends AbstractMethod { @@ -47,14 +47,17 @@ public class SelectForUpdateMethod extends AbstractMethod {
47 47
48 @Override 48 @Override
49 public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) { 49 public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
50 - // 没有主键的表,无法查询首行主键 50 + // 1. 没有主键的表,无法查询首行主键
51 if (kind == Kind.FIRST_IDS && !tableInfo.havePK()) { 51 if (kind == Kind.FIRST_IDS && !tableInfo.havePK()) {
52 return null; 52 return null;
53 } 53 }
  54 + // 2. 拼接 SQL:查询列 + 条件 + 排序与锁定;首行主键查询只查询主键列
54 String columns = kind == Kind.FIRST_IDS ? tableInfo.getKeyColumn() : sqlSelectColumns(tableInfo, true); 55 String columns = kind == Kind.FIRST_IDS ? tableInfo.getKeyColumn() : sqlSelectColumns(tableInfo, true);
55 String sql = "<script>" + sqlFirst() + " SELECT " + columns + " FROM " + tableInfo.getTableName() 56 String sql = "<script>" + sqlFirst() + " SELECT " + columns + " FROM " + tableInfo.getTableName()
56 + NEWLINE + sqlWhere(tableInfo) + NEWLINE + sqlTail(tableInfo) + sqlComment() + NEWLINE + "</script>"; 57 + NEWLINE + sqlWhere(tableInfo) + NEWLINE + sqlTail(tableInfo) + sqlComment() + NEWLINE + "</script>";
  58 + // 3. 使用 LockSqlSource 标记为锁定读的内置查询,供 ForUpdateInnerInterceptor 识别
57 SqlSource sqlSource = new LockSqlSource(kind, super.createSqlSource(configuration, sql, modelClass)); 59 SqlSource sqlSource = new LockSqlSource(kind, super.createSqlSource(configuration, sql, modelClass));
  60 + // 4. 注册 MappedStatement:首行主键查询的结果为主键类型,其它为实体(沿用表的 resultMap)
58 if (kind == Kind.FIRST_IDS) { 61 if (kind == Kind.FIRST_IDS) {
59 return addSelectMappedStatementForOther(mapperClass, methodName, sqlSource, tableInfo.getKeyType()); 62 return addSelectMappedStatementForOther(mapperClass, methodName, sqlSource, tableInfo.getKeyType());
60 } 63 }
@@ -68,16 +71,21 @@ public class SelectForUpdateMethod extends AbstractMethod { @@ -68,16 +71,21 @@ public class SelectForUpdateMethod extends AbstractMethod {
68 * @return WHERE 脚本 71 * @return WHERE 脚本
69 */ 72 */
70 private String sqlWhere(TableInfo tableInfo) { 73 private String sqlWhere(TableInfo tableInfo) {
  74 + // 实体条件:Wrapper 设置了 entity 时生效
71 String entityWhere = SqlScriptUtils.convertIf(tableInfo.getAllSqlWhere(true, true, true, WRAPPER_ENTITY_DOT), 75 String entityWhere = SqlScriptUtils.convertIf(tableInfo.getAllSqlWhere(true, true, true, WRAPPER_ENTITY_DOT),
72 WRAPPER_ENTITY + " != null", true); 76 WRAPPER_ENTITY + " != null", true);
  77 + // 普通条件:整体加括号,避免 OR 条件绕过后面的主键、逻辑删除条件
73 String normalWhere = SqlScriptUtils.convertIf(" AND (${" + WRAPPER_DOT + "expression.normal.sqlSegment})", 78 String normalWhere = SqlScriptUtils.convertIf(" AND (${" + WRAPPER_DOT + "expression.normal.sqlSegment})",
74 WRAPPER_DOT + "nonEmptyOfNormal", true); 79 WRAPPER_DOT + "nonEmptyOfNormal", true);
  80 + // 主键:仅加锁查询、且表有主键时,按传入的主键限制,用于 Oracle 的首行锁定
75 String idWhere = kind == Kind.LOCK && tableInfo.havePK() ? NEWLINE + SqlScriptUtils.convertIf( 81 String idWhere = kind == Kind.LOCK && tableInfo.havePK() ? NEWLINE + SqlScriptUtils.convertIf(
76 " AND " + tableInfo.getKeyColumn() + " = " + SqlScriptUtils.safeParam(PARAM_ID), PARAM_ID + " != null", true) : ""; 82 " AND " + tableInfo.getKeyColumn() + " = " + SqlScriptUtils.safeParam(PARAM_ID), PARAM_ID + " != null", true) : "";
  83 + // 拼接逻辑删除条件,由 <where> 去除开头多余的 AND
77 return SqlScriptUtils.convertWhere(entityWhere + NEWLINE + normalWhere + idWhere 84 return SqlScriptUtils.convertWhere(entityWhere + NEWLINE + normalWhere + idWhere
78 + NEWLINE + tableInfo.getLogicDeleteSql(true, true)); 85 + NEWLINE + tableInfo.getLogicDeleteSql(true, true));
79 } 86 }
80 87
  88 + // DONE @AI:方法内 注释;(处理:已按主键查询、加锁查询两种情况补充注释)
81 /** 89 /**
82 * 排序与锁定: 90 * 排序与锁定:
83 * 1. 加锁查询:Wrapper 的排序(未设置时不排序,避免改变数据库的加锁范围) + FOR UPDATE;首行再加 LIMIT 1 91 * 1. 加锁查询:Wrapper 的排序(未设置时不排序,避免改变数据库的加锁范围) + FOR UPDATE;首行再加 LIMIT 1
@@ -88,19 +96,23 @@ public class SelectForUpdateMethod extends AbstractMethod { @@ -88,19 +96,23 @@ public class SelectForUpdateMethod extends AbstractMethod {
88 */ 96 */
89 private String sqlTail(TableInfo tableInfo) { 97 private String sqlTail(TableInfo tableInfo) {
90 String orderBy = " ${" + WRAPPER_DOT + "expression.orderBy.sqlSegment}"; 98 String orderBy = " ${" + WRAPPER_DOT + "expression.orderBy.sqlSegment}";
  99 + // 情况一:首行主键查询,不加锁;未设置排序时按主键排序
91 if (kind == Kind.FIRST_IDS) { 100 if (kind == Kind.FIRST_IDS) {
92 return SqlScriptUtils.convertChoose(WRAPPER_DOT + "expression.orderBy.isEmpty()", 101 return SqlScriptUtils.convertChoose(WRAPPER_DOT + "expression.orderBy.isEmpty()",
93 " ORDER BY " + tableInfo.getKeyColumn(), orderBy); 102 " ORDER BY " + tableInfo.getKeyColumn(), orderBy);
94 } 103 }
  104 + // 情况二:加锁查询,只使用 Wrapper 的排序,末尾为 FOR UPDATE;首行为 LIMIT 1 FOR UPDATE
95 return SqlScriptUtils.convertIf(orderBy, "!" + WRAPPER_DOT + "expression.orderBy.isEmpty()", true) 105 return SqlScriptUtils.convertIf(orderBy, "!" + WRAPPER_DOT + "expression.orderBy.isEmpty()", true)
96 + NEWLINE + (kind == Kind.LOCK_FIRST ? "LIMIT 1 FOR UPDATE" : "FOR UPDATE"); 106 + NEWLINE + (kind == Kind.LOCK_FIRST ? "LIMIT 1 FOR UPDATE" : "FOR UPDATE");
97 } 107 }
98 108
  109 + // DONE @AI:方法内 注释;(处理:已补充同名 MappedStatement 跳过注入的注释,保留 flushCache、useCache 的说明)
99 @Override 110 @Override
100 protected MappedStatement addMappedStatement(Class<?> mapperClass, String id, SqlSource sqlSource, 111 protected MappedStatement addMappedStatement(Class<?> mapperClass, String id, SqlSource sqlSource,
101 SqlCommandType sqlCommandType, Class<?> parameterType, 112 SqlCommandType sqlCommandType, Class<?> parameterType,
102 String resultMap, Class<?> resultType, KeyGenerator keyGenerator, 113 String resultMap, Class<?> resultType, KeyGenerator keyGenerator,
103 String keyProperty, String keyColumn) { 114 String keyProperty, String keyColumn) {
  115 + // 已通过 XML、注解等定义同名的 MappedStatement 时,不重复注入
104 String statementName = mapperClass.getName() + DOT + id; 116 String statementName = mapperClass.getName() + DOT + id;
105 if (configuration.hasStatement(statementName, false)) { 117 if (configuration.hasStatement(statementName, false)) {
106 logger.warn(LEFT_SQ_BRACKET + statementName + "] Has been loaded by XML or SqlProvider or Mybatis's Annotation, so ignoring this injection for [" + getClass() + RIGHT_SQ_BRACKET); 118 logger.warn(LEFT_SQ_BRACKET + statementName + "] Has been loaded by XML or SqlProvider or Mybatis's Annotation, so ignoring this injection for [" + getClass() + RIGHT_SQ_BRACKET);
@@ -145,6 +157,7 @@ public class SelectForUpdateMethod extends AbstractMethod { @@ -145,6 +157,7 @@ public class SelectForUpdateMethod extends AbstractMethod {
145 * 157 *
146 * MyBatis Plus Join 等插件复制 MappedStatement 时保留同一个 SqlSource,标记仍然有效 158 * MyBatis Plus Join 等插件复制 MappedStatement 时保留同一个 SqlSource,标记仍然有效
147 */ 159 */
  160 + @SuppressWarnings("ClassCanBeRecord")
148 @Getter 161 @Getter
149 @AllArgsConstructor 162 @AllArgsConstructor
150 public static class LockSqlSource implements SqlSource { 163 public static class LockSqlSource implements SqlSource {
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java
1 package cn.iocoder.yudao.framework.mybatis.core.mapper; 1 package cn.iocoder.yudao.framework.mybatis.core.mapper;
2 2
3 import cn.hutool.core.collection.CollUtil; 3 import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.util.ObjectUtil;
4 import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; 5 import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
5 import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil; 6 import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
6 import cn.iocoder.yudao.framework.common.pojo.PageParam; 7 import cn.iocoder.yudao.framework.common.pojo.PageParam;
@@ -151,10 +152,6 @@ public interface BaseMapperX&lt;T&gt; extends MPJBaseMapper&lt;T&gt; { @@ -151,10 +152,6 @@ public interface BaseMapperX&lt;T&gt; extends MPJBaseMapper&lt;T&gt; {
151 return selectOne(new LambdaQueryWrapper<T>().eq(field1, value1).eq(field2, value2).eq(field3, value3)); 152 return selectOne(new LambdaQueryWrapper<T>().eq(field1, value1).eq(field2, value2).eq(field3, value3));
152 } 153 }
153 154
154 - // TODO DONE @AI:能不能把现有的 selectOneForUpdate、selectFirstOneForUpdate、selectListForFirstLock、 这些,做一个整合。目前感觉有点散。。。不然有点乱;包括 cms 项目里,其实也有一些 lock;/Users/yunai/Java/yudao-cms-all/ruoyi-vue-pro-jdk25/yudao-module-cms;最好有个比较万金油的方案。。。。  
155 - // 已整合为 selectOneForUpdate、selectListForUpdate、selectFirstOneForUpdate 三个锁定读入口(含字段重载),boot、cms 的手写 FOR UPDATE 均改为调用它们;  
156 - // 锁定子句由内置查询 SelectForUpdateMethod 生成(不再暴露 selectListForFirstLock 等接口方法),SQL Server 由 ForUpdateInnerInterceptor 改写,Oracle 的首行锁定在本类按数据库类型走多次查询。  
157 -  
158 // ========== 锁定读:需要在事务中调用;Wrapper 不能使用 last、groupBy、having、参数别名 ========== 155 // ========== 锁定读:需要在事务中调用;Wrapper 不能使用 last、groupBy、having、参数别名 ==========
159 156
160 /** 157 /**
@@ -227,31 +224,40 @@ public interface BaseMapperX&lt;T&gt; extends MPJBaseMapper&lt;T&gt; { @@ -227,31 +224,40 @@ public interface BaseMapperX&lt;T&gt; extends MPJBaseMapper&lt;T&gt; {
227 * @param queryWrapper 查询条件 224 * @param queryWrapper 查询条件
228 * @return 实体 225 * @return 实体
229 */ 226 */
  227 + // DONE @AI:方法内注释噢;(处理:已按“情况一 非 Oracle 单条锁定、情况二 Oracle 2.1~2.3 候选、加锁、重读”补充步骤注释)
230 default T selectFirstOneForUpdate(LambdaQueryWrapper<T> queryWrapper) { 228 default T selectFirstOneForUpdate(LambdaQueryWrapper<T> queryWrapper) {
  229 + // 情况一:非 Oracle,单条 LIMIT 1 FOR UPDATE 锁定首行(SQL Server 由 ForUpdateInnerInterceptor 改写)
231 DbType dbType = JdbcUtils.getDbType(); 230 DbType dbType = JdbcUtils.getDbType();
232 - if (!ObjectUtils.equalsAny(dbType, DbType.ORACLE, DbType.ORACLE_12C)) { 231 + // DONE @AI:ObjectUtils notequals any;(处理:已改为 ObjectUtils.notEqualsAny,dbType 为 null 时与原取反写法一致,均走单条锁定)
  232 + if (ObjectUtils.notEqualsAny(dbType, DbType.ORACLE, DbType.ORACLE_12C)) {
233 return CollUtil.getFirst(selectListByForUpdateMethod(SelectForUpdateMethod.Kind.LOCK_FIRST, queryWrapper, null)); 233 return CollUtil.getFirst(selectListByForUpdateMethod(SelectForUpdateMethod.Kind.LOCK_FIRST, queryWrapper, null));
234 } 234 }
  235 +
  236 + // 情况二:Oracle,按“查询候选主键、按主键加锁、重新读取首行主键”重试,最多 5 次
235 int maxAttempts = 5; 237 int maxAttempts = 5;
236 for (int i = 0; i < maxAttempts; i++) { 238 for (int i = 0; i < maxAttempts; i++) {
237 - List<Object> ids = selectListByForUpdateMethod(SelectForUpdateMethod.Kind.FIRST_IDS, queryWrapper, null);  
238 - if (ids.isEmpty()) { 239 + // DONE @AI:getFirst 是不是可以简化下;看看其它地方,是不是也有地方;(处理:候选、重读主键与返回的锁定行均改为 CollUtil.getFirst;是否锁定到候选仍按列表是否为空判断)
  240 + // 2.1 查询首行主键作为候选,不加锁;主键不为 null,取不到时即没有满足条件的记录
  241 + Object id = CollUtil.getFirst(selectListByForUpdateMethod(SelectForUpdateMethod.Kind.FIRST_IDS, queryWrapper, null));
  242 + if (id == null) {
239 return null; 243 return null;
240 } 244 }
241 - Object id = ids.get(0);  
242 - // 是否锁定到候选,以列表是否为空判断:查询列均为 NULL 时,MyBatis 返回的元素为 null 245 + // 2.2 按原条件、且主键等于候选加锁;列表为空说明候选已不满足条件,重试
  246 + // 注意:以列表是否为空判断是否锁定到候选,不能以元素判断:查询列均为 NULL 时,MyBatis 返回的元素为 null
243 List<T> lockRows = selectListByForUpdateMethod(SelectForUpdateMethod.Kind.LOCK, queryWrapper, id); 247 List<T> lockRows = selectListByForUpdateMethod(SelectForUpdateMethod.Kind.LOCK, queryWrapper, id);
244 - if (lockRows.isEmpty()) { 248 + if (CollUtil.isEmpty(lockRows)) {
245 continue; 249 continue;
246 } 250 }
247 - List<Object> latestIds = selectListByForUpdateMethod(SelectForUpdateMethod.Kind.FIRST_IDS, queryWrapper, null);  
248 - if (!latestIds.isEmpty() && Objects.equals(id, latestIds.get(0))) {  
249 - return lockRows.get(0); 251 + // 2.3 锁定后重新读取首行主键,与候选相同才返回锁定的记录(查询列均为 NULL 时为 null);不同时重试
  252 + Object latestId = CollUtil.getFirst(selectListByForUpdateMethod(SelectForUpdateMethod.Kind.FIRST_IDS, queryWrapper, null));
  253 + if (Objects.equals(id, latestId)) {
  254 + return CollUtil.getFirst(lockRows);
250 } 255 }
251 } 256 }
252 throw ServiceExceptionUtil.exception(GlobalErrorCodeConstants.LOCKED); 257 throw ServiceExceptionUtil.exception(GlobalErrorCodeConstants.LOCKED);
253 } 258 }
254 259
  260 + // DONE @AI:方法内注释噢;(处理:已补充校验、构建参数、执行内置查询三步注释)
255 /** 261 /**
256 * 执行锁定读的内置查询,见 {@link SelectForUpdateMethod} 262 * 执行锁定读的内置查询,见 {@link SelectForUpdateMethod}
257 * 263 *
@@ -262,18 +268,23 @@ public interface BaseMapperX&lt;T&gt; extends MPJBaseMapper&lt;T&gt; { @@ -262,18 +268,23 @@ public interface BaseMapperX&lt;T&gt; extends MPJBaseMapper&lt;T&gt; {
262 */ 268 */
263 private <E> List<E> selectListByForUpdateMethod(SelectForUpdateMethod.Kind kind, LambdaQueryWrapper<T> queryWrapper, 269 private <E> List<E> selectListByForUpdateMethod(SelectForUpdateMethod.Kind kind, LambdaQueryWrapper<T> queryWrapper,
264 Object id) { 270 Object id) {
  271 + // 1. 校验查询条件
265 checkForUpdateWrapper(queryWrapper); 272 checkForUpdateWrapper(queryWrapper);
  273 + // 2. 构建参数:查询条件、主键;首行主键查询由分页插件限制为 1 条,不查询总数
266 MapperMethod.ParamMap<Object> param = new MapperMethod.ParamMap<>(); 274 MapperMethod.ParamMap<Object> param = new MapperMethod.ParamMap<>();
267 param.put(Constants.WRAPPER, queryWrapper); 275 param.put(Constants.WRAPPER, queryWrapper);
268 param.put(SelectForUpdateMethod.PARAM_ID, id); 276 param.put(SelectForUpdateMethod.PARAM_ID, id);
269 if (kind == SelectForUpdateMethod.Kind.FIRST_IDS) { 277 if (kind == SelectForUpdateMethod.Kind.FIRST_IDS) {
270 param.put("page", MyBatisUtils.buildPage(0, 1, false)); 278 param.put("page", MyBatisUtils.buildPage(0, 1, false));
271 } 279 }
  280 + // 3. 通过 SqlSession 执行内置查询:未在 Mapper 接口声明,MappedStatement 编号为 Mapper 接口名 + 方法名
272 MapperProxyMetadata mapperProxyMetadata = MybatisUtils.getMapperProxy(this); 281 MapperProxyMetadata mapperProxyMetadata = MybatisUtils.getMapperProxy(this);
273 return mapperProxyMetadata.getSqlSession().selectList( 282 return mapperProxyMetadata.getSqlSession().selectList(
274 mapperProxyMetadata.getMapperInterface().getName() + Constants.DOT + kind.getMethod(), param); 283 mapperProxyMetadata.getMapperInterface().getName() + Constants.DOT + kind.getMethod(), param);
275 } 284 }
276 285
  286 + // DONE @AI:方法内注释噢;(处理:已补充 last、groupBy/having、参数别名的校验注释)
  287 + // DONE @AI:notequals;isNotEmpty 等等,减少取反;(处理:已改为 ObjectUtil.notEqual、CollUtil.isNotEmpty)
277 /** 288 /**
278 * 校验锁定读的查询条件:内置查询只使用实体条件、普通条件与排序 289 * 校验锁定读的查询条件:内置查询只使用实体条件、普通条件与排序
279 * 290 *
@@ -283,14 +294,17 @@ public interface BaseMapperX&lt;T&gt; extends MPJBaseMapper&lt;T&gt; { @@ -283,14 +294,17 @@ public interface BaseMapperX&lt;T&gt; extends MPJBaseMapper&lt;T&gt; {
283 if (queryWrapper == null) { 294 if (queryWrapper == null) {
284 throw new IllegalArgumentException("锁定读的查询条件不能为空"); 295 throw new IllegalArgumentException("锁定读的查询条件不能为空");
285 } 296 }
  297 + // 校验 last:设置 last 时,Wrapper 的 SQL 片段比条件表达式多出 last 部分
286 MergeSegments expression = queryWrapper.getExpression(); 298 MergeSegments expression = queryWrapper.getExpression();
287 - if (!Objects.equals(queryWrapper.getSqlSegment(), expression.getSqlSegment())) { 299 + if (ObjectUtil.notEqual(queryWrapper.getSqlSegment(), expression.getSqlSegment())) {
288 throw new IllegalArgumentException("锁定读的查询条件不能使用 last"); 300 throw new IllegalArgumentException("锁定读的查询条件不能使用 last");
289 } 301 }
290 - if (!expression.getGroupBy().isEmpty() || !expression.getHaving().isEmpty()) { 302 + // 校验 groupBy、having:内置查询不生成这两部分
  303 + if (CollUtil.isNotEmpty(expression.getGroupBy()) || CollUtil.isNotEmpty(expression.getHaving())) {
291 throw new IllegalArgumentException("锁定读的查询条件不能使用 groupBy、having"); 304 throw new IllegalArgumentException("锁定读的查询条件不能使用 groupBy、having");
292 } 305 }
293 - if (!Constants.WRAPPER.equals(queryWrapper.getParamAlias())) { 306 + // 校验参数别名:内置查询的模板固定使用 ew 作为参数名
  307 + if (ObjectUtil.notEqual(Constants.WRAPPER, queryWrapper.getParamAlias())) {
294 throw new IllegalArgumentException("锁定读的查询条件不能设置参数别名"); 308 throw new IllegalArgumentException("锁定读的查询条件不能设置参数别名");
295 } 309 }
296 } 310 }
@@ -305,7 +319,6 @@ public interface BaseMapperX&lt;T&gt; extends MPJBaseMapper&lt;T&gt; { @@ -305,7 +319,6 @@ public interface BaseMapperX&lt;T&gt; extends MPJBaseMapper&lt;T&gt; {
305 * @return 实体 319 * @return 实体
306 */ 320 */
307 default T selectFirstOne(SFunction<T, ?> field, Object value) { 321 default T selectFirstOne(SFunction<T, ?> field, Object value) {
308 - // 如果明确使用 MySQL 等场景,可以考虑使用 LIMIT 1 进行优化  
309 List<T> list = selectList(new LambdaQueryWrapper<T>().eq(field, value)); 322 List<T> list = selectList(new LambdaQueryWrapper<T>().eq(field, value));
310 return CollUtil.getFirst(list); 323 return CollUtil.getFirst(list);
311 } 324 }
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/util/SqlDialectUtils.java
@@ -15,8 +15,6 @@ import java.util.Map; @@ -15,8 +15,6 @@ import java.util.Map;
15 import java.util.Set; 15 import java.util.Set;
16 import java.util.regex.Pattern; 16 import java.util.regex.Pattern;
17 17
18 -// TODO DONE @AI:方法注释;已补齐方言重载与内部辅助方法的参数、返回值及边界。  
19 -// TODO DONE @AI:多余的方法,可以考虑去掉;当然,保留也可以。。。已核对生产 Java/XML 调用,现有方法均有用途,保留。  
20 /** 18 /**
21 * 日期统计、逗号分隔列表关联、JSON 数组匹配、行数限制、分批删除 SQL 片段的方言工具类 19 * 日期统计、逗号分隔列表关联、JSON 数组匹配、行数限制、分批删除 SQL 片段的方言工具类
22 * 20 *
@@ -165,7 +163,6 @@ public class SqlDialectUtils { @@ -165,7 +163,6 @@ public class SqlDialectUtils {
165 * @param column 列名 163 * @param column 列名
166 * @return SQL 片段 164 * @return SQL 片段
167 */ 165 */
168 - // TODO DONE @AI:用不到的,可以暂时先去掉噢; CRM 业绩统计使用 yyyyMM,商城交易统计使用 yyyy-MM,均需保留。  
169 public static String formatYearMonth(String column) { 166 public static String formatYearMonth(String column) {
170 return formatYearMonth(getDialect(), column); 167 return formatYearMonth(getDialect(), column);
171 } 168 }
@@ -196,7 +193,6 @@ public class SqlDialectUtils { @@ -196,7 +193,6 @@ public class SqlDialectUtils {
196 * @param column 列名 193 * @param column 列名
197 * @return SQL 片段 194 * @return SQL 片段
198 */ 195 */
199 - // TODO DONE @AI:用不到的,可以暂时先去掉噢; CRM 业绩统计使用 yyyyMM,商城交易统计使用 yyyy-MM,均需保留。  
200 public static String formatYearDashMonth(String column) { 196 public static String formatYearDashMonth(String column) {
201 return formatYearDashMonth(getDialect(), column); 197 return formatYearDashMonth(getDialect(), column);
202 } 198 }
@@ -248,7 +244,6 @@ public class SqlDialectUtils { @@ -248,7 +244,6 @@ public class SqlDialectUtils {
248 return formatDate(dialect, column); 244 return formatDate(dialect, column);
249 } 245 }
250 246
251 - // TODO DONE @AI:用不到的,可以先清理掉噢; AI1 首页与交易统计的 String 日期消费者仍在使用,保留。  
252 /** 247 /**
253 * 按天分组;原有片段为 CAST(column AS DATE),新方言输出 yyyy-MM-dd 字符串 248 * 按天分组;原有片段为 CAST(column AS DATE),新方言输出 yyyy-MM-dd 字符串
254 * 249 *
@@ -286,12 +281,10 @@ public class SqlDialectUtils { @@ -286,12 +281,10 @@ public class SqlDialectUtils {
286 * @param column 列名 281 * @param column 列名
287 * @return SQL 片段 282 * @return SQL 片段
288 */ 283 */
289 - // TODO DONE @AI:多余的是不是去掉? 会员统计的 LocalDate 消费者仍在使用,保留 DATE 类型转换。  
290 public static String castDate(String column) { 284 public static String castDate(String column) {
291 return castDate(getDialect(), column); 285 return castDate(getDialect(), column);
292 } 286 }
293 287
294 - // TODO DONE @AI:方法注释;已补齐方言重载与内部辅助方法的参数、返回值及边界。  
295 /** 288 /**
296 * 生成 LocalDate 消费者所需的日期值;Oracle 用 TRUNC 去掉时分秒。 289 * 生成 LocalDate 消费者所需的日期值;Oracle 用 TRUNC 去掉时分秒。
297 * 290 *
@@ -337,7 +330,6 @@ public class SqlDialectUtils { @@ -337,7 +330,6 @@ public class SqlDialectUtils {
337 * @param column 列名 330 * @param column 列名
338 * @return SQL 片段 331 * @return SQL 片段
339 */ 332 */
340 - // TODO DONE @AI:方法注释;已补齐方言重载与内部辅助方法的参数、返回值及边界。  
341 public static String year(String column) { 333 public static String year(String column) {
342 return year(getDialect(), column); 334 return year(getDialect(), column);
343 } 335 }
@@ -363,7 +355,6 @@ public class SqlDialectUtils { @@ -363,7 +355,6 @@ public class SqlDialectUtils {
363 return dayOfMonth(getDialect(), column); 355 return dayOfMonth(getDialect(), column);
364 } 356 }
365 357
366 - // TODO DONE @AI:方法注释;已补齐方言重载与内部辅助方法的参数、返回值及边界。  
367 /** 358 /**
368 * 按指定方言提取月内日,原路径保留 DAYOFMONTH。 359 * 按指定方言提取月内日,原路径保留 DAYOFMONTH。
369 * 360 *
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotDatabaseDataRuleAction.java
@@ -9,7 +9,10 @@ import cn.iocoder.yudao.module.iot.enums.rule.IotDataSinkTypeEnum; @@ -9,7 +9,10 @@ import cn.iocoder.yudao.module.iot.enums.rule.IotDataSinkTypeEnum;
9 import com.zaxxer.hikari.HikariConfig; 9 import com.zaxxer.hikari.HikariConfig;
10 import com.zaxxer.hikari.HikariDataSource; 10 import com.zaxxer.hikari.HikariDataSource;
11 import lombok.extern.slf4j.Slf4j; 11 import lombok.extern.slf4j.Slf4j;
  12 +import org.springframework.jdbc.core.ArgumentPreparedStatementSetter;
12 import org.springframework.jdbc.core.JdbcTemplate; 13 import org.springframework.jdbc.core.JdbcTemplate;
  14 +import org.springframework.jdbc.core.PreparedStatementCallback;
  15 +import org.springframework.jdbc.core.PreparedStatementCreator;
13 import org.springframework.stereotype.Component; 16 import org.springframework.stereotype.Component;
14 17
15 /** 18 /**
@@ -26,6 +29,11 @@ import org.springframework.stereotype.Component; @@ -26,6 +29,11 @@ import org.springframework.stereotype.Component;
26 public class IotDatabaseDataRuleAction extends 29 public class IotDatabaseDataRuleAction extends
27 IotDataRuleCacheableAction<IotDataSinkDatabaseConfig, JdbcTemplate> { 30 IotDataRuleCacheableAction<IotDataSinkDatabaseConfig, JdbcTemplate> {
28 31
  32 + /**
  33 + * 不支持 NOW()、改用 CURRENT_TIMESTAMP 的数据库产品名,与 JDBC 驱动的 DatabaseMetaData#getDatabaseProductName 一致
  34 + */
  35 + private static final String[] CURRENT_TIMESTAMP_PRODUCT_NAMES = {"Oracle", "Microsoft SQL Server"};
  36 +
29 @Override 37 @Override
30 public Integer getType() { 38 public Integer getType() {
31 return IotDataSinkTypeEnum.DATABASE.getType(); 39 return IotDataSinkTypeEnum.DATABASE.getType();
@@ -42,17 +50,27 @@ public class IotDatabaseDataRuleAction extends @@ -42,17 +50,27 @@ public class IotDatabaseDataRuleAction extends
42 JdbcTemplate jdbcTemplate = getProducer(config); 50 JdbcTemplate jdbcTemplate = getProducer(config);
43 51
44 // 2. 构建并执行 INSERT SQL 52 // 2. 构建并执行 INSERT SQL
45 - String sql = StrUtil.format(  
46 - "INSERT INTO {} (id, device_id, tenant_id, method, report_time, data, create_time) VALUES (?, ?, ?, ?, ?, ?, NOW())",  
47 - config.getTableName());  
48 String messageJson = JsonUtils.toJsonString(message); 53 String messageJson = JsonUtils.toJsonString(message);
49 - jdbcTemplate.update(sql, 54 + ArgumentPreparedStatementSetter parameterSetter = new ArgumentPreparedStatementSetter(new Object[]{
50 message.getId(), 55 message.getId(),
51 message.getDeviceId(), 56 message.getDeviceId(),
52 message.getTenantId(), 57 message.getTenantId(),
53 message.getMethod(), 58 message.getMethod(),
54 message.getReportTime(), 59 message.getReportTime(),
55 - messageJson); 60 + messageJson});
  61 + // 2.1 在执行 INSERT 的同一外部连接上识别数据库产品,选择 create_time 使用的数据库时间函数
  62 + PreparedStatementCreator statementCreator = connection -> connection.prepareStatement(
  63 + buildInsertSql(config.getTableName(), connection.getMetaData().getDatabaseProductName()));
  64 + // 2.2 Statement 由 JdbcTemplate 创建后再绑定参数:绑定或执行失败时,由 JdbcTemplate 关闭 Statement、释放连接
  65 + PreparedStatementCallback<Integer> statementCallback = statement -> {
  66 + try {
  67 + parameterSetter.setValues(statement);
  68 + return statement.executeUpdate();
  69 + } finally {
  70 + parameterSetter.cleanupParameters();
  71 + }
  72 + };
  73 + jdbcTemplate.execute(statementCreator, statementCallback);
56 log.info("[execute][message({}) config({}) 写入数据库成功,table: {}]", 74 log.info("[execute][message({}) config({}) 写入数据库成功,table: {}]",
57 message.getId(), config, config.getTableName()); 75 message.getId(), config, config.getTableName());
58 } catch (Exception e) { 76 } catch (Exception e) {
@@ -61,6 +79,22 @@ public class IotDatabaseDataRuleAction extends @@ -61,6 +79,22 @@ public class IotDatabaseDataRuleAction extends
61 } 79 }
62 } 80 }
63 81
  82 + /**
  83 + * 构建 INSERT SQL
  84 + *
  85 + * create_time 使用外部数据库的时间:Oracle、SQL Server 不支持 NOW(),使用 CURRENT_TIMESTAMP;其它数据库保持 NOW()
  86 + *
  87 + * @param tableName 已校验的表名
  88 + * @param databaseProductName 外部数据库的产品名,即 DatabaseMetaData#getDatabaseProductName
  89 + * @return INSERT SQL
  90 + */
  91 + private static String buildInsertSql(String tableName, String databaseProductName) {
  92 + String now = StrUtil.equalsAny(databaseProductName, CURRENT_TIMESTAMP_PRODUCT_NAMES) ? "CURRENT_TIMESTAMP" : "NOW()";
  93 + return StrUtil.format(
  94 + "INSERT INTO {} (id, device_id, tenant_id, method, report_time, data, create_time) VALUES (?, ?, ?, ?, ?, ?, {})",
  95 + tableName, now);
  96 + }
  97 +
64 @Override 98 @Override
65 protected JdbcTemplate initProducer(IotDataSinkDatabaseConfig config) throws Exception { 99 protected JdbcTemplate initProducer(IotDataSinkDatabaseConfig config) throws Exception {
66 // 使用 HikariCP 连接池,HikariCP 会根据 JDBC URL 自动检测并加载对应的数据库驱动 100 // 使用 HikariCP 连接池,HikariCP 会根据 JDBC URL 自动检测并加载对应的数据库驱动
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotDatabaseDataRuleActionTest.java
1 package cn.iocoder.yudao.module.iot.service.rule.data.action; 1 package cn.iocoder.yudao.module.iot.service.rule.data.action;
2 2
  3 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
3 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
4 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.config.IotDataSinkDatabaseConfig; 5 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.config.IotDataSinkDatabaseConfig;
5 import cn.iocoder.yudao.module.iot.enums.rule.IotDataSinkTypeEnum; 6 import cn.iocoder.yudao.module.iot.enums.rule.IotDataSinkTypeEnum;
6 import com.zaxxer.hikari.HikariDataSource; 7 import com.zaxxer.hikari.HikariDataSource;
7 import org.junit.jupiter.api.BeforeEach; 8 import org.junit.jupiter.api.BeforeEach;
8 import org.junit.jupiter.api.Test; 9 import org.junit.jupiter.api.Test;
  10 +import org.mockito.InOrder;
9 import org.mockito.Mock; 11 import org.mockito.Mock;
10 import org.mockito.MockitoAnnotations; 12 import org.mockito.MockitoAnnotations;
  13 +import org.springframework.dao.DataAccessException;
11 import org.springframework.jdbc.core.JdbcTemplate; 14 import org.springframework.jdbc.core.JdbcTemplate;
12 15
13 import javax.sql.DataSource; 16 import javax.sql.DataSource;
  17 +import java.sql.Connection;
  18 +import java.sql.DatabaseMetaData;
  19 +import java.sql.PreparedStatement;
  20 +import java.sql.SQLException;
14 import java.time.LocalDateTime; 21 import java.time.LocalDateTime;
15 import java.util.Arrays; 22 import java.util.Arrays;
  23 +import java.util.LinkedHashMap;
  24 +import java.util.Map;
16 25
17 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 26 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
18 import static org.junit.jupiter.api.Assertions.assertEquals; 27 import static org.junit.jupiter.api.Assertions.assertEquals;
  28 +import static org.junit.jupiter.api.Assertions.assertSame;
19 import static org.junit.jupiter.api.Assertions.assertThrows; 29 import static org.junit.jupiter.api.Assertions.assertThrows;
20 import static org.mockito.Mockito.*; 30 import static org.mockito.Mockito.*;
21 31
@@ -26,11 +36,21 @@ import static org.mockito.Mockito.*; @@ -26,11 +36,21 @@ import static org.mockito.Mockito.*;
26 */ 36 */
27 class IotDatabaseDataRuleActionTest { 37 class IotDatabaseDataRuleActionTest {
28 38
  39 + private static final String TABLE_NAME = "iot_device_message";
  40 +
29 private IotDatabaseDataRuleAction databaseDataRuleAction; 41 private IotDatabaseDataRuleAction databaseDataRuleAction;
30 42
31 @Mock 43 @Mock
32 private JdbcTemplate jdbcTemplate; 44 private JdbcTemplate jdbcTemplate;
33 45
  46 + /**
  47 + * 外部数据库:由真实的 {@link JdbcTemplate} 管理连接、Statement 的生命周期
  48 + */
  49 + private DataSource externalDataSource;
  50 + private Connection externalConnection;
  51 + private DatabaseMetaData externalMetaData;
  52 + private PreparedStatement externalStatement;
  53 +
34 @BeforeEach 54 @BeforeEach
35 public void setUp() { 55 public void setUp() {
36 MockitoAnnotations.openMocks(this); 56 MockitoAnnotations.openMocks(this);
@@ -47,17 +67,15 @@ class IotDatabaseDataRuleActionTest { @@ -47,17 +67,15 @@ class IotDatabaseDataRuleActionTest {
47 public void testExecute_tableNameValid() throws Exception { 67 public void testExecute_tableNameValid() throws Exception {
48 // 准备参数:普通表名,以及 schema.table 格式 68 // 准备参数:普通表名,以及 schema.table 格式
49 for (String tableName : Arrays.asList("iot_device_message", "_message01", "iot_data.device_message")) { 69 for (String tableName : Arrays.asList("iot_device_message", "_message01", "iot_data.device_message")) {
  70 + mockExternalDatabase("MySQL");
50 IotDataSinkDatabaseConfig config = buildConfig(tableName); 71 IotDataSinkDatabaseConfig config = buildConfig(tableName);
51 - IotDatabaseDataRuleAction action = spy(databaseDataRuleAction);  
52 - doReturn(jdbcTemplate).when(action).getProducer(same(config)); 72 + IotDatabaseDataRuleAction action = spyAction(config);
53 73
54 // 调用 74 // 调用
55 action.execute(buildMessage(), config); 75 action.execute(buildMessage(), config);
56 76
57 // 断言:表名原样拼接,值使用参数绑定 77 // 断言:表名原样拼接,值使用参数绑定
58 - verify(jdbcTemplate).update(eq("INSERT INTO " + tableName  
59 - + " (id, device_id, tenant_id, method, report_time, data, create_time) VALUES (?, ?, ?, ?, ?, ?, NOW())"),  
60 - any(), any(), any(), any(), any(), any()); 78 + verify(externalConnection).prepareStatement(buildInsertSql(tableName, "NOW()"));
61 } 79 }
62 } 80 }
63 81
@@ -79,6 +97,105 @@ class IotDatabaseDataRuleActionTest { @@ -79,6 +97,105 @@ class IotDatabaseDataRuleActionTest {
79 } 97 }
80 98
81 @Test 99 @Test
  100 + public void testExecute_createTimeByDatabaseProduct() throws Exception {
  101 + // 准备参数:数据库产品名 -> create_time 使用的时间函数
  102 + Map<String, String> cases = new LinkedHashMap<>();
  103 + cases.put("Oracle", "CURRENT_TIMESTAMP");
  104 + cases.put("Microsoft SQL Server", "CURRENT_TIMESTAMP");
  105 + cases.put("MySQL", "NOW()");
  106 + cases.put("PostgreSQL", "NOW()");
  107 + cases.put("DM DBMS", "NOW()"); // 未登记的产品,保持原 NOW()
  108 + cases.put(null, "NOW()");
  109 + for (Map.Entry<String, String> entry : cases.entrySet()) {
  110 + mockExternalDatabase(entry.getKey());
  111 + IotDeviceMessage message = buildMessage();
  112 + IotDataSinkDatabaseConfig config = buildConfig(TABLE_NAME);
  113 + IotDatabaseDataRuleAction action = spyAction(config);
  114 +
  115 + // 调用
  116 + action.execute(message, config);
  117 +
  118 + // 断言:只借用一次外部连接,在同一连接上读取产品名、创建 Statement
  119 + verify(externalDataSource, times(1)).getConnection();
  120 + InOrder inOrder = inOrder(externalConnection, externalMetaData, externalStatement);
  121 + inOrder.verify(externalConnection).getMetaData();
  122 + inOrder.verify(externalMetaData).getDatabaseProductName();
  123 + inOrder.verify(externalConnection).prepareStatement(buildInsertSql(TABLE_NAME, entry.getValue()));
  124 + // 断言:原 6 个参数的值、绑定方式、顺序不变
  125 + inOrder.verify(externalStatement).setString(1, "message_01");
  126 + inOrder.verify(externalStatement).setObject(2, 1L);
  127 + inOrder.verify(externalStatement).setObject(3, 2L);
  128 + inOrder.verify(externalStatement).setString(4, "thing.property.post");
  129 + inOrder.verify(externalStatement).setObject(5, message.getReportTime());
  130 + inOrder.verify(externalStatement).setString(6, JsonUtils.toJsonString(message));
  131 + inOrder.verify(externalStatement).executeUpdate();
  132 + // 断言:Statement、连接由 JdbcTemplate 关闭、归还
  133 + inOrder.verify(externalStatement).close();
  134 + inOrder.verify(externalConnection).close();
  135 + verify(externalConnection, times(1)).prepareStatement(anyString());
  136 + }
  137 + }
  138 +
  139 + @Test
  140 + public void testExecute_getMetaDataFailure() throws Exception {
  141 + // 准备参数:读取外部数据库元数据失败
  142 + mockExternalDatabase("Oracle");
  143 + SQLException metaDataException = new SQLException("metadata failure");
  144 + when(externalConnection.getMetaData()).thenThrow(metaDataException);
  145 + IotDataSinkDatabaseConfig config = buildConfig(TABLE_NAME);
  146 + IotDatabaseDataRuleAction action = spyAction(config);
  147 +
  148 + // 调用,并断言异常:不吞掉原始异常
  149 + DataAccessException exception = assertThrows(DataAccessException.class,
  150 + () -> action.execute(buildMessage(), config));
  151 + assertSame(metaDataException, exception.getCause());
  152 + // 断言:未创建 Statement,连接被归还
  153 + verify(externalConnection, never()).prepareStatement(anyString());
  154 + verify(externalConnection, times(1)).close();
  155 + verify(externalDataSource, times(1)).getConnection();
  156 + }
  157 +
  158 + @Test
  159 + public void testExecute_bindFailure() throws Exception {
  160 + // 准备参数:绑定参数失败
  161 + mockExternalDatabase("Microsoft SQL Server");
  162 + SQLException bindException = new SQLException("bind failure");
  163 + doThrow(bindException).when(externalStatement).setString(1, "message_01");
  164 + IotDataSinkDatabaseConfig config = buildConfig(TABLE_NAME);
  165 + IotDatabaseDataRuleAction action = spyAction(config);
  166 +
  167 + // 调用,并断言异常:不吞掉原始异常
  168 + DataAccessException exception = assertThrows(DataAccessException.class,
  169 + () -> action.execute(buildMessage(), config));
  170 + assertSame(bindException, exception.getCause());
  171 + // 断言:未执行 SQL,已创建的 Statement 被关闭,连接被归还
  172 + verify(externalStatement, never()).executeUpdate();
  173 + verify(externalStatement, times(1)).close();
  174 + verify(externalConnection, times(1)).close();
  175 + verify(externalDataSource, times(1)).getConnection();
  176 + }
  177 +
  178 + @Test
  179 + public void testExecute_executeUpdateFailure() throws Exception {
  180 + // 准备参数:执行 SQL 失败
  181 + mockExternalDatabase("PostgreSQL");
  182 + SQLException executeException = new SQLException("execute failure");
  183 + when(externalStatement.executeUpdate()).thenThrow(executeException);
  184 + IotDataSinkDatabaseConfig config = buildConfig(TABLE_NAME);
  185 + IotDatabaseDataRuleAction action = spyAction(config);
  186 +
  187 + // 调用,并断言异常:不吞掉原始异常
  188 + DataAccessException exception = assertThrows(DataAccessException.class,
  189 + () -> action.execute(buildMessage(), config));
  190 + assertSame(executeException, exception.getCause());
  191 + // 断言:Statement 被关闭,连接被归还
  192 + verify(externalConnection).prepareStatement(buildInsertSql(TABLE_NAME, "NOW()"));
  193 + verify(externalStatement, times(1)).close();
  194 + verify(externalConnection, times(1)).close();
  195 + verify(externalDataSource, times(1)).getConnection();
  196 + }
  197 +
  198 + @Test
82 public void testCloseProducer_whenHikari() throws Exception { 199 public void testCloseProducer_whenHikari() throws Exception {
83 // 准备:底层是 HikariDataSource 200 // 准备:底层是 HikariDataSource
84 HikariDataSource hikari = mock(HikariDataSource.class); 201 HikariDataSource hikari = mock(HikariDataSource.class);
@@ -102,6 +219,35 @@ class IotDatabaseDataRuleActionTest { @@ -102,6 +219,35 @@ class IotDatabaseDataRuleActionTest {
102 verifyNoInteractions(other); 219 verifyNoInteractions(other);
103 } 220 }
104 221
  222 + /**
  223 + * 模拟外部数据库:DataSource 每次返回同一个 Connection,产品名来自该 Connection 的元数据
  224 + */
  225 + private void mockExternalDatabase(String databaseProductName) throws SQLException {
  226 + externalDataSource = mock(DataSource.class);
  227 + externalConnection = mock(Connection.class);
  228 + externalMetaData = mock(DatabaseMetaData.class);
  229 + externalStatement = mock(PreparedStatement.class);
  230 + when(externalDataSource.getConnection()).thenReturn(externalConnection);
  231 + when(externalConnection.getMetaData()).thenReturn(externalMetaData);
  232 + when(externalMetaData.getDatabaseProductName()).thenReturn(databaseProductName);
  233 + when(externalConnection.prepareStatement(anyString())).thenReturn(externalStatement);
  234 + when(externalStatement.executeUpdate()).thenReturn(1);
  235 + }
  236 +
  237 + /**
  238 + * 创建 Action,其 producer 为基于外部数据库的真实 {@link JdbcTemplate}
  239 + */
  240 + private IotDatabaseDataRuleAction spyAction(IotDataSinkDatabaseConfig config) throws Exception {
  241 + IotDatabaseDataRuleAction action = spy(databaseDataRuleAction);
  242 + doReturn(new JdbcTemplate(externalDataSource)).when(action).getProducer(same(config));
  243 + return action;
  244 + }
  245 +
  246 + private static String buildInsertSql(String tableName, String now) {
  247 + return "INSERT INTO " + tableName
  248 + + " (id, device_id, tenant_id, method, report_time, data, create_time) VALUES (?, ?, ?, ?, ?, ?, " + now + ")";
  249 + }
  250 +
105 private static IotDataSinkDatabaseConfig buildConfig(String tableName) { 251 private static IotDataSinkDatabaseConfig buildConfig(String tableName) {
106 IotDataSinkDatabaseConfig config = new IotDataSinkDatabaseConfig(); 252 IotDataSinkDatabaseConfig config = new IotDataSinkDatabaseConfig();
107 config.setJdbcUrl("jdbc:mysql://127.0.0.1:3306/iot_data"); 253 config.setJdbcUrl("jdbc:mysql://127.0.0.1:3306/iot_data");
@@ -112,7 +258,7 @@ class IotDatabaseDataRuleActionTest { @@ -112,7 +258,7 @@ class IotDatabaseDataRuleActionTest {
112 } 258 }
113 259
114 private static IotDeviceMessage buildMessage() { 260 private static IotDeviceMessage buildMessage() {
115 - return new IotDeviceMessage().setId("message_01").setDeviceId(1L).setTenantId(1L) 261 + return new IotDeviceMessage().setId("message_01").setDeviceId(1L).setTenantId(2L)
116 .setMethod("thing.property.post").setReportTime(LocalDateTime.now()); 262 .setMethod("thing.property.post").setReportTime(LocalDateTime.now());
117 } 263 }
118 264
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/cal/plan/MesCalPlanMapper.java
@@ -38,7 +38,6 @@ public interface MesCalPlanMapper extends BaseMapperX&lt;MesCalPlanDO&gt; { @@ -38,7 +38,6 @@ public interface MesCalPlanMapper extends BaseMapperX&lt;MesCalPlanDO&gt; {
38 default List<MesCalPlanDO> selectListByStatusAndDay(Integer status, LocalDate day) { 38 default List<MesCalPlanDO> selectListByStatusAndDay(Integer status, LocalDate day) {
39 return selectList(new LambdaQueryWrapperX<MesCalPlanDO>() 39 return selectList(new LambdaQueryWrapperX<MesCalPlanDO>()
40 .eq(MesCalPlanDO::getStatus, status) 40 .eq(MesCalPlanDO::getStatus, status)
41 - // TODO @AI:dayend 获取下?应该有方法,这样可以 le,更统一点;  
42 .lt(MesCalPlanDO::getStartDate, day.plusDays(1).atStartOfDay()) 41 .lt(MesCalPlanDO::getStartDate, day.plusDays(1).atStartOfDay())
43 .ge(MesCalPlanDO::getEndDate, day.atStartOfDay()) 42 .ge(MesCalPlanDO::getEndDate, day.atStartOfDay())
44 .orderByAsc(MesCalPlanDO::getId)); 43 .orderByAsc(MesCalPlanDO::getId));