Commit 09aa9ad97f2463a034fe596b2d587194179aa38a

Authored by YunaiV
2 parents ea5fd5d9 173264e2

Merge remote-tracking branch 'origin/master-jdk17' into master-jdk17

# Conflicts:
#	yudao-module-pms/src/test/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemUserSortServiceImplTest.java
Showing 310 changed files with 4866 additions and 809 deletions
yudao-dependencies/pom.xml
... ... @@ -34,6 +34,7 @@
34 34 <kingbase.jdbc.version>9.0.1.jre7</kingbase.jdbc.version>
35 35 <opengauss.jdbc.version>7.0.0-RC3-og</opengauss.jdbc.version>
36 36 <taos.version>3.9.2</taos.version>
  37 + <iotdb.version>2.0.8</iotdb.version> <!-- TODO 特殊:暂不升级 2.0.10/2.0.11,其 TablePreparedStatement.execute 不设置 ResultSet,导致 Mapper 的 SELECT 为空、COUNT 为 null -->
37 38 <!-- 消息队列 -->
38 39 <rocketmq-spring.version>2.3.6</rocketmq-spring.version>
39 40 <!-- 服务保障相关 -->
... ... @@ -58,7 +59,7 @@
58 59 <mapstruct.version>1.7.0.Beta2</mapstruct.version>
59 60 <hutool-5.version>5.8.47</hutool-5.version>
60 61 <hutool-6.version>6.0.0-M22</hutool-6.version>
61   - <fastexcel.version>1.3.0</fastexcel.version>
  62 + <fesod.version>2.1.0-incubating</fesod.version>
62 63 <aviator.version>5.4.4</aviator.version>
63 64 <velocity.version>2.4.1</velocity.version>
64 65 <fastjson2.version>2.0.65</fastjson2.version>
... ... @@ -312,6 +313,12 @@
312 313 <version>${taos.version}</version>
313 314 </dependency>
314 315  
  316 + <dependency>
  317 + <groupId>org.apache.iotdb</groupId>
  318 + <artifactId>iotdb-jdbc</artifactId>
  319 + <version>${iotdb.version}</version>
  320 + </dependency>
  321 +
315 322 <!-- Job 定时任务相关 -->
316 323 <dependency>
317 324 <groupId>cn.iocoder.boot</groupId>
... ... @@ -509,9 +516,9 @@
509 516 </dependency>
510 517  
511 518 <dependency>
512   - <groupId>cn.idev.excel</groupId>
513   - <artifactId>fastexcel</artifactId>
514   - <version>${fastexcel.version}</version>
  519 + <groupId>org.apache.fesod</groupId>
  520 + <artifactId>fesod-sheet</artifactId>
  521 + <version>${fesod.version}</version>
515 522 </dependency>
516 523  
517 524 <dependency>
... ...
yudao-framework/yudao-spring-boot-starter-excel/pom.xml
... ... @@ -42,8 +42,8 @@
42 42  
43 43 <!-- 工具类相关 -->
44 44 <dependency>
45   - <groupId>cn.idev.excel</groupId>
46   - <artifactId>fastexcel</artifactId>
  45 + <groupId>org.apache.fesod</groupId>
  46 + <artifactId>fesod-sheet</artifactId>
47 47 </dependency>
48 48  
49 49 <dependency>
... ...
yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/AreaConvert.java
... ... @@ -3,13 +3,13 @@ package cn.iocoder.yudao.framework.excel.core.convert;
3 3 import cn.hutool.core.convert.Convert;
4 4 import cn.iocoder.yudao.framework.ip.core.Area;
5 5 import cn.iocoder.yudao.framework.ip.core.utils.AreaUtils;
6   -import cn.idev.excel.converters.Converter;
7   -import cn.idev.excel.enums.CellDataTypeEnum;
8   -import cn.idev.excel.metadata.GlobalConfiguration;
9   -import cn.idev.excel.metadata.data.ReadCellData;
10   -import cn.idev.excel.metadata.data.WriteCellData;
11   -import cn.idev.excel.metadata.property.ExcelContentProperty;
12 6 import lombok.extern.slf4j.Slf4j;
  7 +import org.apache.fesod.sheet.converters.Converter;
  8 +import org.apache.fesod.sheet.enums.CellDataTypeEnum;
  9 +import org.apache.fesod.sheet.metadata.GlobalConfiguration;
  10 +import org.apache.fesod.sheet.metadata.data.ReadCellData;
  11 +import org.apache.fesod.sheet.metadata.data.WriteCellData;
  12 +import org.apache.fesod.sheet.metadata.property.ExcelContentProperty;
13 13  
14 14 /**
15 15 * Excel 数据地区转换器
... ...
yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/DictConvert.java
... ... @@ -3,13 +3,13 @@ package cn.iocoder.yudao.framework.excel.core.convert;
3 3 import cn.hutool.core.convert.Convert;
4 4 import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils;
5 5 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
6   -import cn.idev.excel.converters.Converter;
7   -import cn.idev.excel.enums.CellDataTypeEnum;
8   -import cn.idev.excel.metadata.GlobalConfiguration;
9   -import cn.idev.excel.metadata.data.ReadCellData;
10   -import cn.idev.excel.metadata.data.WriteCellData;
11   -import cn.idev.excel.metadata.property.ExcelContentProperty;
12 6 import lombok.extern.slf4j.Slf4j;
  7 +import org.apache.fesod.sheet.converters.Converter;
  8 +import org.apache.fesod.sheet.enums.CellDataTypeEnum;
  9 +import org.apache.fesod.sheet.metadata.GlobalConfiguration;
  10 +import org.apache.fesod.sheet.metadata.data.ReadCellData;
  11 +import org.apache.fesod.sheet.metadata.data.WriteCellData;
  12 +import org.apache.fesod.sheet.metadata.property.ExcelContentProperty;
13 13  
14 14 /**
15 15 * Excel 数据字典转换器
... ...
yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/JsonConvert.java
1 1 package cn.iocoder.yudao.framework.excel.core.convert;
2 2  
3 3 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
4   -import cn.idev.excel.converters.Converter;
5   -import cn.idev.excel.enums.CellDataTypeEnum;
6   -import cn.idev.excel.metadata.GlobalConfiguration;
7   -import cn.idev.excel.metadata.data.WriteCellData;
8   -import cn.idev.excel.metadata.property.ExcelContentProperty;
  4 +import org.apache.fesod.sheet.converters.Converter;
  5 +import org.apache.fesod.sheet.enums.CellDataTypeEnum;
  6 +import org.apache.fesod.sheet.metadata.GlobalConfiguration;
  7 +import org.apache.fesod.sheet.metadata.data.WriteCellData;
  8 +import org.apache.fesod.sheet.metadata.property.ExcelContentProperty;
9 9  
10 10 /**
11 11 * Excel Json 转换器
... ...
yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/MoneyConvert.java
1 1 package cn.iocoder.yudao.framework.excel.core.convert;
2 2  
3   -import cn.idev.excel.converters.Converter;
4   -import cn.idev.excel.enums.CellDataTypeEnum;
5   -import cn.idev.excel.metadata.GlobalConfiguration;
6   -import cn.idev.excel.metadata.data.WriteCellData;
7   -import cn.idev.excel.metadata.property.ExcelContentProperty;
  3 +import org.apache.fesod.sheet.converters.Converter;
  4 +import org.apache.fesod.sheet.enums.CellDataTypeEnum;
  5 +import org.apache.fesod.sheet.metadata.GlobalConfiguration;
  6 +import org.apache.fesod.sheet.metadata.data.WriteCellData;
  7 +import org.apache.fesod.sheet.metadata.property.ExcelContentProperty;
8 8  
9 9 import java.math.BigDecimal;
10 10 import java.math.RoundingMode;
... ...
yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/MultiDictConvert.java
... ... @@ -4,13 +4,13 @@ import cn.hutool.core.convert.Convert;
4 4 import cn.hutool.core.util.StrUtil;
5 5 import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils;
6 6 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
7   -import cn.idev.excel.converters.Converter;
8   -import cn.idev.excel.enums.CellDataTypeEnum;
9   -import cn.idev.excel.metadata.GlobalConfiguration;
10   -import cn.idev.excel.metadata.data.ReadCellData;
11   -import cn.idev.excel.metadata.data.WriteCellData;
12   -import cn.idev.excel.metadata.property.ExcelContentProperty;
13 7 import lombok.extern.slf4j.Slf4j;
  8 +import org.apache.fesod.sheet.converters.Converter;
  9 +import org.apache.fesod.sheet.enums.CellDataTypeEnum;
  10 +import org.apache.fesod.sheet.metadata.GlobalConfiguration;
  11 +import org.apache.fesod.sheet.metadata.data.ReadCellData;
  12 +import org.apache.fesod.sheet.metadata.data.WriteCellData;
  13 +import org.apache.fesod.sheet.metadata.property.ExcelContentProperty;
14 14  
15 15 import java.lang.reflect.Array;
16 16 import java.lang.reflect.Field;
... ...
yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/StringListConvert.java
... ... @@ -2,11 +2,11 @@ package cn.iocoder.yudao.framework.excel.core.convert;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
4 4 import cn.hutool.core.util.StrUtil;
5   -import cn.idev.excel.converters.Converter;
6   -import cn.idev.excel.metadata.GlobalConfiguration;
7   -import cn.idev.excel.metadata.data.ReadCellData;
8   -import cn.idev.excel.metadata.data.WriteCellData;
9   -import cn.idev.excel.metadata.property.ExcelContentProperty;
  5 +import org.apache.fesod.sheet.converters.Converter;
  6 +import org.apache.fesod.sheet.metadata.GlobalConfiguration;
  7 +import org.apache.fesod.sheet.metadata.data.ReadCellData;
  8 +import org.apache.fesod.sheet.metadata.data.WriteCellData;
  9 +import org.apache.fesod.sheet.metadata.property.ExcelContentProperty;
10 10  
11 11 import java.util.Collections;
12 12 import java.util.List;
... ...
yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/handler/ColumnWidthMatchStyleStrategy.java
1 1 package cn.iocoder.yudao.framework.excel.core.handler;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
4   -import cn.idev.excel.enums.CellDataTypeEnum;
5   -import cn.idev.excel.metadata.Head;
6   -import cn.idev.excel.metadata.data.WriteCellData;
7   -import cn.idev.excel.util.MapUtils;
8   -import cn.idev.excel.write.metadata.holder.WriteSheetHolder;
9   -import cn.idev.excel.write.style.column.AbstractColumnWidthStyleStrategy;
10   -import cn.idev.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
  4 +import org.apache.fesod.common.util.MapUtils;
  5 +import org.apache.fesod.sheet.enums.CellDataTypeEnum;
  6 +import org.apache.fesod.sheet.metadata.Head;
  7 +import org.apache.fesod.sheet.metadata.data.WriteCellData;
  8 +import org.apache.fesod.sheet.write.metadata.holder.WriteSheetHolder;
  9 +import org.apache.fesod.sheet.write.style.column.AbstractColumnWidthStyleStrategy;
  10 +import org.apache.fesod.sheet.write.style.column.LongestMatchColumnWidthStyleStrategy;
11 11 import org.apache.poi.ss.usermodel.Cell;
12 12  
13 13 import java.nio.charset.StandardCharsets;
... ...
yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/handler/SelectSheetWriteHandler.java
... ... @@ -10,13 +10,13 @@ import cn.iocoder.yudao.framework.common.core.KeyValue;
10 10 import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils;
11 11 import cn.iocoder.yudao.framework.excel.core.annotations.ExcelColumnSelect;
12 12 import cn.iocoder.yudao.framework.excel.core.function.ExcelColumnSelectFunction;
13   -import cn.idev.excel.annotation.ExcelIgnore;
14   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
15   -import cn.idev.excel.annotation.ExcelProperty;
16   -import cn.idev.excel.write.handler.SheetWriteHandler;
17   -import cn.idev.excel.write.metadata.holder.WriteSheetHolder;
18   -import cn.idev.excel.write.metadata.holder.WriteWorkbookHolder;
19 13 import lombok.extern.slf4j.Slf4j;
  14 +import org.apache.fesod.sheet.annotation.ExcelIgnore;
  15 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  16 +import org.apache.fesod.sheet.annotation.ExcelProperty;
  17 +import org.apache.fesod.sheet.write.handler.SheetWriteHandler;
  18 +import org.apache.fesod.sheet.write.metadata.holder.WriteSheetHolder;
  19 +import org.apache.fesod.sheet.write.metadata.holder.WriteWorkbookHolder;
20 20 import org.apache.poi.hssf.usermodel.HSSFDataValidation;
21 21 import org.apache.poi.ss.usermodel.*;
22 22 import org.apache.poi.ss.util.CellRangeAddressList;
... ... @@ -86,7 +86,7 @@ public class SelectSheetWriteHandler implements SheetWriteHandler {
86 86  
87 87 /**
88 88 * 判断字段是否是静态的、最终的、 transient 的
89   - * 原因:FastExcel 默认是忽略 static final 或 transient 的字段,所以需要判断
  89 + * 原因:Fesod 默认是忽略 static final 或 transient 的字段,所以需要判断
90 90 *
91 91 * @param field 字段
92 92 * @return 是否是静态的、最终的、transient 的
... ...
yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java
1 1 package cn.iocoder.yudao.framework.excel.core.util;
2 2  
3   -import cn.idev.excel.FastExcelFactory;
4   -import cn.idev.excel.context.AnalysisContext;
5   -import cn.idev.excel.converters.longconverter.LongStringConverter;
6   -import cn.idev.excel.read.listener.ReadListener;
7 3 import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
8 4 import cn.iocoder.yudao.framework.excel.core.handler.ColumnWidthMatchStyleStrategy;
9 5 import cn.iocoder.yudao.framework.excel.core.handler.SelectSheetWriteHandler;
10 6 import jakarta.servlet.http.HttpServletResponse;
  7 +import org.apache.fesod.sheet.FesodSheet;
  8 +import org.apache.fesod.sheet.context.AnalysisContext;
  9 +import org.apache.fesod.sheet.converters.Converter;
  10 +import org.apache.fesod.sheet.converters.longconverter.LongStringConverter;
  11 +import org.apache.fesod.sheet.enums.CellDataTypeEnum;
  12 +import org.apache.fesod.sheet.read.listener.ReadListener;
11 13 import org.springframework.web.multipart.MultipartFile;
12 14  
13 15 import java.io.IOException;
... ... @@ -25,6 +27,21 @@ import java.util.Map;
25 27 public class ExcelUtils {
26 28  
27 29 /**
  30 + * Long 转 String 的转换器,避免 Long 类型丢失精度
  31 + *
  32 + * 参考 https://t.zsxq.com/a0w1V 帖子:Fesod 写入 xlsx 时,按 supportExcelTypeKey 为 null 查找全局转换器,
  33 + * 所以需要返回 null,否则 registerConverter 的 LongStringConverter 不生效。关联 https://github.com/apache/fesod/issues/648
  34 + */
  35 + private static final Converter<Long> LONG_STRING_CONVERTER = new LongStringConverter() {
  36 +
  37 + @Override
  38 + public CellDataTypeEnum supportExcelTypeKey() {
  39 + return null;
  40 + }
  41 +
  42 + };
  43 +
  44 + /**
28 45 * 将列表以 Excel 响应给前端
29 46 *
30 47 * @param response 响应
... ... @@ -38,11 +55,11 @@ public class ExcelUtils {
38 55 public static <T> void write(HttpServletResponse response, String filename, String sheetName,
39 56 Class<T> head, List<T> data) throws IOException {
40 57 // 输出 Excel
41   - FastExcelFactory.write(response.getOutputStream(), head)
  58 + FesodSheet.write(response.getOutputStream(), head)
42 59 .autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理
43 60 .registerWriteHandler(new ColumnWidthMatchStyleStrategy()) // 基于 column 长度,自动适配。最大 255 宽度
44 61 .registerWriteHandler(new SelectSheetWriteHandler(head)) // 基于固定 sheet 实现下拉框
45   - .registerConverter(new LongStringConverter()) // 避免 Long 类型丢失精度
  62 + .registerConverter(LONG_STRING_CONVERTER) // 避免 Long 类型丢失精度
46 63 .sheet(sheetName).doWrite(data);
47 64 // 设置 header 和 contentType。写在最后的原因是,避免报错时,响应 contentType 已经被修改了
48 65 response.addHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(filename));
... ... @@ -62,10 +79,10 @@ public class ExcelUtils {
62 79 public static void write(HttpServletResponse response, String filename, String sheetName,
63 80 List<List<String>> head, List<List<Object>> data) throws IOException {
64 81 // 输出 Excel
65   - FastExcelFactory.write(response.getOutputStream())
  82 + FesodSheet.write(response.getOutputStream())
66 83 .autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理
67 84 .registerWriteHandler(new ColumnWidthMatchStyleStrategy()) // 基于 column 长度,自动适配。最大 255 宽度
68   - .registerConverter(new LongStringConverter()) // 避免 Long 类型丢失精度
  85 + .registerConverter(LONG_STRING_CONVERTER) // 避免 Long 类型丢失精度
69 86 .head(head).sheet(sheetName).doWrite(data);
70 87 // 设置 header 和 contentType。写在最后的原因是,避免报错时,响应 contentType 已经被修改了
71 88 response.addHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(filename));
... ... @@ -78,7 +95,7 @@ public class ExcelUtils {
78 95 }
79 96 // 参考 https://t.zsxq.com/zM77F 帖子,增加 try 处理,兼容 windows 场景
80 97 try (InputStream inputStream = file.getInputStream()) {
81   - return FastExcelFactory.read(inputStream, head, null)
  98 + return FesodSheet.read(inputStream, head, null)
82 99 .autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理
83 100 .doReadAllSync();
84 101 }
... ... @@ -101,7 +118,7 @@ public class ExcelUtils {
101 118 }
102 119 List<T> rows = new ArrayList<>(Math.min(maxRowCount, 1024));
103 120 try (InputStream inputStream = file.getInputStream()) {
104   - FastExcelFactory.read(inputStream, head, new ReadListener<T>() {
  121 + FesodSheet.read(inputStream, head, new ReadListener<T>() {
105 122  
106 123 @Override
107 124 public void invoke(T data, AnalysisContext context) {
... ... @@ -136,7 +153,7 @@ public class ExcelUtils {
136 153 }
137 154 // 参考 https://t.zsxq.com/zM77F 帖子,增加 try 处理,兼容 windows 场景
138 155 try (InputStream inputStream = file.getInputStream()) {
139   - return FastExcelFactory.read(inputStream)
  156 + return FesodSheet.read(inputStream)
140 157 .autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理
141 158 .headRowNumber(1)
142 159 .sheet()
... ...
yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/PoiExcelUtils.java
... ... @@ -27,7 +27,7 @@ import java.util.Collection;
27 27 /**
28 28 * 原生 POI 的 Excel 工具类
29 29 *
30   - * 全局 {@link ExcelUtils}(FastExcel)无法表达单元格锁定、公式、动态列等复杂模板时,各模块基于本类实现自定义 Excel 读写
  30 + * 全局 {@link ExcelUtils}(Fesod)无法表达单元格锁定、公式、动态列等复杂模板时,各模块基于本类实现自定义 Excel 读写
31 31 *
32 32 * @author 芋道源码
33 33 */
... ...
yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/package-info.java
1 1 /**
2   - * 基于 FastExcel 实现 Excel 相关的操作
  2 + * 基于 Fesod 实现 Excel 相关的操作
3 3 */
4 4 package cn.iocoder.yudao.framework.excel;
... ...
yudao-framework/yudao-spring-boot-starter-excel/src/test/java/cn/iocoder/yudao/framework/excel/core/convert/MultiDictConvertTest.java
1 1 package cn.iocoder.yudao.framework.excel.core.convert;
2 2  
3 3 import cn.hutool.core.collection.ListUtil;
4   -import cn.idev.excel.metadata.data.ReadCellData;
5   -import cn.idev.excel.metadata.data.WriteCellData;
6   -import cn.idev.excel.metadata.property.ExcelContentProperty;
7 4 import cn.iocoder.yudao.framework.common.biz.system.dict.DictDataCommonApi;
8 5 import cn.iocoder.yudao.framework.common.biz.system.dict.dto.DictDataRespDTO;
9 6 import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils;
10 7 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
11 8 import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
  9 +import org.apache.fesod.sheet.metadata.data.ReadCellData;
  10 +import org.apache.fesod.sheet.metadata.data.WriteCellData;
  11 +import org.apache.fesod.sheet.metadata.property.ExcelContentProperty;
12 12 import org.junit.jupiter.api.BeforeEach;
13 13 import org.junit.jupiter.api.Test;
14 14 import org.mockito.Mock;
... ...
yudao-framework/yudao-spring-boot-starter-excel/src/test/java/cn/iocoder/yudao/framework/excel/core/handler/ColumnWidthMatchStyleStrategyTest.java
1 1 package cn.iocoder.yudao.framework.excel.core.handler;
2 2  
3   -import cn.idev.excel.FastExcelFactory;
4   -import cn.idev.excel.annotation.ExcelProperty;
  3 +import org.apache.fesod.sheet.FesodSheet;
  4 +import org.apache.fesod.sheet.annotation.ExcelProperty;
5 5 import org.apache.poi.xssf.usermodel.XSSFWorkbook;
6 6 import org.junit.jupiter.api.Test;
7 7  
... ... @@ -59,7 +59,7 @@ public class ColumnWidthMatchStyleStrategyTest {
59 59  
60 60 private XSSFWorkbook write(List<TestRow> rows) throws Exception {
61 61 ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
62   - FastExcelFactory.write(outputStream, TestRow.class)
  62 + FesodSheet.write(outputStream, TestRow.class)
63 63 .registerWriteHandler(new ColumnWidthMatchStyleStrategy()).sheet().doWrite(rows);
64 64 return new XSSFWorkbook(new ByteArrayInputStream(outputStream.toByteArray()));
65 65 }
... ...
yudao-framework/yudao-spring-boot-starter-excel/src/test/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtilsTest.java
1 1 package cn.iocoder.yudao.framework.excel.core.util;
2 2  
3   -import cn.idev.excel.FastExcelFactory;
4   -import cn.idev.excel.annotation.ExcelProperty;
  3 +import cn.hutool.core.collection.ListUtil;
  4 +import cn.iocoder.yudao.framework.common.biz.system.dict.DictDataCommonApi;
  5 +import cn.iocoder.yudao.framework.common.biz.system.dict.dto.DictDataRespDTO;
  6 +import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils;
  7 +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
  8 +import cn.iocoder.yudao.framework.excel.core.annotations.ExcelColumnSelect;
  9 +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
  10 +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
  11 +import lombok.Data;
  12 +import lombok.experimental.Accessors;
  13 +import org.apache.fesod.sheet.FesodSheet;
  14 +import org.apache.fesod.sheet.annotation.ExcelProperty;
  15 +import org.apache.poi.ss.usermodel.Cell;
  16 +import org.apache.poi.ss.usermodel.CellType;
  17 +import org.apache.poi.ss.usermodel.Sheet;
  18 +import org.apache.poi.ss.usermodel.Workbook;
  19 +import org.apache.poi.ss.usermodel.WorkbookFactory;
  20 +import org.junit.jupiter.api.BeforeEach;
5 21 import org.junit.jupiter.api.Test;
  22 +import org.mockito.Mock;
  23 +import org.springframework.mock.web.MockHttpServletResponse;
6 24 import org.springframework.mock.web.MockMultipartFile;
7 25  
  26 +import java.io.ByteArrayInputStream;
8 27 import java.io.ByteArrayOutputStream;
9 28 import java.util.Arrays;
  29 +import java.util.Collections;
10 30 import java.util.List;
11 31  
  32 +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
12 33 import static org.junit.jupiter.api.Assertions.assertEquals;
  34 +import static org.junit.jupiter.api.Assertions.assertNotNull;
  35 +import static org.mockito.ArgumentMatchers.eq;
  36 +import static org.mockito.Mockito.when;
13 37  
14 38 /**
15 39 * {@link ExcelUtils} 的单元测试
16 40 *
17 41 * @author 芋道源码
18 42 */
19   -public class ExcelUtilsTest {
  43 +public class ExcelUtilsTest extends BaseMockitoUnitTest {
  44 +
  45 + private static final String DICT_TYPE = "test_status";
  46 +
  47 + @Mock
  48 + private DictDataCommonApi dictDataApi;
  49 +
  50 + @BeforeEach
  51 + public void setUp() {
  52 + DictFrameworkUtils.init(dictDataApi);
  53 + DictFrameworkUtils.clearCache();
  54 + }
  55 +
  56 + /**
  57 + * 字段未配置 converter 的 Long 编号,需要导出为文本,避免丢失精度
  58 + */
  59 + @Test
  60 + public void testWrite_class() throws Exception {
  61 + // mock 数据
  62 + mockDictDatas();
  63 + // 准备参数
  64 + MockHttpServletResponse response = new MockHttpServletResponse();
  65 + List<TestUser> list = Arrays.asList(
  66 + new TestUser().setId(1981234567890123456L).setAge(18).setStatus(0),
  67 + new TestUser().setId(Long.MAX_VALUE).setAge(20).setStatus(1));
  68 +
  69 + // 调用
  70 + ExcelUtils.write(response, "用户.xls", "数据", TestUser.class, list);
  71 + // 断言
  72 + try (Workbook workbook = WorkbookFactory.create(new ByteArrayInputStream(response.getContentAsByteArray()))) {
  73 + Sheet sheet = workbook.getSheet("数据");
  74 + // 断言 Long 编号
  75 + assertStringCell("1981234567890123456", sheet.getRow(1).getCell(0));
  76 + assertStringCell("9223372036854775807", sheet.getRow(2).getCell(0));
  77 + // 断言普通数值
  78 + assertNumericCell(18, sheet.getRow(1).getCell(1));
  79 + assertNumericCell(20, sheet.getRow(2).getCell(1));
  80 + // 断言字典转换
  81 + assertStringCell("开启", sheet.getRow(1).getCell(2));
  82 + assertStringCell("关闭", sheet.getRow(2).getCell(2));
  83 + // 断言下拉框
  84 + assertNotNull(workbook.getSheet("字典sheet"));
  85 + assertEquals(1, sheet.getDataValidations().size());
  86 + // 断言列宽:19 位 Long 字符串,加上 2 个字符的留白
  87 + assertEquals((19 + 2) * 256, sheet.getColumnWidth(0));
  88 + }
  89 + }
  90 +
  91 + @Test
  92 + public void testWrite_dynamicHead() throws Exception {
  93 + // 准备参数
  94 + MockHttpServletResponse response = new MockHttpServletResponse();
  95 + List<List<String>> head = Arrays.asList(Collections.singletonList("编号"), Collections.singletonList("年龄"));
  96 + List<List<Object>> list = Arrays.asList(
  97 + Arrays.asList(1981234567890123456L, 18),
  98 + Arrays.asList(Long.MAX_VALUE, 20));
  99 +
  100 + // 调用
  101 + ExcelUtils.write(response, "用户.xls", "数据", head, list);
  102 + // 断言
  103 + try (Workbook workbook = WorkbookFactory.create(new ByteArrayInputStream(response.getContentAsByteArray()))) {
  104 + Sheet sheet = workbook.getSheet("数据");
  105 + assertStringCell("编号", sheet.getRow(0).getCell(0));
  106 + // 断言 Long 编号
  107 + assertStringCell("1981234567890123456", sheet.getRow(1).getCell(0));
  108 + assertStringCell("9223372036854775807", sheet.getRow(2).getCell(0));
  109 + // 断言普通数值
  110 + assertNumericCell(18, sheet.getRow(1).getCell(1));
  111 + assertNumericCell(20, sheet.getRow(2).getCell(1));
  112 + }
  113 + }
20 114  
21 115 @Test
22 116 public void testRead_maxRowCount() throws Exception {
23 117 // mock 数据
24 118 ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
25   - FastExcelFactory.write(outputStream, TestRow.class).sheet().doWrite(Arrays.asList(
  119 + FesodSheet.write(outputStream, TestRow.class).sheet().doWrite(Arrays.asList(
26 120 new TestRow("员工一"), new TestRow("员工二"), new TestRow("员工三")));
27 121 MockMultipartFile file = new MockMultipartFile(
28 122 "file", "employees.xlsx", null, outputStream.toByteArray());
... ... @@ -36,6 +130,41 @@ public class ExcelUtilsTest {
36 130 assertEquals("员工二", rows.get(1).getName());
37 131 }
38 132  
  133 + private void mockDictDatas() {
  134 + List<DictDataRespDTO> dictDatas = ListUtil.of(
  135 + randomPojo(DictDataRespDTO.class, o -> o.setDictType(DICT_TYPE).setValue("0").setLabel("开启")),
  136 + randomPojo(DictDataRespDTO.class, o -> o.setDictType(DICT_TYPE).setValue("1").setLabel("关闭"))
  137 + );
  138 + when(dictDataApi.getDictDataList(eq(DICT_TYPE))).thenReturn(dictDatas);
  139 + }
  140 +
  141 + private static void assertStringCell(String expected, Cell cell) {
  142 + assertEquals(CellType.STRING, cell.getCellType());
  143 + assertEquals(expected, cell.getStringCellValue());
  144 + }
  145 +
  146 + private static void assertNumericCell(double expected, Cell cell) {
  147 + assertEquals(CellType.NUMERIC, cell.getCellType());
  148 + assertEquals(expected, cell.getNumericCellValue());
  149 + }
  150 +
  151 + @Data
  152 + @Accessors(chain = true)
  153 + public static class TestUser {
  154 +
  155 + @ExcelProperty("编号")
  156 + private Long id;
  157 +
  158 + @ExcelProperty("年龄")
  159 + private Integer age;
  160 +
  161 + @ExcelProperty(value = "状态", converter = DictConvert.class)
  162 + @DictFormat(DICT_TYPE)
  163 + @ExcelColumnSelect(dictType = DICT_TYPE)
  164 + private Integer status;
  165 +
  166 + }
  167 +
39 168 public static class TestRow {
40 169  
41 170 @ExcelProperty("姓名")
... ...
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/config/YudaoMybatisAutoConfiguration.java
... ... @@ -4,11 +4,15 @@ import cn.hutool.core.collection.CollUtil;
4 4 import cn.hutool.core.util.StrUtil;
5 5 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
6 6 import cn.iocoder.yudao.framework.mybatis.core.handler.DefaultDBFieldHandler;
  7 +import cn.iocoder.yudao.framework.mybatis.core.injector.YudaoSqlInjector;
  8 +import cn.iocoder.yudao.framework.mybatis.core.util.CaseInsensitiveResultMap;
7 9 import com.baomidou.mybatisplus.annotation.DbType;
  10 +import com.baomidou.mybatisplus.autoconfigure.ConfigurationCustomizer;
8 11 import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
9 12 import com.baomidou.mybatisplus.core.handlers.IJsonTypeHandler;
10 13 import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
11 14 import com.baomidou.mybatisplus.core.incrementer.IKeyGenerator;
  15 +import com.baomidou.mybatisplus.core.injector.ISqlInjector;
12 16 import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
13 17 import com.baomidou.mybatisplus.extension.incrementer.*;
14 18 import com.baomidou.mybatisplus.extension.parser.JsqlParserGlobal;
... ... @@ -17,12 +21,15 @@ import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
17 21 import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
18 22 import com.fasterxml.jackson.databind.ObjectMapper;
19 23 import org.apache.ibatis.annotations.Mapper;
  24 +import org.apache.ibatis.mapping.ResultMap;
20 25 import org.mybatis.spring.annotation.MapperScan;
21 26 import org.springframework.boot.autoconfigure.AutoConfiguration;
22 27 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
23 28 import org.springframework.context.annotation.Bean;
  29 +import org.springframework.context.annotation.Primary;
24 30 import org.springframework.core.env.ConfigurableEnvironment;
25 31  
  32 +import java.util.ArrayList;
26 33 import java.util.List;
27 34 import java.util.concurrent.TimeUnit;
28 35  
... ... @@ -59,6 +66,19 @@ public class YudaoMybatisAutoConfiguration {
59 66 }
60 67  
61 68 @Bean
  69 + @Primary // 特殊:使用 @Primary 的原因,避免 MyBatis Plus Join 再创建 MPJSqlInjector 包装它
  70 + public ISqlInjector yudaoSqlInjector() {
  71 + return new YudaoSqlInjector(); // selectMaps 的结果行,使用 CaseInsensitiveResultMap
  72 + }
  73 +
  74 + @Bean
  75 + public ConfigurationCustomizer caseInsensitiveResultMapConfigurationCustomizer() {
  76 + // 注册 CaseInsensitiveResultMap 的 resultMap,用于注解 SQL 的 @ResultMap
  77 + return configuration -> configuration.addResultMap(new ResultMap.Builder(configuration,
  78 + CaseInsensitiveResultMap.RESULT_MAP_ID, CaseInsensitiveResultMap.class, new ArrayList<>()).build());
  79 + }
  80 +
  81 + @Bean
62 82 @ConditionalOnProperty(prefix = "mybatis-plus.global-config.db-config", name = "id-type", havingValue = "INPUT")
63 83 public IKeyGenerator keyGenerator(ConfigurableEnvironment environment) {
64 84 DbType dbType = IdTypeEnvironmentPostProcessor.getDbType(environment);
... ...
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/injector/YudaoSqlInjector.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.mybatis.core.injector;
  2 +
  3 +import cn.iocoder.yudao.framework.mybatis.core.util.CaseInsensitiveResultMap;
  4 +import com.baomidou.mybatisplus.core.injector.AbstractMethod;
  5 +import com.baomidou.mybatisplus.core.metadata.TableInfo;
  6 +import com.github.yulichang.injector.MPJSqlInjector;
  7 +import com.github.yulichang.method.mp.SelectMaps;
  8 +import org.apache.ibatis.mapping.MappedStatement;
  9 +import org.apache.ibatis.mapping.SqlSource;
  10 +import org.apache.ibatis.session.Configuration;
  11 +
  12 +import java.util.List;
  13 +
  14 +/**
  15 + * 自定义 SQL 注入器
  16 + *
  17 + * 在 MyBatis Plus Join 的 {@link MPJSqlInjector} 基础上,将 selectMaps 的结果行替换为 {@link CaseInsensitiveResultMap},
  18 + * 解决 PostgreSQL、Oracle 等数据库列标签大小写折叠,导致按固定 key 取不到值的问题
  19 + *
  20 + * @author 芋道源码
  21 + */
  22 +public class YudaoSqlInjector extends MPJSqlInjector {
  23 +
  24 + @Override
  25 + public List<AbstractMethod> getMethodList(Configuration configuration, Class<?> mapperClass, TableInfo tableInfo) {
  26 + List<AbstractMethod> methods = super.getMethodList(configuration, mapperClass, tableInfo);
  27 + methods.replaceAll(method -> method instanceof SelectMaps ? new CaseInsensitiveSelectMaps() : method);
  28 + return methods;
  29 + }
  30 +
  31 + /**
  32 + * 结果行为 {@link CaseInsensitiveResultMap} 的 selectMaps
  33 + *
  34 + * 仅修改结果类型,SQL 仍由 MyBatis Plus Join 的 {@link SelectMaps} 生成,兼容 MPJ 的 Wrapper
  35 + */
  36 + private static class CaseInsensitiveSelectMaps extends SelectMaps {
  37 +
  38 + @Override
  39 + protected MappedStatement addSelectMappedStatementForOther(Class<?> mapperClass, String id, SqlSource sqlSource,
  40 + Class<?> resultType) {
  41 + return super.addSelectMappedStatementForOther(mapperClass, id, sqlSource, CaseInsensitiveResultMap.class);
  42 + }
  43 +
  44 + }
  45 +
  46 +}
... ...
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java
... ... @@ -6,6 +6,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
6 6 import cn.iocoder.yudao.framework.common.pojo.SortablePageParam;
7 7 import cn.iocoder.yudao.framework.common.pojo.SortingField;
8 8 import cn.iocoder.yudao.framework.common.util.object.PageUtils;
  9 +import cn.iocoder.yudao.framework.mybatis.core.util.CaseInsensitiveResultMap;
9 10 import cn.iocoder.yudao.framework.mybatis.core.util.JdbcUtils;
10 11 import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
11 12 import com.baomidou.mybatisplus.annotation.DbType;
... ... @@ -14,6 +15,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
14 15 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
15 16 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
16 17 import com.baomidou.mybatisplus.core.metadata.IPage;
  18 +import com.baomidou.mybatisplus.core.toolkit.Constants;
17 19 import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
18 20 import com.baomidou.mybatisplus.extension.toolkit.Db;
19 21 import com.github.yulichang.base.MPJBaseMapper;
... ... @@ -23,6 +25,7 @@ import org.apache.ibatis.annotations.Param;
23 25  
24 26 import java.util.Collection;
25 27 import java.util.List;
  28 +import java.util.Map;
26 29  
27 30 /**
28 31 * 在 MyBatis Plus 的 BaseMapper 的基础上拓展,提供更多的能力
... ... @@ -99,6 +102,24 @@ public interface BaseMapperX&lt;T&gt; extends MPJBaseMapper&lt;T&gt; {
99 102 return new PageResult<>(mpPage.getRecords(), mpPage.getTotal());
100 103 }
101 104  
  105 + @Override
  106 + @SuppressWarnings("unchecked")
  107 + default Map<String, Object> selectJoinMap(@Param(Constants.WRAPPER) MPJBaseJoin<T> wrapper) {
  108 + return selectJoinOne(CaseInsensitiveResultMap.class, wrapper);
  109 + }
  110 +
  111 + @Override
  112 + @SuppressWarnings("unchecked")
  113 + default List<Map<String, Object>> selectJoinMaps(@Param(Constants.WRAPPER) MPJBaseJoin<T> wrapper) {
  114 + return (List<Map<String, Object>>) ((Object) selectJoinList(CaseInsensitiveResultMap.class, wrapper));
  115 + }
  116 +
  117 + @Override
  118 + @SuppressWarnings({"unchecked", "rawtypes"})
  119 + default <P extends IPage<Map<String, Object>>> P selectJoinMapsPage(P page, @Param(Constants.WRAPPER) MPJBaseJoin<T> wrapper) {
  120 + return (P) selectJoinPage((IPage) page, CaseInsensitiveResultMap.class, wrapper);
  121 + }
  122 +
102 123 default T selectOne(String field, Object value) {
103 124 return selectOne(new QueryWrapper<T>().eq(field, value));
104 125 }
... ...
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/util/CaseInsensitiveResultMap.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.mybatis.core.util;
  2 +
  3 +import org.springframework.util.LinkedCaseInsensitiveMap;
  4 +
  5 +import java.util.Locale;
  6 +
  7 +/**
  8 + * key 大小写不敏感的 MyBatis 查询结果 Map
  9 + *
  10 + * 背景:使用 Map 接收查询结果时,key 为 JDBC 返回的列标签。PostgreSQL 等会将未加引号的列名、别名折叠为小写,
  11 + * Oracle、达梦等会折叠为大写,导致 map.get("conversationId") 这类按固定 key 取值的代码,在 MySQL 以外的数据库取不到值。
  12 + *
  13 + * 实现:基于 {@link LinkedCaseInsensitiveMap},get / containsKey / remove 时忽略 key 的大小写,
  14 + * 同时保留原始的列标签作为 key(遍历、序列化时不变)。固定使用 {@link Locale#ROOT},避免服务器默认 Locale(例如土耳其语)影响 key 的匹配。
  15 + *
  16 + * 使用方式:由 MyBatis 直接创建结果行,Mapper 的返回类型仍声明为 Map,业务无需感知该类
  17 + * 1. MyBatis Plus 的 selectMaps:由 YudaoSqlInjector 注入,自动使用该类
  18 + * 2. MyBatis Plus Join 的 selectJoinMaps 等:由 BaseMapperX 覆盖,自动使用该类
  19 + * 3. XML SQL:将 resultType 设置为该类
  20 + * 4. 注解 SQL:添加 @ResultMap({@link #RESULT_MAP_ID})
  21 + *
  22 + * 注意:仅大小写不同的多个 key,会合并为一个,保留后写入的 key 和值
  23 + *
  24 + * @author 芋道源码
  25 + * @see <a href="https://t.zsxq.com/kPvqm">星球讨论:Map 查询结果的跨数据库兼容问题</a>
  26 + */
  27 +public class CaseInsensitiveResultMap<V> extends LinkedCaseInsensitiveMap<V> {
  28 +
  29 + /**
  30 + * 注解 SQL 使用的 resultMap 编号
  31 + *
  32 + * 由 YudaoMybatisAutoConfiguration 注册到 MyBatis 全局配置;包含“.”,所以不会拼接 Mapper 的 namespace
  33 + */
  34 + public static final String RESULT_MAP_ID = "cn.iocoder.yudao.framework.mybatis.core.util.CaseInsensitiveResultMap";
  35 +
  36 + /**
  37 + * 创建空的结果 Map
  38 + *
  39 + * 用途:MyBatis 通过无参构造方法,实例化结果行
  40 + */
  41 + public CaseInsensitiveResultMap() {
  42 + super(Locale.ROOT);
  43 + }
  44 +
  45 +}
... ...
yudao-framework/yudao-spring-boot-starter-mybatis/src/test/java/cn/iocoder/yudao/framework/mybatis/core/util/CaseInsensitiveResultMapTest.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.mybatis.core.util;
  2 +
  3 +import cn.hutool.core.map.MapUtil;
  4 +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
  5 +import org.junit.jupiter.api.Test;
  6 +import org.springframework.util.LinkedCaseInsensitiveMap;
  7 +
  8 +import java.util.*;
  9 +
  10 +import static org.junit.jupiter.api.Assertions.*;
  11 +
  12 +/**
  13 + * {@link CaseInsensitiveResultMap} 的单元测试
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +public class CaseInsensitiveResultMapTest {
  18 +
  19 + @Test
  20 + public void testGet_ignoreCase() {
  21 + // 准备参数
  22 + CaseInsensitiveResultMap<Object> map = new CaseInsensitiveResultMap<>();
  23 + map.put("conversationId", 1024L);
  24 +
  25 + // 调用,并断言:PostgreSQL 小写、Oracle 大写,都可以取到值
  26 + assertEquals(1024L, map.get("conversationId"));
  27 + assertEquals(1024L, map.get("conversationid"));
  28 + assertEquals(1024L, map.get("CONVERSATIONID"));
  29 + assertTrue(map.containsKey("CONVERSATIONID"));
  30 + assertEquals(1024L, MapUtil.getLong(map, "conversationid"));
  31 + // 断言:保留原始的 key
  32 + assertEquals(Collections.singletonList("conversationId"), new ArrayList<>(map.keySet()));
  33 + }
  34 +
  35 + @Test
  36 + public void testPut_nullValue() {
  37 + // 准备参数
  38 + CaseInsensitiveResultMap<Object> map = new CaseInsensitiveResultMap<>();
  39 + map.put("sum_count", null);
  40 +
  41 + // 调用,并断言
  42 + assertTrue(map.containsKey("SUM_COUNT"));
  43 + assertNull(map.get("SUM_COUNT"));
  44 + assertEquals(0, MapUtil.getInt(map, "SUM_COUNT", 0));
  45 + }
  46 +
  47 + @Test
  48 + public void testPut_sameKeyIgnoreCase() {
  49 + // 准备参数
  50 + CaseInsensitiveResultMap<Object> map = new CaseInsensitiveResultMap<>();
  51 +
  52 + // 调用:仅大小写不同的 key
  53 + map.put("id", 1L);
  54 + map.put("ID", 2L);
  55 +
  56 + // 断言:合并为一个,保留后写入的 key 和值
  57 + assertEquals(1, map.size());
  58 + assertEquals(2L, map.get("id"));
  59 + assertEquals(Collections.singletonList("ID"), new ArrayList<>(map.keySet()));
  60 + }
  61 +
  62 + @Test
  63 + public void testGet_turkishDefaultLocale() {
  64 + Locale defaultLocale = Locale.getDefault();
  65 + try {
  66 + // 准备参数:土耳其语下,大写 I 转小写为无点的 ı
  67 + Locale.setDefault(Locale.forLanguageTag("tr-TR"));
  68 + CaseInsensitiveResultMap<Object> map = new CaseInsensitiveResultMap<>();
  69 + map.put("CONVERSATIONID", 1024L);
  70 + LinkedCaseInsensitiveMap<Object> defaultLocaleMap = new LinkedCaseInsensitiveMap<>();
  71 + defaultLocaleMap.put("CONVERSATIONID", 1024L);
  72 +
  73 + // 调用,并断言:固定 Locale.ROOT,不受默认 Locale 影响
  74 + assertEquals(1024L, map.get("conversationId"));
  75 + // 断言:使用默认 Locale 时,取不到值
  76 + assertNull(defaultLocaleMap.get("conversationId"));
  77 + } finally {
  78 + Locale.setDefault(defaultLocale);
  79 + }
  80 + }
  81 +
  82 + @Test
  83 + public void testConvertMap_numberKey() {
  84 + // 准备参数:模拟 Oracle 返回的结果行
  85 + List<Map<String, Object>> rows = Arrays.asList(buildRow(1024L, 3), buildRow(2048L, 5));
  86 +
  87 + // 调用:业务转换为数字 key 的 Map
  88 + Map<Long, Integer> result = CollectionUtils.convertMap(rows,
  89 + row -> MapUtil.getLong(row, "conversationId"),
  90 + row -> MapUtil.getInt(row, "count"));
  91 +
  92 + // 断言:数字 key 的业务 Map 不受影响
  93 + assertInstanceOf(HashMap.class, result);
  94 + assertEquals(3, result.get(1024L));
  95 + assertEquals(5, result.get(2048L));
  96 + }
  97 +
  98 + private static Map<String, Object> buildRow(Long conversationId, Integer count) {
  99 + CaseInsensitiveResultMap<Object> row = new CaseInsensitiveResultMap<>();
  100 + row.put("CONVERSATIONID", conversationId);
  101 + row.put("COUNT", count);
  102 + return row;
  103 + }
  104 +
  105 +}
... ...
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImplTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.service.chat;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  5 +import cn.iocoder.yudao.framework.security.core.LoginUser;
  6 +import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
  7 +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
  8 +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
  9 +import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendReqVO;
  10 +import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendRespVO;
  11 +import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO;
  12 +import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatMessageDO;
  13 +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO;
  14 +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO;
  15 +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiToolDO;
  16 +import cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatMessageMapper;
  17 +import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
  18 +import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeDocumentService;
  19 +import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService;
  20 +import cn.iocoder.yudao.module.ai.service.model.AiModelService;
  21 +import cn.iocoder.yudao.module.ai.service.model.AiToolService;
  22 +import lombok.Data;
  23 +import org.junit.jupiter.api.AfterEach;
  24 +import org.junit.jupiter.api.Test;
  25 +import org.mockito.ArgumentCaptor;
  26 +import org.mockito.InjectMocks;
  27 +import org.mockito.Mock;
  28 +import org.springframework.ai.chat.model.ToolContext;
  29 +import org.springframework.ai.deepseek.DeepSeekChatModel;
  30 +import org.springframework.ai.deepseek.api.DeepSeekApi;
  31 +import org.springframework.ai.deepseek.api.DeepSeekApi.ChatCompletion;
  32 +import org.springframework.ai.deepseek.api.DeepSeekApi.ChatCompletionChunk;
  33 +import org.springframework.ai.deepseek.api.DeepSeekApi.ChatCompletionFinishReason;
  34 +import org.springframework.ai.deepseek.api.DeepSeekApi.ChatCompletionMessage;
  35 +import org.springframework.ai.deepseek.api.DeepSeekApi.ChatCompletionMessage.ChatCompletionFunction;
  36 +import org.springframework.ai.deepseek.api.DeepSeekApi.ChatCompletionMessage.Role;
  37 +import org.springframework.ai.deepseek.api.DeepSeekApi.ChatCompletionMessage.ToolCall;
  38 +import org.springframework.ai.deepseek.api.DeepSeekApi.ChatCompletionRequest;
  39 +import org.springframework.ai.tool.ToolCallback;
  40 +import org.springframework.ai.tool.function.FunctionToolCallback;
  41 +import org.springframework.ai.tool.resolution.ToolCallbackResolver;
  42 +import org.springframework.http.ResponseEntity;
  43 +import org.springframework.mock.web.MockHttpServletRequest;
  44 +import org.springframework.security.core.context.SecurityContextHolder;
  45 +import reactor.core.publisher.Flux;
  46 +
  47 +import java.util.List;
  48 +import java.util.Map;
  49 +import java.util.concurrent.CopyOnWriteArrayList;
  50 +import java.util.concurrent.atomic.AtomicLong;
  51 +import java.util.stream.Collectors;
  52 +
  53 +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
  54 +import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.CHAT_STREAM_ERROR;
  55 +import static cn.iocoder.yudao.module.ai.util.AiUtils.TOOL_CONTEXT_LOGIN_USER;
  56 +import static cn.iocoder.yudao.module.ai.util.AiUtils.TOOL_CONTEXT_TENANT_ID;
  57 +import static java.util.Arrays.asList;
  58 +import static java.util.Collections.singletonList;
  59 +import static org.junit.jupiter.api.Assertions.*;
  60 +import static org.mockito.ArgumentMatchers.any;
  61 +import static org.mockito.ArgumentMatchers.eq;
  62 +import static org.mockito.Mockito.*;
  63 +
  64 +/**
  65 + * {@link AiChatMessageServiceImpl} 的单元测试类
  66 + *
  67 + * 使用真实的 {@link DeepSeekChatModel} 和工具调用,仅 mock 底层的 {@link DeepSeekApi}
  68 + *
  69 + * @author 芋道源码
  70 + */
  71 +public class AiChatMessageServiceImplTest extends BaseMockitoUnitTest {
  72 +
  73 + private static final String MODEL_NAME = "deepseek-test";
  74 + private static final Double TEMPERATURE = 0.5;
  75 + private static final Integer MAX_TOKENS = 1024;
  76 +
  77 + private static final String TOOL_NAME = "query_weather";
  78 + private static final String TOOL_RESULT = "杭州:晴,25℃";
  79 + private static final ChatCompletionMessage TOOL_CALL_MESSAGE = new ChatCompletionMessage("我来查询一下杭州的天气",
  80 + Role.ASSISTANT, null, null, singletonList(new ToolCall("call_1", "function",
  81 + new ChatCompletionFunction(TOOL_NAME, "{\"city\":\"杭州\"}"))));
  82 +
  83 + private static final List<String> TOOL_ANSWER_CHUNKS = asList("杭州今天", "晴,25℃");
  84 + private static final String TOOL_ANSWER = String.join("", TOOL_ANSWER_CHUNKS);
  85 + private static final List<String> PLAIN_ANSWER_CHUNKS = asList("你好,", "我是 AI 助手");
  86 + private static final String PLAIN_ANSWER = String.join("", PLAIN_ANSWER_CHUNKS);
  87 +
  88 + /**
  89 + * assistant 消息编号:先插入 user 消息(编号 1),再插入 assistant 消息(编号 2)
  90 + */
  91 + private static final Long ASSISTANT_MESSAGE_ID = 2L;
  92 +
  93 + @InjectMocks
  94 + private AiChatMessageServiceImpl chatMessageService;
  95 +
  96 + @Mock
  97 + private AiChatMessageMapper chatMessageMapper;
  98 + @Mock
  99 + private AiChatConversationService chatConversationService;
  100 + @Mock
  101 + private AiChatRoleService chatRoleService;
  102 + @Mock
  103 + private AiModelService modalService;
  104 + @Mock
  105 + private AiKnowledgeDocumentService knowledgeDocumentService;
  106 + @Mock
  107 + private AiToolService toolService;
  108 + @Mock
  109 + private ToolCallbackResolver toolCallbackResolver;
  110 + @Mock
  111 + private DeepSeekApi deepSeekApi;
  112 +
  113 + @AfterEach
  114 + public void after() {
  115 + SecurityContextHolder.clearContext();
  116 + TenantContextHolder.clear();
  117 + }
  118 +
  119 + @Test
  120 + public void testSendMessage_toolCall() {
  121 + // 准备参数
  122 + Long userId = randomLongId();
  123 + Long roleId = randomLongId();
  124 + AiChatMessageSendReqVO reqVO = new AiChatMessageSendReqVO().setConversationId(randomLongId())
  125 + .setContent("杭州天气怎么样?");
  126 + // mock 对话、模型
  127 + mockConversation(reqVO.getConversationId(), userId, roleId);
  128 + // mock 工具
  129 + List<ToolContext> toolContexts = new CopyOnWriteArrayList<>();
  130 + mockWeatherTool(roleId, toolContexts);
  131 + // mock 登录用户、租户
  132 + Long tenantId = randomLongId();
  133 + mockLoginUser(userId, tenantId);
  134 + // mock deepSeekApi 的方法:先请求工具,再返回回答
  135 + when(deepSeekApi.chatCompletionEntity(any())).thenReturn(buildChatCompletion(TOOL_CALL_MESSAGE),
  136 + buildChatCompletion(new ChatCompletionMessage(TOOL_ANSWER, Role.ASSISTANT)));
  137 +
  138 + // 调用
  139 + AiChatMessageSendRespVO respVO = chatMessageService.sendMessage(reqVO, userId);
  140 + // 断言
  141 + assertEquals(TOOL_ANSWER, respVO.getReceive().getContent());
  142 + ArgumentCaptor<ChatCompletionRequest> requestCaptor = ArgumentCaptor.forClass(ChatCompletionRequest.class);
  143 + verify(deepSeekApi, times(2)).chatCompletionEntity(requestCaptor.capture());
  144 + assertToolExecuted(requestCaptor.getAllValues(), toolContexts, userId, tenantId);
  145 + // 断言模型参数
  146 + ChatCompletionRequest request = requestCaptor.getAllValues().get(0);
  147 + assertEquals(MODEL_NAME, request.model());
  148 + assertEquals(TEMPERATURE, request.temperature());
  149 + assertEquals(MAX_TOKENS, request.maxTokens());
  150 + // 断言消息保存
  151 + assertAssistantMessageUpdated(TOOL_ANSWER);
  152 + }
  153 +
  154 + @Test
  155 + public void testSendMessage_withoutTool() {
  156 + // 准备参数
  157 + Long userId = randomLongId();
  158 + AiChatMessageSendReqVO reqVO = new AiChatMessageSendReqVO().setConversationId(randomLongId())
  159 + .setContent("你好");
  160 + // mock 对话、模型
  161 + mockConversation(reqVO.getConversationId(), userId, null);
  162 + // mock deepSeekApi 的方法
  163 + when(deepSeekApi.chatCompletionEntity(any()))
  164 + .thenReturn(buildChatCompletion(new ChatCompletionMessage(PLAIN_ANSWER, Role.ASSISTANT)));
  165 +
  166 + // 调用
  167 + AiChatMessageSendRespVO respVO = chatMessageService.sendMessage(reqVO, userId);
  168 + // 断言
  169 + assertEquals(PLAIN_ANSWER, respVO.getReceive().getContent());
  170 + verify(deepSeekApi).chatCompletionEntity(any());
  171 + // 断言消息保存
  172 + assertAssistantMessageUpdated(PLAIN_ANSWER);
  173 + }
  174 +
  175 + @Test
  176 + public void testSendMessage_toolCallThenModelError() {
  177 + // 准备参数
  178 + Long userId = randomLongId();
  179 + Long roleId = randomLongId();
  180 + AiChatMessageSendReqVO reqVO = new AiChatMessageSendReqVO().setConversationId(randomLongId())
  181 + .setContent("杭州天气怎么样?");
  182 + // mock 对话、模型
  183 + mockConversation(reqVO.getConversationId(), userId, roleId);
  184 + // mock 工具
  185 + List<ToolContext> toolContexts = new CopyOnWriteArrayList<>();
  186 + mockWeatherTool(roleId, toolContexts);
  187 + // mock 登录用户、租户
  188 + mockLoginUser(userId, randomLongId());
  189 + // mock deepSeekApi 的方法:先请求工具,再抛出异常
  190 + RuntimeException modelException = new IllegalStateException("模型服务异常");
  191 + when(deepSeekApi.chatCompletionEntity(any())).thenReturn(buildChatCompletion(TOOL_CALL_MESSAGE))
  192 + .thenThrow(modelException);
  193 +
  194 + // 调用,并断言异常
  195 + RuntimeException exception = assertThrows(RuntimeException.class,
  196 + () -> chatMessageService.sendMessage(reqVO, userId));
  197 + assertSame(modelException, exception);
  198 + // 断言工具已执行、消息未保存
  199 + verify(deepSeekApi, times(2)).chatCompletionEntity(any());
  200 + assertEquals(1, toolContexts.size());
  201 + verify(chatMessageMapper, never()).updateById(any(AiChatMessageDO.class));
  202 + }
  203 +
  204 + @Test
  205 + public void testSendChatMessageStream_toolCall() {
  206 + // 准备参数
  207 + Long userId = randomLongId();
  208 + Long roleId = randomLongId();
  209 + AiChatMessageSendReqVO reqVO = new AiChatMessageSendReqVO().setConversationId(randomLongId())
  210 + .setContent("杭州天气怎么样?");
  211 + // mock 对话、模型
  212 + mockConversation(reqVO.getConversationId(), userId, roleId);
  213 + // mock 工具
  214 + List<ToolContext> toolContexts = new CopyOnWriteArrayList<>();
  215 + mockWeatherTool(roleId, toolContexts);
  216 + // mock 登录用户、租户
  217 + Long tenantId = randomLongId();
  218 + mockLoginUser(userId, tenantId);
  219 + // mock deepSeekApi 的方法:先请求工具,再流式返回回答
  220 + when(deepSeekApi.chatCompletionStream(any())).thenReturn(Flux.just(buildChatCompletionChunk(TOOL_CALL_MESSAGE)),
  221 + buildAnswerChunks(TOOL_ANSWER_CHUNKS));
  222 +
  223 + // 调用
  224 + List<CommonResult<AiChatMessageSendRespVO>> results = chatMessageService
  225 + .sendChatMessageStream(reqVO, userId).collectList().block();
  226 + // 断言(工具调用轮次不下发)
  227 + assertNotNull(results);
  228 + assertTrue(results.stream().allMatch(CommonResult::isSuccess));
  229 + assertEquals(TOOL_ANSWER, joinContent(results));
  230 + ArgumentCaptor<ChatCompletionRequest> requestCaptor = ArgumentCaptor.forClass(ChatCompletionRequest.class);
  231 + verify(deepSeekApi, times(2)).chatCompletionStream(requestCaptor.capture());
  232 + assertToolExecuted(requestCaptor.getAllValues(), toolContexts, userId, tenantId);
  233 + // 断言消息保存
  234 + assertAssistantMessageUpdated(TOOL_ANSWER);
  235 + }
  236 +
  237 + @Test
  238 + public void testSendChatMessageStream_withoutTool() {
  239 + // 准备参数
  240 + Long userId = randomLongId();
  241 + AiChatMessageSendReqVO reqVO = new AiChatMessageSendReqVO().setConversationId(randomLongId())
  242 + .setContent("你好");
  243 + // mock 对话、模型
  244 + mockConversation(reqVO.getConversationId(), userId, null);
  245 + // mock deepSeekApi 的方法
  246 + when(deepSeekApi.chatCompletionStream(any())).thenReturn(buildAnswerChunks(PLAIN_ANSWER_CHUNKS));
  247 +
  248 + // 调用
  249 + List<CommonResult<AiChatMessageSendRespVO>> results = chatMessageService
  250 + .sendChatMessageStream(reqVO, userId).collectList().block();
  251 + // 断言
  252 + assertNotNull(results);
  253 + assertTrue(results.stream().allMatch(CommonResult::isSuccess));
  254 + assertEquals(PLAIN_ANSWER, joinContent(results));
  255 + verify(deepSeekApi).chatCompletionStream(any());
  256 + // 断言消息保存
  257 + assertAssistantMessageUpdated(PLAIN_ANSWER);
  258 + }
  259 +
  260 + @Test
  261 + public void testSendChatMessageStream_toolCallThenModelError() {
  262 + // 准备参数
  263 + Long userId = randomLongId();
  264 + Long roleId = randomLongId();
  265 + AiChatMessageSendReqVO reqVO = new AiChatMessageSendReqVO().setConversationId(randomLongId())
  266 + .setContent("杭州天气怎么样?");
  267 + // mock 对话、模型
  268 + mockConversation(reqVO.getConversationId(), userId, roleId);
  269 + // mock 工具
  270 + List<ToolContext> toolContexts = new CopyOnWriteArrayList<>();
  271 + mockWeatherTool(roleId, toolContexts);
  272 + // mock 登录用户、租户
  273 + mockLoginUser(userId, randomLongId());
  274 + // mock deepSeekApi 的方法:先请求工具,再返回异常
  275 + when(deepSeekApi.chatCompletionStream(any())).thenReturn(Flux.just(buildChatCompletionChunk(TOOL_CALL_MESSAGE)),
  276 + Flux.error(new IllegalStateException("模型服务异常")));
  277 +
  278 + // 调用
  279 + List<CommonResult<AiChatMessageSendRespVO>> results = chatMessageService
  280 + .sendChatMessageStream(reqVO, userId).collectList().block();
  281 + // 断言
  282 + assertNotNull(results);
  283 + assertEquals(CHAT_STREAM_ERROR.getCode(), CollUtil.getLast(results).getCode());
  284 + verify(deepSeekApi, times(2)).chatCompletionStream(any());
  285 + assertEquals(1, toolContexts.size());
  286 + // 断言消息删除(无内容)
  287 + verify(chatMessageMapper).deleteById(ASSISTANT_MESSAGE_ID);
  288 + verify(chatMessageMapper, never()).updateById(any(AiChatMessageDO.class));
  289 + }
  290 +
  291 + private void mockConversation(Long conversationId, Long userId, Long roleId) {
  292 + // mock 对话
  293 + AiChatConversationDO conversation = new AiChatConversationDO().setId(conversationId).setUserId(userId)
  294 + .setRoleId(roleId).setModelId(randomLongId()).setTemperature(TEMPERATURE).setMaxTokens(MAX_TOKENS);
  295 + when(chatConversationService.validateChatConversationExists(eq(conversationId))).thenReturn(conversation);
  296 + // mock 模型:真实的 DeepSeekChatModel
  297 + AiModelDO model = new AiModelDO().setId(conversation.getModelId()).setModel(MODEL_NAME)
  298 + .setPlatform(AiPlatformEnum.DEEP_SEEK.getPlatform());
  299 + when(modalService.validateModel(eq(model.getId()))).thenReturn(model);
  300 + when(modalService.getChatModel(eq(model.getId())))
  301 + .thenReturn(DeepSeekChatModel.builder().deepSeekApi(deepSeekApi).build());
  302 + // mock 消息插入:依次分配编号
  303 + AtomicLong messageId = new AtomicLong();
  304 + doAnswer(invocation -> {
  305 + invocation.<AiChatMessageDO>getArgument(0).setId(messageId.incrementAndGet());
  306 + return 1;
  307 + }).when(chatMessageMapper).insert(any(AiChatMessageDO.class));
  308 + }
  309 +
  310 + private void mockWeatherTool(Long roleId, List<ToolContext> toolContexts) {
  311 + // mock 工具:真实的 FunctionToolCallback
  312 + Long toolId = randomLongId();
  313 + ToolCallback toolCallback = FunctionToolCallback.<WeatherRequest, String>builder(TOOL_NAME,
  314 + (request, toolContext) -> {
  315 + toolContexts.add(toolContext);
  316 + return request.getCity() + ":晴,25℃";
  317 + }).description("查询城市天气").inputType(WeatherRequest.class).build();
  318 + when(chatRoleService.getChatRole(eq(roleId))).thenReturn(new AiChatRoleDO().setId(roleId)
  319 + .setToolIds(singletonList(toolId)));
  320 + when(toolService.getToolList(eq(singletonList(toolId))))
  321 + .thenReturn(singletonList(new AiToolDO().setId(toolId).setName(TOOL_NAME)));
  322 + when(toolCallbackResolver.resolve(eq(TOOL_NAME))).thenReturn(toolCallback);
  323 + }
  324 +
  325 + private static void mockLoginUser(Long userId, Long tenantId) {
  326 + SecurityFrameworkUtils.setLoginUser(new LoginUser().setId(userId), new MockHttpServletRequest());
  327 + TenantContextHolder.setTenantId(tenantId);
  328 + }
  329 +
  330 + private static void assertToolExecuted(List<ChatCompletionRequest> requests, List<ToolContext> toolContexts,
  331 + Long userId, Long tenantId) {
  332 + // 断言首轮请求带上工具
  333 + assertEquals(1, requests.get(0).tools().size());
  334 + // 断言工具执行,并拿到登录用户、租户
  335 + assertEquals(1, toolContexts.size());
  336 + Map<String, Object> context = toolContexts.get(0).getContext();
  337 + assertEquals(userId, ((LoginUser) context.get(TOOL_CONTEXT_LOGIN_USER)).getId());
  338 + assertEquals(tenantId, context.get(TOOL_CONTEXT_TENANT_ID));
  339 + // 断言次轮请求回传工具结果
  340 + ChatCompletionMessage toolMessage = CollUtil.getLast(requests.get(1).messages());
  341 + assertEquals(Role.TOOL, toolMessage.role());
  342 + assertTrue(toolMessage.content().contains(TOOL_RESULT));
  343 + }
  344 +
  345 + private void assertAssistantMessageUpdated(String content) {
  346 + ArgumentCaptor<AiChatMessageDO> messageCaptor = ArgumentCaptor.forClass(AiChatMessageDO.class);
  347 + verify(chatMessageMapper).updateById(messageCaptor.capture());
  348 + assertEquals(ASSISTANT_MESSAGE_ID, messageCaptor.getValue().getId());
  349 + assertEquals(content, messageCaptor.getValue().getContent());
  350 + }
  351 +
  352 + private static String joinContent(List<CommonResult<AiChatMessageSendRespVO>> results) {
  353 + return results.stream().map(result -> result.getData().getReceive().getContent())
  354 + .collect(Collectors.joining());
  355 + }
  356 +
  357 + private static ResponseEntity<ChatCompletion> buildChatCompletion(ChatCompletionMessage message) {
  358 + ChatCompletion.Choice choice = new ChatCompletion.Choice(buildFinishReason(message), 0, message, null);
  359 + return ResponseEntity.ok(new ChatCompletion("chatcmpl-1", singletonList(choice), null, MODEL_NAME,
  360 + null, null, null));
  361 + }
  362 +
  363 + private static Flux<ChatCompletionChunk> buildAnswerChunks(List<String> contents) {
  364 + return Flux.fromIterable(contents)
  365 + .map(content -> buildChatCompletionChunk(new ChatCompletionMessage(content, Role.ASSISTANT)));
  366 + }
  367 +
  368 + private static ChatCompletionChunk buildChatCompletionChunk(ChatCompletionMessage delta) {
  369 + ChatCompletionChunk.ChunkChoice choice = new ChatCompletionChunk.ChunkChoice(buildFinishReason(delta),
  370 + 0, delta, null);
  371 + return new ChatCompletionChunk("chatcmpl-1", singletonList(choice), null, MODEL_NAME,
  372 + null, null, null, null);
  373 + }
  374 +
  375 + private static ChatCompletionFinishReason buildFinishReason(ChatCompletionMessage message) {
  376 + return CollUtil.isNotEmpty(message.toolCalls()) ? ChatCompletionFinishReason.TOOL_CALLS
  377 + : ChatCompletionFinishReason.STOP;
  378 + }
  379 +
  380 + @Data
  381 + public static class WeatherRequest {
  382 +
  383 + private String city;
  384 +
  385 + }
  386 +
  387 +}
... ...
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/expression/BpmProcessExpressionRespVO.java
1 1 package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.expression;
2 2  
3   -import cn.idev.excel.annotation.ExcelProperty;
4 3 import io.swagger.v3.oas.annotations.media.Schema;
5 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelProperty;
6 6  
7 7 import java.time.LocalDateTime;
8 8  
... ...
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/business/CrmBusinessRespVO.java
1 1 package cn.iocoder.yudao.module.crm.controller.admin.business.vo.business;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.AllArgsConstructor;
7 5 import lombok.Data;
8 6 import lombok.NoArgsConstructor;
  7 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  8 +import org.apache.fesod.sheet.annotation.ExcelProperty;
9 9  
10 10 import java.math.BigDecimal;
11 11 import java.time.LocalDateTime;
... ...
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueRespVO.java
... ... @@ -3,11 +3,11 @@ package cn.iocoder.yudao.module.crm.controller.admin.clue.vo;
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.module.infra.enums.DictTypeConstants;
6   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
7   -import cn.idev.excel.annotation.ExcelProperty;
8 6 import io.swagger.v3.oas.annotations.media.Schema;
9 7 import lombok.Data;
10 8 import lombok.ToString;
  9 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  10 +import org.apache.fesod.sheet.annotation.ExcelProperty;
11 11  
12 12 import java.time.LocalDateTime;
13 13  
... ...
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactRespVO.java
... ... @@ -3,11 +3,11 @@ package cn.iocoder.yudao.module.crm.controller.admin.contact.vo;
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.module.infra.enums.DictTypeConstants;
6   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
7   -import cn.idev.excel.annotation.ExcelProperty;
8 6 import io.swagger.v3.oas.annotations.media.Schema;
9 7 import lombok.Data;
10 8 import lombok.ToString;
  9 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  10 +import org.apache.fesod.sheet.annotation.ExcelProperty;
11 11  
12 12 import java.time.LocalDateTime;
13 13  
... ...
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/contract/CrmContractRespVO.java
1 1 package cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.AllArgsConstructor;
7 5 import lombok.Data;
8 6 import lombok.NoArgsConstructor;
  7 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  8 +import org.apache.fesod.sheet.annotation.ExcelProperty;
9 9  
10 10 import java.math.BigDecimal;
11 11 import java.time.LocalDateTime;
... ...
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/customer/CrmCustomerImportExcelVO.java
... ... @@ -5,11 +5,11 @@ import cn.iocoder.yudao.framework.excel.core.annotations.ExcelColumnSelect;
5 5 import cn.iocoder.yudao.framework.excel.core.convert.AreaConvert;
6 6 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
7 7 import cn.iocoder.yudao.module.crm.framework.excel.core.AreaExcelColumnSelectFunction;
8   -import cn.idev.excel.annotation.ExcelProperty;
9 8 import lombok.AllArgsConstructor;
10 9 import lombok.Builder;
11 10 import lombok.Data;
12 11 import lombok.NoArgsConstructor;
  12 +import org.apache.fesod.sheet.annotation.ExcelProperty;
13 13  
14 14 import static cn.iocoder.yudao.module.crm.enums.DictTypeConstants.*;
15 15  
... ...
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/customer/CrmCustomerRespVO.java
... ... @@ -3,10 +3,10 @@ package cn.iocoder.yudao.module.crm.controller.admin.customer.vo.customer;
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.module.infra.enums.DictTypeConstants;
6   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
7   -import cn.idev.excel.annotation.ExcelProperty;
8 6 import io.swagger.v3.oas.annotations.media.Schema;
9 7 import lombok.Data;
  8 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  9 +import org.apache.fesod.sheet.annotation.ExcelProperty;
10 10  
11 11 import java.time.LocalDateTime;
12 12  
... ...
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/followup/vo/CrmFollowUpRecordRespVO.java
... ... @@ -2,10 +2,10 @@ package cn.iocoder.yudao.module.crm.controller.admin.followup.vo;
2 2  
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusinessRespVO;
5   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
6   -import cn.idev.excel.annotation.ExcelProperty;
7 5 import io.swagger.v3.oas.annotations.media.Schema;
8 6 import lombok.Data;
  7 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  8 +import org.apache.fesod.sheet.annotation.ExcelProperty;
9 9  
10 10 import java.time.LocalDateTime;
11 11 import java.util.List;
... ...
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/operatelog/vo/CrmOperateLogRespVO.java
1 1 package cn.iocoder.yudao.module.crm.controller.admin.operatelog.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4 3 import io.swagger.v3.oas.annotations.media.Schema;
5 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
6 6  
7 7 import java.time.LocalDateTime;
8 8  
... ...
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/category/CrmProductCategoryListReqVO.java
1 1 package cn.iocoder.yudao.module.crm.controller.admin.product.vo.category;
2 2  
3   -import cn.idev.excel.annotation.ExcelProperty;
4 3 import io.swagger.v3.oas.annotations.media.Schema;
5 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelProperty;
6 6  
7 7 import java.time.LocalDateTime;
8 8  
... ...
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/product/CrmProductRespVO.java
... ... @@ -4,8 +4,8 @@ import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.module.crm.dal.dataobject.product.CrmProductCategoryDO;
6 6 import cn.iocoder.yudao.module.crm.enums.DictTypeConstants;
7   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
8   -import cn.idev.excel.annotation.ExcelProperty;
  7 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  8 +import org.apache.fesod.sheet.annotation.ExcelProperty;
9 9 import org.dromara.core.trans.anno.Trans;
10 10 import org.dromara.core.trans.constant.TransType;
11 11 import org.dromara.core.trans.vo.VO;
... ...
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/plan/CrmReceivablePlanRespVO.java
1 1 package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.plan;
2 2  
3 3 import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable.CrmReceivableRespVO;
4   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
5   -import cn.idev.excel.annotation.ExcelProperty;
6 4 import io.swagger.v3.oas.annotations.media.Schema;
7 5 import lombok.Data;
  6 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  7 +import org.apache.fesod.sheet.annotation.ExcelProperty;
8 8  
9 9 import java.math.BigDecimal;
10 10 import java.time.LocalDateTime;
... ...
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/receivable/CrmReceivableRespVO.java
... ... @@ -4,10 +4,10 @@ import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract.CrmContractRespVO;
6 6 import cn.iocoder.yudao.module.crm.enums.DictTypeConstants;
7   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
8   -import cn.idev.excel.annotation.ExcelProperty;
9 7 import io.swagger.v3.oas.annotations.media.Schema;
10 8 import lombok.Data;
  9 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  10 +import org.apache.fesod.sheet.annotation.ExcelProperty;
11 11  
12 12 import java.math.BigDecimal;
13 13 import java.time.LocalDateTime;
... ...
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/account/ErpAccountRespVO.java
... ... @@ -2,10 +2,10 @@ package cn.iocoder.yudao.module.erp.controller.admin.finance.vo.account;
2 2  
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.module.system.enums.DictTypeConstants;
5   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
6   -import cn.idev.excel.annotation.ExcelProperty;
7 5 import io.swagger.v3.oas.annotations.media.Schema;
8 6 import lombok.Data;
  7 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  8 +import org.apache.fesod.sheet.annotation.ExcelProperty;
9 9  
10 10 import java.time.LocalDateTime;
11 11  
... ...
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/payment/ErpFinancePaymentRespVO.java
1 1 package cn.iocoder.yudao.module.erp.controller.admin.finance.vo.payment;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import jakarta.validation.constraints.NotNull;
7 5 import lombok.Data;
  6 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  7 +import org.apache.fesod.sheet.annotation.ExcelProperty;
8 8  
9 9 import java.math.BigDecimal;
10 10 import java.time.LocalDateTime;
... ...
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/receipt/ErpFinanceReceiptRespVO.java
1 1 package cn.iocoder.yudao.module.erp.controller.admin.finance.vo.receipt;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import jakarta.validation.constraints.NotNull;
7 5 import lombok.Data;
  6 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  7 +import org.apache.fesod.sheet.annotation.ExcelProperty;
8 8  
9 9 import java.math.BigDecimal;
10 10 import java.time.LocalDateTime;
... ...
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/product/vo/category/ErpProductCategoryRespVO.java
... ... @@ -3,10 +3,10 @@ package cn.iocoder.yudao.module.erp.controller.admin.product.vo.category;
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.module.system.enums.DictTypeConstants;
6   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
7   -import cn.idev.excel.annotation.ExcelProperty;
8 6 import io.swagger.v3.oas.annotations.media.Schema;
9 7 import lombok.Data;
  8 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  9 +import org.apache.fesod.sheet.annotation.ExcelProperty;
10 10  
11 11 import java.time.LocalDateTime;
12 12  
... ...
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/product/vo/product/ErpProductRespVO.java
1 1 package cn.iocoder.yudao.module.erp.controller.admin.product.vo.product;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  6 +import org.apache.fesod.sheet.annotation.ExcelProperty;
7 7  
8 8 import java.math.BigDecimal;
9 9 import java.time.LocalDateTime;
... ...
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/product/vo/unit/ErpProductUnitRespVO.java
... ... @@ -2,10 +2,10 @@ package cn.iocoder.yudao.module.erp.controller.admin.product.vo.unit;
2 2  
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.module.system.enums.DictTypeConstants;
5   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
6   -import cn.idev.excel.annotation.ExcelProperty;
7 5 import io.swagger.v3.oas.annotations.media.Schema;
8 6 import lombok.Data;
  7 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  8 +import org.apache.fesod.sheet.annotation.ExcelProperty;
9 9  
10 10 import java.time.LocalDateTime;
11 11  
... ...
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/in/ErpPurchaseInRespVO.java
1 1 package cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.in;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import jakarta.validation.constraints.NotNull;
7 5 import lombok.Data;
  6 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  7 +import org.apache.fesod.sheet.annotation.ExcelProperty;
8 8  
9 9 import java.math.BigDecimal;
10 10 import java.time.LocalDateTime;
... ...
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderRespVO.java
1 1 package cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import jakarta.validation.constraints.NotNull;
7 5 import lombok.Data;
  6 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  7 +import org.apache.fesod.sheet.annotation.ExcelProperty;
8 8  
9 9 import java.math.BigDecimal;
10 10 import java.time.LocalDateTime;
... ...
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/returns/ErpPurchaseReturnRespVO.java
1 1 package cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.returns;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import jakarta.validation.constraints.NotNull;
7 5 import lombok.Data;
  6 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  7 +import org.apache.fesod.sheet.annotation.ExcelProperty;
8 8  
9 9 import java.math.BigDecimal;
10 10 import java.time.LocalDateTime;
... ...
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/supplier/ErpSupplierRespVO.java
... ... @@ -3,10 +3,10 @@ package cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.supplier;
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.module.system.enums.DictTypeConstants;
6   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
7   -import cn.idev.excel.annotation.ExcelProperty;
8 6 import io.swagger.v3.oas.annotations.media.Schema;
9 7 import lombok.Data;
  8 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  9 +import org.apache.fesod.sheet.annotation.ExcelProperty;
10 10  
11 11 import java.math.BigDecimal;
12 12 import java.time.LocalDateTime;
... ...
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/customer/ErpCustomerRespVO.java
1 1 package cn.iocoder.yudao.module.erp.controller.admin.sale.vo.customer;
2 2  
  3 +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
  4 +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
3 5 import io.swagger.v3.oas.annotations.media.Schema;
4 6 import lombok.*;
  7 +import org.apache.fesod.sheet.annotation.*;
  8 +
5 9 import java.math.BigDecimal;
6 10 import java.time.LocalDateTime;
7   -import cn.idev.excel.annotation.*;
8   -import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
9   -import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
10 11  
11 12 @Schema(description = "管理后台 - ERP 客户 Response VO")
12 13 @Data
... ...
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderRespVO.java
1 1 package cn.iocoder.yudao.module.erp.controller.admin.sale.vo.order;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import jakarta.validation.constraints.NotNull;
7 5 import lombok.Data;
  6 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  7 +import org.apache.fesod.sheet.annotation.ExcelProperty;
8 8  
9 9 import java.math.BigDecimal;
10 10 import java.time.LocalDateTime;
... ...
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/out/ErpSaleOutRespVO.java
1 1 package cn.iocoder.yudao.module.erp.controller.admin.sale.vo.out;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import jakarta.validation.constraints.NotNull;
7 5 import lombok.Data;
  6 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  7 +import org.apache.fesod.sheet.annotation.ExcelProperty;
8 8  
9 9 import java.math.BigDecimal;
10 10 import java.time.LocalDateTime;
... ...
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/returns/ErpSaleReturnRespVO.java
1 1 package cn.iocoder.yudao.module.erp.controller.admin.sale.vo.returns;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import jakarta.validation.constraints.NotNull;
7 5 import lombok.Data;
  6 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  7 +import org.apache.fesod.sheet.annotation.ExcelProperty;
8 8  
9 9 import java.math.BigDecimal;
10 10 import java.time.LocalDateTime;
... ...
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/check/ErpStockCheckRespVO.java
... ... @@ -2,11 +2,11 @@ package cn.iocoder.yudao.module.erp.controller.admin.stock.vo.check;
2 2  
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
6   -import cn.idev.excel.annotation.ExcelProperty;
7 5 import io.swagger.v3.oas.annotations.media.Schema;
8 6 import jakarta.validation.constraints.NotNull;
9 7 import lombok.Data;
  8 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  9 +import org.apache.fesod.sheet.annotation.ExcelProperty;
10 10  
11 11 import java.math.BigDecimal;
12 12 import java.time.LocalDateTime;
... ...
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/in/ErpStockInRespVO.java
... ... @@ -2,10 +2,10 @@ package cn.iocoder.yudao.module.erp.controller.admin.stock.vo.in;
2 2  
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
6   -import cn.idev.excel.annotation.ExcelProperty;
7 5 import io.swagger.v3.oas.annotations.media.Schema;
8 6 import lombok.Data;
  7 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  8 +import org.apache.fesod.sheet.annotation.ExcelProperty;
9 9  
10 10 import java.math.BigDecimal;
11 11 import java.time.LocalDateTime;
... ...
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/move/ErpStockMoveRespVO.java
... ... @@ -2,10 +2,10 @@ package cn.iocoder.yudao.module.erp.controller.admin.stock.vo.move;
2 2  
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
6   -import cn.idev.excel.annotation.ExcelProperty;
7 5 import io.swagger.v3.oas.annotations.media.Schema;
8 6 import lombok.Data;
  7 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  8 +import org.apache.fesod.sheet.annotation.ExcelProperty;
9 9  
10 10 import java.math.BigDecimal;
11 11 import java.time.LocalDateTime;
... ...
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/out/ErpStockOutRespVO.java
... ... @@ -2,10 +2,10 @@ package cn.iocoder.yudao.module.erp.controller.admin.stock.vo.out;
2 2  
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
6   -import cn.idev.excel.annotation.ExcelProperty;
7 5 import io.swagger.v3.oas.annotations.media.Schema;
8 6 import lombok.Data;
  7 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  8 +import org.apache.fesod.sheet.annotation.ExcelProperty;
9 9  
10 10 import java.math.BigDecimal;
11 11 import java.time.LocalDateTime;
... ...
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/record/ErpStockRecordRespVO.java
... ... @@ -3,10 +3,10 @@ package cn.iocoder.yudao.module.erp.controller.admin.stock.vo.record;
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.module.erp.enums.DictTypeConstants;
6   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
7   -import cn.idev.excel.annotation.ExcelProperty;
8 6 import io.swagger.v3.oas.annotations.media.Schema;
9 7 import lombok.Data;
  8 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  9 +import org.apache.fesod.sheet.annotation.ExcelProperty;
10 10  
11 11 import java.math.BigDecimal;
12 12 import java.time.LocalDateTime;
... ...
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/stock/ErpStockRespVO.java
1 1 package cn.iocoder.yudao.module.erp.controller.admin.stock.vo.stock;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  6 +import org.apache.fesod.sheet.annotation.ExcelProperty;
7 7  
8 8 import java.math.BigDecimal;
9 9  
... ...
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/warehouse/ErpWarehouseRespVO.java
... ... @@ -3,10 +3,10 @@ package cn.iocoder.yudao.module.erp.controller.admin.stock.vo.warehouse;
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.module.system.enums.DictTypeConstants;
6   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
7   -import cn.idev.excel.annotation.ExcelProperty;
8 6 import io.swagger.v3.oas.annotations.media.Schema;
9 7 import lombok.Data;
  8 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  9 +import org.apache.fesod.sheet.annotation.ExcelProperty;
10 10  
11 11 import java.math.BigDecimal;
12 12 import java.time.LocalDateTime;
... ...
yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/auxiliaryitem/FmsAuxiliaryItemImportExcelVO.java
1 1 package cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliaryitem;
2 2  
3   -import cn.idev.excel.annotation.ExcelProperty;
4 3 import jakarta.validation.constraints.NotBlank;
5 4 import jakarta.validation.constraints.Size;
6 5 import lombok.Data;
  6 +import org.apache.fesod.sheet.annotation.ExcelProperty;
7 7  
8 8 /**
9 9 * FMS 辅助核算项目 Excel 导入 VO
... ...
yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/auxiliaryitem/FmsAuxiliaryItemRespVO.java
1 1 package cn.iocoder.yudao.module.fms.controller.admin.config.vo.auxiliaryitem;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  6 +import org.apache.fesod.sheet.annotation.ExcelProperty;
7 7  
8 8 import java.time.LocalDateTime;
9 9  
... ...
yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/initialbalance/FmsInitialBalanceExcelVO.java
1 1 package cn.iocoder.yudao.module.fms.controller.admin.config.vo.initialbalance;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import lombok.Data;
  4 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  5 +import org.apache.fesod.sheet.annotation.ExcelProperty;
6 6  
7 7 import java.math.BigDecimal;
8 8  
... ...
yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/subject/FmsSubjectImportExcelVO.java
1 1 package cn.iocoder.yudao.module.fms.controller.admin.config.vo.subject;
2 2  
3   -import cn.idev.excel.annotation.ExcelProperty;
4 3 import jakarta.validation.constraints.NotBlank;
5 4 import lombok.AllArgsConstructor;
6 5 import lombok.Builder;
7 6 import lombok.Data;
8 7 import lombok.NoArgsConstructor;
  8 +import org.apache.fesod.sheet.annotation.ExcelProperty;
9 9  
10 10 /**
11 11 * FMS 科目 Excel 导入 VO
... ...
yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/config/vo/subject/FmsSubjectRespVO.java
... ... @@ -4,11 +4,11 @@ import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.framework.excel.core.convert.StringListConvert;
6 6 import cn.iocoder.yudao.module.fms.enums.DictTypeConstants;
7   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
8   -import cn.idev.excel.annotation.ExcelProperty;
9 7 import com.fasterxml.jackson.annotation.JsonIgnore;
10 8 import io.swagger.v3.oas.annotations.media.Schema;
11 9 import lombok.Data;
  10 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  11 +import org.apache.fesod.sheet.annotation.ExcelProperty;
12 12  
13 13 import java.time.LocalDateTime;
14 14 import java.util.List;
... ...
yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/vo/auxiliarybalance/FmsLedgerAuxiliaryBalanceRespVO.java
1 1 package cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.auxiliarybalance;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.Data;
7 5 import lombok.experimental.Accessors;
  6 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  7 +import org.apache.fesod.sheet.annotation.ExcelProperty;
8 8  
9 9 import java.math.BigDecimal;
10 10  
... ...
yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/vo/auxiliarydetail/FmsLedgerAuxiliaryDetailRespVO.java
1 1 package cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.auxiliarydetail;
2 2  
3 3 import com.fasterxml.jackson.annotation.JsonFormat;
4   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
5   -import cn.idev.excel.annotation.ExcelProperty;
6 4 import io.swagger.v3.oas.annotations.media.Schema;
7 5 import lombok.Data;
  6 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  7 +import org.apache.fesod.sheet.annotation.ExcelProperty;
8 8  
9 9 import java.math.BigDecimal;
10 10 import java.time.LocalDate;
... ...
yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/vo/detail/FmsLedgerDetailExportRespVO.java
1 1 package cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.detail;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import lombok.Data;
  4 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  5 +import org.apache.fesod.sheet.annotation.ExcelProperty;
6 6  
7 7 import java.math.BigDecimal;
8 8 import java.time.LocalDate;
... ...
yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/vo/detail/FmsLedgerDetailRespVO.java
1 1 package cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.detail;
2 2  
3 3 import com.fasterxml.jackson.annotation.JsonFormat;
4   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
5   -import cn.idev.excel.annotation.ExcelProperty;
6 4 import io.swagger.v3.oas.annotations.media.Schema;
7 5 import lombok.Data;
  6 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  7 +import org.apache.fesod.sheet.annotation.ExcelProperty;
8 8  
9 9 import java.math.BigDecimal;
10 10 import java.time.LocalDate;
... ...
yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/vo/general/FmsLedgerGeneralRespVO.java
1 1 package cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.general;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  6 +import org.apache.fesod.sheet.annotation.ExcelProperty;
7 7  
8 8 import java.math.BigDecimal;
9 9  
... ...
yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/vo/quantitydetail/FmsLedgerQuantityDetailRespVO.java
1 1 package cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.quantitydetail;
2 2  
3 3 import com.fasterxml.jackson.annotation.JsonFormat;
4   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
5   -import cn.idev.excel.annotation.ExcelProperty;
6 4 import io.swagger.v3.oas.annotations.media.Schema;
7 5 import lombok.Data;
  6 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  7 +import org.apache.fesod.sheet.annotation.ExcelProperty;
8 8  
9 9 import java.math.BigDecimal;
10 10 import java.time.LocalDate;
... ...
yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/vo/quantitygeneral/FmsLedgerQuantityGeneralRespVO.java
1 1 package cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.quantitygeneral;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnore;
4   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
5   -import cn.idev.excel.annotation.ExcelProperty;
6 3 import io.swagger.v3.oas.annotations.media.Schema;
7 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelIgnore;
  6 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  7 +import org.apache.fesod.sheet.annotation.ExcelProperty;
8 8  
9 9 import java.math.BigDecimal;
10 10 import java.util.ArrayList;
... ...
yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/ledger/vo/subjectbalance/FmsLedgerSubjectBalanceRespVO.java
1 1 package cn.iocoder.yudao.module.fms.controller.admin.ledger.vo.subjectbalance;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.Data;
7 5 import lombok.experimental.Accessors;
  6 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  7 +import org.apache.fesod.sheet.annotation.ExcelProperty;
8 8  
9 9 import java.math.BigDecimal;
10 10 import java.util.ArrayList;
... ...
yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/report/vo/FmsReportItemRespVO.java
1 1 package cn.iocoder.yudao.module.fms.controller.admin.report.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  6 +import org.apache.fesod.sheet.annotation.ExcelProperty;
7 7  
8 8 import java.math.BigDecimal;
9 9  
... ...
yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/report/vo/balance/FmsBalanceSheetRowRespVO.java
1 1 package cn.iocoder.yudao.module.fms.controller.admin.report.vo.balance;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  6 +import org.apache.fesod.sheet.annotation.ExcelProperty;
7 7  
8 8 import java.math.BigDecimal;
9 9  
... ...
yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherExportRespVO.java
1 1 package cn.iocoder.yudao.module.fms.controller.admin.voucher.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5   -import cn.idev.excel.annotation.format.DateTimeFormat;
6 3 import io.swagger.v3.oas.annotations.media.Schema;
7 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  6 +import org.apache.fesod.sheet.annotation.ExcelProperty;
  7 +import org.apache.fesod.sheet.annotation.format.DateTimeFormat;
8 8  
9 9 import java.math.BigDecimal;
10 10 import java.time.LocalDateTime;
... ...
yudao-module-fms/src/main/java/cn/iocoder/yudao/module/fms/controller/admin/voucher/vo/FmsVoucherStatisticsRespVO.java
1 1 package cn.iocoder.yudao.module.fms.controller.admin.voucher.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  6 +import org.apache.fesod.sheet.annotation.ExcelProperty;
7 7  
8 8 import java.math.BigDecimal;
9 9  
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/clock/HrmAttendanceClockRespVO.java
1 1 package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.clock;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
6 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
7 5 import cn.iocoder.yudao.module.hrm.enums.DictTypeConstants;
8 6 import io.swagger.v3.oas.annotations.media.Schema;
9 7 import lombok.Data;
  8 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  9 +import org.apache.fesod.sheet.annotation.ExcelProperty;
10 10  
11 11 import java.math.BigDecimal;
12 12 import java.time.LocalDateTime;
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/leave/HrmAttendanceLeaveRespVO.java
1 1 package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.leave;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
6 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
7 5 import cn.iocoder.yudao.module.hrm.enums.DictTypeConstants;
8 6 import io.swagger.v3.oas.annotations.media.Schema;
9 7 import lombok.Data;
  8 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  9 +import org.apache.fesod.sheet.annotation.ExcelProperty;
10 10  
11 11 import java.math.BigDecimal;
12 12 import java.time.LocalDateTime;
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/statistics/HrmAttendanceDailyDetailRespVO.java
1 1 package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.statistics;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.clock.HrmAttendanceClockRespVO;
6 4 import io.swagger.v3.oas.annotations.media.Schema;
7 5 import lombok.Data;
  6 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  7 +import org.apache.fesod.sheet.annotation.ExcelProperty;
8 8  
9 9 import java.math.BigDecimal;
10 10 import java.time.LocalDateTime;
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/statistics/HrmAttendanceMonthRecordRespVO.java
1 1 package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.statistics;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  6 +import org.apache.fesod.sheet.annotation.ExcelProperty;
7 7  
8 8 import java.math.BigDecimal;
9 9 import java.time.LocalDateTime;
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeImportExcelVO.java
1 1 package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import cn.iocoder.yudao.framework.excel.core.annotations.ExcelColumnSelect;
6 4 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
7 5 import cn.iocoder.yudao.framework.excel.core.convert.AreaConvert;
... ... @@ -14,6 +12,8 @@ import lombok.AllArgsConstructor;
14 12 import lombok.Builder;
15 13 import lombok.Data;
16 14 import lombok.NoArgsConstructor;
  15 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  16 +import org.apache.fesod.sheet.annotation.ExcelProperty;
17 17  
18 18 import java.time.LocalDateTime;
19 19  
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/employee/HrmEmployeeRespVO.java
... ... @@ -3,11 +3,11 @@ package cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.employee;
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.module.hrm.enums.DictTypeConstants;
6   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
7   -import cn.idev.excel.annotation.ExcelProperty;
8 6 import io.swagger.v3.oas.annotations.media.Schema;
9 7 import lombok.Data;
10 8 import lombok.experimental.Accessors;
  9 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  10 +import org.apache.fesod.sheet.annotation.ExcelProperty;
11 11  
12 12 import java.time.LocalDateTime;
13 13  
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/changerecord/HrmSalaryChangeRecordRespVO.java
1 1 package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.changerecord;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
6 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
7 5 import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.option.HrmSalaryOptionValueVO;
8 6 import cn.iocoder.yudao.module.hrm.enums.DictTypeConstants;
9 7 import io.swagger.v3.oas.annotations.media.Schema;
10 8 import lombok.Data;
  9 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  10 +import org.apache.fesod.sheet.annotation.ExcelProperty;
11 11  
12 12 import java.math.BigDecimal;
13 13 import java.time.LocalDateTime;
... ...
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/employeeinfo/HrmSalaryEmployeeInfoRespVO.java
1 1 package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.employeeinfo;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
6 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
7 5 import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.option.HrmSalaryOptionValueVO;
8 6 import cn.iocoder.yudao.module.hrm.enums.DictTypeConstants;
9 7 import io.swagger.v3.oas.annotations.media.Schema;
10 8 import lombok.Data;
  9 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  10 +import org.apache.fesod.sheet.annotation.ExcelProperty;
11 11  
12 12 import java.math.BigDecimal;
13 13 import java.time.LocalDateTime;
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/statistics/ImStatisticsManagerMapper.java
1 1 package cn.iocoder.yudao.module.im.dal.mysql.statistics;
2 2  
  3 +import cn.iocoder.yudao.framework.mybatis.core.util.CaseInsensitiveResultMap;
3 4 import org.apache.ibatis.annotations.Mapper;
4 5 import org.apache.ibatis.annotations.Param;
  6 +import org.apache.ibatis.annotations.ResultMap;
5 7 import org.apache.ibatis.annotations.Select;
6 8  
7 9 import java.time.LocalDateTime;
... ... @@ -59,6 +61,7 @@ public interface ImStatisticsManagerMapper {
59 61 @Select("SELECT DATE(create_time) AS date, COUNT(*) AS count FROM system_users " +
60 62 "WHERE deleted = 0 AND create_time >= #{beginTime} AND create_time < #{endTime} " +
61 63 "GROUP BY DATE(create_time)")
  64 + @ResultMap(CaseInsensitiveResultMap.RESULT_MAP_ID)
62 65 List<Map<String, Object>> selectNewUserDailyCount(@Param("beginTime") LocalDateTime beginTime,
63 66 @Param("endTime") LocalDateTime endTime);
64 67  
... ... @@ -74,6 +77,7 @@ public interface ImStatisticsManagerMapper {
74 77 " WHERE deleted = 0 AND " + NORMAL_MESSAGE_CONDITION +
75 78 " AND send_time >= #{beginTime} AND send_time < #{endTime}" +
76 79 ") t GROUP BY day")
  80 + @ResultMap(CaseInsensitiveResultMap.RESULT_MAP_ID)
77 81 List<Map<String, Object>> selectActiveUserDailyCount(@Param("beginTime") LocalDateTime beginTime, @Param("endTime") LocalDateTime endTime);
78 82  
79 83 // ==================== 群 ====================
... ... @@ -111,6 +115,7 @@ public interface ImStatisticsManagerMapper {
111 115 " GROUP BY g.id" +
112 116 ") t " +
113 117 "GROUP BY `range`")
  118 + @ResultMap(CaseInsensitiveResultMap.RESULT_MAP_ID)
114 119 List<Map<String, Object>> selectGroupSizeDistribution();
115 120  
116 121 // ==================== 消息 ====================
... ... @@ -140,6 +145,7 @@ public interface ImStatisticsManagerMapper {
140 145 "WHERE deleted = 0 AND " + NORMAL_MESSAGE_CONDITION +
141 146 " AND send_time >= #{beginTime} AND send_time < #{endTime} " +
142 147 "GROUP BY DATE(send_time)")
  148 + @ResultMap(CaseInsensitiveResultMap.RESULT_MAP_ID)
143 149 List<Map<String, Object>> selectPrivateMessageDailyCount(@Param("beginTime") LocalDateTime beginTime,
144 150 @Param("endTime") LocalDateTime endTime);
145 151  
... ... @@ -150,6 +156,7 @@ public interface ImStatisticsManagerMapper {
150 156 "WHERE deleted = 0 AND " + NORMAL_MESSAGE_CONDITION +
151 157 " AND send_time >= #{beginTime} AND send_time < #{endTime} " +
152 158 "GROUP BY DATE(send_time)")
  159 + @ResultMap(CaseInsensitiveResultMap.RESULT_MAP_ID)
153 160 List<Map<String, Object>> selectGroupMessageDailyCount(@Param("beginTime") LocalDateTime beginTime,
154 161 @Param("endTime") LocalDateTime endTime);
155 162  
... ... @@ -167,6 +174,7 @@ public interface ImStatisticsManagerMapper {
167 174 " WHERE deleted = 0 AND " + NORMAL_MESSAGE_CONDITION +
168 175 " AND send_time >= #{beginTime} AND send_time < #{endTime}" +
169 176 ") t GROUP BY type")
  177 + @ResultMap(CaseInsensitiveResultMap.RESULT_MAP_ID)
170 178 List<Map<String, Object>> selectMessageTypeDistribution(@Param("beginTime") LocalDateTime beginTime,
171 179 @Param("endTime") LocalDateTime endTime);
172 180  
... ... @@ -184,6 +192,7 @@ public interface ImStatisticsManagerMapper {
184 192 " WHERE deleted = 0 AND " + NORMAL_MESSAGE_CONDITION +
185 193 " AND send_time >= #{beginTime} AND send_time < #{endTime}" +
186 194 ") t GROUP BY user_id ORDER BY messageCount DESC LIMIT #{limit}")
  195 + @ResultMap(CaseInsensitiveResultMap.RESULT_MAP_ID)
187 196 List<Map<String, Object>> selectTopSenders(@Param("beginTime") LocalDateTime beginTime,
188 197 @Param("endTime") LocalDateTime endTime,
189 198 @Param("limit") int limit);
... ...
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/dal/mysql/statistics/ImStatisticsManagerMapperTest.java
... ... @@ -228,6 +228,9 @@ public class ImStatisticsManagerMapperTest extends BaseDbUnitTest {
228 228 assertEquals(3L, ((Number) tops.get(0).get("messageCount")).longValue());
229 229 assertEquals(2L, ((Number) tops.get(1).get("userId")).longValue());
230 230 assertEquals(2L, ((Number) tops.get(1).get("messageCount")).longValue());
  231 + // 断言:key 大小写不敏感,兼容 PostgreSQL 小写、Oracle 大写的列标签
  232 + assertEquals(1L, ((Number) tops.get(0).get("userid")).longValue());
  233 + assertEquals(3L, ((Number) tops.get(0).get("MESSAGECOUNT")).longValue());
231 234 }
232 235  
233 236 // ========== 工具方法 ==========
... ...
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/vo/ConfigRespVO.java
... ... @@ -3,10 +3,10 @@ package cn.iocoder.yudao.module.infra.controller.admin.config.vo;
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.module.infra.enums.DictTypeConstants;
6   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
7   -import cn.idev.excel.annotation.ExcelProperty;
8 6 import io.swagger.v3.oas.annotations.media.Schema;
9 7 import lombok.Data;
  8 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  9 +import org.apache.fesod.sheet.annotation.ExcelProperty;
10 10  
11 11 import java.time.LocalDateTime;
12 12  
... ...
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo01/vo/Demo01ContactRespVO.java
... ... @@ -2,10 +2,10 @@ package cn.iocoder.yudao.module.infra.controller.admin.demo.demo01.vo;
2 2  
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
6   -import cn.idev.excel.annotation.ExcelProperty;
7 5 import io.swagger.v3.oas.annotations.media.Schema;
8 6 import lombok.Data;
  7 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  8 +import org.apache.fesod.sheet.annotation.ExcelProperty;
9 9  
10 10 import java.time.LocalDateTime;
11 11  
... ...
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo02/vo/Demo02CategoryRespVO.java
1 1 package cn.iocoder.yudao.module.infra.controller.admin.demo.demo02.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  6 +import org.apache.fesod.sheet.annotation.ExcelProperty;
7 7  
8 8 import java.time.LocalDateTime;
9 9  
... ...
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/erp/vo/Demo03StudentErpRespVO.java
... ... @@ -2,10 +2,10 @@ package cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.erp.vo;
2 2  
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
6   -import cn.idev.excel.annotation.ExcelProperty;
7 5 import io.swagger.v3.oas.annotations.media.Schema;
8 6 import lombok.Data;
  7 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  8 +import org.apache.fesod.sheet.annotation.ExcelProperty;
9 9  
10 10 import java.time.LocalDateTime;
11 11  
... ...
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/inner/vo/Demo03StudentInnerRespVO.java
... ... @@ -2,10 +2,10 @@ package cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.inner.vo;
2 2  
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
6   -import cn.idev.excel.annotation.ExcelProperty;
7 5 import io.swagger.v3.oas.annotations.media.Schema;
8 6 import lombok.Data;
  7 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  8 +import org.apache.fesod.sheet.annotation.ExcelProperty;
9 9  
10 10 import java.time.LocalDateTime;
11 11  
... ...
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/normal/vo/Demo03StudentNormalRespVO.java
... ... @@ -2,10 +2,10 @@ package cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.normal.vo;
2 2  
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
6   -import cn.idev.excel.annotation.ExcelProperty;
7 5 import io.swagger.v3.oas.annotations.media.Schema;
8 6 import lombok.Data;
  7 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  8 +import org.apache.fesod.sheet.annotation.ExcelProperty;
9 9  
10 10 import java.time.LocalDateTime;
11 11  
... ...
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/job/JobRespVO.java
... ... @@ -3,11 +3,11 @@ package cn.iocoder.yudao.module.infra.controller.admin.job.vo.job;
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.module.infra.enums.DictTypeConstants;
6   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
7   -import cn.idev.excel.annotation.ExcelProperty;
8 6 import io.swagger.v3.oas.annotations.media.Schema;
9 7 import jakarta.validation.constraints.NotNull;
10 8 import lombok.Data;
  9 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  10 +import org.apache.fesod.sheet.annotation.ExcelProperty;
11 11  
12 12 import java.time.LocalDateTime;
13 13  
... ...
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/log/JobLogRespVO.java
... ... @@ -3,10 +3,10 @@ package cn.iocoder.yudao.module.infra.controller.admin.job.vo.log;
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.module.infra.enums.DictTypeConstants;
6   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
7   -import cn.idev.excel.annotation.ExcelProperty;
8 6 import io.swagger.v3.oas.annotations.media.Schema;
9 7 import lombok.Data;
  8 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  9 +import org.apache.fesod.sheet.annotation.ExcelProperty;
10 10  
11 11 import java.time.LocalDateTime;
12 12  
... ...
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogRespVO.java
... ... @@ -3,10 +3,10 @@ package cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apiaccesslog;
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.module.infra.enums.DictTypeConstants;
6   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
7   -import cn.idev.excel.annotation.ExcelProperty;
8 6 import io.swagger.v3.oas.annotations.media.Schema;
9 7 import lombok.Data;
  8 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  9 +import org.apache.fesod.sheet.annotation.ExcelProperty;
10 10  
11 11 import java.time.LocalDateTime;
12 12  
... ...
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogRespVO.java
... ... @@ -3,10 +3,10 @@ package cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apierrorlog;
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.module.infra.enums.DictTypeConstants;
6   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
7   -import cn.idev.excel.annotation.ExcelProperty;
8 6 import io.swagger.v3.oas.annotations.media.Schema;
9 7 import lombok.Data;
  8 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  9 +import org.apache.fesod.sheet.annotation.ExcelProperty;
10 10  
11 11 import java.time.LocalDateTime;
12 12  
... ...
yudao-module-infra/src/main/resources/codegen/java/controller/vo/importExcelVO.vm
1 1 package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelProperty;
  3 +import org.apache.fesod.sheet.annotation.ExcelProperty;
4 4 import lombok.AllArgsConstructor;
5 5 import lombok.Builder;
6 6 import lombok.Data;
... ...
yudao-module-infra/src/main/resources/codegen/java/controller/vo/respVO.vm
... ... @@ -19,7 +19,7 @@ import java.time.LocalDateTime;
19 19 #end
20 20 #end
21 21 ## 处理 Excel 导出
22   -import cn.idev.excel.annotation.*;
  22 +import org.apache.fesod.sheet.annotation.*;
23 23 #foreach ($column in $columns)
24 24 #if ("$!column.dictType" != "")## 有设置数据字典
25 25 import ${DictFormatClassName};
... ...
yudao-module-infra/src/main/resources/codegen/java/dal/do.vm
... ... @@ -15,7 +15,7 @@ import ${BaseDOClassName};
15 15 ## 处理 Excel 导出 + Schema 注解(仅 DO 模式)
16 16 #if ($voType == 20)
17 17 import io.swagger.v3.oas.annotations.media.Schema;
18   -import cn.idev.excel.annotation.*;
  18 +import org.apache.fesod.sheet.annotation.*;
19 19 #foreach ($column in $columns)
20 20 #if ("$!column.dictType" != "")## 有设置数据字典
21 21 import ${DictFormatClassName};
... ...
yudao-module-infra/src/test/resources/codegen/vben5_antd_general_master_erp/java/InfraStudentRespVO
... ... @@ -5,7 +5,7 @@ import lombok.*;
5 5 import java.util.*;
6 6 import org.springframework.format.annotation.DateTimeFormat;
7 7 import java.time.LocalDateTime;
8   -import cn.idev.excel.annotation.*;
  8 +import org.apache.fesod.sheet.annotation.*;
9 9 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
10 10 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
11 11  
... ...
yudao-module-infra/src/test/resources/codegen/vben5_antd_general_master_inner/java/InfraStudentRespVO
... ... @@ -5,7 +5,7 @@ import lombok.*;
5 5 import java.util.*;
6 6 import org.springframework.format.annotation.DateTimeFormat;
7 7 import java.time.LocalDateTime;
8   -import cn.idev.excel.annotation.*;
  8 +import org.apache.fesod.sheet.annotation.*;
9 9 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
10 10 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
11 11  
... ...
yudao-module-infra/src/test/resources/codegen/vben5_antd_general_master_normal/java/InfraStudentRespVO
... ... @@ -5,7 +5,7 @@ import lombok.*;
5 5 import java.util.*;
6 6 import org.springframework.format.annotation.DateTimeFormat;
7 7 import java.time.LocalDateTime;
8   -import cn.idev.excel.annotation.*;
  8 +import org.apache.fesod.sheet.annotation.*;
9 9 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
10 10 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
11 11  
... ...
yudao-module-infra/src/test/resources/codegen/vben5_antd_general_one/java/InfraStudentRespVO
... ... @@ -5,7 +5,7 @@ import lombok.*;
5 5 import java.util.*;
6 6 import org.springframework.format.annotation.DateTimeFormat;
7 7 import java.time.LocalDateTime;
8   -import cn.idev.excel.annotation.*;
  8 +import org.apache.fesod.sheet.annotation.*;
9 9 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
10 10 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
11 11  
... ...
yudao-module-infra/src/test/resources/codegen/vben5_antd_general_one_importEnable/java/InfraStudentImportExcelVO
1 1 package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelProperty;
  3 +import org.apache.fesod.sheet.annotation.ExcelProperty;
4 4 import lombok.AllArgsConstructor;
5 5 import lombok.Builder;
6 6 import lombok.Data;
... ...
yudao-module-infra/src/test/resources/codegen/vben5_antd_general_one_importEnable/java/InfraStudentRespVO
... ... @@ -5,7 +5,7 @@ import lombok.*;
5 5 import java.util.*;
6 6 import org.springframework.format.annotation.DateTimeFormat;
7 7 import java.time.LocalDateTime;
8   -import cn.idev.excel.annotation.*;
  8 +import org.apache.fesod.sheet.annotation.*;
9 9 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
10 10 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
11 11  
... ...
yudao-module-infra/src/test/resources/codegen/vben5_antd_general_tree/java/InfraCategoryRespVO
... ... @@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
3 3 import io.swagger.v3.oas.annotations.media.Schema;
4 4 import lombok.*;
5 5 import java.util.*;
6   -import cn.idev.excel.annotation.*;
  6 +import org.apache.fesod.sheet.annotation.*;
7 7  
8 8 @Schema(description = "管理后台 - 分类 Response VO")
9 9 @Data
... ...
yudao-module-infra/src/test/resources/codegen/vben5_antd_schema_master_erp/java/InfraStudentRespVO
... ... @@ -5,7 +5,7 @@ import lombok.*;
5 5 import java.util.*;
6 6 import org.springframework.format.annotation.DateTimeFormat;
7 7 import java.time.LocalDateTime;
8   -import cn.idev.excel.annotation.*;
  8 +import org.apache.fesod.sheet.annotation.*;
9 9 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
10 10 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
11 11  
... ...
yudao-module-infra/src/test/resources/codegen/vben5_antd_schema_master_inner/java/InfraStudentRespVO
... ... @@ -5,7 +5,7 @@ import lombok.*;
5 5 import java.util.*;
6 6 import org.springframework.format.annotation.DateTimeFormat;
7 7 import java.time.LocalDateTime;
8   -import cn.idev.excel.annotation.*;
  8 +import org.apache.fesod.sheet.annotation.*;
9 9 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
10 10 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
11 11  
... ...
yudao-module-infra/src/test/resources/codegen/vben5_antd_schema_master_normal/java/InfraStudentRespVO
... ... @@ -5,7 +5,7 @@ import lombok.*;
5 5 import java.util.*;
6 6 import org.springframework.format.annotation.DateTimeFormat;
7 7 import java.time.LocalDateTime;
8   -import cn.idev.excel.annotation.*;
  8 +import org.apache.fesod.sheet.annotation.*;
9 9 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
10 10 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
11 11  
... ...
yudao-module-infra/src/test/resources/codegen/vben5_antd_schema_one/java/InfraStudentImportExcelVO
1 1 package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelProperty;
  3 +import org.apache.fesod.sheet.annotation.ExcelProperty;
4 4 import lombok.AllArgsConstructor;
5 5 import lombok.Builder;
6 6 import lombok.Data;
... ...
yudao-module-infra/src/test/resources/codegen/vben5_antd_schema_one/java/InfraStudentRespVO
... ... @@ -5,7 +5,7 @@ import lombok.*;
5 5 import java.util.*;
6 6 import org.springframework.format.annotation.DateTimeFormat;
7 7 import java.time.LocalDateTime;
8   -import cn.idev.excel.annotation.*;
  8 +import org.apache.fesod.sheet.annotation.*;
9 9 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
10 10 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
11 11  
... ...
yudao-module-infra/src/test/resources/codegen/vben5_antd_schema_one_importEnable/java/InfraStudentImportExcelVO
1 1 package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelProperty;
  3 +import org.apache.fesod.sheet.annotation.ExcelProperty;
4 4 import lombok.AllArgsConstructor;
5 5 import lombok.Builder;
6 6 import lombok.Data;
... ...
yudao-module-infra/src/test/resources/codegen/vben5_antd_schema_one_importEnable/java/InfraStudentRespVO
... ... @@ -5,7 +5,7 @@ import lombok.*;
5 5 import java.util.*;
6 6 import org.springframework.format.annotation.DateTimeFormat;
7 7 import java.time.LocalDateTime;
8   -import cn.idev.excel.annotation.*;
  8 +import org.apache.fesod.sheet.annotation.*;
9 9 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
10 10 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
11 11  
... ...
yudao-module-infra/src/test/resources/codegen/vben5_antd_schema_tree/java/InfraCategoryRespVO
... ... @@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
3 3 import io.swagger.v3.oas.annotations.media.Schema;
4 4 import lombok.*;
5 5 import java.util.*;
6   -import cn.idev.excel.annotation.*;
  6 +import org.apache.fesod.sheet.annotation.*;
7 7  
8 8 @Schema(description = "管理后台 - 分类 Response VO")
9 9 @Data
... ...
yudao-module-infra/src/test/resources/codegen/vben5_ele_general_master_erp/java/InfraStudentRespVO
... ... @@ -5,7 +5,7 @@ import lombok.*;
5 5 import java.util.*;
6 6 import org.springframework.format.annotation.DateTimeFormat;
7 7 import java.time.LocalDateTime;
8   -import cn.idev.excel.annotation.*;
  8 +import org.apache.fesod.sheet.annotation.*;
9 9 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
10 10 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
11 11  
... ...
yudao-module-infra/src/test/resources/codegen/vben5_ele_general_master_inner/java/InfraStudentRespVO
... ... @@ -5,7 +5,7 @@ import lombok.*;
5 5 import java.util.*;
6 6 import org.springframework.format.annotation.DateTimeFormat;
7 7 import java.time.LocalDateTime;
8   -import cn.idev.excel.annotation.*;
  8 +import org.apache.fesod.sheet.annotation.*;
9 9 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
10 10 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
11 11  
... ...
yudao-module-infra/src/test/resources/codegen/vben5_ele_general_master_normal/java/InfraStudentRespVO
... ... @@ -5,7 +5,7 @@ import lombok.*;
5 5 import java.util.*;
6 6 import org.springframework.format.annotation.DateTimeFormat;
7 7 import java.time.LocalDateTime;
8   -import cn.idev.excel.annotation.*;
  8 +import org.apache.fesod.sheet.annotation.*;
9 9 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
10 10 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
11 11  
... ...
yudao-module-infra/src/test/resources/codegen/vben5_ele_general_one/java/InfraStudentRespVO
... ... @@ -5,7 +5,7 @@ import lombok.*;
5 5 import java.util.*;
6 6 import org.springframework.format.annotation.DateTimeFormat;
7 7 import java.time.LocalDateTime;
8   -import cn.idev.excel.annotation.*;
  8 +import org.apache.fesod.sheet.annotation.*;
9 9 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
10 10 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
11 11  
... ...
yudao-module-infra/src/test/resources/codegen/vben5_ele_general_one_importEnable/java/InfraStudentImportExcelVO
1 1 package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelProperty;
  3 +import org.apache.fesod.sheet.annotation.ExcelProperty;
4 4 import lombok.AllArgsConstructor;
5 5 import lombok.Builder;
6 6 import lombok.Data;
... ...
yudao-module-infra/src/test/resources/codegen/vben5_ele_general_one_importEnable/java/InfraStudentRespVO
... ... @@ -5,7 +5,7 @@ import lombok.*;
5 5 import java.util.*;
6 6 import org.springframework.format.annotation.DateTimeFormat;
7 7 import java.time.LocalDateTime;
8   -import cn.idev.excel.annotation.*;
  8 +import org.apache.fesod.sheet.annotation.*;
9 9 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
10 10 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
11 11  
... ...
yudao-module-infra/src/test/resources/codegen/vben5_ele_general_tree/java/InfraCategoryRespVO
... ... @@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
3 3 import io.swagger.v3.oas.annotations.media.Schema;
4 4 import lombok.*;
5 5 import java.util.*;
6   -import cn.idev.excel.annotation.*;
  6 +import org.apache.fesod.sheet.annotation.*;
7 7  
8 8 @Schema(description = "管理后台 - 分类 Response VO")
9 9 @Data
... ...
yudao-module-infra/src/test/resources/codegen/vben5_ele_schema_master_erp/java/InfraStudentRespVO
... ... @@ -5,7 +5,7 @@ import lombok.*;
5 5 import java.util.*;
6 6 import org.springframework.format.annotation.DateTimeFormat;
7 7 import java.time.LocalDateTime;
8   -import cn.idev.excel.annotation.*;
  8 +import org.apache.fesod.sheet.annotation.*;
9 9 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
10 10 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
11 11  
... ...
yudao-module-infra/src/test/resources/codegen/vben5_ele_schema_master_inner/java/InfraStudentRespVO
... ... @@ -5,7 +5,7 @@ import lombok.*;
5 5 import java.util.*;
6 6 import org.springframework.format.annotation.DateTimeFormat;
7 7 import java.time.LocalDateTime;
8   -import cn.idev.excel.annotation.*;
  8 +import org.apache.fesod.sheet.annotation.*;
9 9 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
10 10 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
11 11  
... ...
yudao-module-infra/src/test/resources/codegen/vben5_ele_schema_master_normal/java/InfraStudentRespVO
... ... @@ -5,7 +5,7 @@ import lombok.*;
5 5 import java.util.*;
6 6 import org.springframework.format.annotation.DateTimeFormat;
7 7 import java.time.LocalDateTime;
8   -import cn.idev.excel.annotation.*;
  8 +import org.apache.fesod.sheet.annotation.*;
9 9 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
10 10 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
11 11  
... ...
yudao-module-infra/src/test/resources/codegen/vben5_ele_schema_one/java/InfraStudentImportExcelVO
1 1 package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelProperty;
  3 +import org.apache.fesod.sheet.annotation.ExcelProperty;
4 4 import lombok.AllArgsConstructor;
5 5 import lombok.Builder;
6 6 import lombok.Data;
... ...
yudao-module-infra/src/test/resources/codegen/vben5_ele_schema_one/java/InfraStudentRespVO
... ... @@ -5,7 +5,7 @@ import lombok.*;
5 5 import java.util.*;
6 6 import org.springframework.format.annotation.DateTimeFormat;
7 7 import java.time.LocalDateTime;
8   -import cn.idev.excel.annotation.*;
  8 +import org.apache.fesod.sheet.annotation.*;
9 9 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
10 10 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
11 11  
... ...
yudao-module-infra/src/test/resources/codegen/vben5_ele_schema_one_importEnable/java/InfraStudentImportExcelVO
1 1 package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelProperty;
  3 +import org.apache.fesod.sheet.annotation.ExcelProperty;
4 4 import lombok.AllArgsConstructor;
5 5 import lombok.Builder;
6 6 import lombok.Data;
... ...
yudao-module-infra/src/test/resources/codegen/vben5_ele_schema_one_importEnable/java/InfraStudentRespVO
... ... @@ -5,7 +5,7 @@ import lombok.*;
5 5 import java.util.*;
6 6 import org.springframework.format.annotation.DateTimeFormat;
7 7 import java.time.LocalDateTime;
8   -import cn.idev.excel.annotation.*;
  8 +import org.apache.fesod.sheet.annotation.*;
9 9 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
10 10 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
11 11  
... ...
yudao-module-infra/src/test/resources/codegen/vben5_ele_schema_tree/java/InfraCategoryRespVO
... ... @@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
3 3 import io.swagger.v3.oas.annotations.media.Schema;
4 4 import lombok.*;
5 5 import java.util.*;
6   -import cn.idev.excel.annotation.*;
  6 +import org.apache.fesod.sheet.annotation.*;
7 7  
8 8 @Schema(description = "管理后台 - 分类 Response VO")
9 9 @Data
... ...
yudao-module-infra/src/test/resources/codegen/vue2_master_erp/java/InfraStudentRespVO
... ... @@ -5,7 +5,7 @@ import lombok.*;
5 5 import java.util.*;
6 6 import org.springframework.format.annotation.DateTimeFormat;
7 7 import java.time.LocalDateTime;
8   -import cn.idev.excel.annotation.*;
  8 +import org.apache.fesod.sheet.annotation.*;
9 9 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
10 10 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
11 11  
... ...
yudao-module-infra/src/test/resources/codegen/vue2_master_inner/java/InfraStudentRespVO
... ... @@ -5,7 +5,7 @@ import lombok.*;
5 5 import java.util.*;
6 6 import org.springframework.format.annotation.DateTimeFormat;
7 7 import java.time.LocalDateTime;
8   -import cn.idev.excel.annotation.*;
  8 +import org.apache.fesod.sheet.annotation.*;
9 9 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
10 10 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
11 11  
... ...
yudao-module-infra/src/test/resources/codegen/vue2_master_normal/java/InfraStudentRespVO
... ... @@ -5,7 +5,7 @@ import lombok.*;
5 5 import java.util.*;
6 6 import org.springframework.format.annotation.DateTimeFormat;
7 7 import java.time.LocalDateTime;
8   -import cn.idev.excel.annotation.*;
  8 +import org.apache.fesod.sheet.annotation.*;
9 9 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
10 10 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
11 11  
... ...
yudao-module-infra/src/test/resources/codegen/vue2_one/java/InfraStudentRespVO
... ... @@ -5,7 +5,7 @@ import lombok.*;
5 5 import java.util.*;
6 6 import org.springframework.format.annotation.DateTimeFormat;
7 7 import java.time.LocalDateTime;
8   -import cn.idev.excel.annotation.*;
  8 +import org.apache.fesod.sheet.annotation.*;
9 9 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
10 10 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
11 11  
... ...
yudao-module-infra/src/test/resources/codegen/vue2_one_importEnable/java/InfraStudentImportExcelVO
1 1 package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelProperty;
  3 +import org.apache.fesod.sheet.annotation.ExcelProperty;
4 4 import lombok.AllArgsConstructor;
5 5 import lombok.Builder;
6 6 import lombok.Data;
... ...
yudao-module-infra/src/test/resources/codegen/vue2_one_importEnable/java/InfraStudentRespVO
... ... @@ -5,7 +5,7 @@ import lombok.*;
5 5 import java.util.*;
6 6 import org.springframework.format.annotation.DateTimeFormat;
7 7 import java.time.LocalDateTime;
8   -import cn.idev.excel.annotation.*;
  8 +import org.apache.fesod.sheet.annotation.*;
9 9 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
10 10 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
11 11  
... ...
yudao-module-infra/src/test/resources/codegen/vue2_tree/java/InfraCategoryRespVO
... ... @@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
3 3 import io.swagger.v3.oas.annotations.media.Schema;
4 4 import lombok.*;
5 5 import java.util.*;
6   -import cn.idev.excel.annotation.*;
  6 +import org.apache.fesod.sheet.annotation.*;
7 7  
8 8 @Schema(description = "管理后台 - 分类 Response VO")
9 9 @Data
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentRespVO
... ... @@ -5,7 +5,7 @@ import lombok.*;
5 5 import java.util.*;
6 6 import org.springframework.format.annotation.DateTimeFormat;
7 7 import java.time.LocalDateTime;
8   -import cn.idev.excel.annotation.*;
  8 +import org.apache.fesod.sheet.annotation.*;
9 9 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
10 10 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
11 11  
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentRespVO
... ... @@ -5,7 +5,7 @@ import lombok.*;
5 5 import java.util.*;
6 6 import org.springframework.format.annotation.DateTimeFormat;
7 7 import java.time.LocalDateTime;
8   -import cn.idev.excel.annotation.*;
  8 +import org.apache.fesod.sheet.annotation.*;
9 9 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
10 10 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
11 11  
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentRespVO
... ... @@ -5,7 +5,7 @@ import lombok.*;
5 5 import java.util.*;
6 6 import org.springframework.format.annotation.DateTimeFormat;
7 7 import java.time.LocalDateTime;
8   -import cn.idev.excel.annotation.*;
  8 +import org.apache.fesod.sheet.annotation.*;
9 9 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
10 10 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
11 11  
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_one/java/InfraStudentRespVO
... ... @@ -5,7 +5,7 @@ import lombok.*;
5 5 import java.util.*;
6 6 import org.springframework.format.annotation.DateTimeFormat;
7 7 import java.time.LocalDateTime;
8   -import cn.idev.excel.annotation.*;
  8 +import org.apache.fesod.sheet.annotation.*;
9 9 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
10 10 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
11 11  
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryRespVO
... ... @@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
3 3 import io.swagger.v3.oas.annotations.media.Schema;
4 4 import lombok.*;
5 5 import java.util.*;
6   -import cn.idev.excel.annotation.*;
  6 +import org.apache.fesod.sheet.annotation.*;
7 7  
8 8 @Schema(description = "管理后台 - 分类 Response VO")
9 9 @Data
... ...
yudao-module-infra/src/test/resources/codegen/vue3_master_erp/java/InfraStudentRespVO
... ... @@ -5,7 +5,7 @@ import lombok.*;
5 5 import java.util.*;
6 6 import org.springframework.format.annotation.DateTimeFormat;
7 7 import java.time.LocalDateTime;
8   -import cn.idev.excel.annotation.*;
  8 +import org.apache.fesod.sheet.annotation.*;
9 9 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
10 10 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
11 11  
... ...
yudao-module-infra/src/test/resources/codegen/vue3_master_inner/java/InfraStudentRespVO
... ... @@ -5,7 +5,7 @@ import lombok.*;
5 5 import java.util.*;
6 6 import org.springframework.format.annotation.DateTimeFormat;
7 7 import java.time.LocalDateTime;
8   -import cn.idev.excel.annotation.*;
  8 +import org.apache.fesod.sheet.annotation.*;
9 9 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
10 10 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
11 11  
... ...
yudao-module-infra/src/test/resources/codegen/vue3_master_normal/java/InfraStudentRespVO
... ... @@ -5,7 +5,7 @@ import lombok.*;
5 5 import java.util.*;
6 6 import org.springframework.format.annotation.DateTimeFormat;
7 7 import java.time.LocalDateTime;
8   -import cn.idev.excel.annotation.*;
  8 +import org.apache.fesod.sheet.annotation.*;
9 9 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
10 10 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
11 11  
... ...
yudao-module-infra/src/test/resources/codegen/vue3_one/java/InfraStudentImportExcelVO
1 1 package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelProperty;
  3 +import org.apache.fesod.sheet.annotation.ExcelProperty;
4 4 import lombok.AllArgsConstructor;
5 5 import lombok.Builder;
6 6 import lombok.Data;
... ...
yudao-module-infra/src/test/resources/codegen/vue3_one/java/InfraStudentRespVO
... ... @@ -5,7 +5,7 @@ import lombok.*;
5 5 import java.util.*;
6 6 import org.springframework.format.annotation.DateTimeFormat;
7 7 import java.time.LocalDateTime;
8   -import cn.idev.excel.annotation.*;
  8 +import org.apache.fesod.sheet.annotation.*;
9 9 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
10 10 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
11 11  
... ...
yudao-module-infra/src/test/resources/codegen/vue3_one_importEnable/java/InfraStudentImportExcelVO
1 1 package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelProperty;
  3 +import org.apache.fesod.sheet.annotation.ExcelProperty;
4 4 import lombok.AllArgsConstructor;
5 5 import lombok.Builder;
6 6 import lombok.Data;
... ...
yudao-module-infra/src/test/resources/codegen/vue3_one_importEnable/java/InfraStudentRespVO
... ... @@ -5,7 +5,7 @@ import lombok.*;
5 5 import java.util.*;
6 6 import org.springframework.format.annotation.DateTimeFormat;
7 7 import java.time.LocalDateTime;
8   -import cn.idev.excel.annotation.*;
  8 +import org.apache.fesod.sheet.annotation.*;
9 9 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
10 10 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
11 11  
... ...
yudao-module-infra/src/test/resources/codegen/vue3_tree/java/InfraCategoryRespVO
... ... @@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
3 3 import io.swagger.v3.oas.annotations.media.Schema;
4 4 import lombok.*;
5 5 import java.util.*;
6   -import cn.idev.excel.annotation.*;
  6 +import org.apache.fesod.sheet.annotation.*;
7 7  
8 8 @Schema(description = "管理后台 - 分类 Response VO")
9 9 @Data
... ...
yudao-module-iot/yudao-module-iot-biz/pom.xml
... ... @@ -50,6 +50,10 @@
50 50 <groupId>com.taosdata.jdbc</groupId>
51 51 <artifactId>taos-jdbcdriver</artifactId>
52 52 </dependency>
  53 + <dependency>
  54 + <groupId>org.apache.iotdb</groupId>
  55 + <artifactId>iotdb-jdbc</artifactId>
  56 + </dependency>
53 57  
54 58 <dependency>
55 59 <groupId>cn.iocoder.boot</groupId>
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceMessageController.java
... ... @@ -10,7 +10,6 @@ import cn.iocoder.yudao.module.iot.controller.admin.device.vo.message.IotDeviceM
10 10 import cn.iocoder.yudao.module.iot.controller.admin.device.vo.message.IotDeviceMessageSendReqVO;
11 11 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
12 12 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceMessageDO;
13   -import cn.iocoder.yudao.module.iot.dal.tdengine.IotDeviceMessageMapper;
14 13 import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
15 14 import cn.iocoder.yudao.module.iot.service.device.message.IotDeviceMessageService;
16 15 import cn.iocoder.yudao.module.iot.service.thingmodel.IotThingModelService;
... ... @@ -41,8 +40,6 @@ public class IotDeviceMessageController {
41 40 private IotDeviceService deviceService;
42 41 @Resource
43 42 private IotThingModelService thingModelService;
44   - @Resource
45   - private IotDeviceMessageMapper deviceMessageMapper;
46 43  
47 44 @GetMapping("/page")
48 45 @Operation(summary = "获得设备消息分页")
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceImportExcelVO.java
1 1 package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device;
2 2  
3   -import cn.idev.excel.annotation.ExcelProperty;
4 3 import io.swagger.v3.oas.annotations.media.Schema;
5 4 import jakarta.validation.constraints.NotEmpty;
6 5 import lombok.AllArgsConstructor;
7 6 import lombok.Builder;
8 7 import lombok.Data;
9 8 import lombok.NoArgsConstructor;
  9 +import org.apache.fesod.sheet.annotation.ExcelProperty;
10 10  
11 11 /**
12 12 * 设备 Excel 导入 VO
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceRespVO.java
1 1 package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
6 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
7 5 import io.swagger.v3.oas.annotations.media.Schema;
8 6 import lombok.Data;
  7 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  8 +import org.apache.fesod.sheet.annotation.ExcelProperty;
9 9  
10 10 import java.math.BigDecimal;
11 11 import java.time.LocalDateTime;
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductRespVO.java
... ... @@ -3,10 +3,10 @@ package cn.iocoder.yudao.module.iot.controller.admin.product.vo.product;
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.module.iot.enums.DictTypeConstants;
6   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
7   -import cn.idev.excel.annotation.ExcelProperty;
8 6 import io.swagger.v3.oas.annotations.media.Schema;
9 7 import lombok.Data;
  8 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  9 +import org.apache.fesod.sheet.annotation.ExcelProperty;
10 10  
11 11 import java.time.LocalDateTime;
12 12  
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceMessageDO.java
... ... @@ -11,7 +11,7 @@ import lombok.NoArgsConstructor;
11 11 /**
12 12 * IoT 设备消息数据 DO
13 13 *
14   - * 目前使用 TDengine 存储
  14 + * 支持 TDengine、IoTDB 存储,由 yudao.iot.storage.type 选择
15 15 *
16 16 * @author alwayssuper
17 17 */
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/model/ThingModelProperty.java
... ... @@ -21,6 +21,16 @@ import java.util.List;
21 21 public class ThingModelProperty {
22 22  
23 23 /**
  24 + * STRUCT、ARRAY 等 JSON 属性值的长度上限:按 UTF-8 编码后的字节数计算,不是字符数
  25 + *
  26 + * 1. 写入前:JSON 字符串超过该长度时,转换为 null,不写入该属性
  27 + * 2. 各存储后端共用,例如说 TDengine 的 VARCHAR 长度
  28 + *
  29 + * 注意:TDengine 一行(所有列)有总长度上限,调大该长度,会减少一个产品可以定义的 STRUCT、ARRAY 属性数量
  30 + */
  31 + public static final int LENGTH_JSON = 1024;
  32 +
  33 + /**
24 34 * 属性标识符
25 35 */
26 36 @NotEmpty(message = "属性标识符不能为空")
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/iotdb/IoTDBSystemMapper.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.dal.iotdb;
  2 +
  3 +import cn.iocoder.yudao.module.iot.framework.iotdb.core.IoTDBUtils;
  4 +import cn.iocoder.yudao.module.iot.framework.iotdb.core.annotation.IoTDBDS;
  5 +import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
  6 +import org.apache.ibatis.annotations.Mapper;
  7 +import org.apache.ibatis.annotations.Param;
  8 +import org.apache.ibatis.annotations.Select;
  9 +
  10 +import java.util.List;
  11 +import java.util.Map;
  12 +
  13 +/**
  14 + * IoTDB 系统信息 Mapper:只读取配置,用于启动前提检查
  15 + *
  16 + * @author 芋道源码
  17 + */
  18 +@Mapper
  19 +@IoTDBDS
  20 +@InterceptorIgnore(tenantLine = "true", dataPermission = "true") // 避免 SQL 解析,因为 JSqlParser 对 IoTDB 的 SQL 解析会报错
  21 +public interface IoTDBSystemMapper {
  22 +
  23 + @Select("SHOW VARIABLES")
  24 + List<Map<String, Object>> selectVariableList();
  25 +
  26 + @Select("SHOW DATANODES")
  27 + List<Map<String, Object>> selectDataNodeList();
  28 +
  29 + /**
  30 + * 获得指定 DataNode 的全部配置
  31 + *
  32 + * @param nodeId DataNode 编号,来自 SHOW DATANODES 的结果,不接收外部输入
  33 + * @return 配置列表
  34 + */
  35 + @Select("SHOW ALL CONFIGURATION ON ${nodeId}")
  36 + List<Map<String, Object>> selectAllConfigurationList(@Param("nodeId") Integer nodeId);
  37 +
  38 + /**
  39 + * 校验 IoTDB 的前提条件,不满足时抛出异常。只检查,不修改任何配置:
  40 + * 1. 数据库已预建:连接 URL 中的数据库不存在时,获取连接即失败
  41 + * 2. TimestampPrecision 为 ms
  42 + * 3. 所有 DataNode 为 Running,且 enable_partial_insert=false
  43 + */
  44 + default void verifyPrerequisites() {
  45 + IoTDBUtils.validateTimestampPrecision(selectVariableList());
  46 + List<Integer> nodeIds = IoTDBUtils.validateDataNodes(selectDataNodeList());
  47 + for (Integer nodeId : nodeIds) {
  48 + IoTDBUtils.validatePartialInsertDisabled(nodeId, selectAllConfigurationList(nodeId));
  49 + }
  50 + }
  51 +
  52 +}
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/iotdb/IotDeviceMessageIoTDBCodec.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.dal.iotdb;
  2 +
  3 +import cn.hutool.core.lang.Assert;
  4 +import cn.hutool.core.util.StrUtil;
  5 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  6 +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceMessageDO;
  7 +
  8 +/**
  9 + * IoTDB 设备消息的编解码器
  10 + *
  11 + * 为什么需要编码?IoTDB 相同 (device_id, time) 再次写入时,NULL 值不会清空旧值,导致“整行替换”的语义不成立,
  12 + * 例如说同一设备、同一毫秒的两条消息(id 不同),会混合成一行。因此,所有 FIELD 都编码为非 NULL 值,每次写入都设置全部 FIELD
  13 + * (id 为 FIELD,原样存储,不编码):
  14 + * 1. Boolean:-1 为 null,0 为 false,1 为 true
  15 + * 2. 其它:n: 为 null,v: + 字符串为非 null 值。例如说:null -> "n:","" -> "v:",123 -> "v:123"
  16 + *
  17 + * 查询条件中的值,也需要使用相同的编码
  18 + *
  19 + * @author 芋道源码
  20 + */
  21 +public class IotDeviceMessageIoTDBCodec {
  22 +
  23 + public static final int BOOLEAN_NULL = -1;
  24 + public static final int BOOLEAN_FALSE = 0;
  25 + public static final int BOOLEAN_TRUE = 1;
  26 +
  27 + public static final String PREFIX_NULL = "n:";
  28 + public static final String PREFIX_VALUE = "v:";
  29 +
  30 + // ========== 消息 ==========
  31 +
  32 + public static IotDeviceMessageIoTDBRow encode(IotDeviceMessageDO message) {
  33 + Assert.notNull(message.getTs(), "消息的 ts 不能为空");
  34 + Assert.notNull(message.getDeviceId(), "消息的 deviceId 不能为空");
  35 + Assert.notEmpty(message.getId(), "消息的 id 不能为空");
  36 + return new IotDeviceMessageIoTDBRow().setTs(message.getTs())
  37 + .setDeviceId(encodeDeviceId(message.getDeviceId())).setId(message.getId())
  38 + .setUpstream(encodeBoolean(message.getUpstream())).setReply(encodeBoolean(message.getReply()))
  39 + .setMethod(encodeString(message.getMethod())).setIdentifier(encodeString(message.getIdentifier()))
  40 + .setRequestId(encodeString(message.getRequestId())).setServerId(encodeString(message.getServerId()))
  41 + .setParams(encodeString(toJsonString(message.getParams())))
  42 + .setData(encodeString(toJsonString(message.getData())))
  43 + .setMsg(encodeString(message.getMsg())).setReportTime(encodeString(message.getReportTime()))
  44 + .setTenantId(encodeString(message.getTenantId())).setCode(encodeString(message.getCode()));
  45 + }
  46 +
  47 + public static IotDeviceMessageDO decode(IotDeviceMessageIoTDBRow row) {
  48 + String reportTime = decodeString(row.getReportTime());
  49 + String tenantId = decodeString(row.getTenantId());
  50 + String code = decodeString(row.getCode());
  51 + return new IotDeviceMessageDO().setTs(row.getTs())
  52 + .setDeviceId(row.getDeviceId() != null ? Long.valueOf(row.getDeviceId()) : null).setId(row.getId())
  53 + .setUpstream(decodeBoolean(row.getUpstream())).setReply(decodeBoolean(row.getReply()))
  54 + .setMethod(decodeString(row.getMethod())).setIdentifier(decodeString(row.getIdentifier()))
  55 + .setRequestId(decodeString(row.getRequestId())).setServerId(decodeString(row.getServerId()))
  56 + .setParams(decodeString(row.getParams())).setData(decodeString(row.getData()))
  57 + .setMsg(decodeString(row.getMsg()))
  58 + .setReportTime(reportTime != null ? Long.valueOf(reportTime) : null)
  59 + .setTenantId(tenantId != null ? Long.valueOf(tenantId) : null)
  60 + .setCode(code != null ? Integer.valueOf(code) : null);
  61 + }
  62 +
  63 + /**
  64 + * 转换 params、data 为字符串:Service 写入前已转为 JSON 字符串,这里兼容其它对象
  65 + */
  66 + private static String toJsonString(Object value) {
  67 + if (value == null || value instanceof String) {
  68 + return (String) value;
  69 + }
  70 + return JsonUtils.toJsonString(value);
  71 + }
  72 +
  73 + // ========== 单个值 ==========
  74 +
  75 + public static String encodeDeviceId(Long deviceId) {
  76 + return deviceId != null ? deviceId.toString() : null;
  77 + }
  78 +
  79 + public static int encodeBoolean(Boolean value) {
  80 + if (value == null) {
  81 + return BOOLEAN_NULL;
  82 + }
  83 + return value ? BOOLEAN_TRUE : BOOLEAN_FALSE;
  84 + }
  85 +
  86 + /**
  87 + * 解码 Boolean
  88 + *
  89 + * @param value 存储值;为 null 时,说明该行不是本系统写入的,按 null 处理
  90 + * @return Boolean
  91 + */
  92 + public static Boolean decodeBoolean(Integer value) {
  93 + if (value == null || value == BOOLEAN_NULL) {
  94 + return null;
  95 + }
  96 + if (value == BOOLEAN_TRUE) {
  97 + return true;
  98 + }
  99 + if (value == BOOLEAN_FALSE) {
  100 + return false;
  101 + }
  102 + throw new IllegalArgumentException(StrUtil.format("无法解码 Boolean 存储值({})", value));
  103 + }
  104 +
  105 + public static String encodeString(Object value) {
  106 + if (value == null) {
  107 + return PREFIX_NULL;
  108 + }
  109 + return PREFIX_VALUE + value;
  110 + }
  111 +
  112 + /**
  113 + * 解码字符串
  114 + *
  115 + * @param value 存储值;为 null 时,说明该行不是本系统写入的,按 null 处理
  116 + * @return 字符串
  117 + */
  118 + public static String decodeString(String value) {
  119 + if (value == null || PREFIX_NULL.equals(value)) {
  120 + return null;
  121 + }
  122 + if (value.startsWith(PREFIX_VALUE)) {
  123 + return value.substring(PREFIX_VALUE.length());
  124 + }
  125 + throw new IllegalArgumentException(StrUtil.format("无法解码字符串存储值({})", value));
  126 + }
  127 +
  128 +}
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/iotdb/IotDeviceMessageIoTDBDAO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.dal.iotdb;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.map.MapUtil;
  5 +import cn.hutool.core.util.ObjUtil;
  6 +import cn.hutool.core.util.StrUtil;
  7 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  8 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  9 +import cn.iocoder.yudao.framework.common.util.object.PageUtils;
  10 +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.message.IotDeviceMessagePageReqVO;
  11 +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceMessageDO;
  12 +import cn.iocoder.yudao.module.iot.dal.storage.IotDeviceMessageDAO;
  13 +import cn.iocoder.yudao.module.iot.framework.iotdb.core.IoTDBUtils;
  14 +import cn.iocoder.yudao.module.iot.framework.storage.config.IotStorageProperties;
  15 +import jakarta.annotation.Resource;
  16 +import lombok.extern.slf4j.Slf4j;
  17 +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
  18 +import org.springframework.stereotype.Repository;
  19 +
  20 +import java.util.*;
  21 +
  22 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
  23 +import static cn.iocoder.yudao.module.iot.dal.iotdb.IotDeviceMessageIoTDBCodec.*;
  24 +
  25 +/**
  26 + * 基于 IoTDB(表模型)的 {@link IotDeviceMessageDAO} 实现类
  27 + *
  28 + * 一张全局表 device_message,身份为 (device_id, time),编解码见 {@link IotDeviceMessageIoTDBCodec}
  29 + *
  30 + * @author 芋道源码
  31 + */
  32 +@Repository
  33 +@ConditionalOnProperty(prefix = IotStorageProperties.PREFIX, name = "type", havingValue = "iotdb")
  34 +@Slf4j
  35 +public class IotDeviceMessageIoTDBDAO implements IotDeviceMessageDAO {
  36 +
  37 + private static final String CATEGORY_TAG = "TAG";
  38 + private static final String CATEGORY_FIELD = "FIELD";
  39 +
  40 + /**
  41 + * 唯一的 TAG 列
  42 + */
  43 + private static final String COLUMN_DEVICE_ID = "device_id";
  44 + /**
  45 + * 消息表的列:列名 -> 数据类型。除 {@link #COLUMN_DEVICE_ID} 为 TAG 外,其它都是 FIELD
  46 + */
  47 + private static final Map<String, String> COLUMN_TYPES = MapUtil.builder(new LinkedHashMap<String, String>())
  48 + .put(COLUMN_DEVICE_ID, "STRING").put("id", "STRING")
  49 + .put("upstream", "INT32").put("reply", "INT32")
  50 + .put("method", "STRING").put("identifier", "STRING").put("request_id", "STRING").put("server_id", "STRING")
  51 + .put("params", "STRING").put("data", "STRING").put("msg", "STRING")
  52 + .put("report_time", "STRING").put("tenant_id", "STRING").put("code", "STRING")
  53 + .build();
  54 +
  55 + @Resource
  56 + private IotDeviceMessageIoTDBMapper deviceMessageMapper;
  57 + @Resource
  58 + private IoTDBSystemMapper systemMapper;
  59 +
  60 + @Override
  61 + public void initialize() {
  62 + // 1. 校验前提条件:只读取配置,不修改
  63 + systemMapper.verifyPrerequisites();
  64 + // 2. 获得消息表的列:表不存在时,创建后再读取
  65 + List<Map<String, Object>> columns = getColumnList();
  66 + if (columns == null) {
  67 + deviceMessageMapper.createTable();
  68 + log.info("[initialize][设备消息表不存在,创建成功]");
  69 + columns = deviceMessageMapper.selectColumnList();
  70 + }
  71 + // 3. 校验表结构:不兼容时,需要人工处理,不会自动修改、删除
  72 + validateColumns(columns);
  73 + log.info("[initialize][设备消息表已就绪]");
  74 + }
  75 +
  76 + /**
  77 + * 获得消息表的列
  78 + *
  79 + * @return 列列表;表不存在时,返回 null
  80 + */
  81 + private List<Map<String, Object>> getColumnList() {
  82 + try {
  83 + return deviceMessageMapper.selectColumnList();
  84 + } catch (Exception exception) {
  85 + // 只有表不存在(550)返回 null;权限、连接等其它错误原样抛出
  86 + if (IoTDBUtils.isTableNotExists(exception)) {
  87 + return null;
  88 + }
  89 + throw exception;
  90 + }
  91 + }
  92 +
  93 + /**
  94 + * 校验消息表的结构:device_id 为唯一的 TAG,其它列都是类型一致的 FIELD
  95 + *
  96 + * 例如说,旧结构的 id 为 TAG,身份不同,且每条消息会产生一个 IoTDB 设备,所以不兼容
  97 + *
  98 + * @param columns DESC 的结果,包括 ColumnName、DataType、Category
  99 + */
  100 + private static void validateColumns(List<Map<String, Object>> columns) {
  101 + // 1. 解析 DB 里的列:列名 -> 类别、类型
  102 + Map<String, String> categories = new HashMap<>();
  103 + Map<String, String> types = new HashMap<>();
  104 + for (Map<String, Object> column : columns) {
  105 + String name = StrUtil.toStringOrNull(IoTDBUtils.getIgnoreCase(column, "ColumnName"));
  106 + categories.put(name, StrUtil.toStringOrNull(IoTDBUtils.getIgnoreCase(column, "Category")));
  107 + types.put(name, StrUtil.toStringOrNull(IoTDBUtils.getIgnoreCase(column, "DataType")));
  108 + }
  109 +
  110 + // 2.1 TAG 只能是 device_id:多出的 TAG 会改变身份
  111 + List<String> errors = new ArrayList<>();
  112 + Set<String> tags = new TreeSet<>();
  113 + categories.forEach((name, category) -> {
  114 + if (StrUtil.equalsIgnoreCase(category, CATEGORY_TAG)) {
  115 + tags.add(name);
  116 + }
  117 + });
  118 + if (!tags.equals(Collections.singleton(COLUMN_DEVICE_ID))) {
  119 + errors.add(StrUtil.format("TAG 只能为 {},当前为 {}", COLUMN_DEVICE_ID, tags));
  120 + }
  121 + // 2.2 各列的类别、类型
  122 + COLUMN_TYPES.forEach((name, type) -> {
  123 + if (!categories.containsKey(name)) {
  124 + errors.add(StrUtil.format("缺少列 {}", name));
  125 + return;
  126 + }
  127 + String category = COLUMN_DEVICE_ID.equals(name) ? CATEGORY_TAG : CATEGORY_FIELD;
  128 + if (!StrUtil.equalsIgnoreCase(categories.get(name), category)
  129 + || !StrUtil.equalsIgnoreCase(types.get(name), type)) {
  130 + errors.add(StrUtil.format("列 {} 应为 {} {},当前为 {} {}",
  131 + name, type, category, types.get(name), categories.get(name)));
  132 + }
  133 + });
  134 + if (errors.isEmpty()) {
  135 + return;
  136 + }
  137 + throw new IllegalStateException(StrUtil.format("[IoTDB 设备消息表] device_message 的结构与当前版本不兼容:{}。" +
  138 + "请人工处理(例如说,导出需要保留的数据后删除该表,或者更换数据库),应用不会自动修改、删除表",
  139 + String.join(";", errors)));
  140 + }
  141 +
  142 + @Override
  143 + public void insert(IotDeviceMessageDO message) {
  144 + deviceMessageMapper.insert(encode(message));
  145 + }
  146 +
  147 + @Override
  148 + public PageResult<IotDeviceMessageDO> selectPage(IotDeviceMessagePageReqVO reqVO) {
  149 + // 1. 编码查询条件:空字符串与 TDengine 保持一致,视为不过滤
  150 + String deviceId = encodeDeviceId(reqVO.getDeviceId());
  151 + String method = StrUtil.isNotEmpty(reqVO.getMethod()) ? encodeString(reqVO.getMethod()) : null;
  152 + Integer upstream = reqVO.getUpstream() != null ? encodeBoolean(reqVO.getUpstream()) : null;
  153 + Integer reply = reqVO.getReply() != null ? encodeBoolean(reqVO.getReply()) : null;
  154 + String identifier = StrUtil.isNotEmpty(reqVO.getIdentifier()) ? encodeString(reqVO.getIdentifier()) : null;
  155 + try {
  156 + // 2.1 查询总数
  157 + Long total = deviceMessageMapper.selectCount(deviceId, method, upstream, reply, identifier);
  158 + if (total == null || total == 0) {
  159 + return PageResult.empty();
  160 + }
  161 + // 2.2 查询列表:pageSize 为 -1 时,不分页
  162 + boolean pageable = ObjUtil.notEqual(reqVO.getPageSize(), PageParam.PAGE_SIZE_NONE);
  163 + List<IotDeviceMessageIoTDBRow> rows = deviceMessageMapper.selectList(deviceId, method, upstream, reply, identifier,
  164 + pageable ? PageUtils.getStart(reqVO) : null, pageable ? reqVO.getPageSize() : null);
  165 + return new PageResult<>(convertList(rows, IotDeviceMessageIoTDBCodec::decode), total);
  166 + } catch (Exception exception) {
  167 + if (IoTDBUtils.isTableNotExists(exception)) {
  168 + return PageResult.empty();
  169 + }
  170 + throw exception;
  171 + }
  172 + }
  173 +
  174 + @Override
  175 + public List<IotDeviceMessageDO> selectListByRequestIdsAndReply(Long deviceId, Collection<String> requestIds, Boolean reply) {
  176 + // reply 为空时,与 TDengine 的 reply = NULL 一致,匹配不到任何消息
  177 + if (reply == null) {
  178 + return Collections.emptyList();
  179 + }
  180 + // requestId 为空的消息,存储值为 n:,不应被 null 匹配到,所以过滤掉
  181 + List<String> encodedRequestIds = new ArrayList<>();
  182 + if (requestIds != null) {
  183 + requestIds.stream().filter(Objects::nonNull).distinct()
  184 + .forEach(requestId -> encodedRequestIds.add(encodeString(requestId)));
  185 + }
  186 + if (CollUtil.isEmpty(encodedRequestIds)) {
  187 + return Collections.emptyList();
  188 + }
  189 + List<IotDeviceMessageIoTDBRow> rows = deviceMessageMapper.selectListByRequestIdsAndReply(
  190 + encodeDeviceId(deviceId), encodedRequestIds, encodeBoolean(reply));
  191 + return convertList(rows, IotDeviceMessageIoTDBCodec::decode);
  192 + }
  193 +
  194 + @Override
  195 + public Long selectCountByCreateTime(Long createTime) {
  196 + return deviceMessageMapper.selectCountByCreateTime(createTime);
  197 + }
  198 +
  199 + @Override
  200 + public List<Map<String, Object>> selectDeviceMessageCountGroupByDate(Long startTime, Long endTime) {
  201 + List<Map<String, Object>> counts = deviceMessageMapper.selectCountGroupByHour(startTime, endTime);
  202 + return convertList(counts, count -> IotDeviceMessageDAO.buildMessageCount(
  203 + IoTDBUtils.getIgnoreCase(count, COUNT_TIME),
  204 + IoTDBUtils.getIgnoreCase(count, COUNT_UPSTREAM),
  205 + IoTDBUtils.getIgnoreCase(count, COUNT_DOWNSTREAM)));
  206 + }
  207 +
  208 +}
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/iotdb/IotDeviceMessageIoTDBMapper.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.dal.iotdb;
  2 +
  3 +import cn.iocoder.yudao.module.iot.framework.iotdb.core.annotation.IoTDBDS;
  4 +import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
  5 +import org.apache.ibatis.annotations.Mapper;
  6 +import org.apache.ibatis.annotations.Param;
  7 +
  8 +import java.util.Collection;
  9 +import java.util.List;
  10 +import java.util.Map;
  11 +
  12 +/**
  13 + * IoTDB 设备消息 Mapper
  14 + *
  15 + * 注意:参数均为 {@link IotDeviceMessageIoTDBCodec} 编码后的存储值;分页通过手写 LIMIT/OFFSET 实现,不依赖 MyBatis Plus 分页插件
  16 + *
  17 + * @author 芋道源码
  18 + */
  19 +@Mapper
  20 +@IoTDBDS
  21 +@InterceptorIgnore(tenantLine = "true", dataPermission = "true") // 避免 SQL 解析,因为 JSqlParser 对 IoTDB 的 SQL 解析会报错;租户由 Service 层控制
  22 +public interface IotDeviceMessageIoTDBMapper {
  23 +
  24 + /**
  25 + * 获得消息表的列
  26 + *
  27 + * @return 列列表,包括 ColumnName、DataType、Category 等
  28 + */
  29 + List<Map<String, Object>> selectColumnList();
  30 +
  31 + void createTable();
  32 +
  33 + void insert(IotDeviceMessageIoTDBRow row);
  34 +
  35 + List<IotDeviceMessageIoTDBRow> selectList(@Param("deviceId") String deviceId,
  36 + @Param("method") String method,
  37 + @Param("upstream") Integer upstream,
  38 + @Param("reply") Integer reply,
  39 + @Param("identifier") String identifier,
  40 + @Param("offset") Integer offset,
  41 + @Param("limit") Integer limit);
  42 +
  43 + Long selectCount(@Param("deviceId") String deviceId,
  44 + @Param("method") String method,
  45 + @Param("upstream") Integer upstream,
  46 + @Param("reply") Integer reply,
  47 + @Param("identifier") String identifier);
  48 +
  49 + List<IotDeviceMessageIoTDBRow> selectListByRequestIdsAndReply(@Param("deviceId") String deviceId,
  50 + @Param("requestIds") Collection<String> requestIds,
  51 + @Param("reply") Integer reply);
  52 +
  53 + Long selectCountByCreateTime(@Param("createTime") Long createTime);
  54 +
  55 + List<Map<String, Object>> selectCountGroupByHour(@Param("startTime") Long startTime,
  56 + @Param("endTime") Long endTime);
  57 +
  58 +}
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/iotdb/IotDeviceMessageIoTDBRow.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.dal.iotdb;
  2 +
  3 +import lombok.Data;
  4 +import lombok.experimental.Accessors;
  5 +
  6 +/**
  7 + * IoTDB 设备消息表 device_message 的一行,字段为编码后的存储值
  8 + *
  9 + * 身份为 (deviceId, ts);编解码见 {@link IotDeviceMessageIoTDBCodec}
  10 + *
  11 + * @author 芋道源码
  12 + */
  13 +@Data
  14 +@Accessors(chain = true)
  15 +public class IotDeviceMessageIoTDBRow {
  16 +
  17 + /**
  18 + * 存储时间(毫秒),对应隐式 time 列
  19 + */
  20 + private Long ts;
  21 + /**
  22 + * 设备编号,唯一的 TAG
  23 + */
  24 + private String deviceId;
  25 + /**
  26 + * 消息编号,FIELD,原样存储
  27 + */
  28 + private String id;
  29 +
  30 + /**
  31 + * 是否上行:-1 为空,0 为否,1 为是
  32 + */
  33 + private Integer upstream;
  34 + /**
  35 + * 是否回复:-1 为空,0 为否,1 为是
  36 + */
  37 + private Integer reply;
  38 +
  39 + // ========== 以下字段,统一使用 n:/v: 编码 ==========
  40 +
  41 + private String method;
  42 + private String identifier;
  43 + private String requestId;
  44 + private String serverId;
  45 + private String params;
  46 + private String data;
  47 + private String msg;
  48 + private String reportTime;
  49 + private String tenantId;
  50 + private String code;
  51 +
  52 +}
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/iotdb/IotDevicePropertyIoTDBDAO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.dal.iotdb;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.date.LocalDateTimeUtil;
  5 +import cn.hutool.core.map.MapUtil;
  6 +import cn.hutool.core.util.StrUtil;
  7 +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.property.IotDevicePropertyHistoryListReqVO;
  8 +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.property.IotDevicePropertyRespVO;
  9 +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
  10 +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO;
  11 +import cn.iocoder.yudao.module.iot.dal.storage.IotDevicePropertyDAO;
  12 +import cn.iocoder.yudao.module.iot.enums.thingmodel.IotDataSpecsDataTypeEnum;
  13 +import cn.iocoder.yudao.module.iot.framework.iotdb.core.IoTDBUtils;
  14 +import cn.iocoder.yudao.module.iot.framework.storage.config.IotStorageProperties;
  15 +import cn.iocoder.yudao.module.iot.framework.storage.core.util.IotStorageFieldUtils;
  16 +import jakarta.annotation.Resource;
  17 +import lombok.extern.slf4j.Slf4j;
  18 +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
  19 +import org.springframework.stereotype.Repository;
  20 +
  21 +import java.util.*;
  22 +
  23 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
  24 +
  25 +/**
  26 + * 基于 IoTDB(表模型)的 {@link IotDevicePropertyDAO} 实现类
  27 + *
  28 + * 每个产品一张表 product_property_{productId},设备共用产品表,通过 device_id TAG 区分。
  29 + * 用户属性列统一为 p_ + 中立字段名,避免与 time、device_id、report_time 等内置列冲突。
  30 + *
  31 + * 与 TDengine 的差异:
  32 + * 1. STRING 没有长度,物模型 TEXT 的长度变化不会改表;长度由写入前的业务转换校验
  33 + * 2. INT、ENUM、BOOL 都映射为 INT32,三者之间的逻辑类型变化不会改表,历史数据保留
  34 + * 3. 物理类型变化通过 DROP + ADD 完成,旧列的历史数据不会保留
  35 + * 4. IoTDB 的 DDL 不参与事务,也无法回滚;执行中途失败时,再次定义即可收敛(ADD/DROP 均为幂等语句)
  36 + *
  37 + * @author 芋道源码
  38 + */
  39 +@Repository
  40 +@ConditionalOnProperty(prefix = IotStorageProperties.PREFIX, name = "type", havingValue = "iotdb")
  41 +@Slf4j
  42 +public class IotDevicePropertyIoTDBDAO implements IotDevicePropertyDAO {
  43 +
  44 + /**
  45 + * 用户属性列的前缀
  46 + */
  47 + public static final String COLUMN_PREFIX = "p_";
  48 +
  49 + public static final String TYPE_INT32 = "INT32";
  50 + public static final String TYPE_FLOAT = "FLOAT";
  51 + public static final String TYPE_DOUBLE = "DOUBLE";
  52 + public static final String TYPE_STRING = "STRING";
  53 + public static final String TYPE_TIMESTAMP = "TIMESTAMP";
  54 +
  55 + /**
  56 + * DESC 结果中,FIELD 列的类别
  57 + */
  58 + private static final String CATEGORY_FIELD = "FIELD";
  59 +
  60 + /**
  61 + * 物模型的数据类型,与 IoTDB 数据类型的映射关系
  62 + *
  63 + * @see <a href="https://iotdb.apache.org/zh/UserGuide/latest-Table/Background-knowledge/Data-Type.html">IoTDB 数据类型</a>
  64 + */
  65 + private static final Map<String, String> TYPE_MAPPING = MapUtil.<String, String>builder()
  66 + .put(IotDataSpecsDataTypeEnum.INT.getDataType(), TYPE_INT32)
  67 + .put(IotDataSpecsDataTypeEnum.FLOAT.getDataType(), TYPE_FLOAT)
  68 + .put(IotDataSpecsDataTypeEnum.DOUBLE.getDataType(), TYPE_DOUBLE)
  69 + .put(IotDataSpecsDataTypeEnum.ENUM.getDataType(), TYPE_INT32)
  70 + .put(IotDataSpecsDataTypeEnum.BOOL.getDataType(), TYPE_INT32)
  71 + .put(IotDataSpecsDataTypeEnum.TEXT.getDataType(), TYPE_STRING)
  72 + .put(IotDataSpecsDataTypeEnum.DATE.getDataType(), TYPE_TIMESTAMP)
  73 + .put(IotDataSpecsDataTypeEnum.STRUCT.getDataType(), TYPE_STRING)
  74 + .put(IotDataSpecsDataTypeEnum.ARRAY.getDataType(), TYPE_STRING)
  75 + .build();
  76 +
  77 + @Resource
  78 + private IotDevicePropertyIoTDBMapper devicePropertyMapper;
  79 + @Resource
  80 + private IoTDBSystemMapper systemMapper;
  81 +
  82 + @Override
  83 + public void initialize() {
  84 + // 产品属性表在产品发布、手动同步时创建,启动时只校验前提条件
  85 + systemMapper.verifyPrerequisites();
  86 + }
  87 +
  88 + @Override
  89 + public void defineProductPropertyTable(Long productId, List<IotThingModelDO> propertyThingModels) {
  90 + // 1.1 校验属性标识符,可以作为列名
  91 + IotStorageFieldUtils.validatePropertyIdentifiers(convertList(propertyThingModels, IotThingModelDO::getIdentifier));
  92 + List<IotDevicePropertyIoTDBMapper.Column> newColumns = buildColumnList(propertyThingModels);
  93 + // 1.2 解析 DB 里的用户属性列
  94 + Map<String, String> oldColumns = getUserColumnTypes(productId);
  95 +
  96 + // 2.1 情况一:如果是新增的时候,需要创建表
  97 + if (oldColumns == null) {
  98 + if (CollUtil.isEmpty(newColumns)) {
  99 + log.info("[defineProductPropertyTable][productId({}) 没有需要定义的属性]", productId);
  100 + return;
  101 + }
  102 + devicePropertyMapper.createTable(productId, newColumns);
  103 + return;
  104 + }
  105 + // 2.2 情况二:如果是修改的时候,需要更新表
  106 + Set<String> newColumnNames = new HashSet<>(convertList(newColumns, IotDevicePropertyIoTDBMapper.Column::getName));
  107 + // 2.2.1 删除不再需要的列
  108 + oldColumns.keySet().stream().filter(name -> !newColumnNames.contains(name))
  109 + .forEach(name -> devicePropertyMapper.dropColumn(productId, name));
  110 + for (IotDevicePropertyIoTDBMapper.Column column : newColumns) {
  111 + String oldType = oldColumns.get(column.getName());
  112 + // 2.2.2 新增的列
  113 + if (oldType == null) {
  114 + devicePropertyMapper.addColumn(productId, column);
  115 + continue;
  116 + }
  117 + // 2.2.3 物理类型变化的列:IoTDB 不支持修改列类型,只能 DROP + ADD,旧列的数据不会保留
  118 + if (!StrUtil.equalsIgnoreCase(oldType, column.getType())) {
  119 + log.warn("[defineProductPropertyTable][productId({}) 列({}) 类型从 {} 变为 {},删除后重建,历史数据不保留]",
  120 + productId, column.getName(), oldType, column.getType());
  121 + devicePropertyMapper.dropColumn(productId, column.getName());
  122 + devicePropertyMapper.addColumn(productId, column);
  123 + }
  124 + }
  125 + }
  126 +
  127 + /**
  128 + * 获得产品属性表的用户属性列
  129 + *
  130 + * @param productId 产品编号
  131 + * @return 列名与类型的映射;表不存在时,返回 null
  132 + */
  133 + private Map<String, String> getUserColumnTypes(Long productId) {
  134 + List<Map<String, Object>> columns;
  135 + try {
  136 + columns = devicePropertyMapper.selectColumnList(productId);
  137 + } catch (Exception exception) {
  138 + if (IoTDBUtils.isTableNotExists(exception)) {
  139 + return null;
  140 + }
  141 + throw exception;
  142 + }
  143 + // 只比较 p_ 开头的 FIELD 列:time、device_id、report_time 由系统维护,不参与差异
  144 + Map<String, String> userColumns = new LinkedHashMap<>();
  145 + for (Map<String, Object> column : columns) {
  146 + String name = StrUtil.toStringOrNull(IoTDBUtils.getIgnoreCase(column, "ColumnName"));
  147 + String category = StrUtil.toStringOrNull(IoTDBUtils.getIgnoreCase(column, "Category"));
  148 + if (StrUtil.startWith(name, COLUMN_PREFIX) && StrUtil.equalsIgnoreCase(category, CATEGORY_FIELD)) {
  149 + userColumns.put(name, StrUtil.toStringOrNull(IoTDBUtils.getIgnoreCase(column, "DataType")));
  150 + }
  151 + }
  152 + return userColumns;
  153 + }
  154 +
  155 + private List<IotDevicePropertyIoTDBMapper.Column> buildColumnList(List<IotThingModelDO> thingModels) {
  156 + return convertList(thingModels, thingModel -> {
  157 + String type = TYPE_MAPPING.get(thingModel.getProperty().getDataType());
  158 + if (type == null) {
  159 + throw new IllegalArgumentException(StrUtil.format("属性({}) 的数据类型({}) 不支持存储",
  160 + thingModel.getIdentifier(), thingModel.getProperty().getDataType()));
  161 + }
  162 + return new IotDevicePropertyIoTDBMapper.Column(buildColumnName(thingModel.getIdentifier()), type, null);
  163 + });
  164 + }
  165 +
  166 + @Override
  167 + public void insert(IotDeviceDO device, Map<String, Object> properties, Long ts, Long reportTime) {
  168 + // 校验属性标识符:在拼接 SQL 之前,避免保留字段、归一化后重名的属性写入
  169 + IotStorageFieldUtils.validatePropertyIdentifiers(properties.keySet());
  170 + if (CollUtil.isEmpty(properties)) {
  171 + return;
  172 + }
  173 + List<IotDevicePropertyIoTDBMapper.Column> columns = new ArrayList<>(properties.size());
  174 + properties.forEach((identifier, value) -> columns.add(
  175 + new IotDevicePropertyIoTDBMapper.Column(buildColumnName(identifier), null, convertValue(value))));
  176 + devicePropertyMapper.insert(device.getProductId(), String.valueOf(device.getId()), ts, reportTime, columns);
  177 + }
  178 +
  179 + /**
  180 + * 转换属性值为 IoTDB 驱动支持的类型
  181 + *
  182 + * 原因:ENUM、BOOL 业务转换后为 Byte(对应 TDengine 的 TINYINT),而 IoTDB 驱动不支持 setByte,需要转为 INT32 对应的 Integer
  183 + */
  184 + private static Object convertValue(Object value) {
  185 + if (value instanceof Byte) {
  186 + return ((Byte) value).intValue();
  187 + }
  188 + return value;
  189 + }
  190 +
  191 + @Override
  192 + public List<IotDevicePropertyRespVO> selectHistoryList(Long productId, IotDevicePropertyHistoryListReqVO reqVO) {
  193 + try {
  194 + return devicePropertyMapper.selectHistoryList(productId, buildColumnName(reqVO.getIdentifier()),
  195 + String.valueOf(reqVO.getDeviceId()),
  196 + LocalDateTimeUtil.toEpochMilli(reqVO.getTimes()[0]),
  197 + LocalDateTimeUtil.toEpochMilli(reqVO.getTimes()[1]));
  198 + } catch (Exception exception) {
  199 + // 只有表不存在(产品尚未定义属性)返回空;列不存在等其它错误原样抛出
  200 + if (IoTDBUtils.isTableNotExists(exception)) {
  201 + return Collections.emptyList();
  202 + }
  203 + throw exception;
  204 + }
  205 + }
  206 +
  207 + /**
  208 + * 构建属性列名:p_ + 中立字段名
  209 + *
  210 + * @param identifier 属性标识符,调用前需已通过 {@link IotStorageFieldUtils#validatePropertyIdentifiers(Collection)} 校验
  211 + * @return 列名
  212 + */
  213 + public static String buildColumnName(String identifier) {
  214 + if (!IotStorageFieldUtils.isPropertyFieldName(identifier)) {
  215 + throw new IllegalArgumentException(StrUtil.format("属性标识符({}) 不能作为存储列", identifier));
  216 + }
  217 + return COLUMN_PREFIX + IotStorageFieldUtils.buildFieldName(identifier);
  218 + }
  219 +
  220 +}
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/iotdb/IotDevicePropertyIoTDBMapper.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.dal.iotdb;
  2 +
  3 +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.property.IotDevicePropertyRespVO;
  4 +import cn.iocoder.yudao.module.iot.framework.iotdb.core.annotation.IoTDBDS;
  5 +import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
  6 +import lombok.AllArgsConstructor;
  7 +import lombok.Data;
  8 +import lombok.NoArgsConstructor;
  9 +import org.apache.ibatis.annotations.Mapper;
  10 +import org.apache.ibatis.annotations.Param;
  11 +
  12 +import java.util.List;
  13 +import java.util.Map;
  14 +
  15 +/**
  16 + * IoTDB 设备属性 Mapper
  17 + *
  18 + * 注意:表名、列名需要动态拼接,只允许传入 Long 产品编号、DAO 归一化并校验后的列名、白名单内的类型;值统一使用 #{} 绑定
  19 + *
  20 + * @author 芋道源码
  21 + */
  22 +@Mapper
  23 +@IoTDBDS
  24 +@InterceptorIgnore(tenantLine = "true", dataPermission = "true") // 避免 SQL 解析,因为 JSqlParser 对 IoTDB 的 SQL 解析会报错;租户由 Service 层控制
  25 +public interface IotDevicePropertyIoTDBMapper {
  26 +
  27 + /**
  28 + * 获得产品属性表的列
  29 + *
  30 + * @param productId 产品编号
  31 + * @return 列列表,包括 ColumnName、DataType、Category 等
  32 + */
  33 + List<Map<String, Object>> selectColumnList(@Param("productId") Long productId);
  34 +
  35 + void createTable(@Param("productId") Long productId,
  36 + @Param("columns") List<Column> columns);
  37 +
  38 + void addColumn(@Param("productId") Long productId,
  39 + @Param("column") Column column);
  40 +
  41 + void dropColumn(@Param("productId") Long productId,
  42 + @Param("columnName") String columnName);
  43 +
  44 + void insert(@Param("productId") Long productId,
  45 + @Param("deviceId") String deviceId,
  46 + @Param("ts") Long ts,
  47 + @Param("reportTime") Long reportTime,
  48 + @Param("columns") List<Column> columns);
  49 +
  50 + List<IotDevicePropertyRespVO> selectHistoryList(@Param("productId") Long productId,
  51 + @Param("columnName") String columnName,
  52 + @Param("deviceId") String deviceId,
  53 + @Param("startTime") Long startTime,
  54 + @Param("endTime") Long endTime);
  55 +
  56 + /**
  57 + * 属性列
  58 + */
  59 + @Data
  60 + @NoArgsConstructor
  61 + @AllArgsConstructor
  62 + class Column {
  63 +
  64 + /**
  65 + * 列名,格式为 p_ + 中立字段名
  66 + */
  67 + private String name;
  68 + /**
  69 + * 数据类型,例如说 INT32、STRING;写入时为空
  70 + */
  71 + private String type;
  72 + /**
  73 + * 值;DDL 时为空
  74 + */
  75 + private Object value;
  76 +
  77 + }
  78 +
  79 +}
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/storage/IotDeviceMessageDAO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.dal.storage;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  4 +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.message.IotDeviceMessagePageReqVO;
  5 +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceMessageDO;
  6 +
  7 +import javax.annotation.Nullable;
  8 +import java.sql.Timestamp;
  9 +import java.util.*;
  10 +
  11 +/**
  12 + * IoT 设备【消息】日志的存储 DAO 接口
  13 + *
  14 + * 由 yudao.iot.storage.type 选择 TDengine、IoTDB 其中一个实现。各实现的消息身份不同:
  15 + * 1. TDengine:(deviceId, ts, id),按 ts DESC, id DESC 排序。同一设备、同一毫秒、不同 id 的消息都会保留
  16 + * 2. IoTDB:(deviceId, ts),id 为普通字段,按 ts DESC 排序。同一设备、同一毫秒再次写入时,后写的整行替换先写的,即使 id 不同。
  17 + * 原因:id 作为 TAG 时,每个设备下每出现一个新的消息 id,就产生一个 IoTDB 元数据设备,元数据随消息量无限增长
  18 + *
  19 + * 重复写入时:
  20 + * 1. 相同 (deviceId, ts, id):两者都只保留一行。IoTDB 由编码保证整行替换(null 字段也会清空旧值);TDengine 沿用原有写入语义
  21 + * 2. 重投、重发等重新获取 ts 的写入,按各自的物理键判断:TDengine 为 (deviceId, ts, id),与已有行都不同时新增,否则覆盖该行;
  22 + * IoTDB 为 (deviceId, ts),该设备该毫秒没有行时新增,已有行时覆盖该行(可能是另一条消息)
  23 + *
  24 + * @author 芋道源码
  25 + */
  26 +public interface IotDeviceMessageDAO {
  27 +
  28 + /**
  29 + * 按小时统计结果的 key:整点时间,类型为 {@link Timestamp}
  30 + */
  31 + String COUNT_TIME = "time";
  32 + /**
  33 + * 按小时统计结果的 key:上行消息数,类型为 {@link Long}
  34 + */
  35 + String COUNT_UPSTREAM = "upstream_count";
  36 + /**
  37 + * 按小时统计结果的 key:下行消息数,类型为 {@link Long}
  38 + */
  39 + String COUNT_DOWNSTREAM = "downstream_count";
  40 +
  41 + /**
  42 + * 初始化:系统启动时,校验存储后端的前提条件,并创建消息表
  43 + */
  44 + void initialize();
  45 +
  46 + /**
  47 + * 写入设备消息
  48 + *
  49 + * @param message 设备消息
  50 + */
  51 + void insert(IotDeviceMessageDO message);
  52 +
  53 + /**
  54 + * 获得设备消息分页
  55 + *
  56 + * 注意:与现有行为保持一致,不按 reqVO.times 过滤
  57 + *
  58 + * @param reqVO 分页查询条件
  59 + * @return 设备消息分页;消息表不存在时,返回空分页
  60 + */
  61 + PageResult<IotDeviceMessageDO> selectPage(IotDeviceMessagePageReqVO reqVO);
  62 +
  63 + /**
  64 + * 按照 requestIds 批量查询消息
  65 + *
  66 + * 各存储后端保持一致:requestIds 中的 null 忽略、重复去重;忽略后为空,或 reply 为 null 时,返回空列表
  67 + *
  68 + * @param deviceId 设备编号
  69 + * @param requestIds 请求编号集合
  70 + * @param reply 是否回复消息
  71 + * @return 消息列表
  72 + */
  73 + List<IotDeviceMessageDO> selectListByRequestIdsAndReply(Long deviceId, Collection<String> requestIds, Boolean reply);
  74 +
  75 + /**
  76 + * 统计设备消息数量
  77 + *
  78 + * @param createTime 创建时间(毫秒),如果为空,则统计所有消息数量
  79 + * @return 消息数量
  80 + */
  81 + Long selectCountByCreateTime(@Nullable Long createTime);
  82 +
  83 + /**
  84 + * 按照时间范围(小时),统计设备的消息数量
  85 + *
  86 + * @param startTime 开始时间(毫秒),包含
  87 + * @param endTime 结束时间(毫秒),包含
  88 + * @return 每个整点的统计,key 见 {@link #COUNT_TIME}、{@link #COUNT_UPSTREAM}、{@link #COUNT_DOWNSTREAM}
  89 + */
  90 + List<Map<String, Object>> selectDeviceMessageCountGroupByDate(Long startTime, Long endTime);
  91 +
  92 + /**
  93 + * 构建按小时统计的一行结果,统一各存储后端的 key 与 Java 类型
  94 + *
  95 + * @param time 整点时间,支持 Timestamp、毫秒数值
  96 + * @param upstreamCount 上行消息数,支持任意 Number
  97 + * @param downstreamCount 下行消息数,支持任意 Number
  98 + * @return 统计结果
  99 + */
  100 + @SuppressWarnings("PatternVariableCanBeUsed") // 保持 JDK8 兼容性
  101 + static Map<String, Object> buildMessageCount(Object time, Object upstreamCount, Object downstreamCount) {
  102 + // 1. 整点时间
  103 + Timestamp timestamp;
  104 + if (time instanceof Timestamp) {
  105 + timestamp = (Timestamp) time;
  106 + } else if (time instanceof Date) {
  107 + timestamp = new Timestamp(((Date) time).getTime());
  108 + } else if (time instanceof Number) {
  109 + timestamp = new Timestamp(((Number) time).longValue());
  110 + } else {
  111 + throw new IllegalArgumentException("无法转换为整点时间:" + time);
  112 + }
  113 + // 2. 上下行数量:SUM、COUNT 等聚合结果的 Number 子类因库而异,统一转为 Long
  114 + Map<String, Object> count = new HashMap<>();
  115 + count.put(COUNT_TIME, timestamp);
  116 + count.put(COUNT_UPSTREAM, upstreamCount != null ? ((Number) upstreamCount).longValue() : 0L);
  117 + count.put(COUNT_DOWNSTREAM, downstreamCount != null ? ((Number) downstreamCount).longValue() : 0L);
  118 + return count;
  119 + }
  120 +
  121 +}
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/storage/IotDevicePropertyDAO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.dal.storage;
  2 +
  3 +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.property.IotDevicePropertyHistoryListReqVO;
  4 +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.property.IotDevicePropertyRespVO;
  5 +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
  6 +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO;
  7 +
  8 +import java.util.List;
  9 +import java.util.Map;
  10 +
  11 +// TODO DONE @AI:storage 包下,是不是加个 package-info.java,说明这个包的作用?已新增 package-info.java
  12 +/**
  13 + * IoT 设备【属性】历史数据的存储 DAO 接口
  14 + *
  15 + * 由 yudao.iot.storage.type 选择 TDengine、IoTDB 其中一个实现。
  16 + * 厂商相关的字段映射、DDL、字段差异、缺表判断等,都在实现类中处理。
  17 + *
  18 + * @author 芋道源码
  19 + */
  20 +public interface IotDevicePropertyDAO {
  21 +
  22 + /**
  23 + * 初始化:系统启动时,校验存储后端的前提条件
  24 + */
  25 + void initialize();
  26 +
  27 + /**
  28 + * 定义(创建或修改)产品的属性表结构
  29 + *
  30 + * @param productId 产品编号
  31 + * @param propertyThingModels 产品的属性类物模型列表
  32 + */
  33 + void defineProductPropertyTable(Long productId, List<IotThingModelDO> propertyThingModels);
  34 +
  35 + /**
  36 + * 写入设备属性。只记录本次上报的属性
  37 + *
  38 + * @param device 设备
  39 + * @param properties 属性,key 为物模型 identifier,value 为 Service 转换后的值
  40 + * @param ts 存储时间(毫秒)
  41 + * @param reportTime 上报时间(毫秒)
  42 + */
  43 + void insert(IotDeviceDO device, Map<String, Object> properties, Long ts, Long reportTime);
  44 +
  45 + /**
  46 + * 查询设备属性的历史数据,按存储时间倒序
  47 + *
  48 + * 调用前,Service 已经校验:设备存在(包括逻辑删除)、属性标识符合法、时间范围完整
  49 + *
  50 + * @param productId 产品编号
  51 + * @param reqVO 查询条件
  52 + * @return 历史数据;属性表不存在时,返回空列表
  53 + */
  54 + List<IotDevicePropertyRespVO> selectHistoryList(Long productId, IotDevicePropertyHistoryListReqVO reqVO);
  55 +
  56 +}
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/storage/package-info.java 0 → 100644
  1 +/**
  2 + * iot 模块的时序数据存储 DAO 接口:设备属性、设备消息的统一契约,与具体的时序库无关
  3 + *
  4 + * 1. Service 只依赖本包的 DAO 接口
  5 + * 2. 实现类分别在 dal/tdengine、dal/iotdb 包下,由 yudao.iot.storage.type 统一选择其中一个
  6 + */
  7 +package cn.iocoder.yudao.module.iot.dal.storage;
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/tdengine/IotDeviceMessageTDengineDAO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.dal.tdengine;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.util.StrUtil;
  5 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  6 +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.message.IotDeviceMessagePageReqVO;
  7 +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceMessageDO;
  8 +import cn.iocoder.yudao.module.iot.dal.storage.IotDeviceMessageDAO;
  9 +import cn.iocoder.yudao.module.iot.framework.storage.config.IotStorageProperties;
  10 +import com.baomidou.mybatisplus.core.metadata.IPage;
  11 +import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
  12 +import jakarta.annotation.Resource;
  13 +import lombok.extern.slf4j.Slf4j;
  14 +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
  15 +import org.springframework.stereotype.Repository;
  16 +
  17 +import java.util.Collection;
  18 +import java.util.Collections;
  19 +import java.util.List;
  20 +import java.util.Map;
  21 +import java.util.Objects;
  22 +import java.util.stream.Collectors;
  23 +
  24 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
  25 +
  26 +/**
  27 + * 基于 TDengine 的 {@link IotDeviceMessageDAO} 实现类
  28 + *
  29 + * 一张超级表 device_message,每个设备一张子表 device_message_{deviceId}
  30 + *
  31 + * @author 芋道源码
  32 + */
  33 +@Repository
  34 +@ConditionalOnProperty(prefix = IotStorageProperties.PREFIX, name = "type", havingValue = "tdengine", matchIfMissing = true)
  35 +@Slf4j
  36 +public class IotDeviceMessageTDengineDAO implements IotDeviceMessageDAO {
  37 +
  38 + @Resource
  39 + private IotDeviceMessageMapper deviceMessageMapper;
  40 +
  41 + @Override
  42 + public void initialize() {
  43 + if (StrUtil.isNotEmpty(deviceMessageMapper.showSTable())) {
  44 + log.info("[initialize][设备消息超级表已存在,创建跳过]");
  45 + return;
  46 + }
  47 + log.info("[initialize][设备消息超级表不存在,创建开始...]");
  48 + deviceMessageMapper.createSTable();
  49 + log.info("[initialize][设备消息超级表不存在,创建成功]");
  50 + }
  51 +
  52 + @Override
  53 + public void insert(IotDeviceMessageDO message) {
  54 + deviceMessageMapper.insert(message);
  55 + }
  56 +
  57 + @Override
  58 + public PageResult<IotDeviceMessageDO> selectPage(IotDeviceMessagePageReqVO reqVO) {
  59 + try {
  60 + IPage<IotDeviceMessageDO> page = deviceMessageMapper.selectPage(
  61 + new Page<>(reqVO.getPageNo(), reqVO.getPageSize()), reqVO);
  62 + return new PageResult<>(fillDeviceId(page.getRecords(), reqVO.getDeviceId()), page.getTotal());
  63 + } catch (Exception exception) {
  64 + if (StrUtil.contains(exception.getMessage(), "Table does not exist")) {
  65 + return PageResult.empty();
  66 + }
  67 + throw exception;
  68 + }
  69 + }
  70 +
  71 + @Override
  72 + public List<IotDeviceMessageDO> selectListByRequestIdsAndReply(Long deviceId, Collection<String> requestIds, Boolean reply) {
  73 + // reply 为空时,reply = NULL 匹配不到任何消息,无需查询
  74 + if (reply == null || requestIds == null) {
  75 + return Collections.emptyList();
  76 + }
  77 + // 与 IoTDB 保持一致:忽略 null、去重,避免 IN 列表中绑定 NULL
  78 + List<String> validRequestIds = requestIds.stream().filter(Objects::nonNull).distinct()
  79 + .collect(Collectors.toList());
  80 + if (CollUtil.isEmpty(validRequestIds)) {
  81 + return Collections.emptyList();
  82 + }
  83 + return fillDeviceId(deviceMessageMapper.selectListByRequestIdsAndReply(deviceId, validRequestIds, reply), deviceId);
  84 + }
  85 +
  86 + /**
  87 + * 补全消息的设备编号
  88 + *
  89 + * 原因:查询的是设备子表 device_message_{deviceId},SELECT 未包含 device_id 标签,读出的 deviceId 为空
  90 + *
  91 + * @param messages 消息列表
  92 + * @param deviceId 设备编号
  93 + * @return 消息列表
  94 + */
  95 + private static List<IotDeviceMessageDO> fillDeviceId(List<IotDeviceMessageDO> messages, Long deviceId) {
  96 + if (messages != null) {
  97 + messages.forEach(message -> message.setDeviceId(deviceId));
  98 + }
  99 + return messages;
  100 + }
  101 +
  102 + @Override
  103 + public Long selectCountByCreateTime(Long createTime) {
  104 + return deviceMessageMapper.selectCountByCreateTime(createTime);
  105 + }
  106 +
  107 + @Override
  108 + public List<Map<String, Object>> selectDeviceMessageCountGroupByDate(Long startTime, Long endTime) {
  109 + List<Map<String, Object>> counts = deviceMessageMapper.selectDeviceMessageCountGroupByDate(startTime, endTime);
  110 + return convertList(counts, count -> IotDeviceMessageDAO.buildMessageCount(
  111 + count.get(COUNT_TIME), count.get(COUNT_UPSTREAM), count.get(COUNT_DOWNSTREAM)));
  112 + }
  113 +
  114 +}
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/tdengine/IotDevicePropertyTDengineDAO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.dal.tdengine;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.map.MapUtil;
  5 +import cn.hutool.core.util.StrUtil;
  6 +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
  7 +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.property.IotDevicePropertyHistoryListReqVO;
  8 +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.property.IotDevicePropertyRespVO;
  9 +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
  10 +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO;
  11 +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.ThingModelProperty;
  12 +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.dataType.ThingModelDateOrTextDataSpecs;
  13 +import cn.iocoder.yudao.module.iot.dal.storage.IotDevicePropertyDAO;
  14 +import cn.iocoder.yudao.module.iot.enums.thingmodel.IotDataSpecsDataTypeEnum;
  15 +import cn.iocoder.yudao.module.iot.framework.storage.config.IotStorageProperties;
  16 +import cn.iocoder.yudao.module.iot.framework.storage.core.util.IotStorageFieldUtils;
  17 +import cn.iocoder.yudao.module.iot.framework.tdengine.core.TDengineTableField;
  18 +import jakarta.annotation.Resource;
  19 +import lombok.extern.slf4j.Slf4j;
  20 +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
  21 +import org.springframework.stereotype.Repository;
  22 +
  23 +import java.util.*;
  24 +
  25 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
  26 +
  27 +/**
  28 + * 基于 TDengine 的 {@link IotDevicePropertyDAO} 实现类
  29 + *
  30 + * 每个产品一张超级表 product_property_{productId},每个设备一张子表 device_property_{deviceId}
  31 + *
  32 + * @author 芋道源码
  33 + */
  34 +@Repository
  35 +@ConditionalOnProperty(prefix = IotStorageProperties.PREFIX, name = "type", havingValue = "tdengine", matchIfMissing = true)
  36 +@Slf4j
  37 +public class IotDevicePropertyTDengineDAO implements IotDevicePropertyDAO {
  38 +
  39 + /**
  40 + * 物模型的数据类型,与 TDengine 数据类型的映射关系
  41 + *
  42 + * @see <a href="https://docs.taosdata.com/reference/taos-sql/data-type/">TDEngine 数据类型</a>
  43 + */
  44 + private static final Map<String, String> TYPE_MAPPING = MapUtil.<String, String>builder()
  45 + .put(IotDataSpecsDataTypeEnum.INT.getDataType(), TDengineTableField.TYPE_INT)
  46 + .put(IotDataSpecsDataTypeEnum.FLOAT.getDataType(), TDengineTableField.TYPE_FLOAT)
  47 + .put(IotDataSpecsDataTypeEnum.DOUBLE.getDataType(), TDengineTableField.TYPE_DOUBLE)
  48 + .put(IotDataSpecsDataTypeEnum.ENUM.getDataType(), TDengineTableField.TYPE_TINYINT)
  49 + .put(IotDataSpecsDataTypeEnum.BOOL.getDataType(), TDengineTableField.TYPE_TINYINT)
  50 + .put(IotDataSpecsDataTypeEnum.TEXT.getDataType(), TDengineTableField.TYPE_VARCHAR)
  51 + .put(IotDataSpecsDataTypeEnum.DATE.getDataType(), TDengineTableField.TYPE_TIMESTAMP)
  52 + .put(IotDataSpecsDataTypeEnum.STRUCT.getDataType(), TDengineTableField.TYPE_VARCHAR)
  53 + .put(IotDataSpecsDataTypeEnum.ARRAY.getDataType(), TDengineTableField.TYPE_VARCHAR)
  54 + .build();
  55 +
  56 + @Resource
  57 + private IotDevicePropertyMapper devicePropertyMapper;
  58 +
  59 + @Override
  60 + public void initialize() {
  61 + // 产品属性表在产品发布、手动同步时创建,启动时无需初始化
  62 + }
  63 +
  64 + @Override
  65 + public void defineProductPropertyTable(Long productId, List<IotThingModelDO> propertyThingModels) {
  66 + // 1.1 校验属性标识符,可以作为字段名
  67 + IotStorageFieldUtils.validatePropertyIdentifiers(convertList(propertyThingModels, IotThingModelDO::getIdentifier));
  68 + // 1.2 解析 DB 里的字段
  69 + List<TDengineTableField> oldFields = new ArrayList<>();
  70 + try {
  71 + oldFields.addAll(devicePropertyMapper.getProductPropertySTableFieldList(productId));
  72 + } catch (Exception e) {
  73 + if (!isTableNotExists(e)) {
  74 + throw e;
  75 + }
  76 + }
  77 +
  78 + // 2.1 情况一:如果是新增的时候,需要创建表
  79 + List<TDengineTableField> newFields = buildTableFieldList(propertyThingModels);
  80 + if (CollUtil.isEmpty(oldFields)) {
  81 + if (CollUtil.isEmpty(newFields)) {
  82 + log.info("[defineProductPropertyTable][productId({}) 没有需要定义的属性]", productId);
  83 + return;
  84 + }
  85 + devicePropertyMapper.createProductPropertySTable(productId, newFields);
  86 + return;
  87 + }
  88 + // 2.2 情况二:如果是修改的时候,需要更新表
  89 + devicePropertyMapper.alterProductPropertySTable(productId, oldFields, newFields);
  90 + }
  91 +
  92 + private List<TDengineTableField> buildTableFieldList(List<IotThingModelDO> thingModels) {
  93 + return convertList(thingModels, thingModel -> {
  94 + TDengineTableField field = new TDengineTableField(
  95 + IotStorageFieldUtils.buildFieldName(thingModel.getIdentifier()),
  96 + TYPE_MAPPING.get(thingModel.getProperty().getDataType()));
  97 + String dataType = thingModel.getProperty().getDataType();
  98 + if (Objects.equals(dataType, IotDataSpecsDataTypeEnum.TEXT.getDataType())) {
  99 + field.setLength(((ThingModelDateOrTextDataSpecs) thingModel.getProperty().getDataSpecs()).getLength());
  100 + } else if (ObjectUtils.equalsAny(dataType, IotDataSpecsDataTypeEnum.STRUCT.getDataType(),
  101 + IotDataSpecsDataTypeEnum.ARRAY.getDataType())) {
  102 + field.setLength(ThingModelProperty.LENGTH_JSON);
  103 + }
  104 + return field;
  105 + });
  106 + }
  107 +
  108 + @Override
  109 + public void insert(IotDeviceDO device, Map<String, Object> properties, Long ts, Long reportTime) {
  110 + // 校验属性标识符:在拼接 SQL 之前,避免保留字段、归一化后重名的属性写入
  111 + IotStorageFieldUtils.validatePropertyIdentifiers(properties.keySet());
  112 + devicePropertyMapper.insert(device, properties, ts, reportTime);
  113 + }
  114 +
  115 + @Override
  116 + public List<IotDevicePropertyRespVO> selectHistoryList(Long productId, IotDevicePropertyHistoryListReqVO reqVO) {
  117 + try {
  118 + return devicePropertyMapper.selectListByHistory(reqVO);
  119 + } catch (Exception exception) {
  120 + if (isTableNotExists(exception)) {
  121 + return Collections.emptyList();
  122 + }
  123 + throw exception;
  124 + }
  125 + }
  126 +
  127 + private static boolean isTableNotExists(Exception exception) {
  128 + return StrUtil.contains(exception.getMessage(), "Table does not exist");
  129 + }
  130 +
  131 +}
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java
... ... @@ -46,6 +46,11 @@ public interface ErrorCodeConstants {
46 46 ErrorCode DEVICE_REGISTER_DISABLED = new ErrorCode(1_050_003_210, "该产品未开启动态注册功能");
47 47 ErrorCode DEVICE_REGISTER_SECRET_INVALID = new ErrorCode(1_050_003_211, "产品密钥验证失败");
48 48 ErrorCode DEVICE_REGISTER_ALREADY_EXISTS = new ErrorCode(1_050_003_212, "设备已存在,不允许重复注册");
  49 + // 设备属性存储相关错误码 1-050-003-300
  50 + ErrorCode DEVICE_PROPERTY_HISTORY_TIMES_INVALID = new ErrorCode(1_050_003_300, "设备属性历史的时间范围,必须包含开始、结束两个时间");
  51 + ErrorCode DEVICE_PROPERTY_FIELD_NAME_INVALID = new ErrorCode(1_050_003_301, "属性标识符({}) 不合法,无法作为存储字段");
  52 + ErrorCode DEVICE_PROPERTY_FIELD_NAME_RESERVED = new ErrorCode(1_050_003_302, "属性标识符({}) 与存储保留字段({}) 冲突");
  53 + ErrorCode DEVICE_PROPERTY_FIELD_NAME_DUPLICATE = new ErrorCode(1_050_003_303, "属性标识符({}) 与({}) 的存储字段({}) 重复");
49 54  
50 55 // ========== 产品分类 1-050-004-000 ==========
51 56 ErrorCode PRODUCT_CATEGORY_NOT_EXISTS = new ErrorCode(1_050_004_000, "产品分类不存在");
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/iotdb/core/IoTDBUtils.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.framework.iotdb.core;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.util.StrUtil;
  5 +import org.apache.iotdb.rpc.StatementExecutionException;
  6 +
  7 +import java.sql.SQLException;
  8 +import java.util.*;
  9 +
  10 +/**
  11 + * IoTDB 工具类
  12 + *
  13 + * 1. 异常分类:识别“表不存在”
  14 + * 2. 启动前提检查:只读取配置并校验,不修改 IoTDB 的任何配置
  15 + *
  16 + * @author 芋道源码
  17 + */
  18 +public class IoTDBUtils {
  19 +
  20 + /**
  21 + * IoTDB 状态码 - 表不存在
  22 + */
  23 + public static final int STATUS_CODE_TABLE_NOT_EXISTS = 550;
  24 +
  25 + /**
  26 + * 要求的时间精度:毫秒
  27 + */
  28 + public static final String TIMESTAMP_PRECISION_MS = "ms";
  29 + /**
  30 + * 要求关闭的配置项:部分写入。开启时,一行中类型错误的列会失败,其它列仍然写入
  31 + */
  32 + public static final String CONFIG_ENABLE_PARTIAL_INSERT = "enable_partial_insert";
  33 + /**
  34 + * DataNode 的运行状态
  35 + */
  36 + public static final String DATA_NODE_STATUS_RUNNING = "Running";
  37 +
  38 + // ========== 异常分类 ==========
  39 +
  40 + /**
  41 + * 判断异常是否为“表不存在”
  42 + *
  43 + * 只识别明确的 550 状态码,并沿 cause 链查找:外层 SQLException 的 errorCode 可能为 0,
  44 + * 真实状态码在内层的 SQLException 或 {@link StatementExecutionException} 中。
  45 + * 列不存在(616)、类型不匹配(507)、权限等其它错误,都返回 false,由调用方原样抛出。
  46 + *
  47 + * @param throwable 异常
  48 + * @return 是否表不存在
  49 + */
  50 + @SuppressWarnings("PatternVariableCanBeUsed") // 保持 JDK8 兼容性
  51 + public static boolean isTableNotExists(Throwable throwable) {
  52 + Set<Throwable> visited = Collections.newSetFromMap(new IdentityHashMap<>());
  53 + for (Throwable cause = throwable; cause != null && visited.add(cause); cause = cause.getCause()) {
  54 + if (cause instanceof StatementExecutionException
  55 + && ((StatementExecutionException) cause).getStatusCode() == STATUS_CODE_TABLE_NOT_EXISTS) {
  56 + return true;
  57 + }
  58 + if (cause instanceof SQLException
  59 + && ((SQLException) cause).getErrorCode() == STATUS_CODE_TABLE_NOT_EXISTS) {
  60 + return true;
  61 + }
  62 + }
  63 + return false;
  64 + }
  65 +
  66 + // ========== 启动前提检查 ==========
  67 +
  68 + /**
  69 + * 校验时间精度为毫秒
  70 + *
  71 + * @param variables SHOW VARIABLES 的结果
  72 + */
  73 + public static void validateTimestampPrecision(List<Map<String, Object>> variables) {
  74 + Map<String, Object> variable = CollUtil.findOne(variables,
  75 + row -> StrUtil.equalsIgnoreCase(getString(row, "Variable"), "TimestampPrecision"));
  76 + if (variable == null) {
  77 + throw new IllegalStateException("[IoTDB 前提检查] 无法读取 TimestampPrecision,请确认账号有查询权限");
  78 + }
  79 + String precision = getString(variable, "Value");
  80 + if (!StrUtil.equals(TIMESTAMP_PRECISION_MS, precision)) {
  81 + throw new IllegalStateException(StrUtil.format(
  82 + "[IoTDB 前提检查] TimestampPrecision 必须为 ms,当前为 {}", precision));
  83 + }
  84 + }
  85 +
  86 + /**
  87 + * 校验所有 DataNode 都处于运行状态,并返回它们的编号
  88 + *
  89 + * @param dataNodes SHOW DATANODES 的结果
  90 + * @return DataNode 编号列表
  91 + */
  92 + public static List<Integer> validateDataNodes(List<Map<String, Object>> dataNodes) {
  93 + if (CollUtil.isEmpty(dataNodes)) {
  94 + throw new IllegalStateException("[IoTDB 前提检查] 无法读取 DataNode 列表,请确认账号有查询权限");
  95 + }
  96 + List<Integer> nodeIds = new ArrayList<>(dataNodes.size());
  97 + for (Map<String, Object> dataNode : dataNodes) {
  98 + Object nodeId = getIgnoreCase(dataNode, "NodeID");
  99 + String status = getString(dataNode, "Status");
  100 + if (!(nodeId instanceof Number)) {
  101 + throw new IllegalStateException(StrUtil.format("[IoTDB 前提检查] 无法识别 DataNode 编号:{}", dataNode));
  102 + }
  103 + if (!StrUtil.equalsIgnoreCase(DATA_NODE_STATUS_RUNNING, status)) {
  104 + throw new IllegalStateException(StrUtil.format(
  105 + "[IoTDB 前提检查] DataNode({}) 状态为 {},必须全部为 Running 才能确认部分写入配置", nodeId, status));
  106 + }
  107 + nodeIds.add(((Number) nodeId).intValue());
  108 + }
  109 + return nodeIds;
  110 + }
  111 +
  112 + /**
  113 + * 校验 DataNode 关闭了部分写入(enable_partial_insert=false)
  114 + *
  115 + * @param nodeId DataNode 编号
  116 + * @param configurations SHOW ALL CONFIGURATION ON nodeId 的结果
  117 + */
  118 + public static void validatePartialInsertDisabled(Integer nodeId, List<Map<String, Object>> configurations) {
  119 + Map<String, Object> configuration = CollUtil.findOne(configurations,
  120 + row -> StrUtil.equals(getString(row, "name"), CONFIG_ENABLE_PARTIAL_INSERT));
  121 + if (configuration == null) {
  122 + throw new IllegalStateException(StrUtil.format(
  123 + "[IoTDB 前提检查] DataNode({}) 无法读取 {} 配置,请确认 IoTDB 版本与账号权限", nodeId, CONFIG_ENABLE_PARTIAL_INSERT));
  124 + }
  125 + String value = getString(configuration, "value");
  126 + if (!StrUtil.equalsIgnoreCase(Boolean.FALSE.toString(), value)) {
  127 + throw new IllegalStateException(StrUtil.format(
  128 + "[IoTDB 前提检查] DataNode({}) 的 {} 必须为 false,当前为 {}。请修改所有 DataNode 的配置文件,应用不会自动修改",
  129 + nodeId, CONFIG_ENABLE_PARTIAL_INSERT, value));
  130 + }
  131 + }
  132 +
  133 + /**
  134 + * 忽略大小写,获得结果行的列值。原因:IoTDB 系统语句的列名为 NodeID、Variable 等,不同版本大小写可能不同
  135 + */
  136 + public static Object getIgnoreCase(Map<String, Object> row, String column) {
  137 + for (Map.Entry<String, Object> entry : row.entrySet()) {
  138 + if (StrUtil.equalsIgnoreCase(entry.getKey(), column)) {
  139 + return entry.getValue();
  140 + }
  141 + }
  142 + return null;
  143 + }
  144 +
  145 + private static String getString(Map<String, Object> row, String column) {
  146 + Object value = getIgnoreCase(row, column);
  147 + return value != null ? value.toString() : null;
  148 + }
  149 +
  150 +}
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/iotdb/core/annotation/IoTDBDS.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.framework.iotdb.core.annotation;
  2 +
  3 +import com.baomidou.dynamic.datasource.annotation.DS;
  4 +
  5 +import java.lang.annotation.*;
  6 +
  7 +/**
  8 + * IoTDB 数据源
  9 + *
  10 + * 对应 spring.datasource.dynamic.datasource.iotdb 条目
  11 + *
  12 + * @author 芋道源码
  13 + */
  14 +@Target({ElementType.TYPE, ElementType.METHOD})
  15 +@Retention(RetentionPolicy.RUNTIME)
  16 +@Documented
  17 +@DS("iotdb")
  18 +public @interface IoTDBDS {
  19 +}
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/iotdb/package-info.java 0 → 100644
  1 +/**
  2 + * iot 模块的 IoTDB 拓展封装
  3 + */
  4 +package cn.iocoder.yudao.module.iot.framework.iotdb;
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/storage/config/IotStorageConfiguration.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.framework.storage.config;
  2 +
  3 +import org.springframework.boot.context.properties.EnableConfigurationProperties;
  4 +import org.springframework.context.annotation.Configuration;
  5 +
  6 +// TODO DONE @AI:按照项目的习惯,这个是不是不用写单测?已移除重复的装配单测 IotStorageConfigurationTest,实际装配由实库验收覆盖
  7 +/**
  8 + * IoT 时序存储的配置类
  9 + *
  10 + * 由 yudao.iot.storage.type 选择存储后端,对应的 DAO 实现通过 @ConditionalOnProperty 装配
  11 + *
  12 + * @author 芋道源码
  13 + */
  14 +@Configuration(proxyBeanMethods = false)
  15 +@EnableConfigurationProperties(IotStorageProperties.class)
  16 +public class IotStorageConfiguration {
  17 +}
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/storage/config/IotStorageInitRunner.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.framework.storage.config;
  2 +
  3 +import cn.iocoder.yudao.module.iot.dal.storage.IotDeviceMessageDAO;
  4 +import cn.iocoder.yudao.module.iot.dal.storage.IotDevicePropertyDAO;
  5 +import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
  6 +import jakarta.annotation.Resource;
  7 +import lombok.extern.slf4j.Slf4j;
  8 +import org.springframework.boot.ApplicationArguments;
  9 +import org.springframework.boot.ApplicationRunner;
  10 +import org.springframework.stereotype.Component;
  11 +
  12 +import javax.sql.DataSource;
  13 +
  14 +// TODO DONE @AI:按照项目的习惯,这个是不是不用写单测?已移除独立单测 IotStorageInitRunnerTest,关键的缺数据源防回落 master 断言并入实库验收
  15 +/**
  16 + * IoT 时序存储的初始化 Runner:校验数据源、前提条件,并创建消息表
  17 + *
  18 + * 只初始化 yudao.iot.storage.type 选中的存储后端。失败时退出系统,避免带着不可用的存储运行
  19 + *
  20 + * @author 芋道源码
  21 + */
  22 +@Component
  23 +@Slf4j
  24 +public class IotStorageInitRunner implements ApplicationRunner {
  25 +
  26 + @Resource
  27 + private IotStorageProperties storageProperties;
  28 + @Resource
  29 + private DataSource dataSource;
  30 +
  31 + @Resource
  32 + private IotDevicePropertyDAO devicePropertyDAO;
  33 + @Resource
  34 + private IotDeviceMessageDAO deviceMessageDAO;
  35 +
  36 + @Override
  37 + public void run(ApplicationArguments args) {
  38 + try {
  39 + initialize();
  40 + } catch (Exception ex) {
  41 + // 初始化失败时打印错误消息并退出系统
  42 + log.error("[run][IoT 时序存储({}) 初始化失败,系统无法正常运行,即将退出]", storageProperties.getType(), ex);
  43 + System.exit(1);
  44 + }
  45 + }
  46 +
  47 + /**
  48 + * 初始化选中的存储后端
  49 + */
  50 + public void initialize() {
  51 + // 1. 校验数据源已配置:dynamic-datasource 找不到数据源时,会回退到 master,必须提前拦截
  52 + validateDataSourceExists();
  53 + // 2. 初始化 DAO
  54 + devicePropertyDAO.initialize();
  55 + deviceMessageDAO.initialize();
  56 + log.info("[initialize][IoT 时序存储({}) 初始化完成]", storageProperties.getType());
  57 + }
  58 +
  59 + private void validateDataSourceExists() {
  60 + String dataSourceName = storageProperties.getType().getDataSourceName();
  61 + if (!(dataSource instanceof DynamicRoutingDataSource)) {
  62 + throw new IllegalStateException(String.format("[IoT 时序存储] 需要使用 dynamic-datasource 多数据源,当前为 %s",
  63 + dataSource.getClass().getName()));
  64 + }
  65 + if (!((DynamicRoutingDataSource) dataSource).getDataSources().containsKey(dataSourceName)) {
  66 + throw new IllegalStateException(String.format("[IoT 时序存储] 未配置数据源 spring.datasource.dynamic.datasource.%s," +
  67 + "请检查 yudao.iot.storage.type=%s 对应的数据源配置", dataSourceName, storageProperties.getType().getType()));
  68 + }
  69 + }
  70 +
  71 +}
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/storage/config/IotStorageProperties.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.framework.storage.config;
  2 +
  3 +import cn.iocoder.yudao.module.iot.framework.storage.core.enums.IotStorageTypeEnum;
  4 +import jakarta.validation.constraints.NotNull;
  5 +import lombok.Data;
  6 +import org.springframework.boot.context.properties.ConfigurationProperties;
  7 +import org.springframework.validation.annotation.Validated;
  8 +
  9 +/**
  10 + * IoT 时序数据存储的配置项
  11 + *
  12 + * @author 芋道源码
  13 + */
  14 +@ConfigurationProperties(prefix = IotStorageProperties.PREFIX)
  15 +@Validated
  16 +@Data
  17 +public class IotStorageProperties {
  18 +
  19 + public static final String PREFIX = "yudao.iot.storage";
  20 +
  21 + /**
  22 + * 存储类型,同时决定属性历史、消息日志的存储后端
  23 + *
  24 + * 写错值时,配置绑定失败,应用启动失败
  25 + */
  26 + @NotNull(message = "IoT 存储类型不能为空")
  27 + private IotStorageTypeEnum type = IotStorageTypeEnum.TDENGINE;
  28 +
  29 +}
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/storage/core/enums/IotStorageTypeEnum.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.framework.storage.core.enums;
  2 +
  3 +import lombok.AllArgsConstructor;
  4 +import lombok.Getter;
  5 +
  6 +/**
  7 + * IoT 时序数据(属性历史、消息日志)的存储类型枚举
  8 + *
  9 + * @author 芋道源码
  10 + */
  11 +@Getter
  12 +@AllArgsConstructor
  13 +public enum IotStorageTypeEnum {
  14 +
  15 + TDENGINE("tdengine", "tdengine"),
  16 + IOTDB("iotdb", "iotdb");
  17 +
  18 + /**
  19 + * 类型,对应 yudao.iot.storage.type 配置项
  20 + */
  21 + private final String type;
  22 + /**
  23 + * 数据源名,对应 spring.datasource.dynamic.datasource 下的条目
  24 + */
  25 + private final String dataSourceName;
  26 +
  27 +}
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/storage/core/util/IotStorageFieldUtils.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.framework.storage.core.util;
  2 +
  3 +import cn.hutool.core.util.StrUtil;
  4 +import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
  5 +
  6 +import java.util.Collection;
  7 +import java.util.HashMap;
  8 +import java.util.Locale;
  9 +import java.util.Map;
  10 +import java.util.Set;
  11 +import java.util.regex.Pattern;
  12 +
  13 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  14 +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*;
  15 +
  16 +/**
  17 + * IoT 时序存储的字段名工具类
  18 + *
  19 + * 属性标识符(identifier)统一归一化为小写下划线的“中立字段名”,各存储后端在此基础上映射物理列名:
  20 + * 1. TDengine:直接使用中立字段名
  21 + * 2. IoTDB:使用 p_ + 中立字段名,避免与 time 等内置列冲突
  22 + *
  23 + * @author 芋道源码
  24 + */
  25 +public class IotStorageFieldUtils {
  26 +
  27 + /**
  28 + * 系统字段 - 存储时间
  29 + */
  30 + public static final String FIELD_TS = "ts";
  31 + /**
  32 + * 系统字段 - 上报时间
  33 + */
  34 + public static final String FIELD_REPORT_TIME = "report_time";
  35 + /**
  36 + * 系统字段 - 设备编号
  37 + */
  38 + public static final String FIELD_DEVICE_ID = "device_id";
  39 +
  40 + /**
  41 + * 保留字段名:属性标识符归一化后,不允许与它们相同
  42 + */
  43 + public static final Set<String> RESERVED_FIELD_NAMES = SetUtils.asSet(FIELD_TS, FIELD_REPORT_TIME, FIELD_DEVICE_ID);
  44 +
  45 + /**
  46 + * 中立字段名的格式:只允许小写字母、数字、下划线,且以字母开头
  47 + *
  48 + * 动态拼接到 SQL 结构名(列名)前,必须通过该格式校验
  49 + */
  50 + private static final Pattern FIELD_NAME_PATTERN = Pattern.compile("^[a-z][a-z0-9_]{0,63}$");
  51 +
  52 + /**
  53 + * 构建中立字段名:转为小写下划线,需要和建表、写入、查询保持一致。
  54 + * 例如:PT -> pt,PfT -> pf_t。
  55 + *
  56 + * @param identifier 属性标识符
  57 + * @return 中立字段名
  58 + */
  59 + public static String buildFieldName(String identifier) {
  60 + if (StrUtil.isBlank(identifier)) {
  61 + return identifier;
  62 + }
  63 + return StrUtil.toUnderlineCase(identifier).toLowerCase(Locale.ROOT);
  64 + }
  65 +
  66 + /**
  67 + * 判断属性标识符,归一化后是否为合法的属性字段名(格式合法,且不是保留字段)
  68 + *
  69 + * @param identifier 属性标识符
  70 + * @return 是否合法
  71 + */
  72 + public static boolean isPropertyFieldName(String identifier) {
  73 + String fieldName = buildFieldName(identifier);
  74 + return fieldName != null && FIELD_NAME_PATTERN.matcher(fieldName).matches()
  75 + && !RESERVED_FIELD_NAMES.contains(fieldName);
  76 + }
  77 +
  78 + /**
  79 + * 校验属性标识符列表,可以作为存储字段。define、insert 拼接 SQL 之前,都需要校验原始标识符:
  80 + * 1. 归一化后格式合法
  81 + * 2. 归一化后不是 ts、report_time、device_id 保留字段
  82 + * 3. 归一化后不重复,例如说 fooBar 与 foo_bar
  83 + *
  84 + * @param identifiers 属性标识符列表
  85 + */
  86 + public static void validatePropertyIdentifiers(Collection<String> identifiers) {
  87 + Map<String, String> fieldNames = new HashMap<>();
  88 + for (String identifier : identifiers) {
  89 + String fieldName = buildFieldName(identifier);
  90 + if (fieldName == null || !FIELD_NAME_PATTERN.matcher(fieldName).matches()) {
  91 + throw exception(DEVICE_PROPERTY_FIELD_NAME_INVALID, identifier);
  92 + }
  93 + if (RESERVED_FIELD_NAMES.contains(fieldName)) {
  94 + throw exception(DEVICE_PROPERTY_FIELD_NAME_RESERVED, identifier, fieldName);
  95 + }
  96 + String existsIdentifier = fieldNames.putIfAbsent(fieldName, identifier);
  97 + if (existsIdentifier != null) {
  98 + throw exception(DEVICE_PROPERTY_FIELD_NAME_DUPLICATE, identifier, existsIdentifier, fieldName);
  99 + }
  100 + }
  101 + }
  102 +
  103 +}
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/storage/package-info.java 0 → 100644
  1 +/**
  2 + * iot 模块的时序数据存储封装:通过 yudao.iot.storage.type 选择 TDengine 或 IoTDB
  3 + */
  4 +package cn.iocoder.yudao.module.iot.framework.storage;
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/config/TDengineTableInitRunner.java deleted
1   -package cn.iocoder.yudao.module.iot.framework.tdengine.config;
2   -
3   -import cn.iocoder.yudao.module.iot.service.device.message.IotDeviceMessageService;
4   -import lombok.RequiredArgsConstructor;
5   -import lombok.extern.slf4j.Slf4j;
6   -import org.springframework.boot.ApplicationArguments;
7   -import org.springframework.boot.ApplicationRunner;
8   -import org.springframework.stereotype.Component;
9   -
10   -/**
11   - * TDengine 表初始化的 Configuration
12   - *
13   - * @author alwayssuper
14   - */
15   -@Component
16   -@RequiredArgsConstructor
17   -@Slf4j
18   -public class TDengineTableInitRunner implements ApplicationRunner {
19   -
20   - private final IotDeviceMessageService deviceMessageService;
21   -
22   - @Override
23   - public void run(ApplicationArguments args) {
24   - try {
25   - // 初始化设备消息表
26   - deviceMessageService.defineDeviceMessageStable();
27   - } catch (Exception ex) {
28   - // 初始化失败时打印错误消息并退出系统
29   - log.error("[run][TDengine初始化设备消息表结构失败,系统无法正常运行,即将退出]", ex);
30   - System.exit(1);
31   - }
32   - }
33   -
34   -}
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/core/TDengineTableField.java
1 1 package cn.iocoder.yudao.module.iot.framework.tdengine.core;
2 2  
3   -import cn.hutool.core.util.StrUtil;
  3 +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.ThingModelProperty;
4 4 import lombok.AllArgsConstructor;
5 5 import lombok.Data;
6 6 import lombok.NoArgsConstructor;
7 7  
8   -import java.util.Locale;
9   -
10 8 /**
11 9 * TDEngine 表字段
12 10 */
... ... @@ -30,9 +28,9 @@ public class TDengineTableField {
30 28 public static final String TYPE_TIMESTAMP = "TIMESTAMP";
31 29  
32 30 /**
33   - * 字段长度 - VARCHAR 默认长度
  31 + * 字段长度 - VARCHAR 默认长度,与 STRUCT、ARRAY 等 JSON 属性的长度上限保持一致
34 32 */
35   - public static final int LENGTH_VARCHAR = 1024;
  33 + public static final int LENGTH_VARCHAR = ThingModelProperty.LENGTH_JSON;
36 34  
37 35 /**
38 36 * 注释 - TAG 字段
... ... @@ -64,15 +62,4 @@ public class TDengineTableField {
64 62 this.type = type;
65 63 }
66 64  
67   - /**
68   - * 构建字段名:TDengine 默认会将字段名转为小写,需要和建表、写入、查询保持一致。
69   - * 例如:PT -> pt,PfT -> pf_t。
70   - */
71   - public static String buildFieldName(String field) {
72   - if (StrUtil.isBlank(field)) {
73   - return field;
74   - }
75   - return StrUtil.toUnderlineCase(field).toLowerCase(Locale.ROOT);
76   - }
77   -
78 65 }
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageService.java
... ... @@ -21,13 +21,6 @@ import java.util.List;
21 21 public interface IotDeviceMessageService {
22 22  
23 23 /**
24   - * 初始化设备消息的 TDengine 超级表
25   - *
26   - * 系统启动时,会自动初始化一次
27   - */
28   - void defineDeviceMessageStable();
29   -
30   - /**
31 24 * 发送设备消息
32 25 *
33 26 * @param message 消息(“codec(编解码)字段” 部分字段)
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageServiceImpl.java
... ... @@ -25,12 +25,10 @@ import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqD
25 25 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
26 26 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
27 27 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceMessageDO;
28   -import cn.iocoder.yudao.module.iot.dal.tdengine.IotDeviceMessageMapper;
  28 +import cn.iocoder.yudao.module.iot.dal.storage.IotDeviceMessageDAO;
29 29 import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
30 30 import cn.iocoder.yudao.module.iot.service.device.property.IotDevicePropertyService;
31 31 import cn.iocoder.yudao.module.iot.service.ota.IotOtaTaskRecordService;
32   -import com.baomidou.mybatisplus.core.metadata.IPage;
33   -import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
34 32 import com.google.common.base.Objects;
35 33 import jakarta.annotation.Resource;
36 34 import lombok.extern.slf4j.Slf4j;
... ... @@ -67,22 +65,11 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
67 65 private IotOtaTaskRecordService otaTaskRecordService;
68 66  
69 67 @Resource
70   - private IotDeviceMessageMapper deviceMessageMapper;
  68 + private IotDeviceMessageDAO deviceMessageDAO;
71 69  
72 70 @Resource
73 71 private IotDeviceMessageProducer deviceMessageProducer;
74 72  
75   - @Override
76   - public void defineDeviceMessageStable() {
77   - if (StrUtil.isNotEmpty(deviceMessageMapper.showSTable())) {
78   - log.info("[defineDeviceMessageStable][设备消息超级表已存在,创建跳过]");
79   - return;
80   - }
81   - log.info("[defineDeviceMessageStable][设备消息超级表不存在,创建开始...]");
82   - deviceMessageMapper.createSTable();
83   - log.info("[defineDeviceMessageStable][设备消息超级表不存在,创建成功]");
84   - }
85   -
86 73 @Async
87 74 void createDeviceLogAsync(IotDeviceMessage message) {
88 75 IotDeviceMessageDO messageDO = BeanUtils.toBean(message, IotDeviceMessageDO.class)
... ... @@ -102,7 +89,7 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
102 89 messageDO.setTs(System.currentTimeMillis());
103 90 }
104 91 try {
105   - deviceMessageMapper.insert(messageDO);
  92 + deviceMessageDAO.insert(messageDO);
106 93 } catch (Exception ex) {
107 94 // 特殊:@Async 方法的异常默认会被 handler 吞掉,这里显式记录便于排查
108 95 log.error("[createDeviceLogAsync][消息日志写入失败 deviceId({}) messageId({}) paramsLen({}) dataLen({})]",
... ... @@ -337,16 +324,7 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
337 324  
338 325 @Override
339 326 public PageResult<IotDeviceMessageDO> getDeviceMessagePage(IotDeviceMessagePageReqVO pageReqVO) {
340   - try {
341   - IPage<IotDeviceMessageDO> page = deviceMessageMapper.selectPage(
342   - new Page<>(pageReqVO.getPageNo(), pageReqVO.getPageSize()), pageReqVO);
343   - return new PageResult<>(page.getRecords(), page.getTotal());
344   - } catch (Exception exception) {
345   - if (exception.getMessage().contains("Table does not exist")) {
346   - return PageResult.empty();
347   - }
348   - throw exception;
349   - }
  327 + return deviceMessageDAO.selectPage(pageReqVO);
350 328 }
351 329  
352 330 @Override
... ... @@ -354,12 +332,12 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
354 332 if (CollUtil.isEmpty(requestIds)) {
355 333 return ListUtil.of();
356 334 }
357   - return deviceMessageMapper.selectListByRequestIdsAndReply(deviceId, requestIds, reply);
  335 + return deviceMessageDAO.selectListByRequestIdsAndReply(deviceId, requestIds, reply);
358 336 }
359 337  
360 338 @Override
361 339 public Long getDeviceMessageCount(LocalDateTime createTime) {
362   - return deviceMessageMapper.selectCountByCreateTime(
  340 + return deviceMessageDAO.selectCountByCreateTime(
363 341 createTime != null ? LocalDateTimeUtil.toEpochMilli(createTime) : null);
364 342 }
365 343  
... ... @@ -367,7 +345,7 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
367 345 public List<IotStatisticsDeviceMessageSummaryByDateRespVO> getDeviceMessageSummaryByDate(
368 346 IotStatisticsDeviceMessageReqVO reqVO) {
369 347 // 1. 按小时统计,获取分项统计数据
370   - List<Map<String, Object>> countList = deviceMessageMapper.selectDeviceMessageCountGroupByDate(
  348 + List<Map<String, Object>> countList = deviceMessageDAO.selectDeviceMessageCountGroupByDate(
371 349 LocalDateTimeUtil.toEpochMilli(reqVO.getTimes()[0]),
372 350 LocalDateTimeUtil.toEpochMilli(reqVO.getTimes()[1]));
373 351  
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/property/IotDevicePropertyServiceImpl.java
... ... @@ -2,11 +2,9 @@ package cn.iocoder.yudao.module.iot.service.device.property;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
4 4 import cn.hutool.core.date.LocalDateTimeUtil;
5   -import cn.hutool.core.map.MapUtil;
6 5 import cn.hutool.core.util.ObjUtil;
7 6 import cn.hutool.core.util.StrUtil;
8 7 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
9   -import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
10 8 import cn.iocoder.yudao.module.iot.controller.admin.device.vo.property.IotDevicePropertyHistoryListReqVO;
11 9 import cn.iocoder.yudao.module.iot.controller.admin.device.vo.property.IotDevicePropertyRespVO;
12 10 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
... ... @@ -14,14 +12,12 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
14 12 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDevicePropertyDO;
15 13 import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
16 14 import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO;
17   -import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.dataType.ThingModelDateOrTextDataSpecs;
18 15 import cn.iocoder.yudao.module.iot.dal.redis.device.DevicePropertyRedisDAO;
19 16 import cn.iocoder.yudao.module.iot.dal.redis.device.DeviceReportTimeRedisDAO;
20 17 import cn.iocoder.yudao.module.iot.dal.redis.device.DeviceServerIdRedisDAO;
21   -import cn.iocoder.yudao.module.iot.dal.tdengine.IotDevicePropertyMapper;
22   -import cn.iocoder.yudao.module.iot.enums.thingmodel.IotDataSpecsDataTypeEnum;
  18 +import cn.iocoder.yudao.module.iot.dal.storage.IotDevicePropertyDAO;
23 19 import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelTypeEnum;
24   -import cn.iocoder.yudao.module.iot.framework.tdengine.core.TDengineTableField;
  20 +import cn.iocoder.yudao.module.iot.framework.storage.core.util.IotStorageFieldUtils;
25 21 import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
26 22 import cn.iocoder.yudao.module.iot.service.product.IotProductService;
27 23 import cn.iocoder.yudao.module.iot.service.thingmodel.IotThingModelService;
... ... @@ -35,8 +31,10 @@ import java.math.BigDecimal;
35 31 import java.time.LocalDateTime;
36 32 import java.util.*;
37 33  
  34 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
38 35 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
39 36 import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.getBigDecimal;
  37 +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DEVICE_PROPERTY_HISTORY_TIMES_INVALID;
40 38  
41 39 /**
42 40 * IoT 设备【属性】数据 Service 实现类
... ... @@ -47,23 +45,6 @@ import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.getBigD
47 45 @Slf4j
48 46 public class IotDevicePropertyServiceImpl implements IotDevicePropertyService {
49 47  
50   - /**
51   - * 物模型的数据类型,与 TDengine 数据类型的映射关系
52   - *
53   - * @see <a href="https://docs.taosdata.com/reference/taos-sql/data-type/">TDEngine 数据类型</a>
54   - */
55   - private static final Map<String, String> TYPE_MAPPING = MapUtil.<String, String>builder()
56   - .put(IotDataSpecsDataTypeEnum.INT.getDataType(), TDengineTableField.TYPE_INT)
57   - .put(IotDataSpecsDataTypeEnum.FLOAT.getDataType(), TDengineTableField.TYPE_FLOAT)
58   - .put(IotDataSpecsDataTypeEnum.DOUBLE.getDataType(), TDengineTableField.TYPE_DOUBLE)
59   - .put(IotDataSpecsDataTypeEnum.ENUM.getDataType(), TDengineTableField.TYPE_TINYINT)
60   - .put(IotDataSpecsDataTypeEnum.BOOL.getDataType(), TDengineTableField.TYPE_TINYINT)
61   - .put(IotDataSpecsDataTypeEnum.TEXT.getDataType(), TDengineTableField.TYPE_VARCHAR)
62   - .put(IotDataSpecsDataTypeEnum.DATE.getDataType(), TDengineTableField.TYPE_TIMESTAMP)
63   - .put(IotDataSpecsDataTypeEnum.STRUCT.getDataType(), TDengineTableField.TYPE_VARCHAR)
64   - .put(IotDataSpecsDataTypeEnum.ARRAY.getDataType(), TDengineTableField.TYPE_VARCHAR)
65   - .build();
66   -
67 48 @Resource
68 49 private IotThingModelService thingModelService;
69 50 @Resource
... ... @@ -81,54 +62,18 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService {
81 62 private DeviceServerIdRedisDAO deviceServerIdRedisDAO;
82 63  
83 64 @Resource
84   - private IotDevicePropertyMapper devicePropertyMapper;
  65 + private IotDevicePropertyDAO devicePropertyDAO;
85 66  
86 67 // ========== 设备属性相关操作 ==========
87 68  
88 69 @Override
89 70 public void defineDevicePropertyData(Long productId) {
90   - // 1.1 查询产品和物模型
  71 + // 1. 查询产品和物模型
91 72 IotProductDO product = productService.validateProductExists(productId);
92 73 List<IotThingModelDO> thingModels = filterList(thingModelService.getThingModelListByProductId(productId),
93 74 thingModel -> IotThingModelTypeEnum.PROPERTY.getType().equals(thingModel.getType()));
94   - // 1.2 解析 DB 里的字段
95   - List<TDengineTableField> oldFields = new ArrayList<>();
96   - try {
97   - oldFields.addAll(devicePropertyMapper.getProductPropertySTableFieldList(product.getId()));
98   - } catch (Exception e) {
99   - if (!e.getMessage().contains("Table does not exist")) {
100   - throw e;
101   - }
102   - }
103   -
104   - // 2.1 情况一:如果是新增的时候,需要创建表
105   - List<TDengineTableField> newFields = buildTableFieldList(thingModels);
106   - if (CollUtil.isEmpty(oldFields)) {
107   - if (CollUtil.isEmpty(newFields)) {
108   - log.info("[defineDevicePropertyData][productId({}) 没有需要定义的属性]", productId);
109   - return;
110   - }
111   - devicePropertyMapper.createProductPropertySTable(product.getId(), newFields);
112   - return;
113   - }
114   - // 2.2 情况二:如果是修改的时候,需要更新表
115   - devicePropertyMapper.alterProductPropertySTable(product.getId(), oldFields, newFields);
116   - }
117   -
118   - private List<TDengineTableField> buildTableFieldList(List<IotThingModelDO> thingModels) {
119   - return convertList(thingModels, thingModel -> {
120   - TDengineTableField field = new TDengineTableField(
121   - TDengineTableField.buildFieldName(thingModel.getIdentifier()),
122   - TYPE_MAPPING.get(thingModel.getProperty().getDataType()));
123   - String dataType = thingModel.getProperty().getDataType();
124   - if (Objects.equals(dataType, IotDataSpecsDataTypeEnum.TEXT.getDataType())) {
125   - field.setLength(((ThingModelDateOrTextDataSpecs) thingModel.getProperty().getDataSpecs()).getLength());
126   - } else if (ObjectUtils.equalsAny(dataType, IotDataSpecsDataTypeEnum.STRUCT.getDataType(),
127   - IotDataSpecsDataTypeEnum.ARRAY.getDataType())) {
128   - field.setLength(TDengineTableField.LENGTH_VARCHAR);
129   - }
130   - return field;
131   - });
  75 + // 2. 定义存储结构:字段映射、差异比较、建表改表,由存储 DAO 实现
  76 + devicePropertyDAO.defineProductPropertyTable(product.getId(), thingModels);
132 77 }
133 78  
134 79 @Override
... ... @@ -174,7 +119,7 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService {
174 119 log.error("[saveDeviceProperty][消息({}) 没有合法的属性]", message);
175 120 } else {
176 121 // 2.1 保存设备属性【数据】
177   - devicePropertyMapper.insert(device, properties,
  122 + devicePropertyDAO.insert(device, properties,
178 123 System.currentTimeMillis(),
179 124 LocalDateTimeUtil.toEpochMilli(message.getReportTime()));
180 125  
... ... @@ -213,14 +158,26 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService {
213 158  
214 159 @Override
215 160 public List<IotDevicePropertyRespVO> getHistoryDevicePropertyList(IotDevicePropertyHistoryListReqVO listReqVO) {
216   - try {
217   - return devicePropertyMapper.selectListByHistory(listReqVO);
218   - } catch (Exception exception) {
219   - if (exception.getMessage().contains("Table does not exist")) {
220   - return Collections.emptyList();
221   - }
222   - throw exception;
  161 + // 1.1 校验时间范围:必须包含开始、结束两个时间;任一为空时,无法确定范围,返回空
  162 + LocalDateTime[] times = listReqVO.getTimes();
  163 + if (times == null || times.length != 2) {
  164 + throw exception(DEVICE_PROPERTY_HISTORY_TIMES_INVALID);
  165 + }
  166 + if (times[0] == null || times[1] == null) {
  167 + return Collections.emptyList();
  168 + }
  169 + // 1.2 校验属性标识符:无法作为存储字段的标识符,不会有历史数据
  170 + if (!IotStorageFieldUtils.isPropertyFieldName(listReqVO.getIdentifier())) {
  171 + return Collections.emptyList();
223 172 }
  173 + // 1.3 校验设备:按当前租户查询,且过滤已删除的设备;不存在时,不查询历史数据
  174 + IotDeviceDO device = deviceService.getDevice(listReqVO.getDeviceId());
  175 + if (device == null) {
  176 + return Collections.emptyList();
  177 + }
  178 +
  179 + // 2. 查询历史数据
  180 + return devicePropertyDAO.selectHistoryList(device.getProductId(), listReqVO);
224 181 }
225 182  
226 183 // ========== 设备时间相关操作 ==========
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImpl.java
... ... @@ -18,6 +18,7 @@ import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelS
18 18 import cn.iocoder.yudao.module.iot.convert.thingmodel.IotThingModelConvert;
19 19 import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
20 20 import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO;
  21 +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.ThingModelProperty;
21 22 import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.dataType.ThingModelBoolOrEnumDataSpecs;
22 23 import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.dataType.ThingModelDataSpecs;
23 24 import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.dataType.ThingModelDateOrTextDataSpecs;
... ... @@ -25,7 +26,6 @@ import cn.iocoder.yudao.module.iot.dal.mysql.thingmodel.IotThingModelMapper;
25 26 import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants;
26 27 import cn.iocoder.yudao.module.iot.enums.product.IotProductStatusEnum;
27 28 import cn.iocoder.yudao.module.iot.enums.thingmodel.IotDataSpecsDataTypeEnum;
28   -import cn.iocoder.yudao.module.iot.framework.tdengine.core.TDengineTableField;
29 29 import cn.iocoder.yudao.module.iot.service.device.IotDeviceModbusPointService;
30 30 import cn.iocoder.yudao.module.iot.service.product.IotProductService;
31 31 import jakarta.annotation.Resource;
... ... @@ -193,8 +193,8 @@ public class IotThingModelServiceImpl implements IotThingModelService {
193 193 String dataType = thingModel.getProperty().getDataType();
194 194 if (ObjectUtils.equalsAny(dataType,
195 195 IotDataSpecsDataTypeEnum.STRUCT.getDataType(), IotDataSpecsDataTypeEnum.ARRAY.getDataType())) {
196   - // 特殊:STRUCT 和 ARRAY 类型,在 TDengine 里,没有对应数据类型,只能通过 JSON 来存储
197   - return convertToVarchar(JsonUtils.toJsonString(value), TDengineTableField.LENGTH_VARCHAR);
  196 + // 特殊:STRUCT 和 ARRAY 类型,在时序库里,没有对应数据类型,只能通过 JSON 来存储
  197 + return convertToVarchar(JsonUtils.toJsonString(value), ThingModelProperty.LENGTH_JSON);
198 198 }
199 199 if (IotDataSpecsDataTypeEnum.INT.getDataType().equals(dataType)) {
200 200 return convertToInt(value);
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/device/IotDevicePropertyMapper.xml
... ... @@ -51,7 +51,7 @@
51 51 TAGS ('${device.id}')
52 52 (ts, report_time,
53 53 <foreach item="key" collection="properties.keys" separator=",">
54   - ${@cn.iocoder.yudao.module.iot.framework.tdengine.core.TDengineTableField@buildFieldName(key)}
  54 + ${@cn.iocoder.yudao.module.iot.framework.storage.core.util.IotStorageFieldUtils@buildFieldName(key)}
55 55 </foreach>
56 56 )
57 57 VALUES
... ... @@ -68,9 +68,9 @@
68 68  
69 69 <select id="selectListByHistory"
70 70 resultType="cn.iocoder.yudao.module.iot.controller.admin.device.vo.property.IotDevicePropertyRespVO">
71   - SELECT ${@cn.iocoder.yudao.module.iot.framework.tdengine.core.TDengineTableField@buildFieldName(reqVO.identifier)} AS `value`, ts AS update_time
  71 + SELECT ${@cn.iocoder.yudao.module.iot.framework.storage.core.util.IotStorageFieldUtils@buildFieldName(reqVO.identifier)} AS `value`, ts AS update_time
72 72 FROM device_property_${reqVO.deviceId}
73   - WHERE ${@cn.iocoder.yudao.module.iot.framework.tdengine.core.TDengineTableField@buildFieldName(reqVO.identifier)} IS NOT NULL
  73 + WHERE ${@cn.iocoder.yudao.module.iot.framework.storage.core.util.IotStorageFieldUtils@buildFieldName(reqVO.identifier)} IS NOT NULL
74 74 AND ts BETWEEN ${@cn.hutool.core.date.LocalDateTimeUtil@toEpochMilli(reqVO.times[0])}
75 75 AND ${@cn.hutool.core.date.LocalDateTimeUtil@toEpochMilli(reqVO.times[1])}
76 76 ORDER BY ts DESC
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/iotdb/IotDeviceMessageIoTDBMapper.xml 0 → 100644
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<!DOCTYPE mapper
  3 + PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  4 + "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  5 +<mapper namespace="cn.iocoder.yudao.module.iot.dal.iotdb.IotDeviceMessageIoTDBMapper">
  6 +
  7 + <!--
  8 + 一张全局表 device_message:
  9 + 1. time:Service 写入日志时的毫秒 ts
  10 + 2. device_id:唯一的 TAG。身份为 device_id + time:同一设备、同一毫秒再次写入时,后写的整行替换先写的,即使 id 不同
  11 + 3. id:消息编号,FIELD。不作为 TAG,避免每条消息产生一个 IoTDB 设备,导致元数据随消息量无限增长
  12 + 4. upstream、reply:INT32,-1/0/1
  13 + 5. 其它 FIELD:STRING,n:/v: 编码,均为非 NULL
  14 + -->
  15 +
  16 + <select id="selectColumnList" resultType="java.util.Map">
  17 + DESC device_message
  18 + </select>
  19 +
  20 + <update id="createTable">
  21 + CREATE TABLE IF NOT EXISTS device_message (
  22 + device_id STRING TAG,
  23 + id STRING FIELD,
  24 + upstream INT32 FIELD,
  25 + reply INT32 FIELD,
  26 + method STRING FIELD,
  27 + identifier STRING FIELD,
  28 + request_id STRING FIELD,
  29 + server_id STRING FIELD,
  30 + params STRING FIELD,
  31 + data STRING FIELD,
  32 + msg STRING FIELD,
  33 + report_time STRING FIELD,
  34 + tenant_id STRING FIELD,
  35 + code STRING FIELD
  36 + )
  37 + </update>
  38 +
  39 + <insert id="insert">
  40 + INSERT INTO device_message (
  41 + time, device_id, id, upstream, reply,
  42 + method, identifier, request_id, server_id, params,
  43 + data, msg, report_time, tenant_id, code
  44 + )
  45 + VALUES (
  46 + #{ts}, #{deviceId}, #{id}, #{upstream}, #{reply},
  47 + #{method}, #{identifier}, #{requestId}, #{serverId}, #{params},
  48 + #{data}, #{msg}, #{reportTime}, #{tenantId}, #{code}
  49 + )
  50 + </insert>
  51 +
  52 + <sql id="selectColumns">
  53 + SELECT time AS ts, device_id, id, upstream, reply,
  54 + method, identifier, request_id, server_id, params,
  55 + data, msg, report_time, tenant_id, code
  56 + FROM device_message
  57 + </sql>
  58 +
  59 + <sql id="pageWhere">
  60 + WHERE device_id = #{deviceId}
  61 + <if test="method != null">
  62 + AND method = #{method}
  63 + </if>
  64 + <if test="upstream != null">
  65 + AND upstream = #{upstream}
  66 + </if>
  67 + <if test="reply != null">
  68 + AND reply = #{reply}
  69 + </if>
  70 + <if test="identifier != null">
  71 + AND identifier = #{identifier}
  72 + </if>
  73 + </sql>
  74 +
  75 + <select id="selectList" resultType="cn.iocoder.yudao.module.iot.dal.iotdb.IotDeviceMessageIoTDBRow">
  76 + <include refid="selectColumns" />
  77 + <include refid="pageWhere" />
  78 + ORDER BY time DESC
  79 + <if test="limit != null">
  80 + LIMIT #{limit} OFFSET #{offset}
  81 + </if>
  82 + </select>
  83 +
  84 + <select id="selectCount" resultType="Long">
  85 + SELECT count(*)
  86 + FROM device_message
  87 + <include refid="pageWhere" />
  88 + </select>
  89 +
  90 + <select id="selectListByRequestIdsAndReply" resultType="cn.iocoder.yudao.module.iot.dal.iotdb.IotDeviceMessageIoTDBRow">
  91 + <include refid="selectColumns" />
  92 + WHERE device_id = #{deviceId}
  93 + AND reply = #{reply}
  94 + AND request_id IN
  95 + <foreach collection="requestIds" item="requestId" open="(" close=")" separator=",">
  96 + #{requestId}
  97 + </foreach>
  98 + ORDER BY time DESC
  99 + </select>
  100 +
  101 + <select id="selectCountByCreateTime" resultType="Long">
  102 + SELECT count(*)
  103 + FROM device_message
  104 + <where>
  105 + <if test="createTime != null">
  106 + AND time &gt;= #{createTime}
  107 + </if>
  108 + </where>
  109 + </select>
  110 +
  111 + <!-- 按 epoch 整点分桶;upstream 为 -1 的消息不计入上行、下行 -->
  112 + <select id="selectCountGroupByHour" resultType="java.util.Map">
  113 + SELECT
  114 + date_bin(1h, time) AS time,
  115 + count_if(upstream = 1) AS upstream_count,
  116 + count_if(upstream = 0) AS downstream_count
  117 + FROM device_message
  118 + <where>
  119 + <if test="startTime != null">
  120 + AND time &gt;= #{startTime}
  121 + </if>
  122 + <if test="endTime != null">
  123 + AND time &lt;= #{endTime}
  124 + </if>
  125 + </where>
  126 + GROUP BY date_bin(1h, time)
  127 + </select>
  128 +
  129 +</mapper>
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/iotdb/IotDevicePropertyIoTDBMapper.xml 0 → 100644
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<!DOCTYPE mapper
  3 + PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  4 + "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  5 +<mapper namespace="cn.iocoder.yudao.module.iot.dal.iotdb.IotDevicePropertyIoTDBMapper">
  6 +
  7 + <!--
  8 + 每个产品一张表 product_property_{productId}:
  9 + 1. time:存储时间(毫秒)
  10 + 2. device_id:设备编号,TAG
  11 + 3. report_time:上报时间,FIELD
  12 + 4. p_*:用户属性,FIELD,按物模型增删、改类型
  13 +
  14 + 注意:${} 只用于 Long 产品编号、DAO 校验后的列名和类型,值全部使用 #{} 绑定
  15 + -->
  16 +
  17 + <select id="selectColumnList" resultType="java.util.Map">
  18 + DESC product_property_${productId}
  19 + </select>
  20 +
  21 + <update id="createTable">
  22 + CREATE TABLE IF NOT EXISTS product_property_${productId} (
  23 + device_id STRING TAG,
  24 + report_time TIMESTAMP FIELD
  25 + <foreach item="column" collection="columns">
  26 + , "${column.name}" ${column.type} FIELD
  27 + </foreach>
  28 + )
  29 + </update>
  30 +
  31 + <update id="addColumn">
  32 + ALTER TABLE product_property_${productId}
  33 + ADD COLUMN IF NOT EXISTS "${column.name}" ${column.type} FIELD
  34 + </update>
  35 +
  36 + <update id="dropColumn">
  37 + ALTER TABLE product_property_${productId}
  38 + DROP COLUMN IF EXISTS "${columnName}"
  39 + </update>
  40 +
  41 + <insert id="insert">
  42 + INSERT INTO product_property_${productId} (time, device_id
  43 + <if test="reportTime != null">
  44 + , report_time
  45 + </if>
  46 + <foreach item="column" collection="columns">
  47 + , "${column.name}"
  48 + </foreach>
  49 + )
  50 + VALUES (#{ts}, #{deviceId}
  51 + <if test="reportTime != null">
  52 + , #{reportTime}
  53 + </if>
  54 + <foreach item="column" collection="columns">
  55 + , #{column.value}
  56 + </foreach>
  57 + )
  58 + </insert>
  59 +
  60 + <select id="selectHistoryList"
  61 + resultType="cn.iocoder.yudao.module.iot.controller.admin.device.vo.property.IotDevicePropertyRespVO">
  62 + SELECT "${columnName}" AS "value", time AS update_time
  63 + FROM product_property_${productId}
  64 + WHERE device_id = #{deviceId}
  65 + AND time &gt;= #{startTime}
  66 + AND time &lt;= #{endTime}
  67 + AND "${columnName}" IS NOT NULL
  68 + ORDER BY time DESC
  69 + </select>
  70 +
  71 +</mapper>
... ...
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/framework/iotdb/core/IoTDBUtilsTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.framework.iotdb.core;
  2 +
  3 +import cn.hutool.core.map.MapUtil;
  4 +import org.apache.iotdb.common.rpc.thrift.TSStatus;
  5 +import org.apache.iotdb.jdbc.IoTDBSQLException;
  6 +import org.apache.iotdb.rpc.StatementExecutionException;
  7 +import org.apache.iotdb.rpc.TSStatusCode;
  8 +import org.junit.jupiter.api.Test;
  9 +import org.springframework.jdbc.UncategorizedSQLException;
  10 +
  11 +import java.util.*;
  12 +
  13 +import static org.junit.jupiter.api.Assertions.*;
  14 +
  15 +/**
  16 + * {@link IoTDBUtils} 的单元测试
  17 + *
  18 + * @author 芋道源码
  19 + */
  20 +public class IoTDBUtilsTest {
  21 +
  22 + // ========== isTableNotExists ==========
  23 +
  24 + @Test
  25 + public void testIsTableNotExists_statusCode() {
  26 + assertEquals(TSStatusCode.TABLE_NOT_EXISTS.getStatusCode(), IoTDBUtils.STATUS_CODE_TABLE_NOT_EXISTS);
  27 + }
  28 +
  29 + @Test
  30 + public void testIsTableNotExists_true() {
  31 + // 驱动抛出的异常:errorCode 为 IoTDB 状态码
  32 + IoTDBSQLException sqlException = new IoTDBSQLException("550: Table 'db.product_property_1' does not exist",
  33 + new TSStatus(TSStatusCode.TABLE_NOT_EXISTS.getStatusCode()));
  34 + assertTrue(IoTDBUtils.isTableNotExists(sqlException));
  35 + // Spring、MyBatis 包装后的异常:沿 cause 链查找
  36 + assertTrue(IoTDBUtils.isTableNotExists(new UncategorizedSQLException("select", "SELECT 1", sqlException)));
  37 + assertTrue(IoTDBUtils.isTableNotExists(new RuntimeException(new RuntimeException(sqlException))));
  38 + // Session 风格的异常
  39 + assertTrue(IoTDBUtils.isTableNotExists(new RuntimeException(
  40 + new StatementExecutionException(new TSStatus(TSStatusCode.TABLE_NOT_EXISTS.getStatusCode())))));
  41 + }
  42 +
  43 + @Test
  44 + public void testIsTableNotExists_false() {
  45 + // 列不存在、类型不匹配:不是表不存在,需要原样抛出
  46 + assertFalse(IoTDBUtils.isTableNotExists(new IoTDBSQLException("616",
  47 + new TSStatus(TSStatusCode.COLUMN_NOT_EXISTS.getStatusCode()))));
  48 + assertFalse(IoTDBUtils.isTableNotExists(new StatementExecutionException(
  49 + new TSStatus(TSStatusCode.DATA_TYPE_MISMATCH.getStatusCode()))));
  50 + // 只靠异常文案,不识别为表不存在
  51 + assertFalse(IoTDBUtils.isTableNotExists(new RuntimeException("Table does not exist")));
  52 + assertFalse(IoTDBUtils.isTableNotExists(null));
  53 + }
  54 +
  55 + @Test
  56 + public void testIsTableNotExists_causeLoop() {
  57 + // 准备参数:cause 链成环
  58 + RuntimeException a = new RuntimeException("a");
  59 + RuntimeException b = new RuntimeException("b");
  60 + a.initCause(b);
  61 + b.initCause(a);
  62 +
  63 + // 调用,并断言:不会死循环
  64 + assertFalse(IoTDBUtils.isTableNotExists(a));
  65 + }
  66 +
  67 + // ========== validateTimestampPrecision ==========
  68 +
  69 + @Test
  70 + public void testValidateTimestampPrecision_success() {
  71 + List<Map<String, Object>> variables = Arrays.asList(
  72 + row("Variable", "ClusterName", "Value", "defaultCluster"),
  73 + row("variable", "TimestampPrecision", "value", "ms"));
  74 + assertDoesNotThrow(() -> IoTDBUtils.validateTimestampPrecision(variables));
  75 + }
  76 +
  77 + @Test
  78 + public void testValidateTimestampPrecision_failure() {
  79 + // 精度不是 ms
  80 + assertThrows(IllegalStateException.class, () -> IoTDBUtils.validateTimestampPrecision(
  81 + Collections.singletonList(row("Variable", "TimestampPrecision", "Value", "ns"))));
  82 + // 无法读取
  83 + assertThrows(IllegalStateException.class, () -> IoTDBUtils.validateTimestampPrecision(
  84 + Collections.singletonList(row("Variable", "ClusterName", "Value", "defaultCluster"))));
  85 + assertThrows(IllegalStateException.class, () -> IoTDBUtils.validateTimestampPrecision(Collections.emptyList()));
  86 + }
  87 +
  88 + // ========== validateDataNodes ==========
  89 +
  90 + @Test
  91 + public void testValidateDataNodes_success() {
  92 + List<Map<String, Object>> dataNodes = Arrays.asList(
  93 + row("NodeID", 1, "Status", "Running"),
  94 + row("NodeID", 3, "Status", "running"));
  95 + assertEquals(Arrays.asList(1, 3), IoTDBUtils.validateDataNodes(dataNodes));
  96 + }
  97 +
  98 + @Test
  99 + public void testValidateDataNodes_failure() {
  100 + // 没有 DataNode
  101 + assertThrows(IllegalStateException.class, () -> IoTDBUtils.validateDataNodes(Collections.emptyList()));
  102 + assertThrows(IllegalStateException.class, () -> IoTDBUtils.validateDataNodes(null));
  103 + // 有 DataNode 不是 Running:无法确认它的配置
  104 + assertThrows(IllegalStateException.class, () -> IoTDBUtils.validateDataNodes(Arrays.asList(
  105 + row("NodeID", 1, "Status", "Running"),
  106 + row("NodeID", 2, "Status", "Unknown"))));
  107 + // 编号无法识别
  108 + assertThrows(IllegalStateException.class, () -> IoTDBUtils.validateDataNodes(
  109 + Collections.singletonList(row("NodeID", "abc", "Status", "Running"))));
  110 + assertThrows(IllegalStateException.class, () -> IoTDBUtils.validateDataNodes(
  111 + Collections.singletonList(row("Status", "Running"))));
  112 + }
  113 +
  114 + // ========== validatePartialInsertDisabled ==========
  115 +
  116 + @Test
  117 + public void testValidatePartialInsertDisabled_success() {
  118 + List<Map<String, Object>> configurations = Arrays.asList(
  119 + row("name", "dn_rpc_port", "value", "6667", "default_value", "6667"),
  120 + row("name", "enable_partial_insert", "value", "false", "default_value", "true"));
  121 + assertDoesNotThrow(() -> IoTDBUtils.validatePartialInsertDisabled(1, configurations));
  122 + }
  123 +
  124 + @Test
  125 + public void testValidatePartialInsertDisabled_failure() {
  126 + // 开启了部分写入
  127 + assertThrows(IllegalStateException.class, () -> IoTDBUtils.validatePartialInsertDisabled(1,
  128 + Collections.singletonList(row("name", "enable_partial_insert", "value", "true"))));
  129 + // 无法读取配置
  130 + assertThrows(IllegalStateException.class, () -> IoTDBUtils.validatePartialInsertDisabled(1,
  131 + Collections.singletonList(row("name", "dn_rpc_port", "value", "6667"))));
  132 + assertThrows(IllegalStateException.class, () -> IoTDBUtils.validatePartialInsertDisabled(1,
  133 + Collections.singletonList(row("name", "enable_partial_insert"))));
  134 + }
  135 +
  136 + // ========== getIgnoreCase ==========
  137 +
  138 + @Test
  139 + public void testGetIgnoreCase() {
  140 + Map<String, Object> row = row("NodeID", 1);
  141 + assertEquals(1, IoTDBUtils.getIgnoreCase(row, "nodeid"));
  142 + assertNull(IoTDBUtils.getIgnoreCase(row, "Status"));
  143 + }
  144 +
  145 + private static Map<String, Object> row(Object... keyValues) {
  146 + Map<String, Object> row = new LinkedHashMap<>();
  147 + for (int i = 0; i < keyValues.length; i += 2) {
  148 + row.put((String) keyValues[i], keyValues[i + 1]);
  149 + }
  150 + return MapUtil.unmodifiable(row);
  151 + }
  152 +
  153 +}
... ...
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/framework/storage/core/util/IotStorageFieldUtilsTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.framework.storage.core.util;
  2 +
  3 +import cn.hutool.core.util.StrUtil;
  4 +import org.junit.jupiter.api.Test;
  5 +
  6 +import java.util.Arrays;
  7 +import java.util.Collections;
  8 +
  9 +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
  10 +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*;
  11 +import static org.junit.jupiter.api.Assertions.*;
  12 +
  13 +/**
  14 + * {@link IotStorageFieldUtils} 的单元测试
  15 + *
  16 + * @author 芋道源码
  17 + */
  18 +public class IotStorageFieldUtilsTest {
  19 +
  20 + @Test
  21 + public void testBuildFieldName_success() {
  22 + assertEquals("ua", IotStorageFieldUtils.buildFieldName("Ua"));
  23 + assertEquals("pf_t", IotStorageFieldUtils.buildFieldName("PfT"));
  24 + assertEquals("pt", IotStorageFieldUtils.buildFieldName("PT"));
  25 + assertEquals("pa", IotStorageFieldUtils.buildFieldName("PA"));
  26 + assertEquals("geo_location", IotStorageFieldUtils.buildFieldName("GeoLocation"));
  27 + assertEquals("light_status", IotStorageFieldUtils.buildFieldName("LightStatus"));
  28 + assertEquals("foo_bar", IotStorageFieldUtils.buildFieldName("foo_bar"));
  29 + }
  30 +
  31 + @Test
  32 + public void testBuildFieldName_blank() {
  33 + // 空白原样返回,由 isPropertyFieldName、validatePropertyIdentifiers 判定为不合法
  34 + assertNull(IotStorageFieldUtils.buildFieldName(null));
  35 + assertEquals("", IotStorageFieldUtils.buildFieldName(""));
  36 + assertEquals(" ", IotStorageFieldUtils.buildFieldName(" "));
  37 + }
  38 +
  39 + @Test
  40 + public void testIsPropertyFieldName_true() {
  41 + assertTrue(IotStorageFieldUtils.isPropertyFieldName("Temperature"));
  42 + assertTrue(IotStorageFieldUtils.isPropertyFieldName("PfT"));
  43 + assertTrue(IotStorageFieldUtils.isPropertyFieldName("a1_b2"));
  44 + assertTrue(IotStorageFieldUtils.isPropertyFieldName(StrUtil.repeat('a', 64)));
  45 + }
  46 +
  47 + @Test
  48 + public void testIsPropertyFieldName_false() {
  49 + // 保留字段:归一化后与系统字段相同
  50 + assertFalse(IotStorageFieldUtils.isPropertyFieldName("ts"));
  51 + assertFalse(IotStorageFieldUtils.isPropertyFieldName("TS"));
  52 + assertFalse(IotStorageFieldUtils.isPropertyFieldName("reportTime"));
  53 + assertFalse(IotStorageFieldUtils.isPropertyFieldName("report_time"));
  54 + assertFalse(IotStorageFieldUtils.isPropertyFieldName("deviceId"));
  55 + // 格式不合法:不能安全地拼接到 SQL 结构名
  56 + assertFalse(IotStorageFieldUtils.isPropertyFieldName(null));
  57 + assertFalse(IotStorageFieldUtils.isPropertyFieldName(""));
  58 + assertFalse(IotStorageFieldUtils.isPropertyFieldName(" "));
  59 + assertFalse(IotStorageFieldUtils.isPropertyFieldName("1abc"));
  60 + assertFalse(IotStorageFieldUtils.isPropertyFieldName("温度"));
  61 + assertFalse(IotStorageFieldUtils.isPropertyFieldName("a-b"));
  62 + assertFalse(IotStorageFieldUtils.isPropertyFieldName("a\"b"));
  63 + assertFalse(IotStorageFieldUtils.isPropertyFieldName("a` OR 1=1"));
  64 + assertFalse(IotStorageFieldUtils.isPropertyFieldName(StrUtil.repeat('a', 65)));
  65 + }
  66 +
  67 + @Test
  68 + public void testValidatePropertyIdentifiers_success() {
  69 + // 调用,并断言
  70 + assertDoesNotThrow(() -> IotStorageFieldUtils.validatePropertyIdentifiers(
  71 + Arrays.asList("Temperature", "PfT", "geo_location")));
  72 + assertDoesNotThrow(() -> IotStorageFieldUtils.validatePropertyIdentifiers(Collections.emptyList()));
  73 + }
  74 +
  75 + @Test
  76 + public void testValidatePropertyIdentifiers_invalid() {
  77 + assertServiceException(() -> IotStorageFieldUtils.validatePropertyIdentifiers(
  78 + Arrays.asList("Temperature", "温度")), DEVICE_PROPERTY_FIELD_NAME_INVALID, "温度");
  79 + assertServiceException(() -> IotStorageFieldUtils.validatePropertyIdentifiers(
  80 + Collections.singletonList(null)), DEVICE_PROPERTY_FIELD_NAME_INVALID, (Object) null);
  81 + }
  82 +
  83 + @Test
  84 + public void testValidatePropertyIdentifiers_reserved() {
  85 + assertServiceException(() -> IotStorageFieldUtils.validatePropertyIdentifiers(
  86 + Collections.singletonList("reportTime")), DEVICE_PROPERTY_FIELD_NAME_RESERVED, "reportTime", "report_time");
  87 + assertServiceException(() -> IotStorageFieldUtils.validatePropertyIdentifiers(
  88 + Collections.singletonList("deviceId")), DEVICE_PROPERTY_FIELD_NAME_RESERVED, "deviceId", "device_id");
  89 + }
  90 +
  91 + @Test
  92 + public void testValidatePropertyIdentifiers_duplicate() {
  93 + assertServiceException(() -> IotStorageFieldUtils.validatePropertyIdentifiers(
  94 + Arrays.asList("fooBar", "foo_bar")), DEVICE_PROPERTY_FIELD_NAME_DUPLICATE, "foo_bar", "fooBar", "foo_bar");
  95 + }
  96 +
  97 +}
... ...
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/framework/tdengine/core/TDengineTableFieldTest.java deleted
1   -package cn.iocoder.yudao.module.iot.framework.tdengine.core;
2   -
3   -import org.junit.jupiter.api.Test;
4   -
5   -import static org.junit.jupiter.api.Assertions.assertEquals;
6   -import static org.junit.jupiter.api.Assertions.assertNull;
7   -
8   -public class TDengineTableFieldTest {
9   -
10   - @Test
11   - public void testBuildFieldName() {
12   - assertEquals("ua", TDengineTableField.buildFieldName("Ua"));
13   - assertEquals("pf_t", TDengineTableField.buildFieldName("PfT"));
14   - assertEquals("pt", TDengineTableField.buildFieldName("PT"));
15   - assertEquals("pa", TDengineTableField.buildFieldName("PA"));
16   - assertEquals("geo_location", TDengineTableField.buildFieldName("GeoLocation"));
17   - assertEquals("light_status", TDengineTableField.buildFieldName("LightStatus"));
18   - }
19   -
20   - @Test
21   - public void testBuildFieldName_blank() {
22   - assertNull(TDengineTableField.buildFieldName(null));
23   - assertEquals("", TDengineTableField.buildFieldName(""));
24   - assertEquals(" ", TDengineTableField.buildFieldName(" "));
25   - }
26   -
27   -}
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageServiceImplTest.java
... ... @@ -2,39 +2,43 @@ package cn.iocoder.yudao.module.iot.service.device.message;
2 2  
3 3 import cn.hutool.core.date.LocalDateTimeUtil;
4 4 import cn.iocoder.yudao.framework.common.exception.ServiceException;
  5 +import cn.iocoder.yudao.framework.common.enums.DateIntervalEnum;
5 6 import cn.iocoder.yudao.framework.common.pojo.PageResult;
6 7 import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
7 8 import cn.iocoder.yudao.module.iot.controller.admin.device.vo.message.IotDeviceMessagePageReqVO;
  9 +import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsDeviceMessageReqVO;
  10 +import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsDeviceMessageSummaryByDateRespVO;
8 11 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
9 12 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
10 13 import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer;
11 14 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
12 15 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceMessageDO;
13   -import cn.iocoder.yudao.module.iot.dal.tdengine.IotDeviceMessageMapper;
  16 +import cn.iocoder.yudao.module.iot.dal.storage.IotDeviceMessageDAO;
14 17 import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
15 18 import cn.iocoder.yudao.module.iot.service.device.property.IotDevicePropertyService;
16 19 import cn.iocoder.yudao.module.iot.service.ota.IotOtaTaskRecordService;
17   -import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
18 20 import org.junit.jupiter.api.Test;
19 21 import org.mockito.ArgumentCaptor;
20 22 import org.mockito.InjectMocks;
21 23 import org.mockito.Mock;
22 24  
  25 +import java.sql.Timestamp;
23 26 import java.time.LocalDateTime;
  27 +import java.util.Arrays;
24 28 import java.util.Collections;
25 29 import java.util.HashMap;
26 30 import java.util.List;
27 31  
28 32 import static org.junit.jupiter.api.Assertions.*;
29 33 import static org.mockito.ArgumentMatchers.any;
30   -import static org.mockito.ArgumentMatchers.eq;
31 34 import static org.mockito.ArgumentMatchers.isNull;
  35 +import static org.mockito.ArgumentMatchers.same;
32 36 import static org.mockito.Mockito.*;
33 37  
34 38 /**
35 39 * {@link IotDeviceMessageServiceImpl} 的单元测试
36 40 *
37   - * 注:TDengine 数据源没有 embedded 替代,mapper 与依赖 service 走 mock;
  41 + * 注:时序库没有 embedded 替代,存储 DAO 与依赖 service 走 mock;
38 42 * handleUpstreamDeviceMessage 与 sendDeviceMessage 下行成功路径依赖 SpringUtil.getBean 的自调用
39 43 * createDeviceLogAsync,更适合放到集成测试,本类不展开。
40 44 *
... ... @@ -52,36 +56,10 @@ public class IotDeviceMessageServiceImplTest extends BaseMockitoUnitTest {
52 56 @Mock
53 57 private IotOtaTaskRecordService otaTaskRecordService;
54 58 @Mock
55   - private IotDeviceMessageMapper deviceMessageMapper;
  59 + private IotDeviceMessageDAO deviceMessageDAO;
56 60 @Mock
57 61 private IotDeviceMessageProducer deviceMessageProducer;
58 62  
59   - // ========== defineDeviceMessageStable ==========
60   -
61   - @Test
62   - public void testDefineDeviceMessageStable_whenTableExists_skipCreate() {
63   - // 准备:showSTable 返回非空 → 表已存在
64   - when(deviceMessageMapper.showSTable()).thenReturn("device_message");
65   -
66   - // 调用
67   - service.defineDeviceMessageStable();
68   -
69   - // 断言:跳过 createSTable
70   - verify(deviceMessageMapper, never()).createSTable();
71   - }
72   -
73   - @Test
74   - public void testDefineDeviceMessageStable_whenTableMissing_create() {
75   - // 准备:showSTable 返回空 → 表不存在
76   - when(deviceMessageMapper.showSTable()).thenReturn("");
77   -
78   - // 调用
79   - service.defineDeviceMessageStable();
80   -
81   - // 断言:触发 createSTable
82   - verify(deviceMessageMapper, times(1)).createSTable();
83   - }
84   -
85 63 // ========== createDeviceLogAsync ==========
86 64  
87 65 @Test
... ... @@ -96,7 +74,7 @@ public class IotDeviceMessageServiceImplTest extends BaseMockitoUnitTest {
96 74  
97 75 // 断言:mapper.insert 接收到的 messageDO 已被填上 ts,值在 [before, after] 区间
98 76 ArgumentCaptor<IotDeviceMessageDO> captor = ArgumentCaptor.forClass(IotDeviceMessageDO.class);
99   - verify(deviceMessageMapper).insert(captor.capture());
  77 + verify(deviceMessageDAO).insert(captor.capture());
100 78 Long actualTs = captor.getValue().getTs();
101 79 assertNotNull(actualTs, "ts 不应为空");
102 80 assertTrue(actualTs >= before && actualTs <= after,
... ... @@ -114,7 +92,7 @@ public class IotDeviceMessageServiceImplTest extends BaseMockitoUnitTest {
114 92  
115 93 // 断言:mapper.insert 接收到的 messageDO 已补充 id
116 94 ArgumentCaptor<IotDeviceMessageDO> captor = ArgumentCaptor.forClass(IotDeviceMessageDO.class);
117   - verify(deviceMessageMapper).insert(captor.capture());
  95 + verify(deviceMessageDAO).insert(captor.capture());
118 96 assertNotNull(captor.getValue().getId(), "id 不应为空");
119 97 }
120 98  
... ... @@ -122,11 +100,11 @@ public class IotDeviceMessageServiceImplTest extends BaseMockitoUnitTest {
122 100 public void testCreateDeviceLogAsync_swallowMapperException() {
123 101 // 准备:mapper.insert 抛异常,验证 @Async 方法内部 try/catch 兜底,不向上抛
124 102 IotDeviceMessage message = buildMessage(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod());
125   - doThrow(new RuntimeException("DB unavailable")).when(deviceMessageMapper).insert(any());
  103 + doThrow(new RuntimeException("DB unavailable")).when(deviceMessageDAO).insert(any());
126 104  
127 105 // 调用 & 断言
128 106 assertDoesNotThrow(() -> service.createDeviceLogAsync(message));
129   - verify(deviceMessageMapper).insert(any(IotDeviceMessageDO.class));
  107 + verify(deviceMessageDAO).insert(any(IotDeviceMessageDO.class));
130 108 }
131 109  
132 110 // ========== sendDeviceMessage ==========
... ... @@ -170,49 +148,18 @@ public class IotDeviceMessageServiceImplTest extends BaseMockitoUnitTest {
170 148 reqVO.setPageNo(1);
171 149 reqVO.setPageSize(10);
172 150 IotDeviceMessageDO record = new IotDeviceMessageDO().setId("msg-1");
173   - Page<IotDeviceMessageDO> page = new Page<>(1, 10, 1L);
174   - page.setRecords(Collections.singletonList(record));
175   - when(deviceMessageMapper.selectPage(any(), eq(reqVO))).thenReturn(page);
  151 + when(deviceMessageDAO.selectPage(same(reqVO)))
  152 + .thenReturn(new PageResult<>(Collections.singletonList(record), 1L));
176 153  
177 154 // 调用
178 155 PageResult<IotDeviceMessageDO> result = service.getDeviceMessagePage(reqVO);
179 156  
180   - // 断言
  157 + // 断言:表不存在等存储差异,由 DAO 处理(见各 DAO 的单元测试)
181 158 assertEquals(1L, result.getTotal());
182 159 assertEquals(1, result.getList().size());
183 160 assertEquals("msg-1", result.getList().get(0).getId());
184 161 }
185 162  
186   - @Test
187   - public void testGetDeviceMessagePage_whenTableMissing_returnEmpty() {
188   - // 准备:mapper 抛 "Table does not exist" → 视为表未创建,返回空结果
189   - IotDeviceMessagePageReqVO reqVO = new IotDeviceMessagePageReqVO();
190   - reqVO.setPageNo(1);
191   - reqVO.setPageSize(10);
192   - when(deviceMessageMapper.selectPage(any(), any()))
193   - .thenThrow(new RuntimeException("Table does not exist"));
194   -
195   - // 调用
196   - PageResult<IotDeviceMessageDO> result = service.getDeviceMessagePage(reqVO);
197   -
198   - // 断言
199   - assertEquals(0L, result.getTotal());
200   - assertTrue(result.getList().isEmpty());
201   - }
202   -
203   - @Test
204   - public void testGetDeviceMessagePage_otherException_rethrow() {
205   - // 准备:mapper 抛非「表不存在」的异常 → 应向上抛
206   - IotDeviceMessagePageReqVO reqVO = new IotDeviceMessagePageReqVO();
207   - reqVO.setPageNo(1);
208   - reqVO.setPageSize(10);
209   - when(deviceMessageMapper.selectPage(any(), any()))
210   - .thenThrow(new RuntimeException("Connection refused"));
211   -
212   - // 调用 & 断言
213   - assertThrows(RuntimeException.class, () -> service.getDeviceMessagePage(reqVO));
214   - }
215   -
216 163 // ========== getDeviceMessageListByRequestIdsAndReply ==========
217 164  
218 165 @Test
... ... @@ -222,7 +169,7 @@ public class IotDeviceMessageServiceImplTest extends BaseMockitoUnitTest {
222 169 1L, Collections.emptyList(), true);
223 170  
224 171 assertTrue(result.isEmpty());
225   - verify(deviceMessageMapper, never()).selectListByRequestIdsAndReply(any(), any(), any());
  172 + verify(deviceMessageDAO, never()).selectListByRequestIdsAndReply(any(), any(), any());
226 173 }
227 174  
228 175 @Test
... ... @@ -230,7 +177,7 @@ public class IotDeviceMessageServiceImplTest extends BaseMockitoUnitTest {
230 177 // 准备
231 178 List<String> requestIds = Collections.singletonList("req-1");
232 179 IotDeviceMessageDO record = new IotDeviceMessageDO().setId("msg-1");
233   - when(deviceMessageMapper.selectListByRequestIdsAndReply(1L, requestIds, true))
  180 + when(deviceMessageDAO.selectListByRequestIdsAndReply(1L, requestIds, true))
234 181 .thenReturn(Collections.singletonList(record));
235 182  
236 183 // 调用
... ... @@ -247,14 +194,14 @@ public class IotDeviceMessageServiceImplTest extends BaseMockitoUnitTest {
247 194 @Test
248 195 public void testGetDeviceMessageCount_whenCreateTimeNull_passNullToMapper() {
249 196 // 准备
250   - when(deviceMessageMapper.selectCountByCreateTime(isNull())).thenReturn(123L);
  197 + when(deviceMessageDAO.selectCountByCreateTime(isNull())).thenReturn(123L);
251 198  
252 199 // 调用
253 200 Long count = service.getDeviceMessageCount(null);
254 201  
255 202 // 断言
256 203 assertEquals(123L, count);
257   - verify(deviceMessageMapper).selectCountByCreateTime(isNull());
  204 + verify(deviceMessageDAO).selectCountByCreateTime(isNull());
258 205 }
259 206  
260 207 @Test
... ... @@ -262,7 +209,7 @@ public class IotDeviceMessageServiceImplTest extends BaseMockitoUnitTest {
262 209 // 准备:service 内部用 LocalDateTimeUtil.toEpochMilli 做转换,断言时也用同函数得到期望值
263 210 LocalDateTime createTime = LocalDateTime.of(2026, 1, 1, 0, 0);
264 211 long expectedMs = LocalDateTimeUtil.toEpochMilli(createTime);
265   - when(deviceMessageMapper.selectCountByCreateTime(expectedMs)).thenReturn(456L);
  212 + when(deviceMessageDAO.selectCountByCreateTime(expectedMs)).thenReturn(456L);
266 213  
267 214 // 调用
268 215 Long count = service.getDeviceMessageCount(createTime);
... ... @@ -271,6 +218,33 @@ public class IotDeviceMessageServiceImplTest extends BaseMockitoUnitTest {
271 218 assertEquals(456L, count);
272 219 }
273 220  
  221 + // ========== getDeviceMessageSummaryByDate ==========
  222 +
  223 + @Test
  224 + public void testGetDeviceMessageSummaryByDate_success() {
  225 + // 准备参数:按天合并 2026-01-01 的小时统计
  226 + IotStatisticsDeviceMessageReqVO reqVO = new IotStatisticsDeviceMessageReqVO();
  227 + reqVO.setInterval(DateIntervalEnum.DAY.getInterval());
  228 + LocalDateTime startTime = LocalDateTime.of(2026, 1, 1, 0, 0);
  229 + LocalDateTime endTime = LocalDateTime.of(2026, 1, 1, 23, 59, 59);
  230 + reqVO.setTimes(new LocalDateTime[]{startTime, endTime});
  231 +
  232 + // mock 方法:DAO 统一返回 Timestamp 时间、Long 数量
  233 + when(deviceMessageDAO.selectDeviceMessageCountGroupByDate(
  234 + LocalDateTimeUtil.toEpochMilli(startTime), LocalDateTimeUtil.toEpochMilli(endTime)))
  235 + .thenReturn(Arrays.asList(
  236 + IotDeviceMessageDAO.buildMessageCount(Timestamp.valueOf(startTime.withHour(1)), 3L, 1L),
  237 + IotDeviceMessageDAO.buildMessageCount(Timestamp.valueOf(startTime.withHour(2)), 2L, 0L)));
  238 +
  239 + // 调用
  240 + List<IotStatisticsDeviceMessageSummaryByDateRespVO> list = service.getDeviceMessageSummaryByDate(reqVO);
  241 +
  242 + // 断言
  243 + assertEquals(1, list.size());
  244 + assertEquals(5, list.get(0).getUpstreamCount());
  245 + assertEquals(1, list.get(0).getDownstreamCount());
  246 + }
  247 +
274 248 // ========== 辅助方法 ==========
275 249  
276 250 /** 构造一条最简消息(指定 method 决定上下行分支) */
... ...
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/device/property/IotDevicePropertyServiceImplTest.java
1 1 package cn.iocoder.yudao.module.iot.service.device.property;
2 2  
  3 +import cn.hutool.core.date.LocalDateTimeUtil;
  4 +import cn.hutool.core.map.MapUtil;
3 5 import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
  6 +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.property.IotDevicePropertyHistoryListReqVO;
  7 +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.property.IotDevicePropertyRespVO;
4 8 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
5 9 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
6 10 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
... ... @@ -9,10 +13,10 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
9 13 import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO;
10 14 import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.ThingModelProperty;
11 15 import cn.iocoder.yudao.module.iot.dal.redis.device.DevicePropertyRedisDAO;
12   -import cn.iocoder.yudao.module.iot.dal.tdengine.IotDevicePropertyMapper;
  16 +import cn.iocoder.yudao.module.iot.dal.storage.IotDevicePropertyDAO;
13 17 import cn.iocoder.yudao.module.iot.enums.thingmodel.IotDataSpecsDataTypeEnum;
14 18 import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelTypeEnum;
15   -import cn.iocoder.yudao.module.iot.framework.tdengine.core.TDengineTableField;
  19 +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
16 20 import cn.iocoder.yudao.module.iot.service.product.IotProductService;
17 21 import cn.iocoder.yudao.module.iot.service.thingmodel.IotThingModelService;
18 22 import org.junit.jupiter.api.Test;
... ... @@ -22,12 +26,14 @@ import org.mockito.Mock;
22 26  
23 27 import java.time.LocalDateTime;
24 28 import java.util.Arrays;
25   -import java.util.Collections;
26 29 import java.util.HashMap;
27 30 import java.util.List;
28 31 import java.util.Map;
29   -import java.util.stream.Collectors;
30 32  
  33 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  34 +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
  35 +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DEVICE_PROPERTY_HISTORY_TIMES_INVALID;
  36 +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.PRODUCT_NOT_EXISTS;
31 37 import static java.util.Collections.singletonList;
32 38 import static org.junit.jupiter.api.Assertions.*;
33 39 import static org.mockito.ArgumentMatchers.*;
... ... @@ -48,7 +54,9 @@ public class IotDevicePropertyServiceImplTest extends BaseMockitoUnitTest {
48 54 @Mock
49 55 private IotProductService productService;
50 56 @Mock
51   - private IotDevicePropertyMapper devicePropertyMapper;
  57 + private IotDeviceService deviceService;
  58 + @Mock
  59 + private IotDevicePropertyDAO devicePropertyDAO;
52 60 @Mock
53 61 private DevicePropertyRedisDAO deviceDataRedisDAO;
54 62  
... ... @@ -97,7 +105,7 @@ public class IotDevicePropertyServiceImplTest extends BaseMockitoUnitTest {
97 105 service.saveDeviceProperty(device, message);
98 106  
99 107 // 断言:没有合法属性,不会写入 TDengine 与 Redis
100   - verify(devicePropertyMapper, never()).insert(any(), any(), anyLong(), anyLong());
  108 + verify(devicePropertyDAO, never()).insert(any(), any(), anyLong(), anyLong());
101 109 verify(deviceDataRedisDAO, never()).putAll(anyLong(), any());
102 110 }
103 111  
... ... @@ -119,7 +127,7 @@ public class IotDevicePropertyServiceImplTest extends BaseMockitoUnitTest {
119 127 assertDoesNotThrow(() -> service.saveDeviceProperty(device, message));
120 128  
121 129 // 断言:没有合法属性,不会写入 TDengine 与 Redis
122   - verify(devicePropertyMapper, never()).insert(any(), any(), anyLong(), anyLong());
  130 + verify(devicePropertyDAO, never()).insert(any(), any(), anyLong(), anyLong());
123 131 verify(deviceDataRedisDAO, never()).putAll(anyLong(), any());
124 132 }
125 133  
... ... @@ -141,7 +149,7 @@ public class IotDevicePropertyServiceImplTest extends BaseMockitoUnitTest {
141 149  
142 150 // 断言:跳过空值,不会转换属性值,也不会写入 TDengine 与 Redis
143 151 verify(thingModelService, never()).convertThingModelPropertyValue(any(), any());
144   - verify(devicePropertyMapper, never()).insert(any(), any(), anyLong(), anyLong());
  152 + verify(devicePropertyDAO, never()).insert(any(), any(), anyLong(), anyLong());
145 153 verify(deviceDataRedisDAO, never()).putAll(anyLong(), any());
146 154 }
147 155  
... ... @@ -170,31 +178,149 @@ public class IotDevicePropertyServiceImplTest extends BaseMockitoUnitTest {
170 178 }
171 179  
172 180 @Test
173   - public void testDefineDevicePropertyData_fieldNameToLowerCase() {
174   - // 准备参数:全大写缩写和驼峰缩写都需要转换为 TDengine 实际的小写字段名
  181 + public void testDefineDevicePropertyData_success() {
  182 + // 准备参数:只有属性类型的物模型,才需要定义存储结构
175 183 Long productId = 2L;
176 184 IotProductDO product = IotProductDO.builder().id(productId).build();
177   - List<IotThingModelDO> thingModels = Arrays.asList(
178   - buildThingModel("Ua", IotDataSpecsDataTypeEnum.FLOAT.getDataType()),
179   - buildThingModel("PfT", IotDataSpecsDataTypeEnum.FLOAT.getDataType()),
180   - buildThingModel("PT", IotDataSpecsDataTypeEnum.FLOAT.getDataType()),
181   - buildThingModel("PA", IotDataSpecsDataTypeEnum.FLOAT.getDataType()));
182   - thingModels.forEach(thingModel -> thingModel.setType(IotThingModelTypeEnum.PROPERTY.getType()));
  185 + IotThingModelDO temperature = buildThingModel("Temperature", IotDataSpecsDataTypeEnum.FLOAT.getDataType());
  186 + temperature.setType(IotThingModelTypeEnum.PROPERTY.getType());
  187 + IotThingModelDO alarm = buildThingModel("Alarm", IotDataSpecsDataTypeEnum.INT.getDataType());
  188 + alarm.setType(IotThingModelTypeEnum.EVENT.getType());
183 189  
184 190 // mock 方法
185 191 when(productService.validateProductExists(productId)).thenReturn(product);
186   - when(thingModelService.getThingModelListByProductId(productId)).thenReturn(thingModels);
187   - when(devicePropertyMapper.getProductPropertySTableFieldList(productId)).thenReturn(Collections.emptyList());
  192 + when(thingModelService.getThingModelListByProductId(productId)).thenReturn(Arrays.asList(temperature, alarm));
188 193  
189 194 // 调用
190 195 service.defineDevicePropertyData(productId);
191 196  
192   - // 断言:字段名统一为小写下划线,避免 PT 和数据库中的 pt 被误判为不同字段
193   - ArgumentCaptor<List<TDengineTableField>> captor = ArgumentCaptor.forClass(List.class);
194   - verify(devicePropertyMapper).createProductPropertySTable(eq(productId), captor.capture());
195   - assertEquals(Arrays.asList("ua", "pf_t", "pt", "pa"), captor.getValue().stream()
196   - .map(TDengineTableField::getField)
197   - .collect(Collectors.toList()));
  197 + // 断言:只传入属性物模型,由存储 DAO 完成字段映射与差异比较
  198 + verify(devicePropertyDAO).defineProductPropertyTable(eq(productId), eq(singletonList(temperature)));
  199 + }
  200 +
  201 + @Test
  202 + public void testDefineDevicePropertyData_productNotExists() {
  203 + // 准备参数
  204 + Long productId = 2L;
  205 +
  206 + // mock 方法:产品不存在
  207 + when(productService.validateProductExists(productId))
  208 + .thenThrow(exception(PRODUCT_NOT_EXISTS));
  209 +
  210 + // 调用,并断言
  211 + assertServiceException(() -> service.defineDevicePropertyData(productId), PRODUCT_NOT_EXISTS);
  212 + verify(devicePropertyDAO, never()).defineProductPropertyTable(any(), any());
  213 + }
  214 +
  215 + @Test
  216 + public void testSaveDeviceProperty_success() {
  217 + // 准备参数
  218 + IotDeviceDO device = buildDevice();
  219 + IotThingModelDO thingModel = buildThingModel("Temperature", IotDataSpecsDataTypeEnum.INT.getDataType());
  220 + Map<String, Object> params = new HashMap<>();
  221 + params.put("Temperature", 36);
  222 + IotDeviceMessage message = buildMessage(params);
  223 +
  224 + // mock 方法
  225 + when(thingModelService.getThingModelListByProductIdFromCache(device.getProductId()))
  226 + .thenReturn(singletonList(thingModel));
  227 + when(thingModelService.convertThingModelPropertyValue(thingModel, 36)).thenReturn(36);
  228 +
  229 + // 调用
  230 + long before = System.currentTimeMillis();
  231 + service.saveDeviceProperty(device, message);
  232 + long after = System.currentTimeMillis();
  233 +
  234 + // 断言:ts 为存储时间,reportTime 为上报时间
  235 + ArgumentCaptor<Long> tsCaptor = ArgumentCaptor.forClass(Long.class);
  236 + verify(devicePropertyDAO).insert(eq(device), eq(MapUtil.of("Temperature", 36)), tsCaptor.capture(),
  237 + eq(LocalDateTimeUtil.toEpochMilli(message.getReportTime())));
  238 + assertTrue(tsCaptor.getValue() >= before && tsCaptor.getValue() <= after);
  239 + }
  240 +
  241 + // ========== getHistoryDevicePropertyList ==========
  242 +
  243 + @Test
  244 + public void testGetHistoryDevicePropertyList_success() {
  245 + // 准备参数
  246 + IotDevicePropertyHistoryListReqVO reqVO = buildHistoryReqVO("Temperature",
  247 + LocalDateTime.of(2026, 1, 1, 0, 0), LocalDateTime.of(2026, 1, 2, 0, 0));
  248 + IotDevicePropertyRespVO respVO = new IotDevicePropertyRespVO();
  249 + respVO.setValue(36);
  250 + respVO.setUpdateTime(1L);
  251 +
  252 + // mock 方法
  253 + when(deviceService.getDevice(eq(1L))).thenReturn(buildDevice());
  254 + when(devicePropertyDAO.selectHistoryList(eq(2L), same(reqVO))).thenReturn(singletonList(respVO));
  255 +
  256 + // 调用
  257 + List<IotDevicePropertyRespVO> list = service.getHistoryDevicePropertyList(reqVO);
  258 +
  259 + // 断言:按设备的产品编号查询时序库;使用按租户、逻辑删除过滤的 getDevice,而不是忽略租户的缓存查询
  260 + assertEquals(singletonList(respVO), list);
  261 + verify(deviceService, never()).getDeviceFromCache(anyLong());
  262 + }
  263 +
  264 + @Test
  265 + public void testGetHistoryDevicePropertyList_timesNull() {
  266 + // 准备参数
  267 + IotDevicePropertyHistoryListReqVO reqVO = buildHistoryReqVO("Temperature");
  268 + reqVO.setTimes(null);
  269 +
  270 + // 调用,并断言
  271 + assertServiceException(() -> service.getHistoryDevicePropertyList(reqVO), DEVICE_PROPERTY_HISTORY_TIMES_INVALID);
  272 + verifyNoInteractions(deviceService, devicePropertyDAO);
  273 + }
  274 +
  275 + @Test
  276 + public void testGetHistoryDevicePropertyList_timesLengthInvalid() {
  277 + // 准备参数
  278 + IotDevicePropertyHistoryListReqVO reqVO = buildHistoryReqVO("Temperature", LocalDateTime.now());
  279 +
  280 + // 调用,并断言
  281 + assertServiceException(() -> service.getHistoryDevicePropertyList(reqVO), DEVICE_PROPERTY_HISTORY_TIMES_INVALID);
  282 + verifyNoInteractions(deviceService, devicePropertyDAO);
  283 + }
  284 +
  285 + @Test
  286 + public void testGetHistoryDevicePropertyList_timeNull() {
  287 + // 准备参数:开始时间为空、结束时间为空
  288 + IotDevicePropertyHistoryListReqVO reqVO1 = buildHistoryReqVO("Temperature", null, LocalDateTime.now());
  289 + IotDevicePropertyHistoryListReqVO reqVO2 = buildHistoryReqVO("Temperature", LocalDateTime.now(), null);
  290 +
  291 + // 调用,并断言:返回空,不查询设备与时序库
  292 + assertTrue(service.getHistoryDevicePropertyList(reqVO1).isEmpty());
  293 + assertTrue(service.getHistoryDevicePropertyList(reqVO2).isEmpty());
  294 + verifyNoInteractions(deviceService, devicePropertyDAO);
  295 + }
  296 +
  297 + @Test
  298 + public void testGetHistoryDevicePropertyList_identifierInvalid() {
  299 + // 准备参数:保留字段、非法格式
  300 + LocalDateTime now = LocalDateTime.now();
  301 + IotDevicePropertyHistoryListReqVO reqVO1 = buildHistoryReqVO("reportTime", now, now);
  302 + IotDevicePropertyHistoryListReqVO reqVO2 = buildHistoryReqVO("ts", now, now);
  303 + IotDevicePropertyHistoryListReqVO reqVO3 = buildHistoryReqVO("a` OR 1=1", now, now);
  304 +
  305 + // 调用,并断言:返回空,不查询设备与时序库
  306 + assertTrue(service.getHistoryDevicePropertyList(reqVO1).isEmpty());
  307 + assertTrue(service.getHistoryDevicePropertyList(reqVO2).isEmpty());
  308 + assertTrue(service.getHistoryDevicePropertyList(reqVO3).isEmpty());
  309 + verifyNoInteractions(deviceService, devicePropertyDAO);
  310 + }
  311 +
  312 + @Test
  313 + public void testGetHistoryDevicePropertyList_deviceNotExists() {
  314 + // 准备参数
  315 + LocalDateTime now = LocalDateTime.now();
  316 + IotDevicePropertyHistoryListReqVO reqVO = buildHistoryReqVO("Temperature", now, now);
  317 +
  318 + // mock 方法:设备不存在、已删除,或者不属于当前租户
  319 + when(deviceService.getDevice(eq(1L))).thenReturn(null);
  320 +
  321 + // 调用,并断言:返回空,不查询时序库
  322 + assertTrue(service.getHistoryDevicePropertyList(reqVO).isEmpty());
  323 + verifyNoInteractions(devicePropertyDAO);
198 324 }
199 325  
200 326 // ========== 辅助方法 ==========
... ... @@ -228,12 +354,23 @@ public class IotDevicePropertyServiceImplTest extends BaseMockitoUnitTest {
228 354 }
229 355  
230 356 /**
231   - * 抓取 mapper.insert 的 properties 入参
  357 + * 构造属性历史查询条件,设备编号固定为 1
  358 + */
  359 + private IotDevicePropertyHistoryListReqVO buildHistoryReqVO(String identifier, LocalDateTime... times) {
  360 + IotDevicePropertyHistoryListReqVO reqVO = new IotDevicePropertyHistoryListReqVO();
  361 + reqVO.setDeviceId(1L);
  362 + reqVO.setIdentifier(identifier);
  363 + reqVO.setTimes(times);
  364 + return reqVO;
  365 + }
  366 +
  367 + /**
  368 + * 抓取 DAO.insert 的 properties 入参
232 369 */
233 370 @SuppressWarnings("unchecked")
234 371 private Map<String, Object> captureMapperInsertProperties() {
235 372 ArgumentCaptor<Map<String, Object>> captor = ArgumentCaptor.forClass(Map.class);
236   - verify(devicePropertyMapper).insert(any(IotDeviceDO.class), captor.capture(), anyLong(), anyLong());
  373 + verify(devicePropertyDAO).insert(any(IotDeviceDO.class), captor.capture(), anyLong(), anyLong());
237 374 return captor.getValue();
238 375 }
239 376  
... ...
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImplTest.java
1 1 package cn.iocoder.yudao.module.iot.service.thingmodel;
2 2  
3 3 import cn.hutool.core.date.LocalDateTimeUtil;
  4 +import cn.hutool.core.util.StrUtil;
4 5 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
5 6 import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
6 7 import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO;
... ... @@ -15,11 +16,14 @@ import org.junit.jupiter.api.Test;
15 16 import org.springframework.context.annotation.Import;
16 17 import org.springframework.test.context.bean.override.mockito.MockitoBean;
17 18  
  19 +import java.nio.charset.StandardCharsets;
18 20 import java.time.LocalDateTime;
19 21 import java.util.Arrays;
20 22 import java.util.HashMap;
  23 +import java.util.List;
21 24 import java.util.Map;
22 25  
  26 +import static java.util.Collections.singletonList;
23 27 import static org.junit.jupiter.api.Assertions.*;
24 28  
25 29 /**
... ... @@ -162,6 +166,51 @@ public class IotThingModelServiceImplTest extends BaseDbUnitTest {
162 166 assertEquals(JsonUtils.toJsonString(value), result);
163 167 }
164 168  
  169 + @Test
  170 + public void testConvertThingModelPropertyValue_jsonMaxLength() {
  171 + // 准备参数:JSON 恰好 LENGTH_JSON 字节,包含中文、emoji 等多字节字符
  172 + Map<String, Object> structValue = buildJsonValue(ThingModelProperty.LENGTH_JSON);
  173 + List<Object> arrayValue = singletonList(buildJsonValue(ThingModelProperty.LENGTH_JSON - 2));
  174 + String structJson = JsonUtils.toJsonString(structValue);
  175 + String arrayJson = JsonUtils.toJsonString(arrayValue);
  176 + assertEquals(ThingModelProperty.LENGTH_JSON, structJson.getBytes(StandardCharsets.UTF_8).length);
  177 + assertEquals(ThingModelProperty.LENGTH_JSON, arrayJson.getBytes(StandardCharsets.UTF_8).length);
  178 + assertTrue(structJson.length() < ThingModelProperty.LENGTH_JSON); // 按字节而不是字符计算
  179 +
  180 + // 调用,并断言:STRUCT、ARRAY 都可以写入
  181 + assertEquals(structJson, thingModelService.convertThingModelPropertyValue(
  182 + buildThingModel("Location", IotDataSpecsDataTypeEnum.STRUCT.getDataType()), structValue));
  183 + assertEquals(arrayJson, thingModelService.convertThingModelPropertyValue(
  184 + buildThingModel("Points", IotDataSpecsDataTypeEnum.ARRAY.getDataType()), arrayValue));
  185 + }
  186 +
  187 + @Test
  188 + public void testConvertThingModelPropertyValue_jsonOverLength() {
  189 + // 准备参数:JSON 超过 LENGTH_JSON 1 个字节,包含中文、emoji 等多字节字符
  190 + Map<String, Object> structValue = buildJsonValue(ThingModelProperty.LENGTH_JSON + 1);
  191 + List<Object> arrayValue = singletonList(buildJsonValue(ThingModelProperty.LENGTH_JSON - 1));
  192 + assertEquals(ThingModelProperty.LENGTH_JSON + 1, JsonUtils.toJsonString(structValue).getBytes(StandardCharsets.UTF_8).length);
  193 + assertEquals(ThingModelProperty.LENGTH_JSON + 1, JsonUtils.toJsonString(arrayValue).getBytes(StandardCharsets.UTF_8).length);
  194 +
  195 + // 调用,并断言:STRUCT、ARRAY 都返回 null,不写入
  196 + assertNull(thingModelService.convertThingModelPropertyValue(
  197 + buildThingModel("Location", IotDataSpecsDataTypeEnum.STRUCT.getDataType()), structValue));
  198 + assertNull(thingModelService.convertThingModelPropertyValue(
  199 + buildThingModel("Points", IotDataSpecsDataTypeEnum.ARRAY.getDataType()), arrayValue));
  200 + }
  201 +
  202 + /**
  203 + * 构建 JSON 序列化后恰好 jsonBytes 字节(UTF-8)的结构体,包含中文、emoji 等多字节字符
  204 + */
  205 + private static Map<String, Object> buildJsonValue(int jsonBytes) {
  206 + String prefix = "中文😀";
  207 + Map<String, Object> value = new HashMap<>();
  208 + value.put("name", prefix);
  209 + int padding = jsonBytes - JsonUtils.toJsonString(value).getBytes(StandardCharsets.UTF_8).length;
  210 + value.put("name", prefix + StrUtil.repeat('x', padding));
  211 + return value;
  212 + }
  213 +
165 214 private void assertBoolValueConvertedToByte(Object reportedValue, byte expected) {
166 215 IotThingModelDO thingModel = buildThingModel("PowerSwitch", IotDataSpecsDataTypeEnum.BOOL.getDataType());
167 216  
... ...
yudao-module-iot/yudao-module-iot-biz/src/test/resources/sql/create_tables.sql
... ... @@ -43,20 +43,23 @@ CREATE TABLE IF NOT EXISTS &quot;iot_product&quot; (
43 43 CREATE TABLE IF NOT EXISTS "iot_device" (
44 44 "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
45 45 "device_name" varchar(255) NOT NULL DEFAULT '',
46   - "product_id" bigint NOT NULL,
47   - "device_key" varchar(100) NOT NULL DEFAULT '',
48   - "device_secret" varchar(100) NOT NULL DEFAULT '',
49 46 "nickname" varchar(255) DEFAULT NULL,
50   - "status" tinyint NOT NULL DEFAULT '0',
51   - "status_last_update_time" timestamp DEFAULT NULL,
52   - "last_online_time" timestamp DEFAULT NULL,
53   - "last_offline_time" timestamp DEFAULT NULL,
54   - "active_time" timestamp DEFAULT NULL,
55   - "ip" varchar(50) DEFAULT NULL,
56   - "firmware_version" varchar(50) DEFAULT NULL,
57   - "device_type" tinyint NOT NULL DEFAULT '0',
  47 + "serial_number" varchar(255) DEFAULT NULL,
  48 + "pic_url" varchar(255) DEFAULT NULL,
  49 + "group_ids" varchar(255) DEFAULT NULL,
  50 + "product_id" bigint NOT NULL,
  51 + "product_key" varchar(100) DEFAULT NULL,
  52 + "device_type" tinyint DEFAULT NULL,
58 53 "gateway_id" bigint DEFAULT NULL,
59   - "sub_device_count" int NOT NULL DEFAULT '0',
  54 + "state" tinyint DEFAULT NULL,
  55 + "online_time" timestamp DEFAULT NULL,
  56 + "offline_time" timestamp DEFAULT NULL,
  57 + "active_time" timestamp DEFAULT NULL,
  58 + "firmware_id" bigint DEFAULT NULL,
  59 + "device_secret" varchar(100) DEFAULT NULL,
  60 + "latitude" decimal(10, 6) DEFAULT NULL,
  61 + "longitude" decimal(10, 6) DEFAULT NULL,
  62 + "config" varchar(4096) DEFAULT NULL,
60 63 "creator" varchar(64) DEFAULT '',
61 64 "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
62 65 "updater" varchar(64) DEFAULT '',
... ...
yudao-module-mall/yudao-module-product/src/main/java/cn/iocoder/yudao/module/product/controller/admin/history/vo/ProductBrowseHistoryRespVO.java
1 1 package cn.iocoder.yudao.module.product.controller.admin.history.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4 3 import io.swagger.v3.oas.annotations.media.Schema;
5 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
6 6  
7 7 @Schema(description = "管理后台 - 商品浏览记录 Response VO")
8 8 @Data
... ...
yudao-module-mall/yudao-module-product/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/vo/ProductSpuRespVO.java
... ... @@ -4,10 +4,10 @@ import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.framework.excel.core.convert.MoneyConvert;
6 6 import cn.iocoder.yudao.module.product.enums.DictTypeConstants;
7   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
8   -import cn.idev.excel.annotation.ExcelProperty;
9 7 import io.swagger.v3.oas.annotations.media.Schema;
10 8 import lombok.Data;
  9 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  10 +import org.apache.fesod.sheet.annotation.ExcelProperty;
11 11  
12 12 import java.time.LocalDateTime;
13 13 import java.util.List;
... ...
yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/point/vo/activity/PointActivityRespVO.java
1 1 package cn.iocoder.yudao.module.promotion.controller.admin.point.vo.activity;
2 2  
3 3 import cn.iocoder.yudao.module.promotion.controller.admin.point.vo.product.PointProductRespVO;
4   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
5   -import cn.idev.excel.annotation.ExcelProperty;
6 4 import io.swagger.v3.oas.annotations.media.Schema;
7 5 import lombok.Data;
  6 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  7 +import org.apache.fesod.sheet.annotation.ExcelProperty;
8 8  
9 9 import java.time.LocalDateTime;
10 10 import java.util.List;
... ...
yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/point/vo/product/PointProductRespVO.java
1 1 package cn.iocoder.yudao.module.promotion.controller.admin.point.vo.product;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4 3 import io.swagger.v3.oas.annotations.media.Schema;
5 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
6 6  
7 7 @Schema(description = "管理后台 - 积分商城商品 Response VO")
8 8 @Data
... ...
yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/point/vo/AppPointActivityRespVO.java
1 1 package cn.iocoder.yudao.module.promotion.controller.app.point.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelProperty;
4 3 import io.swagger.v3.oas.annotations.media.Schema;
5 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelProperty;
6 6  
7 7 @Schema(description = "用户 App - 积分商城活动 Response VO")
8 8 @Data
... ...
yudao-module-mall/yudao-module-statistics/src/main/java/cn/iocoder/yudao/module/statistics/controller/admin/product/vo/ProductStatisticsRespVO.java
1 1 package cn.iocoder.yudao.module.statistics.controller.admin.product.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import com.fasterxml.jackson.annotation.JsonFormat;
6 4 import io.swagger.v3.oas.annotations.media.Schema;
7 5 import lombok.Data;
  6 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  7 +import org.apache.fesod.sheet.annotation.ExcelProperty;
8 8  
9 9 import java.time.LocalDate;
10 10  
... ...
yudao-module-mall/yudao-module-statistics/src/main/java/cn/iocoder/yudao/module/statistics/controller/admin/trade/vo/TradeTrendSummaryExcelVO.java
1 1 package cn.iocoder.yudao.module.statistics.controller.admin.trade.vo;
2 2  
3 3 import cn.iocoder.yudao.framework.excel.core.convert.MoneyConvert;
4   -import cn.idev.excel.annotation.ExcelProperty;
5   -import cn.idev.excel.annotation.format.DateTimeFormat;
6 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelProperty;
  6 +import org.apache.fesod.sheet.annotation.format.DateTimeFormat;
7 7  
8 8 import java.time.LocalDate;
9 9  
... ...
yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/delivery/vo/express/DeliveryExpressExcelVO.java
... ... @@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.trade.controller.admin.delivery.vo.express;
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.module.system.enums.DictTypeConstants;
6   -import cn.idev.excel.annotation.ExcelProperty;
7 6 import lombok.Data;
  7 +import org.apache.fesod.sheet.annotation.ExcelProperty;
8 8  
9 9 import java.time.LocalDateTime;
10 10  
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/cal/plan/vo/MesCalPlanRespVO.java
1 1 package cn.iocoder.yudao.module.mes.controller.admin.cal.plan.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  6 +import org.apache.fesod.sheet.annotation.ExcelProperty;
7 7  
8 8 import java.time.LocalDateTime;
9 9  
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/cal/team/vo/MesCalTeamRespVO.java
1 1 package cn.iocoder.yudao.module.mes.controller.admin.cal.team.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  6 +import org.apache.fesod.sheet.annotation.ExcelProperty;
7 7  
8 8 import java.time.LocalDateTime;
9 9  
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/checkplan/vo/MesDvCheckPlanRespVO.java
1 1 package cn.iocoder.yudao.module.mes.controller.admin.dv.checkplan.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  6 +import org.apache.fesod.sheet.annotation.ExcelProperty;
7 7  
8 8 import java.time.LocalDateTime;
9 9  
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/checkrecord/vo/MesDvCheckRecordRespVO.java
... ... @@ -3,10 +3,10 @@ package cn.iocoder.yudao.module.mes.controller.admin.dv.checkrecord.vo;
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
6   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
7   -import cn.idev.excel.annotation.ExcelProperty;
8 6 import io.swagger.v3.oas.annotations.media.Schema;
9 7 import lombok.Data;
  8 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  9 +import org.apache.fesod.sheet.annotation.ExcelProperty;
10 10  
11 11 import java.time.LocalDateTime;
12 12  
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/checkrecord/vo/line/MesDvCheckRecordLineRespVO.java
... ... @@ -3,10 +3,10 @@ package cn.iocoder.yudao.module.mes.controller.admin.dv.checkrecord.vo.line;
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
6   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
7   -import cn.idev.excel.annotation.ExcelProperty;
8 6 import io.swagger.v3.oas.annotations.media.Schema;
9 7 import lombok.Data;
  8 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  9 +import org.apache.fesod.sheet.annotation.ExcelProperty;
10 10  
11 11 import java.time.LocalDateTime;
12 12  
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/machinery/vo/MesDvMachineryImportExcelVO.java
1 1 package cn.iocoder.yudao.module.mes.controller.admin.dv.machinery.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelProperty;
4 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
5 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
6 5 import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
... ... @@ -8,6 +7,7 @@ import lombok.AllArgsConstructor;
8 7 import lombok.Builder;
9 8 import lombok.Data;
10 9 import lombok.NoArgsConstructor;
  10 +import org.apache.fesod.sheet.annotation.ExcelProperty;
11 11  
12 12 /**
13 13 * 设备台账 Excel 导入 VO
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/machinery/vo/MesDvMachineryRespVO.java
... ... @@ -3,10 +3,10 @@ package cn.iocoder.yudao.module.mes.controller.admin.dv.machinery.vo;
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
6   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
7   -import cn.idev.excel.annotation.ExcelProperty;
8 6 import io.swagger.v3.oas.annotations.media.Schema;
9 7 import lombok.Data;
  8 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  9 +import org.apache.fesod.sheet.annotation.ExcelProperty;
10 10  
11 11 import java.time.LocalDateTime;
12 12  
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/machinery/vo/type/MesDvMachineryTypeRespVO.java
1 1 package cn.iocoder.yudao.module.mes.controller.admin.dv.machinery.vo.type;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  6 +import org.apache.fesod.sheet.annotation.ExcelProperty;
7 7  
8 8 import java.time.LocalDateTime;
9 9  
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/maintenrecord/vo/MesDvMaintenRecordRespVO.java
... ... @@ -3,10 +3,10 @@ package cn.iocoder.yudao.module.mes.controller.admin.dv.maintenrecord.vo;
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
6   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
7   -import cn.idev.excel.annotation.ExcelProperty;
8 6 import io.swagger.v3.oas.annotations.media.Schema;
9 7 import lombok.Data;
  8 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  9 +import org.apache.fesod.sheet.annotation.ExcelProperty;
10 10  
11 11 import java.time.LocalDateTime;
12 12  
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/maintenrecord/vo/line/MesDvMaintenRecordLineRespVO.java
... ... @@ -3,10 +3,10 @@ package cn.iocoder.yudao.module.mes.controller.admin.dv.maintenrecord.vo.line;
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
6   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
7   -import cn.idev.excel.annotation.ExcelProperty;
8 6 import io.swagger.v3.oas.annotations.media.Schema;
9 7 import lombok.Data;
  8 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  9 +import org.apache.fesod.sheet.annotation.ExcelProperty;
10 10  
11 11 import java.time.LocalDateTime;
12 12  
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/repair/vo/MesDvRepairRespVO.java
... ... @@ -3,10 +3,10 @@ package cn.iocoder.yudao.module.mes.controller.admin.dv.repair.vo;
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
6   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
7   -import cn.idev.excel.annotation.ExcelProperty;
8 6 import io.swagger.v3.oas.annotations.media.Schema;
9 7 import lombok.Data;
  8 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  9 +import org.apache.fesod.sheet.annotation.ExcelProperty;
10 10  
11 11 import java.time.LocalDateTime;
12 12  
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/repair/vo/line/MesDvRepairLineRespVO.java
1 1 package cn.iocoder.yudao.module.mes.controller.admin.dv.repair.vo.line;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  6 +import org.apache.fesod.sheet.annotation.ExcelProperty;
7 7  
8 8 import java.time.LocalDateTime;
9 9  
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/subject/vo/MesDvSubjectRespVO.java
... ... @@ -3,10 +3,10 @@ package cn.iocoder.yudao.module.mes.controller.admin.dv.subject.vo;
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
6   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
7   -import cn.idev.excel.annotation.ExcelProperty;
8 6 import io.swagger.v3.oas.annotations.media.Schema;
9 7 import lombok.Data;
  8 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  9 +import org.apache.fesod.sheet.annotation.ExcelProperty;
10 10  
11 11 import java.time.LocalDateTime;
12 12  
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/md/autocode/vo/part/MesMdAutoCodePartRespVO.java
... ... @@ -2,10 +2,10 @@ package cn.iocoder.yudao.module.mes.controller.admin.md.autocode.vo.part;
2 2  
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
6   -import cn.idev.excel.annotation.ExcelProperty;
7 5 import io.swagger.v3.oas.annotations.media.Schema;
8 6 import lombok.Data;
  7 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  8 +import org.apache.fesod.sheet.annotation.ExcelProperty;
9 9  
10 10 import java.time.LocalDateTime;
11 11  
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/md/autocode/vo/rule/MesMdAutoCodeRuleRespVO.java
... ... @@ -3,10 +3,10 @@ package cn.iocoder.yudao.module.mes.controller.admin.md.autocode.vo.rule;
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.module.system.enums.DictTypeConstants;
6   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
7   -import cn.idev.excel.annotation.ExcelProperty;
8 6 import io.swagger.v3.oas.annotations.media.Schema;
9 7 import lombok.Data;
  8 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  9 +import org.apache.fesod.sheet.annotation.ExcelProperty;
10 10  
11 11 import java.time.LocalDateTime;
12 12  
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/md/client/vo/MesMdClientImportExcelVO.java
1 1 package cn.iocoder.yudao.module.mes.controller.admin.md.client.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelProperty;
4 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
5 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
6 5 import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
... ... @@ -8,6 +7,7 @@ import lombok.AllArgsConstructor;
8 7 import lombok.Builder;
9 8 import lombok.Data;
10 9 import lombok.NoArgsConstructor;
  10 +import org.apache.fesod.sheet.annotation.ExcelProperty;
11 11  
12 12 /**
13 13 * 客户 Excel 导入 VO
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/md/client/vo/MesMdClientRespVO.java
... ... @@ -3,10 +3,10 @@ package cn.iocoder.yudao.module.mes.controller.admin.md.client.vo;
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
6   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
7   -import cn.idev.excel.annotation.ExcelProperty;
8 6 import io.swagger.v3.oas.annotations.media.Schema;
9 7 import lombok.Data;
  8 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  9 +import org.apache.fesod.sheet.annotation.ExcelProperty;
10 10  
11 11 import java.time.LocalDateTime;
12 12  
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/md/item/vo/MesMdItemImportExcelVO.java
1 1 package cn.iocoder.yudao.module.mes.controller.admin.md.item.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelProperty;
4 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
5 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
6 5 import cn.iocoder.yudao.module.system.enums.DictTypeConstants;
... ... @@ -8,6 +7,7 @@ import lombok.AllArgsConstructor;
8 7 import lombok.Builder;
9 8 import lombok.Data;
10 9 import lombok.NoArgsConstructor;
  10 +import org.apache.fesod.sheet.annotation.ExcelProperty;
11 11  
12 12 import java.math.BigDecimal;
13 13  
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/md/item/vo/MesMdItemRespVO.java
... ... @@ -3,10 +3,10 @@ package cn.iocoder.yudao.module.mes.controller.admin.md.item.vo;
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.module.system.enums.DictTypeConstants;
6   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
7   -import cn.idev.excel.annotation.ExcelProperty;
8 6 import io.swagger.v3.oas.annotations.media.Schema;
9 7 import lombok.Data;
  8 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  9 +import org.apache.fesod.sheet.annotation.ExcelProperty;
10 10  
11 11 import java.math.BigDecimal;
12 12 import java.time.LocalDateTime;
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/md/item/vo/type/MesMdItemTypeRespVO.java
... ... @@ -3,10 +3,10 @@ package cn.iocoder.yudao.module.mes.controller.admin.md.item.vo.type;
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.module.system.enums.DictTypeConstants;
6   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
7   -import cn.idev.excel.annotation.ExcelProperty;
8 6 import io.swagger.v3.oas.annotations.media.Schema;
9 7 import lombok.Data;
  8 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  9 +import org.apache.fesod.sheet.annotation.ExcelProperty;
10 10  
11 11 import java.time.LocalDateTime;
12 12  
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/md/unitmeasure/vo/MesMdUnitMeasureRespVO.java
... ... @@ -3,10 +3,10 @@ package cn.iocoder.yudao.module.mes.controller.admin.md.unitmeasure.vo;
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.module.system.enums.DictTypeConstants;
6   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
7   -import cn.idev.excel.annotation.ExcelProperty;
8 6 import io.swagger.v3.oas.annotations.media.Schema;
9 7 import lombok.Data;
  8 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  9 +import org.apache.fesod.sheet.annotation.ExcelProperty;
10 10  
11 11 import java.math.BigDecimal;
12 12 import java.time.LocalDateTime;
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/md/vendor/vo/MesMdVendorImportExcelVO.java
1 1 package cn.iocoder.yudao.module.mes.controller.admin.md.vendor.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelProperty;
4 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
5 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
6 5 import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
... ... @@ -8,6 +7,7 @@ import lombok.AllArgsConstructor;
8 7 import lombok.Builder;
9 8 import lombok.Data;
10 9 import lombok.NoArgsConstructor;
  10 +import org.apache.fesod.sheet.annotation.ExcelProperty;
11 11  
12 12 /**
13 13 * 供应商 Excel 导入 VO
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/md/vendor/vo/MesMdVendorRespVO.java
... ... @@ -3,10 +3,10 @@ package cn.iocoder.yudao.module.mes.controller.admin.md.vendor.vo;
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
6   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
7   -import cn.idev.excel.annotation.ExcelProperty;
8 6 import io.swagger.v3.oas.annotations.media.Schema;
9 7 import lombok.Data;
  8 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  9 +import org.apache.fesod.sheet.annotation.ExcelProperty;
10 10  
11 11 import java.time.LocalDateTime;
12 12  
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/md/workstation/vo/MesMdWorkstationRespVO.java
1 1 package cn.iocoder.yudao.module.mes.controller.admin.md.workstation.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  6 +import org.apache.fesod.sheet.annotation.ExcelProperty;
7 7  
8 8 import java.time.LocalDateTime;
9 9  
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/md/workstation/vo/workshop/MesMdWorkshopRespVO.java
1 1 package cn.iocoder.yudao.module.mes.controller.admin.md.workstation.vo.workshop;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  6 +import org.apache.fesod.sheet.annotation.ExcelProperty;
7 7  
8 8 import java.math.BigDecimal;
9 9 import java.time.LocalDateTime;
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/andon/vo/record/MesProAndonRecordRespVO.java
... ... @@ -3,10 +3,10 @@ package cn.iocoder.yudao.module.mes.controller.admin.pro.andon.vo.record;
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
6   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
7   -import cn.idev.excel.annotation.ExcelProperty;
8 6 import io.swagger.v3.oas.annotations.media.Schema;
9 7 import lombok.Data;
  8 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  9 +import org.apache.fesod.sheet.annotation.ExcelProperty;
10 10  
11 11 import java.time.LocalDateTime;
12 12  
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/card/vo/MesProCardRespVO.java
1 1 package cn.iocoder.yudao.module.mes.controller.admin.pro.card.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  6 +import org.apache.fesod.sheet.annotation.ExcelProperty;
7 7  
8 8 import java.math.BigDecimal;
9 9 import java.time.LocalDateTime;
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/feedback/vo/MesProFeedbackRespVO.java
... ... @@ -3,10 +3,10 @@ package cn.iocoder.yudao.module.mes.controller.admin.pro.feedback.vo;
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
6   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
7   -import cn.idev.excel.annotation.ExcelProperty;
8 6 import io.swagger.v3.oas.annotations.media.Schema;
9 7 import lombok.Data;
  8 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  9 +import org.apache.fesod.sheet.annotation.ExcelProperty;
10 10  
11 11 import java.math.BigDecimal;
12 12 import java.time.LocalDateTime;
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/route/vo/MesProRouteRespVO.java
1 1 package cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  6 +import org.apache.fesod.sheet.annotation.ExcelProperty;
7 7  
8 8 import java.time.LocalDateTime;
9 9  
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/vo/MesProWorkOrderRespVO.java
... ... @@ -3,10 +3,10 @@ package cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo;
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
6   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
7   -import cn.idev.excel.annotation.ExcelProperty;
8 6 import io.swagger.v3.oas.annotations.media.Schema;
9 7 import lombok.Data;
  8 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  9 +import org.apache.fesod.sheet.annotation.ExcelProperty;
10 10  
11 11 import java.math.BigDecimal;
12 12 import java.time.LocalDateTime;
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/vo/bom/MesProWorkOrderBomRespVO.java
1 1 package cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.bom;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  6 +import org.apache.fesod.sheet.annotation.ExcelProperty;
7 7  
8 8 import java.math.BigDecimal;
9 9 import java.time.LocalDateTime;
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workrecord/vo/MesProWorkRecordLogRespVO.java
... ... @@ -3,11 +3,11 @@ package cn.iocoder.yudao.module.mes.controller.admin.pro.workrecord.vo;
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
6   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
7   -import cn.idev.excel.annotation.ExcelProperty;
8 6 import io.swagger.v3.oas.annotations.media.Schema;
9 7 import lombok.Data;
10 8 import lombok.experimental.Accessors;
  9 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  10 +import org.apache.fesod.sheet.annotation.ExcelProperty;
11 11  
12 12 import java.time.LocalDateTime;
13 13  
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/defect/vo/MesQcDefectRespVO.java
... ... @@ -3,10 +3,10 @@ package cn.iocoder.yudao.module.mes.controller.admin.qc.defect.vo;
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
6   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
7   -import cn.idev.excel.annotation.ExcelProperty;
8 6 import io.swagger.v3.oas.annotations.media.Schema;
9 7 import lombok.Data;
  8 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  9 +import org.apache.fesod.sheet.annotation.ExcelProperty;
10 10  
11 11 import java.time.LocalDateTime;
12 12  
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/indicator/vo/MesQcIndicatorRespVO.java
... ... @@ -3,10 +3,10 @@ package cn.iocoder.yudao.module.mes.controller.admin.qc.indicator.vo;
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
6   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
7   -import cn.idev.excel.annotation.ExcelProperty;
8 6 import io.swagger.v3.oas.annotations.media.Schema;
9 7 import lombok.Data;
  8 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  9 +import org.apache.fesod.sheet.annotation.ExcelProperty;
10 10  
11 11 import java.time.LocalDateTime;
12 12  
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/ipqc/vo/MesQcIpqcRespVO.java
1 1 package cn.iocoder.yudao.module.mes.controller.admin.qc.ipqc.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  6 +import org.apache.fesod.sheet.annotation.ExcelProperty;
7 7  
8 8 import java.math.BigDecimal;
9 9 import java.time.LocalDateTime;
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/iqc/vo/MesQcIqcRespVO.java
1 1 package cn.iocoder.yudao.module.mes.controller.admin.qc.iqc.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  6 +import org.apache.fesod.sheet.annotation.ExcelProperty;
7 7  
8 8 import java.math.BigDecimal;
9 9 import java.time.LocalDateTime;
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/oqc/vo/MesQcOqcRespVO.java
1 1 package cn.iocoder.yudao.module.mes.controller.admin.qc.oqc.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  6 +import org.apache.fesod.sheet.annotation.ExcelProperty;
7 7  
8 8 import java.math.BigDecimal;
9 9 import java.time.LocalDateTime;
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/rqc/vo/MesQcRqcRespVO.java
1 1 package cn.iocoder.yudao.module.mes.controller.admin.qc.rqc.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  6 +import org.apache.fesod.sheet.annotation.ExcelProperty;
7 7  
8 8 import java.math.BigDecimal;
9 9 import java.time.LocalDateTime;
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/template/vo/MesQcTemplateRespVO.java
1 1 package cn.iocoder.yudao.module.mes.controller.admin.qc.template.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  6 +import org.apache.fesod.sheet.annotation.ExcelProperty;
7 7  
8 8 import java.time.LocalDateTime;
9 9 import java.util.List;
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/template/vo/indicator/MesQcTemplateIndicatorRespVO.java
1 1 package cn.iocoder.yudao.module.mes.controller.admin.qc.template.vo.indicator;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  6 +import org.apache.fesod.sheet.annotation.ExcelProperty;
7 7  
8 8 import java.math.BigDecimal;
9 9 import java.time.LocalDateTime;
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/template/vo/item/MesQcTemplateItemRespVO.java
1 1 package cn.iocoder.yudao.module.mes.controller.admin.qc.template.vo.item;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  6 +import org.apache.fesod.sheet.annotation.ExcelProperty;
7 7  
8 8 import java.math.BigDecimal;
9 9 import java.time.LocalDateTime;
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/tm/tool/vo/MesTmToolRespVO.java
... ... @@ -3,10 +3,10 @@ package cn.iocoder.yudao.module.mes.controller.admin.tm.tool.vo;
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
6   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
7   -import cn.idev.excel.annotation.ExcelProperty;
8 6 import io.swagger.v3.oas.annotations.media.Schema;
9 7 import lombok.Data;
  8 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  9 +import org.apache.fesod.sheet.annotation.ExcelProperty;
10 10  
11 11 import java.time.LocalDateTime;
12 12  
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/tm/tool/vo/type/MesTmToolTypeRespVO.java
... ... @@ -3,10 +3,10 @@ package cn.iocoder.yudao.module.mes.controller.admin.tm.tool.vo.type;
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
6   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
7   -import cn.idev.excel.annotation.ExcelProperty;
8 6 import io.swagger.v3.oas.annotations.media.Schema;
9 7 import lombok.Data;
  8 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  9 +import org.apache.fesod.sheet.annotation.ExcelProperty;
10 10  
11 11 import java.time.LocalDateTime;
12 12  
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/arrivalnotice/vo/MesWmArrivalNoticeRespVO.java
1 1 package cn.iocoder.yudao.module.mes.controller.admin.wm.arrivalnotice.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  6 +import org.apache.fesod.sheet.annotation.ExcelProperty;
7 7  
8 8 import java.time.LocalDateTime;
9 9  
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/barcode/vo/MesWmBarcodeRespVO.java
1 1 package cn.iocoder.yudao.module.mes.controller.admin.wm.barcode.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  6 +import org.apache.fesod.sheet.annotation.ExcelProperty;
7 7  
8 8 import java.time.LocalDateTime;
9 9  
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/barcode/vo/config/MesWmBarcodeConfigRespVO.java
1 1 package cn.iocoder.yudao.module.mes.controller.admin.wm.barcode.vo.config;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  6 +import org.apache.fesod.sheet.annotation.ExcelProperty;
7 7  
8 8 import java.time.LocalDateTime;
9 9  
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/itemreceipt/vo/MesWmItemReceiptRespVO.java
1 1 package cn.iocoder.yudao.module.mes.controller.admin.wm.itemreceipt.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  6 +import org.apache.fesod.sheet.annotation.ExcelProperty;
7 7  
8 8 import java.time.LocalDateTime;
9 9  
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/materialstock/vo/MesWmMaterialStockRespVO.java
1 1 package cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  6 +import org.apache.fesod.sheet.annotation.ExcelProperty;
7 7  
8 8 import java.math.BigDecimal;
9 9 import java.time.LocalDateTime;
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/outsourceissue/vo/MesWmOutsourceIssueRespVO.java
1 1 package cn.iocoder.yudao.module.mes.controller.admin.wm.outsourceissue.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  6 +import org.apache.fesod.sheet.annotation.ExcelProperty;
7 7  
8 8 import java.time.LocalDateTime;
9 9  
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/outsourcereceipt/vo/MesWmOutsourceReceiptRespVO.java
1 1 package cn.iocoder.yudao.module.mes.controller.admin.wm.outsourcereceipt.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  6 +import org.apache.fesod.sheet.annotation.ExcelProperty;
7 7  
8 8 import java.time.LocalDateTime;
9 9  
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/packages/vo/MesWmPackageRespVO.java
1 1 package cn.iocoder.yudao.module.mes.controller.admin.wm.packages.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.Data;
7 5 import lombok.experimental.Accessors;
  6 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  7 +import org.apache.fesod.sheet.annotation.ExcelProperty;
8 8  
9 9 import java.math.BigDecimal;
10 10 import java.time.LocalDateTime;
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/productreceipt/vo/MesWmProductReceiptRespVO.java
1 1 package cn.iocoder.yudao.module.mes.controller.admin.wm.productreceipt.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  6 +import org.apache.fesod.sheet.annotation.ExcelProperty;
7 7  
8 8 import java.time.LocalDateTime;
9 9  
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/productsales/vo/MesWmProductSalesRespVO.java
1 1 package cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  6 +import org.apache.fesod.sheet.annotation.ExcelProperty;
7 7  
8 8 import java.time.LocalDateTime;
9 9  
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/salesnotice/vo/MesWmSalesNoticeRespVO.java
1 1 package cn.iocoder.yudao.module.mes.controller.admin.wm.salesnotice.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  6 +import org.apache.fesod.sheet.annotation.ExcelProperty;
7 7  
8 8 import java.time.LocalDateTime;
9 9  
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/sn/vo/MesWmSnGroupRespVO.java
1 1 package cn.iocoder.yudao.module.mes.controller.admin.wm.sn.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelProperty;
4 3 import io.swagger.v3.oas.annotations.media.Schema;
5 4 import lombok.Data;
6 5 import lombok.experimental.Accessors;
  6 +import org.apache.fesod.sheet.annotation.ExcelProperty;
7 7  
8 8 import java.time.LocalDateTime;
9 9  
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/sn/vo/MesWmSnRespVO.java
1 1 package cn.iocoder.yudao.module.mes.controller.admin.wm.sn.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelProperty;
4 3 import io.swagger.v3.oas.annotations.media.Schema;
5 4 import lombok.Data;
6 5 import lombok.experimental.Accessors;
  6 +import org.apache.fesod.sheet.annotation.ExcelProperty;
7 7  
8 8 import java.time.LocalDateTime;
9 9  
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/stocktaking/plan/vo/MesWmStockTakingPlanRespVO.java
1 1 package cn.iocoder.yudao.module.mes.controller.admin.wm.stocktaking.plan.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  6 +import org.apache.fesod.sheet.annotation.ExcelProperty;
7 7  
8 8 import java.time.LocalDateTime;
9 9  
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/stocktaking/task/vo/MesWmStockTakingTaskRespVO.java
1 1 package cn.iocoder.yudao.module.mes.controller.admin.wm.stocktaking.task.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  6 +import org.apache.fesod.sheet.annotation.ExcelProperty;
7 7  
8 8 import java.time.LocalDateTime;
9 9  
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/stocktaking/task/vo/result/MesWmStockTakingTaskResultRespVO.java
1 1 package cn.iocoder.yudao.module.mes.controller.admin.wm.stocktaking.task.vo.result;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  6 +import org.apache.fesod.sheet.annotation.ExcelProperty;
7 7  
8 8 import java.math.BigDecimal;
9 9 import java.time.LocalDateTime;
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/warehouse/vo/MesWmWarehouseRespVO.java
1 1 package cn.iocoder.yudao.module.mes.controller.admin.wm.warehouse.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  6 +import org.apache.fesod.sheet.annotation.ExcelProperty;
7 7  
8 8 import java.math.BigDecimal;
9 9 import java.time.LocalDateTime;
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/warehouse/vo/area/MesWmWarehouseAreaRespVO.java
1 1 package cn.iocoder.yudao.module.mes.controller.admin.wm.warehouse.vo.area;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  6 +import org.apache.fesod.sheet.annotation.ExcelProperty;
7 7  
8 8 import java.math.BigDecimal;
9 9 import java.time.LocalDateTime;
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/warehouse/vo/location/MesWmWarehouseLocationRespVO.java
1 1 package cn.iocoder.yudao.module.mes.controller.admin.wm.warehouse.vo.location;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  6 +import org.apache.fesod.sheet.annotation.ExcelProperty;
7 7  
8 8 import java.math.BigDecimal;
9 9 import java.time.LocalDateTime;
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/cal/plan/MesCalPlanMapper.java
... ... @@ -8,6 +8,9 @@ import cn.iocoder.yudao.module.mes.dal.dataobject.cal.plan.MesCalPlanDO;
8 8 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
9 9 import org.apache.ibatis.annotations.Mapper;
10 10  
  11 +import java.time.LocalDate;
  12 +import java.util.List;
  13 +
11 14 /**
12 15 * MES 排班计划 Mapper
13 16 *
... ... @@ -32,6 +35,15 @@ public interface MesCalPlanMapper extends BaseMapperX&lt;MesCalPlanDO&gt; {
32 35 return selectOne(MesCalPlanDO::getCode, code);
33 36 }
34 37  
  38 + default List<MesCalPlanDO> selectListByStatusAndDay(Integer status, LocalDate day) {
  39 + return selectList(new LambdaQueryWrapperX<MesCalPlanDO>()
  40 + .eq(MesCalPlanDO::getStatus, status)
  41 + // TODO @AI:dayend 获取下?应该有方法,这样可以 le,更统一点;
  42 + .lt(MesCalPlanDO::getStartDate, day.plusDays(1).atStartOfDay())
  43 + .ge(MesCalPlanDO::getEndDate, day.atStartOfDay())
  44 + .orderByAsc(MesCalPlanDO::getId));
  45 + }
  46 +
35 47 default void updateWithShiftFieldCleanup(MesCalPlanDO plan, boolean clearShiftMethod, boolean clearShiftCount) {
36 48 update(plan, Wrappers.<MesCalPlanDO>lambdaUpdate()
37 49 .eq(MesCalPlanDO::getId, plan.getId())
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
... ... @@ -140,6 +140,9 @@ public interface ErrorCodeConstants {
140 140 ErrorCode CAL_TEAM_SHIFT_GENERATE_SHIFT_NOT_ENOUGH = new ErrorCode(1_040_201_202, "班次数量不满足轮班方式要求");
141 141 ErrorCode CAL_TEAM_SHIFT_GENERATE_SHIFT_COUNT_INVALID = new ErrorCode(1_040_201_203, "按天倒班必须填写合法的倒班天数");
142 142 ErrorCode CAL_TEAM_SHIFT_GENERATE_SHIFT_METHOD_INVALID = new ErrorCode(1_040_201_204, "不支持的倒班方式:{}");
  143 + ErrorCode CAL_TEAM_SHIFT_GENERATE_SHIFT_TIME_INVALID = new ErrorCode(1_040_201_205, "班次【{}】的开始或结束时间格式不正确,应为 HH:mm,结束时间可为 24:00");
  144 + ErrorCode CAL_TEAM_SHIFT_GENERATE_REST_INVALID = new ErrorCode(1_040_201_206,
  145 + "班组【{}】{} {}与 {} {}的上班时间重叠或首尾相接,没有休息间隔,请调整班次时间或倒班方式");
143 146  
144 147 // ========== MES 日历排班-排班计划(1-040-202-000) ==========
145 148 ErrorCode CAL_PLAN_NOT_EXISTS = new ErrorCode(1_040_202_000, "排班计划不存在");
... ... @@ -152,6 +155,8 @@ public interface ErrorCodeConstants {
152 155  
153 156 // ========== MES 日历排班-假期设置(1-040-203-000) ==========
154 157 ErrorCode CAL_HOLIDAY_NOT_EXISTS = new ErrorCode(1_040_203_000, "假期设置不存在");
  158 + ErrorCode CAL_HOLIDAY_PLAN_CONFIRMED = new ErrorCode(1_040_203_001,
  159 + "{} 在已确认的排班计划【{}】范围内,修改是否节假日会与已生成的排班不一致,不允许修改");
155 160  
156 161 // ========== MES 设备管理-设备类型(1-040-300-000) ==========
157 162 ErrorCode DV_MACHINERY_TYPE_NOT_EXISTS = new ErrorCode(1_040_300_000, "设备类型不存在");
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/cal/holiday/MesCalHolidayServiceImpl.java
1 1 package cn.iocoder.yudao.module.mes.service.cal.holiday;
2 2  
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.util.StrUtil;
3 5 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
4 6 import cn.iocoder.yudao.module.mes.controller.admin.cal.holiday.vo.MesCalHolidaySaveReqVO;
5 7 import cn.iocoder.yudao.module.mes.dal.dataobject.cal.holiday.MesCalHolidayDO;
  8 +import cn.iocoder.yudao.module.mes.dal.dataobject.cal.plan.MesCalPlanDO;
6 9 import cn.iocoder.yudao.module.mes.dal.mysql.cal.holiday.MesCalHolidayMapper;
  10 +import cn.iocoder.yudao.module.mes.enums.cal.MesCalHolidayTypeEnum;
  11 +import cn.iocoder.yudao.module.mes.enums.cal.MesCalPlanStatusEnum;
  12 +import cn.iocoder.yudao.module.mes.service.cal.plan.MesCalPlanService;
7 13 import jakarta.annotation.Resource;
  14 +import org.springframework.context.annotation.Lazy;
8 15 import org.springframework.stereotype.Service;
9 16 import org.springframework.validation.annotation.Validated;
10 17  
11 18 import java.time.LocalDateTime;
12 19 import java.util.List;
13 20  
  21 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  22 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
  23 +import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.CAL_HOLIDAY_PLAN_CONFIRMED;
  24 +
14 25 /**
15 26 * MES 假期设置 Service 实现类
16 27 *
... ... @@ -23,10 +34,17 @@ public class MesCalHolidayServiceImpl implements MesCalHolidayService {
23 34 @Resource
24 35 private MesCalHolidayMapper holidayMapper;
25 36  
  37 + @Resource
  38 + @Lazy
  39 + private MesCalPlanService planService;
  40 +
26 41 @Override
27 42 public Long saveHoliday(MesCalHolidaySaveReqVO saveReqVO) {
28   - // Upsert 逻辑:如果该日期已有记录,则更新
  43 + // 1. 校验是否节假日的变化,不影响已确认的排班计划
29 44 MesCalHolidayDO existing = holidayMapper.selectByDay(saveReqVO.getDay());
  45 + validateHolidayChangeForConfirmedPlan(existing, saveReqVO);
  46 +
  47 + // 2.1 Upsert 逻辑:如果该日期已有记录,则更新
30 48 if (existing != null) {
31 49 MesCalHolidayDO updateObj = BeanUtils.toBean(saveReqVO, MesCalHolidayDO.class);
32 50 updateObj.setId(existing.getId());
... ... @@ -34,12 +52,33 @@ public class MesCalHolidayServiceImpl implements MesCalHolidayService {
34 52 return existing.getId();
35 53 }
36 54  
37   - // 插入新记录
  55 + // 2.2 插入新记录
38 56 MesCalHolidayDO holiday = BeanUtils.toBean(saveReqVO, MesCalHolidayDO.class);
39 57 holidayMapper.insert(holiday);
40 58 return holiday.getId();
41 59 }
42 60  
  61 + /**
  62 + * 校验是否节假日的变化,不影响已确认的排班计划
  63 + *
  64 + * 排班计划确认时按“节假日停班”生成排班记录(按天倒班时还会顺延后续倒班),确认后不会重新生成。
  65 + * 因此已确认计划范围内的日期,不允许在“节假日”与“非节假日”之间切换,避免与已生成的排班不一致 https://t.zsxq.com/XdcRW
  66 + */
  67 + private void validateHolidayChangeForConfirmedPlan(MesCalHolidayDO existing, MesCalHolidaySaveReqVO saveReqVO) {
  68 + boolean oldHoliday = existing != null && MesCalHolidayTypeEnum.HOLIDAY.getType().equals(existing.getType());
  69 + boolean newHoliday = MesCalHolidayTypeEnum.HOLIDAY.getType().equals(saveReqVO.getType());
  70 + if (oldHoliday == newHoliday) {
  71 + return;
  72 + }
  73 + List<MesCalPlanDO> plans = planService.getPlanListByStatusAndDay(
  74 + MesCalPlanStatusEnum.CONFIRMED.getStatus(), saveReqVO.getDay().toLocalDate());
  75 + if (CollUtil.isEmpty(plans)) {
  76 + return;
  77 + }
  78 + throw exception(CAL_HOLIDAY_PLAN_CONFIRMED, saveReqVO.getDay().toLocalDate(),
  79 + StrUtil.join("、", convertList(plans, MesCalPlanDO::getName)));
  80 + }
  81 +
43 82 @Override
44 83 public List<MesCalHolidayDO> getHolidayList(LocalDateTime startDay, LocalDateTime endDay) {
45 84 return holidayMapper.selectList(startDay, endDay);
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/cal/plan/MesCalPlanService.java
... ... @@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.mes.controller.admin.cal.plan.vo.MesCalPlanSaveRe
6 6 import cn.iocoder.yudao.module.mes.dal.dataobject.cal.plan.MesCalPlanDO;
7 7 import jakarta.validation.Valid;
8 8  
  9 +import java.time.LocalDate;
9 10 import java.util.Collection;
10 11 import java.util.List;
11 12 import java.util.Map;
... ... @@ -90,4 +91,13 @@ public interface MesCalPlanService {
90 91 */
91 92 PageResult<MesCalPlanDO> getPlanPage(MesCalPlanPageReqVO pageReqVO);
92 93  
  94 + /**
  95 + * 获得指定状态、且起止日期包含指定日期的排班计划列表
  96 + *
  97 + * @param status 状态
  98 + * @param day 日期
  99 + * @return 排班计划列表
  100 + */
  101 + List<MesCalPlanDO> getPlanListByStatusAndDay(Integer status, LocalDate day);
  102 +
93 103 }
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/cal/plan/MesCalPlanServiceImpl.java
... ... @@ -18,6 +18,7 @@ import org.springframework.stereotype.Service;
18 18 import org.springframework.transaction.annotation.Transactional;
19 19 import org.springframework.validation.annotation.Validated;
20 20  
  21 +import java.time.LocalDate;
21 22 import java.util.Collection;
22 23 import java.util.Collections;
23 24 import java.util.List;
... ... @@ -143,6 +144,11 @@ public class MesCalPlanServiceImpl implements MesCalPlanService {
143 144 }
144 145  
145 146 @Override
  147 + public List<MesCalPlanDO> getPlanListByStatusAndDay(Integer status, LocalDate day) {
  148 + return planMapper.selectListByStatusAndDay(status, day);
  149 + }
  150 +
  151 + @Override
146 152 public MesCalPlanDO validatePlanPrepare(Long planId) {
147 153 MesCalPlanDO plan = validatePlanExists(planId);
148 154 if (ObjUtil.notEqual(MesCalPlanStatusEnum.PREPARE.getStatus(), plan.getStatus())) {
... ... @@ -171,11 +177,13 @@ public class MesCalPlanServiceImpl implements MesCalPlanService {
171 177  
172 178 /**
173 179 * 确认排班计划时,校验班组数量是否与轮班方式匹配
  180 + *
  181 + * 排班只会使用轮班方式所需数量的班组,多出的班组不会参与轮休或替班,因此要求数量一致,避免被静默忽略 https://t.zsxq.com/XdcRW
174 182 */
175 183 private void validateTeamCountForConfirm(Long planId, Integer shiftType) {
176 184 Long teamCount = planTeamService.getPlanTeamCountByPlanId(planId);
177 185 MesCalShiftTypeEnum shiftTypeEnum = MesCalShiftTypeEnum.valueOf(shiftType);
178   - if (teamCount < shiftTypeEnum.getRequiredTeamCount()) {
  186 + if (teamCount == null || teamCount != shiftTypeEnum.getRequiredTeamCount()) {
179 187 throw exception(CAL_PLAN_TEAM_COUNT_NOT_MATCH);
180 188 }
181 189 }
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/cal/team/MesCalTeamShiftServiceImpl.java
1 1 package cn.iocoder.yudao.module.mes.service.cal.team;
2 2  
  3 +import cn.hutool.core.collection.CollUtil;
3 4 import cn.iocoder.yudao.module.mes.controller.admin.cal.team.vo.shift.MesCalTeamShiftListReqVO;
  5 +import cn.iocoder.yudao.module.mes.dal.dataobject.cal.holiday.MesCalHolidayDO;
4 6 import cn.iocoder.yudao.module.mes.dal.dataobject.cal.plan.MesCalPlanDO;
5 7 import cn.iocoder.yudao.module.mes.dal.dataobject.cal.plan.MesCalPlanShiftDO;
6 8 import cn.iocoder.yudao.module.mes.dal.dataobject.cal.plan.MesCalPlanTeamDO;
  9 +import cn.iocoder.yudao.module.mes.dal.dataobject.cal.team.MesCalTeamDO;
7 10 import cn.iocoder.yudao.module.mes.dal.dataobject.cal.team.MesCalTeamShiftDO;
8 11 import cn.iocoder.yudao.module.mes.dal.mysql.cal.team.MesCalTeamShiftMapper;
  12 +import cn.iocoder.yudao.module.mes.enums.cal.MesCalHolidayTypeEnum;
9 13 import cn.iocoder.yudao.module.mes.enums.cal.MesCalShiftMethodEnum;
10 14 import cn.iocoder.yudao.module.mes.enums.cal.MesCalShiftTypeEnum;
  15 +import cn.iocoder.yudao.module.mes.service.cal.holiday.MesCalHolidayService;
11 16 import cn.iocoder.yudao.module.mes.service.cal.plan.MesCalPlanService;
12 17 import cn.iocoder.yudao.module.mes.service.cal.plan.MesCalPlanShiftService;
13 18 import cn.iocoder.yudao.module.mes.service.cal.plan.MesCalPlanTeamService;
... ... @@ -20,14 +25,25 @@ import org.springframework.validation.annotation.Validated;
20 25  
21 26 import java.time.LocalDate;
22 27 import java.time.LocalDateTime;
  28 +import java.time.LocalTime;
23 29 import java.time.temporal.ChronoUnit;
24 30 import java.util.ArrayList;
  31 +import java.util.Comparator;
  32 +import java.util.HashMap;
25 33 import java.util.List;
  34 +import java.util.Map;
  35 +import java.util.Set;
  36 +import java.util.regex.Matcher;
  37 +import java.util.regex.Pattern;
26 38  
27 39 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  40 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
  41 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
  42 +import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.CAL_TEAM_SHIFT_GENERATE_REST_INVALID;
28 43 import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.CAL_TEAM_SHIFT_GENERATE_SHIFT_COUNT_INVALID;
29 44 import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.CAL_TEAM_SHIFT_GENERATE_SHIFT_METHOD_INVALID;
30 45 import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.CAL_TEAM_SHIFT_GENERATE_SHIFT_NOT_ENOUGH;
  46 +import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.CAL_TEAM_SHIFT_GENERATE_SHIFT_TIME_INVALID;
31 47 import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.CAL_TEAM_SHIFT_GENERATE_TEAM_NOT_ENOUGH;
32 48  
33 49 /**
... ... @@ -39,6 +55,11 @@ import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.CAL_TEAM_SHIF
39 55 @Validated
40 56 public class MesCalTeamShiftServiceImpl implements MesCalTeamShiftService {
41 57  
  58 + /**
  59 + * 班次时间格式:H:mm 或 HH:mm
  60 + */
  61 + private static final Pattern SHIFT_TIME_PATTERN = Pattern.compile("^(\\d{1,2}):(\\d{2})$");
  62 +
42 63 @Resource
43 64 private MesCalTeamShiftMapper teamShiftMapper;
44 65  
... ... @@ -51,6 +72,12 @@ public class MesCalTeamShiftServiceImpl implements MesCalTeamShiftService {
51 72 @Resource
52 73 @Lazy
53 74 private MesCalPlanTeamService planTeamService;
  75 + @Resource
  76 + @Lazy
  77 + private MesCalHolidayService holidayService;
  78 + @Resource
  79 + @Lazy
  80 + private MesCalTeamService teamService;
54 81  
55 82 @Transactional(rollbackFor = Exception.class)
56 83 @Override
... ... @@ -71,25 +98,118 @@ public class MesCalTeamShiftServiceImpl implements MesCalTeamShiftService {
71 98 throw exception(CAL_TEAM_SHIFT_GENERATE_SHIFT_NOT_ENOUGH);
72 99 }
73 100  
74   - // 2. 计算日期跨度,遍历每天生成排班记录,最后统一写入数据库
  101 + // 2.1 计算日期跨度,查询范围内的节假日
75 102 LocalDate startDate = plan.getStartDate().toLocalDate();
76 103 LocalDate endDate = plan.getEndDate().toLocalDate();
77 104 long days = ChronoUnit.DAYS.between(startDate, endDate) + 1;
  105 + Set<LocalDate> holidayDates = getHolidayDates(startDate, endDate);
  106 + // 2.2 遍历每天生成排班记录,最后统一写入数据库
  107 + // 假期停班:节假日不生成排班,按天倒班只累计有效排班日(假期不消耗倒班天数);按周/月/季度仍按日历周期换班 https://t.zsxq.com/XdcRW
  108 + boolean dayMethod = MesCalShiftMethodEnum.DAY.getMethod().equals(plan.getShiftMethod());
78 109 List<MesCalTeamShiftDO> allRecords = new ArrayList<>();
79 110 int shiftIndex = 0;
  111 + int workDayOffset = 0;
80 112 for (int i = 0; i < days; i++) {
81 113 LocalDate currentDate = startDate.plusDays(i);
82 114 LocalDateTime currentDay = currentDate.atStartOfDay();
83   - // 2.2.1 根据倒班方式计算 shiftIndex 单白班不倒班,跳过
84   - if (!MesCalShiftTypeEnum.SINGLE.getType().equals(plan.getShiftType())) {
85   - shiftIndex = calculateShiftIndex(shiftIndex, i, currentDate, startDate, plan);
  115 + boolean holiday = holidayDates.contains(currentDate);
  116 + // 2.2.1 根据倒班方式计算 shiftIndex 单白班不倒班,跳过;周期起点恰逢假期时,也照常换班
  117 + if (!MesCalShiftTypeEnum.SINGLE.getType().equals(plan.getShiftType()) && !(dayMethod && holiday)) {
  118 + shiftIndex = calculateShiftIndex(shiftIndex, dayMethod ? workDayOffset : i, currentDate, startDate, plan);
  119 + }
  120 + if (holiday) {
  121 + continue;
86 122 }
  123 + workDayOffset++;
87 124 // 2.2.2 根据轮班方式生成排班记录,收集到列表
88 125 buildRecordsForDay(allRecords, planId, currentDay, shiftIndex, plan.getShiftType(), teams, shifts);
89 126 }
  127 + // 2.3 校验同一班组相邻两次排班之间留有休息间隔 https://t.zsxq.com/XdcRW
  128 + validateTeamShiftRest(allRecords, shifts);
90 129  
91 130 // 3. 批量插入排班记录
92   - teamShiftMapper.insertBatch(allRecords);
  131 + if (CollUtil.isNotEmpty(allRecords)) {
  132 + teamShiftMapper.insertBatch(allRecords);
  133 + }
  134 + }
  135 +
  136 + /**
  137 + * 获得日期范围内的节假日(日期类型为节假日的,排班时停班)
  138 + */
  139 + private Set<LocalDate> getHolidayDates(LocalDate startDate, LocalDate endDate) {
  140 + List<MesCalHolidayDO> holidays = holidayService.getHolidayList(
  141 + startDate.atStartOfDay(), endDate.atTime(23, 59, 59));
  142 + return convertSet(holidays, holiday -> holiday.getDay().toLocalDate(),
  143 + holiday -> MesCalHolidayTypeEnum.HOLIDAY.getType().equals(holiday.getType()));
  144 + }
  145 +
  146 + /**
  147 + * 校验同一班组相邻两次排班之间留有休息间隔
  148 + *
  149 + * 按全部排班的上班开始时刻排序,前一班的结束时刻不早于同一班组下一班的开始时刻,
  150 + * 即上班时间重叠或首尾相接(例如夜班次日 08:00 下班后立即接 08:00 的白班),不允许生成
  151 + */
  152 + private void validateTeamShiftRest(List<MesCalTeamShiftDO> records, List<MesCalPlanShiftDO> shifts) {
  153 + // 1. 解析班次的开始、结束时间
  154 + Map<Long, MesCalPlanShiftDO> shiftMap = convertMap(shifts, MesCalPlanShiftDO::getId);
  155 + Map<Long, LocalTime[]> shiftTimeMap = convertMap(shifts, MesCalPlanShiftDO::getId,
  156 + shift -> new LocalTime[]{parseShiftTime(shift, shift.getStartTime(), false),
  157 + parseShiftTime(shift, shift.getEndTime(), true)});
  158 +
  159 + // 2. 按上班开始时刻排序,逐条与同一班组的上一次排班比较
  160 + List<MesCalTeamShiftDO> sortedRecords = new ArrayList<>(records);
  161 + sortedRecords.sort(Comparator.comparing(record -> buildShiftTimeRange(record, shiftTimeMap)[0]));
  162 + Map<Long, MesCalTeamShiftDO> lastRecordMap = new HashMap<>();
  163 + for (MesCalTeamShiftDO record : sortedRecords) {
  164 + MesCalTeamShiftDO lastRecord = lastRecordMap.put(record.getTeamId(), record);
  165 + if (lastRecord == null || buildShiftTimeRange(lastRecord, shiftTimeMap)[1]
  166 + .isBefore(buildShiftTimeRange(record, shiftTimeMap)[0])) {
  167 + continue;
  168 + }
  169 + MesCalTeamDO team = teamService.getTeam(record.getTeamId());
  170 + throw exception(CAL_TEAM_SHIFT_GENERATE_REST_INVALID, team != null ? team.getName() : record.getTeamId(),
  171 + lastRecord.getDay().toLocalDate(), shiftMap.get(lastRecord.getShiftId()).getName(),
  172 + record.getDay().toLocalDate(), shiftMap.get(record.getShiftId()).getName());
  173 + }
  174 + }
  175 +
  176 + /**
  177 + * 计算排班记录的实际上班时间段
  178 + *
  179 + * 开始时间在排班日期当天;结束时间不晚于开始时间(例如 20:00~08:00、16:00~00:00)时,跨午夜到次日结束
  180 + *
  181 + * @return [开始时刻, 结束时刻]
  182 + */
  183 + private static LocalDateTime[] buildShiftTimeRange(MesCalTeamShiftDO record, Map<Long, LocalTime[]> shiftTimeMap) {
  184 + LocalTime[] shiftTime = shiftTimeMap.get(record.getShiftId());
  185 + LocalDate day = record.getDay().toLocalDate();
  186 + LocalDateTime start = day.atTime(shiftTime[0]);
  187 + LocalDateTime end = shiftTime[1].isAfter(shiftTime[0]) ? day.atTime(shiftTime[1])
  188 + : day.plusDays(1).atTime(shiftTime[1]);
  189 + return new LocalDateTime[]{start, end};
  190 + }
  191 +
  192 + /**
  193 + * 解析班次时间,兼容 H:mm(例如 8:00)
  194 + *
  195 + * 结束时间允许 24:00,表示当天 24:00,按 00:00 处理后由“结束时间不晚于开始时间时跨午夜到次日”规则得到次日 00:00 https://t.zsxq.com/XdcRW
  196 + *
  197 + * @param endTime 是否为结束时间
  198 + */
  199 + private static LocalTime parseShiftTime(MesCalPlanShiftDO shift, String time, boolean endTime) {
  200 + Matcher matcher = time != null ? SHIFT_TIME_PATTERN.matcher(time.trim()) : null;
  201 + if (matcher == null || !matcher.matches()) {
  202 + throw exception(CAL_TEAM_SHIFT_GENERATE_SHIFT_TIME_INVALID, shift.getName());
  203 + }
  204 + int hour = Integer.parseInt(matcher.group(1));
  205 + int minute = Integer.parseInt(matcher.group(2));
  206 + if (endTime && hour == 24 && minute == 0) {
  207 + return LocalTime.MIDNIGHT;
  208 + }
  209 + if (hour > 23 || minute > 59) {
  210 + throw exception(CAL_TEAM_SHIFT_GENERATE_SHIFT_TIME_INVALID, shift.getName());
  211 + }
  212 + return LocalTime.of(hour, minute);
93 213 }
94 214  
95 215 /**
... ... @@ -180,7 +300,7 @@ public class MesCalTeamShiftServiceImpl implements MesCalTeamShiftService {
180 300 .sort(1).build());
181 301 }
182 302 } else if (MesCalShiftTypeEnum.THREE.getType().equals(shiftType)) {
183   - // 三班倒
  303 + // 三班倒:按 白班 → 夜班 → 中班 轮转。原 白班 → 中班 → 夜班 轮转时,换班当天中班 24:00 下班的班组会立即接 00:00 的夜班 https://t.zsxq.com/XdcRW
184 304 if (shiftIndex % 3 == 0) {
185 305 // A 组上白班、B 组上中班、C 组上夜班
186 306 allRecords.add(MesCalTeamShiftDO.builder()
... ... @@ -199,39 +319,39 @@ public class MesCalTeamShiftServiceImpl implements MesCalTeamShiftService {
199 319 .shiftId(shifts.get(2).getId())
200 320 .sort(3).build());
201 321 } else if (shiftIndex % 3 == 1) {
202   - // A 组上中班、B 组上夜班、C 组上白班
  322 + // A 组上夜班、B 组上白班、C 组上中班
203 323 allRecords.add(MesCalTeamShiftDO.builder()
204 324 .planId(planId).day(currentDay)
205 325 .teamId(teams.get(0).getTeamId())
206   - .shiftId(shifts.get(1).getId())
207   - .sort(2).build());
208   - allRecords.add(MesCalTeamShiftDO.builder()
209   - .planId(planId).day(currentDay)
210   - .teamId(teams.get(1).getTeamId())
211 326 .shiftId(shifts.get(2).getId())
212 327 .sort(3).build());
213 328 allRecords.add(MesCalTeamShiftDO.builder()
214 329 .planId(planId).day(currentDay)
215   - .teamId(teams.get(2).getTeamId())
  330 + .teamId(teams.get(1).getTeamId())
216 331 .shiftId(shifts.get(0).getId())
217 332 .sort(1).build());
  333 + allRecords.add(MesCalTeamShiftDO.builder()
  334 + .planId(planId).day(currentDay)
  335 + .teamId(teams.get(2).getTeamId())
  336 + .shiftId(shifts.get(1).getId())
  337 + .sort(2).build());
218 338 } else {
219   - // A 组上夜班、B 组上白班、C 组上中班
  339 + // A 组上中班、B 组上夜班、C 组上白班
220 340 allRecords.add(MesCalTeamShiftDO.builder()
221 341 .planId(planId).day(currentDay)
222 342 .teamId(teams.get(0).getTeamId())
223   - .shiftId(shifts.get(2).getId())
224   - .sort(3).build());
  343 + .shiftId(shifts.get(1).getId())
  344 + .sort(2).build());
225 345 allRecords.add(MesCalTeamShiftDO.builder()
226 346 .planId(planId).day(currentDay)
227 347 .teamId(teams.get(1).getTeamId())
228   - .shiftId(shifts.get(0).getId())
229   - .sort(1).build());
  348 + .shiftId(shifts.get(2).getId())
  349 + .sort(3).build());
230 350 allRecords.add(MesCalTeamShiftDO.builder()
231 351 .planId(planId).day(currentDay)
232 352 .teamId(teams.get(2).getTeamId())
233   - .shiftId(shifts.get(1).getId())
234   - .sort(2).build());
  353 + .shiftId(shifts.get(0).getId())
  354 + .sort(1).build());
235 355 }
236 356 }
237 357 }
... ...
yudao-module-mes/src/main/resources/mapper/home/MesHomeStatisticsMapper.xml
... ... @@ -3,7 +3,7 @@
3 3 <mapper namespace="cn.iocoder.yudao.module.mes.dal.mysql.home.MesHomeStatisticsMapper">
4 4  
5 5 <!-- 按状态统计工单数量 -->
6   - <select id="selectWorkOrderCountGroupByStatus" resultType="java.util.Map">
  6 + <select id="selectWorkOrderCountGroupByStatus" resultType="cn.iocoder.yudao.framework.mybatis.core.util.CaseInsensitiveResultMap">
7 7 SELECT status, COUNT(*) AS count
8 8 FROM mes_pro_work_order
9 9 WHERE deleted = 0
... ... @@ -11,7 +11,7 @@
11 11 </select>
12 12  
13 13 <!-- 统计报工产量合计 -->
14   - <select id="selectFeedbackSummary" resultType="java.util.Map">
  14 + <select id="selectFeedbackSummary" resultType="cn.iocoder.yudao.framework.mybatis.core.util.CaseInsensitiveResultMap">
15 15 SELECT IFNULL(SUM(feedback_quantity), 0) AS feedbackQuantity,
16 16 IFNULL(SUM(qualified_quantity), 0) AS qualifiedQuantity,
17 17 IFNULL(SUM(unqualified_quantity), 0) AS unqualifiedQuantity
... ... @@ -25,7 +25,7 @@
25 25 </select>
26 26  
27 27 <!-- 按设备状态统计设备台数 -->
28   - <select id="selectMachineryCountGroupByStatus" resultType="java.util.Map">
  28 + <select id="selectMachineryCountGroupByStatus" resultType="cn.iocoder.yudao.framework.mybatis.core.util.CaseInsensitiveResultMap">
29 29 SELECT status, COUNT(*) AS count
30 30 FROM mes_dv_machinery
31 31 WHERE deleted = 0
... ... @@ -51,7 +51,7 @@
51 51 </select>
52 52  
53 53 <!-- 按天聚合报工产量 -->
54   - <select id="selectDailyFeedbackTrend" resultType="java.util.Map">
  54 + <select id="selectDailyFeedbackTrend" resultType="cn.iocoder.yudao.framework.mybatis.core.util.CaseInsensitiveResultMap">
55 55 SELECT DATE_FORMAT(feedback_time, '%Y-%m-%d') AS date,
56 56 IFNULL(SUM(feedback_quantity), 0) AS quantity,
57 57 IFNULL(SUM(qualified_quantity), 0) AS qualifiedQuantity,
... ...
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/cal/holiday/MesCalHolidayServiceImplTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.mes.service.cal.holiday;
  2 +
  3 +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
  4 +import cn.iocoder.yudao.module.mes.controller.admin.cal.holiday.vo.MesCalHolidaySaveReqVO;
  5 +import cn.iocoder.yudao.module.mes.dal.dataobject.cal.holiday.MesCalHolidayDO;
  6 +import cn.iocoder.yudao.module.mes.dal.dataobject.cal.plan.MesCalPlanDO;
  7 +import cn.iocoder.yudao.module.mes.dal.mysql.cal.holiday.MesCalHolidayMapper;
  8 +import cn.iocoder.yudao.module.mes.enums.cal.MesCalHolidayTypeEnum;
  9 +import cn.iocoder.yudao.module.mes.enums.cal.MesCalPlanStatusEnum;
  10 +import cn.iocoder.yudao.module.mes.service.cal.plan.MesCalPlanService;
  11 +import jakarta.annotation.Resource;
  12 +import org.junit.jupiter.api.Test;
  13 +import org.springframework.context.annotation.Import;
  14 +import org.springframework.test.context.bean.override.mockito.MockitoBean;
  15 +
  16 +import java.time.LocalDateTime;
  17 +
  18 +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
  19 +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
  20 +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
  21 +import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.CAL_HOLIDAY_PLAN_CONFIRMED;
  22 +import static java.util.Collections.emptyList;
  23 +import static java.util.Collections.singletonList;
  24 +import static org.junit.jupiter.api.Assertions.*;
  25 +import static org.mockito.Mockito.*;
  26 +
  27 +/**
  28 + * {@link MesCalHolidayServiceImpl} 的单元测试
  29 + *
  30 + * @author 芋道源码
  31 + */
  32 +@Import(MesCalHolidayServiceImpl.class)
  33 +public class MesCalHolidayServiceImplTest extends BaseDbUnitTest {
  34 +
  35 + @Resource
  36 + private MesCalHolidayServiceImpl holidayService;
  37 +
  38 + @Resource
  39 + private MesCalHolidayMapper holidayMapper;
  40 +
  41 + @MockitoBean
  42 + private MesCalPlanService planService;
  43 +
  44 + @Test
  45 + public void testSaveHoliday_addHolidayInConfirmedPlan() {
  46 + // 准备参数
  47 + MesCalHolidaySaveReqVO reqVO = randomPojo(MesCalHolidaySaveReqVO.class, o -> {
  48 + o.setId(null); // 避免 id 被赋值
  49 + o.setDay(LocalDateTime.of(2026, 10, 20, 0, 0));
  50 + o.setType(MesCalHolidayTypeEnum.HOLIDAY.getType());
  51 + });
  52 + // mock planService 的方法:该日期在已确认的排班计划范围内
  53 + MesCalPlanDO plan = randomPojo(MesCalPlanDO.class, o -> o.setStatus(MesCalPlanStatusEnum.CONFIRMED.getStatus()));
  54 + when(planService.getPlanListByStatusAndDay(eq(MesCalPlanStatusEnum.CONFIRMED.getStatus()),
  55 + eq(reqVO.getDay().toLocalDate()))).thenReturn(singletonList(plan));
  56 +
  57 + // 调用,并断言异常
  58 + assertServiceException(() -> holidayService.saveHoliday(reqVO), CAL_HOLIDAY_PLAN_CONFIRMED,
  59 + reqVO.getDay().toLocalDate(), plan.getName());
  60 + // 断言:未写入假期设置
  61 + assertNull(holidayMapper.selectByDay(reqVO.getDay()));
  62 + }
  63 +
  64 + @Test
  65 + public void testSaveHoliday_cancelHolidayInConfirmedPlan() {
  66 + // mock 数据:已有节假日
  67 + MesCalHolidayDO dbHoliday = randomPojo(MesCalHolidayDO.class, o -> {
  68 + o.setId(null); // 避免 id 被赋值
  69 + o.setDay(LocalDateTime.of(2026, 10, 20, 0, 0));
  70 + o.setType(MesCalHolidayTypeEnum.HOLIDAY.getType());
  71 + });
  72 + holidayMapper.insert(dbHoliday);
  73 + // 准备参数:改为工作日
  74 + MesCalHolidaySaveReqVO reqVO = randomPojo(MesCalHolidaySaveReqVO.class, o -> {
  75 + o.setId(null); // 避免 id 被赋值
  76 + o.setDay(dbHoliday.getDay());
  77 + o.setType(MesCalHolidayTypeEnum.WORKDAY.getType());
  78 + });
  79 + // mock planService 的方法:该日期在已确认的排班计划范围内
  80 + MesCalPlanDO plan = randomPojo(MesCalPlanDO.class, o -> o.setStatus(MesCalPlanStatusEnum.CONFIRMED.getStatus()));
  81 + when(planService.getPlanListByStatusAndDay(eq(MesCalPlanStatusEnum.CONFIRMED.getStatus()),
  82 + eq(reqVO.getDay().toLocalDate()))).thenReturn(singletonList(plan));
  83 +
  84 + // 调用,并断言异常
  85 + assertServiceException(() -> holidayService.saveHoliday(reqVO), CAL_HOLIDAY_PLAN_CONFIRMED,
  86 + reqVO.getDay().toLocalDate(), plan.getName());
  87 + // 断言:假期设置未被修改
  88 + assertPojoEquals(dbHoliday, holidayMapper.selectById(dbHoliday.getId()));
  89 + }
  90 +
  91 + @Test
  92 + public void testSaveHoliday_updateRemarkInConfirmedPlan() {
  93 + // mock 数据:已有节假日
  94 + MesCalHolidayDO dbHoliday = randomPojo(MesCalHolidayDO.class, o -> {
  95 + o.setId(null); // 避免 id 被赋值
  96 + o.setDay(LocalDateTime.of(2026, 10, 20, 0, 0));
  97 + o.setType(MesCalHolidayTypeEnum.HOLIDAY.getType());
  98 + });
  99 + holidayMapper.insert(dbHoliday);
  100 + // 准备参数:仍为节假日,只修改备注
  101 + MesCalHolidaySaveReqVO reqVO = randomPojo(MesCalHolidaySaveReqVO.class, o -> {
  102 + o.setId(null); // 避免 id 被赋值
  103 + o.setDay(dbHoliday.getDay());
  104 + o.setType(MesCalHolidayTypeEnum.HOLIDAY.getType());
  105 + });
  106 +
  107 + // 调用
  108 + Long id = holidayService.saveHoliday(reqVO);
  109 + // 断言:更新已有记录,且是否节假日未变化,无需校验排班计划
  110 + assertEquals(dbHoliday.getId(), id);
  111 + assertPojoEquals(reqVO, holidayMapper.selectById(id), "id");
  112 + verify(planService, never()).getPlanListByStatusAndDay(any(), any());
  113 + }
  114 +
  115 + @Test
  116 + public void testSaveHoliday_createWithoutConfirmedPlan() {
  117 + // 准备参数
  118 + MesCalHolidaySaveReqVO reqVO = randomPojo(MesCalHolidaySaveReqVO.class, o -> {
  119 + o.setId(null); // 避免 id 被赋值
  120 + o.setDay(LocalDateTime.of(2026, 10, 20, 0, 0));
  121 + o.setType(MesCalHolidayTypeEnum.HOLIDAY.getType());
  122 + });
  123 + // mock planService 的方法:该日期没有已确认的排班计划
  124 + when(planService.getPlanListByStatusAndDay(eq(MesCalPlanStatusEnum.CONFIRMED.getStatus()),
  125 + eq(reqVO.getDay().toLocalDate()))).thenReturn(emptyList());
  126 +
  127 + // 调用
  128 + Long id = holidayService.saveHoliday(reqVO);
  129 + // 断言
  130 + assertPojoEquals(reqVO, holidayMapper.selectById(id), "id");
  131 + }
  132 +
  133 +}
... ...
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/cal/plan/MesCalPlanServiceImplTest.java
1 1 package cn.iocoder.yudao.module.mes.service.cal.plan;
2 2  
  3 +import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
3 4 import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
4 5 import cn.iocoder.yudao.module.mes.controller.admin.cal.plan.vo.MesCalPlanSaveReqVO;
5 6 import cn.iocoder.yudao.module.mes.dal.dataobject.cal.plan.MesCalPlanDO;
... ... @@ -14,9 +15,17 @@ import org.springframework.test.context.bean.override.mockito.MockitoBean;
14 15  
15 16 import jakarta.annotation.Resource;
16 17  
  18 +import java.time.LocalDate;
17 19 import java.time.LocalDateTime;
  20 +import java.util.List;
18 21  
  22 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  23 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
  24 +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
19 25 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
  26 +import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.CAL_PLAN_NOT_PREPARE;
  27 +import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.CAL_PLAN_TEAM_COUNT_NOT_MATCH;
  28 +import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.CAL_TEAM_SHIFT_GENERATE_REST_INVALID;
20 29 import static org.junit.jupiter.api.Assertions.*;
21 30 import static org.mockito.Mockito.*;
22 31  
... ... @@ -88,6 +97,85 @@ public class MesCalPlanServiceImplTest extends BaseDbUnitTest {
88 97 assertEquals(MesCalPlanStatusEnum.CONFIRMED.getStatus(), updatePlan.getStatus());
89 98 // 断言 2:触发生成班组排班记录
90 99 verify(teamShiftService).generateTeamShiftRecords(plan.getId());
  100 + // 断言 3:重复确认被拒绝,且不会再次生成班组排班记录
  101 + assertServiceException(() -> planService.confirmPlan(plan.getId()), CAL_PLAN_NOT_PREPARE);
  102 + verify(teamShiftService, times(1)).generateTeamShiftRecords(plan.getId());
  103 + }
  104 +
  105 + @Test
  106 + public void testConfirmPlan_generateFailed_rollbackStatus() {
  107 + // mock 数据:插入一条草稿状态的两班倒排班计划
  108 + MesCalPlanDO plan = randomPojo(MesCalPlanDO.class, o -> {
  109 + o.setCalendarType(1);
  110 + o.setStartDate(LocalDateTime.of(2026, 10, 19, 0, 0));
  111 + o.setEndDate(LocalDateTime.of(2026, 10, 21, 0, 0));
  112 + o.setStatus(MesCalPlanStatusEnum.PREPARE.getStatus());
  113 + o.setShiftType(MesCalShiftTypeEnum.TWO.getType());
  114 + o.setShiftMethod(MesCalShiftMethodEnum.DAY.getMethod());
  115 + o.setShiftCount(1);
  116 + });
  117 + planMapper.insert(plan);
  118 + // mock 方法:班组数量满足要求,但生成排班时校验到没有休息间隔
  119 + when(planTeamService.getPlanTeamCountByPlanId(plan.getId())).thenReturn(2L);
  120 + doThrow(exception(CAL_TEAM_SHIFT_GENERATE_REST_INVALID, "乙班",
  121 + LocalDate.of(2026, 10, 19), "夜班", LocalDate.of(2026, 10, 20), "白班"))
  122 + .when(teamShiftService).generateTeamShiftRecords(plan.getId());
  123 +
  124 + // 调用,并断言异常
  125 + assertServiceException(() -> planService.confirmPlan(plan.getId()), CAL_TEAM_SHIFT_GENERATE_REST_INVALID,
  126 + "乙班", LocalDate.of(2026, 10, 19), "夜班", LocalDate.of(2026, 10, 20), "白班");
  127 + // 断言:事务回滚,计划仍为草稿
  128 + MesCalPlanDO dbPlan = planMapper.selectById(plan.getId());
  129 + assertEquals(MesCalPlanStatusEnum.PREPARE.getStatus(), dbPlan.getStatus());
  130 + }
  131 +
  132 + @Test
  133 + public void testConfirmPlan_teamCountExceed() {
  134 + // mock 数据:插入一条草稿状态的两班倒排班计划
  135 + MesCalPlanDO plan = randomPojo(MesCalPlanDO.class, o -> {
  136 + o.setCalendarType(1);
  137 + o.setStartDate(LocalDateTime.of(2026, 10, 19, 0, 0));
  138 + o.setEndDate(LocalDateTime.of(2026, 10, 21, 0, 0));
  139 + o.setStatus(MesCalPlanStatusEnum.PREPARE.getStatus());
  140 + o.setShiftType(MesCalShiftTypeEnum.TWO.getType());
  141 + o.setShiftMethod(MesCalShiftMethodEnum.DAY.getMethod());
  142 + o.setShiftCount(1);
  143 + });
  144 + planMapper.insert(plan);
  145 + // mock 方法:分配了 3 个班组,多出的班组不会参与排班
  146 + when(planTeamService.getPlanTeamCountByPlanId(plan.getId())).thenReturn(3L);
  147 +
  148 + // 调用,并断言异常
  149 + assertServiceException(() -> planService.confirmPlan(plan.getId()), CAL_PLAN_TEAM_COUNT_NOT_MATCH);
  150 + // 断言:计划仍为草稿,且未生成排班记录
  151 + MesCalPlanDO dbPlan = planMapper.selectById(plan.getId());
  152 + assertEquals(MesCalPlanStatusEnum.PREPARE.getStatus(), dbPlan.getStatus());
  153 + verify(teamShiftService, never()).generateTeamShiftRecords(plan.getId());
  154 + }
  155 +
  156 + @Test
  157 + public void testGetPlanListByStatusAndDay() {
  158 + // mock 数据:已确认,结束日期为当天
  159 + MesCalPlanDO plan01 = randomPlan(LocalDateTime.of(2026, 10, 1, 0, 0),
  160 + LocalDateTime.of(2026, 10, 19, 0, 0), MesCalPlanStatusEnum.CONFIRMED.getStatus());
  161 + planMapper.insert(plan01);
  162 + // mock 数据:已确认,开始日期为当天
  163 + MesCalPlanDO plan02 = randomPlan(LocalDateTime.of(2026, 10, 19, 0, 0),
  164 + LocalDateTime.of(2026, 10, 31, 0, 0), MesCalPlanStatusEnum.CONFIRMED.getStatus());
  165 + planMapper.insert(plan02);
  166 + // mock 数据:已确认,但不包含当天
  167 + planMapper.insert(randomPlan(LocalDateTime.of(2026, 10, 20, 0, 0),
  168 + LocalDateTime.of(2026, 10, 31, 0, 0), MesCalPlanStatusEnum.CONFIRMED.getStatus()));
  169 + // mock 数据:包含当天,但为草稿
  170 + planMapper.insert(randomPlan(LocalDateTime.of(2026, 10, 1, 0, 0),
  171 + LocalDateTime.of(2026, 10, 31, 0, 0), MesCalPlanStatusEnum.PREPARE.getStatus()));
  172 +
  173 + // 调用
  174 + List<MesCalPlanDO> list = planService.getPlanListByStatusAndDay(
  175 + MesCalPlanStatusEnum.CONFIRMED.getStatus(), LocalDate.of(2026, 10, 19));
  176 +
  177 + // 断言:只返回包含当天的已确认计划(起止日期为当天也算)
  178 + assertEquals(SetUtils.asSet(plan01.getId(), plan02.getId()), convertSet(list, MesCalPlanDO::getId));
91 179 }
92 180  
93 181 @Test
... ... @@ -116,4 +204,16 @@ public class MesCalPlanServiceImplTest extends BaseDbUnitTest {
116 204 verify(teamShiftService, never()).deleteByPlanId(plan.getId());
117 205 }
118 206  
  207 + private static MesCalPlanDO randomPlan(LocalDateTime startDate, LocalDateTime endDate, Integer status) {
  208 + return randomPojo(MesCalPlanDO.class, o -> {
  209 + o.setCalendarType(1);
  210 + o.setStartDate(startDate);
  211 + o.setEndDate(endDate);
  212 + o.setStatus(status);
  213 + o.setShiftType(MesCalShiftTypeEnum.TWO.getType());
  214 + o.setShiftMethod(MesCalShiftMethodEnum.DAY.getMethod());
  215 + o.setShiftCount(1);
  216 + });
  217 + }
  218 +
119 219 }
... ...
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/cal/team/MesCalTeamShiftServiceImplTest.java
... ... @@ -2,13 +2,17 @@ package cn.iocoder.yudao.module.mes.service.cal.team;
2 2  
3 3 import cn.hutool.core.collection.ListUtil;
4 4 import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
  5 +import cn.iocoder.yudao.module.mes.dal.dataobject.cal.holiday.MesCalHolidayDO;
5 6 import cn.iocoder.yudao.module.mes.dal.dataobject.cal.plan.MesCalPlanDO;
6 7 import cn.iocoder.yudao.module.mes.dal.dataobject.cal.plan.MesCalPlanShiftDO;
7 8 import cn.iocoder.yudao.module.mes.dal.dataobject.cal.plan.MesCalPlanTeamDO;
  9 +import cn.iocoder.yudao.module.mes.dal.dataobject.cal.team.MesCalTeamDO;
8 10 import cn.iocoder.yudao.module.mes.dal.dataobject.cal.team.MesCalTeamShiftDO;
9 11 import cn.iocoder.yudao.module.mes.dal.mysql.cal.team.MesCalTeamShiftMapper;
  12 +import cn.iocoder.yudao.module.mes.enums.cal.MesCalHolidayTypeEnum;
10 13 import cn.iocoder.yudao.module.mes.enums.cal.MesCalShiftMethodEnum;
11 14 import cn.iocoder.yudao.module.mes.enums.cal.MesCalShiftTypeEnum;
  15 +import cn.iocoder.yudao.module.mes.service.cal.holiday.MesCalHolidayService;
12 16 import cn.iocoder.yudao.module.mes.service.cal.plan.MesCalPlanService;
13 17 import cn.iocoder.yudao.module.mes.service.cal.plan.MesCalPlanShiftService;
14 18 import cn.iocoder.yudao.module.mes.service.cal.plan.MesCalPlanTeamService;
... ... @@ -18,10 +22,15 @@ import org.springframework.test.context.bean.override.mockito.MockitoBean;
18 22  
19 23 import jakarta.annotation.Resource;
20 24  
  25 +import java.time.LocalDate;
21 26 import java.time.LocalDateTime;
  27 +import java.util.ArrayList;
22 28 import java.util.Comparator;
23 29 import java.util.List;
24 30  
  31 +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
  32 +import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.CAL_TEAM_SHIFT_GENERATE_REST_INVALID;
  33 +import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.CAL_TEAM_SHIFT_GENERATE_SHIFT_TIME_INVALID;
25 34 import static org.junit.jupiter.api.Assertions.*;
26 35 import static org.mockito.Mockito.*;
27 36  
... ... @@ -45,6 +54,10 @@ public class MesCalTeamShiftServiceImplTest extends BaseDbUnitTest {
45 54 private MesCalPlanShiftService planShiftService;
46 55 @MockitoBean
47 56 private MesCalPlanTeamService planTeamService;
  57 + @MockitoBean
  58 + private MesCalHolidayService holidayService;
  59 + @MockitoBean
  60 + private MesCalTeamService teamService;
48 61  
49 62 @Test
50 63 public void testGenerateTeamShiftRecords_threeShift_rotateThreeStates() {
... ... @@ -79,22 +92,22 @@ public class MesCalTeamShiftServiceImplTest extends BaseDbUnitTest {
79 92 // 调用
80 93 teamShiftService.generateTeamShiftRecords(planId);
81 94  
82   - // 断言:3 天 × 3 班次 = 9 条排班记录,班组按天轮转
83   - List<MesCalTeamShiftDO> records = teamShiftMapper.selectListByPlanId(planId);
84   - records.sort(Comparator.comparing(MesCalTeamShiftDO::getDay).thenComparing(MesCalTeamShiftDO::getSort));
  95 + // 断言:3 天 × 3 班次 = 9 条排班记录,班组按 白班 → 夜班 → 中班 轮转
  96 + // 说明:原先按 白班 → 中班 → 夜班 轮转时,B 组 4/1 中班 24:00 下班后会立即接 4/2 00:00 的夜班,没有休息间隔
  97 + List<MesCalTeamShiftDO> records = getRecords(planId);
85 98 assertEquals(9, records.size());
86 99 // 断言:第 1 天(shiftIndex=0):A 白班、B 中班、C 夜班
87 100 assertRecord(records.get(0), LocalDateTime.of(2026, 4, 1, 0, 0), 1, 101L, 11L);
88 101 assertRecord(records.get(1), LocalDateTime.of(2026, 4, 1, 0, 0), 2, 102L, 12L);
89 102 assertRecord(records.get(2), LocalDateTime.of(2026, 4, 1, 0, 0), 3, 103L, 13L);
90   - // 断言:第 2 天(shiftIndex=1):C 白班、A 中班、B 夜班
91   - assertRecord(records.get(3), LocalDateTime.of(2026, 4, 2, 0, 0), 1, 103L, 11L);
92   - assertRecord(records.get(4), LocalDateTime.of(2026, 4, 2, 0, 0), 2, 101L, 12L);
93   - assertRecord(records.get(5), LocalDateTime.of(2026, 4, 2, 0, 0), 3, 102L, 13L);
94   - // 断言:第 3 天(shiftIndex=2):B 白班、C 中班、A 夜班
95   - assertRecord(records.get(6), LocalDateTime.of(2026, 4, 3, 0, 0), 1, 102L, 11L);
96   - assertRecord(records.get(7), LocalDateTime.of(2026, 4, 3, 0, 0), 2, 103L, 12L);
97   - assertRecord(records.get(8), LocalDateTime.of(2026, 4, 3, 0, 0), 3, 101L, 13L);
  103 + // 断言:第 2 天(shiftIndex=1):B 白班、C 中班、A 夜班(A、B 休息 8 小时,C 休息 32 小时)
  104 + assertRecord(records.get(3), LocalDateTime.of(2026, 4, 2, 0, 0), 1, 102L, 11L);
  105 + assertRecord(records.get(4), LocalDateTime.of(2026, 4, 2, 0, 0), 2, 103L, 12L);
  106 + assertRecord(records.get(5), LocalDateTime.of(2026, 4, 2, 0, 0), 3, 101L, 13L);
  107 + // 断言:第 3 天(shiftIndex=2):C 白班、A 中班、B 夜班
  108 + assertRecord(records.get(6), LocalDateTime.of(2026, 4, 3, 0, 0), 1, 103L, 11L);
  109 + assertRecord(records.get(7), LocalDateTime.of(2026, 4, 3, 0, 0), 2, 101L, 12L);
  110 + assertRecord(records.get(8), LocalDateTime.of(2026, 4, 3, 0, 0), 3, 102L, 13L);
98 111 }
99 112  
100 113 @Test
... ... @@ -121,6 +134,392 @@ public class MesCalTeamShiftServiceImplTest extends BaseDbUnitTest {
121 134 assertRecord(records.get(1), start.plusDays(1), 1, 101L, 11L);
122 135 }
123 136  
  137 + @Test
  138 + public void testGenerateTeamShiftRecords_singleCrossMidnight() {
  139 + // 准备参数
  140 + Long planId = 3003L;
  141 + LocalDate startDate = LocalDate.of(2026, 10, 19);
  142 + LocalDate endDate = LocalDate.of(2026, 10, 21);
  143 + // mock 方法:单班次 22:00~06:00,跨午夜到次日结束,每天之间休息 16 小时
  144 + mockPlan(planId, MesCalShiftTypeEnum.SINGLE, null, null, startDate, endDate);
  145 + when(planShiftService.getPlanShiftListByPlanId(planId)).thenReturn(ListUtil.of(
  146 + MesCalPlanShiftDO.builder().id(11L).planId(planId).sort(1).name("夜班")
  147 + .startTime("22:00").endTime("06:00").build()));
  148 + mockTeams(planId, 101L);
  149 +
  150 + // 调用
  151 + teamShiftService.generateTeamShiftRecords(planId);
  152 +
  153 + // 断言:每天 1 条排班记录
  154 + List<MesCalTeamShiftDO> records = getRecords(planId);
  155 + assertEquals(3, records.size());
  156 + assertRecord(records.get(0), startDate.atStartOfDay(), 1, 101L, 11L);
  157 + assertRecord(records.get(1), startDate.plusDays(1).atStartOfDay(), 1, 101L, 11L);
  158 + assertRecord(records.get(2), endDate.atStartOfDay(), 1, 101L, 11L);
  159 + }
  160 +
  161 + @Test
  162 + public void testGenerateTeamShiftRecords_twoShiftFullDay_rotateWithoutRest() {
  163 + // 准备参数
  164 + Long planId = 3004L;
  165 + LocalDate startDate = LocalDate.of(2026, 10, 19);
  166 + LocalDate endDate = LocalDate.of(2026, 10, 21);
  167 + // mock 方法:两班倒、按天倒班 1 天,默认班次 08:00~20:00 / 20:00~08:00 覆盖全天
  168 + mockPlan(planId, MesCalShiftTypeEnum.TWO, MesCalShiftMethodEnum.DAY, 1, startDate, endDate);
  169 + mockTwoShifts(planId, "08:00", "20:00", "20:00", "08:00");
  170 + mockTeams(planId, 101L, 102L);
  171 + when(teamService.getTeam(eq(102L))).thenReturn(MesCalTeamDO.builder().id(102L).name("乙班").build());
  172 +
  173 + // 调用,并断言异常:B 组 10-19 夜班次日 08:00 下班后,立即接 10-20 08:00 的白班,连续上班 24 小时
  174 + assertServiceException(() -> teamShiftService.generateTeamShiftRecords(planId),
  175 + CAL_TEAM_SHIFT_GENERATE_REST_INVALID, "乙班",
  176 + LocalDate.of(2026, 10, 19), "夜班", LocalDate.of(2026, 10, 20), "白班");
  177 + // 断言:没有写入任何排班记录
  178 + assertTrue(teamShiftMapper.selectListByPlanId(planId).isEmpty());
  179 + }
  180 +
  181 + @Test
  182 + public void testGenerateTeamShiftRecords_twoShiftFullDay_longerShiftCountOnlyDelay() {
  183 + // 准备参数
  184 + Long planId = 3005L;
  185 + LocalDate startDate = LocalDate.of(2026, 10, 19);
  186 + LocalDate endDate = LocalDate.of(2026, 10, 23);
  187 + // mock 方法:两班倒、按天倒班 3 天,默认班次覆盖全天
  188 + mockPlan(planId, MesCalShiftTypeEnum.TWO, MesCalShiftMethodEnum.DAY, 3, startDate, endDate);
  189 + mockTwoShifts(planId, "08:00", "20:00", "20:00", "08:00");
  190 + mockTeams(planId, 101L, 102L);
  191 + when(teamService.getTeam(eq(102L))).thenReturn(MesCalTeamDO.builder().id(102L).name("乙班").build());
  192 +
  193 + // 调用,并断言异常:增加倒班天数只是推迟到 10-22 换班,B 组仍然夜班下班后立即接白班
  194 + assertServiceException(() -> teamShiftService.generateTeamShiftRecords(planId),
  195 + CAL_TEAM_SHIFT_GENERATE_REST_INVALID, "乙班",
  196 + LocalDate.of(2026, 10, 21), "夜班", LocalDate.of(2026, 10, 22), "白班");
  197 + // 断言:没有写入任何排班记录
  198 + assertTrue(teamShiftMapper.selectListByPlanId(planId).isEmpty());
  199 + }
  200 +
  201 + @Test
  202 + public void testGenerateTeamShiftRecords_twoShiftOverlap() {
  203 + // 准备参数
  204 + Long planId = 3006L;
  205 + LocalDate startDate = LocalDate.of(2026, 10, 19);
  206 + LocalDate endDate = LocalDate.of(2026, 10, 20);
  207 + // mock 方法:两班倒、按天倒班 1 天,夜班 20:00~08:30 与次日 08:00 的白班重叠
  208 + mockPlan(planId, MesCalShiftTypeEnum.TWO, MesCalShiftMethodEnum.DAY, 1, startDate, endDate);
  209 + mockTwoShifts(planId, "08:00", "20:00", "20:00", "08:30");
  210 + mockTeams(planId, 101L, 102L);
  211 + when(teamService.getTeam(eq(102L))).thenReturn(MesCalTeamDO.builder().id(102L).name("乙班").build());
  212 +
  213 + // 调用,并断言异常
  214 + assertServiceException(() -> teamShiftService.generateTeamShiftRecords(planId),
  215 + CAL_TEAM_SHIFT_GENERATE_REST_INVALID, "乙班",
  216 + LocalDate.of(2026, 10, 19), "夜班", LocalDate.of(2026, 10, 20), "白班");
  217 + // 断言:没有写入任何排班记录
  218 + assertTrue(teamShiftMapper.selectListByPlanId(planId).isEmpty());
  219 + }
  220 +
  221 + @Test
  222 + public void testGenerateTeamShiftRecords_twoShiftOneMinuteRest() {
  223 + // 准备参数
  224 + Long planId = 3007L;
  225 + LocalDate startDate = LocalDate.of(2026, 10, 19);
  226 + LocalDate endDate = LocalDate.of(2026, 10, 21);
  227 + // mock 方法:两班倒、按天倒班 1 天,夜班 20:00~07:59,与次日 08:00 的白班之间留 1 分钟
  228 + mockPlan(planId, MesCalShiftTypeEnum.TWO, MesCalShiftMethodEnum.DAY, 1, startDate, endDate);
  229 + mockTwoShifts(planId, "08:00", "20:00", "20:00", "07:59");
  230 + mockTeams(planId, 101L, 102L);
  231 +
  232 + // 调用
  233 + teamShiftService.generateTeamShiftRecords(planId);
  234 +
  235 + // 断言:3 天 × 2 班次 = 6 条排班记录,每天换班
  236 + List<MesCalTeamShiftDO> records = getRecords(planId);
  237 + assertEquals(6, records.size());
  238 + assertRecord(records.get(0), LocalDateTime.of(2026, 10, 19, 0, 0), 1, 101L, 11L);
  239 + assertRecord(records.get(1), LocalDateTime.of(2026, 10, 19, 0, 0), 2, 102L, 12L);
  240 + assertRecord(records.get(2), LocalDateTime.of(2026, 10, 20, 0, 0), 1, 102L, 11L);
  241 + assertRecord(records.get(3), LocalDateTime.of(2026, 10, 20, 0, 0), 2, 101L, 12L);
  242 + assertRecord(records.get(4), LocalDateTime.of(2026, 10, 21, 0, 0), 1, 101L, 11L);
  243 + assertRecord(records.get(5), LocalDateTime.of(2026, 10, 21, 0, 0), 2, 102L, 12L);
  244 + }
  245 +
  246 + @Test
  247 + public void testGenerateTeamShiftRecords_twoShiftFullDay_singleDay() {
  248 + // 准备参数
  249 + Long planId = 3008L;
  250 + LocalDate day = LocalDate.of(2026, 10, 19);
  251 + // mock 方法:两班倒、默认班次覆盖全天,只排 1 天,不发生换班
  252 + mockPlan(planId, MesCalShiftTypeEnum.TWO, MesCalShiftMethodEnum.DAY, 1, day, day);
  253 + mockTwoShifts(planId, "08:00", "20:00", "20:00", "08:00");
  254 + mockTeams(planId, 101L, 102L);
  255 +
  256 + // 调用
  257 + teamShiftService.generateTeamShiftRecords(planId);
  258 +
  259 + // 断言:1 天 × 2 班次 = 2 条排班记录
  260 + List<MesCalTeamShiftDO> records = getRecords(planId);
  261 + assertEquals(2, records.size());
  262 + assertRecord(records.get(0), day.atStartOfDay(), 1, 101L, 11L);
  263 + assertRecord(records.get(1), day.atStartOfDay(), 2, 102L, 12L);
  264 + }
  265 +
  266 + @Test
  267 + public void testGenerateTeamShiftRecords_shiftTimeInvalid() {
  268 + // 准备参数
  269 + Long planId = 3009L;
  270 + LocalDate day = LocalDate.of(2026, 10, 19);
  271 + // mock 方法:白班结束时间不合法
  272 + mockPlan(planId, MesCalShiftTypeEnum.TWO, MesCalShiftMethodEnum.DAY, 1, day, day);
  273 + mockTwoShifts(planId, "08:00", "25:00", "20:00", "08:00");
  274 + mockTeams(planId, 101L, 102L);
  275 +
  276 + // 调用,并断言异常
  277 + assertServiceException(() -> teamShiftService.generateTeamShiftRecords(planId),
  278 + CAL_TEAM_SHIFT_GENERATE_SHIFT_TIME_INVALID, "白班");
  279 + // 断言:没有写入任何排班记录
  280 + assertTrue(teamShiftMapper.selectListByPlanId(planId).isEmpty());
  281 + }
  282 +
  283 + @Test
  284 + public void testGenerateTeamShiftRecords_threeShift_hourWithoutZeroAndEnd24() {
  285 + // 准备参数
  286 + Long planId = 3016L;
  287 + LocalDate startDate = LocalDate.of(2026, 10, 19);
  288 + LocalDate endDate = LocalDate.of(2026, 10, 20);
  289 + // mock 方法:三班倒、按天倒班 1 天,时间写作 8:00、0:00,中班结束写作 24:00
  290 + mockPlan(planId, MesCalShiftTypeEnum.THREE, MesCalShiftMethodEnum.DAY, 1, startDate, endDate);
  291 + when(planShiftService.getPlanShiftListByPlanId(planId)).thenReturn(ListUtil.of(
  292 + MesCalPlanShiftDO.builder().id(11L).planId(planId).sort(1).name("白班").startTime("8:00").endTime("16:00").build(),
  293 + MesCalPlanShiftDO.builder().id(12L).planId(planId).sort(2).name("中班").startTime("16:00").endTime("24:00").build(),
  294 + MesCalPlanShiftDO.builder().id(13L).planId(planId).sort(3).name("夜班").startTime("0:00").endTime("8:00").build()));
  295 + mockTeams(planId, 101L, 102L, 103L);
  296 +
  297 + // 调用
  298 + teamShiftService.generateTeamShiftRecords(planId);
  299 +
  300 + // 断言:与默认班次一致,2 天 × 3 班次 = 6 条排班记录,换班后休息 8 / 8 / 32 小时
  301 + List<MesCalTeamShiftDO> records = getRecords(planId);
  302 + assertEquals(6, records.size());
  303 + assertRecord(records.get(0), startDate.atStartOfDay(), 1, 101L, 11L);
  304 + assertRecord(records.get(1), startDate.atStartOfDay(), 2, 102L, 12L);
  305 + assertRecord(records.get(2), startDate.atStartOfDay(), 3, 103L, 13L);
  306 + assertRecord(records.get(3), endDate.atStartOfDay(), 1, 102L, 11L);
  307 + assertRecord(records.get(4), endDate.atStartOfDay(), 2, 103L, 12L);
  308 + assertRecord(records.get(5), endDate.atStartOfDay(), 3, 101L, 13L);
  309 + }
  310 +
  311 + @Test
  312 + public void testGenerateTeamShiftRecords_singleFullDayEnd24_withoutRest() {
  313 + // 准备参数
  314 + Long planId = 3017L;
  315 + LocalDate startDate = LocalDate.of(2026, 10, 19);
  316 + LocalDate endDate = LocalDate.of(2026, 10, 20);
  317 + // mock 方法:单班次 00:00~24:00,结束于次日 00:00,与次日的班次首尾相接
  318 + mockPlan(planId, MesCalShiftTypeEnum.SINGLE, null, null, startDate, endDate);
  319 + when(planShiftService.getPlanShiftListByPlanId(planId)).thenReturn(ListUtil.of(
  320 + MesCalPlanShiftDO.builder().id(11L).planId(planId).sort(1).name("全天班")
  321 + .startTime("00:00").endTime("24:00").build()));
  322 + mockTeams(planId, 101L);
  323 + when(teamService.getTeam(eq(101L))).thenReturn(MesCalTeamDO.builder().id(101L).name("甲班").build());
  324 +
  325 + // 调用,并断言异常:24:00 不放宽连班校验
  326 + assertServiceException(() -> teamShiftService.generateTeamShiftRecords(planId),
  327 + CAL_TEAM_SHIFT_GENERATE_REST_INVALID, "甲班", startDate, "全天班", endDate, "全天班");
  328 + // 断言:没有写入任何排班记录
  329 + assertTrue(teamShiftMapper.selectListByPlanId(planId).isEmpty());
  330 + }
  331 +
  332 + @Test
  333 + public void testGenerateTeamShiftRecords_shiftTimeInvalid_boundary() {
  334 + // 准备参数:开始时间不允许 24:00;结束时间只允许 24:00 整;分钟、小时越界;非 H:mm 格式
  335 + String[][] cases = {
  336 + {"24:00", "08:00"}, {"08:00", "24:30"}, {"08:60", "16:00"},
  337 + {"08:00", "16:5"}, {"8点", "16:00"}, {"08:00:00", "16:00"}
  338 + };
  339 + for (int i = 0; i < cases.length; i++) {
  340 + Long planId = 3020L + i;
  341 + LocalDate day = LocalDate.of(2026, 10, 19);
  342 + // mock 方法:单班次
  343 + mockPlan(planId, MesCalShiftTypeEnum.SINGLE, null, null, day, day);
  344 + when(planShiftService.getPlanShiftListByPlanId(planId)).thenReturn(ListUtil.of(
  345 + MesCalPlanShiftDO.builder().id(11L).planId(planId).sort(1).name("白班")
  346 + .startTime(cases[i][0]).endTime(cases[i][1]).build()));
  347 + mockTeams(planId, 101L);
  348 +
  349 + // 调用,并断言异常
  350 + assertServiceException(() -> teamShiftService.generateTeamShiftRecords(planId),
  351 + CAL_TEAM_SHIFT_GENERATE_SHIFT_TIME_INVALID, "白班");
  352 + // 断言:没有写入任何排班记录
  353 + assertTrue(teamShiftMapper.selectListByPlanId(planId).isEmpty());
  354 + }
  355 + }
  356 +
  357 + @Test
  358 + public void testGenerateTeamShiftRecords_dayMethod_holidayNotConsumeShiftCount() {
  359 + // 准备参数
  360 + Long planId = 3010L;
  361 + LocalDate startDate = LocalDate.of(2026, 10, 19);
  362 + LocalDate endDate = LocalDate.of(2026, 10, 22);
  363 + // mock 方法:两班倒、按天倒班 2 天,班次之间留有空档
  364 + mockPlan(planId, MesCalShiftTypeEnum.TWO, MesCalShiftMethodEnum.DAY, 2, startDate, endDate);
  365 + mockTwoShifts(planId, "08:00", "17:00", "20:00", "05:00");
  366 + mockTeams(planId, 101L, 102L);
  367 + // mock 方法:10-20 为节假日;10-21 为工作日,照常排班
  368 + mockHolidays(startDate, endDate, buildHoliday(LocalDate.of(2026, 10, 20), MesCalHolidayTypeEnum.HOLIDAY),
  369 + buildHoliday(LocalDate.of(2026, 10, 21), MesCalHolidayTypeEnum.WORKDAY));
  370 +
  371 + // 调用
  372 + teamShiftService.generateTeamShiftRecords(planId);
  373 +
  374 + // 断言:节假日停班,3 个有效排班日 × 2 班次 = 6 条排班记录
  375 + List<MesCalTeamShiftDO> records = getRecords(planId);
  376 + assertEquals(6, records.size());
  377 + // 断言:10-19、10-21 为第 1、2 个有效排班日,A 组白班
  378 + assertRecord(records.get(0), LocalDateTime.of(2026, 10, 19, 0, 0), 1, 101L, 11L);
  379 + assertRecord(records.get(1), LocalDateTime.of(2026, 10, 19, 0, 0), 2, 102L, 12L);
  380 + assertRecord(records.get(2), LocalDateTime.of(2026, 10, 21, 0, 0), 1, 101L, 11L);
  381 + assertRecord(records.get(3), LocalDateTime.of(2026, 10, 21, 0, 0), 2, 102L, 12L);
  382 + // 断言:10-22 为第 3 个有效排班日,才换班
  383 + assertRecord(records.get(4), LocalDateTime.of(2026, 10, 22, 0, 0), 1, 102L, 11L);
  384 + assertRecord(records.get(5), LocalDateTime.of(2026, 10, 22, 0, 0), 2, 101L, 12L);
  385 + }
  386 +
  387 + @Test
  388 + public void testGenerateTeamShiftRecords_dayMethod_firstDayHoliday() {
  389 + // 准备参数
  390 + Long planId = 3011L;
  391 + LocalDate startDate = LocalDate.of(2026, 10, 19);
  392 + LocalDate endDate = LocalDate.of(2026, 10, 21);
  393 + // mock 方法:两班倒、按天倒班 1 天,首日 10-19 为节假日
  394 + mockPlan(planId, MesCalShiftTypeEnum.TWO, MesCalShiftMethodEnum.DAY, 1, startDate, endDate);
  395 + mockTwoShifts(planId, "08:00", "17:00", "20:00", "05:00");
  396 + mockTeams(planId, 101L, 102L);
  397 + mockHolidays(startDate, endDate, buildHoliday(startDate, MesCalHolidayTypeEnum.HOLIDAY));
  398 +
  399 + // 调用
  400 + teamShiftService.generateTeamShiftRecords(planId);
  401 +
  402 + // 断言:从 10-20 开始排班,10-20 为第 1 个有效排班日,不换班
  403 + List<MesCalTeamShiftDO> records = getRecords(planId);
  404 + assertEquals(4, records.size());
  405 + assertRecord(records.get(0), LocalDateTime.of(2026, 10, 20, 0, 0), 1, 101L, 11L);
  406 + assertRecord(records.get(1), LocalDateTime.of(2026, 10, 20, 0, 0), 2, 102L, 12L);
  407 + assertRecord(records.get(2), LocalDateTime.of(2026, 10, 21, 0, 0), 1, 102L, 11L);
  408 + assertRecord(records.get(3), LocalDateTime.of(2026, 10, 21, 0, 0), 2, 101L, 12L);
  409 + }
  410 +
  411 + @Test
  412 + public void testGenerateTeamShiftRecords_allHoliday() {
  413 + // 准备参数
  414 + Long planId = 3012L;
  415 + LocalDate startDate = LocalDate.of(2026, 10, 19);
  416 + LocalDate endDate = LocalDate.of(2026, 10, 20);
  417 + // mock 方法:两班倒、计划范围内全部为节假日
  418 + mockPlan(planId, MesCalShiftTypeEnum.TWO, MesCalShiftMethodEnum.DAY, 1, startDate, endDate);
  419 + mockTwoShifts(planId, "08:00", "20:00", "20:00", "08:00");
  420 + mockTeams(planId, 101L, 102L);
  421 + mockHolidays(startDate, endDate, buildHoliday(startDate, MesCalHolidayTypeEnum.HOLIDAY),
  422 + buildHoliday(endDate, MesCalHolidayTypeEnum.HOLIDAY));
  423 +
  424 + // 调用
  425 + teamShiftService.generateTeamShiftRecords(planId);
  426 +
  427 + // 断言:不生成排班记录
  428 + assertTrue(teamShiftMapper.selectListByPlanId(planId).isEmpty());
  429 + }
  430 +
  431 + @Test
  432 + public void testGenerateTeamShiftRecords_weekMethod_holidayOnWeekStart() {
  433 + // 10-18 周日、10-19 周一为节假日、10-20 周二
  434 + assertRotateAcrossHoliday(3013L, MesCalShiftMethodEnum.WEEK,
  435 + LocalDate.of(2026, 10, 18), LocalDate.of(2026, 10, 19));
  436 + }
  437 +
  438 + @Test
  439 + public void testGenerateTeamShiftRecords_monthMethod_holidayOnMonthStart() {
  440 + // 09-30、10-01 月初为节假日、10-02
  441 + assertRotateAcrossHoliday(3014L, MesCalShiftMethodEnum.MONTH,
  442 + LocalDate.of(2026, 9, 30), LocalDate.of(2026, 10, 1));
  443 + }
  444 +
  445 + @Test
  446 + public void testGenerateTeamShiftRecords_quarterMethod_holidayOnQuarterStart() {
  447 + // 06-30、07-01 季度初为节假日、07-02
  448 + assertRotateAcrossHoliday(3015L, MesCalShiftMethodEnum.QUARTER,
  449 + LocalDate.of(2026, 6, 30), LocalDate.of(2026, 7, 1));
  450 + }
  451 +
  452 + /**
  453 + * 断言按周/月/季度倒班时,周期起点恰逢节假日也照常换班
  454 + *
  455 + * 默认两班倒班次覆盖全天,换班前一天夜班的班组,隔着节假日停班一天再上白班,留有 24 小时休息
  456 + */
  457 + private void assertRotateAcrossHoliday(Long planId, MesCalShiftMethodEnum shiftMethod,
  458 + LocalDate startDate, LocalDate holidayDate) {
  459 + // 准备参数
  460 + LocalDate endDate = holidayDate.plusDays(1);
  461 + // mock 方法:两班倒、默认班次覆盖全天,周期起点为节假日
  462 + mockPlan(planId, MesCalShiftTypeEnum.TWO, shiftMethod, null, startDate, endDate);
  463 + mockTwoShifts(planId, "08:00", "20:00", "20:00", "08:00");
  464 + mockTeams(planId, 101L, 102L);
  465 + mockHolidays(startDate, endDate, buildHoliday(holidayDate, MesCalHolidayTypeEnum.HOLIDAY));
  466 +
  467 + // 调用
  468 + teamShiftService.generateTeamShiftRecords(planId);
  469 +
  470 + // 断言:节假日停班,2 天 × 2 班次 = 4 条排班记录
  471 + List<MesCalTeamShiftDO> records = getRecords(planId);
  472 + assertEquals(4, records.size());
  473 + // 断言:周期起点前,A 组白班、B 组夜班
  474 + assertRecord(records.get(0), startDate.atStartOfDay(), 1, 101L, 11L);
  475 + assertRecord(records.get(1), startDate.atStartOfDay(), 2, 102L, 12L);
  476 + // 断言:周期起点虽为节假日,仍然换班:B 组白班、A 组夜班
  477 + assertRecord(records.get(2), endDate.atStartOfDay(), 1, 102L, 11L);
  478 + assertRecord(records.get(3), endDate.atStartOfDay(), 2, 101L, 12L);
  479 + }
  480 +
  481 + private void mockPlan(Long planId, MesCalShiftTypeEnum shiftType, MesCalShiftMethodEnum shiftMethod,
  482 + Integer shiftCount, LocalDate startDate, LocalDate endDate) {
  483 + when(planService.getPlan(planId)).thenReturn(MesCalPlanDO.builder()
  484 + .id(planId).code("PLAN-" + planId).name("排班计划" + planId)
  485 + .calendarType(1).startDate(startDate.atStartOfDay()).endDate(endDate.atStartOfDay())
  486 + .shiftType(shiftType.getType())
  487 + .shiftMethod(shiftMethod != null ? shiftMethod.getMethod() : null)
  488 + .shiftCount(shiftCount).build());
  489 + }
  490 +
  491 + private void mockTwoShifts(Long planId, String dayStartTime, String dayEndTime,
  492 + String nightStartTime, String nightEndTime) {
  493 + when(planShiftService.getPlanShiftListByPlanId(planId)).thenReturn(ListUtil.of(
  494 + MesCalPlanShiftDO.builder().id(11L).planId(planId).sort(1).name("白班")
  495 + .startTime(dayStartTime).endTime(dayEndTime).build(),
  496 + MesCalPlanShiftDO.builder().id(12L).planId(planId).sort(2).name("夜班")
  497 + .startTime(nightStartTime).endTime(nightEndTime).build()));
  498 + }
  499 +
  500 + private void mockTeams(Long planId, Long... teamIds) {
  501 + List<MesCalPlanTeamDO> teams = new ArrayList<>();
  502 + for (int i = 0; i < teamIds.length; i++) {
  503 + teams.add(MesCalPlanTeamDO.builder().id(21L + i).planId(planId).teamId(teamIds[i]).build());
  504 + }
  505 + when(planTeamService.getPlanTeamListByPlanId(planId)).thenReturn(teams);
  506 + }
  507 +
  508 + private void mockHolidays(LocalDate startDate, LocalDate endDate, MesCalHolidayDO... holidays) {
  509 + when(holidayService.getHolidayList(startDate.atStartOfDay(), endDate.atTime(23, 59, 59)))
  510 + .thenReturn(ListUtil.of(holidays));
  511 + }
  512 +
  513 + private static MesCalHolidayDO buildHoliday(LocalDate day, MesCalHolidayTypeEnum type) {
  514 + return MesCalHolidayDO.builder().day(day.atStartOfDay()).type(type.getType()).build();
  515 + }
  516 +
  517 + private List<MesCalTeamShiftDO> getRecords(Long planId) {
  518 + List<MesCalTeamShiftDO> records = teamShiftMapper.selectListByPlanId(planId);
  519 + records.sort(Comparator.comparing(MesCalTeamShiftDO::getDay).thenComparing(MesCalTeamShiftDO::getSort));
  520 + return records;
  521 + }
  522 +
124 523 private static void assertRecord(MesCalTeamShiftDO actual, LocalDateTime day, Integer sort,
125 524 Long teamId, Long shiftId) {
126 525 assertEquals(day, actual.getDay());
... ...
yudao-module-mes/src/test/resources/sql/clean.sql
... ... @@ -2,6 +2,7 @@ DELETE FROM &quot;mes_cal_team_shift&quot;;
2 2 DELETE FROM "mes_cal_plan_team";
3 3 DELETE FROM "mes_cal_plan_shift";
4 4 DELETE FROM "mes_cal_plan";
  5 +DELETE FROM "mes_cal_holiday";
5 6 DELETE FROM "mes_md_auto_code_rule";
6 7 DELETE FROM "mes_md_auto_code_part";
7 8 DELETE FROM "mes_md_auto_code_record";
... ...
yudao-module-mes/src/test/resources/sql/create_tables.sql
... ... @@ -80,6 +80,23 @@ CREATE TABLE IF NOT EXISTS &quot;mes_cal_team_shift&quot; (
80 80 );
81 81  
82 82 -- ----------------------------
  83 +-- MES 假期设置
  84 +-- ----------------------------
  85 +CREATE TABLE IF NOT EXISTS "mes_cal_holiday" (
  86 + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
  87 + "day" timestamp DEFAULT NULL,
  88 + "type" tinyint DEFAULT NULL,
  89 + "remark" varchar(500) DEFAULT NULL,
  90 + "creator" varchar(64) DEFAULT '',
  91 + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  92 + "updater" varchar(64) DEFAULT '',
  93 + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  94 + "deleted" bit NOT NULL DEFAULT FALSE,
  95 + "tenant_id" bigint NOT NULL DEFAULT 0,
  96 + PRIMARY KEY ("id")
  97 +);
  98 +
  99 +-- ----------------------------
83 100 -- MES 编码规则表
84 101 -- ----------------------------
85 102 CREATE TABLE IF NOT EXISTS "mes_md_auto_code_rule" (
... ...
yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/order/vo/PayOrderExcelVO.java
... ... @@ -4,8 +4,8 @@ import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.framework.excel.core.convert.MoneyConvert;
6 6 import cn.iocoder.yudao.module.pay.enums.DictTypeConstants;
7   -import cn.idev.excel.annotation.ExcelProperty;
8 7 import lombok.Data;
  8 +import org.apache.fesod.sheet.annotation.ExcelProperty;
9 9  
10 10 import java.time.LocalDateTime;
11 11  
... ...
yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/refund/vo/PayRefundExcelVO.java
... ... @@ -4,8 +4,8 @@ import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.framework.excel.core.convert.MoneyConvert;
6 6 import cn.iocoder.yudao.module.pay.enums.DictTypeConstants;
7   -import cn.idev.excel.annotation.ExcelProperty;
8 7 import lombok.Data;
  8 +import org.apache.fesod.sheet.annotation.ExcelProperty;
9 9  
10 10 import java.time.LocalDateTime;
11 11  
... ...
yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/transfer/vo/PayTransferRespVO.java
... ... @@ -5,10 +5,10 @@ import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.framework.excel.core.convert.MoneyConvert;
6 6 import cn.iocoder.yudao.module.pay.enums.DictTypeConstants;
7 7  
8   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
9   -import cn.idev.excel.annotation.ExcelProperty;
10 8 import io.swagger.v3.oas.annotations.media.Schema;
11 9 import lombok.*;
  10 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  11 +import org.apache.fesod.sheet.annotation.ExcelProperty;
12 12 import java.time.LocalDateTime;
13 13 import java.util.Map;
14 14  
... ...
yudao-module-pms/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/workitem/PmsWorkItemImportExcelVO.java
1 1 package cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
6 4 import cn.iocoder.yudao.framework.excel.core.annotations.ExcelColumnSelect;
7 5 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
... ... @@ -15,6 +13,8 @@ import lombok.AllArgsConstructor;
15 13 import lombok.Builder;
16 14 import lombok.Data;
17 15 import lombok.NoArgsConstructor;
  16 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  17 +import org.apache.fesod.sheet.annotation.ExcelProperty;
18 18  
19 19 import java.time.LocalDateTime;
20 20 import java.util.List;
... ...
yudao-module-pms/src/main/java/cn/iocoder/yudao/module/pms/controller/admin/pm/workitem/vo/workitem/PmsWorkItemRespVO.java
1 1 package cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.workitem;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import cn.iocoder.yudao.module.pms.controller.admin.pm.workitem.vo.label.PmsWorkItemLabelRespVO;
6 4 import io.swagger.v3.oas.annotations.media.Schema;
7 5 import lombok.Data;
  6 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  7 +import org.apache.fesod.sheet.annotation.ExcelProperty;
8 8  
9 9 import java.time.LocalDateTime;
10 10 import java.util.List;
... ...
yudao-module-pms/src/main/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectService.java
... ... @@ -45,6 +45,16 @@ public interface PmsProjectService {
45 45 PmsProjectDO getProject(Long id);
46 46  
47 47 /**
  48 + * 获得项目,并锁定项目行
  49 + *
  50 + * 需在事务中调用,锁保持至事务结束
  51 + *
  52 + * @param id 项目编号
  53 + * @return 项目
  54 + */
  55 + PmsProjectDO getProjectForUpdate(Long id);
  56 +
  57 + /**
48 58 * 获得项目详情并记录访问时间
49 59 *
50 60 * @param id 项目编号
... ...
yudao-module-pms/src/main/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectServiceImpl.java
... ... @@ -118,6 +118,11 @@ public class PmsProjectServiceImpl implements PmsProjectService {
118 118 }
119 119  
120 120 @Override
  121 + public PmsProjectDO getProjectForUpdate(Long id) {
  122 + return projectMapper.selectOneForUpdate(PmsProjectDO::getId, id);
  123 + }
  124 +
  125 + @Override
121 126 public PmsProjectDO getProjectAndUpdateVisitTime(Long id, Long userId) {
122 127 // 1. 校验当前用户可以查看项目
123 128 PmsProjectDO project = projectMemberService.validateProjectReadable(id, userId);
... ... @@ -307,8 +312,11 @@ public class PmsProjectServiceImpl implements PmsProjectService {
307 312 @Override
308 313 @Transactional(rollbackFor = Exception.class)
309 314 public void deleteProject(Long id, Long userId) {
310   - // 1.1 校验项目存在
311   - PmsProjectDO project = validateProjectExists(id);
  315 + // 1.1 锁定项目并校验存在,与个人排序保持一致的加锁顺序(https://t.zsxq.com/V48Hl)
  316 + PmsProjectDO project = getProjectForUpdate(id);
  317 + if (project == null) {
  318 + throw exception(PROJECT_NOT_EXISTS);
  319 + }
312 320 // 1.2 校验项目位于回收站
313 321 if (ObjectUtil.notEqual(PmsProjectStatusEnum.RECYCLED.getStatus(), project.getStatus())) {
314 322 throw exception(PROJECT_STATUS_INVALID);
... ...
yudao-module-pms/src/main/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemUserSortServiceImpl.java
1 1 package cn.iocoder.yudao.module.pms.service.pm.workitem;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
  4 +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectDO;
4 5 import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemDO;
5 6 import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemUserSortDO;
6 7 import cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem.PmsWorkItemUserSortMapper;
  8 +import cn.iocoder.yudao.module.pms.enums.pm.project.PmsProjectStatusEnum;
  9 +import cn.iocoder.yudao.module.pms.service.pm.project.PmsProjectService;
7 10 import jakarta.annotation.Resource;
  11 +import org.springframework.context.annotation.Lazy;
8 12 import org.springframework.stereotype.Service;
9 13 import org.springframework.transaction.annotation.Transactional;
10 14 import org.springframework.validation.annotation.Validated;
... ... @@ -13,7 +17,10 @@ import java.util.Comparator;
13 17 import java.util.List;
14 18 import java.util.Map;
15 19  
  20 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
16 21 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
  22 +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_NOT_EXISTS;
  23 +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_STATUS_INVALID;
17 24  
18 25 /**
19 26 * PMS 工作项个人排序 Service 实现类
... ... @@ -27,13 +34,27 @@ public class PmsWorkItemUserSortServiceImpl implements PmsWorkItemUserSortServic
27 34 @Resource
28 35 private PmsWorkItemUserSortMapper workItemUserSortMapper;
29 36  
  37 + @Lazy // 延迟加载,避免循环依赖
  38 + @Resource
  39 + private PmsProjectService projectService;
  40 +
30 41 @Override
31 42 @Transactional(rollbackFor = Exception.class)
32 43 public void updateWorkItemUserSort(Long projectId, List<Long> workItemIds, Long userId) {
33   - // 1. 删除原有个人排序
  44 + // 1.1 锁定项目,避免并发更新个人排序产生重复记录(https://t.zsxq.com/V48Hl)
  45 + PmsProjectDO project = projectService.getProjectForUpdate(projectId);
  46 + // 1.2 校验锁定后的项目状态,避免项目并发回收、删除后继续写入个人排序
  47 + if (project == null) {
  48 + throw exception(PROJECT_NOT_EXISTS);
  49 + }
  50 + if (!PmsProjectStatusEnum.ACTIVE.getStatus().equals(project.getStatus())) {
  51 + throw exception(PROJECT_STATUS_INVALID);
  52 + }
  53 +
  54 + // 2. 删除原有个人排序
34 55 workItemUserSortMapper.deleteByProjectIdAndUserId(projectId, userId);
35 56  
36   - // 2. 按请求顺序保存个人排序
  57 + // 3. 按请求顺序保存个人排序
37 58 for (int index = 0; index < workItemIds.size(); index++) {
38 59 workItemUserSortMapper.insert(new PmsWorkItemUserSortDO().setProjectId(projectId)
39 60 .setWorkItemId(workItemIds.get(index)).setUserId(userId).setSort(index + 1));
... ...
yudao-module-pms/src/test/java/cn/iocoder/yudao/module/pms/service/pm/project/PmsProjectServiceImplTest.java
... ... @@ -33,6 +33,7 @@ import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServic
33 33 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
34 34 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
35 35 import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_ADMIN_REQUIRED;
  36 +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_NOT_EXISTS;
36 37 import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_OWNER_REQUIRED;
37 38 import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_STATUS_INVALID;
38 39 import static java.util.Arrays.asList;
... ... @@ -237,6 +238,35 @@ public class PmsProjectServiceImplTest extends BaseDbUnitTest {
237 238 }
238 239  
239 240 @Test
  241 + public void testDeleteProject_success() {
  242 + // mock 数据
  243 + Long userId = randomLongId();
  244 + PmsProjectDO project = randomProjectDO(PmsProjectStatusEnum.RECYCLED.getStatus());
  245 + projectMapper.insert(project);
  246 + when(projectMemberService.hasProjectOwnerPermission(project.getId(), userId)).thenReturn(true);
  247 +
  248 + // 调用
  249 + projectService.deleteProject(project.getId(), userId);
  250 +
  251 + // 断言
  252 + assertNull(projectMapper.selectById(project.getId()));
  253 + verify(workItemWorkLogService).deleteWorkItemWorkLogListByProjectId(project.getId());
  254 + verify(workItemService).deleteWorkItemListByProjectId(project.getId());
  255 + verify(iterationService).deleteIterationListByProjectId(project.getId());
  256 + verify(projectMemberService).deleteProjectMemberListByProjectId(project.getId());
  257 + }
  258 +
  259 + @Test
  260 + public void testDeleteProject_projectNotExists() {
  261 + // 准备参数
  262 + Long projectId = randomLongId();
  263 + Long userId = randomLongId();
  264 +
  265 + // 调用,并断言异常
  266 + assertServiceException(() -> projectService.deleteProject(projectId, userId), PROJECT_NOT_EXISTS);
  267 + }
  268 +
  269 + @Test
240 270 public void testDeleteProject_statusInvalid() {
241 271 // mock 数据
242 272 Long userId = randomLongId();
... ...
yudao-module-pms/src/test/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemServiceImplTest.java
... ... @@ -24,6 +24,7 @@ import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemMemberD
24 24 import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemStatusDO;
25 25 import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemUserSortDO;
26 26 import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemWorkLogDO;
  27 +import cn.iocoder.yudao.module.pms.dal.mysql.pm.project.PmsProjectMapper;
27 28 import cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem.PmsWorkItemBoardMapper;
28 29 import cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem.PmsWorkItemCommentMapper;
29 30 import cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem.PmsWorkItemMapper;
... ... @@ -39,9 +40,14 @@ import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemPriorityEnum;
39 40 import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemStatusTypeEnum;
40 41 import cn.iocoder.yudao.module.pms.enums.pm.workitem.PmsWorkItemTypeEnum;
41 42 import cn.iocoder.yudao.module.pms.service.pm.iteration.PmsIterationService;
  43 +import cn.iocoder.yudao.module.pms.service.pm.project.PmsProjectAnnouncementService;
  44 +import cn.iocoder.yudao.module.pms.service.pm.project.PmsProjectFavoriteService;
  45 +import cn.iocoder.yudao.module.pms.service.pm.project.PmsProjectGroupService;
42 46 import cn.iocoder.yudao.module.pms.service.pm.project.PmsProjectMemberService;
  47 +import cn.iocoder.yudao.module.pms.service.pm.project.PmsProjectServiceImpl;
43 48 import cn.iocoder.yudao.module.system.api.notify.NotifyMessageSendApi;
44 49 import cn.iocoder.yudao.module.system.api.notify.dto.NotifySendSingleToUserReqDTO;
  50 +import cn.iocoder.yudao.module.system.api.permission.PermissionApi;
45 51 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
46 52 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
47 53 import jakarta.annotation.Resource;
... ... @@ -88,7 +94,8 @@ import static org.mockito.Mockito.when;
88 94 *
89 95 * @author 芋道源码
90 96 */
91   -@Import({PmsWorkItemServiceImpl.class, PmsWorkItemUserSortServiceImpl.class, PmsWorkItemStatusServiceImpl.class,
  97 +@Import({PmsWorkItemServiceImpl.class, PmsWorkItemUserSortServiceImpl.class, PmsProjectServiceImpl.class,
  98 + PmsWorkItemStatusServiceImpl.class,
92 99 PmsWorkItemBoardServiceImpl.class,
93 100 PmsWorkItemCommentServiceImpl.class, PmsWorkItemWorkLogServiceImpl.class})
94 101 public class PmsWorkItemServiceImplTest extends BaseDbUnitTest {
... ... @@ -110,10 +117,18 @@ public class PmsWorkItemServiceImplTest extends BaseDbUnitTest {
110 117 private PmsWorkItemWorkLogMapper workItemWorkLogMapper;
111 118 @Resource
112 119 private PmsWorkItemUserSortMapper workItemUserSortMapper;
  120 + @Resource
  121 + private PmsProjectMapper projectMapper;
113 122  
114 123 @MockitoBean
115 124 private PmsProjectMemberService projectMemberService;
116 125 @MockitoBean
  126 + private PmsProjectGroupService projectGroupService;
  127 + @MockitoBean
  128 + private PmsProjectFavoriteService projectFavoriteService;
  129 + @MockitoBean
  130 + private PmsProjectAnnouncementService projectAnnouncementService;
  131 + @MockitoBean
117 132 private PmsIterationService iterationService;
118 133 @MockitoBean
119 134 private PmsWorkItemLabelService workItemLabelService;
... ... @@ -126,6 +141,8 @@ public class PmsWorkItemServiceImplTest extends BaseDbUnitTest {
126 141 private NotifyMessageSendApi notifyMessageSendApi;
127 142 @MockitoBean
128 143 private AdminUserApi adminUserApi;
  144 + @MockitoBean
  145 + private PermissionApi permissionApi;
129 146  
130 147 @Test
131 148 public void testCreateWorkItem_success() {
... ... @@ -510,6 +527,8 @@ public class PmsWorkItemServiceImplTest extends BaseDbUnitTest {
510 527 // mock 数据
511 528 Long userId = randomLongId();
512 529 Long projectId = randomLongId();
  530 + PmsProjectDO project = randomProjectDO(projectId, PmsProjectTypeEnum.AGILE.getType());
  531 + projectMapper.insert(project);
513 532 PmsWorkItemStatusDO taskStatus = randomStatusDO(projectId, PmsWorkItemTypeEnum.TASK.getType(),
514 533 PmsWorkItemStatusTypeEnum.PENDING.getType(), true, 1);
515 534 workItemStatusMapper.insert(taskStatus);
... ... @@ -521,7 +540,7 @@ public class PmsWorkItemServiceImplTest extends BaseDbUnitTest {
521 540 PmsWorkItemDO second = randomWorkItemDO(projectId, PmsWorkItemTypeEnum.DEFECT.getType(), defectStatus, 2);
522 541 workItemMapper.insert(second);
523 542 when(projectMemberService.validateProjectWritable(projectId, userId))
524   - .thenReturn(randomProjectDO(projectId, PmsProjectTypeEnum.AGILE.getType()));
  543 + .thenReturn(project);
525 544 // 准备参数
526 545 PmsWorkItemPlanningSortReqVO reqVO = new PmsWorkItemPlanningSortReqVO().setProjectId(projectId)
527 546 .setWorkItemIds(Arrays.asList(second.getId(), first.getId()));
... ...
yudao-module-pms/src/test/java/cn/iocoder/yudao/module/pms/service/pm/workitem/PmsWorkItemUserSortServiceImplTest.java
... ... @@ -5,8 +5,18 @@ import cn.iocoder.yudao.framework.tenant.config.TenantProperties;
5 5 import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
6 6 import cn.iocoder.yudao.framework.tenant.core.db.TenantDatabaseInterceptor;
7 7 import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
  8 +import cn.iocoder.yudao.module.pms.dal.dataobject.pm.project.PmsProjectDO;
8 9 import cn.iocoder.yudao.module.pms.dal.dataobject.pm.workitem.PmsWorkItemUserSortDO;
  10 +import cn.iocoder.yudao.module.pms.dal.mysql.pm.project.PmsProjectMapper;
9 11 import cn.iocoder.yudao.module.pms.dal.mysql.pm.workitem.PmsWorkItemUserSortMapper;
  12 +import cn.iocoder.yudao.module.pms.enums.pm.project.PmsProjectStatusEnum;
  13 +import cn.iocoder.yudao.module.pms.service.pm.iteration.PmsIterationService;
  14 +import cn.iocoder.yudao.module.pms.service.pm.project.PmsProjectAnnouncementService;
  15 +import cn.iocoder.yudao.module.pms.service.pm.project.PmsProjectFavoriteService;
  16 +import cn.iocoder.yudao.module.pms.service.pm.project.PmsProjectGroupService;
  17 +import cn.iocoder.yudao.module.pms.service.pm.project.PmsProjectMemberService;
  18 +import cn.iocoder.yudao.module.pms.service.pm.project.PmsProjectServiceImpl;
  19 +import cn.iocoder.yudao.module.system.api.permission.PermissionApi;
10 20 import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
11 21 import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
12 22 import jakarta.annotation.Resource;
... ... @@ -17,15 +27,25 @@ import org.springframework.boot.test.context.TestConfiguration;
17 27 import org.springframework.context.annotation.Bean;
18 28 import org.springframework.context.annotation.Import;
19 29 import org.springframework.jdbc.core.JdbcTemplate;
  30 +import org.springframework.test.context.bean.override.mockito.MockitoBean;
20 31  
21 32 import javax.sql.DataSource;
  33 +import java.util.ArrayList;
22 34 import java.util.Arrays;
23 35 import java.util.Collections;
24 36 import java.util.List;
  37 +import java.util.concurrent.CountDownLatch;
  38 +import java.util.concurrent.ExecutorService;
  39 +import java.util.concurrent.Executors;
  40 +import java.util.concurrent.Future;
  41 +import java.util.concurrent.TimeUnit;
25 42  
26 43 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
27 44 import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
  45 +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
28 46 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
  47 +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_NOT_EXISTS;
  48 +import static cn.iocoder.yudao.module.pms.enums.ErrorCodeConstants.PROJECT_STATUS_INVALID;
29 49 import static org.junit.jupiter.api.Assertions.assertEquals;
30 50  
31 51 /**
... ... @@ -33,18 +53,41 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
33 53 *
34 54 * @author 芋道源码
35 55 */
36   -@Import({PmsWorkItemUserSortServiceImpl.class, PmsWorkItemUserSortServiceImplTest.TenantTestConfiguration.class})
  56 +@Import({PmsWorkItemUserSortServiceImpl.class, PmsProjectServiceImpl.class,
  57 + PmsWorkItemUserSortServiceImplTest.TenantTestConfiguration.class})
37 58 public class PmsWorkItemUserSortServiceImplTest extends BaseDbUnitTest {
38 59  
39 60 @Resource
40 61 private PmsWorkItemUserSortServiceImpl workItemUserSortService;
  62 +
41 63 @Resource
42 64 private PmsWorkItemUserSortMapper workItemUserSortMapper;
43 65 @Resource
  66 + private PmsProjectMapper projectMapper;
  67 + @Resource
44 68 private JdbcTemplate jdbcTemplate;
45 69 @Resource
46 70 private TenantLineInnerInterceptor tenantLineInnerInterceptor;
47 71  
  72 + @MockitoBean
  73 + private PmsProjectMemberService projectMemberService;
  74 + @MockitoBean
  75 + private PmsProjectGroupService projectGroupService;
  76 + @MockitoBean
  77 + private PmsProjectFavoriteService projectFavoriteService;
  78 + @MockitoBean
  79 + private PmsProjectAnnouncementService projectAnnouncementService;
  80 + @MockitoBean
  81 + private PmsIterationService iterationService;
  82 + @MockitoBean
  83 + private PmsWorkItemService workItemService;
  84 + @MockitoBean
  85 + private PmsWorkItemStatusService workItemStatusService;
  86 + @MockitoBean
  87 + private PmsWorkItemWorkLogService workItemWorkLogService;
  88 + @MockitoBean
  89 + private PermissionApi permissionApi;
  90 +
48 91 @BeforeEach
49 92 public void before() {
50 93 TenantContextHolder.setTenantId(0L);
... ... @@ -56,9 +99,33 @@ public class PmsWorkItemUserSortServiceImplTest extends BaseDbUnitTest {
56 99 }
57 100  
58 101 @Test
  102 + public void testUpdateWorkItemUserSort_success() {
  103 + // mock 数据
  104 + PmsProjectDO project = new PmsProjectDO().setName("测试项目").setType(1).setLevel(1);
  105 + projectMapper.insert(project);
  106 + workItemUserSortService.updateWorkItemUserSort(project.getId(), List.of(50L, 51L), 238L);
  107 + workItemUserSortService.updateWorkItemUserSort(project.getId(), List.of(50L, 51L), 239L);
  108 +
  109 + // 调用:连续调整个人排序,并移除不再位于待规划区域的工作项
  110 + workItemUserSortService.updateWorkItemUserSort(project.getId(), List.of(51L, 50L), 238L);
  111 + workItemUserSortService.updateWorkItemUserSort(project.getId(), List.of(50L), 238L);
  112 +
  113 + // 断言:仅保留当前顺序,不影响其他用户的个人排序
  114 + List<PmsWorkItemUserSortDO> userSorts = workItemUserSortMapper
  115 + .selectListByProjectIdAndUserId(project.getId(), 238L);
  116 + assertEquals(List.of(50L), convertList(userSorts, PmsWorkItemUserSortDO::getWorkItemId));
  117 + assertEquals(1, userSorts.get(0).getSort());
  118 + assertEquals(List.of(50L, 51L), convertList(workItemUserSortMapper
  119 + .selectListByProjectIdAndUserId(project.getId(), 239L), PmsWorkItemUserSortDO::getWorkItemId));
  120 + }
  121 +
  122 + @Test
59 123 public void testUpdateWorkItemUserSort_repeated() {
  124 + // mock 数据
  125 + PmsProjectDO project = new PmsProjectDO().setName("测试项目").setType(1).setLevel(1);
  126 + projectMapper.insert(project);
60 127 // 准备参数
61   - Long projectId = randomLongId();
  128 + Long projectId = project.getId();
62 129 Long userId = randomLongId();
63 130 Long firstId = randomLongId();
64 131 Long secondId = randomLongId();
... ... @@ -78,7 +145,9 @@ public class PmsWorkItemUserSortServiceImplTest extends BaseDbUnitTest {
78 145 @Test
79 146 public void testUpdateWorkItemUserSort_deletedRecords() {
80 147 // mock 数据
81   - Long projectId = randomLongId();
  148 + PmsProjectDO project = new PmsProjectDO().setName("测试项目").setType(1).setLevel(1);
  149 + projectMapper.insert(project);
  150 + Long projectId = project.getId();
82 151 Long userId = randomLongId();
83 152 Long workItemId = randomLongId();
84 153 insertUserSort(projectId, workItemId, userId, false);
... ... @@ -99,7 +168,9 @@ public class PmsWorkItemUserSortServiceImplTest extends BaseDbUnitTest {
99 168 @Test
100 169 public void testUpdateWorkItemUserSort_empty() {
101 170 // mock 数据
102   - Long projectId = randomLongId();
  171 + PmsProjectDO project = new PmsProjectDO().setName("测试项目").setType(1).setLevel(1);
  172 + projectMapper.insert(project);
  173 + Long projectId = project.getId();
103 174 Long userId = randomLongId();
104 175 Long workItemId = randomLongId();
105 176 insertUserSort(projectId, workItemId, userId, false);
... ... @@ -113,6 +184,87 @@ public class PmsWorkItemUserSortServiceImplTest extends BaseDbUnitTest {
113 184 }
114 185  
115 186 @Test
  187 + public void testUpdateWorkItemUserSort_projectNotExists() {
  188 + // 调用,并断言异常
  189 + assertServiceException(() -> workItemUserSortService.updateWorkItemUserSort(999L,
  190 + List.of(50L), 238L), PROJECT_NOT_EXISTS);
  191 + assertEquals(0L, workItemUserSortMapper.selectCount());
  192 + }
  193 +
  194 + @Test
  195 + public void testUpdateWorkItemUserSort_projectDeleted() {
  196 + // mock 数据
  197 + PmsProjectDO project = new PmsProjectDO().setName("测试项目").setType(1).setLevel(1);
  198 + projectMapper.insert(project);
  199 + projectMapper.deleteById(project.getId());
  200 +
  201 + // 调用,并断言异常
  202 + assertServiceException(() -> workItemUserSortService.updateWorkItemUserSort(project.getId(),
  203 + List.of(50L), 238L), PROJECT_NOT_EXISTS);
  204 + assertEquals(0L, workItemUserSortMapper.selectCount());
  205 + }
  206 +
  207 + @Test
  208 + public void testUpdateWorkItemUserSort_projectStatusInvalid() {
  209 + // mock 数据
  210 + PmsProjectDO project = new PmsProjectDO().setName("测试项目").setType(1).setLevel(1)
  211 + .setStatus(PmsProjectStatusEnum.RECYCLED.getStatus());
  212 + projectMapper.insert(project);
  213 + workItemUserSortMapper.insert(new PmsWorkItemUserSortDO().setProjectId(project.getId())
  214 + .setWorkItemId(50L).setUserId(238L).setSort(1));
  215 +
  216 + // 调用,并断言异常
  217 + assertServiceException(() -> workItemUserSortService.updateWorkItemUserSort(project.getId(),
  218 + List.of(51L), 238L), PROJECT_STATUS_INVALID);
  219 + // 断言:拒绝更新时保留原有个人排序
  220 + assertEquals(List.of(50L), convertList(workItemUserSortMapper
  221 + .selectListByProjectIdAndUserId(project.getId(), 238L), PmsWorkItemUserSortDO::getWorkItemId));
  222 + }
  223 +
  224 + @Test
  225 + public void testUpdateWorkItemUserSort_concurrent() throws Exception {
  226 + // mock 数据
  227 + PmsProjectDO project = new PmsProjectDO().setName("测试项目").setType(1).setLevel(1);
  228 + projectMapper.insert(project);
  229 + // 准备参数
  230 + CountDownLatch startLatch = new CountDownLatch(1);
  231 + ExecutorService executor = Executors.newFixedThreadPool(4);
  232 + List<Future<?>> futures = new ArrayList<>();
  233 +
  234 + // 调用:同一项目、同一用户并发保存个人排序
  235 + try {
  236 + for (int index = 0; index < 4; index++) {
  237 + futures.add(executor.submit(() -> {
  238 + // 工作线程使用与测试线程相同的租户,结束后清理,避免租户上下文残留
  239 + TenantContextHolder.setTenantId(0L);
  240 + try {
  241 + startLatch.await();
  242 + for (int count = 0; count < 5; count++) {
  243 + workItemUserSortService.updateWorkItemUserSort(project.getId(), List.of(51L, 50L), 238L);
  244 + }
  245 + return null;
  246 + } finally {
  247 + TenantContextHolder.clear();
  248 + }
  249 + }));
  250 + }
  251 + startLatch.countDown();
  252 + for (Future<?> future : futures) {
  253 + future.get(20, TimeUnit.SECONDS);
  254 + }
  255 + } finally {
  256 + executor.shutdownNow();
  257 + executor.awaitTermination(10, TimeUnit.SECONDS);
  258 + }
  259 +
  260 + // 断言:没有重复的有效记录,顺序与最后一次保存一致
  261 + List<PmsWorkItemUserSortDO> userSorts = workItemUserSortMapper
  262 + .selectListByProjectIdAndUserId(project.getId(), 238L);
  263 + assertEquals(List.of(51L, 50L), convertList(userSorts, PmsWorkItemUserSortDO::getWorkItemId));
  264 + assertEquals(List.of(1, 2), convertList(userSorts, PmsWorkItemUserSortDO::getSort));
  265 + }
  266 +
  267 + @Test
116 268 public void testDeleteWorkItemUserSortByWorkItemId_success() {
117 269 // mock 数据
118 270 Long projectId = randomLongId();
... ... @@ -151,7 +303,9 @@ public class PmsWorkItemUserSortServiceImplTest extends BaseDbUnitTest {
151 303 @Test
152 304 public void testDeleteWorkItemUserSort_tenantIsolation() {
153 305 // mock 数据:两个租户使用相同的业务编号
154   - Long projectId = randomLongId();
  306 + PmsProjectDO project = new PmsProjectDO().setName("测试项目").setType(1).setLevel(1);
  307 + projectMapper.insert(project);
  308 + Long projectId = project.getId();
155 309 Long userId = randomLongId();
156 310 Long workItemId = randomLongId();
157 311 insertUserSort(projectId, workItemId, userId, false);
... ...
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostRespVO.java
... ... @@ -3,10 +3,10 @@ package cn.iocoder.yudao.module.system.controller.admin.dept.vo.post;
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.module.system.enums.DictTypeConstants;
6   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
7   -import cn.idev.excel.annotation.ExcelProperty;
8 6 import io.swagger.v3.oas.annotations.media.Schema;
9 7 import lombok.Data;
  8 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  9 +import org.apache.fesod.sheet.annotation.ExcelProperty;
10 10  
11 11 import java.time.LocalDateTime;
12 12  
... ...
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostSimpleRespVO.java
1 1 package cn.iocoder.yudao.module.system.controller.admin.dept.vo.post;
2 2  
3   -import cn.idev.excel.annotation.ExcelProperty;
4 3 import io.swagger.v3.oas.annotations.media.Schema;
5 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelProperty;
6 6  
7 7 @Schema(description = "管理后台 - 岗位信息的精简 Response VO")
8 8 @Data
... ...
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/data/DictDataRespVO.java
... ... @@ -3,10 +3,10 @@ package cn.iocoder.yudao.module.system.controller.admin.dict.vo.data;
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.module.system.enums.DictTypeConstants;
6   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
7   -import cn.idev.excel.annotation.ExcelProperty;
8 6 import io.swagger.v3.oas.annotations.media.Schema;
9 7 import lombok.Data;
  8 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  9 +import org.apache.fesod.sheet.annotation.ExcelProperty;
10 10  
11 11 import java.time.LocalDateTime;
12 12  
... ...
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/type/DictTypeRespVO.java
... ... @@ -3,10 +3,10 @@ package cn.iocoder.yudao.module.system.controller.admin.dict.vo.type;
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.module.system.enums.DictTypeConstants;
6   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
7   -import cn.idev.excel.annotation.ExcelProperty;
8 6 import io.swagger.v3.oas.annotations.media.Schema;
9 7 import lombok.Data;
  8 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  9 +import org.apache.fesod.sheet.annotation.ExcelProperty;
10 10  
11 11 import java.time.LocalDateTime;
12 12  
... ...
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/loginlog/LoginLogRespVO.java
... ... @@ -3,10 +3,10 @@ package cn.iocoder.yudao.module.system.controller.admin.logger.vo.loginlog;
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.module.system.enums.DictTypeConstants;
6   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
7   -import cn.idev.excel.annotation.ExcelProperty;
8 6 import io.swagger.v3.oas.annotations.media.Schema;
9 7 import lombok.Data;
  8 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  9 +import org.apache.fesod.sheet.annotation.ExcelProperty;
10 10  
11 11 import java.time.LocalDateTime;
12 12  
... ...
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/operatelog/OperateLogRespVO.java
... ... @@ -2,9 +2,9 @@ package cn.iocoder.yudao.module.system.controller.admin.logger.vo.operatelog;
2 2  
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
5   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
6   -import cn.idev.excel.annotation.ExcelProperty;
7 5 import cn.iocoder.yudao.module.system.enums.DictTypeConstants;
  6 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  7 +import org.apache.fesod.sheet.annotation.ExcelProperty;
8 8 import org.dromara.core.trans.anno.Trans;
9 9 import org.dromara.core.trans.constant.TransType;
10 10 import org.dromara.core.trans.vo.VO;
... ...
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleRespVO.java
... ... @@ -3,11 +3,11 @@ package cn.iocoder.yudao.module.system.controller.admin.permission.vo.role;
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.module.system.enums.DictTypeConstants;
6   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
7   -import cn.idev.excel.annotation.ExcelProperty;
8 6 import io.swagger.v3.oas.annotations.media.Schema;
9 7 import jakarta.validation.constraints.NotBlank;
10 8 import lombok.Data;
  9 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  10 +import org.apache.fesod.sheet.annotation.ExcelProperty;
11 11  
12 12 import java.time.LocalDateTime;
13 13 import java.util.Set;
... ...
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/log/SmsLogRespVO.java
... ... @@ -4,10 +4,10 @@ import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.framework.excel.core.convert.JsonConvert;
6 6 import cn.iocoder.yudao.module.system.enums.DictTypeConstants;
7   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
8   -import cn.idev.excel.annotation.ExcelProperty;
9 7 import io.swagger.v3.oas.annotations.media.Schema;
10 8 import lombok.Data;
  9 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  10 +import org.apache.fesod.sheet.annotation.ExcelProperty;
11 11  
12 12 import java.time.LocalDateTime;
13 13 import java.util.Map;
... ...
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/template/SmsTemplateRespVO.java
... ... @@ -3,10 +3,10 @@ package cn.iocoder.yudao.module.system.controller.admin.sms.vo.template;
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.module.system.enums.DictTypeConstants;
6   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
7   -import cn.idev.excel.annotation.ExcelProperty;
8 6 import io.swagger.v3.oas.annotations.media.Schema;
9 7 import lombok.Data;
  8 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  9 +import org.apache.fesod.sheet.annotation.ExcelProperty;
10 10  
11 11 import java.time.LocalDateTime;
12 12 import java.util.List;
... ...
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/tenant/TenantRespVO.java
1 1 package cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
6 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
7 5 import cn.iocoder.yudao.module.system.enums.DictTypeConstants;
8 6 import io.swagger.v3.oas.annotations.media.Schema;
9 7 import lombok.Data;
  8 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  9 +import org.apache.fesod.sheet.annotation.ExcelProperty;
10 10  
11 11 import java.time.LocalDateTime;
12 12 import java.util.List;
... ...
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserImportExcelVO.java
1 1 package cn.iocoder.yudao.module.system.controller.admin.user.vo.user;
2 2  
3   -import cn.idev.excel.annotation.ExcelProperty;
4 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
5 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
6 5 import cn.iocoder.yudao.module.system.enums.DictTypeConstants;
... ... @@ -8,6 +7,7 @@ import lombok.AllArgsConstructor;
8 7 import lombok.Builder;
9 8 import lombok.Data;
10 9 import lombok.NoArgsConstructor;
  10 +import org.apache.fesod.sheet.annotation.ExcelProperty;
11 11  
12 12 /**
13 13 * 用户 Excel 导入 VO
... ...
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserRespVO.java
... ... @@ -3,10 +3,10 @@ package cn.iocoder.yudao.module.system.controller.admin.user.vo.user;
3 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
4 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
5 5 import cn.iocoder.yudao.module.system.enums.DictTypeConstants;
6   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
7   -import cn.idev.excel.annotation.ExcelProperty;
8 6 import io.swagger.v3.oas.annotations.media.Schema;
9 7 import lombok.Data;
  8 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  9 +import org.apache.fesod.sheet.annotation.ExcelProperty;
10 10  
11 11 import java.time.LocalDateTime;
12 12 import java.util.Set;
... ...
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/notify/NotifySendServiceImpl.java
... ... @@ -63,7 +63,7 @@ public class NotifySendServiceImpl implements NotifySendService {
63 63 NotifyTemplateDO template = notifyTemplateService.getNotifyTemplateByCodeFromCache(templateCode);
64 64 // 站内信模板不存在
65 65 if (template == null) {
66   - throw exception(NOTICE_NOT_FOUND);
  66 + throw exception(NOTIFY_TEMPLATE_NOT_EXISTS);
67 67 }
68 68 return template;
69 69 }
... ...
yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/notify/NotifySendServiceImplTest.java
... ... @@ -17,8 +17,8 @@ import java.util.Map;
17 17 import static cn.hutool.core.util.RandomUtil.randomEle;
18 18 import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
19 19 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
20   -import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.NOTICE_NOT_FOUND;
21 20 import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.NOTIFY_SEND_TEMPLATE_PARAM_MISS;
  21 +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.NOTIFY_TEMPLATE_NOT_EXISTS;
22 22 import static org.junit.jupiter.api.Assertions.assertEquals;
23 23 import static org.junit.jupiter.api.Assertions.assertNull;
24 24 import static org.mockito.ArgumentMatchers.eq;
... ... @@ -150,14 +150,32 @@ class NotifySendServiceImplTest extends BaseMockitoUnitTest {
150 150 }
151 151  
152 152 @Test
153   - public void testCheckMailTemplateValid_notExists() {
  153 + public void testSendSingleNotifyToAdmin_templateNotExists() {
  154 + // 准备参数
  155 + Long userId = randomLongId();
  156 + String templateCode = randomString();
  157 + Map<String, Object> templateParams = MapUtil.<String, Object>builder().put("code", "1234")
  158 + .put("op", "login").build();
  159 + // mock NotifyTemplateService 的方法
  160 + when(notifyTemplateService.getNotifyTemplateByCodeFromCache(eq(templateCode))).thenReturn(null);
  161 +
  162 + // 调用,并断言异常
  163 + assertServiceException(() -> notifySendService.sendSingleNotifyToAdmin(userId, templateCode, templateParams),
  164 + NOTIFY_TEMPLATE_NOT_EXISTS);
  165 + // 断言
  166 + verify(notifyTemplateService, never()).formatNotifyTemplateContent(anyString(), anyMap());
  167 + verify(notifyMessageService, never()).createNotifyMessage(anyLong(), anyInt(), any(), anyString(), anyMap());
  168 + }
  169 +
  170 + @Test
  171 + public void testValidateNotifyTemplate_notExists() {
154 172 // 准备参数
155 173 String templateCode = randomString();
156 174 // mock 方法
157 175  
158 176 // 调用,并断言异常
159 177 assertServiceException(() -> notifySendService.validateNotifyTemplate(templateCode),
160   - NOTICE_NOT_FOUND);
  178 + NOTIFY_TEMPLATE_NOT_EXISTS);
161 179 }
162 180  
163 181 @Test
... ...
yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/brand/WmsItemBrandRespVO.java
1 1 package cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.brand;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  6 +import org.apache.fesod.sheet.annotation.ExcelProperty;
7 7  
8 8 import java.time.LocalDateTime;
9 9  
... ...
yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/item/vo/item/WmsItemRespVO.java
1 1 package cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.item;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.sku.WmsItemSkuRespVO;
6 4 import io.swagger.v3.oas.annotations.media.Schema;
7 5 import lombok.Data;
  6 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  7 +import org.apache.fesod.sheet.annotation.ExcelProperty;
8 8  
9 9 import java.time.LocalDateTime;
10 10 import java.util.List;
... ...
yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/merchant/vo/WmsMerchantRespVO.java
1 1 package cn.iocoder.yudao.module.wms.controller.admin.md.merchant.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
6 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
7 5 import cn.iocoder.yudao.module.wms.enums.DictTypeConstants;
8 6 import io.swagger.v3.oas.annotations.media.Schema;
9 7 import lombok.Data;
  8 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  9 +import org.apache.fesod.sheet.annotation.ExcelProperty;
10 10  
11 11 import java.time.LocalDateTime;
12 12  
... ...
yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/md/warehouse/vo/WmsWarehouseRespVO.java
1 1 package cn.iocoder.yudao.module.wms.controller.admin.md.warehouse.vo;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.Data;
  5 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  6 +import org.apache.fesod.sheet.annotation.ExcelProperty;
7 7  
8 8 import java.time.LocalDateTime;
9 9  
... ...
yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/check/vo/order/WmsCheckOrderRespVO.java
1 1 package cn.iocoder.yudao.module.wms.controller.admin.order.check.vo.order;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
6 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
7 5 import cn.iocoder.yudao.module.wms.controller.admin.order.check.vo.detail.WmsCheckOrderDetailRespVO;
8 6 import cn.iocoder.yudao.module.wms.enums.DictTypeConstants;
9 7 import io.swagger.v3.oas.annotations.media.Schema;
10 8 import lombok.Data;
  9 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  10 +import org.apache.fesod.sheet.annotation.ExcelProperty;
11 11  
12 12 import java.math.BigDecimal;
13 13 import java.time.LocalDateTime;
... ...
yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/movement/vo/order/WmsMovementOrderRespVO.java
1 1 package cn.iocoder.yudao.module.wms.controller.admin.order.movement.vo.order;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
6 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
7 5 import cn.iocoder.yudao.module.wms.controller.admin.order.movement.vo.detail.WmsMovementOrderDetailRespVO;
8 6 import cn.iocoder.yudao.module.wms.enums.DictTypeConstants;
9 7 import io.swagger.v3.oas.annotations.media.Schema;
10 8 import lombok.Data;
  9 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  10 +import org.apache.fesod.sheet.annotation.ExcelProperty;
11 11  
12 12 import java.math.BigDecimal;
13 13 import java.time.LocalDateTime;
... ...
yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/receipt/vo/order/WmsReceiptOrderRespVO.java
1 1 package cn.iocoder.yudao.module.wms.controller.admin.order.receipt.vo.order;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
6 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
7 5 import cn.iocoder.yudao.module.wms.controller.admin.order.receipt.vo.detail.WmsReceiptOrderDetailRespVO;
8 6 import cn.iocoder.yudao.module.wms.enums.DictTypeConstants;
9 7 import io.swagger.v3.oas.annotations.media.Schema;
10 8 import lombok.Data;
  9 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  10 +import org.apache.fesod.sheet.annotation.ExcelProperty;
11 11  
12 12 import java.math.BigDecimal;
13 13 import java.time.LocalDateTime;
... ...
yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/controller/admin/order/shipment/vo/order/WmsShipmentOrderRespVO.java
1 1 package cn.iocoder.yudao.module.wms.controller.admin.order.shipment.vo.order;
2 2  
3   -import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4   -import cn.idev.excel.annotation.ExcelProperty;
5 3 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
6 4 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
7 5 import cn.iocoder.yudao.module.wms.controller.admin.order.shipment.vo.detail.WmsShipmentOrderDetailRespVO;
8 6 import cn.iocoder.yudao.module.wms.enums.DictTypeConstants;
9 7 import io.swagger.v3.oas.annotations.media.Schema;
10 8 import lombok.Data;
  9 +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
  10 +import org.apache.fesod.sheet.annotation.ExcelProperty;
11 11  
12 12 import java.math.BigDecimal;
13 13 import java.time.LocalDateTime;
... ...
yudao-module-wms/src/main/resources/mapper/home/WmsHomeStatisticsMapper.xml
... ... @@ -3,7 +3,7 @@
3 3 <mapper namespace="cn.iocoder.yudao.module.wms.dal.mysql.home.WmsHomeStatisticsMapper">
4 4  
5 5 <!-- 按单据类型和状态统计单据数量 -->
6   - <select id="selectOrderCountGroupByTypeAndStatus" resultType="java.util.Map">
  6 + <select id="selectOrderCountGroupByTypeAndStatus" resultType="cn.iocoder.yudao.framework.mybatis.core.util.CaseInsensitiveResultMap">
7 7 SELECT orderType, status, SUM(count) AS count
8 8 FROM (
9 9 SELECT ${@cn.iocoder.yudao.module.wms.enums.order.WmsOrderTypeEnum@RECEIPT.type} AS orderType,
... ... @@ -51,7 +51,7 @@
51 51 </select>
52 52  
53 53 <!-- 按天聚合单据数量 -->
54   - <select id="selectDailyOrderTrend" resultType="java.util.Map">
  54 + <select id="selectDailyOrderTrend" resultType="cn.iocoder.yudao.framework.mybatis.core.util.CaseInsensitiveResultMap">
55 55 SELECT date, orderType, SUM(count) AS count
56 56 FROM (
57 57 SELECT DATE_FORMAT(order_time, '%Y-%m-%d') AS date,
... ... @@ -117,7 +117,7 @@
117 117 </select>
118 118  
119 119 <!-- 按商品统计库存数量排行 -->
120   - <select id="selectInventoryItemRank" resultType="java.util.Map">
  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 123 IFNULL(SUM(inventory.quantity), 0) AS quantity
... ... @@ -135,7 +135,7 @@
135 135 </select>
136 136  
137 137 <!-- 按仓库统计库存数量排行 -->
138   - <select id="selectInventoryWarehouseRank" resultType="java.util.Map">
  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 141 IFNULL(SUM(inventory.quantity), 0) AS quantity
... ...
yudao-server/src/main/resources/application-dev.yaml
... ... @@ -55,6 +55,24 @@ spring:
55 55 url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例
56 56 username: root
57 57 password: 123456
  58 +# tdengine: # IoT 数据库(需要 IoT 物联网,且 yudao.iot.storage.type 为 tdengine 再开启噢!)
  59 +# lazy: true # 开启懒加载,保证启动速度
  60 +# url: jdbc:TAOS-WS://127.0.0.1:6041/ruoyi_vue_pro?varcharAsString=true&enableAutoReconnect=true # 参见:https://t.zsxq.com/qaX9c,开启 TDengine WebSocket 自动重连
  61 +# driver-class-name: com.taosdata.jdbc.ws.WebSocketDriver
  62 +# username: root
  63 +# password: taosdata
  64 +# druid:
  65 +# validation-query: SELECT SERVER_STATUS() # TDengine 数据源的有效性检查 SQL
  66 +# iotdb: # IoT 数据库(需要 IoT 物联网,且 yudao.iot.storage.type 为 iotdb 再开启噢!)
  67 +# lazy: true # 开启懒加载,保证启动速度
  68 +# type: com.zaxxer.hikari.HikariDataSource # IoTDB 不使用 Druid 连接池
  69 +# url: jdbc:iotdb://127.0.0.1:6667/ruoyi_vue_pro?sql_dialect=table # 表模型;数据库需要预先创建,且时间精度为 ms
  70 +# driver-class-name: org.apache.iotdb.jdbc.IoTDBDriver
  71 +# username: root
  72 +# password: root
  73 +# hikari:
  74 +# maximum-pool-size: 8
  75 +# connection-test-query: SELECT 1 # IoTDB 数据源的有效性检查 SQL
58 76  
59 77 # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
60 78 data:
... ...
yudao-server/src/main/resources/application-local.yaml
... ... @@ -71,7 +71,7 @@ spring:
71 71 url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true&nullCatalogMeansCurrent=true
72 72 username: root
73 73 password: 123456
74   -# tdengine: # IoT 数据库(需要 IoT 物联网再开启噢!)
  74 +# tdengine: # IoT 数据库(需要 IoT 物联网,且 yudao.iot.storage.type 为 tdengine 再开启噢!)
75 75 # lazy: true # 开启懒加载,保证启动速度
76 76 # url: jdbc:TAOS-WS://127.0.0.1:6041/ruoyi_vue_pro?varcharAsString=true&enableAutoReconnect=true # 参见:https://t.zsxq.com/qaX9c,开启 TDengine WebSocket 自动重连
77 77 # driver-class-name: com.taosdata.jdbc.ws.WebSocketDriver
... ... @@ -79,6 +79,16 @@ spring:
79 79 # password: taosdata
80 80 # druid:
81 81 # validation-query: SELECT SERVER_STATUS() # TDengine 数据源的有效性检查 SQL
  82 +# iotdb: # IoT 数据库(需要 IoT 物联网,且 yudao.iot.storage.type 为 iotdb 再开启噢!)
  83 +# lazy: true # 开启懒加载,保证启动速度
  84 +# type: com.zaxxer.hikari.HikariDataSource # IoTDB 不使用 Druid 连接池
  85 +# url: jdbc:iotdb://127.0.0.1:6667/ruoyi_vue_pro?sql_dialect=table # 表模型;数据库需要预先创建,且时间精度为 ms
  86 +# driver-class-name: org.apache.iotdb.jdbc.IoTDBDriver
  87 +# username: root
  88 +# password: root
  89 +# hikari:
  90 +# maximum-pool-size: 8
  91 +# connection-test-query: SELECT 1 # IoTDB 数据源的有效性检查 SQL
82 92  
83 93 # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
84 94 data:
... ...
yudao-server/src/main/resources/application.yaml
... ... @@ -368,5 +368,7 @@ yudao:
368 368 iot:
369 369 message-bus:
370 370 type: redis # 消息总线的类型
  371 + storage:
  372 + type: tdengine # 时序存储的类型:tdengine、iotdb,需要配置同名的 spring.datasource.dynamic.datasource 数据源
371 373  
372 374 debug: false
... ...