Commit a38811c937b36e2afba97b27087a612a1106108e

Authored by YunaiV
1 parent 7f53d60e

fix: 完善多数据库统计查询与限量清理兼容

按执行数据源选择统计日期、分页及分批删除语法。
统一不计数的限量查询入口并迁移 Lambda 查询,补充按类回归测试。
Showing 60 changed files with 2267 additions and 261 deletions
yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/util/TenantUtils.java
@@ -15,6 +15,11 @@ import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_ @@ -15,6 +15,11 @@ import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_
15 public class TenantUtils { 15 public class TenantUtils {
16 16
17 /** 17 /**
  18 + * 平台共享数据的租户编号;不是未设置租户上下文时的 null
  19 + */
  20 + public static final Long GLOBAL_TENANT_ID = 0L;
  21 +
  22 + /**
18 * 使用指定租户,执行对应的逻辑 23 * 使用指定租户,执行对应的逻辑
19 * 24 *
20 * 注意,如果当前是忽略租户的情况下,会被强制设置成不忽略租户 25 * 注意,如果当前是忽略租户的情况下,会被强制设置成不忽略租户
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/enums/DbTypeEnum.java
@@ -107,11 +107,19 @@ public enum DbTypeEnum { @@ -107,11 +107,19 @@ public enum DbTypeEnum {
107 */ 107 */
108 private final String findInSetTemplate; 108 private final String findInSetTemplate;
109 109
  110 + /**
  111 + * 根据数据库产品名获得 MyBatis Plus 类型
  112 + *
  113 + * @param databaseProductName 数据库产品名
  114 + * @return MyBatis Plus 类型;产品名为空或未登记时返回 null
  115 + */
110 public static DbType find(String databaseProductName) { 116 public static DbType find(String databaseProductName) {
111 if (StrUtil.isBlank(databaseProductName)) { 117 if (StrUtil.isBlank(databaseProductName)) {
112 return null; 118 return null;
113 } 119 }
114 - return MAP_BY_NAME.get(databaseProductName).getMpDbType(); 120 + return Optional.ofNullable(MAP_BY_NAME.get(databaseProductName))
  121 + .map(DbTypeEnum::getMpDbType)
  122 + .orElse(null);
115 } 123 }
116 124
117 public static String getFindInSetTemplate(DbType dbType) { 125 public static String getFindInSetTemplate(DbType dbType) {
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java
@@ -216,6 +216,39 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> { @@ -216,6 +216,39 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
216 return CollUtil.getLast(selectList(queryWrapper)); 216 return CollUtil.getLast(selectList(queryWrapper));
217 } 217 }
218 218
  219 + /**
  220 + * 获取满足条件的第 1 条记录,见 {@link #selectListLimitN(Wrapper, int)}
  221 + *
  222 + * @param queryWrapper 查询条件
  223 + * @return 第 1 条;不存在返回 null
  224 + */
  225 + default T selectFirstOne(Wrapper<T> queryWrapper) {
  226 + return CollUtil.getFirst(selectListLimitN(queryWrapper, 1));
  227 + }
  228 +
  229 + /**
  230 + * 获取满足条件的前 limit 条记录
  231 + *
  232 + * 使用 MyBatis Plus 分页插件,按执行时连接的数据库类型生成 LIMIT、ROWNUM、OFFSET FETCH 等语句,并且不执行 COUNT 查询;
  233 + * 保留 queryWrapper 的排序与 select 投影,不能再通过 last 追加行数限制
  234 + *
  235 + * 注意:SQL Server 的分页语句要求有 ORDER BY,queryWrapper 需要设置排序
  236 + *
  237 + * @param queryWrapper 查询条件
  238 + * @param limit 条数,不能为负数;为 0 时不查询,直接返回空列表
  239 + * @return 记录列表
  240 + */
  241 + default List<T> selectListLimitN(Wrapper<T> queryWrapper, int limit) {
  242 + if (limit < 0) {
  243 + // 分页插件会把负数的 size 视为不分页,从而查询全表
  244 + throw new IllegalArgumentException("limit 不能为负数:" + limit);
  245 + }
  246 + if (limit == 0) {
  247 + return CollUtil.newArrayList();
  248 + }
  249 + return selectPage(MyBatisUtils.<T>buildPage(0, limit, false), queryWrapper).getRecords();
  250 + }
  251 +
219 default Long selectCount() { 252 default Long selectCount() {
220 return selectCount(new QueryWrapper<>()); 253 return selectCount(new QueryWrapper<>());
221 } 254 }
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/query/QueryWrapperX.java
1 package cn.iocoder.yudao.framework.mybatis.core.query; 1 package cn.iocoder.yudao.framework.mybatis.core.query;
2 2
3 -import cn.iocoder.yudao.framework.mybatis.core.util.JdbcUtils;  
4 -import com.baomidou.mybatisplus.annotation.DbType;  
5 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; 3 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
6 import com.baomidou.mybatisplus.core.toolkit.ArrayUtils; 4 import com.baomidou.mybatisplus.core.toolkit.ArrayUtils;
7 import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; 5 import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
@@ -139,28 +137,4 @@ public class QueryWrapperX&lt;T&gt; extends QueryWrapper&lt;T&gt; { @@ -139,28 +137,4 @@ public class QueryWrapperX&lt;T&gt; extends QueryWrapper&lt;T&gt; {
139 return this; 137 return this;
140 } 138 }
141 139
142 - /**  
143 - * 设置只返回最后一条  
144 - *  
145 - * TODO 芋艿:不是完美解,需要在思考下。如果使用多数据源,并且数据源是多种类型时,可能会存在问题:实现之返回一条的语法不同  
146 - *  
147 - * @return this  
148 - */  
149 - public QueryWrapperX<T> limitN(int n) {  
150 - DbType dbType = JdbcUtils.getDbType();  
151 - switch (dbType) {  
152 - case ORACLE:  
153 - case ORACLE_12C:  
154 - super.le("ROWNUM", n);  
155 - break;  
156 - case SQL_SERVER:  
157 - case SQL_SERVER2005:  
158 - super.select("TOP " + n + " *"); // 由于 SQL Server 是通过 SELECT TOP 1 实现限制一条,所以只好使用 * 查询剩余字段  
159 - break;  
160 - default: // MySQL、PostgreSQL、DM 达梦、KingbaseES 大金都是采用 LIMIT 实现  
161 - super.last("LIMIT " + n);  
162 - }  
163 - return this;  
164 - }  
165 -  
166 } 140 }
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/util/JdbcUtils.java
1 package cn.iocoder.yudao.framework.mybatis.core.util; 1 package cn.iocoder.yudao.framework.mybatis.core.util;
2 2
  3 +import cn.hutool.core.util.StrUtil;
3 import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; 4 import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
4 import cn.iocoder.yudao.framework.common.util.spring.SpringUtils; 5 import cn.iocoder.yudao.framework.common.util.spring.SpringUtils;
5 import cn.iocoder.yudao.framework.mybatis.core.enums.DbTypeEnum; 6 import cn.iocoder.yudao.framework.mybatis.core.enums.DbTypeEnum;
6 import com.baomidou.dynamic.datasource.DynamicRoutingDataSource; 7 import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
7 import com.baomidou.mybatisplus.annotation.DbType; 8 import com.baomidou.mybatisplus.annotation.DbType;
8 import org.springframework.beans.factory.NoSuchBeanDefinitionException; 9 import org.springframework.beans.factory.NoSuchBeanDefinitionException;
  10 +import org.springframework.jdbc.datasource.ConnectionHolder;
  11 +import org.springframework.transaction.support.TransactionSynchronizationManager;
  12 +import org.springframework.util.ConcurrentReferenceHashMap;
9 13
10 import javax.sql.DataSource; 14 import javax.sql.DataSource;
11 import java.sql.Connection; 15 import java.sql.Connection;
  16 +import java.sql.DatabaseMetaData;
12 import java.sql.DriverManager; 17 import java.sql.DriverManager;
13 import java.sql.SQLException; 18 import java.sql.SQLException;
  19 +import java.util.Locale;
  20 +import java.util.Map;
14 21
15 /** 22 /**
16 * JDBC 工具类 23 * JDBC 工具类
@@ -20,6 +27,12 @@ import java.sql.SQLException; @@ -20,6 +27,12 @@ import java.sql.SQLException;
20 public class JdbcUtils { 27 public class JdbcUtils {
21 28
22 /** 29 /**
  30 + * 数据源与 DB 类型的缓存;弱引用数据源,动态移除的数据源可被回收
  31 + */
  32 + private static final Map<DataSource, DbType> DB_TYPE_CACHE =
  33 + new ConcurrentReferenceHashMap<>(16, ConcurrentReferenceHashMap.ReferenceType.WEAK);
  34 +
  35 + /**
23 * 判断连接是否正确 36 * 判断连接是否正确
24 * 37 *
25 * @param url 数据源连接 38 * @param url 数据源连接
@@ -55,26 +68,127 @@ public class JdbcUtils { @@ -55,26 +68,127 @@ public class JdbcUtils {
55 } 68 }
56 69
57 /** 70 /**
  71 + * 获得当前执行时路由到的数据源
  72 + *
  73 + * 存在动态数据源时,按 {@link DynamicRoutingDataSource#determineDataSource()} 获取当前线程路由到的数据源;否则使用唯一的数据源
  74 + *
  75 + * @return 数据源
  76 + */
  77 + public static DataSource getDataSource() {
  78 + DataSource dataSource = getRoutingDataSource();
  79 + return dataSource instanceof DynamicRoutingDataSource
  80 + ? ((DynamicRoutingDataSource) dataSource).determineDataSource() : dataSource;
  81 + }
  82 +
  83 + /**
  84 + * 取得注册的路由数据源;没有动态数据源时使用唯一的数据源。
  85 + *
  86 + * @return 路由数据源或唯一数据源
  87 + */
  88 + private static DataSource getRoutingDataSource() {
  89 + try {
  90 + return SpringUtils.getBean(DynamicRoutingDataSource.class);
  91 + } catch (NoSuchBeanDefinitionException e) {
  92 + return SpringUtils.getBean(DataSource.class);
  93 + }
  94 + }
  95 +
  96 + /**
  97 + * 获得当前数据源上 Spring 事务已绑定的连接,不借出新连接,也不转移连接的关闭责任。
  98 + * 动态数据源的事务资源绑定在路由数据源上;事务内再次切换路由键,不会改变已绑定的执行连接。
  99 + */
  100 + static Connection getTransactionalConnection() {
  101 + Object resource = TransactionSynchronizationManager.getResource(getRoutingDataSource());
  102 + return resource instanceof ConnectionHolder ? ((ConnectionHolder) resource).getConnection() : null;
  103 + }
  104 +
  105 + /**
58 * 通过当前数据库连接获得对应的 DB 类型 106 * 通过当前数据库连接获得对应的 DB 类型
59 * 107 *
  108 + * 事务内复用已绑定连接;其它情况按当前路由到的数据源实例缓存,命中缓存时不再获取连接
  109 + *
60 * @return DB 类型 110 * @return DB 类型
61 */ 111 */
62 public static DbType getDbType() { 112 public static DbType getDbType() {
63 - DataSource dataSource;  
64 - try {  
65 - DynamicRoutingDataSource dynamicRoutingDataSource = SpringUtils.getBean(DynamicRoutingDataSource.class);  
66 - dataSource = dynamicRoutingDataSource.determineDataSource();  
67 - } catch (NoSuchBeanDefinitionException e) {  
68 - dataSource = SpringUtils.getBean(DataSource.class); 113 + Connection connection = getTransactionalConnection();
  114 + if (connection != null) {
  115 + try {
  116 + return detectDbType(connection);
  117 + } catch (SQLException e) {
  118 + throw new IllegalArgumentException(e.getMessage());
  119 + }
69 } 120 }
  121 + return getDbType(getDataSource());
  122 + }
  123 +
  124 + // TODO DONE @AI:方法注释;已说明连接归属、缓存或数据源选择。
  125 + /**
  126 + * 按实际数据源实例缓存数据库类型;检测失败时不写入缓存。
  127 + *
  128 + * @param dataSource 实际路由到的数据源
  129 + * @return 数据库类型
  130 + */
  131 + static DbType getDbType(DataSource dataSource) {
  132 + return DB_TYPE_CACHE.computeIfAbsent(dataSource, JdbcUtils::detectDbType);
  133 + }
  134 +
  135 + /**
  136 + * 通过自行借用的连接识别数据库类型,并在结束后关闭该连接。
  137 + *
  138 + * @param dataSource 待识别的数据源
  139 + * @return 数据库类型
  140 + */
  141 + private static DbType detectDbType(DataSource dataSource) {
70 try (Connection conn = dataSource.getConnection()) { 142 try (Connection conn = dataSource.getConnection()) {
71 - return DbTypeEnum.find(conn.getMetaData().getDatabaseProductName()); 143 + return detectDbType(conn);
72 } catch (SQLException e) { 144 } catch (SQLException e) {
73 throw new IllegalArgumentException(e.getMessage()); 145 throw new IllegalArgumentException(e.getMessage());
74 } 146 }
75 } 147 }
76 148
77 /** 149 /**
  150 + * 读取执行连接的元数据识别数据库类型,不关闭调用方持有的连接。
  151 + *
  152 + * @param connection 当前执行连接
  153 + * @return 数据库类型;产品名为空时保留原有 null 返回
  154 + * @throws SQLException 读取数据库元数据失败
  155 + */
  156 + private static DbType detectDbType(Connection connection) throws SQLException {
  157 + DatabaseMetaData metaData = connection.getMetaData();
  158 + String productName = metaData.getDatabaseProductName();
  159 + DbType dbType = DbTypeEnum.find(productName);
  160 + if (dbType != null || StrUtil.isBlank(productName)) {
  161 + return dbType;
  162 + }
  163 + // 不使用 MyBatis Plus 的 URL 识别,避免其在日志中打印完整 URL
  164 + dbType = findDbTypeByNativeUrl(metaData.getURL());
  165 + if (dbType == null) {
  166 + throw new IllegalArgumentException("不支持的数据库产品:" + productName);
  167 + }
  168 + return dbType;
  169 + }
  170 +
  171 + /**
  172 + * 按原生驱动 URL 识别产品名未登记的数据库,与 {@link #getDbType(String)} 的归类保持一致
  173 + *
  174 + * @param url JDBC URL
  175 + * @return DB 类型;无法识别时返回 null
  176 + */
  177 + private static DbType findDbTypeByNativeUrl(String url) {
  178 + if (StrUtil.isBlank(url)) {
  179 + return null;
  180 + }
  181 + String lowerUrl = url.toLowerCase(Locale.ROOT);
  182 + if (lowerUrl.contains(":highgo:")) {
  183 + return DbType.POSTGRE_SQL;
  184 + }
  185 + if (lowerUrl.contains(":mariadb:")) {
  186 + return DbType.MARIADB;
  187 + }
  188 + return null;
  189 + }
  190 +
  191 + /**
78 * 判断 JDBC 连接是否为 SQLServer 数据库 192 * 判断 JDBC 连接是否为 SQLServer 数据库
79 * 193 *
80 * @param url JDBC 连接 194 * @param url JDBC 连接
@@ -95,4 +209,6 @@ public class JdbcUtils { @@ -95,4 +209,6 @@ public class JdbcUtils {
95 return ObjectUtils.equalsAny(dbType, DbType.SQL_SERVER, DbType.SQL_SERVER2005); 209 return ObjectUtils.equalsAny(dbType, DbType.SQL_SERVER, DbType.SQL_SERVER2005);
96 } 210 }
97 211
  212 +
  213 +
98 } 214 }
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/util/MyBatisUtils.java
@@ -61,6 +61,26 @@ public class MyBatisUtils { @@ -61,6 +61,26 @@ public class MyBatisUtils {
61 return page; 61 return page;
62 } 62 }
63 63
  64 + /**
  65 + * 构建指定偏移量的分页对象
  66 + *
  67 + * @param offset 偏移量,负数按 0 处理,不要求是每页条数的整数倍
  68 + * @param size 每页条数
  69 + * @param searchCount 是否查询总数
  70 + * @param <T> 记录类型
  71 + * @return 分页对象
  72 + */
  73 + public static <T> Page<T> buildPage(long offset, long size, boolean searchCount) {
  74 + return new Page<T>(1, size, searchCount) {
  75 +
  76 + @Override
  77 + public long offset() {
  78 + return Math.max(offset, 0);
  79 + }
  80 +
  81 + };
  82 + }
  83 +
64 @SuppressWarnings("PatternVariableCanBeUsed") 84 @SuppressWarnings("PatternVariableCanBeUsed")
65 public static <T> void addOrder(Wrapper<T> wrapper, Collection<SortingField> sortingFields) { 85 public static <T> void addOrder(Wrapper<T> wrapper, Collection<SortingField> sortingFields) {
66 if (CollUtil.isEmpty(sortingFields)) { 86 if (CollUtil.isEmpty(sortingFields)) {
@@ -220,7 +240,13 @@ public class MyBatisUtils { @@ -220,7 +240,13 @@ public class MyBatisUtils {
220 .replace(FIND_IN_SET_VALUE_PLACEHOLDER, "{" + paramIndex + "}"); 240 .replace(FIND_IN_SET_VALUE_PLACEHOLDER, "{" + paramIndex + "}");
221 } 241 }
222 242
223 - private static boolean isSafeColumnName(String columnName) { 243 + /**
  244 + * 判断表名或列名是否只包含字母、数字、下划线及层级分隔点
  245 + *
  246 + * @param columnName 表名或列名,可带表别名前缀
  247 + * @return 是否满足安全标识符格式
  248 + */
  249 + static boolean isSafeColumnName(String columnName) {
224 return StrUtil.isNotEmpty(columnName) && SAFE_COLUMN_NAME_PATTERN.matcher(columnName).matches(); 250 return StrUtil.isNotEmpty(columnName) && SAFE_COLUMN_NAME_PATTERN.matcher(columnName).matches();
225 } 251 }
226 252
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/util/SqlDialectUtils.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.mybatis.core.util;
  2 +
  3 +import cn.hutool.core.util.StrUtil;
  4 +import cn.iocoder.yudao.framework.mybatis.core.enums.DbTypeEnum;
  5 +import com.baomidou.mybatisplus.annotation.DbType;
  6 +import org.springframework.util.ConcurrentReferenceHashMap;
  7 +
  8 +import javax.sql.DataSource;
  9 +import java.sql.Connection;
  10 +import java.sql.DatabaseMetaData;
  11 +import java.sql.SQLException;
  12 +import java.util.EnumSet;
  13 +import java.util.Locale;
  14 +import java.util.Map;
  15 +import java.util.Set;
  16 +import java.util.regex.Pattern;
  17 +
  18 +// TODO DONE @AI:方法注释;已补齐方言重载与内部辅助方法的参数、返回值及边界。
  19 +// TODO DONE @AI:多余的方法,可以考虑去掉;当然,保留也可以。。。已核对生产 Java/XML 调用,现有方法均有用途,保留。
  20 +/**
  21 + * 日期统计、行数限制、分批删除 SQL 片段的方言工具类
  22 + *
  23 + * 1. 每次生成 SQL 时,按当前执行时路由到的数据源选择方言(按数据源实例缓存),不使用启动时固定的 databaseId 或主库
  24 + * 2. 只有明确识别的 PostgreSQL、Oracle、SQL Server 2012+ 输出新片段;其它已识别的数据库(MySQL、MariaDB、OceanBase、H2、
  25 + * 达梦、神通、人大金仓、openGauss、GaussDB、瀚高、MogDB 等)保持原有 SQL 片段;完全未识别的数据库抛出异常
  26 + * 3. 参数只能是代码中固定的列名,经过白名单校验;格式均为常量,业务值必须继续使用 #{} 绑定
  27 + *
  28 + * 在 XML 或注解中使用:${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('create_time')}
  29 + *
  30 + * @author 芋道源码
  31 + */
  32 +@SuppressWarnings("EnhancedSwitchMigration")
  33 +public class SqlDialectUtils {
  34 +
  35 + /**
  36 + * SQL Server 2012 的主版本号
  37 + */
  38 + private static final int SQL_SERVER_2012_MAJOR_VERSION = 11;
  39 +
  40 + /**
  41 + * 保持原有 SQL 片段的数据库类型
  42 + */
  43 + private static final Set<DbType> ORIGINAL_DB_TYPES = EnumSet.of(DbType.MYSQL, DbType.OCEAN_BASE,
  44 + DbType.H2, DbType.DM, DbType.OSCAR, DbType.KINGBASE_ES, DbType.OPENGAUSS, DbType.GAUSS_DB,
  45 + DbType.SQL_SERVER2005);
  46 +
  47 + /**
  48 + * 兼容 PostgreSQL 的原生驱动 URL:瀚高、openGauss、GaussDB、MogDB、人大金仓
  49 + */
  50 + private static final Pattern POSTGRESQL_VARIANT_URL_PATTERN =
  51 + Pattern.compile(":(highgo|opengauss|gaussdb|mogdb|kingbase\\d*):");
  52 +
  53 + /**
  54 + * MyBatis 参数名:参数或其属性路径,例如 limit、reqVO.limit
  55 + */
  56 + private static final Pattern PARAM_NAME_PATTERN =
  57 + Pattern.compile("[A-Za-z_][A-Za-z0-9_]*(\\.[A-Za-z_][A-Za-z0-9_]*)*");
  58 +
  59 + /**
  60 + * 数据源与方言的缓存;弱引用数据源,动态移除的数据源可被回收
  61 + */
  62 + private static final Map<DataSource, Dialect> DIALECT_CACHE =
  63 + new ConcurrentReferenceHashMap<>(16, ConcurrentReferenceHashMap.ReferenceType.WEAK);
  64 +
  65 + /**
  66 + * 方言
  67 + */
  68 + enum Dialect {
  69 +
  70 + /**
  71 + * 原有 SQL 片段
  72 + */
  73 + ORIGINAL,
  74 + POSTGRESQL,
  75 + ORACLE,
  76 + SQLSERVER
  77 +
  78 + }
  79 +
  80 + // ========== 日期字符串 ==========
  81 +
  82 + /**
  83 + * 按天格式化为 yyyy-MM-dd 字符串;原有片段为 DATE_FORMAT(column, '%Y-%m-%d')
  84 + *
  85 + * @param column 列名
  86 + * @return SQL 片段
  87 + */
  88 + public static String formatDate(String column) {
  89 + return formatDate(getDialect(), column);
  90 + }
  91 +
  92 + /**
  93 + * 按指定方言生成 yyyy-MM-dd 字符串,保留各分支原有格式。
  94 + *
  95 + * @param dialect 已识别的数据库方言
  96 + * @param column 代码中固定的安全列名
  97 + * @return SQL 片段
  98 + */
  99 + static String formatDate(Dialect dialect, String column) {
  100 + checkColumn(column);
  101 + switch (dialect) {
  102 + case POSTGRESQL:
  103 + case ORACLE:
  104 + return "TO_CHAR(" + column + ", 'YYYY-MM-DD')";
  105 + case SQLSERVER:
  106 + return "CONVERT(varchar(10), " + column + ", 23)";
  107 + default:
  108 + return "DATE_FORMAT(" + column + ", '%Y-%m-%d')";
  109 + }
  110 + }
  111 +
  112 + /**
  113 + * 按月格式化为 yyyyMM 字符串;原有片段为 DATE_FORMAT(column, '%Y%m')
  114 + *
  115 + * @param column 列名
  116 + * @return SQL 片段
  117 + */
  118 + // TODO DONE @AI:用不到的,可以暂时先去掉噢; CRM 业绩统计使用 yyyyMM,商城交易统计使用 yyyy-MM,均需保留。
  119 + public static String formatYearMonth(String column) {
  120 + return formatYearMonth(getDialect(), column);
  121 + }
  122 +
  123 + /**
  124 + * 按指定方言生成 yyyyMM 字符串,供 CRM 按月统计使用。
  125 + *
  126 + * @param dialect 已识别的数据库方言
  127 + * @param column 代码中固定的安全列名
  128 + * @return SQL 片段
  129 + */
  130 + static String formatYearMonth(Dialect dialect, String column) {
  131 + checkColumn(column);
  132 + switch (dialect) {
  133 + case POSTGRESQL:
  134 + case ORACLE:
  135 + return "TO_CHAR(" + column + ", 'YYYYMM')";
  136 + case SQLSERVER:
  137 + return "CONVERT(varchar(6), " + column + ", 112)";
  138 + default:
  139 + return "DATE_FORMAT(" + column + ", '%Y%m')";
  140 + }
  141 + }
  142 +
  143 + /**
  144 + * 按月格式化为 yyyy-MM 字符串;原有片段为 CONCAT(EXTRACT(YEAR ...), '-', LPAD(...))
  145 + *
  146 + * @param column 列名
  147 + * @return SQL 片段
  148 + */
  149 + // TODO DONE @AI:用不到的,可以暂时先去掉噢; CRM 业绩统计使用 yyyyMM,商城交易统计使用 yyyy-MM,均需保留。
  150 + public static String formatYearDashMonth(String column) {
  151 + return formatYearDashMonth(getDialect(), column);
  152 + }
  153 +
  154 + /**
  155 + * 按指定方言生成 yyyy-MM 字符串,供交易统计月份标签使用。
  156 + *
  157 + * @param dialect 已识别的数据库方言
  158 + * @param column 代码中固定的安全列名
  159 + * @return SQL 片段
  160 + */
  161 + static String formatYearDashMonth(Dialect dialect, String column) {
  162 + checkColumn(column);
  163 + switch (dialect) {
  164 + case POSTGRESQL:
  165 + case ORACLE:
  166 + return "TO_CHAR(" + column + ", 'YYYY-MM')";
  167 + case SQLSERVER:
  168 + return "CONVERT(varchar(7), " + column + ", 120)";
  169 + default:
  170 + return "CONCAT(EXTRACT(YEAR FROM " + column + "), '-', LPAD(CONCAT(EXTRACT(MONTH FROM " + column + ")), 2, '0'))";
  171 + }
  172 + }
  173 +
  174 + /**
  175 + * 按天分组;原有片段为 DATE(column),新方言输出 yyyy-MM-dd 字符串
  176 + *
  177 + * 适用于同时兼容日期值与 yyyy-MM-dd 字符串的消费者
  178 + *
  179 + * @param column 列名
  180 + * @return SQL 片段
  181 + */
  182 + public static String date(String column) {
  183 + return date(getDialect(), column);
  184 + }
  185 +
  186 + /**
  187 + * 生成按天分组表达式;原路径保留 DATE,新分支返回日期文本。
  188 + *
  189 + * @param dialect 已识别的数据库方言
  190 + * @param column 代码中固定的安全列名
  191 + * @return SQL 片段
  192 + */
  193 + static String date(Dialect dialect, String column) {
  194 + checkColumn(column);
  195 + if (dialect == Dialect.ORIGINAL) {
  196 + return "DATE(" + column + ")";
  197 + }
  198 + return formatDate(dialect, column);
  199 + }
  200 +
  201 + // TODO DONE @AI:用不到的,可以先清理掉噢; AI1 首页与交易统计的 String 日期消费者仍在使用,保留。
  202 + /**
  203 + * 按天分组;原有片段为 CAST(column AS DATE),新方言输出 yyyy-MM-dd 字符串
  204 + *
  205 + * 适用于 String 类型的消费者
  206 + *
  207 + * @param column 列名
  208 + * @return SQL 片段
  209 + */
  210 + public static String castDateText(String column) {
  211 + return castDateText(getDialect(), column);
  212 + }
  213 +
  214 + /**
  215 + * 生成 String 日期消费者所需的转换表达式,保留原路径 CAST AS DATE。
  216 + *
  217 + * @param dialect 已识别的数据库方言
  218 + * @param column 代码中固定的安全列名
  219 + * @return SQL 片段
  220 + */
  221 + static String castDateText(Dialect dialect, String column) {
  222 + checkColumn(column);
  223 + if (dialect == Dialect.ORIGINAL) {
  224 + return "CAST(" + column + " AS DATE)";
  225 + }
  226 + return formatDate(dialect, column);
  227 + }
  228 +
  229 + // ========== 日期值 ==========
  230 +
  231 + /**
  232 + * 按天截断为日期值;原有片段为 CAST(column AS DATE)
  233 + *
  234 + * 适用于 LocalDate 类型的消费者
  235 + *
  236 + * @param column 列名
  237 + * @return SQL 片段
  238 + */
  239 + // TODO DONE @AI:多余的是不是去掉? 会员统计的 LocalDate 消费者仍在使用,保留 DATE 类型转换。
  240 + public static String castDate(String column) {
  241 + return castDate(getDialect(), column);
  242 + }
  243 +
  244 + // TODO DONE @AI:方法注释;已补齐方言重载与内部辅助方法的参数、返回值及边界。
  245 + /**
  246 + * 生成 LocalDate 消费者所需的日期值;Oracle 用 TRUNC 去掉时分秒。
  247 + *
  248 + * @param dialect 已识别的数据库方言
  249 + * @param column 代码中固定的安全列名
  250 + * @return SQL 片段
  251 + */
  252 + static String castDate(Dialect dialect, String column) {
  253 + checkColumn(column);
  254 + if (dialect == Dialect.ORACLE) {
  255 + // Oracle 的 DATE 包含时分秒,CAST AS DATE 不会截断
  256 + return "TRUNC(" + column + ")";
  257 + }
  258 + return "CAST(" + column + " AS DATE)";
  259 + }
  260 +
  261 + // ========== 日期部件 ==========
  262 +
  263 + /**
  264 + * 月份(1-12);原有片段为 MONTH(column)
  265 + *
  266 + * @param column 列名
  267 + * @return SQL 片段
  268 + */
  269 + public static String month(String column) {
  270 + return month(getDialect(), column);
  271 + }
  272 +
  273 + /**
  274 + * 按指定方言提取月份。
  275 + *
  276 + * @param dialect 已识别的数据库方言
  277 + * @param column 代码中固定的安全列名
  278 + * @return 月份 SQL 表达式
  279 + */
  280 + static String month(Dialect dialect, String column) {
  281 + return datePart(dialect, column, "MONTH");
  282 + }
  283 +
  284 + /**
  285 + * 年份;原有片段为 YEAR(column)
  286 + *
  287 + * @param column 列名
  288 + * @return SQL 片段
  289 + */
  290 + // TODO DONE @AI:方法注释;已补齐方言重载与内部辅助方法的参数、返回值及边界。
  291 + public static String year(String column) {
  292 + return year(getDialect(), column);
  293 + }
  294 +
  295 + /**
  296 + * 按指定方言提取年份,供 CRM 业绩目标筛选使用。
  297 + *
  298 + * @param dialect 已识别的数据库方言
  299 + * @param column 代码中固定的安全列名
  300 + * @return 年份 SQL 表达式
  301 + */
  302 + static String year(Dialect dialect, String column) {
  303 + return datePart(dialect, column, "YEAR");
  304 + }
  305 +
  306 + /**
  307 + * 日(1-31);原有片段为 DAYOFMONTH(column)
  308 + *
  309 + * @param column 列名
  310 + * @return SQL 片段
  311 + */
  312 + public static String dayOfMonth(String column) {
  313 + return dayOfMonth(getDialect(), column);
  314 + }
  315 +
  316 + // TODO DONE @AI:方法注释;已补齐方言重载与内部辅助方法的参数、返回值及边界。
  317 + /**
  318 + * 按指定方言提取月内日,原路径保留 DAYOFMONTH。
  319 + *
  320 + * @param dialect 已识别的数据库方言
  321 + * @param column 代码中固定的安全列名
  322 + * @return 日 SQL 表达式
  323 + */
  324 + static String dayOfMonth(Dialect dialect, String column) {
  325 + checkColumn(column);
  326 + if (dialect == Dialect.ORIGINAL) {
  327 + return "DAYOFMONTH(" + column + ")";
  328 + }
  329 + return datePart(dialect, column, "DAY");
  330 + }
  331 +
  332 + /**
  333 + * 日期部件:PostgreSQL 的 EXTRACT 返回 numeric,需要转为整数;原有片段与 SQL Server 均为 field(column)
  334 + *
  335 + * @param dialect 已识别的数据库方言
  336 + * @param column 代码中固定的安全列名
  337 + * @param field 代码中固定的 YEAR、MONTH 或 DAY
  338 + * @return 日期部件 SQL 表达式
  339 + */
  340 + private static String datePart(Dialect dialect, String column, String field) {
  341 + checkColumn(column);
  342 + switch (dialect) {
  343 + case POSTGRESQL:
  344 + return "CAST(EXTRACT(" + field + " FROM " + column + ") AS INTEGER)";
  345 + case ORACLE:
  346 + return "EXTRACT(" + field + " FROM " + column + ")";
  347 + default:
  348 + return field + "(" + column + ")";
  349 + }
  350 + }
  351 +
  352 + // ========== 聚合 ==========
  353 +
  354 + /**
  355 + * 平均成交周期(天):两个时间之间的完整天数(向零截断)求平均,保留 1 位小数(截断),无数据时为 0
  356 + *
  357 + * 原有片段为 COALESCE(TRUNCATE(AVG(TIMESTAMPDIFF(DAY, begin, end)), 1), 0)。
  358 + * 注意:仅 PostgreSQL 输出新片段;Oracle、SQL Server 的完整天数表达式依赖实际列类型(DATE / TIMESTAMP / datetime2 / date 等),
  359 + * 尚未依据实际 DDL 在真实数据库验收,因此保持原有片段
  360 + *
  361 + * @param beginColumn 开始时间列名
  362 + * @param endColumn 结束时间列名
  363 + * @return SQL 片段
  364 + */
  365 + public static String customerDealCycle(String beginColumn, String endColumn) {
  366 + return customerDealCycle(getDialect(), beginColumn, endColumn);
  367 + }
  368 +
  369 + /**
  370 + * 按指定方言生成平均完整天数;未验收的数据库继续使用原表达式。
  371 + *
  372 + * @param dialect 已识别的数据库方言
  373 + * @param beginColumn 代码中固定的开始时间列名
  374 + * @param endColumn 代码中固定的结束时间列名
  375 + * @return 平均成交周期 SQL 表达式
  376 + */
  377 + static String customerDealCycle(Dialect dialect, String beginColumn, String endColumn) {
  378 + checkColumn(beginColumn);
  379 + checkColumn(endColumn);
  380 + if (dialect == Dialect.POSTGRESQL) {
  381 + return "COALESCE(TRUNC(AVG(CAST(EXTRACT(DAY FROM (CAST(" + endColumn + " AS TIMESTAMP) - CAST("
  382 + + beginColumn + " AS TIMESTAMP))) AS NUMERIC)), 1), 0)";
  383 + }
  384 + return "COALESCE(TRUNCATE(AVG(TIMESTAMPDIFF(DAY, " + beginColumn + ", " + endColumn + ")), 1), 0)";
  385 + }
  386 +
  387 + // ========== 其它 ==========
  388 +
  389 + /**
  390 + * 顶层 SELECT 无 FROM 时的补充:Oracle 为 FROM DUAL,其它为空
  391 + *
  392 + * @return SQL 片段
  393 + */
  394 + public static String fromDual() {
  395 + return fromDual(getDialect());
  396 + }
  397 +
  398 + /**
  399 + * 为无顶层 FROM 的标量汇总补充 Oracle DUAL,其他分支保持空文本。
  400 + *
  401 + * @param dialect 已识别的数据库方言
  402 + * @return Oracle 为 FROM DUAL,其他为空字符串
  403 + */
  404 + static String fromDual(Dialect dialect) {
  405 + return dialect == Dialect.ORACLE ? "FROM DUAL" : "";
  406 + }
  407 +
  408 + // ========== 行数限制 ==========
  409 +
  410 + /**
  411 + * 只返回前 rows 行,追加在语句末尾(ORDER BY 之后);原有片段为 LIMIT rows
  412 + *
  413 + * 仅用于 XML 或注解中手写的 SQL;使用 Wrapper 查询时,使用 BaseMapperX 的 selectListLimitN(queryWrapper, limit) 分页查询
  414 + *
  415 + * 1. Oracle:FETCH FIRST rows ROWS ONLY,要求 Oracle 12c+
  416 + * 2. SQL Server 2012+:OFFSET 0 ROWS FETCH NEXT rows ROWS ONLY;OFFSET 必须跟在 ORDER BY 之后,
  417 + * 语句没有排序时补充 ORDER BY (SELECT NULL),即不保证返回哪几行,与原有 LIMIT 无排序时一致
  418 + *
  419 + * @param rows 行数,不能为负数;SQL Server 的 FETCH 不支持 0 行
  420 + * @param ordered 语句是否已有 ORDER BY
  421 + * @return SQL 片段
  422 + */
  423 + public static String limitRows(int rows, boolean ordered) {
  424 + return limitRows(getDialect(), rows, ordered);
  425 + }
  426 +
  427 + /**
  428 + * 生成固定行数限制;负数拒绝,SQL Server 的 FETCH 不接受 0 行。
  429 + *
  430 + * @param dialect 已识别的数据库方言
  431 + * @param rows 固定行数
  432 + * @param ordered 原查询是否已有排序
  433 + * @return 追加在查询末尾的 SQL 片段
  434 + */
  435 + static String limitRows(Dialect dialect, int rows, boolean ordered) {
  436 + if (rows < 0) {
  437 + throw new IllegalArgumentException("行数不能为负数:" + rows);
  438 + }
  439 + if (rows == 0 && dialect == Dialect.SQLSERVER) {
  440 + throw new IllegalArgumentException("SQL Server 的 FETCH 行数必须大于 0");
  441 + }
  442 + return limit(dialect, String.valueOf(rows), ordered);
  443 + }
  444 +
  445 + /**
  446 + * 只返回前 #{param} 行,行数使用 #{} 绑定;规则同 {@link #limitRows(int, boolean)}
  447 + *
  448 + * 在 XML 或注解中使用:${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@limitParam('limit', true)}
  449 + *
  450 + * @param param 行数的参数名,例如 limit
  451 + * @param ordered 语句是否已有 ORDER BY
  452 + * @return SQL 片段
  453 + */
  454 + public static String limitParam(String param, boolean ordered) {
  455 + return limitParam(getDialect(), param, ordered);
  456 + }
  457 +
  458 + /**
  459 + * 生成绑定参数的行数限制,参数名通过白名单校验。
  460 + *
  461 + * @param dialect 已识别的数据库方言
  462 + * @param param 行数参数名或属性路径
  463 + * @param ordered 原查询是否已有排序
  464 + * @return 包含 MyBatis 参数占位的 SQL 片段
  465 + */
  466 + static String limitParam(Dialect dialect, String param, boolean ordered) {
  467 + checkParam(param);
  468 + return limit(dialect, "#{" + param + "}", ordered);
  469 + }
  470 +
  471 + /**
  472 + * 拼装行数限制;rows 仅来自已校验的整数或 MyBatis 参数占位。
  473 + *
  474 + * @param dialect 已识别的数据库方言
  475 + * @param rows 固定整数文本或已校验的参数占位
  476 + * @param ordered 原查询是否已有排序
  477 + * @return 行数限制 SQL 片段
  478 + */
  479 + private static String limit(Dialect dialect, String rows, boolean ordered) {
  480 + switch (dialect) {
  481 + case ORACLE:
  482 + return "FETCH FIRST " + rows + " ROWS ONLY";
  483 + case SQLSERVER:
  484 + return (ordered ? "" : "ORDER BY (SELECT NULL) ") + "OFFSET 0 ROWS FETCH NEXT " + rows + " ROWS ONLY";
  485 + default:
  486 + return "LIMIT " + rows;
  487 + }
  488 + }
  489 +
  490 + // ========== 分批删除 ==========
  491 +
  492 + /**
  493 + * 分批删除 column &lt; #{valueParam} 的记录,每次最多删除 #{limitParam} 行;
  494 + * 原有语句为 DELETE FROM table WHERE column &lt; #{valueParam} LIMIT #{limitParam}
  495 + *
  496 + * 1. PostgreSQL:DELETE 不支持 LIMIT,按主键 id 的子查询选出一批
  497 + * 2. Oracle:ROWNUM &lt;= #{limitParam}
  498 + * 3. SQL Server 2012+:按主键 id 的子查询 SELECT TOP (#{limitParam})。
  499 + * 不使用 DELETE TOP (n),因为 JSqlParser 无法解析,租户、数据权限插件解析 DELETE 时会失败
  500 + *
  501 + * 要求表的主键列为 id;表名、列名只能是代码中固定的常量,值均使用 #{} 绑定
  502 + *
  503 + * 在注解中使用:${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@deleteBatchByLessThan('infra_job_log', 'create_time', 'createTime', 'limit')}
  504 + *
  505 + * @param table 表名
  506 + * @param column 比较的列名
  507 + * @param valueParam 比较值的参数名
  508 + * @param limitParam 每批行数的参数名
  509 + * @return SQL 语句
  510 + */
  511 + public static String deleteBatchByLessThan(String table, String column, String valueParam, String limitParam) {
  512 + return deleteBatchByLessThan(getDialect(), table, column, valueParam, limitParam);
  513 + }
  514 +
  515 + /**
  516 + * 按方言生成主键为 id 的分批物理删除;当前用于忽略租户的全局清理任务。
  517 + *
  518 + * @param dialect 已识别的数据库方言
  519 + * @param table 代码中固定的表名
  520 + * @param column 代码中固定的时间列名
  521 + * @param valueParam 时间阈值参数名
  522 + * @param limitParam 每批行数参数名
  523 + * @return 参数绑定的 DELETE 语句
  524 + */
  525 + static String deleteBatchByLessThan(Dialect dialect, String table, String column,
  526 + String valueParam, String limitParam) {
  527 + checkColumn(table);
  528 + checkColumn(column);
  529 + checkParam(valueParam);
  530 + checkParam(limitParam);
  531 + String condition = column + " < #{" + valueParam + "}";
  532 + String limit = "#{" + limitParam + "}";
  533 + switch (dialect) {
  534 + case POSTGRESQL:
  535 + return "DELETE FROM " + table + " WHERE id IN (SELECT id FROM " + table
  536 + + " WHERE " + condition + " LIMIT " + limit + ")";
  537 + case ORACLE:
  538 + return "DELETE FROM " + table + " WHERE " + condition + " AND ROWNUM <= " + limit;
  539 + case SQLSERVER:
  540 + return "DELETE FROM " + table + " WHERE id IN (SELECT TOP (" + limit + ") id FROM " + table
  541 + + " WHERE " + condition + ")";
  542 + default:
  543 + return "DELETE FROM " + table + " WHERE " + condition + " LIMIT " + limit;
  544 + }
  545 + }
  546 +
  547 + // ========== 方言识别 ==========
  548 +
  549 + /**
  550 + * 取得当前执行方言;事务内复用已绑定连接,非事务按路由后的数据源缓存。
  551 + *
  552 + * @return 当前执行方言
  553 + */
  554 + static Dialect getDialect() {
  555 + Connection connection = JdbcUtils.getTransactionalConnection();
  556 + if (connection != null) {
  557 + try {
  558 + // 使用真正执行 SQL 的事务连接;不能将其方言缓存到事务内新切换的路由键上
  559 + return resolveDialect(connection.getMetaData());
  560 + } catch (SQLException e) {
  561 + throw new IllegalStateException("获取数据库方言失败", e);
  562 + }
  563 + }
  564 + return getDialect(JdbcUtils.getDataSource());
  565 + }
  566 +
  567 + /**
  568 + * 按数据源实例缓存方言;识别失败时不写入缓存。
  569 + *
  570 + * @param dataSource 实际路由到的数据源
  571 + * @return 数据库方言
  572 + */
  573 + static Dialect getDialect(DataSource dataSource) {
  574 + return DIALECT_CACHE.computeIfAbsent(dataSource, SqlDialectUtils::detectDialect);
  575 + }
  576 +
  577 + /**
  578 + * 借用一个连接识别数据源方言,并关闭该次自行获取的连接。
  579 + *
  580 + * @param dataSource 待识别的数据源
  581 + * @return 数据库方言
  582 + */
  583 + private static Dialect detectDialect(DataSource dataSource) {
  584 + try (Connection conn = dataSource.getConnection()) {
  585 + return resolveDialect(conn.getMetaData());
  586 + } catch (SQLException e) {
  587 + throw new IllegalStateException("获取数据库方言失败", e);
  588 + }
  589 + }
  590 +
  591 + /**
  592 + * 根据数据库元数据识别方言
  593 + *
  594 + * 注意:使用 PostgreSQL 官方驱动(jdbc:postgresql:)连接 openGauss、瀚高等兼容 PostgreSQL 的数据库时,
  595 + * 产品名与 URL 均与 PostgreSQL 相同,无法区分,会按 PostgreSQL 处理
  596 + *
  597 + * @param metaData 数据库元数据
  598 + * @return 方言
  599 + * @throws SQLException 读取元数据失败
  600 + */
  601 + static Dialect resolveDialect(DatabaseMetaData metaData) throws SQLException {
  602 + String productName = metaData.getDatabaseProductName();
  603 + String url = StrUtil.nullToEmpty(metaData.getURL()).toLowerCase(Locale.ROOT);
  604 + DbType dbType = DbTypeEnum.find(productName);
  605 + if (dbType == DbType.ORACLE) {
  606 + return Dialect.ORACLE;
  607 + }
  608 + if (dbType == DbType.SQL_SERVER) {
  609 + return metaData.getDatabaseMajorVersion() >= SQL_SERVER_2012_MAJOR_VERSION
  610 + ? Dialect.SQLSERVER : Dialect.ORIGINAL;
  611 + }
  612 + if (dbType == DbType.POSTGRE_SQL) {
  613 + if (isPostgreSQLVariantUrl(url)) {
  614 + return Dialect.ORIGINAL;
  615 + }
  616 + if (url.contains(":postgresql:")) {
  617 + return Dialect.POSTGRESQL;
  618 + }
  619 + throw unsupported(productName);
  620 + }
  621 + if (ORIGINAL_DB_TYPES.contains(dbType)) {
  622 + return Dialect.ORIGINAL;
  623 + }
  624 + // 产品名未登记时,仅按明确的原生驱动 URL 识别瀚高、MogDB 等,以及 MariaDB
  625 + if (dbType == null && (isPostgreSQLVariantUrl(url) || url.contains(":mariadb:"))) {
  626 + return Dialect.ORIGINAL;
  627 + }
  628 + throw unsupported(productName);
  629 + }
  630 +
  631 + /**
  632 + * 判断 URL 是否为需要保留原 SQL 路径的 PostgreSQL 兼容产品驱动。
  633 + *
  634 + * @param url 转为小写的 JDBC URL
  635 + * @return 是否命中已登记的兼容产品 URL
  636 + */
  637 + private static boolean isPostgreSQLVariantUrl(String url) {
  638 + return POSTGRESQL_VARIANT_URL_PATTERN.matcher(url).find();
  639 + }
  640 +
  641 + /**
  642 + * 创建未识别产品的异常,不将完整 JDBC URL 写入消息。
  643 + *
  644 + * @param productName 数据库产品名
  645 + * @return 不支持该产品的异常
  646 + */
  647 + private static IllegalStateException unsupported(String productName) {
  648 + return new IllegalStateException("SQL 方言不支持的数据库产品:" + productName);
  649 + }
  650 +
  651 + /**
  652 + * 校验代码中固定的表名或列名,拒绝表达式与语句片段。
  653 + *
  654 + * @param column 待校验的表名或列名
  655 + */
  656 + private static void checkColumn(String column) {
  657 + if (!MyBatisUtils.isSafeColumnName(column)) {
  658 + throw new IllegalArgumentException("Invalid column name: " + column);
  659 + }
  660 + }
  661 +
  662 + /**
  663 + * 校验 MyBatis 参数名或属性路径,拒绝占位符逃逸与 SQL 片段。
  664 + *
  665 + * @param param 待校验的参数名
  666 + */
  667 + private static void checkParam(String param) {
  668 + if (param == null || !PARAM_NAME_PATTERN.matcher(param).matches()) {
  669 + throw new IllegalArgumentException("Invalid param name: " + param);
  670 + }
  671 + }
  672 +
  673 +}
yudao-framework/yudao-spring-boot-starter-mybatis/src/test/java/cn/iocoder/yudao/framework/mybatis/core/enums/DbTypeEnumTest.java
@@ -25,6 +25,15 @@ public class DbTypeEnumTest { @@ -25,6 +25,15 @@ public class DbTypeEnumTest {
25 // 神通 JDBC 驱动(jdbc:oscar:)返回的 ProductName 25 // 神通 JDBC 驱动(jdbc:oscar:)返回的 ProductName
26 assertEquals(DbType.OSCAR, DbTypeEnum.find("OSCAR")); 26 assertEquals(DbType.OSCAR, DbTypeEnum.find("OSCAR"));
27 assertNull(DbTypeEnum.find("")); 27 assertNull(DbTypeEnum.find(""));
  28 + assertNull(DbTypeEnum.find(null));
  29 + }
  30 +
  31 + @Test
  32 + public void testFind_unregistered() {
  33 + // 未登记的产品名返回 null,而不是 NPE,由调用方给出明确的处理
  34 + assertNull(DbTypeEnum.find("HighGo"));
  35 + assertNull(DbTypeEnum.find("MariaDB"));
  36 + assertNull(DbTypeEnum.find("SQLite"));
28 } 37 }
29 38
30 @Test 39 @Test
yudao-framework/yudao-spring-boot-starter-mybatis/src/test/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperXTest.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.mybatis.core.mapper;
  2 +
  3 +import com.baomidou.mybatisplus.annotation.DbType;
  4 +import com.baomidou.mybatisplus.core.conditions.Wrapper;
  5 +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
  6 +import com.baomidou.mybatisplus.core.metadata.IPage;
  7 +import com.baomidou.mybatisplus.extension.plugins.pagination.DialectFactory;
  8 +import net.sf.jsqlparser.parser.CCJSqlParserUtil;
  9 +import org.junit.jupiter.api.Test;
  10 +
  11 +import java.lang.reflect.InvocationHandler;
  12 +import java.lang.reflect.Method;
  13 +import java.lang.reflect.Proxy;
  14 +import java.util.Collections;
  15 +import java.util.List;
  16 +
  17 +import static org.junit.jupiter.api.Assertions.*;
  18 +
  19 +/**
  20 + * {@link BaseMapperX} 限量查询的单元测试
  21 + *
  22 + * 不连接数据库:通过 JDK 动态代理执行 default 方法,记录传给 MyBatis Plus 的分页参数
  23 + *
  24 + * @author 芋道源码
  25 + */
  26 +public class BaseMapperXTest {
  27 +
  28 + @Test
  29 + public void testSelectListLimitN_limit() {
  30 + FakeMapper fake = new FakeMapper(List.of("a", "b"));
  31 + QueryWrapper<String> wrapper = new QueryWrapper<String>()
  32 + .select("id", "name")
  33 + .eq("status", 1)
  34 + .orderByDesc("id");
  35 + String sqlSegment = wrapper.getSqlSegment();
  36 +
  37 + // 调用
  38 + List<String> list = fake.mapper.selectListLimitN(wrapper, 5);
  39 + // 断言:第 1 页、每页 5 条、不执行 COUNT、不追加排序
  40 + assertEquals(List.of("a", "b"), list);
  41 + assertEquals(1, fake.invocations);
  42 + assertEquals(1, fake.page.getCurrent());
  43 + assertEquals(5, fake.page.getSize());
  44 + assertFalse(fake.page.searchCount());
  45 + assertTrue(fake.page.orders().isEmpty());
  46 + // 断言:条件、排序、投影均不变,行数限制交给分页插件生成
  47 + assertSame(wrapper, fake.wrapper);
  48 + assertEquals(sqlSegment, wrapper.getSqlSegment());
  49 + assertEquals("id,name", wrapper.getSqlSelect());
  50 + assertFalse(wrapper.getSqlSegment().toUpperCase().contains("LIMIT"));
  51 + }
  52 +
  53 + @Test
  54 + public void testSelectListLimitN_zero() {
  55 + FakeMapper fake = new FakeMapper(List.of("a"));
  56 +
  57 + // 调用,并断言:不查询,避免分页插件按不分页处理而查询全表
  58 + List<String> list = fake.mapper.selectListLimitN(new QueryWrapper<String>().orderByDesc("id"), 0);
  59 + assertTrue(list.isEmpty());
  60 + assertEquals(0, fake.invocations);
  61 + }
  62 +
  63 + @Test
  64 + public void testSelectListLimitN_negative() {
  65 + FakeMapper fake = new FakeMapper(List.of("a"));
  66 +
  67 + // 调用,并断言:分页插件会把负数的 size 视为不分页,因此直接拒绝
  68 + assertThrows(IllegalArgumentException.class,
  69 + () -> fake.mapper.selectListLimitN(new QueryWrapper<String>().orderByDesc("id"), -1));
  70 + assertEquals(0, fake.invocations);
  71 + }
  72 +
  73 + @Test
  74 + public void testSelectFirstOne() {
  75 + FakeMapper fake = new FakeMapper(List.of("a", "b"));
  76 + QueryWrapper<String> wrapper = new QueryWrapper<String>().eq("status", 1).orderByAsc("id");
  77 +
  78 + // 调用,并断言:只取 1 条,返回第 1 条
  79 + assertEquals("a", fake.mapper.selectFirstOne(wrapper));
  80 + assertEquals(1, fake.invocations);
  81 + assertEquals(1, fake.page.getCurrent());
  82 + assertEquals(1, fake.page.getSize());
  83 + assertFalse(fake.page.searchCount());
  84 + assertSame(wrapper, fake.wrapper);
  85 + }
  86 +
  87 + @Test
  88 + public void testSelectFirstOne_empty() {
  89 + FakeMapper fake = new FakeMapper(Collections.emptyList());
  90 +
  91 + // 调用,并断言:不存在时返回 null
  92 + assertNull(fake.mapper.selectFirstOne(new QueryWrapper<String>().orderByAsc("id")));
  93 + assertEquals(1, fake.invocations);
  94 + }
  95 +
  96 + /**
  97 + * 分页插件按执行连接识别的数据库类型生成的分页语句(第 1 页的 offset 为 0),均可被 JSqlParser 解析
  98 + *
  99 + * 注意:SQL Server 的 OFFSET FETCH 要求原语句有 ORDER BY,因此调用方必须设置排序
  100 + */
  101 + @Test
  102 + public void testPaginationDialect() throws Exception {
  103 + String sql = "SELECT id, name FROM system_users WHERE status = ? ORDER BY id DESC";
  104 +
  105 + // MySQL、OSCAR 神通等:LIMIT
  106 + assertEquals(sql + " LIMIT ?", paginate(DbType.MYSQL, sql));
  107 + assertEquals(sql + " LIMIT ?", paginate(DbType.OSCAR, sql));
  108 + // PostgreSQL、H2、人大金仓、openGauss、瀚高等:LIMIT
  109 + assertEquals(sql + " LIMIT ?", paginate(DbType.POSTGRE_SQL, sql));
  110 + assertEquals(sql + " LIMIT ?", paginate(DbType.H2, sql));
  111 + assertEquals(sql + " LIMIT ?", paginate(DbType.KINGBASE_ES, sql));
  112 + assertEquals(sql + " LIMIT ?", paginate(DbType.OPENGAUSS, sql));
  113 + assertEquals(sql + " LIMIT ?", paginate(DbType.HIGH_GO, sql));
  114 + // Oracle、达梦:ROWNUM 包裹原语句,先排序后取行,兼容 Oracle 11g
  115 + String oracle = "SELECT * FROM ( SELECT TMP.*, ROWNUM ROW_ID FROM ( " + sql
  116 + + " ) TMP WHERE ROWNUM <=?) WHERE ROW_ID > ?";
  117 + assertEquals(oracle, paginate(DbType.ORACLE, sql));
  118 + assertEquals(oracle, paginate(DbType.DM, sql));
  119 + // SQL Server 2012+:OFFSET FETCH 直接追加在 ORDER BY 之后
  120 + assertEquals(sql + " OFFSET ? ROWS FETCH NEXT ? ROWS ONLY", paginate(DbType.SQL_SERVER, sql));
  121 +
  122 + for (DbType dbType : List.of(DbType.MYSQL, DbType.POSTGRE_SQL, DbType.ORACLE, DbType.SQL_SERVER)) {
  123 + assertNotNull(CCJSqlParserUtil.parse(paginate(dbType, sql)), dbType.name());
  124 + }
  125 + }
  126 +
  127 + private static String paginate(DbType dbType, String sql) {
  128 + return DialectFactory.getDialect(dbType).buildPaginationSql(sql, 0, 5).getDialectSql();
  129 + }
  130 +
  131 + /**
  132 + * 基于 JDK 动态代理的 {@link BaseMapperX}:default 方法真实执行,selectList(IPage, Wrapper) 记录参数并返回固定结果
  133 + *
  134 + * @author 芋道源码
  135 + */
  136 + private static class FakeMapper implements InvocationHandler {
  137 +
  138 + private final List<String> records;
  139 + private final BaseMapperX<String> mapper;
  140 + private IPage<?> page;
  141 + private Wrapper<?> wrapper;
  142 + private int invocations;
  143 +
  144 + @SuppressWarnings("unchecked")
  145 + FakeMapper(List<String> records) {
  146 + this.records = records;
  147 + this.mapper = (BaseMapperX<String>) Proxy.newProxyInstance(getClass().getClassLoader(),
  148 + new Class<?>[]{BaseMapperX.class}, this);
  149 + }
  150 +
  151 + @Override
  152 + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  153 + if (method.isDefault()) {
  154 + return InvocationHandler.invokeDefault(proxy, method, args);
  155 + }
  156 + if ("selectList".equals(method.getName()) && args != null && args.length == 2
  157 + && args[0] instanceof IPage && args[1] instanceof Wrapper) {
  158 + invocations++;
  159 + page = (IPage<?>) args[0];
  160 + wrapper = (Wrapper<?>) args[1];
  161 + return records;
  162 + }
  163 + throw new UnsupportedOperationException(method.getName());
  164 + }
  165 +
  166 + }
  167 +
  168 +}
yudao-framework/yudao-spring-boot-starter-mybatis/src/test/java/cn/iocoder/yudao/framework/mybatis/core/util/JdbcUtilsTest.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.mybatis.core.util;
  2 +
  3 +import cn.hutool.core.exceptions.UtilException;
  4 +import cn.hutool.extra.spring.SpringUtil;
  5 +import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
  6 +import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
  7 +import com.baomidou.mybatisplus.annotation.DbType;
  8 +import org.junit.jupiter.api.AfterEach;
  9 +import org.junit.jupiter.api.BeforeEach;
  10 +import org.junit.jupiter.api.Test;
  11 +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
  12 +import org.springframework.beans.factory.support.DefaultListableBeanFactory;
  13 +import org.springframework.jdbc.datasource.ConnectionHolder;
  14 +import org.springframework.transaction.support.TransactionSynchronizationManager;
  15 +
  16 +import javax.sql.DataSource;
  17 +import java.lang.reflect.InvocationHandler;
  18 +import java.lang.reflect.Method;
  19 +import java.lang.reflect.Proxy;
  20 +import java.sql.Connection;
  21 +import java.sql.DatabaseMetaData;
  22 +import java.sql.SQLException;
  23 +import java.util.Collections;
  24 +
  25 +import static org.junit.jupiter.api.Assertions.*;
  26 +
  27 +/**
  28 + * {@link JdbcUtils} 的单元测试
  29 + *
  30 + * @author 芋道源码
  31 + */
  32 +public class JdbcUtilsTest {
  33 +
  34 + private ConfigurableListableBeanFactory originBeanFactory;
  35 +
  36 + @BeforeEach
  37 + public void setUp() {
  38 + try {
  39 + originBeanFactory = SpringUtil.getConfigurableBeanFactory();
  40 + } catch (UtilException e) {
  41 + originBeanFactory = null;
  42 + }
  43 + }
  44 +
  45 + @AfterEach
  46 + public void tearDown() {
  47 + DynamicDataSourceContextHolder.clear();
  48 + new SpringUtil().postProcessBeanFactory(originBeanFactory);
  49 + }
  50 +
  51 + @Test
  52 + public void testGetDbType_cachedPerDataSource() {
  53 + FakeDataSource mysql = new FakeDataSource("MySQL", "jdbc:mysql://127.0.0.1:3306/a");
  54 + FakeDataSource sqlServer = new FakeDataSource("Microsoft SQL Server", "jdbc:sqlserver://127.0.0.1:1433");
  55 +
  56 + for (int i = 0; i < 3; i++) {
  57 + assertEquals(DbType.MYSQL, JdbcUtils.getDbType(mysql.getDataSource()));
  58 + assertEquals(DbType.SQL_SERVER, JdbcUtils.getDbType(sqlServer.getDataSource()));
  59 + }
  60 + assertEquals(1, mysql.getConnectionCount());
  61 + assertEquals(1, sqlServer.getConnectionCount());
  62 + }
  63 +
  64 + @Test
  65 + public void testGetDbType_followsDynamicRouting() {
  66 + FakeDataSource postgresql = new FakeDataSource("PostgreSQL", "jdbc:postgresql://127.0.0.1:5432/a");
  67 + FakeDataSource oracle = new FakeDataSource("Oracle", "jdbc:oracle:thin:@127.0.0.1:1521:xe");
  68 + DynamicRoutingDataSource dynamicRoutingDataSource = new DynamicRoutingDataSource(Collections.emptyList());
  69 + dynamicRoutingDataSource.setPrimary("master");
  70 + dynamicRoutingDataSource.addDataSource("master", postgresql.getDataSource());
  71 + dynamicRoutingDataSource.addDataSource("oracle", oracle.getDataSource());
  72 + mockBeanFactory("dynamicRoutingDataSource", dynamicRoutingDataSource);
  73 +
  74 + for (int i = 0; i < 3; i++) {
  75 + assertSame(postgresql.getDataSource(), JdbcUtils.getDataSource());
  76 + assertEquals(DbType.POSTGRE_SQL, JdbcUtils.getDbType());
  77 + DynamicDataSourceContextHolder.push("oracle");
  78 + assertSame(oracle.getDataSource(), JdbcUtils.getDataSource());
  79 + assertEquals(DbType.ORACLE, JdbcUtils.getDbType());
  80 + DynamicDataSourceContextHolder.poll();
  81 + }
  82 + assertEquals(1, postgresql.getConnectionCount());
  83 + assertEquals(1, oracle.getConnectionCount());
  84 + }
  85 +
  86 + @Test
  87 + public void testGetDbType_withoutDynamicRoutingDataSource() {
  88 + FakeDataSource h2 = new FakeDataSource("H2", "jdbc:h2:mem:testdb;MODE=MYSQL");
  89 + mockBeanFactory("dataSource", h2.getDataSource());
  90 +
  91 + assertSame(h2.getDataSource(), JdbcUtils.getDataSource());
  92 + assertEquals(DbType.H2, JdbcUtils.getDbType());
  93 + }
  94 +
  95 + @Test
  96 + public void testGetDbType_transactionConnectionDoesNotBorrowOrClose() throws SQLException {
  97 + FakeDataSource h2 = new FakeDataSource("H2", "jdbc:h2:mem:single_connection");
  98 + mockBeanFactory("dataSource", h2.getDataSource());
  99 + Connection held = h2.getDataSource().getConnection();
  100 + h2.setFailOnConnection(true);
  101 + TransactionSynchronizationManager.bindResource(h2.getDataSource(), new ConnectionHolder(held));
  102 + try {
  103 + assertEquals(DbType.H2, JdbcUtils.getDbType());
  104 + assertEquals(1, h2.getConnectionCount());
  105 + assertEquals(0, h2.closeCount);
  106 + } finally {
  107 + TransactionSynchronizationManager.unbindResource(h2.getDataSource());
  108 + held.close();
  109 + }
  110 + }
  111 +
  112 + @Test
  113 + public void testGetDbType_transactionUsesBoundConnectionWithoutPoisoningRouteCache() throws SQLException {
  114 + FakeDataSource postgresql = new FakeDataSource("PostgreSQL", "jdbc:postgresql://probe/transaction");
  115 + FakeDataSource oracle = new FakeDataSource("Oracle", "jdbc:oracle:thin:@probe");
  116 + DynamicRoutingDataSource routing = new DynamicRoutingDataSource(Collections.emptyList());
  117 + routing.setPrimary("master");
  118 + routing.addDataSource("master", postgresql.getDataSource());
  119 + routing.addDataSource("oracle", oracle.getDataSource());
  120 + mockBeanFactory("dynamicRoutingDataSource", routing);
  121 + Connection held = postgresql.getDataSource().getConnection();
  122 + TransactionSynchronizationManager.bindResource(routing, new ConnectionHolder(held));
  123 + DynamicDataSourceContextHolder.push("oracle");
  124 + try {
  125 + assertEquals(DbType.POSTGRE_SQL, JdbcUtils.getDbType());
  126 + assertEquals(1, postgresql.getConnectionCount());
  127 + assertEquals(0, oracle.getConnectionCount());
  128 + assertEquals(0, postgresql.closeCount);
  129 + } finally {
  130 + TransactionSynchronizationManager.unbindResource(routing);
  131 + held.close();
  132 + }
  133 + assertEquals(DbType.ORACLE, JdbcUtils.getDbType());
  134 + assertEquals(1, oracle.getConnectionCount());
  135 + }
  136 +
  137 + @Test
  138 + public void testGetDbType_unregisteredProductName() {
  139 + // 产品名未登记时,按明确的原生驱动 URL 识别,与 getDbType(url) 的归类一致
  140 + assertEquals(DbType.POSTGRE_SQL, JdbcUtils.getDbType(
  141 + new FakeDataSource("HighGo", "jdbc:highgo://127.0.0.1:5866/highgo").getDataSource()));
  142 + assertEquals(DbType.MARIADB, JdbcUtils.getDbType(
  143 + new FakeDataSource("MariaDB", "jdbc:mariadb://127.0.0.1:3306/a").getDataSource()));
  144 +
  145 + // 完全未识别的产品:明确异常,而不是 NPE;异常信息不包含 URL
  146 + FakeDataSource sqlite = new FakeDataSource("SQLite", "jdbc:sqlite:/tmp/test.db?user=root&password=secret");
  147 + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
  148 + () -> JdbcUtils.getDbType(sqlite.getDataSource()));
  149 + assertTrue(ex.getMessage().contains("SQLite"));
  150 + assertFalse(ex.getMessage().contains("secret"));
  151 + }
  152 +
  153 + @Test
  154 + public void testGetDbType_blankProductName() {
  155 + assertNull(JdbcUtils.getDbType(new FakeDataSource("", "jdbc:mysql://127.0.0.1:3306/a").getDataSource()));
  156 + }
  157 +
  158 + @Test
  159 + public void testGetDbType_failureNotCached() {
  160 + FakeDataSource broken = new FakeDataSource("MySQL", "jdbc:mysql://127.0.0.1:3306/a");
  161 + broken.setFailOnConnection(true);
  162 + assertThrows(IllegalArgumentException.class, () -> JdbcUtils.getDbType(broken.getDataSource()));
  163 + assertThrows(IllegalArgumentException.class, () -> JdbcUtils.getDbType(broken.getDataSource()));
  164 + assertEquals(2, broken.getConnectionCount());
  165 +
  166 + broken.setFailOnConnection(false);
  167 + assertEquals(DbType.MYSQL, JdbcUtils.getDbType(broken.getDataSource()));
  168 + }
  169 +
  170 + private static void mockBeanFactory(String beanName, Object bean) {
  171 + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
  172 + beanFactory.registerSingleton(beanName, bean);
  173 + new SpringUtil().postProcessBeanFactory(beanFactory);
  174 + }
  175 +
  176 + /**
  177 + * 基于 JDK 动态代理的数据源,只提供识别 DB 类型所需的元数据,并记录获取连接的次数
  178 + */
  179 + private static class FakeDataSource implements InvocationHandler {
  180 +
  181 + private final String productName;
  182 + private final String url;
  183 + private final DataSource dataSource;
  184 + private int connectionCount;
  185 + private int closeCount;
  186 + private boolean failOnConnection;
  187 +
  188 + FakeDataSource(String productName, String url) {
  189 + this.productName = productName;
  190 + this.url = url;
  191 + this.dataSource = proxy(DataSource.class);
  192 + }
  193 +
  194 + DataSource getDataSource() {
  195 + return dataSource;
  196 + }
  197 +
  198 + int getConnectionCount() {
  199 + return connectionCount;
  200 + }
  201 +
  202 + void setFailOnConnection(boolean failOnConnection) {
  203 + this.failOnConnection = failOnConnection;
  204 + }
  205 +
  206 + @SuppressWarnings("unchecked")
  207 + private <T> T proxy(Class<T> type) {
  208 + return (T) Proxy.newProxyInstance(getClass().getClassLoader(), new Class<?>[]{type}, this);
  209 + }
  210 +
  211 + @Override
  212 + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  213 + switch (method.getName()) {
  214 + case "hashCode":
  215 + return System.identityHashCode(proxy);
  216 + case "equals":
  217 + return proxy == args[0];
  218 + case "toString":
  219 + return "FakeDataSource(" + productName + ")";
  220 + case "getConnection":
  221 + connectionCount++;
  222 + if (failOnConnection) {
  223 + throw new SQLException("connection refused");
  224 + }
  225 + return proxy(Connection.class);
  226 + case "getMetaData":
  227 + return proxy(DatabaseMetaData.class);
  228 + case "close":
  229 + closeCount++;
  230 + return null;
  231 + case "getDatabaseProductName":
  232 + return productName;
  233 + case "getURL":
  234 + return url;
  235 + default:
  236 + throw new UnsupportedOperationException(method.getName());
  237 + }
  238 + }
  239 +
  240 + }
  241 +
  242 +}
yudao-framework/yudao-spring-boot-starter-mybatis/src/test/java/cn/iocoder/yudao/framework/mybatis/core/util/MyBatisUtilsTest.java
@@ -18,12 +18,57 @@ import static org.junit.jupiter.api.Assertions.assertFalse; @@ -18,12 +18,57 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
18 import static org.junit.jupiter.api.Assertions.assertThrows; 18 import static org.junit.jupiter.api.Assertions.assertThrows;
19 import static org.junit.jupiter.api.Assertions.assertTrue; 19 import static org.junit.jupiter.api.Assertions.assertTrue;
20 20
  21 +// TODO DONE @AI:参考 system user test 的风格;统一准备参数、调用、断言分区。
21 /** 22 /**
22 * {@link MyBatisUtils} 的单元测试 23 * {@link MyBatisUtils} 的单元测试
23 */ 24 */
24 public class MyBatisUtilsTest { 25 public class MyBatisUtilsTest {
25 26
26 @Test 27 @Test
  28 + public void testBuildPage_offset() {
  29 + // 准备参数
  30 + long offset = 3;
  31 + long size = 10;
  32 +
  33 + // 调用
  34 + Page<Object> page = MyBatisUtils.buildPage(offset, size, false);
  35 +
  36 + // 断言
  37 + assertEquals(3L, page.offset());
  38 + assertEquals(10L, page.getSize());
  39 + assertFalse(page.searchCount());
  40 + }
  41 +
  42 + @Test
  43 + public void testBuildPage_negativeOffset() {
  44 + // 准备参数
  45 + long offset = -1;
  46 + long size = 10;
  47 +
  48 + // 调用
  49 + Page<Object> page = MyBatisUtils.buildPage(offset, size, true);
  50 +
  51 + // 断言
  52 + assertEquals(0L, page.offset());
  53 + assertEquals(10L, page.getSize());
  54 + assertTrue(page.searchCount());
  55 + assertEquals(0L, MyBatisUtils.buildPage(0, 10, false).offset());
  56 + }
  57 +
  58 + @Test
  59 + public void testBuildPage_largeOffset() {
  60 + // 准备参数
  61 + long offset = (long) Integer.MAX_VALUE + 3;
  62 +
  63 + // 调用
  64 + Page<Object> page = MyBatisUtils.buildPage(offset, 10, false);
  65 +
  66 + // 断言
  67 + assertEquals(offset, page.offset());
  68 + assertEquals(Long.MAX_VALUE, MyBatisUtils.buildPage(Long.MAX_VALUE, 10, false).offset());
  69 + }
  70 +
  71 + @Test
27 public void testBuildPage_sortingFields() { 72 public void testBuildPage_sortingFields() {
28 // 准备参数 73 // 准备参数
29 PageParam pageParam = new PageParam(); 74 PageParam pageParam = new PageParam();
@@ -94,15 +139,28 @@ public class MyBatisUtilsTest { @@ -94,15 +139,28 @@ public class MyBatisUtilsTest {
94 139
95 @Test 140 @Test
96 public void testOrderDirection() { 141 public void testOrderDirection() {
97 - assertTrue(MyBatisUtils.isAscOrder(SortingField.ORDER_ASC));  
98 - assertFalse(MyBatisUtils.isAscOrder(SortingField.ORDER_DESC));  
99 - assertEquals("ASC", MyBatisUtils.getOrderDirection(SortingField.ORDER_ASC));  
100 - assertEquals("DESC", MyBatisUtils.getOrderDirection(SortingField.ORDER_DESC));  
101 - assertEquals("DESC", MyBatisUtils.getOrderDirection(null)); 142 + // 准备参数
  143 + String ascOrder = SortingField.ORDER_ASC;
  144 + String descOrder = SortingField.ORDER_DESC;
  145 +
  146 + // 调用
  147 + boolean asc = MyBatisUtils.isAscOrder(ascOrder);
  148 + boolean desc = MyBatisUtils.isAscOrder(descOrder);
  149 + String ascDirection = MyBatisUtils.getOrderDirection(ascOrder);
  150 + String descDirection = MyBatisUtils.getOrderDirection(descOrder);
  151 + String defaultDirection = MyBatisUtils.getOrderDirection(null);
  152 +
  153 + // 断言
  154 + assertTrue(asc);
  155 + assertFalse(desc);
  156 + assertEquals("ASC", ascDirection);
  157 + assertEquals("DESC", descDirection);
  158 + assertEquals("DESC", defaultDirection);
102 } 159 }
103 160
104 @Test 161 @Test
105 public void testFindInSet() { 162 public void testFindInSet() {
  163 + // 调用,并断言各数据库模板保持原有参数序号和列名
106 assertEquals("FIND_IN_SET({0}, websites) <> 0", 164 assertEquals("FIND_IN_SET({0}, websites) <> 0",
107 MyBatisUtils.findInSet(DbType.MYSQL, "websites", 0)); 165 MyBatisUtils.findInSet(DbType.MYSQL, "websites", 0));
108 assertEquals("POSITION(',' || CAST({0} AS VARCHAR) || ',' IN ',' || websites || ',') > 0", 166 assertEquals("POSITION(',' || CAST({0} AS VARCHAR) || ',' IN ',' || websites || ',') > 0",
@@ -121,6 +179,7 @@ public class MyBatisUtilsTest { @@ -121,6 +179,7 @@ public class MyBatisUtilsTest {
121 179
122 @Test 180 @Test
123 public void testFindInSet_invalidColumnName() { 181 public void testFindInSet_invalidColumnName() {
  182 + // 调用,并断言异常
124 assertThrows(IllegalArgumentException.class, 183 assertThrows(IllegalArgumentException.class,
125 () -> MyBatisUtils.findInSet(DbType.MYSQL, "websites;drop table system_tenant", 0)); 184 () -> MyBatisUtils.findInSet(DbType.MYSQL, "websites;drop table system_tenant", 0));
126 assertThrows(IllegalArgumentException.class, 185 assertThrows(IllegalArgumentException.class,
@@ -129,12 +188,14 @@ public class MyBatisUtilsTest { @@ -129,12 +188,14 @@ public class MyBatisUtilsTest {
129 188
130 @Test 189 @Test
131 public void testFindInSet_invalidParamIndex() { 190 public void testFindInSet_invalidParamIndex() {
  191 + // 调用,并断言异常
132 assertThrows(IllegalArgumentException.class, 192 assertThrows(IllegalArgumentException.class,
133 () -> MyBatisUtils.findInSet(DbType.MYSQL, "websites", -1)); 193 () -> MyBatisUtils.findInSet(DbType.MYSQL, "websites", -1));
134 } 194 }
135 195
136 @Test 196 @Test
137 public void testFindInSet_multipleValues() { 197 public void testFindInSet_multipleValues() {
  198 + // 调用,并断言多个值使用独立参数序号;空集合拒绝生成条件
138 assertEquals("FIND_IN_SET({0}, tag_ids) <> 0 OR FIND_IN_SET({1}, tag_ids) <> 0", 199 assertEquals("FIND_IN_SET({0}, tag_ids) <> 0 OR FIND_IN_SET({1}, tag_ids) <> 0",
139 MyBatisUtils.findInSet(DbType.MYSQL, "tag_ids", Arrays.asList(1L, 2L))); 200 MyBatisUtils.findInSet(DbType.MYSQL, "tag_ids", Arrays.asList(1L, 2L)));
140 assertThrows(IllegalArgumentException.class, 201 assertThrows(IllegalArgumentException.class,
yudao-framework/yudao-spring-boot-starter-mybatis/src/test/java/cn/iocoder/yudao/framework/mybatis/core/util/SqlDialectUtilsTest.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.mybatis.core.util;
  2 +
  3 +import cn.hutool.core.exceptions.UtilException;
  4 +import cn.hutool.extra.spring.SpringUtil;
  5 +import cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils.Dialect;
  6 +import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
  7 +import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
  8 +import net.sf.jsqlparser.parser.CCJSqlParserUtil;
  9 +import org.apache.ibatis.mapping.BoundSql;
  10 +import org.apache.ibatis.mapping.ParameterMapping;
  11 +import org.apache.ibatis.mapping.SqlSource;
  12 +import org.apache.ibatis.scripting.xmltags.XMLLanguageDriver;
  13 +import org.apache.ibatis.session.Configuration;
  14 +import org.junit.jupiter.api.AfterEach;
  15 +import org.junit.jupiter.api.BeforeEach;
  16 +import org.junit.jupiter.api.Test;
  17 +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
  18 +import org.springframework.beans.factory.support.DefaultListableBeanFactory;
  19 +import org.springframework.jdbc.datasource.ConnectionHolder;
  20 +import org.springframework.transaction.support.TransactionSynchronizationManager;
  21 +
  22 +import javax.sql.DataSource;
  23 +import java.lang.reflect.InvocationHandler;
  24 +import java.lang.reflect.Method;
  25 +import java.lang.reflect.Proxy;
  26 +import java.sql.Connection;
  27 +import java.sql.DatabaseMetaData;
  28 +import java.sql.SQLException;
  29 +import java.time.LocalDateTime;
  30 +import java.util.Collections;
  31 +import java.util.HashMap;
  32 +import java.util.List;
  33 +import java.util.Map;
  34 +import java.util.function.BiFunction;
  35 +
  36 +import static org.junit.jupiter.api.Assertions.*;
  37 +
  38 +/**
  39 + * {@link SqlDialectUtils} 的单元测试
  40 + *
  41 + * @author 芋道源码
  42 + */
  43 +public class SqlDialectUtilsTest {
  44 +
  45 + private static final String P = "${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@";
  46 +
  47 + private ConfigurableListableBeanFactory originBeanFactory;
  48 +
  49 + @BeforeEach
  50 + public void setUp() {
  51 + try {
  52 + originBeanFactory = SpringUtil.getConfigurableBeanFactory();
  53 + } catch (UtilException e) {
  54 + originBeanFactory = null;
  55 + }
  56 + }
  57 +
  58 + @AfterEach
  59 + public void tearDown() {
  60 + DynamicDataSourceContextHolder.clear();
  61 + new SpringUtil().postProcessBeanFactory(originBeanFactory);
  62 + }
  63 +
  64 + // ========== SQL 片段 ==========
  65 +
  66 + @Test
  67 + public void testFormatDate() {
  68 + assertEquals("DATE_FORMAT(create_time, '%Y-%m-%d')", SqlDialectUtils.formatDate(Dialect.ORIGINAL, "create_time"));
  69 + assertEquals("TO_CHAR(t.create_time, 'YYYY-MM-DD')", SqlDialectUtils.formatDate(Dialect.POSTGRESQL, "t.create_time"));
  70 + assertEquals("TO_CHAR(create_time, 'YYYY-MM-DD')", SqlDialectUtils.formatDate(Dialect.ORACLE, "create_time"));
  71 + assertEquals("CONVERT(varchar(10), create_time, 23)", SqlDialectUtils.formatDate(Dialect.SQLSERVER, "create_time"));
  72 + }
  73 +
  74 + @Test
  75 + public void testFormatYearMonth() {
  76 + assertEquals("DATE_FORMAT(order_date, '%Y%m')", SqlDialectUtils.formatYearMonth(Dialect.ORIGINAL, "order_date"));
  77 + assertEquals("TO_CHAR(order_date, 'YYYYMM')", SqlDialectUtils.formatYearMonth(Dialect.POSTGRESQL, "order_date"));
  78 + assertEquals("TO_CHAR(order_date, 'YYYYMM')", SqlDialectUtils.formatYearMonth(Dialect.ORACLE, "order_date"));
  79 + assertEquals("CONVERT(varchar(6), order_date, 112)", SqlDialectUtils.formatYearMonth(Dialect.SQLSERVER, "order_date"));
  80 + }
  81 +
  82 + @Test
  83 + public void testFormatYearDashMonth() {
  84 + assertEquals("CONCAT(EXTRACT(YEAR FROM pay_time), '-', LPAD(CONCAT(EXTRACT(MONTH FROM pay_time)), 2, '0'))",
  85 + SqlDialectUtils.formatYearDashMonth(Dialect.ORIGINAL, "pay_time"));
  86 + assertEquals("TO_CHAR(pay_time, 'YYYY-MM')", SqlDialectUtils.formatYearDashMonth(Dialect.POSTGRESQL, "pay_time"));
  87 + assertEquals("TO_CHAR(pay_time, 'YYYY-MM')", SqlDialectUtils.formatYearDashMonth(Dialect.ORACLE, "pay_time"));
  88 + assertEquals("CONVERT(varchar(7), pay_time, 120)", SqlDialectUtils.formatYearDashMonth(Dialect.SQLSERVER, "pay_time"));
  89 + }
  90 +
  91 + @Test
  92 + public void testDate() {
  93 + assertEquals("DATE(send_time)", SqlDialectUtils.date(Dialect.ORIGINAL, "send_time"));
  94 + assertEquals("TO_CHAR(send_time, 'YYYY-MM-DD')", SqlDialectUtils.date(Dialect.POSTGRESQL, "send_time"));
  95 + assertEquals("TO_CHAR(send_time, 'YYYY-MM-DD')", SqlDialectUtils.date(Dialect.ORACLE, "send_time"));
  96 + assertEquals("CONVERT(varchar(10), send_time, 23)", SqlDialectUtils.date(Dialect.SQLSERVER, "send_time"));
  97 + }
  98 +
  99 + @Test
  100 + public void testCastDateText() {
  101 + assertEquals("CAST(create_time AS DATE)", SqlDialectUtils.castDateText(Dialect.ORIGINAL, "create_time"));
  102 + assertEquals("TO_CHAR(create_time, 'YYYY-MM-DD')", SqlDialectUtils.castDateText(Dialect.POSTGRESQL, "create_time"));
  103 + assertEquals("TO_CHAR(create_time, 'YYYY-MM-DD')", SqlDialectUtils.castDateText(Dialect.ORACLE, "create_time"));
  104 + assertEquals("CONVERT(varchar(10), create_time, 23)", SqlDialectUtils.castDateText(Dialect.SQLSERVER, "create_time"));
  105 + }
  106 +
  107 + @Test
  108 + public void testCastDate() {
  109 + assertEquals("CAST(create_time AS DATE)", SqlDialectUtils.castDate(Dialect.ORIGINAL, "create_time"));
  110 + assertEquals("CAST(create_time AS DATE)", SqlDialectUtils.castDate(Dialect.POSTGRESQL, "create_time"));
  111 + assertEquals("TRUNC(create_time)", SqlDialectUtils.castDate(Dialect.ORACLE, "create_time"));
  112 + assertEquals("CAST(create_time AS DATE)", SqlDialectUtils.castDate(Dialect.SQLSERVER, "create_time"));
  113 + }
  114 +
  115 + @Test
  116 + public void testMonth() {
  117 + assertEquals("MONTH(order_date)", SqlDialectUtils.month(Dialect.ORIGINAL, "order_date"));
  118 + assertEquals("CAST(EXTRACT(MONTH FROM order_date) AS INTEGER)", SqlDialectUtils.month(Dialect.POSTGRESQL, "order_date"));
  119 + assertEquals("EXTRACT(MONTH FROM order_date)", SqlDialectUtils.month(Dialect.ORACLE, "order_date"));
  120 + assertEquals("MONTH(order_date)", SqlDialectUtils.month(Dialect.SQLSERVER, "order_date"));
  121 + }
  122 +
  123 + @Test
  124 + public void testYear() {
  125 + assertEquals("YEAR(return_time)", SqlDialectUtils.year(Dialect.ORIGINAL, "return_time"));
  126 + assertEquals("CAST(EXTRACT(YEAR FROM return_time) AS INTEGER)", SqlDialectUtils.year(Dialect.POSTGRESQL, "return_time"));
  127 + assertEquals("EXTRACT(YEAR FROM return_time)", SqlDialectUtils.year(Dialect.ORACLE, "return_time"));
  128 + assertEquals("YEAR(return_time)", SqlDialectUtils.year(Dialect.SQLSERVER, "return_time"));
  129 + }
  130 +
  131 + @Test
  132 + public void testDayOfMonth() {
  133 + assertEquals("DAYOFMONTH(birthday)", SqlDialectUtils.dayOfMonth(Dialect.ORIGINAL, "birthday"));
  134 + assertEquals("CAST(EXTRACT(DAY FROM birthday) AS INTEGER)", SqlDialectUtils.dayOfMonth(Dialect.POSTGRESQL, "birthday"));
  135 + assertEquals("EXTRACT(DAY FROM birthday)", SqlDialectUtils.dayOfMonth(Dialect.ORACLE, "birthday"));
  136 + assertEquals("DAY(birthday)", SqlDialectUtils.dayOfMonth(Dialect.SQLSERVER, "birthday"));
  137 + }
  138 +
  139 + @Test
  140 + public void testCustomerDealCycle() {
  141 + String original = "COALESCE(TRUNCATE(AVG(TIMESTAMPDIFF(DAY, customer.create_time, contract.order_date)), 1), 0)";
  142 + assertEquals(original, SqlDialectUtils.customerDealCycle(Dialect.ORIGINAL, "customer.create_time", "contract.order_date"));
  143 + // PostgreSQL:两个 TIMESTAMP 相减为 interval,EXTRACT(DAY) 即完整天数(向零截断、保留负号),TRUNC 截断保留 1 位
  144 + assertEquals("COALESCE(TRUNC(AVG(CAST(EXTRACT(DAY FROM (CAST(contract.order_date AS TIMESTAMP) - "
  145 + + "CAST(customer.create_time AS TIMESTAMP))) AS NUMERIC)), 1), 0)",
  146 + SqlDialectUtils.customerDealCycle(Dialect.POSTGRESQL, "customer.create_time", "contract.order_date"));
  147 + // Oracle、SQL Server:完整天数表达式依赖实际列类型,尚未验收,保持原有片段
  148 + assertEquals(original, SqlDialectUtils.customerDealCycle(Dialect.ORACLE, "customer.create_time", "contract.order_date"));
  149 + assertEquals(original, SqlDialectUtils.customerDealCycle(Dialect.SQLSERVER, "customer.create_time", "contract.order_date"));
  150 + }
  151 +
  152 + @Test
  153 + public void testFromDual() {
  154 + assertEquals("", SqlDialectUtils.fromDual(Dialect.ORIGINAL));
  155 + assertEquals("", SqlDialectUtils.fromDual(Dialect.POSTGRESQL));
  156 + assertEquals("FROM DUAL", SqlDialectUtils.fromDual(Dialect.ORACLE));
  157 + assertEquals("", SqlDialectUtils.fromDual(Dialect.SQLSERVER));
  158 + }
  159 +
  160 + @Test
  161 + public void testInvalidColumnName() {
  162 + List<BiFunction<Dialect, String, String>> fragments = List.of(SqlDialectUtils::formatDate,
  163 + SqlDialectUtils::formatYearMonth, SqlDialectUtils::formatYearDashMonth, SqlDialectUtils::date,
  164 + SqlDialectUtils::castDateText, SqlDialectUtils::castDate, SqlDialectUtils::month,
  165 + SqlDialectUtils::year, SqlDialectUtils::dayOfMonth);
  166 + for (BiFunction<Dialect, String, String> fragment : fragments) {
  167 + for (Dialect dialect : Dialect.values()) {
  168 + assertThrows(IllegalArgumentException.class, () -> fragment.apply(dialect, "create_time) OR (1=1"));
  169 + assertThrows(IllegalArgumentException.class, () -> fragment.apply(dialect, "create_time;drop table t"));
  170 + assertThrows(IllegalArgumentException.class, () -> fragment.apply(dialect, "'2026-01-01'"));
  171 + assertThrows(IllegalArgumentException.class, () -> fragment.apply(dialect, ""));
  172 + assertThrows(IllegalArgumentException.class, () -> fragment.apply(dialect, null));
  173 + }
  174 + }
  175 + for (Dialect dialect : Dialect.values()) {
  176 + assertThrows(IllegalArgumentException.class,
  177 + () -> SqlDialectUtils.customerDealCycle(dialect, "customer.create_time", "NOW()"));
  178 + assertThrows(IllegalArgumentException.class,
  179 + () -> SqlDialectUtils.customerDealCycle(dialect, "1) - (1", "contract.order_date"));
  180 + }
  181 + }
  182 +
  183 + /**
  184 + * MyBatis Plus 租户等插件会使用 JSqlParser 解析最终 SQL,所有方言的片段均需可被解析
  185 + */
  186 + @Test
  187 + public void testFragments_parsedByJSqlParser() throws Exception {
  188 + for (Dialect dialect : Dialect.values()) {
  189 + List<String> expressions = List.of(
  190 + SqlDialectUtils.formatDate(dialect, "t.create_time"),
  191 + SqlDialectUtils.formatYearMonth(dialect, "t.create_time"),
  192 + SqlDialectUtils.formatYearDashMonth(dialect, "t.create_time"),
  193 + SqlDialectUtils.date(dialect, "t.create_time"),
  194 + SqlDialectUtils.castDateText(dialect, "t.create_time"),
  195 + SqlDialectUtils.castDate(dialect, "t.create_time"),
  196 + SqlDialectUtils.month(dialect, "t.create_time"),
  197 + SqlDialectUtils.year(dialect, "t.create_time"),
  198 + SqlDialectUtils.dayOfMonth(dialect, "t.create_time"));
  199 + for (String expression : expressions) {
  200 + String sql = "SELECT " + expression + " AS statDate, COUNT(1) AS total FROM crm_customer t"
  201 + + " WHERE t.create_time >= ? GROUP BY " + expression + " ORDER BY statDate";
  202 + assertNotNull(CCJSqlParserUtil.parse(sql), dialect + " " + expression);
  203 + }
  204 + String dealCycleSql = "SELECT customer.owner_user_id, "
  205 + + SqlDialectUtils.customerDealCycle(dialect, "customer.create_time", "contract.order_date")
  206 + + " AS customer_deal_cycle FROM crm_customer customer"
  207 + + " LEFT JOIN crm_contract contract ON contract.customer_id = customer.id"
  208 + + " GROUP BY customer.owner_user_id";
  209 + assertNotNull(CCJSqlParserUtil.parse(dealCycleSql), dialect + " customerDealCycle");
  210 + String summarySql = "SELECT (SELECT COUNT(1) FROM ai1_agent) AS agentCount, "
  211 + + "(SELECT COUNT(1) FROM ai1_model) AS modelCount " + SqlDialectUtils.fromDual(dialect);
  212 + assertNotNull(CCJSqlParserUtil.parse(summarySql), dialect + " fromDual");
  213 + }
  214 + }
  215 +
  216 + // ========== 行数限制 ==========
  217 +
  218 + @Test
  219 + public void testLimitRows() {
  220 + assertEquals("LIMIT 1", SqlDialectUtils.limitRows(Dialect.ORIGINAL, 1, false));
  221 + assertEquals("LIMIT 10", SqlDialectUtils.limitRows(Dialect.POSTGRESQL, 10, true));
  222 + assertEquals("FETCH FIRST 1 ROWS ONLY", SqlDialectUtils.limitRows(Dialect.ORACLE, 1, false));
  223 + assertEquals("FETCH FIRST 10 ROWS ONLY", SqlDialectUtils.limitRows(Dialect.ORACLE, 10, true));
  224 + // SQL Server:OFFSET 必须跟在 ORDER BY 之后,无排序时补充常量排序
  225 + assertEquals("OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY", SqlDialectUtils.limitRows(Dialect.SQLSERVER, 10, true));
  226 + assertEquals("ORDER BY (SELECT NULL) OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY",
  227 + SqlDialectUtils.limitRows(Dialect.SQLSERVER, 1, false));
  228 + }
  229 +
  230 + @Test
  231 + public void testLimitRows_zeroAndNegative() {
  232 + assertEquals("LIMIT 0", SqlDialectUtils.limitRows(Dialect.ORIGINAL, 0, true));
  233 + assertEquals("LIMIT 0", SqlDialectUtils.limitRows(Dialect.POSTGRESQL, 0, true));
  234 + assertEquals("FETCH FIRST 0 ROWS ONLY", SqlDialectUtils.limitRows(Dialect.ORACLE, 0, true));
  235 + // SQL Server 的 FETCH 不支持 0 行
  236 + assertThrows(IllegalArgumentException.class, () -> SqlDialectUtils.limitRows(Dialect.SQLSERVER, 0, true));
  237 + for (Dialect dialect : Dialect.values()) {
  238 + assertThrows(IllegalArgumentException.class, () -> SqlDialectUtils.limitRows(dialect, -1, true));
  239 + }
  240 + }
  241 +
  242 + @Test
  243 + public void testLimitParam() {
  244 + assertEquals("LIMIT #{limit}", SqlDialectUtils.limitParam(Dialect.ORIGINAL, "limit", true));
  245 + assertEquals("LIMIT #{reqVO.limit}", SqlDialectUtils.limitParam(Dialect.POSTGRESQL, "reqVO.limit", true));
  246 + assertEquals("FETCH FIRST #{limit} ROWS ONLY", SqlDialectUtils.limitParam(Dialect.ORACLE, "limit", false));
  247 + assertEquals("OFFSET 0 ROWS FETCH NEXT #{limit} ROWS ONLY", SqlDialectUtils.limitParam(Dialect.SQLSERVER, "limit", true));
  248 + assertEquals("ORDER BY (SELECT NULL) OFFSET 0 ROWS FETCH NEXT #{limit} ROWS ONLY",
  249 + SqlDialectUtils.limitParam(Dialect.SQLSERVER, "limit", false));
  250 + // 参数名只能是标识符或属性路径
  251 + for (Dialect dialect : Dialect.values()) {
  252 + assertThrows(IllegalArgumentException.class, () -> SqlDialectUtils.limitParam(dialect, "limit} OR 1=1 #{a", true));
  253 + assertThrows(IllegalArgumentException.class, () -> SqlDialectUtils.limitParam(dialect, "1", true));
  254 + assertThrows(IllegalArgumentException.class, () -> SqlDialectUtils.limitParam(dialect, "", true));
  255 + assertThrows(IllegalArgumentException.class, () -> SqlDialectUtils.limitParam(dialect, null, true));
  256 + }
  257 + }
  258 +
  259 + // ========== 分批删除 ==========
  260 +
  261 + @Test
  262 + public void testDeleteBatchByLessThan() {
  263 + // 原有语句保持不变
  264 + assertEquals("DELETE FROM infra_job_log WHERE create_time < #{createTime} LIMIT #{limit}",
  265 + SqlDialectUtils.deleteBatchByLessThan(Dialect.ORIGINAL, "infra_job_log", "create_time", "createTime", "limit"));
  266 + assertEquals("DELETE FROM infra_job_log WHERE id IN (SELECT id FROM infra_job_log"
  267 + + " WHERE create_time < #{createTime} LIMIT #{limit})",
  268 + SqlDialectUtils.deleteBatchByLessThan(Dialect.POSTGRESQL, "infra_job_log", "create_time", "createTime", "limit"));
  269 + assertEquals("DELETE FROM system_oauth2_access_token WHERE expires_time < #{expiresTime} AND ROWNUM <= #{limit}",
  270 + SqlDialectUtils.deleteBatchByLessThan(Dialect.ORACLE, "system_oauth2_access_token", "expires_time", "expiresTime", "limit"));
  271 + assertEquals("DELETE FROM system_oauth2_access_token WHERE id IN (SELECT TOP (#{limit}) id FROM system_oauth2_access_token"
  272 + + " WHERE expires_time < #{expiresTime})",
  273 + SqlDialectUtils.deleteBatchByLessThan(Dialect.SQLSERVER, "system_oauth2_access_token", "expires_time", "expiresTime", "limit"));
  274 + }
  275 +
  276 + @Test
  277 + public void testDeleteBatchByLessThan_invalidName() {
  278 + for (Dialect dialect : Dialect.values()) {
  279 + assertThrows(IllegalArgumentException.class, () -> SqlDialectUtils.deleteBatchByLessThan(dialect,
  280 + "infra_job_log; DROP TABLE t", "create_time", "createTime", "limit"));
  281 + assertThrows(IllegalArgumentException.class, () -> SqlDialectUtils.deleteBatchByLessThan(dialect,
  282 + "infra_job_log", "create_time) OR (1=1", "createTime", "limit"));
  283 + assertThrows(IllegalArgumentException.class, () -> SqlDialectUtils.deleteBatchByLessThan(dialect,
  284 + "infra_job_log", "create_time", "createTime} OR 1=1 OR #{a", "limit"));
  285 + assertThrows(IllegalArgumentException.class, () -> SqlDialectUtils.deleteBatchByLessThan(dialect,
  286 + "infra_job_log", "create_time", "createTime", null));
  287 + }
  288 + }
  289 +
  290 + /**
  291 + * MyBatis Plus 租户、数据权限插件会使用 JSqlParser 解析最终 SQL,所有方言的语句均需可被解析
  292 + *
  293 + * 注意:JSqlParser 5.2 不支持 DELETE TOP (?),因此 SQL Server 使用主键子查询
  294 + */
  295 + @Test
  296 + public void testLimitAndDelete_parsedByJSqlParser() throws Exception {
  297 + for (Dialect dialect : Dialect.values()) {
  298 + String deleteSql = SqlDialectUtils.deleteBatchByLessThan(dialect, "infra_job_log", "create_time", "createTime", "limit");
  299 + assertNotNull(CCJSqlParserUtil.parse(bind(deleteSql)), dialect + " " + deleteSql);
  300 +
  301 + String orderedSql = "SELECT user_id AS userId, COUNT(*) AS messageCount FROM im_private_message"
  302 + + " WHERE send_time >= ? GROUP BY user_id ORDER BY messageCount DESC "
  303 + + SqlDialectUtils.limitParam(dialect, "limit", true);
  304 + assertNotNull(CCJSqlParserUtil.parse(bind(orderedSql)), dialect + " " + orderedSql);
  305 + String unorderedSql = "SELECT * FROM hrm_salary_option WHERE code = ? "
  306 + + SqlDialectUtils.limitRows(dialect, 1, false);
  307 + assertNotNull(CCJSqlParserUtil.parse(unorderedSql), dialect + " " + unorderedSql);
  308 + }
  309 + }
  310 +
  311 + // ========== 方言识别 ==========
  312 +
  313 + @Test
  314 + public void testResolveDialect_accepted() throws SQLException {
  315 + assertEquals(Dialect.POSTGRESQL, resolve("PostgreSQL", "jdbc:postgresql://127.0.0.1:5432/ruoyi-vue-pro", 17));
  316 + assertEquals(Dialect.POSTGRESQL, resolve("PostgreSQL", "jdbc:p6spy:postgresql://127.0.0.1:5432/ruoyi-vue-pro", 17));
  317 + assertEquals(Dialect.ORACLE, resolve("Oracle", "jdbc:oracle:thin:@127.0.0.1:1521:xe", 21));
  318 + assertEquals(Dialect.SQLSERVER, resolve("Microsoft SQL Server", "jdbc:sqlserver://127.0.0.1:1433", 16));
  319 + assertEquals(Dialect.SQLSERVER, resolve("Microsoft SQL Server", "jdbc:sqlserver://127.0.0.1:1433", 11));
  320 + }
  321 +
  322 + @Test
  323 + public void testResolveDialect_original() throws SQLException {
  324 + // MySQL 族
  325 + assertEquals(Dialect.ORIGINAL, resolve("MySQL", "jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro", 8));
  326 + assertEquals(Dialect.ORIGINAL, resolve("MariaDB", "jdbc:mariadb://127.0.0.1:3306/ruoyi-vue-pro", 11));
  327 + assertEquals(Dialect.ORIGINAL, resolve("OceanBase", "jdbc:oceanbase://127.0.0.1:2881/ruoyi-vue-pro", 4));
  328 + // 已有旧路径:H2、达梦、神通、人大金仓
  329 + assertEquals(Dialect.ORIGINAL, resolve("H2", "jdbc:h2:mem:testdb;MODE=MYSQL", 2));
  330 + assertEquals(Dialect.ORIGINAL, resolve("DM DBMS", "jdbc:dm://127.0.0.1:5236", 8));
  331 + assertEquals(Dialect.ORIGINAL, resolve("OSCAR", "jdbc:oscar://127.0.0.1:2003/osrdb", 7));
  332 + assertEquals(Dialect.ORIGINAL, resolve("KingbaseES", "jdbc:kingbase8://127.0.0.1:54321/test", 8));
  333 + // 未验收的 PostgreSQL 变种:原生产品名
  334 + assertEquals(Dialect.ORIGINAL, resolve("openGauss", "jdbc:opengauss://127.0.0.1:5432/test", 5));
  335 + assertEquals(Dialect.ORIGINAL, resolve("GaussDB", "jdbc:gaussdb://127.0.0.1:8000/test", 5));
  336 + // 未验收的 PostgreSQL 变种:产品名未登记或与 PostgreSQL 相同,按原生驱动 URL 识别
  337 + assertEquals(Dialect.ORIGINAL, resolve("HighGo", "jdbc:highgo://127.0.0.1:5866/highgo", 6));
  338 + assertEquals(Dialect.ORIGINAL, resolve("PostgreSQL", "jdbc:highgo://127.0.0.1:5866/highgo", 6));
  339 + assertEquals(Dialect.ORIGINAL, resolve("PostgreSQL", "jdbc:opengauss://127.0.0.1:5432/test", 9));
  340 + assertEquals(Dialect.ORIGINAL, resolve("PostgreSQL", "jdbc:mogdb://127.0.0.1:5432/test", 9));
  341 + assertEquals(Dialect.ORIGINAL, resolve("PostgreSQL", "jdbc:gaussdb://127.0.0.1:8000/test", 9));
  342 + assertEquals(Dialect.ORIGINAL, resolve("PostgreSQL", "jdbc:kingbase8://127.0.0.1:54321/test", 12));
  343 + // 未验收的 SQL Server 版本
  344 + assertEquals(Dialect.ORIGINAL, resolve("Microsoft SQL Server", "jdbc:sqlserver://127.0.0.1:1433", 10));
  345 + assertEquals(Dialect.ORIGINAL, resolve("Microsoft SQL Server 2005", "jdbc:jtds:sqlserver://127.0.0.1:1433", 9));
  346 + }
  347 +
  348 + @Test
  349 + public void testResolveDialect_unsupported() {
  350 + String secretUrl = "jdbc:sqlite:/tmp/test.db?user=root&password=secret";
  351 + IllegalStateException ex = assertThrows(IllegalStateException.class, () -> resolve("SQLite", secretUrl, 3));
  352 + assertTrue(ex.getMessage().contains("SQLite"));
  353 + assertFalse(ex.getMessage().contains("secret"));
  354 + assertFalse(ex.getMessage().contains("jdbc:"));
  355 + // 产品名为 PostgreSQL,但 URL 不是 PostgreSQL 或已识别的变种
  356 + assertThrows(IllegalStateException.class, () -> resolve("PostgreSQL", "jdbc:vastbase://127.0.0.1:5432/test", 9));
  357 + // 产品名为空
  358 + assertThrows(IllegalStateException.class, () -> resolve(null, "jdbc:unknown://127.0.0.1/test", 1));
  359 + }
  360 +
  361 + @Test
  362 + public void testGetDialect_cachedPerDataSource() {
  363 + FakeDataSource postgresql = new FakeDataSource("PostgreSQL", "jdbc:postgresql://127.0.0.1:5432/a", 17);
  364 + FakeDataSource mysql = new FakeDataSource("MySQL", "jdbc:mysql://127.0.0.1:3306/a", 8);
  365 +
  366 + assertEquals(Dialect.POSTGRESQL, SqlDialectUtils.getDialect(postgresql.getDataSource()));
  367 + assertEquals(Dialect.ORIGINAL, SqlDialectUtils.getDialect(mysql.getDataSource()));
  368 + assertEquals(Dialect.POSTGRESQL, SqlDialectUtils.getDialect(postgresql.getDataSource()));
  369 + assertEquals(Dialect.ORIGINAL, SqlDialectUtils.getDialect(mysql.getDataSource()));
  370 + assertEquals(1, postgresql.getConnectionCount());
  371 + assertEquals(1, mysql.getConnectionCount());
  372 + }
  373 +
  374 + @Test
  375 + public void testGetDialect_failureNotCached() {
  376 + FakeDataSource broken = new FakeDataSource("PostgreSQL", "jdbc:postgresql://127.0.0.1:5432/a", 17);
  377 + broken.setFailOnConnection(true);
  378 + assertThrows(IllegalStateException.class, () -> SqlDialectUtils.getDialect(broken.getDataSource()));
  379 + assertThrows(IllegalStateException.class, () -> SqlDialectUtils.getDialect(broken.getDataSource()));
  380 + assertEquals(2, broken.getConnectionCount());
  381 +
  382 + // 恢复后可以正常识别
  383 + broken.setFailOnConnection(false);
  384 + assertEquals(Dialect.POSTGRESQL, SqlDialectUtils.getDialect(broken.getDataSource()));
  385 + }
  386 +
  387 + @Test
  388 + public void testFormatDate_transactionConnectionDoesNotBorrowOrClose() throws SQLException {
  389 + FakeDataSource h2 = new FakeDataSource("H2", "jdbc:h2:mem:single_connection", 2);
  390 + Connection held = h2.getDataSource().getConnection();
  391 + h2.setFailOnConnection(true); // 当前事务已占用唯一连接,任何额外借用都会失败
  392 + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
  393 + beanFactory.registerSingleton("dataSource", h2.getDataSource());
  394 + new SpringUtil().postProcessBeanFactory(beanFactory);
  395 + TransactionSynchronizationManager.bindResource(h2.getDataSource(), new ConnectionHolder(held));
  396 + try {
  397 + assertEquals("DATE_FORMAT(create_time, '%Y-%m-%d')", SqlDialectUtils.formatDate("create_time"));
  398 + assertEquals(1, h2.getConnectionCount());
  399 + assertEquals(0, h2.closeCount);
  400 + } finally {
  401 + TransactionSynchronizationManager.unbindResource(h2.getDataSource());
  402 + held.close();
  403 + }
  404 + }
  405 +
  406 + @Test
  407 + public void testGetDialect_transactionUsesBoundConnectionWithoutPoisoningRouteCache() throws SQLException {
  408 + FakeDataSource postgresql = new FakeDataSource("PostgreSQL", "jdbc:postgresql://probe/transaction", 17);
  409 + FakeDataSource mysql = new FakeDataSource("MySQL", "jdbc:mysql://probe/other", 8);
  410 + mockDynamicRoutingDataSource(Map.of("master", postgresql, "mysql", mysql));
  411 + DynamicRoutingDataSource routing = SpringUtil.getBean(DynamicRoutingDataSource.class);
  412 + Connection held = postgresql.getDataSource().getConnection();
  413 + TransactionSynchronizationManager.bindResource(routing, new ConnectionHolder(held));
  414 + DynamicDataSourceContextHolder.push("mysql");
  415 + try {
  416 + // Spring 事务的执行连接仍为 PG,切换线程路由键不会改变它
  417 + assertEquals("CAST(EXTRACT(MONTH FROM birthday) AS INTEGER)", SqlDialectUtils.month("birthday"));
  418 + assertEquals(1, postgresql.getConnectionCount());
  419 + assertEquals(0, mysql.getConnectionCount());
  420 + assertEquals(0, postgresql.closeCount);
  421 + } finally {
  422 + TransactionSynchronizationManager.unbindResource(routing);
  423 + held.close();
  424 + }
  425 + // 事务结束后该路由真正使用 MySQL,不能命中错误的 PG 缓存
  426 + assertEquals("MONTH(birthday)", SqlDialectUtils.month("birthday"));
  427 + assertEquals(1, mysql.getConnectionCount());
  428 + }
  429 +
  430 + @Test
  431 + public void testFormatDate_followsDynamicRouting() {
  432 + FakeDataSource postgresql = new FakeDataSource("PostgreSQL", "jdbc:postgresql://127.0.0.1:5432/a", 17);
  433 + FakeDataSource mysql = new FakeDataSource("MySQL", "jdbc:mysql://127.0.0.1:3306/a", 8);
  434 + FakeDataSource oracle = new FakeDataSource("Oracle", "jdbc:oracle:thin:@127.0.0.1:1521:xe", 21);
  435 + mockDynamicRoutingDataSource(Map.of("master", postgresql, "mysql", mysql, "oracle", oracle));
  436 +
  437 + for (int i = 0; i < 3; i++) {
  438 + // 未切换时为主库
  439 + assertEquals("TO_CHAR(create_time, 'YYYY-MM-DD')", SqlDialectUtils.formatDate("create_time"));
  440 + DynamicDataSourceContextHolder.push("mysql");
  441 + assertEquals("DATE_FORMAT(create_time, '%Y-%m-%d')", SqlDialectUtils.formatDate("create_time"));
  442 + DynamicDataSourceContextHolder.push("oracle");
  443 + assertEquals("TO_CHAR(create_time, 'YYYY-MM-DD')", SqlDialectUtils.formatDate("create_time"));
  444 + assertEquals("FROM DUAL", SqlDialectUtils.fromDual());
  445 + DynamicDataSourceContextHolder.poll();
  446 + assertEquals("", SqlDialectUtils.fromDual());
  447 + DynamicDataSourceContextHolder.poll();
  448 + }
  449 + assertEquals(1, postgresql.getConnectionCount());
  450 + assertEquals(1, mysql.getConnectionCount());
  451 + assertEquals(1, oracle.getConnectionCount());
  452 + }
  453 +
  454 + @Test
  455 + public void testBoundSql_xmlScript() {
  456 + FakeDataSource postgresql = new FakeDataSource("PostgreSQL", "jdbc:postgresql://127.0.0.1:5432/a", 17);
  457 + FakeDataSource mysql = new FakeDataSource("MySQL", "jdbc:mysql://127.0.0.1:3306/a", 8);
  458 + mockDynamicRoutingDataSource(Map.of("master", postgresql, "mysql", mysql));
  459 + SqlSource sqlSource = new XMLLanguageDriver().createSqlSource(new Configuration(),
  460 + "<script>SELECT " + P + "formatDate('create_time')} AS time, COUNT(*) AS customerCreateCount"
  461 + + " FROM crm_customer WHERE create_time BETWEEN #{beginTime} AND #{endTime}"
  462 + + " GROUP BY " + P + "formatDate('create_time')}</script>", Map.class);
  463 + Map<String, Object> params = new HashMap<>();
  464 + params.put("beginTime", LocalDateTime.of(2026, 1, 1, 0, 0));
  465 + params.put("endTime", LocalDateTime.of(2026, 1, 31, 23, 59, 59));
  466 +
  467 + // 主库:PostgreSQL
  468 + BoundSql boundSql = sqlSource.getBoundSql(params);
  469 + assertEquals("SELECT TO_CHAR(create_time, 'YYYY-MM-DD') AS time, COUNT(*) AS customerCreateCount"
  470 + + " FROM crm_customer WHERE create_time BETWEEN ? AND ?"
  471 + + " GROUP BY TO_CHAR(create_time, 'YYYY-MM-DD')", normalize(boundSql.getSql()));
  472 + assertParameters(boundSql, "beginTime", "endTime");
  473 +
  474 + // 同一个 SqlSource 在下一次执行时,跟随路由切换到 MySQL
  475 + DynamicDataSourceContextHolder.push("mysql");
  476 + boundSql = sqlSource.getBoundSql(params);
  477 + assertEquals("SELECT DATE_FORMAT(create_time, '%Y-%m-%d') AS time, COUNT(*) AS customerCreateCount"
  478 + + " FROM crm_customer WHERE create_time BETWEEN ? AND ?"
  479 + + " GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d')", normalize(boundSql.getSql()));
  480 + assertParameters(boundSql, "beginTime", "endTime");
  481 + }
  482 +
  483 + @Test
  484 + public void testBoundSql_annotation() {
  485 + FakeDataSource sqlServer = new FakeDataSource("Microsoft SQL Server", "jdbc:sqlserver://127.0.0.1:1433", 16);
  486 + FakeDataSource h2 = new FakeDataSource("H2", "jdbc:h2:mem:testdb;MODE=MYSQL", 2);
  487 + mockDynamicRoutingDataSource(Map.of("master", sqlServer, "h2", h2));
  488 + // 与 @Select 注解相同:不使用 <script>,仅包含 ${} 时按动态 SQL 处理
  489 + SqlSource sqlSource = new XMLLanguageDriver().createSqlSource(new Configuration(),
  490 + "SELECT " + P + "date('send_time')} AS statDate, COUNT(*) AS count FROM im_private_message"
  491 + + " WHERE send_time >= #{beginTime} AND send_time < #{endTime}"
  492 + + " GROUP BY " + P + "date('send_time')}", Map.class);
  493 + Map<String, Object> params = new HashMap<>();
  494 + params.put("beginTime", LocalDateTime.of(2026, 1, 1, 0, 0));
  495 + params.put("endTime", LocalDateTime.of(2026, 2, 1, 0, 0));
  496 +
  497 + BoundSql boundSql = sqlSource.getBoundSql(params);
  498 + assertEquals("SELECT CONVERT(varchar(10), send_time, 23) AS statDate, COUNT(*) AS count FROM im_private_message"
  499 + + " WHERE send_time >= ? AND send_time < ?"
  500 + + " GROUP BY CONVERT(varchar(10), send_time, 23)", normalize(boundSql.getSql()));
  501 + assertParameters(boundSql, "beginTime", "endTime");
  502 +
  503 + DynamicDataSourceContextHolder.push("h2");
  504 + boundSql = sqlSource.getBoundSql(params);
  505 + assertEquals("SELECT DATE(send_time) AS statDate, COUNT(*) AS count FROM im_private_message"
  506 + + " WHERE send_time >= ? AND send_time < ?"
  507 + + " GROUP BY DATE(send_time)", normalize(boundSql.getSql()));
  508 + assertParameters(boundSql, "beginTime", "endTime");
  509 + }
  510 +
  511 + @Test
  512 + public void testBoundSql_deleteBatch() {
  513 + FakeDataSource postgresql = new FakeDataSource("PostgreSQL", "jdbc:postgresql://127.0.0.1:5432/a", 17);
  514 + FakeDataSource sqlServer = new FakeDataSource("Microsoft SQL Server", "jdbc:sqlserver://127.0.0.1:1433", 16);
  515 + FakeDataSource mysql = new FakeDataSource("MySQL", "jdbc:mysql://127.0.0.1:3306/a", 8);
  516 + FakeDataSource oracle = new FakeDataSource("Oracle", "jdbc:oracle:thin:@127.0.0.1:1521:xe", 21);
  517 + mockDynamicRoutingDataSource(Map.of("master", postgresql, "sqlserver", sqlServer, "mysql", mysql, "oracle", oracle));
  518 + // 与 @Delete 注解相同
  519 + SqlSource sqlSource = new XMLLanguageDriver().createSqlSource(new Configuration(),
  520 + P + "deleteBatchByLessThan('infra_job_log', 'create_time', 'createTime', 'limit')}", Map.class);
  521 + Map<String, Object> params = new HashMap<>();
  522 + params.put("createTime", LocalDateTime.of(2026, 1, 1, 0, 0));
  523 + params.put("limit", 100);
  524 +
  525 + // 主库:PostgreSQL
  526 + BoundSql boundSql = sqlSource.getBoundSql(params);
  527 + assertEquals("DELETE FROM infra_job_log WHERE id IN (SELECT id FROM infra_job_log WHERE create_time < ? LIMIT ?)",
  528 + normalize(boundSql.getSql()));
  529 + assertParameters(boundSql, "createTime", "limit");
  530 +
  531 + DynamicDataSourceContextHolder.push("sqlserver");
  532 + boundSql = sqlSource.getBoundSql(params);
  533 + assertEquals("DELETE FROM infra_job_log WHERE id IN (SELECT TOP (?) id FROM infra_job_log WHERE create_time < ?)",
  534 + normalize(boundSql.getSql()));
  535 + assertParameters(boundSql, "limit", "createTime");
  536 +
  537 + DynamicDataSourceContextHolder.push("oracle");
  538 + boundSql = sqlSource.getBoundSql(params);
  539 + assertEquals("DELETE FROM infra_job_log WHERE create_time < ? AND ROWNUM <= ?", normalize(boundSql.getSql()));
  540 + assertParameters(boundSql, "createTime", "limit");
  541 +
  542 + DynamicDataSourceContextHolder.push("mysql");
  543 + boundSql = sqlSource.getBoundSql(params);
  544 + assertEquals("DELETE FROM infra_job_log WHERE create_time < ? LIMIT ?", normalize(boundSql.getSql()));
  545 + assertParameters(boundSql, "createTime", "limit");
  546 + }
  547 +
  548 + @Test
  549 + public void testBoundSql_limit() {
  550 + FakeDataSource mysql = new FakeDataSource("MySQL", "jdbc:mysql://127.0.0.1:3306/a", 8);
  551 + FakeDataSource oracle = new FakeDataSource("Oracle", "jdbc:oracle:thin:@127.0.0.1:1521:xe", 21);
  552 + FakeDataSource sqlServer = new FakeDataSource("Microsoft SQL Server", "jdbc:sqlserver://127.0.0.1:1433", 16);
  553 + mockDynamicRoutingDataSource(Map.of("master", mysql, "oracle", oracle, "sqlserver", sqlServer));
  554 + // 与 @Select 注解相同:已有排序时使用参数绑定的行数,无排序时使用固定行数
  555 + SqlSource orderedSource = new XMLLanguageDriver().createSqlSource(new Configuration(),
  556 + "SELECT user_id, COUNT(*) AS messageCount FROM im_private_message WHERE send_time >= #{beginTime}"
  557 + + " GROUP BY user_id ORDER BY messageCount DESC " + P + "limitParam('limit', true)}", Map.class);
  558 + SqlSource unorderedSource = new XMLLanguageDriver().createSqlSource(new Configuration(),
  559 + "SELECT * FROM hrm_salary_option WHERE code = #{code} " + P + "limitRows(1, false)}", Map.class);
  560 + Map<String, Object> params = new HashMap<>();
  561 + params.put("beginTime", LocalDateTime.of(2026, 1, 1, 0, 0));
  562 + params.put("limit", 10);
  563 + params.put("code", 1);
  564 + String ordered = "SELECT user_id, COUNT(*) AS messageCount FROM im_private_message WHERE send_time >= ?"
  565 + + " GROUP BY user_id ORDER BY messageCount DESC ";
  566 +
  567 + // 主库:MySQL,保持原有 LIMIT
  568 + BoundSql boundSql = orderedSource.getBoundSql(params);
  569 + assertEquals(ordered + "LIMIT ?", normalize(boundSql.getSql()));
  570 + assertParameters(boundSql, "beginTime", "limit");
  571 + assertEquals("SELECT * FROM hrm_salary_option WHERE code = ? LIMIT 1",
  572 + normalize(unorderedSource.getBoundSql(params).getSql()));
  573 +
  574 + DynamicDataSourceContextHolder.push("oracle");
  575 + boundSql = orderedSource.getBoundSql(params);
  576 + assertEquals(ordered + "FETCH FIRST ? ROWS ONLY", normalize(boundSql.getSql()));
  577 + assertParameters(boundSql, "beginTime", "limit");
  578 + assertEquals("SELECT * FROM hrm_salary_option WHERE code = ? FETCH FIRST 1 ROWS ONLY",
  579 + normalize(unorderedSource.getBoundSql(params).getSql()));
  580 +
  581 + DynamicDataSourceContextHolder.push("sqlserver");
  582 + boundSql = orderedSource.getBoundSql(params);
  583 + assertEquals(ordered + "OFFSET 0 ROWS FETCH NEXT ? ROWS ONLY", normalize(boundSql.getSql()));
  584 + assertParameters(boundSql, "beginTime", "limit");
  585 + boundSql = unorderedSource.getBoundSql(params);
  586 + assertEquals("SELECT * FROM hrm_salary_option WHERE code = ? ORDER BY (SELECT NULL) OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY",
  587 + normalize(boundSql.getSql()));
  588 + assertParameters(boundSql, "code");
  589 + }
  590 +
  591 + @Test
  592 + public void testBoundSql_unsupportedProduct() {
  593 + FakeDataSource sqlite = new FakeDataSource("SQLite", "jdbc:sqlite:/tmp/test.db", 3);
  594 + mockDynamicRoutingDataSource(Map.of("master", sqlite));
  595 + SqlSource sqlSource = new XMLLanguageDriver().createSqlSource(new Configuration(),
  596 + "SELECT " + P + "month('order_date')} AS month FROM crm_contract"
  597 + + " WHERE " + P + "year('order_date')} = #{year}", Map.class);
  598 +
  599 + // OGNL 会包装异常,因此按异常链的信息断言
  600 + Exception ex = assertThrows(Exception.class, () -> sqlSource.getBoundSql(Map.of("year", 2026)));
  601 + assertTrue(causeMessages(ex).contains("不支持的数据库产品:SQLite"), causeMessages(ex));
  602 + }
  603 +
  604 + // ========== 工具方法 ==========
  605 +
  606 + private static Dialect resolve(String productName, String url, int majorVersion) throws SQLException {
  607 + return SqlDialectUtils.resolveDialect(new FakeDataSource(productName, url, majorVersion).getMetaData());
  608 + }
  609 +
  610 + private static void mockDynamicRoutingDataSource(Map<String, FakeDataSource> dataSources) {
  611 + DynamicRoutingDataSource dynamicRoutingDataSource = new DynamicRoutingDataSource(Collections.emptyList());
  612 + dynamicRoutingDataSource.setPrimary("master");
  613 + dataSources.forEach((name, dataSource) -> dynamicRoutingDataSource.addDataSource(name, dataSource.getDataSource()));
  614 + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
  615 + beanFactory.registerSingleton("dynamicRoutingDataSource", dynamicRoutingDataSource);
  616 + new SpringUtil().postProcessBeanFactory(beanFactory);
  617 + }
  618 +
  619 + private static String normalize(String sql) {
  620 + return sql.replaceAll("\\s+", " ").trim();
  621 + }
  622 +
  623 + /**
  624 + * 将 #{} 参数替换为 JDBC 占位符,模拟 MyBatis 生成的最终 SQL
  625 + */
  626 + private static String bind(String sql) {
  627 + return sql.replaceAll("#\\{[^}]+}", "?");
  628 + }
  629 +
  630 + private static void assertParameters(BoundSql boundSql, String... properties) {
  631 + List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
  632 + assertEquals(properties.length, parameterMappings.size());
  633 + for (int i = 0; i < properties.length; i++) {
  634 + assertEquals(properties[i], parameterMappings.get(i).getProperty());
  635 + }
  636 + }
  637 +
  638 + private static String causeMessages(Throwable ex) {
  639 + StringBuilder messages = new StringBuilder();
  640 + for (Throwable cause = ex; cause != null && cause != cause.getCause(); cause = cause.getCause()) {
  641 + messages.append(cause).append('\n');
  642 + }
  643 + return messages.toString();
  644 + }
  645 +
  646 + /**
  647 + * 基于 JDK 动态代理的数据源,只提供识别方言所需的元数据,并记录获取连接的次数
  648 + */
  649 + private static class FakeDataSource implements InvocationHandler {
  650 +
  651 + private final String productName;
  652 + private final String url;
  653 + private final int majorVersion;
  654 + private final DataSource dataSource;
  655 + private int connectionCount;
  656 + private int closeCount;
  657 + private boolean failOnConnection;
  658 +
  659 + FakeDataSource(String productName, String url, int majorVersion) {
  660 + this.productName = productName;
  661 + this.url = url;
  662 + this.majorVersion = majorVersion;
  663 + this.dataSource = proxy(DataSource.class);
  664 + }
  665 +
  666 + DataSource getDataSource() {
  667 + return dataSource;
  668 + }
  669 +
  670 + DatabaseMetaData getMetaData() {
  671 + return proxy(DatabaseMetaData.class);
  672 + }
  673 +
  674 + int getConnectionCount() {
  675 + return connectionCount;
  676 + }
  677 +
  678 + void setFailOnConnection(boolean failOnConnection) {
  679 + this.failOnConnection = failOnConnection;
  680 + }
  681 +
  682 + @SuppressWarnings("unchecked")
  683 + private <T> T proxy(Class<T> type) {
  684 + return (T) Proxy.newProxyInstance(getClass().getClassLoader(), new Class<?>[]{type}, this);
  685 + }
  686 +
  687 + @Override
  688 + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  689 + switch (method.getName()) {
  690 + case "hashCode":
  691 + return System.identityHashCode(proxy);
  692 + case "equals":
  693 + return proxy == args[0];
  694 + case "toString":
  695 + return "FakeDataSource(" + productName + ")";
  696 + case "getConnection":
  697 + connectionCount++;
  698 + if (failOnConnection) {
  699 + throw new SQLException("connection refused");
  700 + }
  701 + return proxy(Connection.class);
  702 + case "getMetaData":
  703 + return proxy(DatabaseMetaData.class);
  704 + case "close":
  705 + closeCount++;
  706 + return null;
  707 + case "getDatabaseProductName":
  708 + return productName;
  709 + case "getURL":
  710 + return url;
  711 + case "getDatabaseMajorVersion":
  712 + return majorVersion;
  713 + default:
  714 + throw new UnsupportedOperationException(method.getName());
  715 + }
  716 + }
  717 +
  718 + }
  719 +
  720 +}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiApiKeyMapper.java
@@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.ai.dal.mysql.model; @@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.ai.dal.mysql.model;
3 import cn.iocoder.yudao.framework.common.pojo.PageResult; 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; 4 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
5 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; 5 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
6 -import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;  
7 import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeyPageReqVO; 6 import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeyPageReqVO;
8 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiApiKeyDO; 7 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiApiKeyDO;
9 import org.apache.ibatis.annotations.Mapper; 8 import org.apache.ibatis.annotations.Mapper;
@@ -25,11 +24,10 @@ public interface AiApiKeyMapper extends BaseMapperX&lt;AiApiKeyDO&gt; { @@ -25,11 +24,10 @@ public interface AiApiKeyMapper extends BaseMapperX&lt;AiApiKeyDO&gt; {
25 } 24 }
26 25
27 default AiApiKeyDO selectFirstByPlatformAndStatus(String platform, Integer status) { 26 default AiApiKeyDO selectFirstByPlatformAndStatus(String platform, Integer status) {
28 - return selectOne(new QueryWrapperX<AiApiKeyDO>()  
29 - .eq("platform", platform)  
30 - .eq("status", status)  
31 - .limitN(1)  
32 - .orderByAsc("id")); 27 + return selectFirstOne(new LambdaQueryWrapperX<AiApiKeyDO>()
  28 + .eq(AiApiKeyDO::getPlatform, platform)
  29 + .eq(AiApiKeyDO::getStatus, status)
  30 + .orderByAsc(AiApiKeyDO::getId));
33 } 31 }
34 32
35 } 33 }
36 \ No newline at end of file 34 \ No newline at end of file
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiChatMapper.java
@@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.ai.dal.mysql.model; @@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.ai.dal.mysql.model;
3 import cn.iocoder.yudao.framework.common.pojo.PageResult; 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; 4 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
5 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; 5 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
6 -import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;  
7 import cn.iocoder.yudao.module.ai.controller.admin.model.vo.model.AiModelPageReqVO; 6 import cn.iocoder.yudao.module.ai.controller.admin.model.vo.model.AiModelPageReqVO;
8 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; 7 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO;
9 import org.apache.ibatis.annotations.Mapper; 8 import org.apache.ibatis.annotations.Mapper;
@@ -20,11 +19,10 @@ import java.util.List; @@ -20,11 +19,10 @@ import java.util.List;
20 public interface AiChatMapper extends BaseMapperX<AiModelDO> { 19 public interface AiChatMapper extends BaseMapperX<AiModelDO> {
21 20
22 default AiModelDO selectFirstByStatus(Integer type, Integer status) { 21 default AiModelDO selectFirstByStatus(Integer type, Integer status) {
23 - return selectOne(new QueryWrapperX<AiModelDO>()  
24 - .eq("type", type)  
25 - .eq("status", status)  
26 - .limitN(1)  
27 - .orderByAsc("sort")); 22 + return selectFirstOne(new LambdaQueryWrapperX<AiModelDO>()
  23 + .eq(AiModelDO::getType, type)
  24 + .eq(AiModelDO::getStatus, status)
  25 + .orderByAsc(AiModelDO::getSort));
28 } 26 }
29 27
30 default PageResult<AiModelDO> selectPage(AiModelPageReqVO reqVO) { 28 default PageResult<AiModelDO> selectPage(AiModelPageReqVO reqVO) {
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/dal/mysql/home/Ai1HomeMapper.java
@@ -23,16 +23,17 @@ public interface Ai1HomeMapper { @@ -23,16 +23,17 @@ public interface Ai1HomeMapper {
23 @Select("SELECT (SELECT COUNT(1) FROM ai1_agent WHERE deleted = 0) AS agentCount, " + 23 @Select("SELECT (SELECT COUNT(1) FROM ai1_agent WHERE deleted = 0) AS agentCount, " +
24 "(SELECT COUNT(1) FROM ai1_skill WHERE deleted = 0) AS skillCount, " + 24 "(SELECT COUNT(1) FROM ai1_skill WHERE deleted = 0) AS skillCount, " +
25 "(SELECT COUNT(1) FROM ai1_mcp WHERE deleted = 0) AS mcpCount, " + 25 "(SELECT COUNT(1) FROM ai1_mcp WHERE deleted = 0) AS mcpCount, " +
26 - "(SELECT COUNT(1) FROM ai1_model WHERE deleted = 0) AS modelCount") 26 + "(SELECT COUNT(1) FROM ai1_model WHERE deleted = 0) AS modelCount " +
  27 + "${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@fromDual()}")
27 Ai1HomeSummaryRespVO selectSummary(); 28 Ai1HomeSummaryRespVO selectSummary();
28 29
29 /** 30 /**
30 - * 按天统计消息数量,仅返回有消息的日期;使用 CAST AS DATE 兼容 MySQL 与 H2 31 + * 按天统计消息数量,仅返回有消息的日期;日期按方言输出 yyyy-MM-dd,MySQL、H2 等保持 CAST AS DATE
31 */ 32 */
32 - @Select("SELECT CAST(create_time AS DATE) AS statDate, COUNT(1) AS count " + 33 + @Select("SELECT ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@castDateText('create_time')} AS statDate, COUNT(1) AS count " +
33 "FROM ai1_session_message " + 34 "FROM ai1_session_message " +
34 "WHERE create_time BETWEEN #{beginTime} AND #{endTime} AND deleted = 0 " + 35 "WHERE create_time BETWEEN #{beginTime} AND #{endTime} AND deleted = 0 " +
35 - "GROUP BY CAST(create_time AS DATE) " + 36 + "GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@castDateText('create_time')} " +
36 "ORDER BY statDate") 37 "ORDER BY statDate")
37 @Results({ 38 @Results({
38 @Result(column = "statDate", property = "date"), 39 @Result(column = "statDate", property = "date"),
yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/dal/mysql/session/Ai1SessionMessageMapper.java
@@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.ai1.dal.mysql.session; @@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.ai1.dal.mysql.session;
3 import cn.iocoder.yudao.framework.common.pojo.PageResult; 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; 4 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
5 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; 5 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
6 -import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;  
7 import cn.iocoder.yudao.module.ai1.controller.admin.session.vo.message.Ai1SessionMessagePageReqVO; 6 import cn.iocoder.yudao.module.ai1.controller.admin.session.vo.message.Ai1SessionMessagePageReqVO;
8 import cn.iocoder.yudao.module.ai1.dal.dataobject.session.Ai1SessionMessageDO; 7 import cn.iocoder.yudao.module.ai1.dal.dataobject.session.Ai1SessionMessageDO;
9 import org.apache.ibatis.annotations.Mapper; 8 import org.apache.ibatis.annotations.Mapper;
@@ -49,23 +48,21 @@ public interface Ai1SessionMessageMapper extends BaseMapperX&lt;Ai1SessionMessageDO @@ -49,23 +48,21 @@ public interface Ai1SessionMessageMapper extends BaseMapperX&lt;Ai1SessionMessageDO
49 * 按编号倒序,取指定消息之前最近的 limit 条 48 * 按编号倒序,取指定消息之前最近的 limit 条
50 */ 49 */
51 default List<Ai1SessionMessageDO> selectListBySessionIdAndIdLessThan(Long sessionId, Long id, Integer limit) { 50 default List<Ai1SessionMessageDO> selectListBySessionIdAndIdLessThan(Long sessionId, Long id, Integer limit) {
52 - return selectList(new QueryWrapperX<Ai1SessionMessageDO>()  
53 - .limitN(limit)  
54 - .eq("session_id", sessionId)  
55 - .lt("id", id)  
56 - .orderByDesc("id")); 51 + return selectListLimitN(new LambdaQueryWrapperX<Ai1SessionMessageDO>()
  52 + .eq(Ai1SessionMessageDO::getSessionId, sessionId)
  53 + .lt(Ai1SessionMessageDO::getId, id)
  54 + .orderByDesc(Ai1SessionMessageDO::getId), limit);
57 } 55 }
58 56
59 /** 57 /**
60 * 获得多个会话中,编号大于 id 的第一条指定状态的消息 58 * 获得多个会话中,编号大于 id 的第一条指定状态的消息
61 */ 59 */
62 default Ai1SessionMessageDO selectFirstBySessionIdsAndStatusAndIdGt(Collection<Long> sessionIds, Integer status, Long id) { 60 default Ai1SessionMessageDO selectFirstBySessionIdsAndStatusAndIdGt(Collection<Long> sessionIds, Integer status, Long id) {
63 - return selectOne(new QueryWrapperX<Ai1SessionMessageDO>()  
64 - .limitN(1)  
65 - .in("session_id", sessionIds)  
66 - .eq("status", status)  
67 - .gt("id", id)  
68 - .orderByAsc("id")); 61 + return selectFirstOne(new LambdaQueryWrapperX<Ai1SessionMessageDO>()
  62 + .in(Ai1SessionMessageDO::getSessionId, sessionIds)
  63 + .eq(Ai1SessionMessageDO::getStatus, status)
  64 + .gt(Ai1SessionMessageDO::getId, id)
  65 + .orderByAsc(Ai1SessionMessageDO::getId));
69 } 66 }
70 67
71 /** 68 /**
@@ -73,13 +70,12 @@ public interface Ai1SessionMessageMapper extends BaseMapperX&lt;Ai1SessionMessageDO @@ -73,13 +70,12 @@ public interface Ai1SessionMessageMapper extends BaseMapperX&lt;Ai1SessionMessageDO
73 */ 70 */
74 default List<Ai1SessionMessageDO> selectListBySessionIdsAndStatusAndIdBetween(Collection<Long> sessionIds, Integer status, 71 default List<Ai1SessionMessageDO> selectListBySessionIdsAndStatusAndIdBetween(Collection<Long> sessionIds, Integer status,
75 Long minId, Long maxId, Integer limit) { 72 Long minId, Long maxId, Integer limit) {
76 - return selectList(new QueryWrapperX<Ai1SessionMessageDO>()  
77 - .limitN(limit)  
78 - .in("session_id", sessionIds)  
79 - .eq("status", status)  
80 - .gt("id", minId)  
81 - .lt(maxId != null, "id", maxId)  
82 - .orderByAsc("id")); 73 + return selectListLimitN(new LambdaQueryWrapperX<Ai1SessionMessageDO>()
  74 + .in(Ai1SessionMessageDO::getSessionId, sessionIds)
  75 + .eq(Ai1SessionMessageDO::getStatus, status)
  76 + .gt(Ai1SessionMessageDO::getId, minId)
  77 + .lt(maxId != null, Ai1SessionMessageDO::getId, maxId)
  78 + .orderByAsc(Ai1SessionMessageDO::getId), limit);
83 } 79 }
84 80
85 default Long selectCountBySessionId(Long sessionId) { 81 default Long selectCountBySessionId(Long sessionId) {
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/contract/CrmContractConfigMapper.java
1 package cn.iocoder.yudao.module.crm.dal.mysql.contract; 1 package cn.iocoder.yudao.module.crm.dal.mysql.contract;
2 2
3 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; 3 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
4 -import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX; 4 +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
5 import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractConfigDO; 5 import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractConfigDO;
6 import org.apache.ibatis.annotations.Mapper; 6 import org.apache.ibatis.annotations.Mapper;
7 7
@@ -14,7 +14,7 @@ import org.apache.ibatis.annotations.Mapper; @@ -14,7 +14,7 @@ import org.apache.ibatis.annotations.Mapper;
14 public interface CrmContractConfigMapper extends BaseMapperX<CrmContractConfigDO> { 14 public interface CrmContractConfigMapper extends BaseMapperX<CrmContractConfigDO> {
15 15
16 default CrmContractConfigDO selectOne() { 16 default CrmContractConfigDO selectOne() {
17 - return selectOne(new QueryWrapperX<CrmContractConfigDO>().limitN(1)); 17 + return selectFirstOne(new LambdaQueryWrapperX<CrmContractConfigDO>().orderByAsc(CrmContractConfigDO::getId));
18 } 18 }
19 19
20 } 20 }
21 \ No newline at end of file 21 \ No newline at end of file
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerPoolConfigMapper.java
@@ -14,7 +14,8 @@ import org.apache.ibatis.annotations.Mapper; @@ -14,7 +14,8 @@ import org.apache.ibatis.annotations.Mapper;
14 public interface CrmCustomerPoolConfigMapper extends BaseMapperX<CrmCustomerPoolConfigDO> { 14 public interface CrmCustomerPoolConfigMapper extends BaseMapperX<CrmCustomerPoolConfigDO> {
15 15
16 default CrmCustomerPoolConfigDO selectOne() { 16 default CrmCustomerPoolConfigDO selectOne() {
17 - return selectOne(new LambdaQueryWrapperX<CrmCustomerPoolConfigDO>().last("LIMIT 1")); 17 + return selectFirstOne(new LambdaQueryWrapperX<CrmCustomerPoolConfigDO>()
  18 + .orderByAsc(CrmCustomerPoolConfigDO::getId));
18 } 19 }
19 20
20 } 21 }
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/CrmReceivablePlanMapper.java
@@ -25,10 +25,9 @@ import java.util.Objects; @@ -25,10 +25,9 @@ import java.util.Objects;
25 public interface CrmReceivablePlanMapper extends BaseMapperX<CrmReceivablePlanDO> { 25 public interface CrmReceivablePlanMapper extends BaseMapperX<CrmReceivablePlanDO> {
26 26
27 default CrmReceivablePlanDO selectMaxPeriodByContractId(Long contractId) { 27 default CrmReceivablePlanDO selectMaxPeriodByContractId(Long contractId) {
28 - return selectOne(new LambdaQueryWrapperX<CrmReceivablePlanDO>() 28 + return selectFirstOne(new LambdaQueryWrapperX<CrmReceivablePlanDO>()
29 .eq(CrmReceivablePlanDO::getContractId, contractId) 29 .eq(CrmReceivablePlanDO::getContractId, contractId)
30 - .orderByDesc(CrmReceivablePlanDO::getPeriod)  
31 - .last("LIMIT 1")); 30 + .orderByDesc(CrmReceivablePlanDO::getPeriod));
32 } 31 }
33 32
34 default PageResult<CrmReceivablePlanDO> selectPageByCustomerId(CrmReceivablePlanPageReqVO reqVO) { 33 default PageResult<CrmReceivablePlanDO> selectPageByCustomerId(CrmReceivablePlanPageReqVO reqVO) {
yudao-module-crm/src/main/resources/mapper/permission/CrmOwnerRecordMapper.xml
@@ -5,7 +5,7 @@ @@ -5,7 +5,7 @@
5 5
6 <select id="selectPoolCustomerPutCountByDate" 6 <select id="selectPoolCustomerPutCountByDate"
7 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsPoolSummaryByDateRespVO"> 7 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsPoolSummaryByDateRespVO">
8 - SELECT DATE_FORMAT(create_time, '%Y-%m-%d') AS time, 8 + SELECT ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('create_time')} AS time,
9 COUNT(*) AS customer_put_count 9 COUNT(*) AS customer_put_count
10 FROM crm_owner_record 10 FROM crm_owner_record
11 WHERE deleted = 0 11 WHERE deleted = 0
@@ -16,12 +16,12 @@ @@ -16,12 +16,12 @@
16 </foreach> 16 </foreach>
17 AND post_owner_user_id IS NULL 17 AND post_owner_user_id IS NULL
18 AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime} 18 AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
19 - GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d') 19 + GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('create_time')}
20 </select> 20 </select>
21 21
22 <select id="selectPoolCustomerTakeCountByDate" 22 <select id="selectPoolCustomerTakeCountByDate"
23 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsPoolSummaryByDateRespVO"> 23 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsPoolSummaryByDateRespVO">
24 - SELECT DATE_FORMAT(create_time, '%Y-%m-%d') AS time, 24 + SELECT ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('create_time')} AS time,
25 COUNT(*) AS customer_take_count 25 COUNT(*) AS customer_take_count
26 FROM crm_owner_record 26 FROM crm_owner_record
27 WHERE deleted = 0 27 WHERE deleted = 0
@@ -32,7 +32,7 @@ @@ -32,7 +32,7 @@
32 #{userId} 32 #{userId}
33 </foreach> 33 </foreach>
34 AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime} 34 AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
35 - GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d') 35 + GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('create_time')}
36 </select> 36 </select>
37 37
38 <select id="selectPoolCustomerPutCountByUser" 38 <select id="selectPoolCustomerPutCountByUser"
yudao-module-crm/src/main/resources/mapper/statistics/CrmStatisticsCustomerMapper.xml
@@ -4,7 +4,7 @@ @@ -4,7 +4,7 @@
4 4
5 <select id="selectCustomerCreateCountGroupByDate" 5 <select id="selectCustomerCreateCountGroupByDate"
6 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerSummaryByDateRespVO"> 6 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerSummaryByDateRespVO">
7 - SELECT DATE_FORMAT(create_time, '%Y-%m-%d') AS time, 7 + SELECT ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('create_time')} AS time,
8 COUNT(*) AS customerCreateCount 8 COUNT(*) AS customerCreateCount
9 FROM crm_customer 9 FROM crm_customer
10 WHERE deleted = 0 10 WHERE deleted = 0
@@ -13,12 +13,12 @@ @@ -13,12 +13,12 @@
13 #{userId} 13 #{userId}
14 </foreach> 14 </foreach>
15 AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime} 15 AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
16 - GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d') 16 + GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('create_time')}
17 </select> 17 </select>
18 18
19 <select id="selectCustomerDealCountGroupByDate" 19 <select id="selectCustomerDealCountGroupByDate"
20 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerSummaryByDateRespVO"> 20 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerSummaryByDateRespVO">
21 - SELECT DATE_FORMAT(customer.create_time, '%Y-%m-%d') AS time, 21 + SELECT ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('customer.create_time')} AS time,
22 COUNT(DISTINCT customer.id) AS customer_deal_count 22 COUNT(DISTINCT customer.id) AS customer_deal_count
23 FROM crm_customer customer 23 FROM crm_customer customer
24 LEFT JOIN crm_contract contract ON contract.customer_id = customer.id 24 LEFT JOIN crm_contract contract ON contract.customer_id = customer.id
@@ -30,7 +30,7 @@ @@ -30,7 +30,7 @@
30 #{userId} 30 #{userId}
31 </foreach> 31 </foreach>
32 AND customer.create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime} 32 AND customer.create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
33 - GROUP BY DATE_FORMAT(customer.create_time, '%Y-%m-%d') 33 + GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('customer.create_time')}
34 </select> 34 </select>
35 35
36 <select id="selectCustomerCreateCountGroupByUser" 36 <select id="selectCustomerCreateCountGroupByUser"
@@ -96,7 +96,7 @@ @@ -96,7 +96,7 @@
96 96
97 <select id="selectFollowUpRecordCountGroupByDate" 97 <select id="selectFollowUpRecordCountGroupByDate"
98 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsFollowUpSummaryByDateRespVO"> 98 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsFollowUpSummaryByDateRespVO">
99 - SELECT DATE_FORMAT(create_time, '%Y-%m-%d') AS time, 99 + SELECT ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('create_time')} AS time,
100 COUNT(*) AS follow_up_record_count 100 COUNT(*) AS follow_up_record_count
101 FROM crm_follow_up_record 101 FROM crm_follow_up_record
102 WHERE creator IN 102 WHERE creator IN
@@ -105,12 +105,12 @@ @@ -105,12 +105,12 @@
105 </foreach> 105 </foreach>
106 AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime} 106 AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
107 AND biz_type = ${@cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum@CRM_CUSTOMER.type} 107 AND biz_type = ${@cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum@CRM_CUSTOMER.type}
108 - GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d') 108 + GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('create_time')}
109 </select> 109 </select>
110 110
111 <select id="selectFollowUpCustomerCountGroupByDate" 111 <select id="selectFollowUpCustomerCountGroupByDate"
112 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsFollowUpSummaryByDateRespVO"> 112 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsFollowUpSummaryByDateRespVO">
113 - SELECT DATE_FORMAT(create_time, '%Y-%m-%d') AS time, 113 + SELECT ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('create_time')} AS time,
114 COUNT(DISTINCT biz_id) AS follow_up_customer_count 114 COUNT(DISTINCT biz_id) AS follow_up_customer_count
115 FROM crm_follow_up_record 115 FROM crm_follow_up_record
116 WHERE creator IN 116 WHERE creator IN
@@ -119,7 +119,7 @@ @@ -119,7 +119,7 @@
119 </foreach> 119 </foreach>
120 AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime} 120 AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
121 AND biz_type = ${@cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum@CRM_CUSTOMER.type} 121 AND biz_type = ${@cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum@CRM_CUSTOMER.type}
122 - GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d') 122 + GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('create_time')}
123 </select> 123 </select>
124 124
125 <select id="selectFollowUpRecordCountGroupByUser" 125 <select id="selectFollowUpRecordCountGroupByUser"
@@ -189,11 +189,11 @@ @@ -189,11 +189,11 @@
189 AND customer.create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime} 189 AND customer.create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
190 </select> 190 </select>
191 191
192 - <!-- TIMESTAMPDIFF 用于求差值;AVG 求平均;TRUNCATE 去掉小数点、只保留整数 --> 192 + <!-- 成交周期:客户创建到合同下单的完整天数(向零截断)求平均,截断保留 1 位小数,无数据时为 0;表达式按方言见 SqlDialectUtils.customerDealCycle -->
193 <select id="selectCustomerDealCycleGroupByDate" 193 <select id="selectCustomerDealCycleGroupByDate"
194 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerDealCycleByDateRespVO"> 194 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerDealCycleByDateRespVO">
195 - SELECT DATE_FORMAT(contract.order_date, '%Y-%m-%d') AS time,  
196 - COALESCE(TRUNCATE(AVG(TIMESTAMPDIFF(DAY, customer.create_time, contract.order_date)), 1), 0) AS customer_deal_cycle 195 + SELECT ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('contract.order_date')} AS time,
  196 + ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@customerDealCycle('customer.create_time', 'contract.order_date')} AS customer_deal_cycle
197 FROM crm_customer customer 197 FROM crm_customer customer
198 LEFT JOIN crm_contract contract ON contract.customer_id = customer.id 198 LEFT JOIN crm_contract contract ON contract.customer_id = customer.id
199 WHERE customer.deleted = 0 AND contract.deleted = 0 199 WHERE customer.deleted = 0 AND contract.deleted = 0
@@ -203,13 +203,13 @@ @@ -203,13 +203,13 @@
203 #{userId} 203 #{userId}
204 </foreach> 204 </foreach>
205 AND customer.create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime} 205 AND customer.create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
206 - GROUP BY DATE_FORMAT(contract.order_date, '%Y-%m-%d') 206 + GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('contract.order_date')}
207 </select> 207 </select>
208 208
209 <select id="selectCustomerDealCycleGroupByUser" 209 <select id="selectCustomerDealCycleGroupByUser"
210 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerDealCycleByUserRespVO"> 210 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerDealCycleByUserRespVO">
211 SELECT customer.owner_user_id, 211 SELECT customer.owner_user_id,
212 - COALESCE(TRUNCATE(AVG(TIMESTAMPDIFF(DAY, customer.create_time, contract.order_date)), 1), 0) AS customer_deal_cycle 212 + ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@customerDealCycle('customer.create_time', 'contract.order_date')} AS customer_deal_cycle
213 FROM crm_customer customer 213 FROM crm_customer customer
214 LEFT JOIN crm_contract contract ON contract.customer_id = customer.id 214 LEFT JOIN crm_contract contract ON contract.customer_id = customer.id
215 WHERE customer.deleted = 0 AND contract.deleted = 0 215 WHERE customer.deleted = 0 AND contract.deleted = 0
@@ -225,7 +225,7 @@ @@ -225,7 +225,7 @@
225 <select id="selectCustomerDealCycleGroupByAreaId" 225 <select id="selectCustomerDealCycleGroupByAreaId"
226 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerDealCycleByAreaRespVO"> 226 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerDealCycleByAreaRespVO">
227 SELECT customer.area_id AS area_id, 227 SELECT customer.area_id AS area_id,
228 - COALESCE(TRUNCATE(AVG(TIMESTAMPDIFF(DAY, customer.create_time, contract.order_date)), 1), 0) AS customer_deal_cycle, 228 + ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@customerDealCycle('customer.create_time', 'contract.order_date')} AS customer_deal_cycle,
229 COUNT(DISTINCT customer.id) AS customer_deal_count 229 COUNT(DISTINCT customer.id) AS customer_deal_count
230 FROM crm_customer customer 230 FROM crm_customer customer
231 LEFT JOIN crm_contract contract ON customer.id = contract.customer_id 231 LEFT JOIN crm_contract contract ON customer.id = contract.customer_id
@@ -244,7 +244,7 @@ @@ -244,7 +244,7 @@
244 <select id="selectCustomerDealCycleGroupByProductId" 244 <select id="selectCustomerDealCycleGroupByProductId"
245 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerDealCycleByProductRespVO"> 245 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerDealCycleByProductRespVO">
246 SELECT product.name AS product_name, 246 SELECT product.name AS product_name,
247 - COALESCE(TRUNCATE(AVG(TIMESTAMPDIFF(DAY, customer.create_time, contract.order_date)), 1), 0) AS customer_deal_cycle, 247 + ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@customerDealCycle('customer.create_time', 'contract.order_date')} AS customer_deal_cycle,
248 COUNT(DISTINCT customer.id) AS customer_deal_count 248 COUNT(DISTINCT customer.id) AS customer_deal_count
249 FROM crm_customer customer 249 FROM crm_customer customer
250 LEFT JOIN crm_contract contract ON customer.id = contract.customer_id 250 LEFT JOIN crm_contract contract ON customer.id = contract.customer_id
yudao-module-crm/src/main/resources/mapper/statistics/CrmStatisticsFunnelMapper.xml
@@ -86,7 +86,7 @@ @@ -86,7 +86,7 @@
86 <select id="selectBusinessSummaryGroupByDate" 86 <select id="selectBusinessSummaryGroupByDate"
87 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.CrmStatisticsBusinessSummaryByDateRespVO"> 87 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.CrmStatisticsBusinessSummaryByDateRespVO">
88 SELECT 88 SELECT
89 - DATE_FORMAT(create_time, '%Y-%m-%d') AS time, 89 + ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('create_time')} AS time,
90 COUNT(*) AS businessCreateCount, 90 COUNT(*) AS businessCreateCount,
91 COALESCE(SUM(total_price), 0) AS totalPrice 91 COALESCE(SUM(total_price), 0) AS totalPrice
92 FROM crm_business 92 FROM crm_business
@@ -100,13 +100,13 @@ @@ -100,13 +100,13 @@
100 </foreach> 100 </foreach>
101 AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND 101 AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND
102 #{times[1],javaType=java.time.LocalDateTime} 102 #{times[1],javaType=java.time.LocalDateTime}
103 - GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d') 103 + GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('create_time')}
104 </select> 104 </select>
105 105
106 <select id="selectBusinessInversionRateSummaryByDate" 106 <select id="selectBusinessInversionRateSummaryByDate"
107 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.CrmStatisticsBusinessInversionRateSummaryByDateRespVO"> 107 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.CrmStatisticsBusinessInversionRateSummaryByDateRespVO">
108 SELECT 108 SELECT
109 - DATE_FORMAT(create_time, '%Y-%m-%d') AS time, 109 + ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('create_time')} AS time,
110 COUNT(*) AS businessCount, 110 COUNT(*) AS businessCount,
111 SUM(CASE WHEN end_status = 1 THEN 1 ELSE 0 END) AS businessWinCount 111 SUM(CASE WHEN end_status = 1 THEN 1 ELSE 0 END) AS businessWinCount
112 FROM crm_business 112 FROM crm_business
@@ -120,7 +120,7 @@ @@ -120,7 +120,7 @@
120 </foreach> 120 </foreach>
121 AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND 121 AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND
122 #{times[1],javaType=java.time.LocalDateTime} 122 #{times[1],javaType=java.time.LocalDateTime}
123 - GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d') 123 + GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('create_time')}
124 </select> 124 </select>
125 125
126 </mapper> 126 </mapper>
yudao-module-crm/src/main/resources/mapper/statistics/CrmStatisticsPerformanceMapper.xml
@@ -5,7 +5,7 @@ @@ -5,7 +5,7 @@
5 <select id="selectContractCountPerformance" 5 <select id="selectContractCountPerformance"
6 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceRespVO"> 6 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceRespVO">
7 SELECT 7 SELECT
8 - DATE_FORMAT(order_date, '%Y%m') AS time, 8 + ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatYearMonth('order_date')} AS time,
9 COUNT(1) AS currentMonthCount 9 COUNT(1) AS currentMonthCount
10 FROM crm_contract 10 FROM crm_contract
11 WHERE deleted = 0 11 WHERE deleted = 0
@@ -16,13 +16,13 @@ @@ -16,13 +16,13 @@
16 </foreach> 16 </foreach>
17 AND order_date between #{times[0],javaType=java.time.LocalDateTime} and 17 AND order_date between #{times[0],javaType=java.time.LocalDateTime} and
18 #{times[1],javaType=java.time.LocalDateTime} 18 #{times[1],javaType=java.time.LocalDateTime}
19 - GROUP BY DATE_FORMAT(order_date, '%Y%m') 19 + GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatYearMonth('order_date')}
20 </select> 20 </select>
21 21
22 <select id="selectContractPricePerformance" 22 <select id="selectContractPricePerformance"
23 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceRespVO"> 23 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceRespVO">
24 SELECT 24 SELECT
25 - DATE_FORMAT(order_date, '%Y%m') AS time, 25 + ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatYearMonth('order_date')} AS time,
26 COALESCE(SUM(total_price), 0) AS currentMonthCount 26 COALESCE(SUM(total_price), 0) AS currentMonthCount
27 FROM crm_contract 27 FROM crm_contract
28 WHERE deleted = 0 28 WHERE deleted = 0
@@ -33,13 +33,13 @@ @@ -33,13 +33,13 @@
33 </foreach> 33 </foreach>
34 AND order_date between #{times[0],javaType=java.time.LocalDateTime} and 34 AND order_date between #{times[0],javaType=java.time.LocalDateTime} and
35 #{times[1],javaType=java.time.LocalDateTime} 35 #{times[1],javaType=java.time.LocalDateTime}
36 - GROUP BY DATE_FORMAT(order_date, '%Y%m') 36 + GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatYearMonth('order_date')}
37 </select> 37 </select>
38 38
39 <select id="selectReceivablePricePerformance" 39 <select id="selectReceivablePricePerformance"
40 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceRespVO"> 40 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceRespVO">
41 SELECT 41 SELECT
42 - DATE_FORMAT(return_time, '%Y%m') AS time, 42 + ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatYearMonth('return_time')} AS time,
43 COALESCE(SUM(price), 0) AS currentMonthCount 43 COALESCE(SUM(price), 0) AS currentMonthCount
44 FROM crm_receivable 44 FROM crm_receivable
45 WHERE deleted = 0 45 WHERE deleted = 0
@@ -50,12 +50,12 @@ @@ -50,12 +50,12 @@
50 </foreach> 50 </foreach>
51 AND return_time between #{times[0],javaType=java.time.LocalDateTime} and 51 AND return_time between #{times[0],javaType=java.time.LocalDateTime} and
52 #{times[1],javaType=java.time.LocalDateTime} 52 #{times[1],javaType=java.time.LocalDateTime}
53 - GROUP BY DATE_FORMAT(return_time, '%Y%m') 53 + GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatYearMonth('return_time')}
54 </select> 54 </select>
55 55
56 <select id="selectContractSummary" 56 <select id="selectContractSummary"
57 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceSummaryRespVO"> 57 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceSummaryRespVO">
58 - SELECT DATE_FORMAT(contract.order_date, '%Y%m') AS time, 58 + SELECT ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatYearMonth('contract.order_date')} AS time,
59 COUNT(DISTINCT contract.id) AS contract_count, 59 COUNT(DISTINCT contract.id) AS contract_count,
60 COALESCE(SUM(contract.total_price), 0) AS contract_price, 60 COALESCE(SUM(contract.total_price), 0) AS contract_price,
61 COALESCE(SUM(receivable.receivable_price), 0) AS receivable_price, 61 COALESCE(SUM(receivable.receivable_price), 0) AS receivable_price,
@@ -77,7 +77,7 @@ @@ -77,7 +77,7 @@
77 </foreach> 77 </foreach>
78 AND contract.order_date between #{times[0],javaType=java.time.LocalDateTime} and 78 AND contract.order_date between #{times[0],javaType=java.time.LocalDateTime} and
79 #{times[1],javaType=java.time.LocalDateTime} 79 #{times[1],javaType=java.time.LocalDateTime}
80 - GROUP BY DATE_FORMAT(contract.order_date, '%Y%m') 80 + GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatYearMonth('contract.order_date')}
81 </select> 81 </select>
82 82
83 </mapper> 83 </mapper>
yudao-module-crm/src/main/resources/mapper/statistics/CrmStatisticsPerformanceTargetMapper.xml
@@ -10,7 +10,7 @@ @@ -10,7 +10,7 @@
10 10
11 <select id="selectContractActualByMonth" 11 <select id="selectContractActualByMonth"
12 resultMap="CrmStatisticsPerformanceTargetActualResultMap"> 12 resultMap="CrmStatisticsPerformanceTargetActualResultMap">
13 - SELECT MONTH(order_date) AS month, 13 + SELECT ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@month('order_date')} AS month,
14 COALESCE(SUM(total_price), 0) AS current_price 14 COALESCE(SUM(total_price), 0) AS current_price
15 FROM crm_contract 15 FROM crm_contract
16 WHERE deleted = 0 16 WHERE deleted = 0
@@ -19,13 +19,13 @@ @@ -19,13 +19,13 @@
19 <foreach collection="userIds" item="userId" open="(" close=")" separator=","> 19 <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
20 #{userId} 20 #{userId}
21 </foreach> 21 </foreach>
22 - AND YEAR(order_date) = #{year}  
23 - GROUP BY MONTH(order_date) 22 + AND ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@year('order_date')} = #{year}
  23 + GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@month('order_date')}
24 </select> 24 </select>
25 25
26 <select id="selectReceivableActualByMonth" 26 <select id="selectReceivableActualByMonth"
27 resultMap="CrmStatisticsPerformanceTargetActualResultMap"> 27 resultMap="CrmStatisticsPerformanceTargetActualResultMap">
28 - SELECT MONTH(return_time) AS month, 28 + SELECT ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@month('return_time')} AS month,
29 COALESCE(SUM(price), 0) AS current_price 29 COALESCE(SUM(price), 0) AS current_price
30 FROM crm_receivable 30 FROM crm_receivable
31 WHERE deleted = 0 31 WHERE deleted = 0
@@ -34,8 +34,8 @@ @@ -34,8 +34,8 @@
34 <foreach collection="userIds" item="userId" open="(" close=")" separator=","> 34 <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
35 #{userId} 35 #{userId}
36 </foreach> 36 </foreach>
37 - AND YEAR(return_time) = #{year}  
38 - GROUP BY MONTH(return_time) 37 + AND ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@year('return_time')} = #{year}
  38 + GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@month('return_time')}
