Commit 7f53d60ed30251cd44cff383529c13858d374a7f

Authored by YunaiV
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 11 import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
12 12 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
13 13 import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
  14 +import cn.iocoder.yudao.framework.common.util.jdbc.JdbcExceptionUtils;
14 15 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
15 16 import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils;
16 17 import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
... ... @@ -396,10 +397,10 @@ public class GlobalExceptionHandler {
396 397 * @return 如果是 Table 不存在的异常,则返回对应的 CommonResult
397 398 */
398 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 401 return null;
402 402 }
  403 + String message = StrUtil.nullToEmpty(ExceptionUtil.getRootCauseMessage(ex));
403 404 // 1. 工作流
404 405 if (message.contains("bpm_")) {
405 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 5 import cn.iocoder.yudao.module.ai1.controller.admin.home.vo.Ai1HomeSummaryRespVO;
6 6 import org.apache.ibatis.annotations.Mapper;
7 7 import org.apache.ibatis.annotations.Param;
  8 +import org.apache.ibatis.annotations.Result;
  9 +import org.apache.ibatis.annotations.Results;
8 10 import org.apache.ibatis.annotations.Select;
9 11  
10 12 import java.time.LocalDateTime;
... ... @@ -18,32 +20,36 @@ import java.util.List;
18 20 @Mapper
19 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 27 Ai1HomeSummaryRespVO selectSummary();
26 28  
27 29 /**
28 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 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 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 41 List<Ai1HomeMessageSummaryByDateRespVO> selectMessageSummaryListByCreateTimeBetweenGroupByDate(
36 42 @Param("beginTime") LocalDateTime beginTime, @Param("endTime") LocalDateTime endTime);
37 43  
38 44 /**
39 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 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 51 "GROUP BY c.agent_id " +
46   - "ORDER BY `count` DESC, c.agent_id")
  52 + "ORDER BY count DESC, c.agent_id")
47 53 List<Ai1HomeMessageSummaryByAgentRespVO> selectMessageSummaryListByCreateTimeBetweenGroupByAgentId(
48 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 16 </foreach>
17 17 AND post_owner_user_id IS NULL
18 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 20 </select>
21 21  
22 22 <select id="selectPoolCustomerTakeCountByDate"
... ... @@ -32,7 +32,7 @@
32 32 #{userId}
33 33 </foreach>
34 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 36 </select>
37 37  
38 38 <select id="selectPoolCustomerPutCountByUser"
... ...
yudao-module-crm/src/main/resources/mapper/statistics/CrmStatisticsCustomerMapper.xml
... ... @@ -13,15 +13,15 @@
13 13 #{userId}
14 14 </foreach>
15 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 17 </select>
18 18  
19 19 <select id="selectCustomerDealCountGroupByDate"
20 20 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerSummaryByDateRespVO">
21 21 SELECT DATE_FORMAT(customer.create_time, '%Y-%m-%d') AS time,
22 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 25 WHERE customer.deleted = 0
26 26 AND contract.deleted = 0
27 27 AND contract.audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
... ... @@ -30,7 +30,7 @@
30 30 #{userId}
31 31 </foreach>
32 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 34 </select>
35 35  
36 36 <select id="selectCustomerCreateCountGroupByUser"
... ... @@ -51,8 +51,8 @@
51 51 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerSummaryByUserRespVO">
52 52 SELECT customer.owner_user_id,
53 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 56 WHERE customer.deleted = 0
57 57 AND contract.deleted = 0
58 58 AND contract.audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
... ... @@ -67,7 +67,7 @@
67 67 <select id="selectContractPriceGroupByUser"
68 68 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerSummaryByUserRespVO">
69 69 SELECT owner_user_id,
70   - IFNULL(SUM(total_price), 0) AS contract_price
  70 + COALESCE(SUM(total_price), 0) AS contract_price
71 71 FROM crm_contract
72 72 WHERE deleted = 0
73 73 AND audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
... ... @@ -82,7 +82,7 @@
82 82 <select id="selectReceivablePriceGroupByUser"
83 83 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerSummaryByUserRespVO">
84 84 SELECT owner_user_id,
85   - IFNULL(SUM(price), 0) AS receivable_price
  85 + COALESCE(SUM(price), 0) AS receivable_price
86 86 FROM crm_receivable
87 87 WHERE deleted = 0
88 88 AND audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
... ... @@ -105,7 +105,7 @@
105 105 </foreach>
106 106 AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
107 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 109 </select>
110 110  
111 111 <select id="selectFollowUpCustomerCountGroupByDate"
... ... @@ -119,7 +119,7 @@
119 119 </foreach>
120 120 AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
121 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 123 </select>
124 124  
125 125 <select id="selectFollowUpRecordCountGroupByUser"
... ... @@ -162,7 +162,7 @@
162 162 </foreach>
163 163 AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND #{times[1],javaType=java.time.LocalDateTime}
164 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 166 </select>
167 167  
168 168 <select id="selectContractSummary"
... ... @@ -176,10 +176,10 @@
176 176 contract.name AS contract_name,
177 177 contract.total_price,
178 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 183 WHERE customer.deleted = 0 AND contract.deleted = 0 AND receivable.deleted = 0
184 184 AND contract.audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
185 185 AND customer.owner_user_id IN
... ... @@ -193,9 +193,9 @@
193 193 <select id="selectCustomerDealCycleGroupByDate"
194 194 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerDealCycleByDateRespVO">
195 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 199 WHERE customer.deleted = 0 AND contract.deleted = 0
200 200 AND contract.audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
201 201 AND customer.owner_user_id IN
... ... @@ -203,15 +203,15 @@
203 203 #{userId}
204 204 </foreach>
205 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 207 </select>
208 208  
209 209 <select id="selectCustomerDealCycleGroupByUser"
210 210 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerDealCycleByUserRespVO">
211 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 215 WHERE customer.deleted = 0 AND contract.deleted = 0
216 216 AND contract.audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
217 217 AND customer.owner_user_id IN
... ... @@ -225,10 +225,10 @@
225 225 <select id="selectCustomerDealCycleGroupByAreaId"
226 226 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerDealCycleByAreaRespVO">
227 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 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 232 WHERE customer.deleted = 0
233 233 AND contract.deleted = 0
234 234 AND contract.audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
... ... @@ -244,12 +244,12 @@
244 244 <select id="selectCustomerDealCycleGroupByProductId"
245 245 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.customer.CrmStatisticsCustomerDealCycleByProductRespVO">
246 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 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 253 WHERE customer.deleted = 0
254 254 AND contract.deleted = 0
255 255 AND contract_product.deleted = 0
... ... @@ -260,7 +260,7 @@
260 260 #{userId}
261 261 </foreach>
262 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 264 </select>
265 265  
266 266 </mapper>
... ...
yudao-module-crm/src/main/resources/mapper/statistics/CrmStatisticsFunnelMapper.xml
... ... @@ -41,7 +41,7 @@
41 41 SELECT
42 42 end_status AS endStatus,
43 43 COUNT(*) AS businessCount,
44   - IFNULL(SUM(total_price), 0) AS totalPrice
  44 + COALESCE(SUM(total_price), 0) AS totalPrice
45 45 FROM crm_business
46 46 WHERE deleted = 0 AND end_status IS NOT NULL
47 47 <if test="statusTypeId != null">
... ... @@ -64,7 +64,7 @@
64 64 status.percent AS statusPercent,
65 65 status.sort AS sort,
66 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 68 FROM crm_business_status status
69 69 LEFT JOIN crm_business business ON business.status_id = status.id
70 70 AND business.deleted = 0
... ... @@ -88,7 +88,7 @@
88 88 SELECT
89 89 DATE_FORMAT(create_time, '%Y-%m-%d') AS time,
90 90 COUNT(*) AS businessCreateCount,
91   - IFNULL(SUM(total_price), 0) AS totalPrice
  91 + COALESCE(SUM(total_price), 0) AS totalPrice
92 92 FROM crm_business
93 93 WHERE deleted = 0
94 94 <if test="statusTypeId != null">
... ... @@ -100,7 +100,7 @@
100 100 </foreach>
101 101 AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND
102 102 #{times[1],javaType=java.time.LocalDateTime}
103   - GROUP BY time
  103 + GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d')
104 104 </select>
105 105  
106 106 <select id="selectBusinessInversionRateSummaryByDate"
... ... @@ -108,7 +108,7 @@
108 108 SELECT
109 109 DATE_FORMAT(create_time, '%Y-%m-%d') AS time,
110 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 112 FROM crm_business
113 113 WHERE deleted = 0
114 114 <if test="statusTypeId != null">
... ... @@ -120,7 +120,7 @@
120 120 </foreach>
121 121 AND create_time BETWEEN #{times[0],javaType=java.time.LocalDateTime} AND
122 122 #{times[1],javaType=java.time.LocalDateTime}
123   - GROUP BY time
  123 + GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d')
124 124 </select>
125 125  
126 126 </mapper>
... ...
yudao-module-crm/src/main/resources/mapper/statistics/CrmStatisticsPerformanceMapper.xml
... ... @@ -16,14 +16,14 @@
16 16 </foreach>
17 17 AND order_date between #{times[0],javaType=java.time.LocalDateTime} and
18 18 #{times[1],javaType=java.time.LocalDateTime}
19   - GROUP BY time
  19 + GROUP BY DATE_FORMAT(order_date, '%Y%m')
20 20 </select>
21 21  
22 22 <select id="selectContractPricePerformance"
23 23 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceRespVO">
24 24 SELECT
25 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 27 FROM crm_contract
28 28 WHERE deleted = 0
29 29 AND audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
... ... @@ -33,14 +33,14 @@
33 33 </foreach>
34 34 AND order_date between #{times[0],javaType=java.time.LocalDateTime} and
35 35 #{times[1],javaType=java.time.LocalDateTime}
36   - GROUP BY time
  36 + GROUP BY DATE_FORMAT(order_date, '%Y%m')
37 37 </select>
38 38  
39 39 <select id="selectReceivablePricePerformance"
40 40 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceRespVO">
41 41 SELECT
42 42 DATE_FORMAT(return_time, '%Y%m') AS time,
43   - IFNULL(SUM(price), 0) AS currentMonthCount
  43 + COALESCE(SUM(price), 0) AS currentMonthCount
44 44 FROM crm_receivable
45 45 WHERE deleted = 0
46 46 AND audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
... ... @@ -50,25 +50,25 @@
50 50 </foreach>
51 51 AND return_time between #{times[0],javaType=java.time.LocalDateTime} and
52 52 #{times[1],javaType=java.time.LocalDateTime}
53   - GROUP BY time
  53 + GROUP BY DATE_FORMAT(return_time, '%Y%m')
54 54 </select>
55 55  
56 56 <select id="selectContractSummary"
57 57 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.performance.CrmStatisticsPerformanceSummaryRespVO">
58 58 SELECT DATE_FORMAT(contract.order_date, '%Y%m') AS time,
59 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 64 LEFT JOIN (
65 65 SELECT contract_id,
66   - IFNULL(SUM(price), 0) AS receivable_price
  66 + COALESCE(SUM(price), 0) AS receivable_price
67 67 FROM crm_receivable
68 68 WHERE deleted = 0
69 69 AND audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
70 70 GROUP BY contract_id
71   - ) AS receivable ON contract.id = receivable.contract_id
  71 + ) receivable ON contract.id = receivable.contract_id
72 72 WHERE contract.deleted = 0
73 73 AND contract.audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
74 74 AND contract.owner_user_id in
... ... @@ -77,7 +77,7 @@
77 77 </foreach>
78 78 AND contract.order_date between #{times[0],javaType=java.time.LocalDateTime} and
79 79 #{times[1],javaType=java.time.LocalDateTime}
80   - GROUP BY time
  80 + GROUP BY DATE_FORMAT(contract.order_date, '%Y%m')
81 81 </select>
82 82  
83 83 </mapper>
... ...
yudao-module-crm/src/main/resources/mapper/statistics/CrmStatisticsPerformanceTargetMapper.xml
... ... @@ -11,7 +11,7 @@
11 11 <select id="selectContractActualByMonth"
12 12 resultMap="CrmStatisticsPerformanceTargetActualResultMap">
13 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 15 FROM crm_contract
16 16 WHERE deleted = 0
17 17 AND audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
... ... @@ -26,7 +26,7 @@
26 26 <select id="selectReceivableActualByMonth"
27 27 resultMap="CrmStatisticsPerformanceTargetActualResultMap">
28 28 SELECT MONTH(return_time) AS month,
29   - IFNULL(SUM(price), 0) AS current_price
  29 + COALESCE(SUM(price), 0) AS current_price
30 30 FROM crm_receivable
31 31 WHERE deleted = 0
32 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 26 <select id="selectProductSalesList"
27 27 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.product.CrmStatisticsProductSalesRespVO">
28 28 SELECT category.id AS category_id,
29   - IFNULL(category.name, '未分类') AS category_name,
  29 + COALESCE(category.name, '未分类') AS category_name,
30 30 product.id AS product_id,
31 31 product.name AS product_name,
32 32 contract.id AS contract_id,
... ... @@ -39,12 +39,12 @@
39 39 contract_product.product_price,
40 40 contract_product.count AS product_count,
41 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 48 WHERE <include refid="ProductStatisticsWhere"/>
49 49 ORDER BY category.id ASC, product.id ASC, contract.order_date DESC
50 50 </select>
... ... @@ -52,14 +52,14 @@
52 52 <select id="selectProductCategorySummary"
53 53 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.product.CrmStatisticsProductCategoryRespVO">
54 54 SELECT category.id AS category_id,
55   - IFNULL(category.name, '未分类') AS category_name,
  55 + COALESCE(category.name, '未分类') AS category_name,
56 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 63 WHERE <include refid="ProductStatisticsWhere"/>
64 64 GROUP BY category.id, category.name
65 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 4  
5 5 <select id="selectContractPriceRank"
6 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 8 FROM crm_contract
9 9 WHERE deleted = 0
10 10 AND audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
... ... @@ -18,7 +18,7 @@
18 18  
19 19 <select id="selectReceivablePriceRank"
20 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 22 FROM crm_receivable
23 23 WHERE deleted = 0
24 24 AND audit_status = ${@cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum@APPROVE.status}
... ... @@ -88,8 +88,8 @@
88 88 <select id="selectFollowCountRank"
89 89 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.rank.CrmStatisticsRankRespVO">
90 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 93 WHERE cfur.deleted = 0
94 94 AND cc.deleted = 0
95 95 AND cc.owner_user_id in
... ... @@ -103,8 +103,8 @@
103 103 <select id="selectFollowCustomerCountRank"
104 104 resultType="cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.rank.CrmStatisticsRankRespVO">
105 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 108 WHERE cfur.deleted = 0
109 109 AND cc.deleted = 0
110 110 AND cc.owner_user_id in
... ...
yudao-module-erp/src/main/resources/mapper/statistics/ErpPurchaseStatisticsMapper.xml
... ... @@ -4,7 +4,7 @@
4 4  
5 5 <select id="getPurchasePrice" resultType="java.math.BigDecimal">
6 6 SELECT
7   - (SELECT IFNULL(SUM(total_price), 0)
  7 + (SELECT COALESCE(SUM(total_price), 0)
8 8 FROM erp_purchase_in
9 9 WHERE in_time >= #{beginTime}
10 10 <if test="endTime != null">
... ... @@ -15,7 +15,7 @@
15 15 </if>
16 16 AND deleted = 0
17 17 AND status = 20) -
18   - (SELECT IFNULL(SUM(total_price), 0)
  18 + (SELECT COALESCE(SUM(total_price), 0)
19 19 FROM erp_purchase_return
20 20 WHERE return_time >= #{beginTime}
21 21 <if test="endTime != null">
... ...
yudao-module-erp/src/main/resources/mapper/statistics/ErpSaleStatisticsMapper.xml
... ... @@ -4,7 +4,7 @@
4 4  
5 5 <select id="getSalePrice" resultType="java.math.BigDecimal">
6 6 SELECT
7   - (SELECT IFNULL(SUM(total_price), 0)
  7 + (SELECT COALESCE(SUM(total_price), 0)
8 8 FROM erp_sale_out
9 9 WHERE out_time >= #{beginTime}
10 10 <if test="endTime != null">
... ... @@ -15,7 +15,7 @@
15 15 </if>
16 16 AND deleted = 0
17 17 AND status = 20) -
18   - (SELECT IFNULL(SUM(total_price), 0)
  18 + (SELECT COALESCE(SUM(total_price), 0)
19 19 FROM erp_sale_return
20 20 WHERE return_time >= #{beginTime}
21 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 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 62 "WHERE deleted = 0 AND create_time >= #{beginTime} AND create_time < #{endTime} " +
63 63 "GROUP BY DATE(create_time)")
64 64 @ResultMap(CaseInsensitiveResultMap.RESULT_MAP_ID)
... ... @@ -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 72 " SELECT DATE(send_time) AS day, sender_id AS user_id FROM im_private_message " +
73 73 " WHERE deleted = 0 AND " + NORMAL_MESSAGE_CONDITION +
74 74 " AND send_time >= #{beginTime} AND send_time < #{endTime}" +
... ... @@ -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 120 @ResultMap(CaseInsensitiveResultMap.RESULT_MAP_ID)
119 121 List<Map<String, Object>> selectGroupSizeDistribution();
120 122  
... ... @@ -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 147 "WHERE deleted = 0 AND " + NORMAL_MESSAGE_CONDITION +
146 148 " AND send_time >= #{beginTime} AND send_time < #{endTime} " +
147 149 "GROUP BY DATE(send_time)")
... ... @@ -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 158 "WHERE deleted = 0 AND " + NORMAL_MESSAGE_CONDITION +
157 159 " AND send_time >= #{beginTime} AND send_time < #{endTime} " +
158 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 70 public Map<String, Long> getGroupSizeCountMap() {
71 71 List<Map<String, Object>> rows = statisticsMapper.selectGroupSizeDistribution();
72 72 return convertMap(rows,
73   - row -> (String) row.get("range"),
  73 + row -> (String) row.get("sizeRange"),
74 74 row -> Convert.toLong(row.get("count")));
75 75 }
76 76  
... ... @@ -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 120 private static Map<LocalDateTime, Long> toDailyCountMap(List<Map<String, Object>> rows) {
121 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 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 164  
165 165 // 断言:分桶包含「1-9 人」和「10-49 人」
166 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 168 assertEquals(1L, byRange.get("1-9 人"));
169 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 76  
77 77 @Test
78 78 public void testGetNewUserDailyCountMap_dateConvert() {
79   - // 准备:date 字段以 SQL Date / String 混入,count 混入多种 Number
  79 + // 准备:statDate 字段以 SQL Date / String 混入,count 混入多种 Number
80 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 85 Map<LocalDateTime, Long> result = service.getNewUserDailyCountMap(BEGIN, END);
86 86  
87   - // 断言:date → LocalDateTime 起始零点
  87 + // 断言:statDate → LocalDateTime 起始零点
88 88 assertEquals(2, result.size());
89 89 assertEquals(5L, result.get(LocalDate.of(2026, 1, 10).atStartOfDay()));
90 90 assertEquals(8L, result.get(LocalDate.of(2026, 1, 11).atStartOfDay()));
... ... @@ -93,7 +93,7 @@ public class ImStatisticsManagerServiceImplTest extends BaseMockitoUnitTest {
93 93 @Test
94 94 public void testGetActiveUserDailyCountMap_dateConvert() {
95 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 98 Map<LocalDateTime, Long> result = service.getActiveUserDailyCountMap(BEGIN, END);
99 99 assertEquals(3L, result.get(LocalDate.of(2026, 1, 10).atStartOfDay()));
... ... @@ -102,7 +102,7 @@ public class ImStatisticsManagerServiceImplTest extends BaseMockitoUnitTest {
102 102 @Test
103 103 public void testGetPrivateMessageDailyCountMap_dateConvert() {
104 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 106 assertEquals(7L, service.getPrivateMessageDailyCountMap(BEGIN, END)
107 107 .get(LocalDate.of(2026, 1, 10).atStartOfDay()));
108 108 }
... ... @@ -110,7 +110,7 @@ public class ImStatisticsManagerServiceImplTest extends BaseMockitoUnitTest {
110 110 @Test
111 111 public void testGetGroupMessageDailyCountMap_dateConvert() {
112 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 114 assertEquals(9L, service.getGroupMessageDailyCountMap(BEGIN, END)
115 115 .get(LocalDate.of(2026, 1, 10).atStartOfDay()));
116 116 }
... ... @@ -120,8 +120,8 @@ public class ImStatisticsManagerServiceImplTest extends BaseMockitoUnitTest {
120 120 @Test
121 121 public void testGetGroupSizeCountMap() {
122 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 126 Map<String, Long> result = service.getGroupSizeCountMap();
127 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 50 </if>
51 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 61 count(1) AS count
57 62 FROM member_user
58 63 WHERE create_time BETWEEN #{beginTime} AND #{endTime}
... ... @@ -60,8 +65,8 @@
60 65 <if test="@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId() != null">
61 66 AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId()}
62 67 </if>
63   - GROUP BY date
64   - ORDER BY date
  68 + GROUP BY CAST(create_time AS DATE)
  69 + ORDER BY statDate
65 70 </select>
66 71  
67 72 </mapper>
... ...
yudao-module-mall/yudao-module-statistics/src/main/resources/mapper/trade/TradeOrderStatisticsMapper.xml
... ... @@ -6,14 +6,14 @@
6 6 resultType="cn.iocoder.yudao.module.statistics.service.member.bo.MemberAreaStatisticsRespBO">
7 7 SELECT receiver_area_id AS areaId,
8 8 (SELECT COUNT(DISTINCT s.user_id)
9   - FROM trade_order AS s
  9 + FROM trade_order s
10 10 WHERE s.receiver_area_id = m.receiver_area_id
11 11 <if test="@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId() != null">
12 12 AND s.tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId()}
13 13 </if>
14 14 ) AS orderCreateUserCount,
15 15 (SELECT COUNT(DISTINCT s.user_id)
16   - FROM trade_order AS s
  16 + FROM trade_order s
17 17 WHERE s.receiver_area_id = m.receiver_area_id
18 18 AND s.pay_status = TRUE
19 19 AND s.deleted = 0
... ... @@ -22,7 +22,7 @@
22 22 </if>
23 23 ) AS orderPayUserCount,
24 24 (SELECT SUM(s.pay_price)
25   - FROM trade_order AS s
  25 + FROM trade_order s
26 26 WHERE s.receiver_area_id = m.receiver_area_id
27 27 AND s.pay_status = TRUE
28 28 AND s.deleted = 0
... ... @@ -82,7 +82,7 @@
82 82  
83 83 <select id="selectSummaryPriceByPayTimeBetween" resultType="java.lang.Integer">
84 84 SELECT SUM(pay_price)
85   - FROM trade_order AS s
  85 + FROM trade_order s
86 86 WHERE s.pay_status = TRUE
87 87 AND deleted = 0
88 88 AND create_time BETWEEN #{beginTime} AND #{endTime}
... ... @@ -91,11 +91,17 @@
91 91 </if>
92 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 105 FROM trade_order
100 106 WHERE pay_status = TRUE
101 107 AND create_time BETWEEN #{beginTime} AND #{endTime}
... ... @@ -103,14 +109,13 @@
103 109 <if test="@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId() != null">
104 110 AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId()}
105 111 </if>
106   - GROUP BY date
107   - ORDER BY date
  112 + GROUP BY CAST(pay_time AS DATE)
  113 + ORDER BY statDate
108 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 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 119 COUNT(1) AS order_pay_count,
115 120 SUM(pay_price) AS order_pay_price
116 121 FROM trade_order
... ... @@ -120,8 +125,9 @@
120 125 <if test="@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId() != null">
121 126 AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId()}
122 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 131 </select>
126 132  
127 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&lt;BrokerageRecordDO&gt; {
80 80  
81 81 @Select("SELECT SUM(price) FROM trade_brokerage_record " +
82 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 84 Integer selectSummaryPriceByUserIdAndBizTypeAndCreateTimeBetween(@Param("userId") Long userId,
85 85 @Param("bizType") Integer bizType,
86 86 @Param("status") Integer status,
... ... @@ -89,7 +89,7 @@ public interface BrokerageRecordMapper extends BaseMapperX&lt;BrokerageRecordDO&gt; {
89 89  
90 90 // TODO @芋艿:收敛掉 @Select 注解操作,统一成 MyBatis-Plus 的方式,或者 xml
91 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 93 "AND unfreeze_time BETWEEN #{beginTime} AND #{endTime} " +
94 94 "GROUP BY user_id " +
95 95 "ORDER BY brokeragePrice DESC")
... ... @@ -100,7 +100,7 @@ public interface BrokerageRecordMapper extends BaseMapperX&lt;BrokerageRecordDO&gt; {
100 100 @Param("endTime") LocalDateTime endTime);
101 101  
102 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 104 "AND unfreeze_time BETWEEN #{beginTime} AND #{endTime} " +
105 105 "GROUP BY user_id HAVING SUM(price) > #{brokeragePrice}")
106 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&lt;BrokerageUserDO&gt; {
128 128 }
129 129  
130 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 132 "AND bind_user_time BETWEEN #{beginTime} AND #{endTime} " +
133 133 "GROUP BY bind_user_id " +
134 134 "ORDER BY brokerageUserCount DESC")
... ...
yudao-module-mall/yudao-module-trade/src/main/resources/mapper/brokerage/BrokerageUserMapper.xml
... ... @@ -22,7 +22,7 @@
22 22 WHERE r.user_id = bu.id AND biz_type = #{bizType} AND r.status = #{status} AND r.deleted = 0) AS brokerageOrderCount,
23 23 (SELECT COUNT(1) FROM trade_brokerage_user c
24 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 26 <where>
27 27 bu.deleted = 0
28 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 59 *
60 60 * @param beginTime >= 开始时间
61 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 64 List<Map<String, Object>> selectDailyFeedbackTrend(@Param("beginTime") LocalDateTime beginTime,
65 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&lt;MesProTaskDO&gt; {
62 62 BigDecimal incrUnqualifyQuantity) {
63 63 update(null, new LambdaUpdateWrapper<MesProTaskDO>()
64 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&lt;MesProWorkOrderDO&gt; {
38 38 default void updateProducedQuantity(Long id, BigDecimal incrQuantityProduced) {
39 39 update(null, new LambdaUpdateWrapper<MesProWorkOrderDO>()
40 40 .eq(MesProWorkOrderDO::getId, id)
41   - .setSql("quantity_produced = IFNULL(quantity_produced, 0) + " + incrQuantityProduced));
  41 + .setIncrBy(MesProWorkOrderDO::getQuantityProduced, incrQuantityProduced));
42 42 }
43 43  
44 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 121 List<Map<String, Object>> dbData = homeStatisticsMapper.selectDailyFeedbackTrend(beginTime, endTime);
122 122 Map<String, Map<String, Object>> dateMap = new LinkedHashMap<>();
123 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 127 // 补全所有日期(无数据的日期填 0)
... ...
yudao-module-mes/src/main/resources/mapper/home/MesHomeStatisticsMapper.xml
... ... @@ -12,9 +12,9 @@
12 12  
13 13 <!-- 统计报工产量合计 -->
14 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 18 FROM mes_pro_feedback
19 19 WHERE deleted = 0
20 20 AND status = ${@cn.iocoder.yudao.module.mes.enums.pro.MesProFeedbackStatusEnum@FINISHED.status}
... ... @@ -52,10 +52,10 @@
52 52  
53 53 <!-- 按天聚合报工产量 -->
54 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 59 FROM mes_pro_feedback
60 60 WHERE deleted = 0
61 61 AND status = ${@cn.iocoder.yudao.module.mes.enums.pro.MesProFeedbackStatusEnum@FINISHED.status}
... ... @@ -64,7 +64,7 @@
64 64 AND feedback_time &lt; #{endTime}
65 65 </if>
66 66 GROUP BY DATE_FORMAT(feedback_time, '%Y-%m-%d')
67   - ORDER BY date
  67 + ORDER BY statDate
68 68 </select>
69 69  
70 70 </mapper>
... ...
yudao-module-mes/src/main/resources/mapper/qc/pendinginspect/MesQcPendingInspectMapper.xml
... ... @@ -10,6 +10,7 @@
10 10 4. 销售出库单(OQC):单据状态=CONFIRMED(1),行的 oqc_check_flag=1 且 oqc_id 为空
11 11 5. 生产退料单(RQC):单据状态=CONFIRMED(1),行的 rqc_check_flag=1 且 rqc_id 为空 且 quality_status=0(待检)
12 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 15 <select id="selectQcPendingPage" resultType="cn.iocoder.yudao.module.mes.controller.admin.qc.pendinginspect.vo.MesQcPendingInspectRespVO">
15 16 SELECT * FROM (
... ... @@ -24,10 +25,10 @@
24 25 l.item_id AS itemId,
25 26 l.arrival_quantity AS quantity,
26 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 32 FROM mes_wm_arrival_notice n
32 33 JOIN mes_wm_arrival_notice_line l ON n.id = l.notice_id
33 34 WHERE n.status = ${@cn.iocoder.yudao.module.mes.enums.wm.MesWmArrivalNoticeStatusEnum@PENDING_QC.status}
... ... @@ -49,10 +50,10 @@
49 50 rl.item_id AS itemId,
50 51 rl.quantity AS quantity,
51 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 57 FROM mes_wm_outsource_receipt r
57 58 JOIN mes_wm_outsource_receipt_line rl ON r.id = rl.receipt_id
58 59 WHERE r.status = ${@cn.iocoder.yudao.module.mes.enums.wm.MesWmOutsourceReceiptStatusEnum@CONFIRMED.status}
... ... @@ -73,11 +74,11 @@
73 74 ${@cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum@IPQC.type} AS qcType,
74 75 ppl.item_id AS itemId,
75 76 ppl.quantity AS quantity,
76   - NULL AS vendorId,
  77 + CASE WHEN 1 = 0 THEN pf.id END AS vendorId,
77 78 pp.work_order_id AS workOrderId,
78 79 pp.workstation_id AS workstationId,
79 80 pp.task_id AS taskId,
80   - NULL AS clientId
  81 + CASE WHEN 1 = 0 THEN pf.id END AS clientId
81 82 FROM mes_wm_product_produce_line ppl
82 83 JOIN mes_wm_product_produce pp ON ppl.produce_id = pp.id
83 84 JOIN mes_pro_feedback pf ON pp.feedback_id = pf.id
... ... @@ -98,10 +99,10 @@
98 99 ${@cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum@OQC.type} AS qcType,
99 100 sl.item_id AS itemId,
100 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 106 s.client_id AS clientId
106 107 FROM mes_wm_product_sales s
107 108 JOIN mes_wm_product_sales_line sl ON s.id = sl.sales_id
... ... @@ -123,11 +124,11 @@
123 124 ${@cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum@RQC.type} AS qcType,
124 125 ril.item_id AS itemId,
125 126 ril.quantity AS quantity,
126   - NULL AS vendorId,
  127 + CASE WHEN 1 = 0 THEN ri.id END AS vendorId,
127 128 ri.work_order_id AS workOrderId,
128 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 132 FROM mes_wm_return_issue ri
132 133 JOIN mes_wm_return_issue_line ril ON ri.id = ril.issue_id
133 134 WHERE ri.status = ${@cn.iocoder.yudao.module.mes.enums.wm.MesWmReturnIssueStatusEnum@CONFIRMED.status}
... ... @@ -149,10 +150,10 @@
149 150 ${@cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum@RQC.type} AS qcType,
150 151 rl.item_id AS itemId,
151 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 157 r.client_id AS clientId
157 158 FROM mes_wm_return_sales r
158 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 &quot;mes_wm_item_receipt_detail&quot;;
20 20 DELETE FROM "mes_wm_batch";
21 21 DELETE FROM "mes_wm_material_stock";
22 22 DELETE FROM "mes_pro_task";
  23 +DELETE FROM "mes_pro_work_order";
23 24 DELETE FROM "mes_pro_route_process";
24 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 &quot;mes_pro_task&quot; (
756 756 "process_id" bigint DEFAULT NULL,
757 757 "item_id" bigint DEFAULT NULL,
758 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 762 "changed_quantity" decimal(14,2) DEFAULT NULL,
763 763 "client_id" bigint DEFAULT NULL,
764 764 "start_time" timestamp DEFAULT NULL,
... ... @@ -779,6 +779,39 @@ CREATE TABLE IF NOT EXISTS &quot;mes_pro_task&quot; (
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 815 -- MES 工艺路线工序
783 816 -- ----------------------------
784 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 4 import cn.hutool.core.util.ObjUtil;
5 5 import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
6 6 import cn.iocoder.yudao.framework.common.pojo.PageResult;
  7 +import cn.iocoder.yudao.framework.common.util.jdbc.JdbcExceptionUtils;
7 8 import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
8 9 import cn.iocoder.yudao.module.mp.controller.admin.account.vo.MpAccountCreateReqVO;
9 10 import cn.iocoder.yudao.module.mp.controller.admin.account.vo.MpAccountPageReqVO;
... ... @@ -73,7 +74,7 @@ public class MpAccountServiceImpl implements MpAccountService {
73 74 try {
74 75 accounts = mpAccountMapper.selectList();
75 76 } catch (Throwable ex) {
76   - if (!ex.getMessage().contains("doesn't exist")) {
  77 + if (!JdbcExceptionUtils.isTableNotExists(ex)) {
77 78 throw ex;
78 79 }
79 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&lt;OaAnnouncementDO&gt; {
40 40 .selectAll(OaAnnouncementDO.class)
41 41 .innerJoin(OaAnnouncementReceiverDO.class, OaAnnouncementReceiverDO::getAnnouncementId,OaAnnouncementDO::getId)
42 42 .eq(OaAnnouncementReceiverDO::getReceiverUserId, receiverUserId)
43   - .eq(OaAnnouncementReceiverDO::getDeleted, false)
  43 + .eq(OaAnnouncementReceiverDO::getDeleted, 0)
44 44 .eqIfPresent(OaAnnouncementReceiverDO::getReadStatus, pageReqVO.getReadStatus())
45 45 .likeIfPresent(OaAnnouncementDO::getTitle, pageReqVO.getTitle())
46 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&lt;OaContactShareDO&gt; {
28 28 MPJLambdaWrapperX<OaContactShareDO> query = new MPJLambdaWrapperX<>();
29 29 query.selectAll(OaContactShareDO.class)
30 30 .innerJoin(OaContactDO.class, OaContactDO::getId, OaContactShareDO::getContactId)
31   - .eq(OaContactDO::getDeleted, false)
  31 + .eq(OaContactDO::getDeleted, 0)
32 32 .eq(OaContactShareDO::getCreator, userId.toString())
33 33 .ne(OaContactShareDO::getUserId, userId)
34 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 439 }
440 440  
441 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 464 public void testUpdateAnnouncement_preserveCreateTime() {
443 465 // mock 数据
444 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 30 * @param beginTime >= 开始时间
31 31 * @param endTime < 结束时间
32 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 35 List<Map<String, Object>> selectDailyOrderTrend(@Param("beginTime") LocalDateTime beginTime,
36 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 68 List<Map<String, Object>> dbData = homeStatisticsMapper.selectDailyOrderTrend(beginTime, endTime, warehouseId);
69 69 Map<String, Map<Integer, Long>> dateMap = new LinkedHashMap<>();
70 70 for (Map<String, Object> row : dbData) {
71   - String date = MapUtil.getStr(row, "date");
  71 + String date = MapUtil.getStr(row, "statDate");
72 72 Integer orderType = MapUtil.getInt(row, "orderType");
73 73 Long count = MapUtil.getLong(row, "count", 0L);
74 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 52  
53 53 <!-- 按天聚合单据数量 -->
54 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 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 58 ${@cn.iocoder.yudao.module.wms.enums.order.WmsOrderTypeEnum@RECEIPT.type} AS orderType,
59 59 COUNT(*) AS count
60 60 FROM wms_receipt_order
... ... @@ -66,7 +66,7 @@
66 66 </if>
67 67 GROUP BY DATE_FORMAT(order_time, '%Y-%m-%d')
68 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 70 ${@cn.iocoder.yudao.module.wms.enums.order.WmsOrderTypeEnum@SHIPMENT.type} AS orderType,
71 71 COUNT(*) AS count
72 72 FROM wms_shipment_order
... ... @@ -78,7 +78,7 @@
78 78 </if>
79 79 GROUP BY DATE_FORMAT(order_time, '%Y-%m-%d')
80 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 82 ${@cn.iocoder.yudao.module.wms.enums.order.WmsOrderTypeEnum@MOVEMENT.type} AS orderType,
83 83 COUNT(*) AS count
84 84 FROM wms_movement_order
... ... @@ -90,7 +90,7 @@
90 90 </if>
91 91 GROUP BY DATE_FORMAT(order_time, '%Y-%m-%d')
92 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 94 ${@cn.iocoder.yudao.module.wms.enums.order.WmsOrderTypeEnum@CHECK.type} AS orderType,
95 95 COUNT(*) AS count
96 96 FROM wms_check_order
... ... @@ -102,13 +102,13 @@
102 102 </if>
103 103 GROUP BY DATE_FORMAT(order_time, '%Y-%m-%d')
104 104 ) t
105   - GROUP BY date, orderType
106   - ORDER BY date, orderType
  105 + GROUP BY statDate, orderType
  106 + ORDER BY statDate, orderType
107 107 </select>
108 108  
109 109 <!-- 统计库存总数量 -->
110 110 <select id="selectInventoryTotalQuantity" resultType="java.math.BigDecimal">
111   - SELECT IFNULL(SUM(quantity), 0)
  111 + SELECT COALESCE(SUM(quantity), 0)
112 112 FROM wms_inventory
113 113 WHERE deleted = 0
114 114 <if test="warehouseId != null">
... ... @@ -120,7 +120,7 @@
120 120 <select id="selectInventoryItemRank" resultType="cn.iocoder.yudao.framework.mybatis.core.util.CaseInsensitiveResultMap">
121 121 SELECT item.id AS id,
122 122 item.name AS name,
123   - IFNULL(SUM(inventory.quantity), 0) AS quantity
  123 + COALESCE(SUM(inventory.quantity), 0) AS quantity
124 124 FROM wms_inventory inventory
125 125 INNER JOIN wms_item_sku sku ON sku.id = inventory.sku_id AND sku.deleted = 0
126 126 INNER JOIN wms_item item ON item.id = sku.item_id AND item.deleted = 0
... ... @@ -138,7 +138,7 @@
138 138 <select id="selectInventoryWarehouseRank" resultType="cn.iocoder.yudao.framework.mybatis.core.util.CaseInsensitiveResultMap">
139 139 SELECT warehouse.id AS id,
140 140 warehouse.name AS name,
141   - IFNULL(SUM(inventory.quantity), 0) AS quantity
  141 + COALESCE(SUM(inventory.quantity), 0) AS quantity
142 142 FROM wms_inventory inventory
143 143 INNER JOIN wms_warehouse warehouse ON warehouse.id = inventory.warehouse_id AND warehouse.deleted = 0
144 144 WHERE inventory.deleted = 0
... ...