diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/jdbc/JdbcExceptionUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/jdbc/JdbcExceptionUtils.java new file mode 100644 index 0000000..b88adff --- /dev/null +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/jdbc/JdbcExceptionUtils.java @@ -0,0 +1,88 @@ +package cn.iocoder.yudao.framework.common.util.jdbc; + +import cn.hutool.core.util.StrUtil; + +import java.sql.SQLException; +import java.util.*; + +/** + * JDBC 异常工具类 + * + * @author 芋道源码 + */ +public class JdbcExceptionUtils { + + /** + * 表示“表不存在”的 SQLState + * + * 1. 42S02:ANSI / ODBC 标准,MySQL(1146)、MariaDB、H2、SQL Server(jTDS)等使用 + * 2. S0002:X/Open 旧标准,SQL Server(208,mssql-jdbc)使用 + * 3. 42P01:PostgreSQL 的 undefined_table,openGauss、KingbaseES 等 PostgreSQL 兼容数据库同样使用 + */ + private static final Set TABLE_NOT_EXISTS_SQL_STATES = new HashSet<>(Arrays.asList("42S02", "S0002", "42P01")); + + /** + * Oracle 的“表或视图不存在”错误码:ORA-00942,对应 SQLState 为通用的 42000,需要同时匹配错误码 + */ + private static final int ORACLE_TABLE_NOT_EXISTS_ERROR_CODE = 942; + private static final String ORACLE_TABLE_NOT_EXISTS_SQL_STATE = "42000"; + + /** + * 兜底的“表不存在”异常文案,兼容未携带 SQLState 的场景(MySQL 的 Table 'xxx' doesn't exist) + */ + private static final String TABLE_NOT_EXISTS_MESSAGE = "doesn't exist"; + + /** + * 判断异常是否为“表不存在” + * + * 沿 cause 链、{@link SQLException#getNextException()} 链查找,优先识别 SQLState、错误码。 + * 仅在整条链都没有有效 SQLState 时兜底匹配异常文案,避免外层包装信息覆盖明确的数据库错误。 + * 列不存在、权限不足、语法错误等其它异常,都返回 false,由调用方原样抛出。 + * + * @param throwable 异常 + * @return 是否表不存在 + */ + @SuppressWarnings("PatternVariableCanBeUsed") // 保持 JDK8 兼容性 + public static boolean isTableNotExists(Throwable throwable) { + Set visited = Collections.newSetFromMap(new IdentityHashMap<>()); + Deque queue = new ArrayDeque<>(); + if (throwable != null) { + queue.add(throwable); + } + boolean hasSqlState = false; + boolean hasLegacyMessage = false; + while (!queue.isEmpty()) { + Throwable current = queue.poll(); + if (!visited.add(current)) { // 避免循环引用 + continue; + } + if (current instanceof SQLException) { + SQLException sqlException = (SQLException) current; + if (isTableNotExists(sqlException)) { + return true; + } + hasSqlState |= StrUtil.isNotBlank(sqlException.getSQLState()); + addIfNotNull(queue, sqlException.getNextException()); + } + hasLegacyMessage |= StrUtil.contains(current.getMessage(), TABLE_NOT_EXISTS_MESSAGE); + addIfNotNull(queue, current.getCause()); + } + return !hasSqlState && hasLegacyMessage; + } + + private static boolean isTableNotExists(SQLException sqlException) { + String sqlState = sqlException.getSQLState(); + if (TABLE_NOT_EXISTS_SQL_STATES.contains(sqlState)) { + return true; + } + return sqlException.getErrorCode() == ORACLE_TABLE_NOT_EXISTS_ERROR_CODE + && ORACLE_TABLE_NOT_EXISTS_SQL_STATE.equals(sqlState); + } + + private static void addIfNotNull(Deque queue, Throwable throwable) { + if (throwable != null) { + queue.add(throwable); + } + } + +} diff --git a/yudao-framework/yudao-common/src/test/java/cn/iocoder/yudao/framework/common/util/jdbc/JdbcExceptionUtilsTest.java b/yudao-framework/yudao-common/src/test/java/cn/iocoder/yudao/framework/common/util/jdbc/JdbcExceptionUtilsTest.java new file mode 100644 index 0000000..9a06d7a --- /dev/null +++ b/yudao-framework/yudao-common/src/test/java/cn/iocoder/yudao/framework/common/util/jdbc/JdbcExceptionUtilsTest.java @@ -0,0 +1,170 @@ +package cn.iocoder.yudao.framework.common.util.jdbc; + +import org.junit.jupiter.api.Test; + +import java.sql.SQLException; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * {@link JdbcExceptionUtils} 的单元测试 + * + * @author 芋道源码 + */ +public class JdbcExceptionUtilsTest { + + // ========== 表不存在:SQLState、错误码 ========== + + @Test + public void testIsTableNotExists_mysql() { + assertTrue(JdbcExceptionUtils.isTableNotExists( + new SQLException("Table 'ruoyi-vue-pro.bpm_category' doesn't exist", "42S02", 1146))); + // SQLState 识别,不依赖文案 + assertTrue(JdbcExceptionUtils.isTableNotExists(new SQLException("unknown", "42S02", 1146))); + } + + @Test + public void testIsTableNotExists_postgresql() { + assertTrue(JdbcExceptionUtils.isTableNotExists( + new SQLException("ERROR: relation \"bpm_category\" does not exist", "42P01"))); + } + + @Test + public void testIsTableNotExists_oracle() { + assertTrue(JdbcExceptionUtils.isTableNotExists( + new SQLException("ORA-00942: table or view does not exist\n", "42000", 942))); + } + + @Test + public void testIsTableNotExists_sqlServer() { + // mssql-jdbc + assertTrue(JdbcExceptionUtils.isTableNotExists( + new SQLException("Invalid object name 'bpm_category'.", "S0002", 208))); + // jTDS + assertTrue(JdbcExceptionUtils.isTableNotExists( + new SQLException("Invalid object name 'bpm_category'.", "42S02", 208))); + } + + @Test + public void testIsTableNotExists_messageFallback() { + // 未携带 SQLState 时,兜底匹配 MySQL 文案 + assertTrue(JdbcExceptionUtils.isTableNotExists( + new SQLException("Table 'ruoyi-vue-pro.mp_account' doesn't exist"))); + assertTrue(JdbcExceptionUtils.isTableNotExists( + new RuntimeException("### Cause: Table 'ruoyi-vue-pro.mp_account' doesn't exist"))); + } + + // ========== 表不存在:异常链 ========== + + @Test + public void testIsTableNotExists_causeChain() { + SQLException sqlException = new SQLException("ERROR: relation \"mp_account\" does not exist", "42P01"); + // 外层包装的文案不包含“表不存在”,需要沿 cause 链查找 + assertTrue(JdbcExceptionUtils.isTableNotExists( + new RuntimeException("nested exception", new IllegalStateException(sqlException)))); + } + + @Test + public void testIsTableNotExists_nextExceptionChain() { + SQLException batchException = new SQLException("Batch entry 0 was aborted", "25P02"); + batchException.setNextException(new SQLException("ERROR: relation \"mp_account\" does not exist", "42P01")); + assertTrue(JdbcExceptionUtils.isTableNotExists(new RuntimeException(batchException))); + } + + // ========== 非表不存在:原样返回 false ========== + + @Test + public void testIsTableNotExists_columnNotExists() { + // MySQL + assertFalse(JdbcExceptionUtils.isTableNotExists( + new SQLException("Unknown column 'xxx' in 'field list'", "42S22", 1054))); + // PostgreSQL + assertFalse(JdbcExceptionUtils.isTableNotExists( + new SQLException("ERROR: column \"xxx\" does not exist", "42703"))); + // Oracle + assertFalse(JdbcExceptionUtils.isTableNotExists( + new SQLException("ORA-00904: \"XXX\": invalid identifier\n", "42000", 904))); + // SQL Server + assertFalse(JdbcExceptionUtils.isTableNotExists( + new SQLException("Invalid column name 'xxx'.", "S0001", 207))); + } + + @Test + public void testIsTableNotExists_permissionDenied() { + // MySQL + assertFalse(JdbcExceptionUtils.isTableNotExists( + new SQLException("SELECT command denied to user 'u'@'localhost' for table 'bpm_category'", "42000", 1142))); + // PostgreSQL + assertFalse(JdbcExceptionUtils.isTableNotExists( + new SQLException("ERROR: permission denied for table bpm_category", "42501"))); + // Oracle + assertFalse(JdbcExceptionUtils.isTableNotExists( + new SQLException("ORA-01031: insufficient privileges\n", "42000", 1031))); + } + + @Test + public void testIsTableNotExists_sqlStateTakesPrecedenceOverLegacyMessage() { + // 用户名或外层包装信息可能包含旧文案,明确的权限/列错误仍应保留。 + assertFalse(JdbcExceptionUtils.isTableNotExists(new SQLException( + "SELECT command denied to user 'doesn't exist'@'localhost' for table 'erp_product'", + "42000", 1142))); + assertFalse(JdbcExceptionUtils.isTableNotExists(new RuntimeException( + "Previous lookup: Table 'erp_product' doesn't exist", + new SQLException("column missing", "42703")))); + } + + @Test + public void testIsTableNotExists_otherErrors() { + // 语法错误:与 Oracle ORA-00942 同为 42000,但错误码不同 + assertFalse(JdbcExceptionUtils.isTableNotExists( + new SQLException("You have an error in your SQL syntax", "42000", 1064))); + assertFalse(JdbcExceptionUtils.isTableNotExists( + new SQLException("ERROR: syntax error at or near \"FROM\"", "42601"))); + // 错误码相同,但 SQLState 不是“表不存在”:Oracle ORA-01146、SQL Server 942 + assertFalse(JdbcExceptionUtils.isTableNotExists( + new SQLException("ORA-01146: cannot start online backup", "72000", 1146))); + assertFalse(JdbcExceptionUtils.isTableNotExists( + new SQLException("Database 'xxx' cannot be opened because it is offline.", "S0001", 942))); + // 唯一键冲突、连接异常 + assertFalse(JdbcExceptionUtils.isTableNotExists( + new SQLException("Duplicate entry '1' for key 'PRIMARY'", "23000", 1062))); + assertFalse(JdbcExceptionUtils.isTableNotExists(new SQLException("Connection refused", "08001"))); + // 非 MySQL 文案的“does not exist”,且无 SQLState + assertFalse(JdbcExceptionUtils.isTableNotExists(new RuntimeException("Table does not exist"))); + } + + @Test + public void testIsTableNotExists_nullAndEmpty() { + assertFalse(JdbcExceptionUtils.isTableNotExists(null)); + assertFalse(JdbcExceptionUtils.isTableNotExists(new RuntimeException((String) null))); + assertFalse(JdbcExceptionUtils.isTableNotExists(new SQLException((String) null))); + assertFalse(JdbcExceptionUtils.isTableNotExists( + new RuntimeException(null, new SQLException(null, null, 0)))); + } + + @Test + public void testIsTableNotExists_cycle() { + // cause 链循环 + RuntimeException a = new RuntimeException("a"); + RuntimeException b = new RuntimeException("b", a); + a.initCause(b); + assertFalse(JdbcExceptionUtils.isTableNotExists(a)); + + // nextException 链循环 + SQLException s1 = new SQLException("s1", "HY000", 1); + SQLException s2 = new SQLException("s2", "HY000", 2); + s1.setNextException(s2); + s2.setNextException(s1); + assertFalse(JdbcExceptionUtils.isTableNotExists(s1)); + + // 循环链中存在“表不存在”时,仍可识别 + SQLException s3 = new SQLException("s3", "HY000", 3); + SQLException s4 = new SQLException("ERROR: relation \"mp_account\" does not exist", "42P01"); + s3.setNextException(s4); + s4.setNextException(s3); + assertTrue(JdbcExceptionUtils.isTableNotExists(s4)); + assertTrue(JdbcExceptionUtils.isTableNotExists(s3)); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java index 8edf7c0..8757105 100644 --- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java @@ -11,6 +11,7 @@ import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.util.collection.SetUtils; +import cn.iocoder.yudao.framework.common.util.jdbc.JdbcExceptionUtils; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils; import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; @@ -396,10 +397,10 @@ public class GlobalExceptionHandler { * @return 如果是 Table 不存在的异常,则返回对应的 CommonResult */ private CommonResult handleTableNotExists(Throwable ex) { - String message = ExceptionUtil.getRootCauseMessage(ex); - if (!message.contains("doesn't exist")) { + if (!JdbcExceptionUtils.isTableNotExists(ex)) { return null; } + String message = StrUtil.nullToEmpty(ExceptionUtil.getRootCauseMessage(ex)); // 1. 工作流 if (message.contains("bpm_")) { log.error("[工作流模块 yudao-module-bpm - 表结构未导入][参考 https://cloud.iocoder.cn/bpm/ 开启]"); diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/test/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandlerTest.java b/yudao-framework/yudao-spring-boot-starter-web/src/test/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandlerTest.java new file mode 100644 index 0000000..ee391a6 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-web/src/test/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandlerTest.java @@ -0,0 +1,118 @@ +package cn.iocoder.yudao.framework.web.core.handler; + +import cn.iocoder.yudao.framework.common.biz.infra.logger.ApiErrorLogCommonApi; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import jakarta.servlet.http.HttpServletRequest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.sql.SQLException; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.NOT_IMPLEMENTED; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; + +/** + * {@link GlobalExceptionHandler} 的单元测试 + * + * 重点验证“表不存在”的识别:按 SQLState / 错误码识别多种数据库,其它 SQL 异常不会被当作表不存在 + * + * @author 芋道源码 + */ +public class GlobalExceptionHandlerTest { + + private GlobalExceptionHandler globalExceptionHandler; + + private HttpServletRequest request; + + @BeforeEach + public void setUp() { + globalExceptionHandler = new GlobalExceptionHandler("test", mock(ApiErrorLogCommonApi.class)); + request = mock(HttpServletRequest.class); + } + + @Test + public void testDefaultExceptionHandler_tableNotExists_mysql() { + // 准备参数 + Exception ex = new RuntimeException("nested exception", + new SQLException("Table 'ruoyi-vue-pro.erp_product' doesn't exist", "42S02", 1146)); + + // 调用 + CommonResult result = globalExceptionHandler.defaultExceptionHandler(request, ex); + // 断言 + assertEquals(NOT_IMPLEMENTED.getCode(), result.getCode()); + assertTrue(result.getMsg().contains("yudao-module-erp")); + } + + @Test + public void testDefaultExceptionHandler_tableNotExists_postgresql() { + // 准备参数:PostgreSQL 的文案不包含 doesn't exist,通过 SQLState 42P01 识别 + Exception ex = new RuntimeException("nested exception", + new SQLException("ERROR: relation \"bpm_category\" does not exist", "42P01")); + + // 调用 + CommonResult result = globalExceptionHandler.defaultExceptionHandler(request, ex); + // 断言 + assertEquals(NOT_IMPLEMENTED.getCode(), result.getCode()); + assertTrue(result.getMsg().contains("yudao-module-bpm")); + } + + @Test + public void testDefaultExceptionHandler_tableNotExists_noPrefixMatched() { + // 准备参数:Oracle 的文案不包含表名,无法匹配模块前缀,按系统异常处理 + Exception ex = new RuntimeException("nested exception", + new SQLException("ORA-00942: table or view does not exist\n", "42000", 942)); + + // 调用 + CommonResult result = globalExceptionHandler.defaultExceptionHandler(request, ex); + // 断言 + assertEquals(INTERNAL_SERVER_ERROR.getCode(), result.getCode()); + } + + @Test + public void testDefaultExceptionHandler_columnNotExists() { + // 准备参数:列不存在,即使文案包含模块前缀,也不是表不存在 + Exception ex = new RuntimeException("nested exception", + new SQLException("ERROR: column \"xxx\" of relation \"bpm_category\" does not exist", "42703")); + + // 调用 + CommonResult result = globalExceptionHandler.defaultExceptionHandler(request, ex); + // 断言 + assertEquals(INTERNAL_SERVER_ERROR.getCode(), result.getCode()); + } + + @Test + public void testDefaultExceptionHandler_permissionDenied() { + // 准备参数:权限不足,不是表不存在 + Exception ex = new RuntimeException("nested exception", + new SQLException("SELECT command denied to user 'u'@'localhost' for table 'erp_product'", "42000", 1142)); + + // 调用 + CommonResult result = globalExceptionHandler.defaultExceptionHandler(request, ex); + // 断言 + assertEquals(INTERNAL_SERVER_ERROR.getCode(), result.getCode()); + } + + @Test + public void testDefaultExceptionHandler_permissionDeniedWithLegacyPhrase() { + Exception ex = new RuntimeException("nested exception", new SQLException( + "SELECT command denied to user 'doesn't exist'@'localhost' for table 'erp_product'", + "42000", 1142)); + + CommonResult result = globalExceptionHandler.defaultExceptionHandler(request, ex); + + assertEquals(INTERNAL_SERVER_ERROR.getCode(), result.getCode()); + } + + @Test + public void testDefaultExceptionHandler_nullMessage() { + // 调用 + CommonResult result = globalExceptionHandler.defaultExceptionHandler(request, + new RuntimeException((String) null)); + // 断言 + assertEquals(INTERNAL_SERVER_ERROR.getCode(), result.getCode()); + } + +} diff --git a/yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/dal/mysql/home/Ai1HomeMapper.java b/yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/dal/mysql/home/Ai1HomeMapper.java index e4a7d16..8f7a796 100644 --- a/yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/dal/mysql/home/Ai1HomeMapper.java +++ b/yudao-module-ai1-springai/src/main/java/cn/iocoder/yudao/module/ai1/dal/mysql/home/Ai1HomeMapper.java @@ -5,6 +5,8 @@ import cn.iocoder.yudao.module.ai1.controller.admin.home.vo.Ai1HomeMessageSummar import cn.iocoder.yudao.module.ai1.controller.admin.home.vo.Ai1HomeSummaryRespVO; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Result; +import org.apache.ibatis.annotations.Results; import org.apache.ibatis.annotations.Select; import java.time.LocalDateTime; @@ -18,32 +20,36 @@ import java.util.List; @Mapper public interface Ai1HomeMapper { - @Select("SELECT (SELECT COUNT(1) FROM ai1_agent WHERE deleted = FALSE) AS agentCount, " + - "(SELECT COUNT(1) FROM ai1_skill WHERE deleted = FALSE) AS skillCount, " + - "(SELECT COUNT(1) FROM ai1_mcp WHERE deleted = FALSE) AS mcpCount, " + - "(SELECT COUNT(1) FROM ai1_model WHERE deleted = FALSE) AS modelCount") + @Select("SELECT (SELECT COUNT(1) FROM ai1_agent WHERE deleted = 0) AS agentCount, " + + "(SELECT COUNT(1) FROM ai1_skill WHERE deleted = 0) AS skillCount, " + + "(SELECT COUNT(1) FROM ai1_mcp WHERE deleted = 0) AS mcpCount, " + + "(SELECT COUNT(1) FROM ai1_model WHERE deleted = 0) AS modelCount") Ai1HomeSummaryRespVO selectSummary(); /** * 按天统计消息数量,仅返回有消息的日期;使用 CAST AS DATE 兼容 MySQL 与 H2 */ - @Select("SELECT CAST(create_time AS DATE) AS `date`, COUNT(1) AS `count` " + + @Select("SELECT CAST(create_time AS DATE) AS statDate, COUNT(1) AS count " + "FROM ai1_session_message " + - "WHERE create_time BETWEEN #{beginTime} AND #{endTime} AND deleted = FALSE " + + "WHERE create_time BETWEEN #{beginTime} AND #{endTime} AND deleted = 0 " + "GROUP BY CAST(create_time AS DATE) " + - "ORDER BY `date`") + "ORDER BY statDate") + @Results({ + @Result(column = "statDate", property = "date"), + @Result(column = "count", property = "count") + }) List selectMessageSummaryListByCreateTimeBetweenGroupByDate( @Param("beginTime") LocalDateTime beginTime, @Param("endTime") LocalDateTime endTime); /** * 按 Agent 统计消息数量,按数量倒序,数量相同按 Agent 编号升序;已删除会话下的消息不计入 */ - @Select("SELECT c.agent_id AS agentId, COUNT(1) AS `count` " + + @Select("SELECT c.agent_id AS agentId, COUNT(1) AS count " + "FROM ai1_session_message m " + - "INNER JOIN ai1_session c ON m.session_id = c.id AND c.deleted = FALSE " + - "WHERE m.create_time BETWEEN #{beginTime} AND #{endTime} AND m.deleted = FALSE " + + "INNER JOIN ai1_session c ON m.session_id = c.id AND c.deleted = 0 " + + "WHERE m.create_time BETWEEN #{beginTime} AND #{endTime} AND m.deleted = 0 " + "GROUP BY c.agent_id " + - "ORDER BY `count` DESC, c.agent_id") + "ORDER BY count DESC, c.agent_id") List selectMessageSummaryListByCreateTimeBetweenGroupByAgentId( @Param("beginTime") LocalDateTime beginTime, @Param("endTime") LocalDateTime endTime); diff --git a/yudao-module-crm/src/main/resources/mapper/permission/CrmOwnerRecordMapper.xml b/yudao-module-crm/src/main/resources/mapper/permission/CrmOwnerRecordMapper.xml index efafb61..42a1e0f 100644 --- a/yudao-module-crm/src/main/resources/mapper/permission/CrmOwnerRecordMapper.xml +++ b/yudao-module-crm/src/main/resources/mapper/permission/CrmOwnerRecordMapper.xml @@ -16,7 +16,7 @@ AND post_owner_user_id IS NULL AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime} - GROUP BY time + GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d') SELECT owner_user_id, - IFNULL(SUM(total_price), 0) AS contract_price + COALESCE(SUM(total_price), 0) AS contract_price FROM crm_contract WHERE deleted = 0 AND audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status} @@ -82,7 +82,7 @@ SELECT customer.area_id AS area_id, - IFNULL(TRUNCATE(AVG(TIMESTAMPDIFF(DAY, customer.create_time, contract.order_date)), 1), 0) AS customer_deal_cycle, + COALESCE(TRUNCATE(AVG(TIMESTAMPDIFF(DAY, customer.create_time, contract.order_date)), 1), 0) AS customer_deal_cycle, COUNT(DISTINCT customer.id) AS customer_deal_count - FROM crm_customer AS customer - LEFT JOIN crm_contract AS contract ON customer.id = contract.customer_id + FROM crm_customer customer + LEFT JOIN crm_contract contract ON customer.id = contract.customer_id WHERE customer.deleted = 0 AND contract.deleted = 0 AND contract.audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status} @@ -244,12 +244,12 @@ diff --git a/yudao-module-crm/src/main/resources/mapper/statistics/CrmStatisticsFunnelMapper.xml b/yudao-module-crm/src/main/resources/mapper/statistics/CrmStatisticsFunnelMapper.xml index e3ef36d..3e2a7fd 100644 --- a/yudao-module-crm/src/main/resources/mapper/statistics/CrmStatisticsFunnelMapper.xml +++ b/yudao-module-crm/src/main/resources/mapper/statistics/CrmStatisticsFunnelMapper.xml @@ -41,7 +41,7 @@ SELECT end_status AS endStatus, COUNT(*) AS businessCount, - IFNULL(SUM(total_price), 0) AS totalPrice + COALESCE(SUM(total_price), 0) AS totalPrice FROM crm_business WHERE deleted = 0 AND end_status IS NOT NULL @@ -64,7 +64,7 @@ status.percent AS statusPercent, status.sort AS sort, COUNT(business.id) AS businessCount, - IFNULL(SUM(business.total_price), 0) AS totalPrice + COALESCE(SUM(business.total_price), 0) AS totalPrice FROM crm_business_status status LEFT JOIN crm_business business ON business.status_id = status.id AND business.deleted = 0 @@ -88,7 +88,7 @@ SELECT DATE_FORMAT(create_time, '%Y-%m-%d') AS time, COUNT(*) AS businessCreateCount, - IFNULL(SUM(total_price), 0) AS totalPrice + COALESCE(SUM(total_price), 0) AS totalPrice FROM crm_business WHERE deleted = 0 @@ -100,7 +100,7 @@ AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime} - GROUP BY time + GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d') diff --git a/yudao-module-crm/src/main/resources/mapper/statistics/CrmStatisticsPerformanceMapper.xml b/yudao-module-crm/src/main/resources/mapper/statistics/CrmStatisticsPerformanceMapper.xml index 2f5c855..23509d5 100644 --- a/yudao-module-crm/src/main/resources/mapper/statistics/CrmStatisticsPerformanceMapper.xml +++ b/yudao-module-crm/src/main/resources/mapper/statistics/CrmStatisticsPerformanceMapper.xml @@ -16,14 +16,14 @@ AND order_date between #{times[0],javaType=java.time.LocalDateTime} and #{times[1],javaType=java.time.LocalDateTime} - GROUP BY time + GROUP BY DATE_FORMAT(order_date, '%Y%m') diff --git a/yudao-module-crm/src/main/resources/mapper/statistics/CrmStatisticsPerformanceTargetMapper.xml b/yudao-module-crm/src/main/resources/mapper/statistics/CrmStatisticsPerformanceTargetMapper.xml index 36b2552..cd33c4b 100644 --- a/yudao-module-crm/src/main/resources/mapper/statistics/CrmStatisticsPerformanceTargetMapper.xml +++ b/yudao-module-crm/src/main/resources/mapper/statistics/CrmStatisticsPerformanceTargetMapper.xml @@ -11,7 +11,7 @@ SELECT MONTH(return_time) AS month, - IFNULL(SUM(price), 0) AS current_price + COALESCE(SUM(price), 0) AS current_price FROM crm_receivable WHERE deleted = 0 AND audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status} diff --git a/yudao-module-crm/src/main/resources/mapper/statistics/CrmStatisticsProductMapper.xml b/yudao-module-crm/src/main/resources/mapper/statistics/CrmStatisticsProductMapper.xml index b69e0c1..d5b829d 100644 --- a/yudao-module-crm/src/main/resources/mapper/statistics/CrmStatisticsProductMapper.xml +++ b/yudao-module-crm/src/main/resources/mapper/statistics/CrmStatisticsProductMapper.xml @@ -26,7 +26,7 @@ @@ -52,14 +52,14 @@ - SELECT IFNULL(SUM(total_price), 0) AS count, owner_user_id + SELECT COALESCE(SUM(total_price), 0) AS count, owner_user_id FROM crm_contract WHERE deleted = 0 AND audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status} @@ -18,7 +18,7 @@ SELECT COUNT(1) AS count, cc.owner_user_id - FROM crm_follow_up_record AS cfur - LEFT JOIN crm_contact AS cc ON FIND_IN_SET(cc.id, cfur.contact_ids) + FROM crm_follow_up_record cfur + LEFT JOIN crm_contact cc ON FIND_IN_SET(cc.id, cfur.contact_ids) WHERE cfur.deleted = 0 AND cc.deleted = 0 AND cc.owner_user_id in @@ -103,8 +103,8 @@ SELECT - (SELECT IFNULL(SUM(total_price), 0) + (SELECT COALESCE(SUM(total_price), 0) FROM erp_purchase_in WHERE in_time >= #{beginTime} @@ -15,7 +15,7 @@ AND deleted = 0 AND status = 20) - - (SELECT IFNULL(SUM(total_price), 0) + (SELECT COALESCE(SUM(total_price), 0) FROM erp_purchase_return WHERE return_time >= #{beginTime} diff --git a/yudao-module-erp/src/main/resources/mapper/statistics/ErpSaleStatisticsMapper.xml b/yudao-module-erp/src/main/resources/mapper/statistics/ErpSaleStatisticsMapper.xml index db89efa..0aad038 100644 --- a/yudao-module-erp/src/main/resources/mapper/statistics/ErpSaleStatisticsMapper.xml +++ b/yudao-module-erp/src/main/resources/mapper/statistics/ErpSaleStatisticsMapper.xml @@ -4,7 +4,7 @@ - + SELECT CAST(create_time AS DATE) AS statDate, count(1) AS count FROM member_user WHERE create_time BETWEEN #{beginTime} AND #{endTime} @@ -60,8 +65,8 @@ AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId()} - GROUP BY date - ORDER BY date + GROUP BY CAST(create_time AS DATE) + ORDER BY statDate diff --git a/yudao-module-mall/yudao-module-statistics/src/main/resources/mapper/trade/TradeOrderStatisticsMapper.xml b/yudao-module-mall/yudao-module-statistics/src/main/resources/mapper/trade/TradeOrderStatisticsMapper.xml index 434b04b..5c47b14 100644 --- a/yudao-module-mall/yudao-module-statistics/src/main/resources/mapper/trade/TradeOrderStatisticsMapper.xml +++ b/yudao-module-mall/yudao-module-statistics/src/main/resources/mapper/trade/TradeOrderStatisticsMapper.xml @@ -6,14 +6,14 @@ resultType="cn.iocoder.yudao.module.statistics.service.member.bo.MemberAreaStatisticsRespBO"> SELECT receiver_area_id AS areaId, (SELECT COUNT(DISTINCT s.user_id) - FROM trade_order AS s + FROM trade_order s WHERE s.receiver_area_id = m.receiver_area_id AND s.tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId()} ) AS orderCreateUserCount, (SELECT COUNT(DISTINCT s.user_id) - FROM trade_order AS s + FROM trade_order s WHERE s.receiver_area_id = m.receiver_area_id AND s.pay_status = TRUE AND s.deleted = 0 @@ -22,7 +22,7 @@ ) AS orderPayUserCount, (SELECT SUM(s.pay_price) - FROM trade_order AS s + FROM trade_order s WHERE s.receiver_area_id = m.receiver_area_id AND s.pay_status = TRUE AND s.deleted = 0 @@ -82,7 +82,7 @@ - + SELECT CAST(pay_time AS DATE) AS statDate, + COUNT(1) AS order_pay_count, + SUM(pay_price) AS order_pay_price FROM trade_order WHERE pay_status = TRUE AND create_time BETWEEN #{beginTime} AND #{endTime} @@ -103,14 +109,13 @@ AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId()} - GROUP BY date - ORDER BY date + GROUP BY CAST(pay_time AS DATE) + ORDER BY statDate - SELECT CONCAT(EXTRACT(YEAR FROM pay_time), '-', - LPAD(CONCAT(EXTRACT(MONTH FROM pay_time)), 2, '0')) AS date, + LPAD(CONCAT(EXTRACT(MONTH FROM pay_time)), 2, '0')) AS statDate, COUNT(1) AS order_pay_count, SUM(pay_price) AS order_pay_price FROM trade_order @@ -120,8 +125,9 @@ AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId()} - GROUP BY date - ORDER BY date + GROUP BY CONCAT(EXTRACT(YEAR FROM pay_time), '-', + LPAD(CONCAT(EXTRACT(MONTH FROM pay_time)), 2, '0')) + ORDER BY statDate - SELECT IFNULL(SUM(feedback_quantity), 0) AS feedbackQuantity, - IFNULL(SUM(qualified_quantity), 0) AS qualifiedQuantity, - IFNULL(SUM(unqualified_quantity), 0) AS unqualifiedQuantity + SELECT COALESCE(SUM(feedback_quantity), 0) AS feedbackQuantity, + COALESCE(SUM(qualified_quantity), 0) AS qualifiedQuantity, + COALESCE(SUM(unqualified_quantity), 0) AS unqualifiedQuantity FROM mes_pro_feedback WHERE deleted = 0 AND status = ${@cn.iocoder.yudao.module.mes.enums.pro.MesProFeedbackStatusEnum@FINISHED.status} @@ -52,10 +52,10 @@ diff --git a/yudao-module-mes/src/main/resources/mapper/qc/pendinginspect/MesQcPendingInspectMapper.xml b/yudao-module-mes/src/main/resources/mapper/qc/pendinginspect/MesQcPendingInspectMapper.xml index ce2de7c..f6ba094 100644 --- a/yudao-module-mes/src/main/resources/mapper/qc/pendinginspect/MesQcPendingInspectMapper.xml +++ b/yudao-module-mes/src/main/resources/mapper/qc/pendinginspect/MesQcPendingInspectMapper.xml @@ -10,6 +10,7 @@ 4. 销售出库单(OQC):单据状态=CONFIRMED(1),行的 oqc_check_flag=1 且 oqc_id 为空 5. 生产退料单(RQC):单据状态=CONFIRMED(1),行的 rqc_check_flag=1 且 rqc_id 为空 且 quality_status=0(待检) 6. 销售退货单(RQC):单据状态=CONFIRMED(1),行的 rqc_check_flag=1 且 rqc_id 为空 且 quality_status=0(待检) + 注意:各分支无值的 ID 列使用 CASE WHEN 1 = 0 THEN 本分支 bigint 列 END 产生“带类型的 NULL”,避免 UNION 中裸 NULL 的类型推断差异 --> - SELECT date, orderType, SUM(count) AS count + SELECT statDate, orderType, SUM(count) AS count FROM ( - SELECT DATE_FORMAT(order_time, '%Y-%m-%d') AS date, + SELECT DATE_FORMAT(order_time, '%Y-%m-%d') AS statDate, ${@cn.iocoder.yudao.module.wms.enums.order.WmsOrderTypeEnum@RECEIPT.type} AS orderType, COUNT(*) AS count FROM wms_receipt_order @@ -66,7 +66,7 @@ GROUP BY DATE_FORMAT(order_time, '%Y-%m-%d') UNION ALL - SELECT DATE_FORMAT(order_time, '%Y-%m-%d') AS date, + SELECT DATE_FORMAT(order_time, '%Y-%m-%d') AS statDate, ${@cn.iocoder.yudao.module.wms.enums.order.WmsOrderTypeEnum@SHIPMENT.type} AS orderType, COUNT(*) AS count FROM wms_shipment_order @@ -78,7 +78,7 @@ GROUP BY DATE_FORMAT(order_time, '%Y-%m-%d') UNION ALL - SELECT DATE_FORMAT(order_time, '%Y-%m-%d') AS date, + SELECT DATE_FORMAT(order_time, '%Y-%m-%d') AS statDate, ${@cn.iocoder.yudao.module.wms.enums.order.WmsOrderTypeEnum@MOVEMENT.type} AS orderType, COUNT(*) AS count FROM wms_movement_order @@ -90,7 +90,7 @@ GROUP BY DATE_FORMAT(order_time, '%Y-%m-%d') UNION ALL - SELECT DATE_FORMAT(order_time, '%Y-%m-%d') AS date, + SELECT DATE_FORMAT(order_time, '%Y-%m-%d') AS statDate, ${@cn.iocoder.yudao.module.wms.enums.order.WmsOrderTypeEnum@CHECK.type} AS orderType, COUNT(*) AS count FROM wms_check_order @@ -102,13 +102,13 @@ GROUP BY DATE_FORMAT(order_time, '%Y-%m-%d') ) t - GROUP BY date, orderType - ORDER BY date, orderType + GROUP BY statDate, orderType + ORDER BY statDate, orderType SELECT item.id AS id, item.name AS name, - IFNULL(SUM(inventory.quantity), 0) AS quantity + COALESCE(SUM(inventory.quantity), 0) AS quantity FROM wms_inventory inventory INNER JOIN wms_item_sku sku ON sku.id = inventory.sku_id AND sku.deleted = 0 INNER JOIN wms_item item ON item.id = sku.item_id AND item.deleted = 0 @@ -138,7 +138,7 @@