39 </select> 39 </select>
40 40
41 </mapper> 41 </mapper>
yudao-module-erp/src/main/resources/mapper/statistics/ErpPurchaseStatisticsMapper.xml
@@ -26,6 +26,7 @@ @@ -26,6 +26,7 @@
26 </if> 26 </if>
27 AND deleted = 0 27 AND deleted = 0
28 AND status = 20) 28 AND status = 20)
  29 + ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@fromDual()}
29 </select> 30 </select>
30 31
31 </mapper> 32 </mapper>
32 \ No newline at end of file 33 \ No newline at end of file
yudao-module-erp/src/main/resources/mapper/statistics/ErpSaleStatisticsMapper.xml
@@ -26,6 +26,7 @@ @@ -26,6 +26,7 @@
26 </if> 26 </if>
27 AND deleted = 0 27 AND deleted = 0
28 AND status = 20) 28 AND status = 20)
  29 + ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@fromDual()}
29 </select> 30 </select>
30 31
31 </mapper> 32 </mapper>
32 \ No newline at end of file 33 \ No newline at end of file
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/employee/info/HrmEmployeeMapper.java
@@ -9,6 +9,7 @@ import cn.iocoder.yudao.framework.common.util.object.PageUtils; @@ -9,6 +9,7 @@ import cn.iocoder.yudao.framework.common.util.object.PageUtils;
9 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; 9 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
10 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; 10 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
11 import cn.iocoder.yudao.framework.mybatis.core.query.MPJLambdaWrapperX; 11 import cn.iocoder.yudao.framework.mybatis.core.query.MPJLambdaWrapperX;
  12 +import cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils;
