Commit 7f53d60ed30251cd44cff383529c13858d374a7f
1 parent
971c5a65
fix: 修复首批关系型数据库通用 SQL 兼容问题
Showing
38 changed files
with
657 additions
and
203 deletions
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/jdbc/JdbcExceptionUtils.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.common.util.jdbc; | ||
| 2 | + | ||
| 3 | +import cn.hutool.core.util.StrUtil; | ||
| 4 | + | ||
| 5 | +import java.sql.SQLException; | ||
| 6 | +import java.util.*; | ||
| 7 | + | ||
| 8 | +/** | ||
| 9 | + * JDBC 异常工具类 | ||
| 10 | + * | ||
| 11 | + * @author 芋道源码 | ||
| 12 | + */ | ||
| 13 | +public class JdbcExceptionUtils { | ||
| 14 | + | ||
| 15 | + /** | ||
| 16 | + * 表示“表不存在”的 SQLState | ||
| 17 | + * | ||
| 18 | + * 1. 42S02:ANSI / ODBC 标准,MySQL(1146)、MariaDB、H2、SQL Server(jTDS)等使用 | ||
| 19 | + * 2. S0002:X/Open 旧标准,SQL Server(208,mssql-jdbc)使用 | ||
| 20 | + * 3. 42P01:PostgreSQL 的 undefined_table,openGauss、KingbaseES 等 PostgreSQL 兼容数据库同样使用 | ||
| 21 | + */ | ||
| 22 | + private static final Set<String> TABLE_NOT_EXISTS_SQL_STATES = new HashSet<>(Arrays.asList("42S02", "S0002", "42P01")); | ||
| 23 | + | ||
| 24 | + /** | ||
| 25 | + * Oracle 的“表或视图不存在”错误码:ORA-00942,对应 SQLState 为通用的 42000,需要同时匹配错误码 | ||
| 26 | + */ | ||
| 27 | + private static final int ORACLE_TABLE_NOT_EXISTS_ERROR_CODE = 942; | ||
| 28 | + private static final String ORACLE_TABLE_NOT_EXISTS_SQL_STATE = "42000"; | ||
| 29 | + | ||
| 30 | + /** | ||
| 31 | + * 兜底的“表不存在”异常文案,兼容未携带 SQLState 的场景(MySQL 的 Table 'xxx' doesn't exist) | ||
| 32 | + */ | ||
| 33 | + private static final String TABLE_NOT_EXISTS_MESSAGE = "doesn't exist"; | ||
| 34 | + | ||
| 35 | + /** | ||
| 36 | + * 判断异常是否为“表不存在” | ||
| 37 | + * | ||
| 38 | + * 沿 cause 链、{@link SQLException#getNextException()} 链查找,优先识别 SQLState、错误码。 | ||
| 39 | + * 仅在整条链都没有有效 SQLState 时兜底匹配异常文案,避免外层包装信息覆盖明确的数据库错误。 | ||
| 40 | + * 列不存在、权限不足、语法错误等其它异常,都返回 false,由调用方原样抛出。 | ||
| 41 | + * | ||
| 42 | + * @param throwable 异常 | ||
| 43 | + * @return 是否表不存在 | ||
| 44 | + */ | ||
| 45 | + @SuppressWarnings("PatternVariableCanBeUsed") // 保持 JDK8 兼容性 | ||
| 46 | + public static boolean isTableNotExists(Throwable throwable) { | ||
| 47 | + Set<Throwable> visited = Collections.newSetFromMap(new IdentityHashMap<>()); | ||
| 48 | + Deque<Throwable> queue = new ArrayDeque<>(); | ||
| 49 | + if (throwable != null) { | ||
| 50 | + queue.add(throwable); | ||
| 51 | + } | ||
| 52 | + boolean hasSqlState = false; | ||
| 53 | + boolean hasLegacyMessage = false; | ||
| 54 | + while (!queue.isEmpty()) { | ||
| 55 | + Throwable current = queue.poll(); | ||
| 56 | + if (!visited.add(current)) { // 避免循环引用 | ||
| 57 | + continue; | ||
| 58 | + } | ||
| 59 | + if (current instanceof SQLException) { | ||
| 60 | + SQLException sqlException = (SQLException) current; | ||
| 61 | + if (isTableNotExists(sqlException)) { | ||
| 62 | + return true; | ||
| 63 | + } | ||
| 64 | + hasSqlState |= StrUtil.isNotBlank(sqlException.getSQLState()); | ||
| 65 | + addIfNotNull(queue, sqlException.getNextException()); | ||
| 66 | + } | ||
| 67 | + hasLegacyMessage |= StrUtil.contains(current.getMessage(), TABLE_NOT_EXISTS_MESSAGE); | ||
| 68 | + addIfNotNull(queue, current.getCause()); | ||
| 69 | + } | ||
| 70 | + return !hasSqlState && hasLegacyMessage; | ||
| 71 | + } | ||
| 72 | + | ||
| 73 | + private static boolean isTableNotExists(SQLException sqlException) { | ||
| 74 | + String sqlState = sqlException.getSQLState(); | ||
| 75 | + if (TABLE_NOT_EXISTS_SQL_STATES.contains(sqlState)) { | ||
| 76 | + return true; | ||
| 77 | + } | ||
| 78 | + return sqlException.getErrorCode() == ORACLE_TABLE_NOT_EXISTS_ERROR_CODE | ||
| 79 | + && ORACLE_TABLE_NOT_EXISTS_SQL_STATE.equals(sqlState); | ||
| 80 | + } | ||
| 81 | + | ||
| 82 | + private static void addIfNotNull(Deque<Throwable> queue, Throwable throwable) { | ||
| 83 | + if (throwable != null) { | ||
| 84 | + queue.add(throwable); | ||
| 85 | + } | ||
| 86 | + } | ||
| 87 | + | ||
| 88 | +} |
yudao-framework/yudao-common/src/test/java/cn/iocoder/yudao/framework/common/util/jdbc/JdbcExceptionUtilsTest.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.common.util.jdbc; | ||
| 2 | + | ||
| 3 | +import org.junit.jupiter.api.Test; | ||
| 4 | + | ||
| 5 | +import java.sql.SQLException; | ||
| 6 | + | ||
| 7 | +import static org.junit.jupiter.api.Assertions.assertFalse; | ||
| 8 | +import static org.junit.jupiter.api.Assertions.assertTrue; | ||
| 9 | + | ||
| 10 | +/** | ||
| 11 | + * {@link JdbcExceptionUtils} 的单元测试 | ||
| 12 | + * | ||
| 13 | + * @author 芋道源码 | ||
| 14 | + */ | ||
| 15 | +public class JdbcExceptionUtilsTest { | ||
| 16 | + | ||
| 17 | + // ========== 表不存在:SQLState、错误码 ========== | ||
| 18 | + | ||
| 19 | + @Test | ||
| 20 | + public void testIsTableNotExists_mysql() { | ||
| 21 | + assertTrue(JdbcExceptionUtils.isTableNotExists( | ||
| 22 | + new SQLException("Table 'ruoyi-vue-pro.bpm_category' doesn't exist", "42S02", 1146))); | ||
| 23 | + // SQLState 识别,不依赖文案 | ||
| 24 | + assertTrue(JdbcExceptionUtils.isTableNotExists(new SQLException("unknown", "42S02", 1146))); | ||
| 25 | + } | ||
| 26 | + | ||
| 27 | + @Test | ||
| 28 | + public void testIsTableNotExists_postgresql() { | ||
| 29 | + assertTrue(JdbcExceptionUtils.isTableNotExists( | ||
| 30 | + new SQLException("ERROR: relation \"bpm_category\" does not exist", "42P01"))); | ||
| 31 | + } | ||
| 32 | + | ||
| 33 | + @Test | ||
| 34 | + public void testIsTableNotExists_oracle() { | ||
| 35 | + assertTrue(JdbcExceptionUtils.isTableNotExists( | ||
| 36 | + new SQLException("ORA-00942: table or view does not exist\n", "42000", 942))); | ||
| 37 | + } | ||
| 38 | + | ||
| 39 | + @Test | ||
| 40 | + public void testIsTableNotExists_sqlServer() { | ||
| 41 | + // mssql-jdbc | ||
| 42 | + assertTrue(JdbcExceptionUtils.isTableNotExists( | ||
| 43 | + new SQLException("Invalid object name 'bpm_category'.", "S0002", 208))); | ||
| 44 | + // jTDS | ||
| 45 | + assertTrue(JdbcExceptionUtils.isTableNotExists( | ||
| 46 | + new SQLException("Invalid object name 'bpm_category'.", "42S02", 208))); | ||
| 47 | + } | ||
| 48 | + | ||
| 49 | + @Test | ||
| 50 | + public void testIsTableNotExists_messageFallback() { | ||
| 51 | + // 未携带 SQLState 时,兜底匹配 MySQL 文案 | ||
| 52 | + assertTrue(JdbcExceptionUtils.isTableNotExists( | ||
| 53 | + new SQLException("Table 'ruoyi-vue-pro.mp_account' doesn't exist"))); | ||
| 54 | + assertTrue(JdbcExceptionUtils.isTableNotExists( | ||
| 55 | + new RuntimeException("### Cause: Table 'ruoyi-vue-pro.mp_account' doesn't exist"))); | ||
| 56 | + } | ||
| 57 | + | ||
| 58 | + // ========== 表不存在:异常链 ========== | ||
| 59 | + | ||
| 60 | + @Test | ||
| 61 | + public void testIsTableNotExists_causeChain() { | ||
| 62 | + SQLException sqlException = new SQLException("ERROR: relation \"mp_account\" does not exist", "42P01"); | ||
| 63 | + // 外层包装的文案不包含“表不存在”,需要沿 cause 链查找 | ||
| 64 | + assertTrue(JdbcExceptionUtils.isTableNotExists( | ||
| 65 | + new RuntimeException("nested exception", new IllegalStateException(sqlException)))); | ||
| 66 | + } | ||
| 67 | + | ||
| 68 | + @Test | ||
| 69 | + public void testIsTableNotExists_nextExceptionChain() { | ||
| 70 | + SQLException batchException = new SQLException("Batch entry 0 was aborted", "25P02"); | ||
| 71 | + batchException.setNextException(new SQLException("ERROR: relation \"mp_account\" does not exist", "42P01")); | ||
| 72 | + assertTrue(JdbcExceptionUtils.isTableNotExists(new RuntimeException(batchException))); | ||
| 73 | + } | ||
| 74 | + | ||
| 75 | + // ========== 非表不存在:原样返回 false ========== | ||
| 76 | + | ||
| 77 | + @Test | ||
| 78 | + public void testIsTableNotExists_columnNotExists() { | ||
| 79 | + // MySQL | ||
| 80 | + assertFalse(JdbcExceptionUtils.isTableNotExists( | ||
| 81 | + new SQLException("Unknown column 'xxx' in 'field list'", "42S22", 1054))); | ||
| 82 | + // PostgreSQL | ||
| 83 | + assertFalse(JdbcExceptionUtils.isTableNotExists( | ||
| 84 | + new SQLException("ERROR: column \"xxx\" does not exist", "42703"))); | ||
| 85 | + // Oracle | ||
| 86 | + assertFalse(JdbcExceptionUtils.isTableNotExists( | ||
| 87 | + new SQLException("ORA-00904: \"XXX\": invalid identifier\n", "42000", 904))); | ||
| 88 | + // SQL Server | ||
| 89 | + assertFalse(JdbcExceptionUtils.isTableNotExists( | ||
| 90 | + new SQLException("Invalid column name 'xxx'.", "S0001", 207))); | ||
| 91 | + } | ||
| 92 | + | ||
| 93 | + @Test | ||
| 94 | + public void testIsTableNotExists_permissionDenied() { | ||
| 95 | + // MySQL | ||
| 96 | + assertFalse(JdbcExceptionUtils.isTableNotExists( | ||
| 97 | + new SQLException("SELECT command denied to user 'u'@'localhost' for table 'bpm_category'", "42000", 1142))); | ||
| 98 | + // PostgreSQL | ||
| 99 | + assertFalse(JdbcExceptionUtils.isTableNotExists( | ||
| 100 | + new SQLException("ERROR: permission denied for table bpm_category", "42501"))); | ||
| 101 | + // Oracle | ||
| 102 | + assertFalse(JdbcExceptionUtils.isTableNotExists( | ||
| 103 | + new SQLException("ORA-01031: insufficient privileges\n", "42000", 1031))); | ||
| 104 | + } | ||
| 105 | + | ||
| 106 | + @Test | ||
| 107 | + public void testIsTableNotExists_sqlStateTakesPrecedenceOverLegacyMessage() { | ||
| 108 | + // 用户名或外层包装信息可能包含旧文案,明确的权限/列错误仍应保留。 | ||
| 109 | + assertFalse(JdbcExceptionUtils.isTableNotExists(new SQLException( | ||
| 110 | + "SELECT command denied to user 'doesn't exist'@'localhost' for table 'erp_product'", | ||
| 111 | + "42000", 1142))); | ||
| 112 | + assertFalse(JdbcExceptionUtils.isTableNotExists(new RuntimeException( | ||
| 113 | + "Previous lookup: Table 'erp_product' doesn't exist", | ||
| 114 | + new SQLException("column missing", "42703")))); | ||
| 115 | + } | ||
| 116 | + | ||
| 117 | + @Test | ||
| 118 | + public void testIsTableNotExists_otherErrors() { | ||
| 119 | + // 语法错误:与 Oracle ORA-00942 同为 42000,但错误码不同 | ||
| 120 | + assertFalse(JdbcExceptionUtils.isTableNotExists( | ||
| 121 | + new SQLException("You have an error in your SQL syntax", "42000", 1064))); | ||
| 122 | + assertFalse(JdbcExceptionUtils.isTableNotExists( | ||
| 123 | + new SQLException("ERROR: syntax error at or near \"FROM\"", "42601"))); | ||
| 124 | + // 错误码相同,但 SQLState 不是“表不存在”:Oracle ORA-01146、SQL Server 942 | ||
| 125 | + assertFalse(JdbcExceptionUtils.isTableNotExists( | ||
| 126 | + new SQLException("ORA-01146: cannot start online backup", "72000", 1146))); | ||
| 127 | + assertFalse(JdbcExceptionUtils.isTableNotExists( | ||
| 128 | + new SQLException("Database 'xxx' cannot be opened because it is offline.", "S0001", 942))); | ||
| 129 | + // 唯一键冲突、连接异常 | ||
| 130 | + assertFalse(JdbcExceptionUtils.isTableNotExists( | ||
| 131 | + new SQLException("Duplicate entry '1' for key 'PRIMARY'", "23000", 1062))); | ||
| 132 | + assertFalse(JdbcExceptionUtils.isTableNotExists(new SQLException("Connection refused", "08001"))); | ||
| 133 | + // 非 MySQL 文案的“does not exist”,且无 SQLState | ||
| 134 | + assertFalse(JdbcExceptionUtils.isTableNotExists(new RuntimeException("Table does not exist"))); | ||
| 135 | + } | ||
| 136 | + | ||
| 137 | + @Test | ||
| 138 | + public void testIsTableNotExists_nullAndEmpty() { | ||
| 139 | + assertFalse(JdbcExceptionUtils.isTableNotExists(null)); | ||
| 140 | + assertFalse(JdbcExceptionUtils.isTableNotExists(new RuntimeException((String) null))); | ||
| 141 | + assertFalse(JdbcExceptionUtils.isTableNotExists(new SQLException((String) null))); | ||
| 142 | + assertFalse(JdbcExceptionUtils.isTableNotExists( | ||
| 143 | + new RuntimeException(null, new SQLException(null, null, 0)))); | ||
| 144 | + } | ||
| 145 | + | ||
| 146 | + @Test | ||
| 147 | + public void testIsTableNotExists_cycle() { | ||
| 148 | + // cause 链循环 | ||
| 149 | + RuntimeException a = new RuntimeException("a"); | ||
| 150 | + RuntimeException b = new RuntimeException("b", a); | ||
| 151 | + a.initCause(b); | ||
| 152 | + assertFalse(JdbcExceptionUtils.isTableNotExists(a)); | ||
| 153 | + | ||
| 154 | + // nextException 链循环 | ||
| 155 | + SQLException s1 = new SQLException("s1", "HY000", 1); | ||
| 156 | + SQLException s2 = new SQLException("s2", "HY000", 2); | ||
| 157 | + s1.setNextException(s2); | ||
| 158 | + s2.setNextException(s1); | ||
| 159 | + assertFalse(JdbcExceptionUtils.isTableNotExists(s1)); | ||
| 160 | + | ||
| 161 | + // 循环链中存在“表不存在”时,仍可识别 | ||
| 162 | + SQLException s3 = new SQLException("s3", "HY000", 3); | ||
| 163 | + SQLException s4 = new SQLException("ERROR: relation \"mp_account\" does not exist", "42P01"); | ||
| 164 | + s3.setNextException(s4); | ||
| 165 | + s4.setNextException(s3); | ||
| 166 | + assertTrue(JdbcExceptionUtils.isTableNotExists(s4)); | ||
| 167 | + assertTrue(JdbcExceptionUtils.isTableNotExists(s3)); | ||
| 168 | + } | ||
| 169 | + | ||
| 170 | +} |
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; | @@ -11,6 +11,7 @@ import cn.iocoder.yudao.framework.common.exception.ServiceException; | ||
| 11 | import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil; | 11 | import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil; |
| 12 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; | 12 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 13 | import cn.iocoder.yudao.framework.common.util.collection.SetUtils; | 13 | import cn.iocoder.yudao.framework.common.util.collection.SetUtils; |
| 14 | +import cn.iocoder.yudao.framework.common.util.jdbc.JdbcExceptionUtils; | ||
| 14 | import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | 15 | import cn.iocoder.yudao.framework.common.util.json.JsonUtils; |
| 15 | import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils; | 16 | import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils; |
| 16 | import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; | 17 | import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; |
| @@ -396,10 +397,10 @@ public class GlobalExceptionHandler { | @@ -396,10 +397,10 @@ public class GlobalExceptionHandler { | ||
| 396 | * @return 如果是 Table 不存在的异常,则返回对应的 CommonResult | 397 | * @return 如果是 Table 不存在的异常,则返回对应的 CommonResult |
| 397 | */ | 398 | */ |
| 398 | private CommonResult<?> handleTableNotExists(Throwable ex) { | 399 | private CommonResult<?> handleTableNotExists(Throwable ex) { |
| 399 | - String message = ExceptionUtil.getRootCauseMessage(ex); | ||
| 400 | - if (!message.contains("doesn't exist")) { | 400 | + if (!JdbcExceptionUtils.isTableNotExists(ex)) { |
| 401 | return null; | 401 | return null; |
| 402 | } | 402 | } |
| 403 | + String message = StrUtil.nullToEmpty(ExceptionUtil.getRootCauseMessage(ex)); | ||
| 403 | // 1. 工作流 | 404 | // 1. 工作流 |
| 404 | if (message.contains("bpm_")) { | 405 | if (message.contains("bpm_")) { |
| 405 | log.error("[工作流模块 yudao-module-bpm - 表结构未导入][参考 https://cloud.iocoder.cn/bpm/ 开启]"); | 406 | log.error("[工作流模块 yudao-module-bpm - 表结构未导入][参考 https://cloud.iocoder.cn/bpm/ 开启]"); |
yudao-framework/yudao-spring-boot-starter-web/src/test/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandlerTest.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.web.core.handler; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.common.biz.infra.logger.ApiErrorLogCommonApi; | ||
| 4 | +import cn.iocoder.yudao.framework.common.pojo.CommonResult; | ||
| 5 | +import jakarta.servlet.http.HttpServletRequest; | ||
| 6 | +import org.junit.jupiter.api.BeforeEach; | ||
| 7 | +import org.junit.jupiter.api.Test; | ||
| 8 | + | ||
| 9 | +import java.sql.SQLException; | ||
| 10 | + | ||
| 11 | +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; | ||
| 12 | +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.NOT_IMPLEMENTED; | ||
| 13 | +import static org.junit.jupiter.api.Assertions.assertEquals; | ||
| 14 | +import static org.junit.jupiter.api.Assertions.assertTrue; | ||
| 15 | +import static org.mockito.Mockito.mock; | ||
| 16 | + | ||
| 17 | +/** | ||
| 18 | + * {@link GlobalExceptionHandler} 的单元测试 | ||
| 19 | + * | ||
| 20 | + * 重点验证“表不存在”的识别:按 SQLState / 错误码识别多种数据库,其它 SQL 异常不会被当作表不存在 | ||
| 21 | + * | ||
| 22 | + * @author 芋道源码 | ||
| 23 | + */ | ||
| 24 | +public class GlobalExceptionHandlerTest { | ||
| 25 | + | ||
| 26 | + private GlobalExceptionHandler globalExceptionHandler; | ||
| 27 | + | ||
| 28 | + private HttpServletRequest request; | ||
| 29 | + | ||
| 30 | + @BeforeEach | ||
| 31 | + public void setUp() { | ||
| 32 | + globalExceptionHandler = new GlobalExceptionHandler("test", mock(ApiErrorLogCommonApi.class)); | ||
| 33 | + request = mock(HttpServletRequest.class); | ||
| 34 | + } | ||
| 35 | + | ||
| 36 | + @Test | ||
| 37 | + public void testDefaultExceptionHandler_tableNotExists_mysql() { | ||
| 38 | + // 准备参数 | ||
| 39 | + Exception ex = new RuntimeException("nested exception", | ||
| 40 | + new SQLException("Table 'ruoyi-vue-pro.erp_product' doesn't exist", "42S02", 1146)); | ||
| 41 | + | ||
| 42 | + // 调用 | ||
| 43 | + CommonResult<?> result = globalExceptionHandler.defaultExceptionHandler(request, ex); | ||
| 44 | + // 断言 | ||
| 45 | + assertEquals(NOT_IMPLEMENTED.getCode(), result.getCode()); | ||
| 46 | + assertTrue(result.getMsg().contains("yudao-module-erp")); | ||
| 47 | + } | ||
| 48 | + | ||
| 49 | + @Test | ||
| 50 | + public void testDefaultExceptionHandler_tableNotExists_postgresql() { | ||
| 51 | + // 准备参数:PostgreSQL 的文案不包含 doesn't exist,通过 SQLState 42P01 识别 | ||
| 52 | + Exception ex = new RuntimeException("nested exception", | ||
| 53 | + new SQLException("ERROR: relation \"bpm_category\" does not exist", "42P01")); | ||
| 54 | + | ||
| 55 | + // 调用 | ||
| 56 | + CommonResult<?> result = globalExceptionHandler.defaultExceptionHandler(request, ex); | ||
| 57 | + // 断言 | ||
| 58 | + assertEquals(NOT_IMPLEMENTED.getCode(), result.getCode()); | ||
| 59 | + assertTrue(result.getMsg().contains("yudao-module-bpm")); | ||
| 60 | + } | ||
| 61 | + | ||
| 62 | + @Test | ||
| 63 | + public void testDefaultExceptionHandler_tableNotExists_noPrefixMatched() { | ||
| 64 | + // 准备参数:Oracle 的文案不包含表名,无法匹配模块前缀,按系统异常处理 | ||
| 65 | + Exception ex = new RuntimeException("nested exception", | ||
| 66 | + new SQLException("ORA-00942: table or view does not exist\n", "42000", 942)); | ||
| 67 | + | ||
| 68 | + // 调用 | ||
| 69 | + CommonResult<?> result = globalExceptionHandler.defaultExceptionHandler(request, ex); | ||
| 70 | + // 断言 | ||
| 71 | + assertEquals(INTERNAL_SERVER_ERROR.getCode(), result.getCode()); | ||
| 72 | + } | ||
| 73 | + | ||
| 74 | + @Test | ||
| 75 | + public void testDefaultExceptionHandler_columnNotExists() { | ||
| 76 | + // 准备参数:列不存在,即使文案包含模块前缀,也不是表不存在 | ||
| 77 | + Exception ex = new RuntimeException("nested exception", | ||
| 78 | + new SQLException("ERROR: column \"xxx\" of relation \"bpm_category\" does not exist", "42703")); | ||
| 79 | + | ||
| 80 | + // 调用 | ||
| 81 | + CommonResult<?> result = globalExceptionHandler.defaultExceptionHandler(request, ex); | ||
| 82 | + // 断言 | ||
| 83 | + assertEquals(INTERNAL_SERVER_ERROR.getCode(), result.getCode()); | ||
| 84 | + } | ||
| 85 | + | ||
| 86 | + @Test | ||
| 87 | + public void testDefaultExceptionHandler_permissionDenied() { | ||
| 88 | + // 准备参数:权限不足,不是表不存在 | ||
| 89 | + Exception ex = new RuntimeException("nested exception", | ||
| 90 | + new SQLException("SELECT command denied to user 'u'@'localhost' for table 'erp_product'", "42000", 1142)); | ||
| 91 | + | ||
| 92 | + // 调用 | ||
| 93 | + CommonResult<?> result = globalExceptionHandler.defaultExceptionHandler(request, ex); | ||
| 94 | + // 断言 | ||
| 95 | + assertEquals(INTERNAL_SERVER_ERROR.getCode(), result.getCode()); | ||
| 96 | + } | ||
| 97 | + | ||
| 98 | + @Test | ||
| 99 | + public void testDefaultExceptionHandler_permissionDeniedWithLegacyPhrase() { | ||
| 100 | + Exception ex = new RuntimeException("nested exception", new SQLException( | ||
| 101 | + "SELECT command denied to user 'doesn't exist'@'localhost' for table 'erp_product'", | ||
| 102 | + "42000", 1142)); | ||
| 103 | + | ||
| 104 | + CommonResult<?> result = globalExceptionHandler.defaultExceptionHandler(request, ex); | ||
| 105 | + | ||
| 106 | + assertEquals(INTERNAL_SERVER_ERROR.getCode(), result.getCode()); | ||
| 107 | + } | ||
| 108 | + | ||
| 109 | + @Test | ||
| 110 | + public void testDefaultExceptionHandler_nullMessage() { | ||
| 111 | + // 调用 | ||
| 112 | + CommonResult<?> result = globalExceptionHandler.defaultExceptionHandler(request, | ||
| 113 | + new RuntimeException((String) null)); | ||
| 114 | + // 断言 | ||
| 115 | + assertEquals(INTERNAL_SERVER_ERROR.getCode(), result.getCode()); | ||
| 116 | + } | ||
| 117 | + | ||
| 118 | +} |
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 | @@ -5,6 +5,8 @@ import cn.iocoder.yudao.module.ai1.controller.admin.home.vo.Ai1HomeMessageSummar | ||
| 5 | import cn.iocoder.yudao.module.ai1.controller.admin.home.vo.Ai1HomeSummaryRespVO; | 5 | import cn.iocoder.yudao.module.ai1.controller.admin.home.vo.Ai1HomeSummaryRespVO; |
| 6 | import org.apache.ibatis.annotations.Mapper; | 6 | import org.apache.ibatis.annotations.Mapper; |
| 7 | import org.apache.ibatis.annotations.Param; | 7 | import org.apache.ibatis.annotations.Param; |
| 8 | +import org.apache.ibatis.annotations.Result; | ||
| 9 | +import org.apache.ibatis.annotations.Results; | ||
| 8 | import org.apache.ibatis.annotations.Select; | 10 | import org.apache.ibatis.annotations.Select; |
| 9 | 11 | ||
| 10 | import java.time.LocalDateTime; | 12 | import java.time.LocalDateTime; |
| @@ -18,32 +20,36 @@ import java.util.List; | @@ -18,32 +20,36 @@ import java.util.List; | ||
| 18 | @Mapper | 20 | @Mapper |
| 19 | public interface Ai1HomeMapper { | 21 | public interface Ai1HomeMapper { |
| 20 | 22 | ||
| 21 | - @Select("SELECT (SELECT COUNT(1) FROM ai1_agent WHERE deleted = FALSE) AS agentCount, " + | ||
| 22 | - "(SELECT COUNT(1) FROM ai1_skill WHERE deleted = FALSE) AS skillCount, " + | ||
| 23 | - "(SELECT COUNT(1) FROM ai1_mcp WHERE deleted = FALSE) AS mcpCount, " + | ||
| 24 | - "(SELECT COUNT(1) FROM ai1_model WHERE deleted = FALSE) AS modelCount") | 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, " + | ||
| 25 | + "(SELECT COUNT(1) FROM ai1_mcp WHERE deleted = 0) AS mcpCount, " + | ||
| 26 | + "(SELECT COUNT(1) FROM ai1_model WHERE deleted = 0) AS modelCount") | ||
| 25 | Ai1HomeSummaryRespVO selectSummary(); | 27 | Ai1HomeSummaryRespVO selectSummary(); |
| 26 | 28 | ||
| 27 | /** | 29 | /** |
| 28 | * 按天统计消息数量,仅返回有消息的日期;使用 CAST AS DATE 兼容 MySQL 与 H2 | 30 | * 按天统计消息数量,仅返回有消息的日期;使用 CAST AS DATE 兼容 MySQL 与 H2 |
| 29 | */ | 31 | */ |
| 30 | - @Select("SELECT CAST(create_time AS DATE) AS `date`, COUNT(1) AS `count` " + | 32 | + @Select("SELECT CAST(create_time AS DATE) AS statDate, COUNT(1) AS count " + |
| 31 | "FROM ai1_session_message " + | 33 | "FROM ai1_session_message " + |
| 32 | - "WHERE create_time BETWEEN #{beginTime} AND #{endTime} AND deleted = FALSE " + | 34 | + "WHERE create_time BETWEEN #{beginTime} AND #{endTime} AND deleted = 0 " + |
| 33 | "GROUP BY CAST(create_time AS DATE) " + | 35 | "GROUP BY CAST(create_time AS DATE) " + |
| 34 | - "ORDER BY `date`") | 36 | + "ORDER BY statDate") |
| 37 | + @Results({ | ||
| 38 | + @Result(column = "statDate", property = "date"), | ||
| 39 | + @Result(column = "count", property = "count") | ||
| 40 | + }) | ||
| 35 | List<Ai1HomeMessageSummaryByDateRespVO> selectMessageSummaryListByCreateTimeBetweenGroupByDate( | 41 | List<Ai1HomeMessageSummaryByDateRespVO> selectMessageSummaryListByCreateTimeBetweenGroupByDate( |
| 36 | @Param("beginTime") LocalDateTime beginTime, @Param("endTime") LocalDateTime endTime); | 42 | @Param("beginTime") LocalDateTime beginTime, @Param("endTime") LocalDateTime endTime); |
| 37 | 43 | ||
| 38 | /** | 44 | /** |
| 39 | * 按 Agent 统计消息数量,按数量倒序,数量相同按 Agent 编号升序;已删除会话下的消息不计入 | 45 | * 按 Agent 统计消息数量,按数量倒序,数量相同按 Agent 编号升序;已删除会话下的消息不计入 |
| 40 | */ | 46 | */ |
| 41 | - @Select("SELECT c.agent_id AS agentId, COUNT(1) AS `count` " + | 47 | + @Select("SELECT c.agent_id AS agentId, COUNT(1) AS count " + |
| 42 | "FROM ai1_session_message m " + | 48 | "FROM ai1_session_message m " + |
| 43 | - "INNER JOIN ai1_session c ON m.session_id = c.id AND c.deleted = FALSE " + | ||
| 44 | - "WHERE m.create_time BETWEEN #{beginTime} AND #{endTime} AND m.deleted = FALSE " + | 49 | + "INNER JOIN ai1_session c ON m.session_id = c.id AND c.deleted = 0 " + |
| 50 | + "WHERE m.create_time BETWEEN #{beginTime} AND #{endTime} AND m.deleted = 0 " + | ||
| 45 | "GROUP BY c.agent_id " + | 51 | "GROUP BY c.agent_id " + |
| 46 | - "ORDER BY `count` DESC, c.agent_id") | 52 | + "ORDER BY count DESC, c.agent_id") |
| 47 | List<Ai1HomeMessageSummaryByAgentRespVO> selectMessageSummaryListByCreateTimeBetweenGroupByAgentId( | 53 | List<Ai1HomeMessageSummaryByAgentRespVO> selectMessageSummaryListByCreateTimeBetweenGroupByAgentId( |
| 48 | @Param("beginTime") LocalDateTime beginTime, @Param("endTime") LocalDateTime endTime); | 54 | @Param("beginTime") LocalDateTime beginTime, @Param("endTime") LocalDateTime endTime); |
| 49 | 55 |
yudao-module-crm/src/main/resources/mapper/permission/CrmOwnerRecordMapper.xml
| @@ -16,7 +16,7 @@ | @@ -16,7 +16,7 @@ | ||
| 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 time | 19 | + GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d') |
| 20 | </select> | 20 | </select> |
| 21 | 21 | ||
| 22 | <select id="selectPoolCustomerTakeCountByDate" | 22 | <select id="selectPoolCustomerTakeCountByDate" |
| @@ -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 time | 35 | + GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d') |
| 36 | </select> | 36 | </select> |
| 37 | 37 | ||
| 38 | <select id="selectPoolCustomerPutCountByUser" | 38 | <select id="selectPoolCustomerPutCountByUser" |
yudao-module-crm/src/main/resources/mapper/statistics/CrmStatisticsCustomerMapper.xml
| @@ -13,15 +13,15 @@ | @@ -13,15 +13,15 @@ | ||
| 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 time | 16 | + GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d') |
| 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 DATE_FORMAT(customer.create_time, '%Y-%m-%d') AS time, |
| 22 | COUNT(DISTINCT customer.id) AS customer_deal_count | 22 | COUNT(DISTINCT customer.id) AS customer_deal_count |
| 23 | - FROM crm_customer AS customer | ||
| 24 | - LEFT JOIN crm_contract AS contract ON contract.customer_id = customer.id | 23 | + FROM crm_customer customer |
| 24 | + LEFT JOIN crm_contract contract ON contract.customer_id = customer.id | ||
| 25 | WHERE customer.deleted = 0 | 25 | WHERE customer.deleted = 0 |
| 26 | AND contract.deleted = 0 | 26 | AND contract.deleted = 0 |
| 27 | AND contract.audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status} | 27 | AND contract.audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status} |
| @@ -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 time | 33 | + GROUP BY DATE_FORMAT(customer.create_time, '%Y-%m-%d') |
| 34 | </select> | 34 | </select> |
| 35 | 35 | ||
| 36 | <select id="selectCustomerCreateCountGroupByUser" | 36 | <select id="selectCustomerCreateCountGroupByUser" |
| @@ -51,8 +51,8 @@ | @@ -51,8 +51,8 @@ | ||
| 51 | resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerSummaryByUserRespVO"> | 51 | resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerSummaryByUserRespVO"> |
| 52 | SELECT customer.owner_user_id, | 52 | SELECT customer.owner_user_id, |
| 53 | COUNT(DISTINCT customer.id) AS customer_deal_count | 53 | COUNT(DISTINCT customer.id) AS customer_deal_count |
| 54 | - FROM crm_customer AS customer | ||
| 55 | - LEFT JOIN crm_contract AS contract ON contract.customer_id = customer.id | 54 | + FROM crm_customer customer |
| 55 | + LEFT JOIN crm_contract contract ON contract.customer_id = customer.id | ||
| 56 | WHERE customer.deleted = 0 | 56 | WHERE customer.deleted = 0 |
| 57 | AND contract.deleted = 0 | 57 | AND contract.deleted = 0 |
| 58 | AND contract.audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status} | 58 | AND contract.audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status} |
| @@ -67,7 +67,7 @@ | @@ -67,7 +67,7 @@ | ||
| 67 | <select id="selectContractPriceGroupByUser" | 67 | <select id="selectContractPriceGroupByUser" |
| 68 | resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerSummaryByUserRespVO"> | 68 | resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerSummaryByUserRespVO"> |
| 69 | SELECT owner_user_id, | 69 | SELECT owner_user_id, |
| 70 | - IFNULL(SUM(total_price), 0) AS contract_price | 70 | + COALESCE(SUM(total_price), 0) AS contract_price |
| 71 | FROM crm_contract | 71 | FROM crm_contract |
| 72 | WHERE deleted = 0 | 72 | WHERE deleted = 0 |
| 73 | AND audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status} | 73 | AND audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status} |
| @@ -82,7 +82,7 @@ | @@ -82,7 +82,7 @@ | ||
| 82 | <select id="selectReceivablePriceGroupByUser" | 82 | <select id="selectReceivablePriceGroupByUser" |
| 83 | resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerSummaryByUserRespVO"> | 83 | resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerSummaryByUserRespVO"> |
| 84 | SELECT owner_user_id, | 84 | SELECT owner_user_id, |
| 85 | - IFNULL(SUM(price), 0) AS receivable_price | 85 | + COALESCE(SUM(price), 0) AS receivable_price |
| 86 | FROM crm_receivable | 86 | FROM crm_receivable |
| 87 | WHERE deleted = 0 | 87 | WHERE deleted = 0 |
| 88 | AND audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status} | 88 | AND audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status} |
| @@ -105,7 +105,7 @@ | @@ -105,7 +105,7 @@ | ||
| 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 time | 108 | + GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d') |
| 109 | </select> | 109 | </select> |
| 110 | 110 | ||
| 111 | <select id="selectFollowUpCustomerCountGroupByDate" | 111 | <select id="selectFollowUpCustomerCountGroupByDate" |
| @@ -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 time | 122 | + GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d') |
| 123 | </select> | 123 | </select> |
| 124 | 124 | ||
| 125 | <select id="selectFollowUpRecordCountGroupByUser" | 125 | <select id="selectFollowUpRecordCountGroupByUser" |
| @@ -162,7 +162,7 @@ | @@ -162,7 +162,7 @@ | ||
| 162 | </foreach> | 162 | </foreach> |
| 163 | AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime} | 163 | AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime} |
| 164 | AND biz_type = ${@cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum@CRM_CUSTOMER.type} | 164 | AND biz_type = ${@cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum@CRM_CUSTOMER.type} |
| 165 | - GROUP BY follow_up_type | 165 | + GROUP BY type |
| 166 | </select> | 166 | </select> |
| 167 | 167 | ||
| 168 | <select id="selectContractSummary" | 168 | <select id="selectContractSummary" |
| @@ -176,10 +176,10 @@ | @@ -176,10 +176,10 @@ | ||
| 176 | contract.name AS contract_name, | 176 | contract.name AS contract_name, |
| 177 | contract.total_price, | 177 | contract.total_price, |
| 178 | contract.order_date, | 178 | contract.order_date, |
| 179 | - IFNULL(receivable.price, 0) AS receivable_price | ||
| 180 | - FROM crm_customer AS customer | ||
| 181 | - INNER JOIN crm_contract AS contract ON customer.id = contract.customer_id | ||
| 182 | - LEFT JOIN crm_receivable AS receivable ON contract.id = receivable.contract_id | 179 | + COALESCE(receivable.price, 0) AS receivable_price |
| 180 | + FROM crm_customer customer | ||
| 181 | + INNER JOIN crm_contract contract ON customer.id = contract.customer_id | ||
| 182 | + LEFT JOIN crm_receivable receivable ON contract.id = receivable.contract_id | ||
| 183 | WHERE customer.deleted = 0 AND contract.deleted = 0 AND receivable.deleted = 0 | 183 | WHERE customer.deleted = 0 AND contract.deleted = 0 AND receivable.deleted = 0 |
| 184 | AND contract.audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status} | 184 | AND contract.audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status} |
| 185 | AND customer.owner_user_id IN | 185 | AND customer.owner_user_id IN |
| @@ -193,9 +193,9 @@ | @@ -193,9 +193,9 @@ | ||
| 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, | 195 | SELECT DATE_FORMAT(contract.order_date, '%Y-%m-%d') AS time, |
| 196 | - IFNULL(TRUNCATE(AVG(TIMESTAMPDIFF(DAY, customer.create_time, contract.order_date)), 1), 0) AS customer_deal_cycle | ||
| 197 | - FROM crm_customer AS customer | ||
| 198 | - LEFT JOIN crm_contract AS contract ON contract.customer_id = customer.id | 196 | + COALESCE(TRUNCATE(AVG(TIMESTAMPDIFF(DAY, customer.create_time, contract.order_date)), 1), 0) AS customer_deal_cycle |
| 197 | + FROM crm_customer customer | ||
| 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 |
| 200 | AND contract.audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status} | 200 | AND contract.audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status} |
| 201 | AND customer.owner_user_id IN | 201 | AND customer.owner_user_id IN |
| @@ -203,15 +203,15 @@ | @@ -203,15 +203,15 @@ | ||
| 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 time | 206 | + GROUP BY DATE_FORMAT(contract.order_date, '%Y-%m-%d') |
| 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 | - IFNULL(TRUNCATE(AVG(TIMESTAMPDIFF(DAY, customer.create_time, contract.order_date)), 1), 0) AS customer_deal_cycle | ||
| 213 | - FROM crm_customer AS customer | ||
| 214 | - LEFT JOIN crm_contract AS contract ON contract.customer_id = customer.id | 212 | + COALESCE(TRUNCATE(AVG(TIMESTAMPDIFF(DAY, customer.create_time, contract.order_date)), 1), 0) AS customer_deal_cycle |
| 213 | + FROM crm_customer customer | ||
| 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 |
| 216 | AND contract.audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status} | 216 | AND contract.audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status} |
| 217 | AND customer.owner_user_id IN | 217 | AND customer.owner_user_id IN |
| @@ -225,10 +225,10 @@ | @@ -225,10 +225,10 @@ | ||
| 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 | - IFNULL(TRUNCATE(AVG(TIMESTAMPDIFF(DAY, customer.create_time, contract.order_date)), 1), 0) AS customer_deal_cycle, | 228 | + COALESCE(TRUNCATE(AVG(TIMESTAMPDIFF(DAY, customer.create_time, contract.order_date)), 1), 0) 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 AS customer | ||
| 231 | - LEFT JOIN crm_contract AS contract ON customer.id = contract.customer_id | 230 | + FROM crm_customer customer |
| 231 | + LEFT JOIN crm_contract contract ON customer.id = contract.customer_id | ||
| 232 | WHERE customer.deleted = 0 | 232 | WHERE customer.deleted = 0 |
| 233 | AND contract.deleted = 0 | 233 | AND contract.deleted = 0 |
| 234 | AND contract.audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status} | 234 | AND contract.audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status} |
| @@ -244,12 +244,12 @@ | @@ -244,12 +244,12 @@ | ||
| 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 | - IFNULL(TRUNCATE(AVG(TIMESTAMPDIFF(DAY, customer.create_time, contract.order_date)), 1), 0) AS customer_deal_cycle, | 247 | + COALESCE(TRUNCATE(AVG(TIMESTAMPDIFF(DAY, customer.create_time, contract.order_date)), 1), 0) 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 AS customer | ||
| 250 | - LEFT JOIN crm_contract AS contract ON customer.id = contract.customer_id | ||
| 251 | - LEFT JOIN crm_contract_product AS contract_product ON contract_product.contract_id = contract.id | ||
| 252 | - LEFT JOIN crm_product AS product ON contract_product.product_id = product.id | 249 | + FROM crm_customer customer |
| 250 | + LEFT JOIN crm_contract contract ON customer.id = contract.customer_id | ||
| 251 | + LEFT JOIN crm_contract_product contract_product ON contract_product.contract_id = contract.id | ||
| 252 | + LEFT JOIN crm_product product ON contract_product.product_id = product.id | ||
| 253 | WHERE customer.deleted = 0 | 253 | WHERE customer.deleted = 0 |
| 254 | AND contract.deleted = 0 | 254 | AND contract.deleted = 0 |
| 255 | AND contract_product.deleted = 0 | 255 | AND contract_product.deleted = 0 |
| @@ -260,7 +260,7 @@ | @@ -260,7 +260,7 @@ | ||
| 260 | #{userId} | 260 | #{userId} |
| 261 | </foreach> | 261 | </foreach> |
| 262 | AND customer.create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime} | 262 | AND customer.create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime} |
| 263 | - GROUP BY product.id | 263 | + GROUP BY product.id, product.name |
| 264 | </select> | 264 | </select> |
| 265 | 265 | ||
| 266 | </mapper> | 266 | </mapper> |
yudao-module-crm/src/main/resources/mapper/statistics/CrmStatisticsFunnelMapper.xml
| @@ -41,7 +41,7 @@ | @@ -41,7 +41,7 @@ | ||
| 41 | SELECT | 41 | SELECT |
| 42 | end_status AS endStatus, | 42 | end_status AS endStatus, |
| 43 | COUNT(*) AS businessCount, | 43 | COUNT(*) AS businessCount, |
| 44 | - IFNULL(SUM(total_price), 0) AS totalPrice | 44 | + COALESCE(SUM(total_price), 0) AS totalPrice |
| 45 | FROM crm_business | 45 | FROM crm_business |
| 46 | WHERE deleted = 0 AND end_status IS NOT NULL | 46 | WHERE deleted = 0 AND end_status IS NOT NULL |
| 47 | <if test="statusTypeId != null"> | 47 | <if test="statusTypeId != null"> |
| @@ -64,7 +64,7 @@ | @@ -64,7 +64,7 @@ | ||
| 64 | status.percent AS statusPercent, | 64 | status.percent AS statusPercent, |
| 65 | status.sort AS sort, | 65 | status.sort AS sort, |
| 66 | COUNT(business.id) AS businessCount, | 66 | COUNT(business.id) AS businessCount, |
| 67 | - IFNULL(SUM(business.total_price), 0) AS totalPrice | 67 | + COALESCE(SUM(business.total_price), 0) AS totalPrice |
| 68 | FROM crm_business_status status | 68 | FROM crm_business_status status |
| 69 | LEFT JOIN crm_business business ON business.status_id = status.id | 69 | LEFT JOIN crm_business business ON business.status_id = status.id |
| 70 | AND business.deleted = 0 | 70 | AND business.deleted = 0 |
| @@ -88,7 +88,7 @@ | @@ -88,7 +88,7 @@ | ||
| 88 | SELECT | 88 | SELECT |
| 89 | DATE_FORMAT(create_time, '%Y-%m-%d') AS time, | 89 | DATE_FORMAT(create_time, '%Y-%m-%d') AS time, |
| 90 | COUNT(*) AS businessCreateCount, | 90 | COUNT(*) AS businessCreateCount, |
| 91 | - IFNULL(SUM(total_price), 0) AS totalPrice | 91 | + COALESCE(SUM(total_price), 0) AS totalPrice |
| 92 | FROM crm_business | 92 | FROM crm_business |
| 93 | WHERE deleted = 0 | 93 | WHERE deleted = 0 |
| 94 | <if test="statusTypeId != null"> | 94 | <if test="statusTypeId != null"> |
| @@ -100,7 +100,7 @@ | @@ -100,7 +100,7 @@ | ||
| 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 time | 103 | + GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d') |
| 104 | </select> | 104 | </select> |
| 105 | 105 | ||
| 106 | <select id="selectBusinessInversionRateSummaryByDate" | 106 | <select id="selectBusinessInversionRateSummaryByDate" |
| @@ -108,7 +108,7 @@ | @@ -108,7 +108,7 @@ | ||
| 108 | SELECT | 108 | SELECT |
| 109 | DATE_FORMAT(create_time, '%Y-%m-%d') AS time, | 109 | DATE_FORMAT(create_time, '%Y-%m-%d') AS time, |
| 110 | COUNT(*) AS businessCount, | 110 | COUNT(*) AS businessCount, |
| 111 | - SUM(IF(end_status = 1, 1, 0)) AS businessWinCount | 111 | + SUM(CASE WHEN end_status = 1 THEN 1 ELSE 0 END) AS businessWinCount |
| 112 | FROM crm_business | 112 | FROM crm_business |
| 113 | WHERE deleted = 0 | 113 | WHERE deleted = 0 |
| 114 | <if test="statusTypeId != null"> | 114 | <if test="statusTypeId != null"> |
| @@ -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 time | 123 | + GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d') |
| 124 | </select> | 124 | </select> |
| 125 | 125 | ||
| 126 | </mapper> | 126 | </mapper> |
yudao-module-crm/src/main/resources/mapper/statistics/CrmStatisticsPerformanceMapper.xml
| @@ -16,14 +16,14 @@ | @@ -16,14 +16,14 @@ | ||
| 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 time | 19 | + GROUP BY DATE_FORMAT(order_date, '%Y%m') |
| 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 | DATE_FORMAT(order_date, '%Y%m') AS time, |
| 26 | - IFNULL(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 |
| 29 | AND audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status} | 29 | AND audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status} |
| @@ -33,14 +33,14 @@ | @@ -33,14 +33,14 @@ | ||
| 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 time | 36 | + GROUP BY DATE_FORMAT(order_date, '%Y%m') |
| 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 | DATE_FORMAT(return_time, '%Y%m') AS time, |
| 43 | - IFNULL(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 |
| 46 | AND audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status} | 46 | AND audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status} |
| @@ -50,25 +50,25 @@ | @@ -50,25 +50,25 @@ | ||
| 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 time | 53 | + GROUP BY DATE_FORMAT(return_time, '%Y%m') |
| 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 DATE_FORMAT(contract.order_date, '%Y%m') AS time, |
| 59 | COUNT(DISTINCT contract.id) AS contract_count, | 59 | COUNT(DISTINCT contract.id) AS contract_count, |
| 60 | - IFNULL(SUM(contract.total_price), 0) AS contract_price, | ||
| 61 | - IFNULL(SUM(receivable.receivable_price), 0) AS receivable_price, | ||
| 62 | - IFNULL(SUM(contract.total_price), 0) - IFNULL(SUM(receivable.receivable_price), 0) AS unreceived_price | ||
| 63 | - FROM crm_contract AS contract | 60 | + COALESCE(SUM(contract.total_price), 0) AS contract_price, |
| 61 | + COALESCE(SUM(receivable.receivable_price), 0) AS receivable_price, | ||
| 62 | + COALESCE(SUM(contract.total_price), 0) - COALESCE(SUM(receivable.receivable_price), 0) AS unreceived_price | ||
| 63 | + FROM crm_contract contract | ||
| 64 | LEFT JOIN ( | 64 | LEFT JOIN ( |
| 65 | SELECT contract_id, | 65 | SELECT contract_id, |
| 66 | - IFNULL(SUM(price), 0) AS receivable_price | 66 | + COALESCE(SUM(price), 0) AS receivable_price |
| 67 | FROM crm_receivable | 67 | FROM crm_receivable |
| 68 | WHERE deleted = 0 | 68 | WHERE deleted = 0 |
| 69 | AND audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status} | 69 | AND audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status} |
| 70 | GROUP BY contract_id | 70 | GROUP BY contract_id |
| 71 | - ) AS receivable ON contract.id = receivable.contract_id | 71 | + ) receivable ON contract.id = receivable.contract_id |
| 72 | WHERE contract.deleted = 0 | 72 | WHERE contract.deleted = 0 |
| 73 | AND contract.audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status} | 73 | AND contract.audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status} |
| 74 | AND contract.owner_user_id in | 74 | AND contract.owner_user_id in |
| @@ -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 time | 80 | + GROUP BY DATE_FORMAT(contract.order_date, '%Y%m') |
| 81 | </select> | 81 | </select> |
| 82 | 82 | ||
| 83 | </mapper> | 83 | </mapper> |
yudao-module-crm/src/main/resources/mapper/statistics/CrmStatisticsPerformanceTargetMapper.xml
| @@ -11,7 +11,7 @@ | @@ -11,7 +11,7 @@ | ||
| 11 | <select id="selectContractActualByMonth" | 11 | <select id="selectContractActualByMonth" |
| 12 | resultMap="CrmStatisticsPerformanceTargetActualResultMap"> | 12 | resultMap="CrmStatisticsPerformanceTargetActualResultMap"> |
| 13 | SELECT MONTH(order_date) AS month, | 13 | SELECT MONTH(order_date) AS month, |
| 14 | - IFNULL(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 |
| 17 | AND audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status} | 17 | AND audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status} |
| @@ -26,7 +26,7 @@ | @@ -26,7 +26,7 @@ | ||
| 26 | <select id="selectReceivableActualByMonth" | 26 | <select id="selectReceivableActualByMonth" |
| 27 | resultMap="CrmStatisticsPerformanceTargetActualResultMap"> | 27 | resultMap="CrmStatisticsPerformanceTargetActualResultMap"> |
| 28 | SELECT MONTH(return_time) AS month, | 28 | SELECT MONTH(return_time) AS month, |
| 29 | - IFNULL(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 |
| 32 | AND audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status} | 32 | AND audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status} |
yudao-module-crm/src/main/resources/mapper/statistics/CrmStatisticsProductMapper.xml
| @@ -26,7 +26,7 @@ | @@ -26,7 +26,7 @@ | ||
| 26 | <select id="selectProductSalesList" | 26 | <select id="selectProductSalesList" |
| 27 | resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.product.CrmStatisticsProductSalesRespVO"> | 27 | resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.product.CrmStatisticsProductSalesRespVO"> |
| 28 | SELECT category.id AS category_id, | 28 | SELECT category.id AS category_id, |
| 29 | - IFNULL(category.name, '未分类') AS category_name, | 29 | + COALESCE(category.name, '未分类') AS category_name, |
| 30 | product.id AS product_id, | 30 | product.id AS product_id, |
| 31 | product.name AS product_name, | 31 | product.name AS product_name, |
| 32 | contract.id AS contract_id, | 32 | contract.id AS contract_id, |
| @@ -39,12 +39,12 @@ | @@ -39,12 +39,12 @@ | ||
| 39 | contract_product.product_price, | 39 | contract_product.product_price, |
| 40 | contract_product.count AS product_count, | 40 | contract_product.count AS product_count, |
| 41 | contract_product.total_price AS product_total_price | 41 | contract_product.total_price AS product_total_price |
| 42 | - FROM crm_contract_product AS contract_product | ||
| 43 | - INNER JOIN crm_contract AS contract ON contract_product.contract_id = contract.id | ||
| 44 | - INNER JOIN crm_product AS product ON contract_product.product_id = product.id | ||
| 45 | - LEFT JOIN crm_product_category AS category ON product.category_id = category.id AND category.deleted = 0 | ||
| 46 | - LEFT JOIN crm_customer AS customer ON contract.customer_id = customer.id AND customer.deleted = 0 | ||
| 47 | - LEFT JOIN system_users AS owner ON contract.owner_user_id = owner.id AND owner.deleted = 0 | 42 | + FROM crm_contract_product contract_product |
| 43 | + INNER JOIN crm_contract contract ON contract_product.contract_id = contract.id | ||
| 44 | + INNER JOIN crm_product product ON contract_product.product_id = product.id | ||
| 45 | + LEFT JOIN crm_product_category category ON product.category_id = category.id AND category.deleted = 0 | ||
| 46 | + LEFT JOIN crm_customer customer ON contract.customer_id = customer.id AND customer.deleted = 0 | ||
| 47 | + LEFT JOIN system_users owner ON contract.owner_user_id = owner.id AND owner.deleted = 0 | ||
| 48 | WHERE <include refid="ProductStatisticsWhere"/> | 48 | WHERE <include refid="ProductStatisticsWhere"/> |
| 49 | ORDER BY category.id ASC, product.id ASC, contract.order_date DESC | 49 | ORDER BY category.id ASC, product.id ASC, contract.order_date DESC |
| 50 | </select> | 50 | </select> |
| @@ -52,14 +52,14 @@ | @@ -52,14 +52,14 @@ | ||
| 52 | <select id="selectProductCategorySummary" | 52 | <select id="selectProductCategorySummary" |
| 53 | resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.product.CrmStatisticsProductCategoryRespVO"> | 53 | resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.product.CrmStatisticsProductCategoryRespVO"> |
| 54 | SELECT category.id AS category_id, | 54 | SELECT category.id AS category_id, |
| 55 | - IFNULL(category.name, '未分类') AS category_name, | 55 | + COALESCE(category.name, '未分类') AS category_name, |
| 56 | COUNT(DISTINCT contract.id) AS contract_count, | 56 | COUNT(DISTINCT contract.id) AS contract_count, |
| 57 | - IFNULL(SUM(contract_product.count), 0) AS product_count, | ||
| 58 | - IFNULL(SUM(contract_product.total_price), 0) AS product_total_price | ||
| 59 | - FROM crm_contract_product AS contract_product | ||
| 60 | - INNER JOIN crm_contract AS contract ON contract_product.contract_id = contract.id | ||
| 61 | - INNER JOIN crm_product AS product ON contract_product.product_id = product.id | ||
| 62 | - LEFT JOIN crm_product_category AS category ON product.category_id = category.id AND category.deleted = 0 | 57 | + COALESCE(SUM(contract_product.count), 0) AS product_count, |
| 58 | + COALESCE(SUM(contract_product.total_price), 0) AS product_total_price | ||
| 59 | + FROM crm_contract_product contract_product | ||
| 60 | + INNER JOIN crm_contract contract ON contract_product.contract_id = contract.id | ||
| 61 | + INNER JOIN crm_product product ON contract_product.product_id = product.id | ||
| 62 | + LEFT JOIN crm_product_category category ON product.category_id = category.id AND category.deleted = 0 | ||
| 63 | WHERE <include refid="ProductStatisticsWhere"/> | 63 | WHERE <include refid="ProductStatisticsWhere"/> |
| 64 | GROUP BY category.id, category.name | 64 | GROUP BY category.id, category.name |
| 65 | ORDER BY product_count DESC, product_total_price DESC | 65 | ORDER BY product_count DESC, product_total_price DESC |
yudao-module-crm/src/main/resources/mapper/statistics/CrmStatisticsRankMapper.xml
| @@ -4,7 +4,7 @@ | @@ -4,7 +4,7 @@ | ||
| 4 | 4 | ||
| 5 | <select id="selectContractPriceRank" | 5 | <select id="selectContractPriceRank" |
| 6 | resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.rank.CrmStatisticsRankRespVO"> | 6 | resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.rank.CrmStatisticsRankRespVO"> |
| 7 | - SELECT IFNULL(SUM(total_price), 0) AS count, owner_user_id | 7 | + SELECT COALESCE(SUM(total_price), 0) AS count, owner_user_id |
| 8 | FROM crm_contract | 8 | FROM crm_contract |
| 9 | WHERE deleted = 0 | 9 | WHERE deleted = 0 |
| 10 | AND audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status} | 10 | AND audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status} |
| @@ -18,7 +18,7 @@ | @@ -18,7 +18,7 @@ | ||
| 18 | 18 | ||
| 19 | <select id="selectReceivablePriceRank" | 19 | <select id="selectReceivablePriceRank" |
| 20 | resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.rank.CrmStatisticsRankRespVO"> | 20 | resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.rank.CrmStatisticsRankRespVO"> |
| 21 | - SELECT IFNULL(SUM(price), 0) AS count, owner_user_id | 21 | + SELECT COALESCE(SUM(price), 0) AS count, owner_user_id |
| 22 | FROM crm_receivable | 22 | FROM crm_receivable |
| 23 | WHERE deleted = 0 | 23 | WHERE deleted = 0 |
| 24 | AND audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status} | 24 | AND audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status} |
| @@ -88,8 +88,8 @@ | @@ -88,8 +88,8 @@ | ||
| 88 | <select id="selectFollowCountRank" | 88 | <select id="selectFollowCountRank" |
| 89 | resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.rank.CrmStatisticsRankRespVO"> | 89 | resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.rank.CrmStatisticsRankRespVO"> |
| 90 | SELECT COUNT(1) AS count, cc.owner_user_id | 90 | SELECT COUNT(1) AS count, cc.owner_user_id |
| 91 | - FROM crm_follow_up_record AS cfur | ||
| 92 | - LEFT JOIN crm_contact AS cc ON FIND_IN_SET(cc.id, cfur.contact_ids) | 91 | + FROM crm_follow_up_record cfur |
| 92 | + LEFT JOIN crm_contact cc ON FIND_IN_SET(cc.id, cfur.contact_ids) | ||
| 93 | WHERE cfur.deleted = 0 | 93 | WHERE cfur.deleted = 0 |
| 94 | AND cc.deleted = 0 | 94 | AND cc.deleted = 0 |
| 95 | AND cc.owner_user_id in | 95 | AND cc.owner_user_id in |
| @@ -103,8 +103,8 @@ | @@ -103,8 +103,8 @@ | ||
| 103 | <select id="selectFollowCustomerCountRank" | 103 | <select id="selectFollowCustomerCountRank" |
| 104 | resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.rank.CrmStatisticsRankRespVO"> | 104 | resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.rank.CrmStatisticsRankRespVO"> |
| 105 | SELECT COUNT(DISTINCT cc.id) AS count, cc.owner_user_id | 105 | SELECT COUNT(DISTINCT cc.id) AS count, cc.owner_user_id |
| 106 | - FROM crm_follow_up_record AS cfur | ||
| 107 | - LEFT JOIN crm_contact AS cc ON FIND_IN_SET(cc.id, cfur.contact_ids) | 106 | + FROM crm_follow_up_record cfur |
| 107 | + LEFT JOIN crm_contact cc ON FIND_IN_SET(cc.id, cfur.contact_ids) | ||
| 108 | WHERE cfur.deleted = 0 | 108 | WHERE cfur.deleted = 0 |
| 109 | AND cc.deleted = 0 | 109 | AND cc.deleted = 0 |
| 110 | AND cc.owner_user_id in | 110 | AND cc.owner_user_id in |
yudao-module-erp/src/main/resources/mapper/statistics/ErpPurchaseStatisticsMapper.xml
| @@ -4,7 +4,7 @@ | @@ -4,7 +4,7 @@ | ||
| 4 | 4 | ||
| 5 | <select id="getPurchasePrice" resultType="java.math.BigDecimal"> | 5 | <select id="getPurchasePrice" resultType="java.math.BigDecimal"> |
| 6 | SELECT | 6 | SELECT |
| 7 | - (SELECT IFNULL(SUM(total_price), 0) | 7 | + (SELECT COALESCE(SUM(total_price), 0) |
| 8 | FROM erp_purchase_in | 8 | FROM erp_purchase_in |
| 9 | WHERE in_time >= #{beginTime} | 9 | WHERE in_time >= #{beginTime} |
| 10 | <if test="endTime != null"> | 10 | <if test="endTime != null"> |
| @@ -15,7 +15,7 @@ | @@ -15,7 +15,7 @@ | ||
| 15 | </if> | 15 | </if> |
| 16 | AND deleted = 0 | 16 | AND deleted = 0 |
| 17 | AND status = 20) - | 17 | AND status = 20) - |
| 18 | - (SELECT IFNULL(SUM(total_price), 0) | 18 | + (SELECT COALESCE(SUM(total_price), 0) |
| 19 | FROM erp_purchase_return | 19 | FROM erp_purchase_return |
| 20 | WHERE return_time >= #{beginTime} | 20 | WHERE return_time >= #{beginTime} |
| 21 | <if test="endTime != null"> | 21 | <if test="endTime != null"> |
yudao-module-erp/src/main/resources/mapper/statistics/ErpSaleStatisticsMapper.xml
| @@ -4,7 +4,7 @@ | @@ -4,7 +4,7 @@ | ||
| 4 | 4 | ||
| 5 | <select id="getSalePrice" resultType="java.math.BigDecimal"> | 5 | <select id="getSalePrice" resultType="java.math.BigDecimal"> |
| 6 | SELECT | 6 | SELECT |
| 7 | - (SELECT IFNULL(SUM(total_price), 0) | 7 | + (SELECT COALESCE(SUM(total_price), 0) |
| 8 | FROM erp_sale_out | 8 | FROM erp_sale_out |
| 9 | WHERE out_time >= #{beginTime} | 9 | WHERE out_time >= #{beginTime} |
| 10 | <if test="endTime != null"> | 10 | <if test="endTime != null"> |
| @@ -15,7 +15,7 @@ | @@ -15,7 +15,7 @@ | ||
| 15 | </if> | 15 | </if> |
| 16 | AND deleted = 0 | 16 | AND deleted = 0 |
| 17 | AND status = 20) - | 17 | AND status = 20) - |
| 18 | - (SELECT IFNULL(SUM(total_price), 0) | 18 | + (SELECT COALESCE(SUM(total_price), 0) |
| 19 | FROM erp_sale_return | 19 | FROM erp_sale_return |
| 20 | WHERE return_time >= #{beginTime} | 20 | WHERE return_time >= #{beginTime} |
| 21 | <if test="endTime != null"> | 21 | <if test="endTime != null"> |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/statistics/ImStatisticsManagerMapper.java
| @@ -56,9 +56,9 @@ public interface ImStatisticsManagerMapper { | @@ -56,9 +56,9 @@ public interface ImStatisticsManagerMapper { | ||
| 56 | /** | 56 | /** |
| 57 | * 区间内每日新增用户数(按天分组) | 57 | * 区间内每日新增用户数(按天分组) |
| 58 | * | 58 | * |
| 59 | - * @return [{date: "yyyy-MM-dd", count: 123}, ...] | 59 | + * @return [{statDate: "yyyy-MM-dd", count: 123}, ...] |
| 60 | */ | 60 | */ |
| 61 | - @Select("SELECT DATE(create_time) AS date, COUNT(*) AS count FROM system_users " + | 61 | + @Select("SELECT 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 DATE(create_time)") |
| 64 | @ResultMap(CaseInsensitiveResultMap.RESULT_MAP_ID) | 64 | @ResultMap(CaseInsensitiveResultMap.RESULT_MAP_ID) |
| @@ -68,7 +68,7 @@ public interface ImStatisticsManagerMapper { | @@ -68,7 +68,7 @@ public interface ImStatisticsManagerMapper { | ||
| 68 | /** | 68 | /** |
| 69 | * 区间内每日活跃用户数(按天分组、跨私聊+群聊去重) | 69 | * 区间内每日活跃用户数(按天分组、跨私聊+群聊去重) |
| 70 | */ | 70 | */ |
| 71 | - @Select("SELECT day AS date, 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 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}" + |
| @@ -98,23 +98,25 @@ public interface ImStatisticsManagerMapper { | @@ -98,23 +98,25 @@ public interface ImStatisticsManagerMapper { | ||
| 98 | /** | 98 | /** |
| 99 | * 群规模分布(按群成员数分桶) | 99 | * 群规模分布(按群成员数分桶) |
| 100 | * | 100 | * |
| 101 | - * @return [{range: "1-9 人", count: 123}, ...] | 101 | + * @return [{sizeRange: "1-9 人", count: 123}, ...] |
| 102 | */ | 102 | */ |
| 103 | - @Select("SELECT " + | ||
| 104 | - " CASE " + | ||
| 105 | - " WHEN cnt < 10 THEN '1-9 人' " + | ||
| 106 | - " WHEN cnt < 50 THEN '10-49 人' " + | ||
| 107 | - " WHEN cnt < 200 THEN '50-199 人' " + | ||
| 108 | - " ELSE '200+ 人' " + | ||
| 109 | - " END AS `range`, COUNT(*) AS count " + | ||
| 110 | - "FROM (" + | ||
| 111 | - " SELECT g.id, COUNT(m.id) AS cnt " + | ||
| 112 | - " FROM im_group g " + | ||
| 113 | - " LEFT JOIN im_group_member m ON m.group_id = g.id AND m.status = 0 AND m.deleted = 0 " + | ||
| 114 | - " WHERE g.deleted = 0 AND g.status = 0 " + | ||
| 115 | - " GROUP BY g.id" + | ||
| 116 | - ") t " + | ||
| 117 | - "GROUP BY `range`") | 103 | + @Select("SELECT sizeRange, COUNT(*) AS count FROM (" + |
| 104 | + " SELECT " + | ||
| 105 | + " CASE " + | ||
| 106 | + " WHEN cnt < 10 THEN '1-9 人' " + | ||
| 107 | + " WHEN cnt < 50 THEN '10-49 人' " + | ||
| 108 | + " WHEN cnt < 200 THEN '50-199 人' " + | ||
| 109 | + " ELSE '200+ 人' " + | ||
| 110 | + " END AS sizeRange " + | ||
| 111 | + " FROM (" + | ||
| 112 | + " SELECT g.id, COUNT(m.id) AS cnt " + | ||
| 113 | + " FROM im_group g " + | ||
| 114 | + " LEFT JOIN im_group_member m ON m.group_id = g.id AND m.status = 0 AND m.deleted = 0 " + | ||
| 115 | + " WHERE g.deleted = 0 AND g.status = 0 " + | ||
| 116 | + " GROUP BY g.id" + | ||
| 117 | + " ) t0" + | ||
| 118 | + ") t1 " + | ||
| 119 | + "GROUP BY sizeRange") | ||
| 118 | @ResultMap(CaseInsensitiveResultMap.RESULT_MAP_ID) | 120 | @ResultMap(CaseInsensitiveResultMap.RESULT_MAP_ID) |
| 119 | List<Map<String, Object>> selectGroupSizeDistribution(); | 121 | List<Map<String, Object>> selectGroupSizeDistribution(); |
| 120 | 122 | ||
| @@ -141,7 +143,7 @@ public interface ImStatisticsManagerMapper { | @@ -141,7 +143,7 @@ public interface ImStatisticsManagerMapper { | ||
| 141 | /** | 143 | /** |
| 142 | * 区间内每日私聊消息数(按天分组) | 144 | * 区间内每日私聊消息数(按天分组) |
| 143 | */ | 145 | */ |
| 144 | - @Select("SELECT DATE(send_time) AS date, COUNT(*) AS count FROM im_private_message " + | 146 | + @Select("SELECT DATE(send_time) AS statDate, COUNT(*) AS count FROM im_private_message " + |
| 145 | "WHERE deleted = 0 AND " + NORMAL_MESSAGE_CONDITION + | 147 | "WHERE deleted = 0 AND " + NORMAL_MESSAGE_CONDITION + |
| 146 | " AND send_time >= #{beginTime} AND send_time < #{endTime} " + | 148 | " AND send_time >= #{beginTime} AND send_time < #{endTime} " + |
| 147 | "GROUP BY DATE(send_time)") | 149 | "GROUP BY DATE(send_time)") |
| @@ -152,7 +154,7 @@ public interface ImStatisticsManagerMapper { | @@ -152,7 +154,7 @@ public interface ImStatisticsManagerMapper { | ||
| 152 | /** | 154 | /** |
| 153 | * 区间内每日群聊消息数(按天分组) | 155 | * 区间内每日群聊消息数(按天分组) |
| 154 | */ | 156 | */ |
| 155 | - @Select("SELECT DATE(send_time) AS date, COUNT(*) AS count FROM im_group_message " + | 157 | + @Select("SELECT DATE(send_time) AS statDate, COUNT(*) AS count FROM im_group_message " + |
| 156 | "WHERE deleted = 0 AND " + NORMAL_MESSAGE_CONDITION + | 158 | "WHERE deleted = 0 AND " + NORMAL_MESSAGE_CONDITION + |
| 157 | " AND send_time >= #{beginTime} AND send_time < #{endTime} " + | 159 | " AND send_time >= #{beginTime} AND send_time < #{endTime} " + |
| 158 | "GROUP BY DATE(send_time)") | 160 | "GROUP BY DATE(send_time)") |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/statistics/ImStatisticsManagerServiceImpl.java
| @@ -70,7 +70,7 @@ public class ImStatisticsManagerServiceImpl implements ImStatisticsManagerServic | @@ -70,7 +70,7 @@ public class ImStatisticsManagerServiceImpl implements ImStatisticsManagerServic | ||
| 70 | public Map<String, Long> getGroupSizeCountMap() { | 70 | public Map<String, Long> getGroupSizeCountMap() { |
| 71 | List<Map<String, Object>> rows = statisticsMapper.selectGroupSizeDistribution(); | 71 | List<Map<String, Object>> rows = statisticsMapper.selectGroupSizeDistribution(); |
| 72 | return convertMap(rows, | 72 | return convertMap(rows, |
| 73 | - row -> (String) row.get("range"), | 73 | + row -> (String) row.get("sizeRange"), |
| 74 | row -> Convert.toLong(row.get("count"))); | 74 | row -> Convert.toLong(row.get("count"))); |
| 75 | } | 75 | } |
| 76 | 76 | ||
| @@ -115,11 +115,11 @@ public class ImStatisticsManagerServiceImpl implements ImStatisticsManagerServic | @@ -115,11 +115,11 @@ public class ImStatisticsManagerServiceImpl implements ImStatisticsManagerServic | ||
| 115 | } | 115 | } |
| 116 | 116 | ||
| 117 | /** | 117 | /** |
| 118 | - * 把 [{date, count}] 行映射为 {LocalDateTime -> Long}; | 118 | + * 把 [{statDate, count}] 行映射为 {LocalDateTime -> Long}; |
| 119 | */ | 119 | */ |
| 120 | private static Map<LocalDateTime, Long> toDailyCountMap(List<Map<String, Object>> rows) { | 120 | private static Map<LocalDateTime, Long> toDailyCountMap(List<Map<String, Object>> rows) { |
| 121 | return convertMap(rows, | 121 | return convertMap(rows, |
| 122 | - row -> Convert.convert(LocalDate.class, row.get("date")).atStartOfDay(), | 122 | + row -> Convert.convert(LocalDate.class, row.get("statDate")).atStartOfDay(), |
| 123 | row -> Convert.toLong(row.get("count"))); | 123 | row -> Convert.toLong(row.get("count"))); |
| 124 | } | 124 | } |
| 125 | 125 |
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/dal/mysql/statistics/ImStatisticsManagerMapperTest.java
| @@ -164,7 +164,7 @@ public class ImStatisticsManagerMapperTest extends BaseDbUnitTest { | @@ -164,7 +164,7 @@ public class ImStatisticsManagerMapperTest extends BaseDbUnitTest { | ||
| 164 | 164 | ||
| 165 | // 断言:分桶包含「1-9 人」和「10-49 人」 | 165 | // 断言:分桶包含「1-9 人」和「10-49 人」 |
| 166 | Map<Object, Long> byRange = dist.stream().collect(java.util.stream.Collectors.toMap( | 166 | Map<Object, Long> byRange = dist.stream().collect(java.util.stream.Collectors.toMap( |
| 167 | - m -> m.get("range"), m -> ((Number) m.get("count")).longValue())); | 167 | + m -> m.get("sizeRange"), m -> ((Number) m.get("count")).longValue())); |
| 168 | assertEquals(1L, byRange.get("1-9 人")); | 168 | assertEquals(1L, byRange.get("1-9 人")); |
| 169 | assertEquals(1L, byRange.get("10-49 人")); | 169 | assertEquals(1L, byRange.get("10-49 人")); |
| 170 | } | 170 | } |
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/statistics/ImStatisticsManagerServiceImplTest.java
| @@ -76,15 +76,15 @@ public class ImStatisticsManagerServiceImplTest extends BaseMockitoUnitTest { | @@ -76,15 +76,15 @@ public class ImStatisticsManagerServiceImplTest extends BaseMockitoUnitTest { | ||
| 76 | 76 | ||
| 77 | @Test | 77 | @Test |
| 78 | public void testGetNewUserDailyCountMap_dateConvert() { | 78 | public void testGetNewUserDailyCountMap_dateConvert() { |
| 79 | - // 准备:date 字段以 SQL Date / String 混入,count 混入多种 Number | 79 | + // 准备:statDate 字段以 SQL Date / String 混入,count 混入多种 Number |
| 80 | when(statisticsMapper.selectNewUserDailyCount(eq(BEGIN), eq(END))).thenReturn(Arrays.asList( | 80 | when(statisticsMapper.selectNewUserDailyCount(eq(BEGIN), eq(END))).thenReturn(Arrays.asList( |
| 81 | - row("date", java.sql.Date.valueOf("2026-01-10"), "count", BigInteger.valueOf(5)), | ||
| 82 | - row("date", "2026-01-11", "count", 8))); | 81 | + row("statDate", java.sql.Date.valueOf("2026-01-10"), "count", BigInteger.valueOf(5)), |
| 82 | + row("statDate", "2026-01-11", "count", 8))); | ||
| 83 | 83 | ||
| 84 | // 调用 | 84 | // 调用 |
| 85 | Map<LocalDateTime, Long> result = service.getNewUserDailyCountMap(BEGIN, END); | 85 | Map<LocalDateTime, Long> result = service.getNewUserDailyCountMap(BEGIN, END); |
| 86 | 86 | ||
| 87 | - // 断言:date → LocalDateTime 起始零点 | 87 | + // 断言:statDate → LocalDateTime 起始零点 |
| 88 | assertEquals(2, result.size()); | 88 | assertEquals(2, result.size()); |
| 89 | assertEquals(5L, result.get(LocalDate.of(2026, 1, 10).atStartOfDay())); | 89 | assertEquals(5L, result.get(LocalDate.of(2026, 1, 10).atStartOfDay())); |
| 90 | assertEquals(8L, result.get(LocalDate.of(2026, 1, 11).atStartOfDay())); | 90 | assertEquals(8L, result.get(LocalDate.of(2026, 1, 11).atStartOfDay())); |
| @@ -93,7 +93,7 @@ public class ImStatisticsManagerServiceImplTest extends BaseMockitoUnitTest { | @@ -93,7 +93,7 @@ public class ImStatisticsManagerServiceImplTest extends BaseMockitoUnitTest { | ||
| 93 | @Test | 93 | @Test |
| 94 | public void testGetActiveUserDailyCountMap_dateConvert() { | 94 | public void testGetActiveUserDailyCountMap_dateConvert() { |
| 95 | when(statisticsMapper.selectActiveUserDailyCount(any(), any())).thenReturn(Arrays.asList( | 95 | when(statisticsMapper.selectActiveUserDailyCount(any(), any())).thenReturn(Arrays.asList( |
| 96 | - row("date", "2026-01-10", "count", 3L))); | 96 | + row("statDate", "2026-01-10", "count", 3L))); |
| 97 | 97 | ||
| 98 | Map<LocalDateTime, Long> result = service.getActiveUserDailyCountMap(BEGIN, END); | 98 | Map<LocalDateTime, Long> result = service.getActiveUserDailyCountMap(BEGIN, END); |
| 99 | assertEquals(3L, result.get(LocalDate.of(2026, 1, 10).atStartOfDay())); | 99 | assertEquals(3L, result.get(LocalDate.of(2026, 1, 10).atStartOfDay())); |
| @@ -102,7 +102,7 @@ public class ImStatisticsManagerServiceImplTest extends BaseMockitoUnitTest { | @@ -102,7 +102,7 @@ public class ImStatisticsManagerServiceImplTest extends BaseMockitoUnitTest { | ||
| 102 | @Test | 102 | @Test |
| 103 | public void testGetPrivateMessageDailyCountMap_dateConvert() { | 103 | public void testGetPrivateMessageDailyCountMap_dateConvert() { |
| 104 | when(statisticsMapper.selectPrivateMessageDailyCount(any(), any())).thenReturn(Arrays.asList( | 104 | when(statisticsMapper.selectPrivateMessageDailyCount(any(), any())).thenReturn(Arrays.asList( |
| 105 | - row("date", "2026-01-10", "count", 7L))); | 105 | + row("statDate", "2026-01-10", "count", 7L))); |
| 106 | assertEquals(7L, service.getPrivateMessageDailyCountMap(BEGIN, END) | 106 | assertEquals(7L, service.getPrivateMessageDailyCountMap(BEGIN, END) |
| 107 | .get(LocalDate.of(2026, 1, 10).atStartOfDay())); | 107 | .get(LocalDate.of(2026, 1, 10).atStartOfDay())); |
| 108 | } | 108 | } |
| @@ -110,7 +110,7 @@ public class ImStatisticsManagerServiceImplTest extends BaseMockitoUnitTest { | @@ -110,7 +110,7 @@ public class ImStatisticsManagerServiceImplTest extends BaseMockitoUnitTest { | ||
| 110 | @Test | 110 | @Test |
| 111 | public void testGetGroupMessageDailyCountMap_dateConvert() { | 111 | public void testGetGroupMessageDailyCountMap_dateConvert() { |
| 112 | when(statisticsMapper.selectGroupMessageDailyCount(any(), any())).thenReturn(Arrays.asList( | 112 | when(statisticsMapper.selectGroupMessageDailyCount(any(), any())).thenReturn(Arrays.asList( |
| 113 | - row("date", "2026-01-10", "count", 9L))); | 113 | + row("statDate", "2026-01-10", "count", 9L))); |
| 114 | assertEquals(9L, service.getGroupMessageDailyCountMap(BEGIN, END) | 114 | assertEquals(9L, service.getGroupMessageDailyCountMap(BEGIN, END) |
| 115 | .get(LocalDate.of(2026, 1, 10).atStartOfDay())); | 115 | .get(LocalDate.of(2026, 1, 10).atStartOfDay())); |
| 116 | } | 116 | } |
| @@ -120,8 +120,8 @@ public class ImStatisticsManagerServiceImplTest extends BaseMockitoUnitTest { | @@ -120,8 +120,8 @@ public class ImStatisticsManagerServiceImplTest extends BaseMockitoUnitTest { | ||
| 120 | @Test | 120 | @Test |
| 121 | public void testGetGroupSizeCountMap() { | 121 | public void testGetGroupSizeCountMap() { |
| 122 | when(statisticsMapper.selectGroupSizeDistribution()).thenReturn(Arrays.asList( | 122 | when(statisticsMapper.selectGroupSizeDistribution()).thenReturn(Arrays.asList( |
| 123 | - row("range", "1-9 人", "count", BigInteger.valueOf(3)), | ||
| 124 | - row("range", "10-49 人", "count", 2))); | 123 | + row("sizeRange", "1-9 人", "count", BigInteger.valueOf(3)), |
| 124 | + row("sizeRange", "10-49 人", "count", 2))); | ||
| 125 | 125 | ||
| 126 | Map<String, Long> result = service.getGroupSizeCountMap(); | 126 | Map<String, Long> result = service.getGroupSizeCountMap(); |
| 127 | assertEquals(3L, result.get("1-9 人")); | 127 | assertEquals(3L, result.get("1-9 人")); |
yudao-module-mall/yudao-module-statistics/src/main/resources/mapper/member/MemberStatisticsMapper.xml
| @@ -50,9 +50,14 @@ | @@ -50,9 +50,14 @@ | ||
| 50 | </if> | 50 | </if> |
| 51 | </select> | 51 | </select> |
| 52 | 52 | ||
| 53 | - <select id="selectListByCreateTimeBetween" | ||
| 54 | - resultType="cn.iocoder.yudao.module.statistics.controller.admin.member.vo.MemberRegisterCountRespVO"> | ||
| 55 | - SELECT CAST(create_time AS DATE) AS date, | 53 | + <resultMap id="MemberRegisterCountRespVOMap" |
| 54 | + type="cn.iocoder.yudao.module.statistics.controller.admin.member.vo.MemberRegisterCountRespVO"> | ||
| 55 | + <result column="statDate" property="date"/> | ||
| 56 | + <result column="count" property="count"/> | ||
| 57 | + </resultMap> | ||
| 58 | + | ||
| 59 | + <select id="selectListByCreateTimeBetween" resultMap="MemberRegisterCountRespVOMap"> | ||
| 60 | + SELECT CAST(create_time AS DATE) AS statDate, | ||
| 56 | count(1) AS count | 61 | count(1) AS count |
| 57 | FROM member_user | 62 | FROM member_user |
| 58 | WHERE create_time BETWEEN #{beginTime} AND #{endTime} | 63 | WHERE create_time BETWEEN #{beginTime} AND #{endTime} |
| @@ -60,8 +65,8 @@ | @@ -60,8 +65,8 @@ | ||
| 60 | <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"> |
| 61 | 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()} |
| 62 | </if> | 67 | </if> |
| 63 | - GROUP BY date | ||
| 64 | - ORDER BY date | 68 | + GROUP BY CAST(create_time AS DATE) |
| 69 | + ORDER BY statDate | ||
| 65 | </select> | 70 | </select> |
| 66 | 71 | ||
| 67 | </mapper> | 72 | </mapper> |
yudao-module-mall/yudao-module-statistics/src/main/resources/mapper/trade/TradeOrderStatisticsMapper.xml
| @@ -6,14 +6,14 @@ | @@ -6,14 +6,14 @@ | ||
| 6 | resultType="cn.iocoder.yudao.module.statistics.service.member.bo.MemberAreaStatisticsRespBO"> | 6 | resultType="cn.iocoder.yudao.module.statistics.service.member.bo.MemberAreaStatisticsRespBO"> |
| 7 | SELECT receiver_area_id AS areaId, | 7 | SELECT receiver_area_id AS areaId, |
| 8 | (SELECT COUNT(DISTINCT s.user_id) | 8 | (SELECT COUNT(DISTINCT s.user_id) |
| 9 | - FROM trade_order AS s | 9 | + FROM trade_order s |
| 10 | WHERE s.receiver_area_id = m.receiver_area_id | 10 | WHERE s.receiver_area_id = m.receiver_area_id |
| 11 | <if test="@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId() != null"> | 11 | <if test="@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId() != null"> |
| 12 | AND s.tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId()} | 12 | AND s.tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId()} |
| 13 | </if> | 13 | </if> |
| 14 | ) AS orderCreateUserCount, | 14 | ) AS orderCreateUserCount, |
| 15 | (SELECT COUNT(DISTINCT s.user_id) | 15 | (SELECT COUNT(DISTINCT s.user_id) |
| 16 | - FROM trade_order AS s | 16 | + FROM trade_order s |
| 17 | WHERE s.receiver_area_id = m.receiver_area_id | 17 | WHERE s.receiver_area_id = m.receiver_area_id |
| 18 | AND s.pay_status = TRUE | 18 | AND s.pay_status = TRUE |
| 19 | AND s.deleted = 0 | 19 | AND s.deleted = 0 |
| @@ -22,7 +22,7 @@ | @@ -22,7 +22,7 @@ | ||
| 22 | </if> | 22 | </if> |
| 23 | ) AS orderPayUserCount, | 23 | ) AS orderPayUserCount, |
| 24 | (SELECT SUM(s.pay_price) | 24 | (SELECT SUM(s.pay_price) |
| 25 | - FROM trade_order AS s | 25 | + FROM trade_order s |
| 26 | WHERE s.receiver_area_id = m.receiver_area_id | 26 | WHERE s.receiver_area_id = m.receiver_area_id |
| 27 | AND s.pay_status = TRUE | 27 | AND s.pay_status = TRUE |
| 28 | AND s.deleted = 0 | 28 | AND s.deleted = 0 |
| @@ -82,7 +82,7 @@ | @@ -82,7 +82,7 @@ | ||
| 82 | 82 | ||
| 83 | <select id="selectSummaryPriceByPayTimeBetween" resultType="java.lang.Integer"> | 83 | <select id="selectSummaryPriceByPayTimeBetween" resultType="java.lang.Integer"> |
| 84 | SELECT SUM(pay_price) | 84 | SELECT SUM(pay_price) |
| 85 | - FROM trade_order AS s | 85 | + FROM trade_order s |
| 86 | WHERE s.pay_status = TRUE | 86 | WHERE s.pay_status = TRUE |
| 87 | AND deleted = 0 | 87 | AND deleted = 0 |
| 88 | AND create_time BETWEEN #{beginTime} AND #{endTime} | 88 | AND create_time BETWEEN #{beginTime} AND #{endTime} |
| @@ -91,11 +91,17 @@ | @@ -91,11 +91,17 @@ | ||
| 91 | </if> | 91 | </if> |
| 92 | </select> | 92 | </select> |
| 93 | 93 | ||
| 94 | - <select id="selectListByPayTimeBetweenAndGroupByDay" | ||
| 95 | - resultType="cn.iocoder.yudao.module.statistics.controller.admin.trade.vo.TradeOrderTrendRespVO"> | ||
| 96 | - SELECT CAST(pay_time AS DATE) AS date, | ||
| 97 | - COUNT(1) AS orderPayCount, | ||
| 98 | - SUM(pay_price) AS orderPayPrice | 94 | + <resultMap id="TradeOrderTrendRespVOMap" |
| 95 | + type="cn.iocoder.yudao.module.statistics.controller.admin.trade.vo.TradeOrderTrendRespVO"> | ||
| 96 | + <result column="statDate" property="date"/> | ||
| 97 | + <result column="order_pay_count" property="orderPayCount"/> | ||
| 98 | + <result column="order_pay_price" property="orderPayPrice"/> | ||
| 99 | + </resultMap> | ||
| 100 | + | ||
| 101 | + <select id="selectListByPayTimeBetweenAndGroupByDay" resultMap="TradeOrderTrendRespVOMap"> | ||
| 102 | + SELECT CAST(pay_time AS DATE) AS statDate, | ||
| 103 | + COUNT(1) AS order_pay_count, | ||
| 104 | + SUM(pay_price) AS order_pay_price | ||
| 99 | FROM trade_order | 105 | FROM trade_order |
| 100 | WHERE pay_status = TRUE | 106 | WHERE pay_status = TRUE |
| 101 | AND create_time BETWEEN #{beginTime} AND #{endTime} | 107 | AND create_time BETWEEN #{beginTime} AND #{endTime} |
| @@ -103,14 +109,13 @@ | @@ -103,14 +109,13 @@ | ||
| 103 | <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"> |
| 104 | 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()} |
| 105 | </if> | 111 | </if> |
| 106 | - GROUP BY date | ||
| 107 | - ORDER BY date | 112 | + GROUP BY CAST(pay_time AS DATE) |
| 113 | + ORDER BY statDate | ||
| 108 | </select> | 114 | </select> |
| 109 | 115 | ||
| 110 | - <select id="selectListByPayTimeBetweenAndGroupByMonth" | ||
| 111 | - resultType="cn.iocoder.yudao.module.statistics.controller.admin.trade.vo.TradeOrderTrendRespVO"> | 116 | + <select id="selectListByPayTimeBetweenAndGroupByMonth" resultMap="TradeOrderTrendRespVOMap"> |
| 112 | SELECT CONCAT(EXTRACT(YEAR FROM pay_time), '-', | 117 | SELECT CONCAT(EXTRACT(YEAR FROM pay_time), '-', |
| 113 | - LPAD(CONCAT(EXTRACT(MONTH FROM pay_time)), 2, '0')) AS date, | 118 | + LPAD(CONCAT(EXTRACT(MONTH FROM pay_time)), 2, '0')) AS statDate, |
| 114 | COUNT(1) AS order_pay_count, | 119 | COUNT(1) AS order_pay_count, |
| 115 | SUM(pay_price) AS order_pay_price | 120 | SUM(pay_price) AS order_pay_price |
| 116 | FROM trade_order | 121 | FROM trade_order |
| @@ -120,8 +125,9 @@ | @@ -120,8 +125,9 @@ | ||
| 120 | <if test="@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId() != null"> | 125 | <if test="@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId() != null"> |
| 121 | AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId()} | 126 | AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId()} |
| 122 | </if> | 127 | </if> |
| 123 | - GROUP BY date | ||
| 124 | - ORDER BY date | 128 | + GROUP BY CONCAT(EXTRACT(YEAR FROM pay_time), '-', |
| 129 | + LPAD(CONCAT(EXTRACT(MONTH FROM pay_time)), 2, '0')) | ||
| 130 | + ORDER BY statDate | ||
| 125 | </select> | 131 | </select> |
| 126 | 132 | ||
| 127 | <select id="selectCountByStatusAndDeliveryType" resultType="java.lang.Long"> | 133 | <select id="selectCountByStatusAndDeliveryType" resultType="java.lang.Long"> |
yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/brokerage/BrokerageRecordMapper.java
| @@ -80,7 +80,7 @@ public interface BrokerageRecordMapper extends BaseMapperX<BrokerageRecordDO> { | @@ -80,7 +80,7 @@ public interface BrokerageRecordMapper extends BaseMapperX<BrokerageRecordDO> { | ||
| 80 | 80 | ||
| 81 | @Select("SELECT SUM(price) FROM trade_brokerage_record " + | 81 | @Select("SELECT SUM(price) FROM trade_brokerage_record " + |
| 82 | "WHERE user_id = #{userId} AND biz_type = #{bizType} AND status = #{status} " + | 82 | "WHERE user_id = #{userId} AND biz_type = #{bizType} AND status = #{status} " + |
| 83 | - "AND unfreeze_time BETWEEN #{beginTime} AND #{endTime} AND deleted = FALSE") | 83 | + "AND unfreeze_time BETWEEN #{beginTime} AND #{endTime} AND deleted = 0") |
| 84 | Integer selectSummaryPriceByUserIdAndBizTypeAndCreateTimeBetween(@Param("userId") Long userId, | 84 | Integer selectSummaryPriceByUserIdAndBizTypeAndCreateTimeBetween(@Param("userId") Long userId, |
| 85 | @Param("bizType") Integer bizType, | 85 | @Param("bizType") Integer bizType, |
| 86 | @Param("status") Integer status, | 86 | @Param("status") Integer status, |
| @@ -89,7 +89,7 @@ public interface BrokerageRecordMapper extends BaseMapperX<BrokerageRecordDO> { | @@ -89,7 +89,7 @@ public interface BrokerageRecordMapper extends BaseMapperX<BrokerageRecordDO> { | ||
| 89 | 89 | ||
| 90 | // TODO @芋艿:收敛掉 @Select 注解操作,统一成 MyBatis-Plus 的方式,或者 xml | 90 | // TODO @芋艿:收敛掉 @Select 注解操作,统一成 MyBatis-Plus 的方式,或者 xml |
| 91 | @Select("SELECT user_id AS id, SUM(price) AS brokeragePrice FROM trade_brokerage_record " + | 91 | @Select("SELECT user_id AS id, SUM(price) AS brokeragePrice FROM trade_brokerage_record " + |
| 92 | - "WHERE biz_type = #{bizType} AND status = #{status} AND deleted = FALSE " + | 92 | + "WHERE biz_type = #{bizType} AND status = #{status} AND deleted = 0 " + |
| 93 | "AND unfreeze_time BETWEEN #{beginTime} AND #{endTime} " + | 93 | "AND unfreeze_time BETWEEN #{beginTime} AND #{endTime} " + |
| 94 | "GROUP BY user_id " + | 94 | "GROUP BY user_id " + |
| 95 | "ORDER BY brokeragePrice DESC") | 95 | "ORDER BY brokeragePrice DESC") |
| @@ -100,7 +100,7 @@ public interface BrokerageRecordMapper extends BaseMapperX<BrokerageRecordDO> { | @@ -100,7 +100,7 @@ public interface BrokerageRecordMapper extends BaseMapperX<BrokerageRecordDO> { | ||
| 100 | @Param("endTime") LocalDateTime endTime); | 100 | @Param("endTime") LocalDateTime endTime); |
| 101 | 101 | ||
| 102 | @Select("SELECT COUNT(1) FROM trade_brokerage_record " + | 102 | @Select("SELECT COUNT(1) FROM trade_brokerage_record " + |
| 103 | - "WHERE biz_type = #{bizType} AND status = #{status} AND deleted = FALSE " + | 103 | + "WHERE biz_type = #{bizType} AND status = #{status} AND deleted = 0 " + |
| 104 | "AND unfreeze_time BETWEEN #{beginTime} AND #{endTime} " + | 104 | "AND unfreeze_time BETWEEN #{beginTime} AND #{endTime} " + |
| 105 | "GROUP BY user_id HAVING SUM(price) > #{brokeragePrice}") | 105 | "GROUP BY user_id HAVING SUM(price) > #{brokeragePrice}") |
| 106 | Integer selectCountByPriceGt(@Param("brokeragePrice") Integer brokeragePrice, | 106 | Integer selectCountByPriceGt(@Param("brokeragePrice") Integer brokeragePrice, |
yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/brokerage/BrokerageUserMapper.java
| @@ -128,7 +128,7 @@ public interface BrokerageUserMapper extends BaseMapperX<BrokerageUserDO> { | @@ -128,7 +128,7 @@ public interface BrokerageUserMapper extends BaseMapperX<BrokerageUserDO> { | ||
| 128 | } | 128 | } |
| 129 | 129 | ||
| 130 | @Select("SELECT bind_user_id AS id, COUNT(1) AS brokerageUserCount FROM trade_brokerage_user " + | 130 | @Select("SELECT bind_user_id AS id, COUNT(1) AS brokerageUserCount FROM trade_brokerage_user " + |
| 131 | - "WHERE bind_user_id IS NOT NULL AND deleted = FALSE " + | 131 | + "WHERE bind_user_id IS NOT NULL AND deleted = 0 " + |
| 132 | "AND bind_user_time BETWEEN #{beginTime} AND #{endTime} " + | 132 | "AND bind_user_time BETWEEN #{beginTime} AND #{endTime} " + |
| 133 | "GROUP BY bind_user_id " + | 133 | "GROUP BY bind_user_id " + |
| 134 | "ORDER BY brokerageUserCount DESC") | 134 | "ORDER BY brokerageUserCount DESC") |
yudao-module-mall/yudao-module-trade/src/main/resources/mapper/brokerage/BrokerageUserMapper.xml
| @@ -22,7 +22,7 @@ | @@ -22,7 +22,7 @@ | ||
| 22 | WHERE r.user_id = bu.id AND biz_type = #{bizType} AND r.status = #{status} AND r.deleted = 0) AS brokerageOrderCount, | 22 | WHERE r.user_id = bu.id AND biz_type = #{bizType} AND r.status = #{status} AND r.deleted = 0) AS brokerageOrderCount, |
| 23 | (SELECT COUNT(1) FROM trade_brokerage_user c | 23 | (SELECT COUNT(1) FROM trade_brokerage_user c |
| 24 | WHERE c.bind_user_id = bu.id AND c.deleted = 0) AS brokerageUserCount | 24 | WHERE c.bind_user_id = bu.id AND c.deleted = 0) AS brokerageUserCount |
| 25 | - FROM trade_brokerage_user AS bu | 25 | + FROM trade_brokerage_user bu |
| 26 | <where> | 26 | <where> |
| 27 | bu.deleted = 0 | 27 | bu.deleted = 0 |
| 28 | <if test="ids != null and ids.size() > 0"> | 28 | <if test="ids != null and ids.size() > 0"> |
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/home/MesHomeStatisticsMapper.java
| @@ -59,7 +59,7 @@ public interface MesHomeStatisticsMapper { | @@ -59,7 +59,7 @@ public interface MesHomeStatisticsMapper { | ||
| 59 | * | 59 | * |
| 60 | * @param beginTime >= 开始时间 | 60 | * @param beginTime >= 开始时间 |
| 61 | * @param endTime < 结束时间 | 61 | * @param endTime < 结束时间 |
| 62 | - * @return [{ "date": "2026-04-05", "quantity": 1234, "qualifiedQuantity": 1200, "unqualifiedQuantity": 34 }, ...] | 62 | + * @return [{ "statDate": "2026-04-05", "quantity": 1234, "qualifiedQuantity": 1200, "unqualifiedQuantity": 34 }, ...] |
| 63 | */ | 63 | */ |
| 64 | List<Map<String, Object>> selectDailyFeedbackTrend(@Param("beginTime") LocalDateTime beginTime, | 64 | List<Map<String, Object>> selectDailyFeedbackTrend(@Param("beginTime") LocalDateTime beginTime, |
| 65 | @Param("endTime") LocalDateTime endTime); | 65 | @Param("endTime") LocalDateTime endTime); |
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/task/MesProTaskMapper.java
| @@ -62,9 +62,9 @@ public interface MesProTaskMapper extends BaseMapperX<MesProTaskDO> { | @@ -62,9 +62,9 @@ public interface MesProTaskMapper extends BaseMapperX<MesProTaskDO> { | ||
| 62 | BigDecimal incrUnqualifyQuantity) { | 62 | BigDecimal incrUnqualifyQuantity) { |
| 63 | update(null, new LambdaUpdateWrapper<MesProTaskDO>() | 63 | update(null, new LambdaUpdateWrapper<MesProTaskDO>() |
| 64 | .eq(MesProTaskDO::getId, id) | 64 | .eq(MesProTaskDO::getId, id) |
| 65 | - .setSql("produced_quantity = IFNULL(produced_quantity, 0) + " + incrProducedQuantity) | ||
| 66 | - .setSql("qualify_quantity = IFNULL(qualify_quantity, 0) + " + incrQualifyQuantity) | ||
| 67 | - .setSql("unqualify_quantity = IFNULL(unqualify_quantity, 0) + " + incrUnqualifyQuantity)); | 65 | + .setIncrBy(MesProTaskDO::getProducedQuantity, incrProducedQuantity) |
| 66 | + .setIncrBy(MesProTaskDO::getQualifyQuantity, incrQualifyQuantity) | ||
| 67 | + .setIncrBy(MesProTaskDO::getUnqualifyQuantity, incrUnqualifyQuantity)); | ||
| 68 | } | 68 | } |
| 69 | 69 | ||
| 70 | } | 70 | } |
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/workorder/MesProWorkOrderMapper.java
| @@ -38,7 +38,7 @@ public interface MesProWorkOrderMapper extends BaseMapperX<MesProWorkOrderDO> { | @@ -38,7 +38,7 @@ public interface MesProWorkOrderMapper extends BaseMapperX<MesProWorkOrderDO> { | ||
| 38 | default void updateProducedQuantity(Long id, BigDecimal incrQuantityProduced) { | 38 | default void updateProducedQuantity(Long id, BigDecimal incrQuantityProduced) { |
| 39 | update(null, new LambdaUpdateWrapper<MesProWorkOrderDO>() | 39 | update(null, new LambdaUpdateWrapper<MesProWorkOrderDO>() |
| 40 | .eq(MesProWorkOrderDO::getId, id) | 40 | .eq(MesProWorkOrderDO::getId, id) |
| 41 | - .setSql("quantity_produced = IFNULL(quantity_produced, 0) + " + incrQuantityProduced)); | 41 | + .setIncrBy(MesProWorkOrderDO::getQuantityProduced, incrQuantityProduced)); |
| 42 | } | 42 | } |
| 43 | 43 | ||
| 44 | default Long selectCountByVendorId(Long vendorId) { | 44 | default Long selectCountByVendorId(Long vendorId) { |
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/home/MesHomeStatisticsServiceImpl.java
| @@ -121,7 +121,7 @@ public class MesHomeStatisticsServiceImpl implements MesHomeStatisticsService { | @@ -121,7 +121,7 @@ public class MesHomeStatisticsServiceImpl implements MesHomeStatisticsService { | ||
| 121 | List<Map<String, Object>> dbData = homeStatisticsMapper.selectDailyFeedbackTrend(beginTime, endTime); | 121 | List<Map<String, Object>> dbData = homeStatisticsMapper.selectDailyFeedbackTrend(beginTime, endTime); |
| 122 | Map<String, Map<String, Object>> dateMap = new LinkedHashMap<>(); | 122 | Map<String, Map<String, Object>> dateMap = new LinkedHashMap<>(); |
| 123 | for (Map<String, Object> row : dbData) { | 123 | for (Map<String, Object> row : dbData) { |
| 124 | - dateMap.put((String) row.get("date"), row); | 124 | + dateMap.put((String) row.get("statDate"), row); |
| 125 | } | 125 | } |
| 126 | 126 | ||
| 127 | // 补全所有日期(无数据的日期填 0) | 127 | // 补全所有日期(无数据的日期填 0) |
yudao-module-mes/src/main/resources/mapper/home/MesHomeStatisticsMapper.xml
| @@ -12,9 +12,9 @@ | @@ -12,9 +12,9 @@ | ||
| 12 | 12 | ||
| 13 | <!-- 统计报工产量合计 --> | 13 | <!-- 统计报工产量合计 --> |
| 14 | <select id="selectFeedbackSummary" resultType="cn.iocoder.yudao.framework.mybatis.core.util.CaseInsensitiveResultMap"> | 14 | <select id="selectFeedbackSummary" resultType="cn.iocoder.yudao.framework.mybatis.core.util.CaseInsensitiveResultMap"> |
| 15 | - SELECT IFNULL(SUM(feedback_quantity), 0) AS feedbackQuantity, | ||
| 16 | - IFNULL(SUM(qualified_quantity), 0) AS qualifiedQuantity, | ||
| 17 | - IFNULL(SUM(unqualified_quantity), 0) AS unqualifiedQuantity | 15 | + SELECT COALESCE(SUM(feedback_quantity), 0) AS feedbackQuantity, |
| 16 | + COALESCE(SUM(qualified_quantity), 0) AS qualifiedQuantity, | ||
| 17 | + COALESCE(SUM(unqualified_quantity), 0) AS unqualifiedQuantity | ||
| 18 | FROM mes_pro_feedback | 18 | FROM mes_pro_feedback |
| 19 | WHERE deleted = 0 | 19 | WHERE deleted = 0 |
| 20 | AND status = ${@cn.iocoder.yudao.module.mes.enums.pro.MesProFeedbackStatusEnum@FINISHED.status} | 20 | AND status = ${@cn.iocoder.yudao.module.mes.enums.pro.MesProFeedbackStatusEnum@FINISHED.status} |
| @@ -52,10 +52,10 @@ | @@ -52,10 +52,10 @@ | ||
| 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 date, | ||
| 56 | - IFNULL(SUM(feedback_quantity), 0) AS quantity, | ||
| 57 | - IFNULL(SUM(qualified_quantity), 0) AS qualifiedQuantity, | ||
| 58 | - IFNULL(SUM(unqualified_quantity), 0) AS unqualifiedQuantity | 55 | + SELECT DATE_FORMAT(feedback_time, '%Y-%m-%d') AS statDate, |
| 56 | + COALESCE(SUM(feedback_quantity), 0) AS quantity, | ||
| 57 | + COALESCE(SUM(qualified_quantity), 0) AS qualifiedQuantity, | ||
| 58 | + COALESCE(SUM(unqualified_quantity), 0) AS unqualifiedQuantity | ||
| 59 | FROM mes_pro_feedback | 59 | FROM mes_pro_feedback |
| 60 | WHERE deleted = 0 | 60 | WHERE deleted = 0 |
| 61 | AND status = ${@cn.iocoder.yudao.module.mes.enums.pro.MesProFeedbackStatusEnum@FINISHED.status} | 61 | AND status = ${@cn.iocoder.yudao.module.mes.enums.pro.MesProFeedbackStatusEnum@FINISHED.status} |
| @@ -64,7 +64,7 @@ | @@ -64,7 +64,7 @@ | ||
| 64 | AND feedback_time < #{endTime} | 64 | AND feedback_time < #{endTime} |
| 65 | </if> | 65 | </if> |
| 66 | GROUP BY DATE_FORMAT(feedback_time, '%Y-%m-%d') | 66 | GROUP BY DATE_FORMAT(feedback_time, '%Y-%m-%d') |
| 67 | - ORDER BY date | 67 | + ORDER BY statDate |
| 68 | </select> | 68 | </select> |
| 69 | 69 | ||
| 70 | </mapper> | 70 | </mapper> |
yudao-module-mes/src/main/resources/mapper/qc/pendinginspect/MesQcPendingInspectMapper.xml
| @@ -10,6 +10,7 @@ | @@ -10,6 +10,7 @@ | ||
| 10 | 4. 销售出库单(OQC):单据状态=CONFIRMED(1),行的 oqc_check_flag=1 且 oqc_id 为空 | 10 | 4. 销售出库单(OQC):单据状态=CONFIRMED(1),行的 oqc_check_flag=1 且 oqc_id 为空 |
| 11 | 5. 生产退料单(RQC):单据状态=CONFIRMED(1),行的 rqc_check_flag=1 且 rqc_id 为空 且 quality_status=0(待检) | 11 | 5. 生产退料单(RQC):单据状态=CONFIRMED(1),行的 rqc_check_flag=1 且 rqc_id 为空 且 quality_status=0(待检) |
| 12 | 6. 销售退货单(RQC):单据状态=CONFIRMED(1),行的 rqc_check_flag=1 且 rqc_id 为空 且 quality_status=0(待检) | 12 | 6. 销售退货单(RQC):单据状态=CONFIRMED(1),行的 rqc_check_flag=1 且 rqc_id 为空 且 quality_status=0(待检) |
| 13 | + 注意:各分支无值的 ID 列使用 CASE WHEN 1 = 0 THEN 本分支 bigint 列 END 产生“带类型的 NULL”,避免 UNION 中裸 NULL 的类型推断差异 | ||
| 13 | --> | 14 | --> |
| 14 | <select id="selectQcPendingPage" resultType="cn.iocoder.yudao.module.mes.controller.admin.qc.pendinginspect.vo.MesQcPendingInspectRespVO"> | 15 | <select id="selectQcPendingPage" resultType="cn.iocoder.yudao.module.mes.controller.admin.qc.pendinginspect.vo.MesQcPendingInspectRespVO"> |
| 15 | SELECT * FROM ( | 16 | SELECT * FROM ( |
| @@ -24,10 +25,10 @@ | @@ -24,10 +25,10 @@ | ||
| 24 | l.item_id AS itemId, | 25 | l.item_id AS itemId, |
| 25 | l.arrival_quantity AS quantity, | 26 | l.arrival_quantity AS quantity, |
| 26 | n.vendor_id AS vendorId, | 27 | n.vendor_id AS vendorId, |
| 27 | - NULL AS workOrderId, | ||
| 28 | - NULL AS workstationId, | ||
| 29 | - NULL AS taskId, | ||
| 30 | - NULL AS clientId | 28 | + CASE WHEN 1 = 0 THEN n.id END AS workOrderId, |
| 29 | + CASE WHEN 1 = 0 THEN n.id END AS workstationId, | ||
| 30 | + CASE WHEN 1 = 0 THEN n.id END AS taskId, | ||
| 31 | + CASE WHEN 1 = 0 THEN n.id END AS clientId | ||
| 31 | FROM mes_wm_arrival_notice n | 32 | FROM mes_wm_arrival_notice n |
| 32 | JOIN mes_wm_arrival_notice_line l ON n.id = l.notice_id | 33 | JOIN mes_wm_arrival_notice_line l ON n.id = l.notice_id |
| 33 | WHERE n.status = ${@cn.iocoder.yudao.module.mes.enums.wm.MesWmArrivalNoticeStatusEnum@PENDING_QC.status} | 34 | WHERE n.status = ${@cn.iocoder.yudao.module.mes.enums.wm.MesWmArrivalNoticeStatusEnum@PENDING_QC.status} |
| @@ -49,10 +50,10 @@ | @@ -49,10 +50,10 @@ | ||
| 49 | rl.item_id AS itemId, | 50 | rl.item_id AS itemId, |
| 50 | rl.quantity AS quantity, | 51 | rl.quantity AS quantity, |
| 51 | r.vendor_id AS vendorId, | 52 | r.vendor_id AS vendorId, |
| 52 | - NULL AS workOrderId, | ||
| 53 | - NULL AS workstationId, | ||
| 54 | - NULL AS taskId, | ||
| 55 | - NULL AS clientId | 53 | + CASE WHEN 1 = 0 THEN r.id END AS workOrderId, |
| 54 | + CASE WHEN 1 = 0 THEN r.id END AS workstationId, | ||
| 55 | + CASE WHEN 1 = 0 THEN r.id END AS taskId, | ||
| 56 | + CASE WHEN 1 = 0 THEN r.id END AS clientId | ||
| 56 | FROM mes_wm_outsource_receipt r | 57 | FROM mes_wm_outsource_receipt r |
| 57 | JOIN mes_wm_outsource_receipt_line rl ON r.id = rl.receipt_id | 58 | JOIN mes_wm_outsource_receipt_line rl ON r.id = rl.receipt_id |
| 58 | WHERE r.status = ${@cn.iocoder.yudao.module.mes.enums.wm.MesWmOutsourceReceiptStatusEnum@CONFIRMED.status} | 59 | WHERE r.status = ${@cn.iocoder.yudao.module.mes.enums.wm.MesWmOutsourceReceiptStatusEnum@CONFIRMED.status} |
| @@ -73,11 +74,11 @@ | @@ -73,11 +74,11 @@ | ||
| 73 | ${@cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum@IPQC.type} AS qcType, | 74 | ${@cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum@IPQC.type} AS qcType, |
| 74 | ppl.item_id AS itemId, | 75 | ppl.item_id AS itemId, |
| 75 | ppl.quantity AS quantity, | 76 | ppl.quantity AS quantity, |
| 76 | - NULL AS vendorId, | 77 | + CASE WHEN 1 = 0 THEN pf.id END AS vendorId, |
| 77 | pp.work_order_id AS workOrderId, | 78 | pp.work_order_id AS workOrderId, |
| 78 | pp.workstation_id AS workstationId, | 79 | pp.workstation_id AS workstationId, |
| 79 | pp.task_id AS taskId, | 80 | pp.task_id AS taskId, |
| 80 | - NULL AS clientId | 81 | + CASE WHEN 1 = 0 THEN pf.id END AS clientId |
| 81 | FROM mes_wm_product_produce_line ppl | 82 | FROM mes_wm_product_produce_line ppl |
| 82 | JOIN mes_wm_product_produce pp ON ppl.produce_id = pp.id | 83 | JOIN mes_wm_product_produce pp ON ppl.produce_id = pp.id |
| 83 | JOIN mes_pro_feedback pf ON pp.feedback_id = pf.id | 84 | JOIN mes_pro_feedback pf ON pp.feedback_id = pf.id |
| @@ -98,10 +99,10 @@ | @@ -98,10 +99,10 @@ | ||
| 98 | ${@cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum@OQC.type} AS qcType, | 99 | ${@cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum@OQC.type} AS qcType, |
| 99 | sl.item_id AS itemId, | 100 | sl.item_id AS itemId, |
| 100 | sl.quantity AS quantity, | 101 | sl.quantity AS quantity, |
| 101 | - NULL AS vendorId, | ||
| 102 | - NULL AS workOrderId, | ||
| 103 | - NULL AS workstationId, | ||
| 104 | - NULL AS taskId, | 102 | + CASE WHEN 1 = 0 THEN s.id END AS vendorId, |
| 103 | + CASE WHEN 1 = 0 THEN s.id END AS workOrderId, | ||
| 104 | + CASE WHEN 1 = 0 THEN s.id END AS workstationId, | ||
| 105 | + CASE WHEN 1 = 0 THEN s.id END AS taskId, | ||
| 105 | s.client_id AS clientId | 106 | s.client_id AS clientId |
| 106 | FROM mes_wm_product_sales s | 107 | FROM mes_wm_product_sales s |
| 107 | JOIN mes_wm_product_sales_line sl ON s.id = sl.sales_id | 108 | JOIN mes_wm_product_sales_line sl ON s.id = sl.sales_id |
| @@ -123,11 +124,11 @@ | @@ -123,11 +124,11 @@ | ||
| 123 | ${@cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum@RQC.type} AS qcType, | 124 | ${@cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum@RQC.type} AS qcType, |
| 124 | ril.item_id AS itemId, | 125 | ril.item_id AS itemId, |
| 125 | ril.quantity AS quantity, | 126 | ril.quantity AS quantity, |
| 126 | - NULL AS vendorId, | 127 | + CASE WHEN 1 = 0 THEN ri.id END AS vendorId, |
| 127 | ri.work_order_id AS workOrderId, | 128 | ri.work_order_id AS workOrderId, |
| 128 | ri.workstation_id AS workstationId, | 129 | ri.workstation_id AS workstationId, |
| 129 | - NULL AS taskId, | ||
| 130 | - NULL AS clientId | 130 | + CASE WHEN 1 = 0 THEN ri.id END AS taskId, |
| 131 | + CASE WHEN 1 = 0 THEN ri.id END AS clientId | ||
| 131 | FROM mes_wm_return_issue ri | 132 | FROM mes_wm_return_issue ri |
| 132 | JOIN mes_wm_return_issue_line ril ON ri.id = ril.issue_id | 133 | JOIN mes_wm_return_issue_line ril ON ri.id = ril.issue_id |
| 133 | WHERE ri.status = ${@cn.iocoder.yudao.module.mes.enums.wm.MesWmReturnIssueStatusEnum@CONFIRMED.status} | 134 | WHERE ri.status = ${@cn.iocoder.yudao.module.mes.enums.wm.MesWmReturnIssueStatusEnum@CONFIRMED.status} |
| @@ -149,10 +150,10 @@ | @@ -149,10 +150,10 @@ | ||
| 149 | ${@cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum@RQC.type} AS qcType, | 150 | ${@cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum@RQC.type} AS qcType, |
| 150 | rl.item_id AS itemId, | 151 | rl.item_id AS itemId, |
| 151 | rl.quantity AS quantity, | 152 | rl.quantity AS quantity, |
| 152 | - NULL AS vendorId, | ||
| 153 | - NULL AS workOrderId, | ||
| 154 | - NULL AS workstationId, | ||
| 155 | - NULL AS taskId, | 153 | + CASE WHEN 1 = 0 THEN r.id END AS vendorId, |
| 154 | + CASE WHEN 1 = 0 THEN r.id END AS workOrderId, | ||
| 155 | + CASE WHEN 1 = 0 THEN r.id END AS workstationId, | ||
| 156 | + CASE WHEN 1 = 0 THEN r.id END AS taskId, | ||
| 156 | r.client_id AS clientId | 157 | r.client_id AS clientId |
| 157 | FROM mes_wm_return_sales r | 158 | FROM mes_wm_return_sales r |
| 158 | JOIN mes_wm_return_sales_line rl ON r.id = rl.return_id | 159 | JOIN mes_wm_return_sales_line rl ON r.id = rl.return_id |
yudao-module-mes/src/test/resources/sql/clean.sql
| @@ -20,5 +20,6 @@ DELETE FROM "mes_wm_item_receipt_detail"; | @@ -20,5 +20,6 @@ DELETE FROM "mes_wm_item_receipt_detail"; | ||
| 20 | DELETE FROM "mes_wm_batch"; | 20 | DELETE FROM "mes_wm_batch"; |
| 21 | DELETE FROM "mes_wm_material_stock"; | 21 | DELETE FROM "mes_wm_material_stock"; |
| 22 | DELETE FROM "mes_pro_task"; | 22 | DELETE FROM "mes_pro_task"; |
| 23 | +DELETE FROM "mes_pro_work_order"; | ||
| 23 | DELETE FROM "mes_pro_route_process"; | 24 | DELETE FROM "mes_pro_route_process"; |
| 24 | DELETE FROM "mes_qc_indicator_result"; | 25 | DELETE FROM "mes_qc_indicator_result"; |
yudao-module-mes/src/test/resources/sql/create_tables.sql
| @@ -756,9 +756,9 @@ CREATE TABLE IF NOT EXISTS "mes_pro_task" ( | @@ -756,9 +756,9 @@ CREATE TABLE IF NOT EXISTS "mes_pro_task" ( | ||
| 756 | "process_id" bigint DEFAULT NULL, | 756 | "process_id" bigint DEFAULT NULL, |
| 757 | "item_id" bigint DEFAULT NULL, | 757 | "item_id" bigint DEFAULT NULL, |
| 758 | "quantity" decimal(14,2) DEFAULT NULL, | 758 | "quantity" decimal(14,2) DEFAULT NULL, |
| 759 | - "produced_quantity" decimal(14,2) DEFAULT NULL, | ||
| 760 | - "qualify_quantity" decimal(14,2) DEFAULT NULL, | ||
| 761 | - "unqualify_quantity" decimal(14,2) DEFAULT NULL, | 759 | + "produced_quantity" decimal(14,2) NOT NULL DEFAULT 0, |
| 760 | + "qualify_quantity" decimal(14,2) NOT NULL DEFAULT 0, | ||
| 761 | + "unqualify_quantity" decimal(14,2) NOT NULL DEFAULT 0, | ||
| 762 | "changed_quantity" decimal(14,2) DEFAULT NULL, | 762 | "changed_quantity" decimal(14,2) DEFAULT NULL, |
| 763 | "client_id" bigint DEFAULT NULL, | 763 | "client_id" bigint DEFAULT NULL, |
| 764 | "start_time" timestamp DEFAULT NULL, | 764 | "start_time" timestamp DEFAULT NULL, |
| @@ -779,6 +779,39 @@ CREATE TABLE IF NOT EXISTS "mes_pro_task" ( | @@ -779,6 +779,39 @@ CREATE TABLE IF NOT EXISTS "mes_pro_task" ( | ||
| 779 | ); | 779 | ); |
| 780 | 780 | ||
| 781 | -- ---------------------------- | 781 | -- ---------------------------- |
| 782 | +-- MES 生产工单 | ||
| 783 | +-- ---------------------------- | ||
| 784 | +CREATE TABLE IF NOT EXISTS "mes_pro_work_order" ( | ||
| 785 | + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, | ||
| 786 | + "code" varchar(64) DEFAULT NULL, | ||
| 787 | + "name" varchar(255) DEFAULT NULL, | ||
| 788 | + "type" tinyint DEFAULT NULL, | ||
| 789 | + "order_source_type" tinyint DEFAULT NULL, | ||
| 790 | + "order_source_code" varchar(64) DEFAULT NULL, | ||
| 791 | + "product_id" bigint DEFAULT NULL, | ||
| 792 | + "quantity" decimal(14,2) NOT NULL DEFAULT 0, | ||
| 793 | + "quantity_produced" decimal(14,2) NOT NULL DEFAULT 0, | ||
| 794 | + "quantity_changed" decimal(14,2) NOT NULL DEFAULT 0, | ||
| 795 | + "quantity_scheduled" decimal(14,2) NOT NULL DEFAULT 0, | ||
| 796 | + "client_id" bigint DEFAULT NULL, | ||
| 797 | + "vendor_id" bigint DEFAULT NULL, | ||
| 798 | + "batch_code" varchar(64) DEFAULT NULL, | ||
| 799 | + "request_date" timestamp DEFAULT NULL, | ||
| 800 | + "parent_id" bigint NOT NULL DEFAULT 0, | ||
| 801 | + "finish_date" timestamp DEFAULT NULL, | ||
| 802 | + "cancel_date" timestamp DEFAULT NULL, | ||
| 803 | + "status" tinyint NOT NULL DEFAULT 0, | ||
| 804 | + "remark" varchar(500) DEFAULT '', | ||
| 805 | + "creator" varchar(64) DEFAULT '', | ||
| 806 | + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
| 807 | + "updater" varchar(64) DEFAULT '', | ||
| 808 | + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
| 809 | + "deleted" bit NOT NULL DEFAULT FALSE, | ||
| 810 | + "tenant_id" bigint NOT NULL DEFAULT 0, | ||
| 811 | + PRIMARY KEY ("id") | ||
| 812 | +); | ||
| 813 | + | ||
| 814 | +-- ---------------------------- | ||
| 782 | -- MES 工艺路线工序 | 815 | -- MES 工艺路线工序 |
| 783 | -- ---------------------------- | 816 | -- ---------------------------- |
| 784 | CREATE TABLE IF NOT EXISTS "mes_pro_route_process" ( | 817 | CREATE TABLE IF NOT EXISTS "mes_pro_route_process" ( |
yudao-module-mp/src/main/java/cn/iocoder/yudao/module/mp/service/account/MpAccountServiceImpl.java
| @@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil; | @@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil; | ||
| 4 | import cn.hutool.core.util.ObjUtil; | 4 | import cn.hutool.core.util.ObjUtil; |
| 5 | import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil; | 5 | import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil; |
| 6 | import cn.iocoder.yudao.framework.common.pojo.PageResult; | 6 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 7 | +import cn.iocoder.yudao.framework.common.util.jdbc.JdbcExceptionUtils; | ||
| 7 | import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; | 8 | import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; |
| 8 | import cn.iocoder.yudao.module.mp.controller.admin.account.vo.MpAccountCreateReqVO; | 9 | import cn.iocoder.yudao.module.mp.controller.admin.account.vo.MpAccountCreateReqVO; |
| 9 | import cn.iocoder.yudao.module.mp.controller.admin.account.vo.MpAccountPageReqVO; | 10 | import cn.iocoder.yudao.module.mp.controller.admin.account.vo.MpAccountPageReqVO; |
| @@ -73,7 +74,7 @@ public class MpAccountServiceImpl implements MpAccountService { | @@ -73,7 +74,7 @@ public class MpAccountServiceImpl implements MpAccountService { | ||
| 73 | try { | 74 | try { |
| 74 | accounts = mpAccountMapper.selectList(); | 75 | accounts = mpAccountMapper.selectList(); |
| 75 | } catch (Throwable ex) { | 76 | } catch (Throwable ex) { |
| 76 | - if (!ex.getMessage().contains("doesn't exist")) { | 77 | + if (!JdbcExceptionUtils.isTableNotExists(ex)) { |
| 77 | throw ex; | 78 | throw ex; |
| 78 | } | 79 | } |
| 79 | log.error("[微信公众号 yudao-module-mp - 表结构未导入][参考 https://doc.iocoder.cn/mp/build/ 开启]"); | 80 | log.error("[微信公众号 yudao-module-mp - 表结构未导入][参考 https://doc.iocoder.cn/mp/build/ 开启]"); |
yudao-module-oa/src/main/java/cn/iocoder/yudao/module/oa/dal/mysql/announcement/OaAnnouncementMapper.java
| @@ -40,7 +40,7 @@ public interface OaAnnouncementMapper extends BaseMapperX<OaAnnouncementDO> { | @@ -40,7 +40,7 @@ public interface OaAnnouncementMapper extends BaseMapperX<OaAnnouncementDO> { | ||
| 40 | .selectAll(OaAnnouncementDO.class) | 40 | .selectAll(OaAnnouncementDO.class) |
| 41 | .innerJoin(OaAnnouncementReceiverDO.class, OaAnnouncementReceiverDO::getAnnouncementId,OaAnnouncementDO::getId) | 41 | .innerJoin(OaAnnouncementReceiverDO.class, OaAnnouncementReceiverDO::getAnnouncementId,OaAnnouncementDO::getId) |
| 42 | .eq(OaAnnouncementReceiverDO::getReceiverUserId, receiverUserId) | 42 | .eq(OaAnnouncementReceiverDO::getReceiverUserId, receiverUserId) |
| 43 | - .eq(OaAnnouncementReceiverDO::getDeleted, false) | 43 | + .eq(OaAnnouncementReceiverDO::getDeleted, 0) |
| 44 | .eqIfPresent(OaAnnouncementReceiverDO::getReadStatus, pageReqVO.getReadStatus()) | 44 | .eqIfPresent(OaAnnouncementReceiverDO::getReadStatus, pageReqVO.getReadStatus()) |
| 45 | .likeIfPresent(OaAnnouncementDO::getTitle, pageReqVO.getTitle()) | 45 | .likeIfPresent(OaAnnouncementDO::getTitle, pageReqVO.getTitle()) |
| 46 | .eqIfPresent(OaAnnouncementDO::getType, pageReqVO.getType()) | 46 | .eqIfPresent(OaAnnouncementDO::getType, pageReqVO.getType()) |
yudao-module-oa/src/main/java/cn/iocoder/yudao/module/oa/dal/mysql/contact/OaContactShareMapper.java
| @@ -28,7 +28,7 @@ public interface OaContactShareMapper extends BaseMapperX<OaContactShareDO> { | @@ -28,7 +28,7 @@ public interface OaContactShareMapper extends BaseMapperX<OaContactShareDO> { | ||
| 28 | MPJLambdaWrapperX<OaContactShareDO> query = new MPJLambdaWrapperX<>(); | 28 | MPJLambdaWrapperX<OaContactShareDO> query = new MPJLambdaWrapperX<>(); |
| 29 | query.selectAll(OaContactShareDO.class) | 29 | query.selectAll(OaContactShareDO.class) |
| 30 | .innerJoin(OaContactDO.class, OaContactDO::getId, OaContactShareDO::getContactId) | 30 | .innerJoin(OaContactDO.class, OaContactDO::getId, OaContactShareDO::getContactId) |
| 31 | - .eq(OaContactDO::getDeleted, false) | 31 | + .eq(OaContactDO::getDeleted, 0) |
| 32 | .eq(OaContactShareDO::getCreator, userId.toString()) | 32 | .eq(OaContactShareDO::getCreator, userId.toString()) |
| 33 | .ne(OaContactShareDO::getUserId, userId) | 33 | .ne(OaContactShareDO::getUserId, userId) |
| 34 | .in(OaContactShareDO::getContactId, contactIds) | 34 | .in(OaContactShareDO::getContactId, contactIds) |
yudao-module-oa/src/test/java/cn/iocoder/yudao/module/oa/service/announcement/OaAnnouncementServiceImplTest.java
| @@ -439,6 +439,28 @@ public class OaAnnouncementServiceImplTest extends BaseDbUnitTest { | @@ -439,6 +439,28 @@ public class OaAnnouncementServiceImplTest extends BaseDbUnitTest { | ||
| 439 | } | 439 | } |
| 440 | 440 | ||
| 441 | @Test | 441 | @Test |
| 442 | + public void testGetReceivedAnnouncementPage_excludeDeletedReceiver() { | ||
| 443 | + // mock 数据:同一用户的两条接收关系,其中一条被逻辑删除 | ||
| 444 | + Long userId = randomLongId(); | ||
| 445 | + OaAnnouncementDO keptAnnouncement = buildAnnouncementDO(randomLongId()).setTitle("保留的公告"); | ||
| 446 | + announcementMapper.insert(keptAnnouncement); | ||
| 447 | + announcementReceiverMapper.insert(buildReceiverDO(keptAnnouncement.getId(), userId, false)); | ||
| 448 | + OaAnnouncementDO removedAnnouncement = buildAnnouncementDO(randomLongId()).setTitle("已移除的公告"); | ||
| 449 | + announcementMapper.insert(removedAnnouncement); | ||
| 450 | + OaAnnouncementReceiverDO removedReceiver = buildReceiverDO(removedAnnouncement.getId(), userId, true); | ||
| 451 | + announcementReceiverMapper.insert(removedReceiver); | ||
| 452 | + announcementReceiverMapper.deleteById(removedReceiver.getId()); | ||
| 453 | + | ||
| 454 | + // 调用 | ||
| 455 | + PageResult<OaAnnouncementDO> result = announcementService.getReceivedAnnouncementPage( | ||
| 456 | + new OaAnnouncementPageReqVO(), userId); | ||
| 457 | + | ||
| 458 | + // 断言:接收关系 deleted = 0 的条件以整数绑定后,仍只返回未删除的接收关系 | ||
| 459 | + assertEquals(Collections.singletonList(keptAnnouncement.getId()), | ||
| 460 | + convertList(result.getList(), OaAnnouncementDO::getId)); | ||
| 461 | + } | ||
| 462 | + | ||
| 463 | + @Test | ||
| 442 | public void testUpdateAnnouncement_preserveCreateTime() { | 464 | public void testUpdateAnnouncement_preserveCreateTime() { |
| 443 | // mock 数据 | 465 | // mock 数据 |
| 444 | Long userId = randomLongId(); | 466 | Long userId = randomLongId(); |
yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/dal/mysql/home/WmsHomeStatisticsMapper.java
| @@ -30,7 +30,7 @@ public interface WmsHomeStatisticsMapper { | @@ -30,7 +30,7 @@ public interface WmsHomeStatisticsMapper { | ||
| 30 | * @param beginTime >= 开始时间 | 30 | * @param beginTime >= 开始时间 |
| 31 | * @param endTime < 结束时间 | 31 | * @param endTime < 结束时间 |
| 32 | * @param warehouseId 仓库编号 | 32 | * @param warehouseId 仓库编号 |
| 33 | - * @return [{ "date": "2026-05-14", "orderType": 1, "count": 5 }, ...] | 33 | + * @return [{ "statDate": "2026-05-14", "orderType": 1, "count": 5 }, ...] |
| 34 | */ | 34 | */ |
| 35 | List<Map<String, Object>> selectDailyOrderTrend(@Param("beginTime") LocalDateTime beginTime, | 35 | List<Map<String, Object>> selectDailyOrderTrend(@Param("beginTime") LocalDateTime beginTime, |
| 36 | @Param("endTime") LocalDateTime endTime, | 36 | @Param("endTime") LocalDateTime endTime, |
yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/home/WmsHomeStatisticsServiceImpl.java
| @@ -68,7 +68,7 @@ public class WmsHomeStatisticsServiceImpl implements WmsHomeStatisticsService { | @@ -68,7 +68,7 @@ public class WmsHomeStatisticsServiceImpl implements WmsHomeStatisticsService { | ||
| 68 | List<Map<String, Object>> dbData = homeStatisticsMapper.selectDailyOrderTrend(beginTime, endTime, warehouseId); | 68 | List<Map<String, Object>> dbData = homeStatisticsMapper.selectDailyOrderTrend(beginTime, endTime, warehouseId); |
| 69 | Map<String, Map<Integer, Long>> dateMap = new LinkedHashMap<>(); | 69 | Map<String, Map<Integer, Long>> dateMap = new LinkedHashMap<>(); |
| 70 | for (Map<String, Object> row : dbData) { | 70 | for (Map<String, Object> row : dbData) { |
| 71 | - String date = MapUtil.getStr(row, "date"); | 71 | + String date = MapUtil.getStr(row, "statDate"); |
| 72 | Integer orderType = MapUtil.getInt(row, "orderType"); | 72 | Integer orderType = MapUtil.getInt(row, "orderType"); |
| 73 | Long count = MapUtil.getLong(row, "count", 0L); | 73 | Long count = MapUtil.getLong(row, "count", 0L); |
| 74 | dateMap.computeIfAbsent(date, key -> new HashMap<>()).put(orderType, count); | 74 | dateMap.computeIfAbsent(date, key -> new HashMap<>()).put(orderType, count); |
yudao-module-wms/src/main/resources/mapper/home/WmsHomeStatisticsMapper.xml
| @@ -52,9 +52,9 @@ | @@ -52,9 +52,9 @@ | ||
| 52 | 52 | ||
| 53 | <!-- 按天聚合单据数量 --> | 53 | <!-- 按天聚合单据数量 --> |
| 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 date, 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 date, | 57 | + SELECT DATE_FORMAT(order_time, '%Y-%m-%d') 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 |
| @@ -66,7 +66,7 @@ | @@ -66,7 +66,7 @@ | ||
| 66 | </if> | 66 | </if> |
| 67 | GROUP BY DATE_FORMAT(order_time, '%Y-%m-%d') | 67 | GROUP BY DATE_FORMAT(order_time, '%Y-%m-%d') |
| 68 | UNION ALL | 68 | UNION ALL |
| 69 | - SELECT DATE_FORMAT(order_time, '%Y-%m-%d') AS date, | 69 | + SELECT DATE_FORMAT(order_time, '%Y-%m-%d') 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 |
| @@ -78,7 +78,7 @@ | @@ -78,7 +78,7 @@ | ||
| 78 | </if> | 78 | </if> |
| 79 | GROUP BY DATE_FORMAT(order_time, '%Y-%m-%d') | 79 | GROUP BY DATE_FORMAT(order_time, '%Y-%m-%d') |
| 80 | UNION ALL | 80 | UNION ALL |
| 81 | - SELECT DATE_FORMAT(order_time, '%Y-%m-%d') AS date, | 81 | + SELECT DATE_FORMAT(order_time, '%Y-%m-%d') 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 |
| @@ -90,7 +90,7 @@ | @@ -90,7 +90,7 @@ | ||
| 90 | </if> | 90 | </if> |
| 91 | GROUP BY DATE_FORMAT(order_time, '%Y-%m-%d') | 91 | GROUP BY DATE_FORMAT(order_time, '%Y-%m-%d') |
| 92 | UNION ALL | 92 | UNION ALL |
| 93 | - SELECT DATE_FORMAT(order_time, '%Y-%m-%d') AS date, | 93 | + SELECT DATE_FORMAT(order_time, '%Y-%m-%d') 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 |
| @@ -102,13 +102,13 @@ | @@ -102,13 +102,13 @@ | ||
| 102 | </if> | 102 | </if> |
| 103 | GROUP BY DATE_FORMAT(order_time, '%Y-%m-%d') | 103 | GROUP BY DATE_FORMAT(order_time, '%Y-%m-%d') |
| 104 | ) t | 104 | ) t |
| 105 | - GROUP BY date, orderType | ||
| 106 | - ORDER BY date, orderType | 105 | + GROUP BY statDate, orderType |
| 106 | + ORDER BY statDate, orderType | ||
| 107 | </select> | 107 | </select> |
| 108 | 108 | ||
| 109 | <!-- 统计库存总数量 --> | 109 | <!-- 统计库存总数量 --> |
| 110 | <select id="selectInventoryTotalQuantity" resultType="java.math.BigDecimal"> | 110 | <select id="selectInventoryTotalQuantity" resultType="java.math.BigDecimal"> |
| 111 | - SELECT IFNULL(SUM(quantity), 0) | 111 | + SELECT COALESCE(SUM(quantity), 0) |
| 112 | FROM wms_inventory | 112 | FROM wms_inventory |
| 113 | WHERE deleted = 0 | 113 | WHERE deleted = 0 |
| 114 | <if test="warehouseId != null"> | 114 | <if test="warehouseId != null"> |
| @@ -120,7 +120,7 @@ | @@ -120,7 +120,7 @@ | ||
| 120 | <select id="selectInventoryItemRank" resultType="cn.iocoder.yudao.framework.mybatis.core.util.CaseInsensitiveResultMap"> | 120 | <select id="selectInventoryItemRank" resultType="cn.iocoder.yudao.framework.mybatis.core.util.CaseInsensitiveResultMap"> |
| 121 | SELECT item.id AS id, | 121 | SELECT item.id AS id, |
| 122 | item.name AS name, | 122 | item.name AS name, |
| 123 | - IFNULL(SUM(inventory.quantity), 0) AS quantity | 123 | + COALESCE(SUM(inventory.quantity), 0) AS quantity |
| 124 | FROM wms_inventory inventory | 124 | FROM wms_inventory inventory |
| 125 | INNER JOIN wms_item_sku sku ON sku.id = inventory.sku_id AND sku.deleted = 0 | 125 | INNER JOIN wms_item_sku sku ON sku.id = inventory.sku_id AND sku.deleted = 0 |
| 126 | INNER JOIN wms_item item ON item.id = sku.item_id AND item.deleted = 0 | 126 | INNER JOIN wms_item item ON item.id = sku.item_id AND item.deleted = 0 |
| @@ -138,7 +138,7 @@ | @@ -138,7 +138,7 @@ | ||
| 138 | <select id="selectInventoryWarehouseRank" resultType="cn.iocoder.yudao.framework.mybatis.core.util.CaseInsensitiveResultMap"> | 138 | <select id="selectInventoryWarehouseRank" resultType="cn.iocoder.yudao.framework.mybatis.core.util.CaseInsensitiveResultMap"> |
| 139 | SELECT warehouse.id AS id, | 139 | SELECT warehouse.id AS id, |
| 140 | warehouse.name AS name, | 140 | warehouse.name AS name, |
| 141 | - IFNULL(SUM(inventory.quantity), 0) AS quantity | 141 | + COALESCE(SUM(inventory.quantity), 0) AS quantity |
| 142 | FROM wms_inventory inventory | 142 | FROM wms_inventory inventory |
| 143 | INNER JOIN wms_warehouse warehouse ON warehouse.id = inventory.warehouse_id AND warehouse.deleted = 0 | 143 | INNER JOIN wms_warehouse warehouse ON warehouse.id = inventory.warehouse_id AND warehouse.deleted = 0 |
| 144 | WHERE inventory.deleted = 0 | 144 | WHERE inventory.deleted = 0 |