12 import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee.HrmEmployeePageReqVO; 13 import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee.HrmEmployeePageReqVO;
13 import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.employment.HrmEmployeeChangeRecordDO; 14 import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.employment.HrmEmployeeChangeRecordDO;
14 import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO; 15 import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.info.HrmEmployeeDO;
@@ -286,11 +287,11 @@ public interface HrmEmployeeMapper extends BaseMapperX&lt;HrmEmployeeDO&gt; { @@ -286,11 +287,11 @@ public interface HrmEmployeeMapper extends BaseMapperX&lt;HrmEmployeeDO&gt; {
286 } else if (HrmEmployeeTodoTypeEnum.PENDING_ENTRY.getType().equals(todoType)) { 287 } else if (HrmEmployeeTodoTypeEnum.PENDING_ENTRY.getType().equals(todoType)) {
287 query.betweenIfPresent(HrmEmployeeDO::getEntryTime, monthTimes); 288 query.betweenIfPresent(HrmEmployeeDO::getEntryTime, monthTimes);
288 } else if (HrmEmployeeTodoTypeEnum.BIRTHDAY.getType().equals(todoType)) { 289 } else if (HrmEmployeeTodoTypeEnum.BIRTHDAY.getType().equals(todoType)) {
289 - // MySQL 和 H2 MySQL 模式均支持 MONTH,用于跨年筛选当月生日。  
290 - query.apply("MONTH(birthday) = {0}", now.getMonthValue()); 290 + // 按当前路由数据源的方言取月份,用于跨年筛选当月生日;MySQL、H2 等保持 MONTH。
  291 + query.apply(SqlDialectUtils.month("birthday") + " = {0}", now.getMonthValue());
291 if (now.getMonthValue() == 2 && !now.isLeapYear()) { 292 if (now.getMonthValue() == 2 && !now.isLeapYear()) {
292 // 非闰年不存在 2 月 29 日,与首页日历和年龄计算保持一致。 293 // 非闰年不存在 2 月 29 日,与首页日历和年龄计算保持一致。
293 - query.apply("DAYOFMONTH(birthday) <> 29"); 294 + query.apply(SqlDialectUtils.dayOfMonth("birthday") + " <> 29");
294 } 295 }
295 } 296 }
296 } 297 }
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/recruit/candidate/HrmRecruitInterviewMapper.java
@@ -21,11 +21,10 @@ public interface HrmRecruitInterviewMapper extends BaseMapperX&lt;HrmRecruitIntervi @@ -21,11 +21,10 @@ public interface HrmRecruitInterviewMapper extends BaseMapperX&lt;HrmRecruitIntervi
21 } 21 }
22 22
23 default HrmRecruitInterviewDO selectLatestByCandidateId(Long candidateId) { 23 default HrmRecruitInterviewDO selectLatestByCandidateId(Long candidateId) {
24 - return selectOne(new LambdaQueryWrapperX<HrmRecruitInterviewDO>() 24 + return selectFirstOne(new LambdaQueryWrapperX<HrmRecruitInterviewDO>()
25 .eq(HrmRecruitInterviewDO::getCandidateId, candidateId) 25 .eq(HrmRecruitInterviewDO::getCandidateId, candidateId)
26 .orderByDesc(HrmRecruitInterviewDO::getStageNumber) 26 .orderByDesc(HrmRecruitInterviewDO::getStageNumber)
27 - .orderByDesc(HrmRecruitInterviewDO::getId)  
28 - .last("LIMIT 1")); 27 + .orderByDesc(HrmRecruitInterviewDO::getId));
29 } 28 }
30 29
31 default void deleteByCandidateId(Long candidateId) { 30 default void deleteByCandidateId(Long candidateId) {
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/salary/config/HrmSalaryConfigMapper.java
@@ -9,9 +9,8 @@ import org.apache.ibatis.annotations.Mapper; @@ -9,9 +9,8 @@ import org.apache.ibatis.annotations.Mapper;
9 public interface HrmSalaryConfigMapper extends BaseMapperX<HrmSalaryConfigDO> { 9 public interface HrmSalaryConfigMapper extends BaseMapperX<HrmSalaryConfigDO> {
10 10
11 default HrmSalaryConfigDO selectFirst() { 11 default HrmSalaryConfigDO selectFirst() {
12 - return selectOne(new LambdaQueryWrapperX<HrmSalaryConfigDO>()  
13 - .orderByAsc(HrmSalaryConfigDO::getId)  
14 - .last("LIMIT 1")); 12 + return selectFirstOne(new LambdaQueryWrapperX<HrmSalaryConfigDO>()
  13 + .orderByAsc(HrmSalaryConfigDO::getId));
15 } 14 }
16 15
17 } 16 }
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/salary/config/HrmSalaryOptionMapper.java
@@ -17,7 +17,8 @@ public interface HrmSalaryOptionMapper extends BaseMapperX&lt;HrmSalaryOptionDO&gt; { @@ -17,7 +17,8 @@ public interface HrmSalaryOptionMapper extends BaseMapperX&lt;HrmSalaryOptionDO&gt; {
17 return selectFirstOne(HrmSalaryOptionDO::getCode, code); 17 return selectFirstOne(HrmSalaryOptionDO::getCode, code);
18 } 18 }
19 19
20 - @Select("SELECT * FROM hrm_salary_option WHERE code = #{code} LIMIT 1") 20 + @Select("SELECT * FROM hrm_salary_option WHERE code = #{code} "
  21 + + "${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@limitRows(1, false)}")
21 HrmSalaryOptionDO selectByCodeIncludeDeleted(@Param("code") Integer code); 22 HrmSalaryOptionDO selectByCodeIncludeDeleted(@Param("code") Integer code);
22 23
23 default HrmSalaryOptionDO selectByCodeForUpdate(Integer code) { 24 default HrmSalaryOptionDO selectByCodeForUpdate(Integer code) {
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/salary/monthrecord/HrmSalaryMonthRecordMapper.java
@@ -24,10 +24,9 @@ public interface HrmSalaryMonthRecordMapper extends BaseMapperX&lt;HrmSalaryMonthRe @@ -24,10 +24,9 @@ public interface HrmSalaryMonthRecordMapper extends BaseMapperX&lt;HrmSalaryMonthRe
24 } 24 }
25 25
26 default HrmSalaryMonthRecordDO selectLast() { 26 default HrmSalaryMonthRecordDO selectLast() {
27 - return selectOne(new LambdaQueryWrapperX<HrmSalaryMonthRecordDO>() 27 + return selectFirstOne(new LambdaQueryWrapperX<HrmSalaryMonthRecordDO>()
28 .orderByDesc(HrmSalaryMonthRecordDO::getYear) 28 .orderByDesc(HrmSalaryMonthRecordDO::getYear)
29 - .orderByDesc(HrmSalaryMonthRecordDO::getMonth)  
30 - .last("LIMIT 1")); 29 + .orderByDesc(HrmSalaryMonthRecordDO::getMonth));
31 } 30 }
32 31
33 default PageResult<HrmSalaryMonthRecordDO> selectPage(HrmSalaryMonthRecordPageReqVO reqVO) { 32 default PageResult<HrmSalaryMonthRecordDO> selectPage(HrmSalaryMonthRecordPageReqVO reqVO) {
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/salary/slip/HrmSalarySlipTemplateMapper.java
@@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.hrm.dal.mysql.salary.slip; @@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.hrm.dal.mysql.salary.slip;
2 2
3 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; 3 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
4 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; 4 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
5 -import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX; 5 +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
6 import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.slip.HrmSalarySlipTemplateDO; 6 import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.slip.HrmSalarySlipTemplateDO;
7 import org.apache.ibatis.annotations.Mapper; 7 import org.apache.ibatis.annotations.Mapper;
8 8
@@ -25,11 +25,10 @@ public interface HrmSalarySlipTemplateMapper extends BaseMapperX&lt;HrmSalarySlipTe @@ -25,11 +25,10 @@ public interface HrmSalarySlipTemplateMapper extends BaseMapperX&lt;HrmSalarySlipTe
25 * @return 平台级默认工资条模板 25 * @return 平台级默认工资条模板
26 */ 26 */
27 default HrmSalarySlipTemplateDO selectGlobalDefaultTemplate() { 27 default HrmSalarySlipTemplateDO selectGlobalDefaultTemplate() {
28 - return selectOne(new QueryWrapperX<HrmSalarySlipTemplateDO>()  
29 - .eq("tenant_id", 0L)  
30 - .eq("default_status", true)  
31 - .orderByDesc("id")  
32 - .limitN(1)); 28 + return selectFirstOne(new LambdaQueryWrapperX<HrmSalarySlipTemplateDO>()
  29 + .apply("tenant_id = {0}", TenantUtils.GLOBAL_TENANT_ID)
  30 + .eq(HrmSalarySlipTemplateDO::getDefaultStatus, true)
  31 + .orderByDesc(HrmSalarySlipTemplateDO::getId));
33 } 32 }
34 33
35 } 34 }
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/conversation/ImConversationReadMapper.java
@@ -57,8 +57,7 @@ public interface ImConversationReadMapper extends BaseMapperX&lt;ImConversationRead @@ -57,8 +57,7 @@ public interface ImConversationReadMapper extends BaseMapperX&lt;ImConversationRead
57 query.and(w -> w.gt(ImConversationReadDO::getUpdateTime, lastTime) 57 query.and(w -> w.gt(ImConversationReadDO::getUpdateTime, lastTime)
58 .or(n -> n.eq(ImConversationReadDO::getUpdateTime, lastTime).gt(ImConversationReadDO::getId, lastId))); 58 .or(n -> n.eq(ImConversationReadDO::getUpdateTime, lastTime).gt(ImConversationReadDO::getId, lastId)));
59 } 59 }
60 - return selectList(query.orderByAsc(ImConversationReadDO::getUpdateTime).orderByAsc(ImConversationReadDO::getId)  
61 - .last("LIMIT " + limit)); 60 + return selectListLimitN(query.orderByAsc(ImConversationReadDO::getUpdateTime).orderByAsc(ImConversationReadDO::getId), limit);
62 } 61 }
63 62
64 /** 63 /**
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/friend/ImFriendMapper.java
@@ -50,8 +50,7 @@ public interface ImFriendMapper extends BaseMapperX&lt;ImFriendDO&gt; { @@ -50,8 +50,7 @@ public interface ImFriendMapper extends BaseMapperX&lt;ImFriendDO&gt; {
50 query.and(w -> w.gt(ImFriendDO::getUpdateTime, lastTime) 50 query.and(w -> w.gt(ImFriendDO::getUpdateTime, lastTime)
51 .or(n -> n.eq(ImFriendDO::getUpdateTime, lastTime).gt(ImFriendDO::getId, lastId))); 51 .or(n -> n.eq(ImFriendDO::getUpdateTime, lastTime).gt(ImFriendDO::getId, lastId)));
52 } 52 }
53 - return selectList(query.orderByAsc(ImFriendDO::getUpdateTime).orderByAsc(ImFriendDO::getId)  
54 - .last("LIMIT " + limit)); 53 + return selectListLimitN(query.orderByAsc(ImFriendDO::getUpdateTime).orderByAsc(ImFriendDO::getId), limit);
55 } 54 }
56 55
57 default List<ImFriendDO> selectListByUserIdAndStatus(Long userId, Integer status) { 56 default List<ImFriendDO> selectListByUserIdAndStatus(Long userId, Integer status) {
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/friend/ImFriendRequestMapper.java
@@ -47,9 +47,8 @@ public interface ImFriendRequestMapper extends BaseMapperX&lt;ImFriendRequestDO&gt; { @@ -47,9 +47,8 @@ public interface ImFriendRequestMapper extends BaseMapperX&lt;ImFriendRequestDO&gt; {
47 .lt(ImFriendRequestDO::getId, maxId))); 47 .lt(ImFriendRequestDO::getId, maxId)));
48 } 48 }
49 wrapper.orderByDesc(ImFriendRequestDO::getUpdateTime) 49 wrapper.orderByDesc(ImFriendRequestDO::getUpdateTime)
50 - .orderByDesc(ImFriendRequestDO::getId)  
51 - .last("LIMIT " + limit);  
52 - return selectList(wrapper); 50 + .orderByDesc(ImFriendRequestDO::getId);
  51 + return selectListLimitN(wrapper, limit);
53 } 52 }
54 53
55 /** 54 /**
@@ -70,8 +69,8 @@ public interface ImFriendRequestMapper extends BaseMapperX&lt;ImFriendRequestDO&gt; { @@ -70,8 +69,8 @@ public interface ImFriendRequestMapper extends BaseMapperX&lt;ImFriendRequestDO&gt; {
70 query.and(w -> w.gt(ImFriendRequestDO::getUpdateTime, lastTime) 69 query.and(w -> w.gt(ImFriendRequestDO::getUpdateTime, lastTime)
71 .or(n -> n.eq(ImFriendRequestDO::getUpdateTime, lastTime).gt(ImFriendRequestDO::getId, lastId))); 70 .or(n -> n.eq(ImFriendRequestDO::getUpdateTime, lastTime).gt(ImFriendRequestDO::getId, lastId)));
72 } 71 }
73 - return selectList(query.orderByAsc(ImFriendRequestDO::getUpdateTime).orderByAsc(ImFriendRequestDO::getId)  
74 - .last("LIMIT " + limit)); 72 + return selectListLimitN(query.orderByAsc(ImFriendRequestDO::getUpdateTime).orderByAsc(ImFriendRequestDO::getId),
  73 + limit);
75 } 74 }
76 75
77 default int updateByIdAndHandleResult(Long id, Integer handleResult, ImFriendRequestDO updateObj) { 76 default int updateByIdAndHandleResult(Long id, Integer handleResult, ImFriendRequestDO updateObj) {
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/group/ImGroupRequestMapper.java
@@ -62,8 +62,7 @@ public interface ImGroupRequestMapper extends BaseMapperX&lt;ImGroupRequestDO&gt; { @@ -62,8 +62,7 @@ public interface ImGroupRequestMapper extends BaseMapperX&lt;ImGroupRequestDO&gt; {
62 query.and(w -> w.gt(ImGroupRequestDO::getUpdateTime, lastTime) 62 query.and(w -> w.gt(ImGroupRequestDO::getUpdateTime, lastTime)
63 .or(n -> n.eq(ImGroupRequestDO::getUpdateTime, lastTime).gt(ImGroupRequestDO::getId, lastId))); 63 .or(n -> n.eq(ImGroupRequestDO::getUpdateTime, lastTime).gt(ImGroupRequestDO::getId, lastId)));
64 } 64 }
65 - return selectList(query.orderByAsc(ImGroupRequestDO::getUpdateTime).orderByAsc(ImGroupRequestDO::getId)  
66 - .last("LIMIT " + limit)); 65 + return selectListLimitN(query.orderByAsc(ImGroupRequestDO::getUpdateTime).orderByAsc(ImGroupRequestDO::getId), limit);
67 } 66 }
68 67
69 default int updateByIdAndHandleResult(Long id, Integer expectedHandleResult, ImGroupRequestDO updateObj) { 68 default int updateByIdAndHandleResult(Long id, Integer expectedHandleResult, ImGroupRequestDO updateObj) {
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/message/ImChannelMessageMapper.java
@@ -30,13 +30,12 @@ public interface ImChannelMessageMapper extends BaseMapperX&lt;ImChannelMessageDO&gt; @@ -30,13 +30,12 @@ public interface ImChannelMessageMapper extends BaseMapperX&lt;ImChannelMessageDO&gt;
30 * @return 频道消息列表;按 id 升序 30 * @return 频道消息列表;按 id 升序
31 */ 31 */
32 default List<ImChannelMessageDO> selectListByUserAndMinId(Long userId, Long minId, Integer size) { 32 default List<ImChannelMessageDO> selectListByUserAndMinId(Long userId, Long minId, Integer size) {
33 - return selectList(new LambdaQueryWrapperX<ImChannelMessageDO>() 33 + return selectListLimitN(new LambdaQueryWrapperX<ImChannelMessageDO>()
34 .gt(ImChannelMessageDO::getId, minId) 34 .gt(ImChannelMessageDO::getId, minId)
35 .and(w -> w.isNull(ImChannelMessageDO::getReceiverUserIds) 35 .and(w -> w.isNull(ImChannelMessageDO::getReceiverUserIds)
36 .or().eq(ImChannelMessageDO::getReceiverUserIds, Collections.emptyList()) 36 .or().eq(ImChannelMessageDO::getReceiverUserIds, Collections.emptyList())
37 .or().apply(MyBatisUtils.findInSet("receiver_user_ids"), userId)) 37 .or().apply(MyBatisUtils.findInSet("receiver_user_ids"), userId))
38 - .orderByAsc(ImChannelMessageDO::getId)  
39 - .last("LIMIT " + size)); 38 + .orderByAsc(ImChannelMessageDO::getId), size);
40 } 39 }
41 40
42 default Long selectCountByMaterialId(Long materialId) { 41 default Long selectCountByMaterialId(Long materialId) {
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/message/ImGroupMessageMapper.java
@@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.im.dal.mysql.message; @@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.im.dal.mysql.message;
3 import cn.iocoder.yudao.framework.common.pojo.PageResult; 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; 4 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
5 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; 5 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
6 -import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;  
7 import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; 6 import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
8 import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.group.ImGroupMessageManagerPageReqVO; 7 import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.group.ImGroupMessageManagerPageReqVO;
9 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO; 8 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO;
@@ -35,14 +34,13 @@ public interface ImGroupMessageMapper extends BaseMapperX&lt;ImGroupMessageDO&gt; { @@ -35,14 +34,13 @@ public interface ImGroupMessageMapper extends BaseMapperX&lt;ImGroupMessageDO&gt; {
35 */ 34 */
36 default List<ImGroupMessageDO> selectListByMinId(Long userId, Collection<Long> groupIds, Long minId, 35 default List<ImGroupMessageDO> selectListByMinId(Long userId, Collection<Long> groupIds, Long minId,
37 LocalDateTime minSendTime, Integer size) { 36 LocalDateTime minSendTime, Integer size) {
38 - QueryWrapperX<ImGroupMessageDO> wrapper = new QueryWrapperX<>();  
39 - wrapper.in("group_id", groupIds)  
40 - .gt("id", minId)  
41 - .gt("send_time", minSendTime) 37 + LambdaQueryWrapperX<ImGroupMessageDO> wrapper = new LambdaQueryWrapperX<>();
  38 + wrapper.in(ImGroupMessageDO::getGroupId, groupIds)
  39 + .gt(ImGroupMessageDO::getId, minId)
  40 + .gt(ImGroupMessageDO::getSendTime, minSendTime)
42 .apply(MyBatisUtils.findInSet("receiver_user_ids"), userId) 41 .apply(MyBatisUtils.findInSet("receiver_user_ids"), userId)
43 - .orderByAsc("id");  
44 - wrapper.limitN(size);  
45 - return selectList(wrapper); 42 + .orderByAsc(ImGroupMessageDO::getId);
  43 + return selectListLimitN(wrapper, size);
46 } 44 }
47 45
48 /** 46 /**
@@ -55,13 +53,12 @@ public interface ImGroupMessageMapper extends BaseMapperX&lt;ImGroupMessageDO&gt; { @@ -55,13 +53,12 @@ public interface ImGroupMessageMapper extends BaseMapperX&lt;ImGroupMessageDO&gt; {
55 * @return 消息列表(按 id 倒序) 53 * @return 消息列表(按 id 倒序)
56 */ 54 */
57 default List<ImGroupMessageDO> selectHistoryListByUser(Long userId, Long groupId, Long maxId, Integer limit) { 55 default List<ImGroupMessageDO> selectHistoryListByUser(Long userId, Long groupId, Long maxId, Integer limit) {
58 - QueryWrapperX<ImGroupMessageDO> wrapper = new QueryWrapperX<>();  
59 - wrapper.eq("group_id", groupId)  
60 - .lt(maxId != null, "id", maxId) 56 + LambdaQueryWrapperX<ImGroupMessageDO> wrapper = new LambdaQueryWrapperX<>();
  57 + wrapper.eq(ImGroupMessageDO::getGroupId, groupId)
  58 + .lt(maxId != null, ImGroupMessageDO::getId, maxId)
61 .apply(MyBatisUtils.findInSet("receiver_user_ids"), userId) 59 .apply(MyBatisUtils.findInSet("receiver_user_ids"), userId)
62 - .orderByDesc("id");  
63 - wrapper.limitN(limit);  
64 - return selectList(wrapper); 60 + .orderByDesc(ImGroupMessageDO::getId);
  61 + return selectListLimitN(wrapper, limit);
65 } 62 }
66 63
67 default ImGroupMessageDO selectBySenderIdAndClientMessageId(Long senderId, String clientMessageId) { 64 default ImGroupMessageDO selectBySenderIdAndClientMessageId(Long senderId, String clientMessageId) {
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/message/ImPrivateMessageMapper.java
@@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.im.dal.mysql.message; @@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.im.dal.mysql.message;
3 import cn.iocoder.yudao.framework.common.pojo.PageResult; 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; 4 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
5 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; 5 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
6 -import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;  
7 import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.privates.ImPrivateMessageManagerPageReqVO; 6 import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.privates.ImPrivateMessageManagerPageReqVO;
8 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO; 7 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO;
9 import org.apache.ibatis.annotations.Mapper; 8 import org.apache.ibatis.annotations.Mapper;
@@ -30,15 +29,14 @@ public interface ImPrivateMessageMapper extends BaseMapperX&lt;ImPrivateMessageDO&gt; @@ -30,15 +29,14 @@ public interface ImPrivateMessageMapper extends BaseMapperX&lt;ImPrivateMessageDO&gt;
30 */ 29 */
31 default List<ImPrivateMessageDO> selectListByMinId(Long userId, Long minId, 30 default List<ImPrivateMessageDO> selectListByMinId(Long userId, Long minId,
32 LocalDateTime minSendTime, Integer size) { 31 LocalDateTime minSendTime, Integer size) {
33 - QueryWrapperX<ImPrivateMessageDO> wrapper = new QueryWrapperX<>();  
34 - wrapper.and(w -> w.eq("sender_id", userId) 32 + LambdaQueryWrapperX<ImPrivateMessageDO> wrapper = new LambdaQueryWrapperX<>();
  33 + wrapper.and(w -> w.eq(ImPrivateMessageDO::getSenderId, userId)
35 .or() 34 .or()
36 - .eq("receiver_id", userId))  
37 - .gt("id", minId)  
38 - .gt("send_time", minSendTime)  
39 - .orderByAsc("id");  
40 - wrapper.limitN(size);  
41 - return selectList(wrapper); 35 + .eq(ImPrivateMessageDO::getReceiverId, userId))
  36 + .gt(ImPrivateMessageDO::getId, minId)
  37 + .gt(ImPrivateMessageDO::getSendTime, minSendTime)
  38 + .orderByAsc(ImPrivateMessageDO::getId);
  39 + return selectListLimitN(wrapper, size);
42 } 40 }
43 41
44 /** 42 /**
@@ -51,14 +49,13 @@ public interface ImPrivateMessageMapper extends BaseMapperX&lt;ImPrivateMessageDO&gt; @@ -51,14 +49,13 @@ public interface ImPrivateMessageMapper extends BaseMapperX&lt;ImPrivateMessageDO&gt;
51 * @return 消息列表(按 id 倒序) 49 * @return 消息列表(按 id 倒序)
52 */ 50 */
53 default List<ImPrivateMessageDO> selectHistoryList(Long userId, Long receiverId, Long maxId, Integer limit) { 51 default List<ImPrivateMessageDO> selectHistoryList(Long userId, Long receiverId, Long maxId, Integer limit) {
54 - QueryWrapperX<ImPrivateMessageDO> wrapper = new QueryWrapperX<>();  
55 - wrapper.and(w -> w.eq("sender_id", userId).eq("receiver_id", receiverId) 52 + LambdaQueryWrapperX<ImPrivateMessageDO> wrapper = new LambdaQueryWrapperX<>();
  53 + wrapper.and(w -> w.eq(ImPrivateMessageDO::getSenderId, userId).eq(ImPrivateMessageDO::getReceiverId, receiverId)
56 .or() 54 .or()
57 - .eq("sender_id", receiverId).eq("receiver_id", userId))  
58 - .lt(maxId != null, "id", maxId)  
59 - .orderByDesc("id");  
60 - wrapper.limitN(limit);  
61 - return selectList(wrapper); 55 + .eq(ImPrivateMessageDO::getSenderId, receiverId).eq(ImPrivateMessageDO::getReceiverId, userId))
  56 + .lt(maxId != null, ImPrivateMessageDO::getId, maxId)
  57 + .orderByDesc(ImPrivateMessageDO::getId);
  58 + return selectListLimitN(wrapper, limit);
62 } 59 }
63 60
64 default ImPrivateMessageDO selectBySenderIdAndClientMessageId(Long senderId, String clientMessageId) { 61 default ImPrivateMessageDO selectBySenderIdAndClientMessageId(Long senderId, String clientMessageId) {
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/statistics/ImStatisticsManagerMapper.java
@@ -58,9 +58,9 @@ public interface ImStatisticsManagerMapper { @@ -58,9 +58,9 @@ public interface ImStatisticsManagerMapper {
58 * 58 *
59 * @return [{statDate: "yyyy-MM-dd", count: 123}, ...] 59 * @return [{statDate: "yyyy-MM-dd", count: 123}, ...]
60 */ 60 */
61 - @Select("SELECT DATE(create_time) AS statDate, COUNT(*) AS count FROM system_users " + 61 + @Select("SELECT ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@date('create_time')} AS statDate, COUNT(*) AS count FROM system_users " +
62 "WHERE deleted = 0 AND create_time >= #{beginTime} AND create_time < #{endTime} " + 62 "WHERE deleted = 0 AND create_time >= #{beginTime} AND create_time < #{endTime} " +
63 - "GROUP BY DATE(create_time)") 63 + "GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@date('create_time')}")
64 @ResultMap(CaseInsensitiveResultMap.RESULT_MAP_ID) 64 @ResultMap(CaseInsensitiveResultMap.RESULT_MAP_ID)
65 List<Map<String, Object>> selectNewUserDailyCount(@Param("beginTime") LocalDateTime beginTime, 65 List<Map<String, Object>> selectNewUserDailyCount(@Param("beginTime") LocalDateTime beginTime,
66 @Param("endTime") LocalDateTime endTime); 66 @Param("endTime") LocalDateTime endTime);
@@ -69,11 +69,11 @@ public interface ImStatisticsManagerMapper { @@ -69,11 +69,11 @@ public interface ImStatisticsManagerMapper {
69 * 区间内每日活跃用户数(按天分组、跨私聊+群聊去重) 69 * 区间内每日活跃用户数(按天分组、跨私聊+群聊去重)
70 */ 70 */
71 @Select("SELECT day AS statDate, COUNT(DISTINCT user_id) AS count FROM (" + 71 @Select("SELECT day AS statDate, COUNT(DISTINCT user_id) AS count FROM (" +
72 - " SELECT DATE(send_time) AS day, sender_id AS user_id FROM im_private_message " + 72 + " SELECT ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@date('send_time')} AS day, sender_id AS user_id FROM im_private_message " +
73 " WHERE deleted = 0 AND " + NORMAL_MESSAGE_CONDITION + 73 " WHERE deleted = 0 AND " + NORMAL_MESSAGE_CONDITION +
74 " AND send_time >= #{beginTime} AND send_time < #{endTime}" + 74 " AND send_time >= #{beginTime} AND send_time < #{endTime}" +
75 " UNION ALL " + 75 " UNION ALL " +
76 - " SELECT DATE(send_time) AS day, sender_id AS user_id FROM im_group_message " + 76 + " SELECT ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@date('send_time')} AS day, sender_id AS user_id FROM im_group_message " +
77 " WHERE deleted = 0 AND " + NORMAL_MESSAGE_CONDITION + 77 " WHERE deleted = 0 AND " + NORMAL_MESSAGE_CONDITION +
78 " AND send_time >= #{beginTime} AND send_time < #{endTime}" + 78 " AND send_time >= #{beginTime} AND send_time < #{endTime}" +
79 ") t GROUP BY day") 79 ") t GROUP BY day")
@@ -143,10 +143,10 @@ public interface ImStatisticsManagerMapper { @@ -143,10 +143,10 @@ public interface ImStatisticsManagerMapper {
143 /** 143 /**
144 * 区间内每日私聊消息数(按天分组) 144 * 区间内每日私聊消息数(按天分组)
145 */ 145 */
146 - @Select("SELECT DATE(send_time) AS statDate, COUNT(*) AS count FROM im_private_message " + 146 + @Select("SELECT ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@date('send_time')} AS statDate, COUNT(*) AS count FROM im_private_message " +
147 "WHERE deleted = 0 AND " + NORMAL_MESSAGE_CONDITION + 147 "WHERE deleted = 0 AND " + NORMAL_MESSAGE_CONDITION +
148 " AND send_time >= #{beginTime} AND send_time < #{endTime} " + 148 " AND send_time >= #{beginTime} AND send_time < #{endTime} " +
149 - "GROUP BY DATE(send_time)") 149 + "GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@date('send_time')}")
150 @ResultMap(CaseInsensitiveResultMap.RESULT_MAP_ID) 150 @ResultMap(CaseInsensitiveResultMap.RESULT_MAP_ID)
151 List<Map<String, Object>> selectPrivateMessageDailyCount(@Param("beginTime") LocalDateTime beginTime, 151 List<Map<String, Object>> selectPrivateMessageDailyCount(@Param("beginTime") LocalDateTime beginTime,
152 @Param("endTime") LocalDateTime endTime); 152 @Param("endTime") LocalDateTime endTime);
@@ -154,10 +154,10 @@ public interface ImStatisticsManagerMapper { @@ -154,10 +154,10 @@ public interface ImStatisticsManagerMapper {
154 /** 154 /**
155 * 区间内每日群聊消息数(按天分组) 155 * 区间内每日群聊消息数(按天分组)
156 */ 156 */
157 - @Select("SELECT DATE(send_time) AS statDate, COUNT(*) AS count FROM im_group_message " + 157 + @Select("SELECT ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@date('send_time')} AS statDate, COUNT(*) AS count FROM im_group_message " +
158 "WHERE deleted = 0 AND " + NORMAL_MESSAGE_CONDITION + 158 "WHERE deleted = 0 AND " + NORMAL_MESSAGE_CONDITION +
159 " AND send_time >= #{beginTime} AND send_time < #{endTime} " + 159 " AND send_time >= #{beginTime} AND send_time < #{endTime} " +
160 - "GROUP BY DATE(send_time)") 160 + "GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@date('send_time')}")
161 @ResultMap(CaseInsensitiveResultMap.RESULT_MAP_ID) 161 @ResultMap(CaseInsensitiveResultMap.RESULT_MAP_ID)
162 List<Map<String, Object>> selectGroupMessageDailyCount(@Param("beginTime") LocalDateTime beginTime, 162 List<Map<String, Object>> selectGroupMessageDailyCount(@Param("beginTime") LocalDateTime beginTime,
163 @Param("endTime") LocalDateTime endTime); 163 @Param("endTime") LocalDateTime endTime);
@@ -193,7 +193,8 @@ public interface ImStatisticsManagerMapper { @@ -193,7 +193,8 @@ public interface ImStatisticsManagerMapper {
193 " SELECT sender_id AS user_id FROM im_group_message " + 193 " SELECT sender_id AS user_id FROM im_group_message " +
194 " WHERE deleted = 0 AND " + NORMAL_MESSAGE_CONDITION + 194 " WHERE deleted = 0 AND " + NORMAL_MESSAGE_CONDITION +
195 " AND send_time >= #{beginTime} AND send_time < #{endTime}" + 195 " AND send_time >= #{beginTime} AND send_time < #{endTime}" +
196 - ") t GROUP BY user_id ORDER BY messageCount DESC LIMIT #{limit}") 196 + ") t GROUP BY user_id ORDER BY messageCount DESC " +
  197 + "${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@limitParam('limit', true)}")
197 @ResultMap(CaseInsensitiveResultMap.RESULT_MAP_ID) 198 @ResultMap(CaseInsensitiveResultMap.RESULT_MAP_ID)
198 List<Map<String, Object>> selectTopSenders(@Param("beginTime") LocalDateTime beginTime, 199 List<Map<String, Object>> selectTopSenders(@Param("beginTime") LocalDateTime beginTime,
199 @Param("endTime") LocalDateTime endTime, 200 @Param("endTime") LocalDateTime endTime,
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/job/JobLogMapper.java
@@ -37,7 +37,7 @@ public interface JobLogMapper extends BaseMapperX&lt;JobLogDO&gt; { @@ -37,7 +37,7 @@ public interface JobLogMapper extends BaseMapperX&lt;JobLogDO&gt; {
37 * @param limit 删除条数,防止一次删除太多 37 * @param limit 删除条数,防止一次删除太多
38 * @return 删除条数 38 * @return 删除条数
39 */ 39 */
40 - @Delete("DELETE FROM infra_job_log WHERE create_time < #{createTime} LIMIT #{limit}") 40 + @Delete("${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@deleteBatchByLessThan('infra_job_log', 'create_time', 'createTime', 'limit')}")
41 Integer deleteByCreateTimeLt(@Param("createTime") LocalDateTime createTime, @Param("limit") Integer limit); 41 Integer deleteByCreateTimeLt(@Param("createTime") LocalDateTime createTime, @Param("limit") Integer limit);
42 42
43 } 43 }
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/logger/ApiAccessLogMapper.java
@@ -39,7 +39,7 @@ public interface ApiAccessLogMapper extends BaseMapperX&lt;ApiAccessLogDO&gt; { @@ -39,7 +39,7 @@ public interface ApiAccessLogMapper extends BaseMapperX&lt;ApiAccessLogDO&gt; {
39 * @param limit 删除条数,防止一次删除太多 39 * @param limit 删除条数,防止一次删除太多
40 * @return 删除条数 40 * @return 删除条数
41 */ 41 */
42 - @Delete("DELETE FROM infra_api_access_log WHERE create_time < #{createTime} LIMIT #{limit}") 42 + @Delete("${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@deleteBatchByLessThan('infra_api_access_log', 'create_time', 'createTime', 'limit')}")
43 Integer deleteByCreateTimeLt(@Param("createTime") LocalDateTime createTime, @Param("limit") Integer limit); 43 Integer deleteByCreateTimeLt(@Param("createTime") LocalDateTime createTime, @Param("limit") Integer limit);
44 44
45 } 45 }
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/logger/ApiErrorLogMapper.java
@@ -38,7 +38,7 @@ public interface ApiErrorLogMapper extends BaseMapperX&lt;ApiErrorLogDO&gt; { @@ -38,7 +38,7 @@ public interface ApiErrorLogMapper extends BaseMapperX&lt;ApiErrorLogDO&gt; {
38 * @param limit 删除条数,防止一次删除太多 38 * @param limit 删除条数,防止一次删除太多
39 * @return 删除条数 39 * @return 删除条数
40 */ 40 */
41 - @Delete("DELETE FROM infra_api_error_log WHERE create_time < #{createTime} LIMIT #{limit}") 41 + @Delete("${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@deleteBatchByLessThan('infra_api_error_log', 'create_time', 'createTime', 'limit')}")
42 Integer deleteByCreateTimeLt(@Param("createTime") LocalDateTime createTime, @Param("limit") Integer limit); 42 Integer deleteByCreateTimeLt(@Param("createTime") LocalDateTime createTime, @Param("limit") Integer limit);
43 43
44 } 44 }
yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/bargain/BargainActivityMapper.java
@@ -76,11 +76,11 @@ public interface BargainActivityMapper extends BaseMapperX&lt;BargainActivityDO&gt; { @@ -76,11 +76,11 @@ public interface BargainActivityMapper extends BaseMapperX&lt;BargainActivityDO&gt; {
76 * @return 活动分页 76 * @return 活动分页
77 */ 77 */
78 default List<BargainActivityDO> selectList(Integer count, Integer status, LocalDateTime now) { 78 default List<BargainActivityDO> selectList(Integer count, Integer status, LocalDateTime now) {
79 - return selectList(new LambdaQueryWrapperX<BargainActivityDO>() 79 + return selectListLimitN(new LambdaQueryWrapperX<BargainActivityDO>()
80 .eq(BargainActivityDO::getStatus, status) 80 .eq(BargainActivityDO::getStatus, status)
81 .le(BargainActivityDO::getStartTime, now) 81 .le(BargainActivityDO::getStartTime, now)
82 .ge(BargainActivityDO::getEndTime, now) 82 .ge(BargainActivityDO::getEndTime, now)
83 - .last("LIMIT " + count)); 83 + .orderByAsc(BargainActivityDO::getId), count);
84 } 84 }
85 85
86 default BargainActivityDO selectBySpuIdAndStatusAndNow(Long spuId, Integer status) { 86 default BargainActivityDO selectBySpuIdAndStatusAndNow(Long spuId, Integer status) {
yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/bargain/BargainRecordMapper.java
@@ -43,11 +43,10 @@ public interface BargainRecordMapper extends BaseMapperX&lt;BargainRecordDO&gt; { @@ -43,11 +43,10 @@ public interface BargainRecordMapper extends BaseMapperX&lt;BargainRecordDO&gt; {
43 } 43 }
44 44
45 default BargainRecordDO selectLastByUserIdAndActivityId(Long userId, Long activityId) { 45 default BargainRecordDO selectLastByUserIdAndActivityId(Long userId, Long activityId) {
46 - return selectOne(new LambdaQueryWrapper<>(BargainRecordDO.class) 46 + return selectFirstOne(new LambdaQueryWrapper<>(BargainRecordDO.class)
47 .eq(BargainRecordDO::getUserId, userId) 47 .eq(BargainRecordDO::getUserId, userId)
48 .eq(BargainRecordDO::getActivityId, activityId) 48 .eq(BargainRecordDO::getActivityId, activityId)
49 - .orderByDesc(BargainRecordDO::getId)  
50 - .last("LIMIT 1")); 49 + .orderByDesc(BargainRecordDO::getId));
51 } 50 }
52 51
53 default Long selectCountByUserIdAndActivityIdAndStatus( 52 default Long selectCountByUserIdAndActivityIdAndStatus(
@@ -104,9 +103,9 @@ public interface BargainRecordMapper extends BaseMapperX&lt;BargainRecordDO&gt; { @@ -104,9 +103,9 @@ public interface BargainRecordMapper extends BaseMapperX&lt;BargainRecordDO&gt; {
104 } 103 }
105 104
106 default List<BargainRecordDO> selectListByStatusAndCount(Integer status, Integer count) { 105 default List<BargainRecordDO> selectListByStatusAndCount(Integer status, Integer count) {
107 - return selectList(new LambdaQueryWrapper<>(BargainRecordDO.class) 106 + return selectListLimitN(new LambdaQueryWrapper<>(BargainRecordDO.class)
108 .eq(BargainRecordDO::getStatus, status) 107 .eq(BargainRecordDO::getStatus, status)
109 - .last("LIMIT " + count)); 108 + .orderByAsc(BargainRecordDO::getId), count);
110 } 109 }
111 110
112 /** 111 /**
yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/combination/CombinationRecordMapper.java
@@ -63,19 +63,17 @@ public interface CombinationRecordMapper extends BaseMapperX&lt;CombinationRecordDO @@ -63,19 +63,17 @@ public interface CombinationRecordMapper extends BaseMapperX&lt;CombinationRecordDO
63 * @return 拼团记录列表 63 * @return 拼团记录列表
64 */ 64 */
65 default List<CombinationRecordDO> selectLatestList(int count) { 65 default List<CombinationRecordDO> selectLatestList(int count) {
66 - return selectList(new LambdaQueryWrapperX<CombinationRecordDO>()  
67 - .orderByDesc(CombinationRecordDO::getId)  
68 - .last("LIMIT " + count)); 66 + return selectListLimitN(new LambdaQueryWrapperX<CombinationRecordDO>()
  67 + .orderByDesc(CombinationRecordDO::getId), count);
69 } 68 }
70 69
71 default List<CombinationRecordDO> selectListByActivityIdAndStatusAndHeadId(Long activityId, Integer status, 70 default List<CombinationRecordDO> selectListByActivityIdAndStatusAndHeadId(Long activityId, Integer status,
72 Long headId, Integer count) { 71 Long headId, Integer count) {
73 - return selectList(new LambdaQueryWrapperX<CombinationRecordDO>() 72 + return selectListLimitN(new LambdaQueryWrapperX<CombinationRecordDO>()
74 .eqIfPresent(CombinationRecordDO::getActivityId, activityId) 73 .eqIfPresent(CombinationRecordDO::getActivityId, activityId)
75 .eqIfPresent(CombinationRecordDO::getStatus, status) 74 .eqIfPresent(CombinationRecordDO::getStatus, status)
76 .eq(CombinationRecordDO::getHeadId, headId) 75 .eq(CombinationRecordDO::getHeadId, headId)
77 - .orderByDesc(CombinationRecordDO::getId)  
78 - .last("LIMIT " + count)); 76 + .orderByDesc(CombinationRecordDO::getId), count);
79 } 77 }
80 78
81 default Map<Long, Integer> selectCombinationRecordCountMapByActivityIdAndStatusAndHeadId(Collection<Long> activityIds, 79 default Map<Long, Integer> selectCombinationRecordCountMapByActivityIdAndStatusAndHeadId(Collection<Long> activityIds,
yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponTemplateMapper.java 100755 → 100644
@@ -60,13 +60,12 @@ public interface CouponTemplateMapper extends BaseMapperX&lt;CouponTemplateDO&gt; { @@ -60,13 +60,12 @@ public interface CouponTemplateMapper extends BaseMapperX&lt;CouponTemplateDO&gt; {
60 default List<CouponTemplateDO> selectList(List<Integer> canTakeTypes, Integer productScope, Long productScopeValue, Integer count) { 60 default List<CouponTemplateDO> selectList(List<Integer> canTakeTypes, Integer productScope, Long productScopeValue, Integer count) {
61 // 构建可领取的查询条件 61 // 构建可领取的查询条件
62 Consumer<LambdaQueryWrapper<CouponTemplateDO>> canTakeConsumer = buildCanTakeQueryConsumer(canTakeTypes); 62 Consumer<LambdaQueryWrapper<CouponTemplateDO>> canTakeConsumer = buildCanTakeQueryConsumer(canTakeTypes);
63 - return selectList(new LambdaQueryWrapperX<CouponTemplateDO>() 63 + return selectListLimitN(new LambdaQueryWrapperX<CouponTemplateDO>()
64 .eqIfPresent(CouponTemplateDO::getProductScope, productScope) 64 .eqIfPresent(CouponTemplateDO::getProductScope, productScope)
65 .and(productScopeValue != null, w -> w.apply(MyBatisUtils.findInSet("product_scope_values"), 65 .and(productScopeValue != null, w -> w.apply(MyBatisUtils.findInSet("product_scope_values"),
66 productScopeValue)) 66 productScopeValue))
67 .and(canTakeConsumer != null, canTakeConsumer) 67 .and(canTakeConsumer != null, canTakeConsumer)
68 - .last(" LIMIT " + count)  
69 - .orderByDesc(CouponTemplateDO::getId)); 68 + .orderByDesc(CouponTemplateDO::getId), count);
70 } 69 }
71 70
72 static Consumer<LambdaQueryWrapper<CouponTemplateDO>> buildCanTakeQueryConsumer(List<Integer> canTakeTypes) { 71 static Consumer<LambdaQueryWrapper<CouponTemplateDO>> buildCanTakeQueryConsumer(List<Integer> canTakeTypes) {
yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/kefu/KeFuMessageMapper.java
1 package cn.iocoder.yudao.module.promotion.dal.mysql.kefu; 1 package cn.iocoder.yudao.module.promotion.dal.mysql.kefu;
2 2
3 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; 3 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
4 -import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX; 4 +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
5 import cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.message.KeFuMessageListReqVO; 5 import cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.message.KeFuMessageListReqVO;
6 import cn.iocoder.yudao.module.promotion.dal.dataobject.kefu.KeFuMessageDO; 6 import cn.iocoder.yudao.module.promotion.dal.dataobject.kefu.KeFuMessageDO;
7 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 7 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
@@ -28,11 +28,10 @@ public interface KeFuMessageMapper extends BaseMapperX&lt;KeFuMessageDO&gt; { @@ -28,11 +28,10 @@ public interface KeFuMessageMapper extends BaseMapperX&lt;KeFuMessageDO&gt; {
28 * @return 消息列表 28 * @return 消息列表
29 */ 29 */
30 default List<KeFuMessageDO> selectList(KeFuMessageListReqVO reqVO) { 30 default List<KeFuMessageDO> selectList(KeFuMessageListReqVO reqVO) {
31 - return selectList(new QueryWrapperX<KeFuMessageDO>()  
32 - .eqIfPresent("conversation_id", reqVO.getConversationId())  
33 - .ltIfPresent("create_time", reqVO.getCreateTime())  
34 - .orderByDesc("create_time")  
35 - .limitN(reqVO.getLimit())); 31 + return selectListLimitN(new LambdaQueryWrapperX<KeFuMessageDO>()
  32 + .eqIfPresent(KeFuMessageDO::getConversationId, reqVO.getConversationId())
  33 + .ltIfPresent(KeFuMessageDO::getCreateTime, reqVO.getCreateTime())
  34 + .orderByDesc(KeFuMessageDO::getCreateTime), reqVO.getLimit());
36 } 35 }
37 36
38 default List<KeFuMessageDO> selectListByConversationIdAndUserTypeAndReadStatus(Long conversationId, Integer userType, 37 default List<KeFuMessageDO> selectListByConversationIdAndUserTypeAndReadStatus(Long conversationId, Integer userType,
yudao-module-mall/yudao-module-statistics/src/main/resources/mapper/member/MemberStatisticsMapper.xml
@@ -57,7 +57,7 @@ @@ -57,7 +57,7 @@
57 </resultMap> 57 </resultMap>
58 58
59 <select id="selectListByCreateTimeBetween" resultMap="MemberRegisterCountRespVOMap"> 59 <select id="selectListByCreateTimeBetween" resultMap="MemberRegisterCountRespVOMap">
60 - SELECT CAST(create_time AS DATE) AS statDate, 60 + SELECT ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@castDate('create_time')} AS statDate,
61 count(1) AS count 61 count(1) AS count
62 FROM member_user 62 FROM member_user
63 WHERE create_time BETWEEN #{beginTime} AND #{endTime} 63 WHERE create_time BETWEEN #{beginTime} AND #{endTime}
@@ -65,7 +65,7 @@ @@ -65,7 +65,7 @@
65 <if test="@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId() != null"> 65 <if test="@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId() != null">
66 AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId()} 66 AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId()}
67 </if> 67 </if>
68 - GROUP BY CAST(create_time AS DATE) 68 + GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@castDate('create_time')}
69 ORDER BY statDate 69 ORDER BY statDate
70 </select> 70 </select>
71 71
yudao-module-mall/yudao-module-statistics/src/main/resources/mapper/trade/TradeOrderStatisticsMapper.xml
@@ -99,7 +99,7 @@ @@ -99,7 +99,7 @@
99 </resultMap> 99 </resultMap>
100 100
101 <select id="selectListByPayTimeBetweenAndGroupByDay" resultMap="TradeOrderTrendRespVOMap"> 101 <select id="selectListByPayTimeBetweenAndGroupByDay" resultMap="TradeOrderTrendRespVOMap">
102 - SELECT CAST(pay_time AS DATE) AS statDate, 102 + SELECT ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@castDateText('pay_time')} AS statDate,
103 COUNT(1) AS order_pay_count, 103 COUNT(1) AS order_pay_count,
104 SUM(pay_price) AS order_pay_price 104 SUM(pay_price) AS order_pay_price
105 FROM trade_order 105 FROM trade_order
@@ -109,13 +109,12 @@ @@ -109,13 +109,12 @@
109 <if test="@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId() != null"> 109 <if test="@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId() != null">
110 AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId()} 110 AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId()}
111 </if> 111 </if>
112 - GROUP BY CAST(pay_time AS DATE) 112 + GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@castDateText('pay_time')}
113 ORDER BY statDate 113 ORDER BY statDate
114 </select> 114 </select>
115 115
116 <select id="selectListByPayTimeBetweenAndGroupByMonth" resultMap="TradeOrderTrendRespVOMap"> 116 <select id="selectListByPayTimeBetweenAndGroupByMonth" resultMap="TradeOrderTrendRespVOMap">
117 - SELECT CONCAT(EXTRACT(YEAR FROM pay_time), '-',  
118 - LPAD(CONCAT(EXTRACT(MONTH FROM pay_time)), 2, '0')) AS statDate, 117 + SELECT ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatYearDashMonth('pay_time')} AS statDate,
119 COUNT(1) AS order_pay_count, 118 COUNT(1) AS order_pay_count,
120 SUM(pay_price) AS order_pay_price 119 SUM(pay_price) AS order_pay_price
121 FROM trade_order 120 FROM trade_order
@@ -125,8 +124,7 @@ @@ -125,8 +124,7 @@
125 <if test="@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId() != null"> 124 <if test="@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId() != null">
126 AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId()} 125 AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId()}
127 </if> 126 </if>
128 - GROUP BY CONCAT(EXTRACT(YEAR FROM pay_time), '-',  
129 - LPAD(CONCAT(EXTRACT(MONTH FROM pay_time)), 2, '0')) 127 + GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatYearDashMonth('pay_time')}
130 ORDER BY statDate 128 ORDER BY statDate
131 </select> 129 </select>
132 130
yudao-module-member/src/main/java/cn/iocoder/yudao/module/member/dal/mysql/signin/MemberSignInRecordMapper.java
@@ -6,7 +6,6 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; @@ -6,7 +6,6 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
6 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; 6 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
7 import cn.iocoder.yudao.module.member.controller.admin.signin.vo.record.MemberSignInRecordPageReqVO; 7 import cn.iocoder.yudao.module.member.controller.admin.signin.vo.record.MemberSignInRecordPageReqVO;
8 import cn.iocoder.yudao.module.member.dal.dataobject.signin.MemberSignInRecordDO; 8 import cn.iocoder.yudao.module.member.dal.dataobject.signin.MemberSignInRecordDO;
9 -import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;  
10 import org.apache.ibatis.annotations.Mapper; 9 import org.apache.ibatis.annotations.Mapper;
11 10
12 import java.util.List; 11 import java.util.List;
@@ -42,10 +41,9 @@ public interface MemberSignInRecordMapper extends BaseMapperX&lt;MemberSignInRecord @@ -42,10 +41,9 @@ public interface MemberSignInRecordMapper extends BaseMapperX&lt;MemberSignInRecord
42 * @return 签到记录列表 41 * @return 签到记录列表
43 */ 42 */
44 default MemberSignInRecordDO selectLastRecordByUserId(Long userId) { 43 default MemberSignInRecordDO selectLastRecordByUserId(Long userId) {
45 - return selectOne(new QueryWrapper<MemberSignInRecordDO>()  
46 - .eq("user_id", userId)  
47 - .orderByDesc("create_time")  
48 - .last("limit 1")); 44 + return selectFirstOne(new LambdaQueryWrapperX<MemberSignInRecordDO>()
  45 + .eq(MemberSignInRecordDO::getUserId, userId)
  46 + .orderByDesc(MemberSignInRecordDO::getCreateTime));
49 } 47 }
50 48
51 default Long selectCountByUserId(Long userId) { 49 default Long selectCountByUserId(Long userId) {
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/batch/MesWmBatchMapper.java
@@ -133,8 +133,7 @@ public interface MesWmBatchMapper extends BaseMapperX&lt;MesWmBatchDO&gt; { @@ -133,8 +133,7 @@ public interface MesWmBatchMapper extends BaseMapperX&lt;MesWmBatchDO&gt; {
133 133
134 // 返回 ID 最小的批次 134 // 返回 ID 最小的批次
135 query.orderByAsc(MesWmBatchDO::getId); 135 query.orderByAsc(MesWmBatchDO::getId);
136 - query.last("LIMIT 1");  
137 - return selectOne(query); 136 + return selectFirstOne(query);
138 } 137 }
139 138
140 /** 139 /**
yudao-module-mes/src/main/resources/mapper/home/MesHomeStatisticsMapper.xml
@@ -52,7 +52,7 @@ @@ -52,7 +52,7 @@
52 52
53 <!-- 按天聚合报工产量 --> 53 <!-- 按天聚合报工产量 -->
54 <select id="selectDailyFeedbackTrend" resultType="cn.iocoder.yudao.framework.mybatis.core.util.CaseInsensitiveResultMap"> 54 <select id="selectDailyFeedbackTrend" resultType="cn.iocoder.yudao.framework.mybatis.core.util.CaseInsensitiveResultMap">
55 - SELECT DATE_FORMAT(feedback_time, '%Y-%m-%d') AS statDate, 55 + SELECT ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('feedback_time')} AS statDate,
56 COALESCE(SUM(feedback_quantity), 0) AS quantity, 56 COALESCE(SUM(feedback_quantity), 0) AS quantity,
57 COALESCE(SUM(qualified_quantity), 0) AS qualifiedQuantity, 57 COALESCE(SUM(qualified_quantity), 0) AS qualifiedQuantity,
58 COALESCE(SUM(unqualified_quantity), 0) AS unqualifiedQuantity 58 COALESCE(SUM(unqualified_quantity), 0) AS unqualifiedQuantity
@@ -63,7 +63,7 @@ @@ -63,7 +63,7 @@
63 <if test="endTime != null"> 63 <if test="endTime != null">
64 AND feedback_time &lt; #{endTime} 64 AND feedback_time &lt; #{endTime}
65 </if> 65 </if>
66 - GROUP BY DATE_FORMAT(feedback_time, '%Y-%m-%d') 66 + GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('feedback_time')}
67 ORDER BY statDate 67 ORDER BY statDate
68 </select> 68 </select>
69 69
yudao-module-mes/src/main/resources/mapper/qc/pendinginspect/MesQcPendingInspectMapper.xml
@@ -166,7 +166,8 @@ @@ -166,7 +166,8 @@
166 ) t 166 ) t
167 <where> 167 <where>
168 <if test="sourceDocCode != null and sourceDocCode != ''"> 168 <if test="sourceDocCode != null and sourceDocCode != ''">
169 - AND t.sourceDocCode LIKE CONCAT('%', #{sourceDocCode}, '%') 169 + <bind name="sourceDocCodeLike" value="'%' + sourceDocCode + '%'"/>
  170 + AND t.sourceDocCode LIKE #{sourceDocCodeLike}
170 </if> 171 </if>
171 <if test="qcType != null"> 172 <if test="qcType != null">
172 AND t.qcType = #{qcType} 173 AND t.qcType = #{qcType}
yudao-module-pms/src/main/java/cn/iocoder/yudao/module/pms/dal/mysql/pm/workitem/PmsWorkItemActivityMapper.java
@@ -19,11 +19,10 @@ public interface PmsWorkItemActivityMapper extends BaseMapperX&lt;PmsWorkItemActivi @@ -19,11 +19,10 @@ public interface PmsWorkItemActivityMapper extends BaseMapperX&lt;PmsWorkItemActivi
19 } 19 }
20 20
21 default List<PmsWorkItemActivityDO> selectListByWorkItemIds(Collection<Long> workItemIds, int limit) { 21 default List<PmsWorkItemActivityDO> selectListByWorkItemIds(Collection<Long> workItemIds, int limit) {
22 - return selectList(new LambdaQueryWrapperX<PmsWorkItemActivityDO>() 22 + return selectListLimitN(new LambdaQueryWrapperX<PmsWorkItemActivityDO>()
23 .in(PmsWorkItemActivityDO::getWorkItemId, workItemIds) 23 .in(PmsWorkItemActivityDO::getWorkItemId, workItemIds)
24 .orderByDesc(PmsWorkItemActivityDO::getCreateTime) 24 .orderByDesc(PmsWorkItemActivityDO::getCreateTime)
25 - .orderByDesc(PmsWorkItemActivityDO::getId)  
26 - .last("LIMIT " + limit)); 25 + .orderByDesc(PmsWorkItemActivityDO::getId), limit);
27 } 26 }
28 27
29 default void deleteByWorkItemId(Long workItemId) { 28 default void deleteByWorkItemId(Long workItemId) {
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/notify/NotifyMessageMapper.java
@@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.system.dal.mysql.notify; @@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.system.dal.mysql.notify;
3 import cn.iocoder.yudao.framework.common.pojo.PageResult; 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; 4 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
5 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; 5 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
6 -import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;  
7 import cn.iocoder.yudao.module.system.controller.admin.notify.vo.message.NotifyMessageMyPageReqVO; 6 import cn.iocoder.yudao.module.system.controller.admin.notify.vo.message.NotifyMessageMyPageReqVO;
8 import cn.iocoder.yudao.module.system.controller.admin.notify.vo.message.NotifyMessagePageReqVO; 7 import cn.iocoder.yudao.module.system.controller.admin.notify.vo.message.NotifyMessagePageReqVO;
9 import cn.iocoder.yudao.module.system.dal.dataobject.notify.NotifyMessageDO; 8 import cn.iocoder.yudao.module.system.dal.dataobject.notify.NotifyMessageDO;
@@ -53,11 +52,11 @@ public interface NotifyMessageMapper extends BaseMapperX&lt;NotifyMessageDO&gt; { @@ -53,11 +52,11 @@ public interface NotifyMessageMapper extends BaseMapperX&lt;NotifyMessageDO&gt; {
53 } 52 }
54 53
55 default List<NotifyMessageDO> selectUnreadListByUserIdAndUserType(Long userId, Integer userType, Integer size) { 54 default List<NotifyMessageDO> selectUnreadListByUserIdAndUserType(Long userId, Integer userType, Integer size) {
56 - return selectList(new QueryWrapperX<NotifyMessageDO>() // 由于要使用 limitN 语句,所以只能用 QueryWrapperX  
57 - .eq("user_id", userId)  
58 - .eq("user_type", userType)  
59 - .eq("read_status", false)  
60 - .orderByDesc("id").limitN(size)); 55 + return selectListLimitN(new LambdaQueryWrapperX<NotifyMessageDO>()
  56 + .eq(NotifyMessageDO::getUserId, userId)
  57 + .eq(NotifyMessageDO::getUserType, userType)
  58 + .eq(NotifyMessageDO::getReadStatus, false)
  59 + .orderByDesc(NotifyMessageDO::getId), size);
61 } 60 }
62 61
63 default Long selectUnreadCountByUserIdAndUserType(Long userId, Integer userType) { 62 default Long selectUnreadCountByUserIdAndUserType(Long userId, Integer userType) {
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/oauth2/OAuth2AccessTokenMapper.java
@@ -46,7 +46,7 @@ public interface OAuth2AccessTokenMapper extends BaseMapperX&lt;OAuth2AccessTokenDO @@ -46,7 +46,7 @@ public interface OAuth2AccessTokenMapper extends BaseMapperX&lt;OAuth2AccessTokenDO
46 * @param limit 删除条数,防止一次删除太多 46 * @param limit 删除条数,防止一次删除太多
47 * @return 删除条数 47 * @return 删除条数
48 */ 48 */
49 - @Delete("DELETE FROM system_oauth2_access_token WHERE expires_time < #{expiresTime} LIMIT #{limit}") 49 + @Delete("${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@deleteBatchByLessThan('system_oauth2_access_token', 'expires_time', 'expiresTime', 'limit')}")
50 Integer deleteByExpiresTimeLt(@Param("expiresTime") LocalDateTime expiresTime, @Param("limit") Integer limit); 50 Integer deleteByExpiresTimeLt(@Param("expiresTime") LocalDateTime expiresTime, @Param("limit") Integer limit);
51 51
52 } 52 }
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/oauth2/OAuth2RefreshTokenMapper.java
@@ -30,6 +30,6 @@ public interface OAuth2RefreshTokenMapper extends BaseMapperX&lt;OAuth2RefreshToken @@ -30,6 +30,6 @@ public interface OAuth2RefreshTokenMapper extends BaseMapperX&lt;OAuth2RefreshToken
30 * @param limit 删除条数,防止一次删除太多 30 * @param limit 删除条数,防止一次删除太多
31 * @return 删除条数 31 * @return 删除条数
32 */ 32 */
33 - @Delete("DELETE FROM system_oauth2_refresh_token WHERE expires_time < #{expiresTime} LIMIT #{limit}") 33 + @Delete("${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@deleteBatchByLessThan('system_oauth2_refresh_token', 'expires_time', 'expiresTime', 'limit')}")
34 Integer deleteByExpiresTimeLt(@Param("expiresTime") LocalDateTime expiresTime, @Param("limit") Integer limit); 34 Integer deleteByExpiresTimeLt(@Param("expiresTime") LocalDateTime expiresTime, @Param("limit") Integer limit);
35 } 35 }
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/sms/SmsCodeMapper.java
1 package cn.iocoder.yudao.module.system.dal.mysql.sms; 1 package cn.iocoder.yudao.module.system.dal.mysql.sms;
2 2
3 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; 3 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
4 -import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX; 4 +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
5 import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsCodeDO; 5 import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsCodeDO;
6 import org.apache.ibatis.annotations.Mapper; 6 import org.apache.ibatis.annotations.Mapper;
7 7
@@ -17,12 +17,11 @@ public interface SmsCodeMapper extends BaseMapperX&lt;SmsCodeDO&gt; { @@ -17,12 +17,11 @@ public interface SmsCodeMapper extends BaseMapperX&lt;SmsCodeDO&gt; {
17 * @return 手机验证码 17 * @return 手机验证码
18 */ 18 */
19 default SmsCodeDO selectLastByMobile(String mobile, String code, Integer scene) { 19 default SmsCodeDO selectLastByMobile(String mobile, String code, Integer scene) {
20 - return selectOne(new QueryWrapperX<SmsCodeDO>()  
21 - .eq("mobile", mobile)  
22 - .eqIfPresent("scene", scene)  
23 - .eqIfPresent("code", code)  
24 - .orderByDesc("id")  
25 - .limitN(1)); 20 + return selectFirstOne(new LambdaQueryWrapperX<SmsCodeDO>()
  21 + .eq(SmsCodeDO::getMobile, mobile)
  22 + .eq(scene != null, SmsCodeDO::getScene, scene)
  23 + .eq(code != null, SmsCodeDO::getCode, code)
  24 + .orderByDesc(SmsCodeDO::getId));
26 } 25 }
27 26
28 } 27 }
yudao-module-wms/src/main/resources/mapper/home/WmsHomeStatisticsMapper.xml
@@ -54,7 +54,7 @@ @@ -54,7 +54,7 @@
54 <select id="selectDailyOrderTrend" resultType="cn.iocoder.yudao.framework.mybatis.core.util.CaseInsensitiveResultMap"> 54 <select id="selectDailyOrderTrend" resultType="cn.iocoder.yudao.framework.mybatis.core.util.CaseInsensitiveResultMap">
55 SELECT statDate, orderType, SUM(count) AS count 55 SELECT statDate, orderType, SUM(count) AS count
56 FROM ( 56 FROM (
57 - SELECT DATE_FORMAT(order_time, '%Y-%m-%d') AS statDate, 57 + SELECT ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('order_time')} AS statDate,
58 ${@cn.iocoder.yudao.module.wms.enums.order.WmsOrderTypeEnum@RECEIPT.type} AS orderType, 58 ${@cn.iocoder.yudao.module.wms.enums.order.WmsOrderTypeEnum@RECEIPT.type} AS orderType,
59 COUNT(*) AS count 59 COUNT(*) AS count
60 FROM wms_receipt_order 60 FROM wms_receipt_order
@@ -64,9 +64,9 @@ @@ -64,9 +64,9 @@
64 <if test="warehouseId != null"> 64 <if test="warehouseId != null">
65 AND warehouse_id = #{warehouseId} 65 AND warehouse_id = #{warehouseId}
66 </if> 66 </if>
67 - GROUP BY DATE_FORMAT(order_time, '%Y-%m-%d') 67 + GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('order_time')}
68 UNION ALL 68 UNION ALL
69 - SELECT DATE_FORMAT(order_time, '%Y-%m-%d') AS statDate, 69 + SELECT ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('order_time')} AS statDate,
70 ${@cn.iocoder.yudao.module.wms.enums.order.WmsOrderTypeEnum@SHIPMENT.type} AS orderType, 70 ${@cn.iocoder.yudao.module.wms.enums.order.WmsOrderTypeEnum@SHIPMENT.type} AS orderType,
71 COUNT(*) AS count 71 COUNT(*) AS count
72 FROM wms_shipment_order 72 FROM wms_shipment_order
@@ -76,9 +76,9 @@ @@ -76,9 +76,9 @@
76 <if test="warehouseId != null"> 76 <if test="warehouseId != null">
77 AND warehouse_id = #{warehouseId} 77 AND warehouse_id = #{warehouseId}
78 </if> 78 </if>
79 - GROUP BY DATE_FORMAT(order_time, '%Y-%m-%d') 79 + GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('order_time')}
80 UNION ALL 80 UNION ALL
81 - SELECT DATE_FORMAT(order_time, '%Y-%m-%d') AS statDate, 81 + SELECT ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('order_time')} AS statDate,
82 ${@cn.iocoder.yudao.module.wms.enums.order.WmsOrderTypeEnum@MOVEMENT.type} AS orderType, 82 ${@cn.iocoder.yudao.module.wms.enums.order.WmsOrderTypeEnum@MOVEMENT.type} AS orderType,
83 COUNT(*) AS count 83 COUNT(*) AS count
84 FROM wms_movement_order 84 FROM wms_movement_order
@@ -88,9 +88,9 @@ @@ -88,9 +88,9 @@
88 <if test="warehouseId != null"> 88 <if test="warehouseId != null">
89 AND (source_warehouse_id = #{warehouseId} OR target_warehouse_id = #{warehouseId}) 89 AND (source_warehouse_id = #{warehouseId} OR target_warehouse_id = #{warehouseId})
90 </if> 90 </if>
91 - GROUP BY DATE_FORMAT(order_time, '%Y-%m-%d') 91 + GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('order_time')}
92 UNION ALL 92 UNION ALL
93 - SELECT DATE_FORMAT(order_time, '%Y-%m-%d') AS statDate, 93 + SELECT ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('order_time')} AS statDate,
94 ${@cn.iocoder.yudao.module.wms.enums.order.WmsOrderTypeEnum@CHECK.type} AS orderType, 94 ${@cn.iocoder.yudao.module.wms.enums.order.WmsOrderTypeEnum@CHECK.type} AS orderType,
95 COUNT(*) AS count 95 COUNT(*) AS count
96 FROM wms_check_order 96 FROM wms_check_order
@@ -100,7 +100,7 @@ @@ -100,7 +100,7 @@
100 <if test="warehouseId != null"> 100 <if test="warehouseId != null">
101 AND warehouse_id = #{warehouseId} 101 AND warehouse_id = #{warehouseId}
102 </if> 102 </if>
103 - GROUP BY DATE_FORMAT(order_time, '%Y-%m-%d') 103 + GROUP BY ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@formatDate('order_time')}
104 ) t 104 ) t
105 GROUP BY statDate, orderType 105 GROUP BY statDate, orderType
106 ORDER BY statDate, orderType 106 ORDER BY statDate, orderType
@@ -131,7 +131,7 @@ @@ -131,7 +131,7 @@
131 </if> 131 </if>
132 GROUP BY item.id, item.name 132 GROUP BY item.id, item.name
133 ORDER BY quantity DESC, item.id ASC 133 ORDER BY quantity DESC, item.id ASC
134 - LIMIT #{limit} 134 + ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@limitParam('limit', true)}
135 </select> 135 </select>
136 136
137 <!-- 按仓库统计库存数量排行 --> 137 <!-- 按仓库统计库存数量排行 -->
@@ -148,7 +148,7 @@ @@ -148,7 +148,7 @@
148 </if> 148 </if>
149 GROUP BY warehouse.id, warehouse.name 149 GROUP BY warehouse.id, warehouse.name
150 ORDER BY quantity DESC, warehouse.id ASC 150 ORDER BY quantity DESC, warehouse.id ASC
151 - LIMIT #{limit} 151 + ${@cn.iocoder.yudao.framework.mybatis.core.util.SqlDialectUtils@limitParam('limit', true)}
152 </select> 152 </select>
153 153
154 </mapper> 154 </mapper>