From ce2a7d2c96df8f41ffc1557bea00bd4bbd34d09e Mon Sep 17 00:00:00 2001 From: Ken Date: Thu, 22 May 2025 13:30:27 +0800 Subject: [PATCH 01/89] =?UTF-8?q?feat(dict):=20=E5=AE=9E=E7=8E=B0=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E6=95=B0=E6=8D=AE=E5=AD=97=E5=85=B8=E6=A0=A1=E9=AA=8C?= =?UTF-8?q?=E6=B3=A8=E8=A7=A3=20@InDict=EF=BC=88#785=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增注解 @InDict,用于在参数校验阶段判断传入值是否存在于指定的数据字典中。 BREAKING CHANGE: 无 Closes #785 --- .../yudao-spring-boot-starter-excel/pom.xml | 6 +++ .../dict/core/DictFrameworkUtils.java | 5 +++ .../framework/dict/validation/InDict.java | 38 ++++++++++++++++++ .../validation/InDictCollectionValidator.java | 40 +++++++++++++++++++ .../dict/validation/InDictValidator.java | 40 +++++++++++++++++++ 5 files changed, 129 insertions(+) create mode 100644 yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/validation/InDict.java create mode 100644 yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/validation/InDictCollectionValidator.java create mode 100644 yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/validation/InDictValidator.java diff --git a/yudao-framework/yudao-spring-boot-starter-excel/pom.xml b/yudao-framework/yudao-spring-boot-starter-excel/pom.xml index 0413986a64..0316c62e50 100644 --- a/yudao-framework/yudao-spring-boot-starter-excel/pom.xml +++ b/yudao-framework/yudao-spring-boot-starter-excel/pom.xml @@ -46,6 +46,12 @@ easyexcel + + jakarta.validation + jakarta.validation-api + provided + + com.google.guava guava diff --git a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/core/DictFrameworkUtils.java b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/core/DictFrameworkUtils.java index 9fc67bfe7f..2a2350f02b 100644 --- a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/core/DictFrameworkUtils.java +++ b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/core/DictFrameworkUtils.java @@ -76,4 +76,9 @@ public class DictFrameworkUtils { return dictData!= null ? dictData.getValue(): null; } + @SneakyThrows + public static List getDictDataValueList(String dictType) { + List dictDatas = GET_DICT_DATA_CACHE.get(dictType); + return convertList(dictDatas, DictDataRespDTO::getValue); + } } diff --git a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/validation/InDict.java b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/validation/InDict.java new file mode 100644 index 0000000000..dab0af782f --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/validation/InDict.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.framework.dict.validation; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ + ElementType.METHOD, + ElementType.FIELD, + ElementType.ANNOTATION_TYPE, + ElementType.CONSTRUCTOR, + ElementType.PARAMETER, + ElementType.TYPE_USE +}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Constraint( + validatedBy = {InDictValidator.class, InDictCollectionValidator.class} +) +public @interface InDict { + + /** + * 数据字典type + */ + String type(); + + String message() default "必须在指定范围 {value}"; + + Class[] groups() default {}; + + Class[] payload() default {}; + +} diff --git a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/validation/InDictCollectionValidator.java b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/validation/InDictCollectionValidator.java new file mode 100644 index 0000000000..ab036171e2 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/validation/InDictCollectionValidator.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.framework.dict.validation; + +import cn.iocoder.yudao.framework.common.util.string.StrUtils; +import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +import java.util.Collection; +import java.util.List; + +public class InDictCollectionValidator implements ConstraintValidator> { + + private String dictType; + + @Override + public void initialize(InDict annotation) { + this.dictType = annotation.type(); + } + + @Override + public boolean isValid(Collection list, ConstraintValidatorContext context) { + if (list == null) { + return true; + } + List dbValues = DictFrameworkUtils.getDictDataValueList(dictType); + boolean match = list.stream().allMatch(v -> dbValues.stream() + .anyMatch(dbValue -> dbValue.equalsIgnoreCase(v.toString()))); + if (match) { + return true; + } + // 校验不通过,自定义提示语句 + context.disableDefaultConstraintViolation(); // 禁用默认的 message 的值 + context.buildConstraintViolationWithTemplate( + context.getDefaultConstraintMessageTemplate().replaceAll("\\{value}",dbValues.toString()) + ).addConstraintViolation(); // 重新添加错误提示语句 + return false; + } + +} + diff --git a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/validation/InDictValidator.java b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/validation/InDictValidator.java new file mode 100644 index 0000000000..05f50cb839 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/validation/InDictValidator.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.framework.dict.validation; + +import cn.iocoder.yudao.framework.common.util.string.StrUtils; +import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +import java.util.List; + +public class InDictValidator implements ConstraintValidator { + + private String dictType; + + @Override + public void initialize(InDict annotation) { + this.dictType = annotation.type(); + } + + @Override + public boolean isValid(Object value, ConstraintValidatorContext context) { + // 为空时,默认不校验,即认为通过 + if (value == null) { + return true; + } + // 校验通过 + final List values = DictFrameworkUtils.getDictDataValueList(dictType); + boolean match = values.stream().anyMatch(v -> v.equalsIgnoreCase(value.toString())); + if (match) { + return true; + } + // 校验不通过,自定义提示语句 + context.disableDefaultConstraintViolation(); // 禁用默认的 message 的值 + context.buildConstraintViolationWithTemplate( + context.getDefaultConstraintMessageTemplate().replaceAll("\\{value}", values.toString()) + ).addConstraintViolation(); // 重新添加错误提示语句 + return false; + } + +} + From 3b2a3dd0ea75b4288b615436af0c1c98f32e1118 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8A=8B=E9=81=93=E6=BA=90=E7=A0=81?= Date: Mon, 9 Jun 2025 06:36:11 +0000 Subject: [PATCH 02/89] =?UTF-8?q?fix:=20ERP=E7=BB=9F=E8=AE=A1=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=E5=9C=A8=E5=A4=9A=E7=A7=9F=E6=88=B7=E5=85=B3=E9=97=AD?= =?UTF-8?q?=E6=97=B6=E7=9A=84NullPointerException=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复ErpSaleStatisticsMapper.xml中硬编码使用getRequiredTenantId()导致的空指针异常 - 修复ErpPurchaseStatisticsMapper.xml中硬编码使用getRequiredTenantId()导致的空指针异常 - 使用条件判断getTenantId() != null来决定是否添加租户条件 - 添加单元测试验证多租户开启和关闭时的统计查询功能 - 确保向后兼容,多租户开启时正常工作,关闭时不报错 --- .../ErpPurchaseStatisticsMapper.xml | 8 +- .../statistics/ErpSaleStatisticsMapper.xml | 8 +- .../statistics/ErpStatisticsServiceTest.java | 155 ++++++++++++++++++ 3 files changed, 167 insertions(+), 4 deletions(-) create mode 100644 yudao-module-erp/src/test/java/cn/iocoder/yudao/module/erp/service/statistics/ErpStatisticsServiceTest.java diff --git a/yudao-module-erp/src/main/resources/mapper/statistics/ErpPurchaseStatisticsMapper.xml b/yudao-module-erp/src/main/resources/mapper/statistics/ErpPurchaseStatisticsMapper.xml index 699286b5da..a88fb3a88c 100644 --- a/yudao-module-erp/src/main/resources/mapper/statistics/ErpPurchaseStatisticsMapper.xml +++ b/yudao-module-erp/src/main/resources/mapper/statistics/ErpPurchaseStatisticsMapper.xml @@ -10,7 +10,9 @@ AND in_time < #{endTime} - AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getRequiredTenantId()} + + AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId()} + AND deleted = 0) - (SELECT IFNULL(SUM(total_price), 0) FROM erp_purchase_return @@ -18,7 +20,9 @@ AND return_time < #{endTime} - AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getRequiredTenantId()} + + AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId()} + AND deleted = 0) diff --git a/yudao-module-erp/src/main/resources/mapper/statistics/ErpSaleStatisticsMapper.xml b/yudao-module-erp/src/main/resources/mapper/statistics/ErpSaleStatisticsMapper.xml index 324cbd432b..3e04ee706e 100644 --- a/yudao-module-erp/src/main/resources/mapper/statistics/ErpSaleStatisticsMapper.xml +++ b/yudao-module-erp/src/main/resources/mapper/statistics/ErpSaleStatisticsMapper.xml @@ -10,7 +10,9 @@ AND out_time < #{endTime} - AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getRequiredTenantId()} + + AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId()} + AND deleted = 0) - (SELECT IFNULL(SUM(total_price), 0) FROM erp_sale_return @@ -18,7 +20,9 @@ AND return_time < #{endTime} - AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getRequiredTenantId()} + + AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId()} + AND deleted = 0) diff --git a/yudao-module-erp/src/test/java/cn/iocoder/yudao/module/erp/service/statistics/ErpStatisticsServiceTest.java b/yudao-module-erp/src/test/java/cn/iocoder/yudao/module/erp/service/statistics/ErpStatisticsServiceTest.java new file mode 100644 index 0000000000..c96f2d35fa --- /dev/null +++ b/yudao-module-erp/src/test/java/cn/iocoder/yudao/module/erp/service/statistics/ErpStatisticsServiceTest.java @@ -0,0 +1,155 @@ +package cn.iocoder.yudao.module.erp.service.statistics; + +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; +import cn.iocoder.yudao.module.erp.dal.mysql.statistics.ErpPurchaseStatisticsMapper; +import cn.iocoder.yudao.module.erp.dal.mysql.statistics.ErpSaleStatisticsMapper; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.ActiveProfiles; + +import jakarta.annotation.Resource; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +/** + * ERP 统计服务测试类 + * 主要测试在多租户关闭情况下,统计查询是否能正常工作 + * + * @author 芋道源码 + */ +@SpringBootTest +@ActiveProfiles("unit-test") +public class ErpStatisticsServiceTest { + + @Resource + private ErpSaleStatisticsService saleStatisticsService; + + @Resource + private ErpPurchaseStatisticsService purchaseStatisticsService; + + @MockBean + private ErpSaleStatisticsMapper saleStatisticsMapper; + + @MockBean + private ErpPurchaseStatisticsMapper purchaseStatisticsMapper; + + @BeforeEach + void setUp() { + // 清理租户上下文 + TenantContextHolder.clear(); + } + + @AfterEach + void tearDown() { + // 清理租户上下文 + TenantContextHolder.clear(); + } + + @Test + void testSaleStatisticsWithoutTenant() { + // 准备参数 + LocalDateTime beginTime = LocalDateTime.of(2024, 1, 1, 0, 0, 0); + LocalDateTime endTime = LocalDateTime.of(2024, 1, 31, 23, 59, 59); + BigDecimal expectedPrice = new BigDecimal("1000.00"); + + // Mock 返回值 + when(saleStatisticsMapper.getSalePrice(any(LocalDateTime.class), any(LocalDateTime.class))) + .thenReturn(expectedPrice); + + // 测试:在没有租户ID的情况下调用销售统计 + assertDoesNotThrow(() -> { + BigDecimal result = saleStatisticsService.getSalePrice(beginTime, endTime); + assertEquals(expectedPrice, result); + }, "在多租户关闭时,销售统计查询应该能正常工作"); + } + + @Test + void testPurchaseStatisticsWithoutTenant() { + // 准备参数 + LocalDateTime beginTime = LocalDateTime.of(2024, 1, 1, 0, 0, 0); + LocalDateTime endTime = LocalDateTime.of(2024, 1, 31, 23, 59, 59); + BigDecimal expectedPrice = new BigDecimal("800.00"); + + // Mock 返回值 + when(purchaseStatisticsMapper.getPurchasePrice(any(LocalDateTime.class), any(LocalDateTime.class))) + .thenReturn(expectedPrice); + + // 测试:在没有租户ID的情况下调用采购统计 + assertDoesNotThrow(() -> { + BigDecimal result = purchaseStatisticsService.getPurchasePrice(beginTime, endTime); + assertEquals(expectedPrice, result); + }, "在多租户关闭时,采购统计查询应该能正常工作"); + } + + @Test + void testSaleStatisticsWithTenant() { + // 设置租户ID + Long tenantId = 1L; + TenantContextHolder.setTenantId(tenantId); + + // 准备参数 + LocalDateTime beginTime = LocalDateTime.of(2024, 1, 1, 0, 0, 0); + LocalDateTime endTime = LocalDateTime.of(2024, 1, 31, 23, 59, 59); + BigDecimal expectedPrice = new BigDecimal("1500.00"); + + // Mock 返回值 + when(saleStatisticsMapper.getSalePrice(any(LocalDateTime.class), any(LocalDateTime.class))) + .thenReturn(expectedPrice); + + // 测试:在有租户ID的情况下调用销售统计 + assertDoesNotThrow(() -> { + BigDecimal result = saleStatisticsService.getSalePrice(beginTime, endTime); + assertEquals(expectedPrice, result); + }, "在多租户开启时,销售统计查询应该能正常工作"); + + // 验证租户ID是否正确设置 + assertEquals(tenantId, TenantContextHolder.getTenantId()); + } + + @Test + void testPurchaseStatisticsWithTenant() { + // 设置租户ID + Long tenantId = 2L; + TenantContextHolder.setTenantId(tenantId); + + // 准备参数 + LocalDateTime beginTime = LocalDateTime.of(2024, 1, 1, 0, 0, 0); + LocalDateTime endTime = LocalDateTime.of(2024, 1, 31, 23, 59, 59); + BigDecimal expectedPrice = new BigDecimal("1200.00"); + + // Mock 返回值 + when(purchaseStatisticsMapper.getPurchasePrice(any(LocalDateTime.class), any(LocalDateTime.class))) + .thenReturn(expectedPrice); + + // 测试:在有租户ID的情况下调用采购统计 + assertDoesNotThrow(() -> { + BigDecimal result = purchaseStatisticsService.getPurchasePrice(beginTime, endTime); + assertEquals(expectedPrice, result); + }, "在多租户开启时,采购统计查询应该能正常工作"); + + // 验证租户ID是否正确设置 + assertEquals(tenantId, TenantContextHolder.getTenantId()); + } + + @Test + void testTenantContextHolderMethods() { + // 测试 getTenantId() 在没有设置租户时返回 null + assertNull(TenantContextHolder.getTenantId(), "未设置租户时应该返回 null"); + + // 设置租户ID + Long tenantId = 3L; + TenantContextHolder.setTenantId(tenantId); + assertEquals(tenantId, TenantContextHolder.getTenantId(), "设置租户后应该能正确获取"); + + // 清理租户上下文 + TenantContextHolder.clear(); + assertNull(TenantContextHolder.getTenantId(), "清理后应该返回 null"); + } +} From 9dec02d02ec0721481392e3774be8131d2dc26e8 Mon Sep 17 00:00:00 2001 From: xingyu4j Date: Tue, 10 Jun 2025 14:51:41 +0800 Subject: [PATCH 03/89] =?UTF-8?q?feat:=20=E4=BD=BF=E7=94=A8=20fastExcel=20?= =?UTF-8?q?=E6=9B=BF=E6=8D=A2=20easyExcel=20(https://github.com/fast-excel?= =?UTF-8?q?/fastexcel)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yudao-dependencies/pom.xml | 21 +++++-------------- .../yudao-spring-boot-starter-excel/pom.xml | 9 ++------ .../excel/core/convert/AreaConvert.java | 10 ++++----- .../excel/core/convert/DictConvert.java | 12 +++++------ .../excel/core/convert/JsonConvert.java | 10 ++++----- .../excel/core/convert/MoneyConvert.java | 10 ++++----- .../core/handler/SelectSheetWriteHandler.java | 10 ++++----- .../framework/excel/core/util/ExcelUtils.java | 6 +++--- .../BpmProcessExpressionRespVO.java | 4 ++-- .../vo/business/CrmBusinessRespVO.java | 4 ++-- .../admin/clue/vo/CrmClueRespVO.java | 4 ++-- .../admin/contact/vo/CrmContactRespVO.java | 4 ++-- .../vo/contract/CrmContractRespVO.java | 4 ++-- .../vo/customer/CrmCustomerImportExcelVO.java | 2 +- .../vo/customer/CrmCustomerRespVO.java | 4 ++-- .../followup/vo/CrmFollowUpRecordRespVO.java | 6 +++--- .../operatelog/vo/CrmOperateLogRespVO.java | 2 +- .../category/CrmProductCategoryListReqVO.java | 2 +- .../product/vo/product/CrmProductRespVO.java | 4 ++-- .../vo/plan/CrmReceivablePlanRespVO.java | 4 ++-- .../vo/receivable/CrmReceivableRespVO.java | 4 ++-- .../finance/vo/account/ErpAccountRespVO.java | 6 +++--- .../vo/payment/ErpFinancePaymentRespVO.java | 6 +++--- .../vo/receipt/ErpFinanceReceiptRespVO.java | 6 +++--- .../vo/category/ErpProductCategoryRespVO.java | 6 +++--- .../product/vo/product/ErpProductRespVO.java | 6 +++--- .../product/vo/unit/ErpProductUnitRespVO.java | 6 +++--- .../purchase/vo/in/ErpPurchaseInRespVO.java | 6 +++--- .../vo/order/ErpPurchaseOrderRespVO.java | 6 +++--- .../vo/returns/ErpPurchaseReturnRespVO.java | 6 +++--- .../vo/supplier/ErpSupplierRespVO.java | 6 +++--- .../sale/vo/customer/ErpCustomerRespVO.java | 7 ++----- .../sale/vo/order/ErpSaleOrderRespVO.java | 6 +++--- .../admin/sale/vo/out/ErpSaleOutRespVO.java | 6 +++--- .../sale/vo/returns/ErpSaleReturnRespVO.java | 6 +++--- .../stock/vo/check/ErpStockCheckRespVO.java | 6 +++--- .../admin/stock/vo/in/ErpStockInRespVO.java | 6 +++--- .../stock/vo/move/ErpStockMoveRespVO.java | 6 +++--- .../admin/stock/vo/out/ErpStockOutRespVO.java | 6 +++--- .../stock/vo/record/ErpStockRecordRespVO.java | 6 +++--- .../admin/stock/vo/stock/ErpStockRespVO.java | 6 +++--- .../vo/warehouse/ErpWarehouseRespVO.java | 6 +++--- .../admin/config/vo/ConfigRespVO.java | 4 ++-- .../demo/demo01/vo/Demo01ContactRespVO.java | 6 +++--- .../demo/demo02/vo/Demo02CategoryRespVO.java | 6 +++--- .../demo03/erp/vo/Demo03StudentErpRespVO.java | 6 +++--- .../inner/vo/Demo03StudentInnerRespVO.java | 6 +++--- .../normal/vo/Demo03StudentNormalRespVO.java | 6 +++--- .../admin/job/vo/job/JobRespVO.java | 4 ++-- .../admin/job/vo/log/JobLogRespVO.java | 4 ++-- .../vo/apiaccesslog/ApiAccessLogRespVO.java | 4 ++-- .../vo/apierrorlog/ApiErrorLogRespVO.java | 4 ++-- .../codegen/java/controller/vo/respVO.vm | 4 ++-- .../src/main/resources/codegen/java/dal/do.vm | 4 ++-- .../vue2_master_erp/java/InfraStudentRespVO | 4 ++-- .../vue2_master_inner/java/InfraStudentRespVO | 4 ++-- .../java/InfraStudentRespVO | 4 ++-- .../vue2_one/java/InfraStudentRespVO | 4 ++-- .../vue2_tree/java/InfraCategoryRespVO | 4 ++-- .../vue3_master_erp/java/InfraStudentRespVO | 4 ++-- .../vue3_master_inner/java/InfraStudentRespVO | 4 ++-- .../java/InfraStudentRespVO | 4 ++-- .../vue3_one/java/InfraStudentRespVO | 4 ++-- .../vue3_tree/java/InfraCategoryRespVO | 4 ++-- .../vo/device/IotDeviceImportExcelVO.java | 4 ++-- .../IotDeviceMqttConnectionParamsRespVO.java | 6 +++--- .../device/vo/device/IotDeviceRespVO.java | 6 +++--- .../product/vo/product/IotProductRespVO.java | 6 +++--- .../thingmodel/vo/IotThingModelRespVO.java | 6 +++--- .../vo/ProductBrowseHistoryRespVO.java | 2 +- .../admin/spu/vo/ProductSpuRespVO.java | 4 ++-- .../kefu/vo/message/KeFuMessageRespVO.java | 3 +-- .../vo/activity/PointActivityRespVO.java | 6 +++--- .../point/vo/product/PointProductRespVO.java | 4 ++-- .../app/point/vo/AppPointActivityRespVO.java | 2 +- .../product/vo/ProductStatisticsRespVO.java | 6 +++--- .../trade/vo/TradeTrendSummaryExcelVO.java | 4 ++-- .../vo/express/DeliveryExpressExcelVO.java | 2 +- .../admin/order/vo/PayOrderExcelVO.java | 2 +- .../admin/refund/vo/PayRefundExcelVO.java | 2 +- .../admin/transfer/vo/PayTransferRespVO.java | 4 ++-- .../admin/dept/vo/post/PostRespVO.java | 4 ++-- .../admin/dept/vo/post/PostSimpleRespVO.java | 2 +- .../admin/dict/vo/data/DictDataRespVO.java | 4 ++-- .../admin/dict/vo/type/DictTypeRespVO.java | 4 ++-- .../logger/vo/loginlog/LoginLogRespVO.java | 4 ++-- .../vo/operatelog/OperateLogRespVO.java | 4 ++-- .../admin/permission/vo/role/RoleRespVO.java | 4 ++-- .../admin/sms/vo/log/SmsLogRespVO.java | 4 ++-- .../sms/vo/template/SmsTemplateRespVO.java | 4 ++-- .../admin/tenant/vo/tenant/TenantRespVO.java | 4 ++-- .../admin/user/vo/user/UserImportExcelVO.java | 2 +- .../admin/user/vo/user/UserRespVO.java | 4 ++-- 93 files changed, 227 insertions(+), 247 deletions(-) diff --git a/yudao-dependencies/pom.xml b/yudao-dependencies/pom.xml index f7f615b492..63e31b0886 100644 --- a/yudao-dependencies/pom.xml +++ b/yudao-dependencies/pom.xml @@ -54,7 +54,7 @@ 1.6.3 5.8.35 6.0.0-M19 - 4.0.3 + 1.2.0 2.4.1 1.2.83 33.4.8-jre @@ -69,8 +69,6 @@ 0.9.0 4.5.13 - 2.17.0 - 1.27.1 2.30.14 1.16.7 1.4.0 @@ -479,20 +477,11 @@ - com.alibaba - easyexcel - ${easyexcel.version} - - - commons-io - commons-io - ${commons-io.version} - - - org.apache.commons - commons-compress - ${commons-compress.version} + cn.idev.excel + fastexcel + ${fastexcel.version} + org.apache.tika tika-core diff --git a/yudao-framework/yudao-spring-boot-starter-excel/pom.xml b/yudao-framework/yudao-spring-boot-starter-excel/pom.xml index 0413986a64..b4af03d033 100644 --- a/yudao-framework/yudao-spring-boot-starter-excel/pom.xml +++ b/yudao-framework/yudao-spring-boot-starter-excel/pom.xml @@ -42,8 +42,8 @@ - com.alibaba - easyexcel + cn.idev.excel + fastexcel @@ -51,11 +51,6 @@ guava - - org.apache.commons - commons-compress - - cn.iocoder.boot yudao-spring-boot-starter-biz-ip diff --git a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/AreaConvert.java b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/AreaConvert.java index 9778b17aea..b5ca863173 100644 --- a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/AreaConvert.java +++ b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/AreaConvert.java @@ -3,11 +3,11 @@ package cn.iocoder.yudao.framework.excel.core.convert; import cn.hutool.core.convert.Convert; import cn.iocoder.yudao.framework.ip.core.Area; import cn.iocoder.yudao.framework.ip.core.utils.AreaUtils; -import com.alibaba.excel.converters.Converter; -import com.alibaba.excel.enums.CellDataTypeEnum; -import com.alibaba.excel.metadata.GlobalConfiguration; -import com.alibaba.excel.metadata.data.ReadCellData; -import com.alibaba.excel.metadata.property.ExcelContentProperty; +import cn.idev.excel.converters.Converter; +import cn.idev.excel.enums.CellDataTypeEnum; +import cn.idev.excel.metadata.GlobalConfiguration; +import cn.idev.excel.metadata.data.ReadCellData; +import cn.idev.excel.metadata.property.ExcelContentProperty; import lombok.extern.slf4j.Slf4j; /** diff --git a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/DictConvert.java b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/DictConvert.java index e393195ed1..b9e0dcb735 100644 --- a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/DictConvert.java +++ b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/DictConvert.java @@ -3,12 +3,12 @@ package cn.iocoder.yudao.framework.excel.core.convert; import cn.hutool.core.convert.Convert; import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; -import com.alibaba.excel.converters.Converter; -import com.alibaba.excel.enums.CellDataTypeEnum; -import com.alibaba.excel.metadata.GlobalConfiguration; -import com.alibaba.excel.metadata.data.ReadCellData; -import com.alibaba.excel.metadata.data.WriteCellData; -import com.alibaba.excel.metadata.property.ExcelContentProperty; +import cn.idev.excel.converters.Converter; +import cn.idev.excel.enums.CellDataTypeEnum; +import cn.idev.excel.metadata.GlobalConfiguration; +import cn.idev.excel.metadata.data.ReadCellData; +import cn.idev.excel.metadata.data.WriteCellData; +import cn.idev.excel.metadata.property.ExcelContentProperty; import lombok.extern.slf4j.Slf4j; /** diff --git a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/JsonConvert.java b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/JsonConvert.java index 0d4794e5fa..6958c32e0c 100644 --- a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/JsonConvert.java +++ b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/JsonConvert.java @@ -1,11 +1,11 @@ package cn.iocoder.yudao.framework.excel.core.convert; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import com.alibaba.excel.converters.Converter; -import com.alibaba.excel.enums.CellDataTypeEnum; -import com.alibaba.excel.metadata.GlobalConfiguration; -import com.alibaba.excel.metadata.data.WriteCellData; -import com.alibaba.excel.metadata.property.ExcelContentProperty; +import cn.idev.excel.converters.Converter; +import cn.idev.excel.enums.CellDataTypeEnum; +import cn.idev.excel.metadata.GlobalConfiguration; +import cn.idev.excel.metadata.data.WriteCellData; +import cn.idev.excel.metadata.property.ExcelContentProperty; /** * Excel Json 转换器 diff --git a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/MoneyConvert.java b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/MoneyConvert.java index ee66fe7dec..9ed0bd581f 100644 --- a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/MoneyConvert.java +++ b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/MoneyConvert.java @@ -1,10 +1,10 @@ package cn.iocoder.yudao.framework.excel.core.convert; -import com.alibaba.excel.converters.Converter; -import com.alibaba.excel.enums.CellDataTypeEnum; -import com.alibaba.excel.metadata.GlobalConfiguration; -import com.alibaba.excel.metadata.data.WriteCellData; -import com.alibaba.excel.metadata.property.ExcelContentProperty; +import cn.idev.excel.converters.Converter; +import cn.idev.excel.enums.CellDataTypeEnum; +import cn.idev.excel.metadata.GlobalConfiguration; +import cn.idev.excel.metadata.data.WriteCellData; +import cn.idev.excel.metadata.property.ExcelContentProperty; import java.math.BigDecimal; import java.math.RoundingMode; diff --git a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/handler/SelectSheetWriteHandler.java b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/handler/SelectSheetWriteHandler.java index ef1eb65bcd..754fa1f7c0 100644 --- a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/handler/SelectSheetWriteHandler.java +++ b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/handler/SelectSheetWriteHandler.java @@ -10,10 +10,10 @@ import cn.iocoder.yudao.framework.common.core.KeyValue; import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils; import cn.iocoder.yudao.framework.excel.core.annotations.ExcelColumnSelect; import cn.iocoder.yudao.framework.excel.core.function.ExcelColumnSelectFunction; -import com.alibaba.excel.annotation.ExcelProperty; -import com.alibaba.excel.write.handler.SheetWriteHandler; -import com.alibaba.excel.write.metadata.holder.WriteSheetHolder; -import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder; +import cn.idev.excel.annotation.ExcelProperty; +import cn.idev.excel.write.handler.SheetWriteHandler; +import cn.idev.excel.write.metadata.holder.WriteSheetHolder; +import cn.idev.excel.write.metadata.holder.WriteWorkbookHolder; import lombok.extern.slf4j.Slf4j; import org.apache.poi.hssf.usermodel.HSSFDataValidation; import org.apache.poi.ss.usermodel.*; @@ -155,4 +155,4 @@ public class SelectSheetWriteHandler implements SheetWriteHandler { writeSheetHolder.getSheet().addValidationData(validation); } -} \ No newline at end of file +} diff --git a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java index eb037d9e17..bae20d6c53 100644 --- a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java +++ b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java @@ -2,9 +2,9 @@ package cn.iocoder.yudao.framework.excel.core.util; import cn.iocoder.yudao.framework.common.util.http.HttpUtils; import cn.iocoder.yudao.framework.excel.core.handler.SelectSheetWriteHandler; -import com.alibaba.excel.EasyExcel; -import com.alibaba.excel.converters.longconverter.LongStringConverter; -import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy; +import cn.idev.excel.EasyExcel; +import cn.idev.excel.converters.longconverter.LongStringConverter; +import cn.idev.excel.write.style.column.LongestMatchColumnWidthStyleStrategy; import jakarta.servlet.http.HttpServletResponse; import org.springframework.web.multipart.MultipartFile; diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/expression/BpmProcessExpressionRespVO.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/expression/BpmProcessExpressionRespVO.java index d877f60a88..7ce45b3350 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/expression/BpmProcessExpressionRespVO.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/expression/BpmProcessExpressionRespVO.java @@ -1,6 +1,6 @@ package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.expression; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -27,4 +27,4 @@ public class BpmProcessExpressionRespVO { @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/business/CrmBusinessRespVO.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/business/CrmBusinessRespVO.java index 49cdcb80be..d94c856210 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/business/CrmBusinessRespVO.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/business/CrmBusinessRespVO.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.module.crm.controller.admin.business.vo.business; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueRespVO.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueRespVO.java index 56e5c25612..911f48d130 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueRespVO.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueRespVO.java @@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.crm.controller.admin.clue.vo; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.module.infra.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.ToString; diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactRespVO.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactRespVO.java index b2b1e83848..e198531179 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactRespVO.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactRespVO.java @@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.crm.controller.admin.contact.vo; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.module.infra.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.ToString; diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/contract/CrmContractRespVO.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/contract/CrmContractRespVO.java index a01bc110b9..307a7472fb 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/contract/CrmContractRespVO.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/contract/CrmContractRespVO.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/customer/CrmCustomerImportExcelVO.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/customer/CrmCustomerImportExcelVO.java index a45e9115fe..07d86a4f03 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/customer/CrmCustomerImportExcelVO.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/customer/CrmCustomerImportExcelVO.java @@ -5,7 +5,7 @@ import cn.iocoder.yudao.framework.excel.core.annotations.ExcelColumnSelect; import cn.iocoder.yudao.framework.excel.core.convert.AreaConvert; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.module.crm.framework.excel.core.AreaExcelColumnSelectFunction; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelProperty; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/customer/CrmCustomerRespVO.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/customer/CrmCustomerRespVO.java index 236129918c..1b337282ac 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/customer/CrmCustomerRespVO.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/customer/CrmCustomerRespVO.java @@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.crm.controller.admin.customer.vo.customer; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.module.infra.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/followup/vo/CrmFollowUpRecordRespVO.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/followup/vo/CrmFollowUpRecordRespVO.java index 1ce10b73e3..075c3b360f 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/followup/vo/CrmFollowUpRecordRespVO.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/followup/vo/CrmFollowUpRecordRespVO.java @@ -2,8 +2,8 @@ package cn.iocoder.yudao.module.crm.controller.admin.followup.vo; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusinessRespVO; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -61,4 +61,4 @@ public class CrmFollowUpRecordRespVO { @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/operatelog/vo/CrmOperateLogRespVO.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/operatelog/vo/CrmOperateLogRespVO.java index 8e458a8a0f..a9c2a7a701 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/operatelog/vo/CrmOperateLogRespVO.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/operatelog/vo/CrmOperateLogRespVO.java @@ -1,6 +1,6 @@ package cn.iocoder.yudao.module.crm.controller.admin.operatelog.vo; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/category/CrmProductCategoryListReqVO.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/category/CrmProductCategoryListReqVO.java index 6144c95c4d..c8e181e341 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/category/CrmProductCategoryListReqVO.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/category/CrmProductCategoryListReqVO.java @@ -1,6 +1,6 @@ package cn.iocoder.yudao.module.crm.controller.admin.product.vo.category; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/product/CrmProductRespVO.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/product/CrmProductRespVO.java index aa955a9f4e..5ef01a37f5 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/product/CrmProductRespVO.java +++ b/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; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.module.crm.dal.dataobject.product.CrmProductCategoryDO; import cn.iocoder.yudao.module.crm.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import com.fhs.core.trans.anno.Trans; import com.fhs.core.trans.constant.TransType; import com.fhs.core.trans.vo.VO; diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/plan/CrmReceivablePlanRespVO.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/plan/CrmReceivablePlanRespVO.java index ad1ce3a7b7..2208887db5 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/plan/CrmReceivablePlanRespVO.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/plan/CrmReceivablePlanRespVO.java @@ -1,8 +1,8 @@ package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.plan; import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable.CrmReceivableRespVO; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/receivable/CrmReceivableRespVO.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/receivable/CrmReceivableRespVO.java index 6d712e3a54..75827db68f 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/receivable/CrmReceivableRespVO.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/receivable/CrmReceivableRespVO.java @@ -4,8 +4,8 @@ import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract.CrmContractRespVO; import cn.iocoder.yudao.module.crm.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/account/ErpAccountRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/account/ErpAccountRespVO.java index a1c2e954db..3e03169689 100644 --- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/account/ErpAccountRespVO.java +++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/account/ErpAccountRespVO.java @@ -2,8 +2,8 @@ package cn.iocoder.yudao.module.erp.controller.admin.finance.vo.account; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.module.system.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -47,4 +47,4 @@ public class ErpAccountRespVO { @ExcelProperty("创建时间") private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/payment/ErpFinancePaymentRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/payment/ErpFinancePaymentRespVO.java index 12b5a7df8e..df89465f00 100644 --- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/payment/ErpFinancePaymentRespVO.java +++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/payment/ErpFinancePaymentRespVO.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.module.erp.controller.admin.finance.vo.payment; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.Data; @@ -96,4 +96,4 @@ public class ErpFinancePaymentRespVO { } -} \ No newline at end of file +} diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/receipt/ErpFinanceReceiptRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/receipt/ErpFinanceReceiptRespVO.java index ec82875957..2398e065ca 100644 --- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/receipt/ErpFinanceReceiptRespVO.java +++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/receipt/ErpFinanceReceiptRespVO.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.module.erp.controller.admin.finance.vo.receipt; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.Data; @@ -96,4 +96,4 @@ public class ErpFinanceReceiptRespVO { } -} \ No newline at end of file +} diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/product/vo/category/ErpProductCategoryRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/product/vo/category/ErpProductCategoryRespVO.java index 23d7d9e8ff..29d4c2fd6e 100644 --- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/product/vo/category/ErpProductCategoryRespVO.java +++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/product/vo/category/ErpProductCategoryRespVO.java @@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.erp.controller.admin.product.vo.category; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.module.system.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -44,4 +44,4 @@ public class ErpProductCategoryRespVO { @ExcelProperty("创建时间") private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/product/vo/product/ErpProductRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/product/vo/product/ErpProductRespVO.java index 9be9bc2559..8dcc9f89ba 100644 --- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/product/vo/product/ErpProductRespVO.java +++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/product/vo/product/ErpProductRespVO.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.module.erp.controller.admin.product.vo.product; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -73,4 +73,4 @@ public class ErpProductRespVO { @ExcelProperty("创建时间") private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/product/vo/unit/ErpProductUnitRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/product/vo/unit/ErpProductUnitRespVO.java index 06f604920c..3044d5aef1 100644 --- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/product/vo/unit/ErpProductUnitRespVO.java +++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/product/vo/unit/ErpProductUnitRespVO.java @@ -2,8 +2,8 @@ package cn.iocoder.yudao.module.erp.controller.admin.product.vo.unit; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.module.system.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -31,4 +31,4 @@ public class ErpProductUnitRespVO { @ExcelProperty("创建时间") private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/in/ErpPurchaseInRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/in/ErpPurchaseInRespVO.java index beeeab8692..03c446ed1c 100644 --- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/in/ErpPurchaseInRespVO.java +++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/in/ErpPurchaseInRespVO.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.in; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.Data; @@ -142,4 +142,4 @@ public class ErpPurchaseInRespVO { } -} \ No newline at end of file +} diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderRespVO.java index bc76720eed..e6a6aa4f64 100644 --- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderRespVO.java +++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderRespVO.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.Data; @@ -149,4 +149,4 @@ public class ErpPurchaseOrderRespVO { } -} \ No newline at end of file +} diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/returns/ErpPurchaseReturnRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/returns/ErpPurchaseReturnRespVO.java index 223b9327e9..676241949c 100644 --- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/returns/ErpPurchaseReturnRespVO.java +++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/returns/ErpPurchaseReturnRespVO.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.returns; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.Data; @@ -142,4 +142,4 @@ public class ErpPurchaseReturnRespVO { } -} \ No newline at end of file +} diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/supplier/ErpSupplierRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/supplier/ErpSupplierRespVO.java index 5ba5892c1b..538a3b1e60 100644 --- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/supplier/ErpSupplierRespVO.java +++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/supplier/ErpSupplierRespVO.java @@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.supplier; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.module.system.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -81,4 +81,4 @@ public class ErpSupplierRespVO { @ExcelProperty("创建时间") private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/customer/ErpCustomerRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/customer/ErpCustomerRespVO.java index f1a58a03d5..c75a99a086 100644 --- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/customer/ErpCustomerRespVO.java +++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/customer/ErpCustomerRespVO.java @@ -2,12 +2,9 @@ package cn.iocoder.yudao.module.erp.controller.admin.sale.vo.customer; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; -import java.util.*; -import java.util.*; import java.math.BigDecimal; -import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; -import com.alibaba.excel.annotation.*; +import cn.idev.excel.annotation.*; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; @@ -81,4 +78,4 @@ public class ErpCustomerRespVO { @ExcelProperty("创建时间") private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderRespVO.java index e5958a841f..b1374e8248 100644 --- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderRespVO.java +++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderRespVO.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.module.erp.controller.admin.sale.vo.order; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.Data; @@ -152,4 +152,4 @@ public class ErpSaleOrderRespVO { } -} \ No newline at end of file +} diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/out/ErpSaleOutRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/out/ErpSaleOutRespVO.java index bc15a13398..a004ed3889 100644 --- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/out/ErpSaleOutRespVO.java +++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/out/ErpSaleOutRespVO.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.module.erp.controller.admin.sale.vo.out; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.Data; @@ -145,4 +145,4 @@ public class ErpSaleOutRespVO { } -} \ No newline at end of file +} diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/returns/ErpSaleReturnRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/returns/ErpSaleReturnRespVO.java index ba52f4f80d..6a1ade0b33 100644 --- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/returns/ErpSaleReturnRespVO.java +++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/returns/ErpSaleReturnRespVO.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.module.erp.controller.admin.sale.vo.returns; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.Data; @@ -145,4 +145,4 @@ public class ErpSaleReturnRespVO { } -} \ No newline at end of file +} diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/check/ErpStockCheckRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/check/ErpStockCheckRespVO.java index af53e3c726..6f9b8da0f8 100644 --- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/check/ErpStockCheckRespVO.java +++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/check/ErpStockCheckRespVO.java @@ -2,8 +2,8 @@ package cn.iocoder.yudao.module.erp.controller.admin.stock.vo.check; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.Data; @@ -108,4 +108,4 @@ public class ErpStockCheckRespVO { } -} \ No newline at end of file +} diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/in/ErpStockInRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/in/ErpStockInRespVO.java index 077b9dd1b6..1d23f3692c 100644 --- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/in/ErpStockInRespVO.java +++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/in/ErpStockInRespVO.java @@ -2,8 +2,8 @@ package cn.iocoder.yudao.module.erp.controller.admin.stock.vo.in; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -107,4 +107,4 @@ public class ErpStockInRespVO { } -} \ No newline at end of file +} diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/move/ErpStockMoveRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/move/ErpStockMoveRespVO.java index 799ddc3f16..f5f7650aee 100644 --- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/move/ErpStockMoveRespVO.java +++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/move/ErpStockMoveRespVO.java @@ -2,8 +2,8 @@ package cn.iocoder.yudao.module.erp.controller.admin.stock.vo.move; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -104,4 +104,4 @@ public class ErpStockMoveRespVO { } -} \ No newline at end of file +} diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/out/ErpStockOutRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/out/ErpStockOutRespVO.java index 22a88e7c93..a1d0f51016 100644 --- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/out/ErpStockOutRespVO.java +++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/out/ErpStockOutRespVO.java @@ -2,8 +2,8 @@ package cn.iocoder.yudao.module.erp.controller.admin.stock.vo.out; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -107,4 +107,4 @@ public class ErpStockOutRespVO { } -} \ No newline at end of file +} diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/record/ErpStockRecordRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/record/ErpStockRecordRespVO.java index ff4b3e12a1..47e1261831 100644 --- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/record/ErpStockRecordRespVO.java +++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/record/ErpStockRecordRespVO.java @@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.erp.controller.admin.stock.vo.record; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.module.erp.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -84,4 +84,4 @@ public class ErpStockRecordRespVO { @ExcelProperty("创建人") private String creatorName; -} \ No newline at end of file +} diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/stock/ErpStockRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/stock/ErpStockRespVO.java index 06366a0dd4..8d84789901 100644 --- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/stock/ErpStockRespVO.java +++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/stock/ErpStockRespVO.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.module.erp.controller.admin.stock.vo.stock; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -46,4 +46,4 @@ public class ErpStockRespVO { @ExcelProperty("仓库名称") private String warehouseName; -} \ No newline at end of file +} diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/warehouse/ErpWarehouseRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/warehouse/ErpWarehouseRespVO.java index 188d426997..06825b848b 100644 --- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/warehouse/ErpWarehouseRespVO.java +++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/warehouse/ErpWarehouseRespVO.java @@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.erp.controller.admin.stock.vo.warehouse; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.module.system.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -61,4 +61,4 @@ public class ErpWarehouseRespVO { @ExcelProperty("创建时间") private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/vo/ConfigRespVO.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/vo/ConfigRespVO.java index b4f642f697..622bb04269 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/vo/ConfigRespVO.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/vo/ConfigRespVO.java @@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.infra.controller.admin.config.vo; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.module.infra.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo01/vo/Demo01ContactRespVO.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo01/vo/Demo01ContactRespVO.java index 17ee9fef84..df0ef60401 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo01/vo/Demo01ContactRespVO.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo01/vo/Demo01ContactRespVO.java @@ -2,8 +2,8 @@ package cn.iocoder.yudao.module.infra.controller.admin.demo.demo01.vo; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -43,4 +43,4 @@ public class Demo01ContactRespVO { @ExcelProperty("创建时间") private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo02/vo/Demo02CategoryRespVO.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo02/vo/Demo02CategoryRespVO.java index 1f2efd46a7..bac533bca6 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo02/vo/Demo02CategoryRespVO.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo02/vo/Demo02CategoryRespVO.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.module.infra.controller.admin.demo.demo02.vo; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -28,4 +28,4 @@ public class Demo02CategoryRespVO { @ExcelProperty("创建时间") private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/erp/vo/Demo03StudentErpRespVO.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/erp/vo/Demo03StudentErpRespVO.java index 84dfe6e037..30944843f5 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/erp/vo/Demo03StudentErpRespVO.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/erp/vo/Demo03StudentErpRespVO.java @@ -2,8 +2,8 @@ package cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.erp.vo; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -39,4 +39,4 @@ public class Demo03StudentErpRespVO { @ExcelProperty("创建时间") private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/inner/vo/Demo03StudentInnerRespVO.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/inner/vo/Demo03StudentInnerRespVO.java index 48d5e4889a..3db315b3ea 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/inner/vo/Demo03StudentInnerRespVO.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/inner/vo/Demo03StudentInnerRespVO.java @@ -2,8 +2,8 @@ package cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.inner.vo; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -39,4 +39,4 @@ public class Demo03StudentInnerRespVO { @ExcelProperty("创建时间") private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/normal/vo/Demo03StudentNormalRespVO.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/normal/vo/Demo03StudentNormalRespVO.java index e36a7965c1..f100c80dce 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/normal/vo/Demo03StudentNormalRespVO.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/normal/vo/Demo03StudentNormalRespVO.java @@ -2,8 +2,8 @@ package cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.normal.vo; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -39,4 +39,4 @@ public class Demo03StudentNormalRespVO { @ExcelProperty("创建时间") private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/job/JobRespVO.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/job/JobRespVO.java index aee0d9bcf1..9a4be2eafe 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/job/JobRespVO.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/job/JobRespVO.java @@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.infra.controller.admin.job.vo.job; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.module.infra.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.Data; diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/log/JobLogRespVO.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/log/JobLogRespVO.java index 543339d695..3574e98d59 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/log/JobLogRespVO.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/log/JobLogRespVO.java @@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.infra.controller.admin.job.vo.log; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.module.infra.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogRespVO.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogRespVO.java index d9e65c403c..45fc4df130 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogRespVO.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogRespVO.java @@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apiaccesslog; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.module.infra.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogRespVO.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogRespVO.java index a3f6f0e84c..7097924e96 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogRespVO.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogRespVO.java @@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apierrorlog; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.module.infra.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; diff --git a/yudao-module-infra/src/main/resources/codegen/java/controller/vo/respVO.vm b/yudao-module-infra/src/main/resources/codegen/java/controller/vo/respVO.vm index 24c3519451..eae083a893 100644 --- a/yudao-module-infra/src/main/resources/codegen/java/controller/vo/respVO.vm +++ b/yudao-module-infra/src/main/resources/codegen/java/controller/vo/respVO.vm @@ -19,7 +19,7 @@ import java.time.LocalDateTime; #end #end ## 处理 Excel 导出 -import com.alibaba.excel.annotation.*; +import cn.idev.excel.annotation.*; #foreach ($column in $columns) #if ("$!column.dictType" != "")## 有设置数据字典 import ${DictFormatClassName}; @@ -50,4 +50,4 @@ public class ${sceneEnum.prefixClass}${table.className}RespVO { #end #end -} \ No newline at end of file +} diff --git a/yudao-module-infra/src/main/resources/codegen/java/dal/do.vm b/yudao-module-infra/src/main/resources/codegen/java/dal/do.vm index baf53f5986..f15ada4309 100644 --- a/yudao-module-infra/src/main/resources/codegen/java/dal/do.vm +++ b/yudao-module-infra/src/main/resources/codegen/java/dal/do.vm @@ -15,7 +15,7 @@ import ${BaseDOClassName}; ## 处理 Excel 导出 + Schema 注解(仅 DO 模式) #if ($voType == 20) import io.swagger.v3.oas.annotations.media.Schema; -import com.alibaba.excel.annotation.*; +import cn.idev.excel.annotation.*; #foreach ($column in $columns) #if ("$!column.dictType" != "")## 有设置数据字典 import ${DictFormatClassName}; @@ -100,4 +100,4 @@ public class ${table.className}DO extends BaseDO { #end #end -} \ No newline at end of file +} diff --git a/yudao-module-infra/src/test/resources/codegen/windows10/vue2_master_erp/java/InfraStudentRespVO b/yudao-module-infra/src/test/resources/codegen/windows10/vue2_master_erp/java/InfraStudentRespVO index c41a5501fe..f86f6791c4 100644 --- a/yudao-module-infra/src/test/resources/codegen/windows10/vue2_master_erp/java/InfraStudentRespVO +++ b/yudao-module-infra/src/test/resources/codegen/windows10/vue2_master_erp/java/InfraStudentRespVO @@ -6,7 +6,7 @@ import java.util.*; import java.util.*; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; -import com.alibaba.excel.annotation.*; +import cn.idev.excel.annotation.*; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; @@ -57,4 +57,4 @@ public class InfraStudentRespVO { @ExcelProperty("创建时间") private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-infra/src/test/resources/codegen/windows10/vue2_master_inner/java/InfraStudentRespVO b/yudao-module-infra/src/test/resources/codegen/windows10/vue2_master_inner/java/InfraStudentRespVO index c41a5501fe..f86f6791c4 100644 --- a/yudao-module-infra/src/test/resources/codegen/windows10/vue2_master_inner/java/InfraStudentRespVO +++ b/yudao-module-infra/src/test/resources/codegen/windows10/vue2_master_inner/java/InfraStudentRespVO @@ -6,7 +6,7 @@ import java.util.*; import java.util.*; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; -import com.alibaba.excel.annotation.*; +import cn.idev.excel.annotation.*; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; @@ -57,4 +57,4 @@ public class InfraStudentRespVO { @ExcelProperty("创建时间") private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-infra/src/test/resources/codegen/windows10/vue2_master_normal/java/InfraStudentRespVO b/yudao-module-infra/src/test/resources/codegen/windows10/vue2_master_normal/java/InfraStudentRespVO index c41a5501fe..f86f6791c4 100644 --- a/yudao-module-infra/src/test/resources/codegen/windows10/vue2_master_normal/java/InfraStudentRespVO +++ b/yudao-module-infra/src/test/resources/codegen/windows10/vue2_master_normal/java/InfraStudentRespVO @@ -6,7 +6,7 @@ import java.util.*; import java.util.*; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; -import com.alibaba.excel.annotation.*; +import cn.idev.excel.annotation.*; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; @@ -57,4 +57,4 @@ public class InfraStudentRespVO { @ExcelProperty("创建时间") private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-infra/src/test/resources/codegen/windows10/vue2_one/java/InfraStudentRespVO b/yudao-module-infra/src/test/resources/codegen/windows10/vue2_one/java/InfraStudentRespVO index c41a5501fe..f86f6791c4 100644 --- a/yudao-module-infra/src/test/resources/codegen/windows10/vue2_one/java/InfraStudentRespVO +++ b/yudao-module-infra/src/test/resources/codegen/windows10/vue2_one/java/InfraStudentRespVO @@ -6,7 +6,7 @@ import java.util.*; import java.util.*; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; -import com.alibaba.excel.annotation.*; +import cn.idev.excel.annotation.*; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; @@ -57,4 +57,4 @@ public class InfraStudentRespVO { @ExcelProperty("创建时间") private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-infra/src/test/resources/codegen/windows10/vue2_tree/java/InfraCategoryRespVO b/yudao-module-infra/src/test/resources/codegen/windows10/vue2_tree/java/InfraCategoryRespVO index 6325d866c5..335881c4b7 100644 --- a/yudao-module-infra/src/test/resources/codegen/windows10/vue2_tree/java/InfraCategoryRespVO +++ b/yudao-module-infra/src/test/resources/codegen/windows10/vue2_tree/java/InfraCategoryRespVO @@ -4,7 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.util.*; import java.util.*; -import com.alibaba.excel.annotation.*; +import cn.idev.excel.annotation.*; @Schema(description = "管理后台 - 分类 Response VO") @Data @@ -23,4 +23,4 @@ public class InfraCategoryRespVO { @ExcelProperty("父编号") private Long parentId; -} \ No newline at end of file +} diff --git a/yudao-module-infra/src/test/resources/codegen/windows10/vue3_master_erp/java/InfraStudentRespVO b/yudao-module-infra/src/test/resources/codegen/windows10/vue3_master_erp/java/InfraStudentRespVO index c41a5501fe..f86f6791c4 100644 --- a/yudao-module-infra/src/test/resources/codegen/windows10/vue3_master_erp/java/InfraStudentRespVO +++ b/yudao-module-infra/src/test/resources/codegen/windows10/vue3_master_erp/java/InfraStudentRespVO @@ -6,7 +6,7 @@ import java.util.*; import java.util.*; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; -import com.alibaba.excel.annotation.*; +import cn.idev.excel.annotation.*; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; @@ -57,4 +57,4 @@ public class InfraStudentRespVO { @ExcelProperty("创建时间") private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-infra/src/test/resources/codegen/windows10/vue3_master_inner/java/InfraStudentRespVO b/yudao-module-infra/src/test/resources/codegen/windows10/vue3_master_inner/java/InfraStudentRespVO index c41a5501fe..f86f6791c4 100644 --- a/yudao-module-infra/src/test/resources/codegen/windows10/vue3_master_inner/java/InfraStudentRespVO +++ b/yudao-module-infra/src/test/resources/codegen/windows10/vue3_master_inner/java/InfraStudentRespVO @@ -6,7 +6,7 @@ import java.util.*; import java.util.*; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; -import com.alibaba.excel.annotation.*; +import cn.idev.excel.annotation.*; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; @@ -57,4 +57,4 @@ public class InfraStudentRespVO { @ExcelProperty("创建时间") private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-infra/src/test/resources/codegen/windows10/vue3_master_normal/java/InfraStudentRespVO b/yudao-module-infra/src/test/resources/codegen/windows10/vue3_master_normal/java/InfraStudentRespVO index c41a5501fe..f86f6791c4 100644 --- a/yudao-module-infra/src/test/resources/codegen/windows10/vue3_master_normal/java/InfraStudentRespVO +++ b/yudao-module-infra/src/test/resources/codegen/windows10/vue3_master_normal/java/InfraStudentRespVO @@ -6,7 +6,7 @@ import java.util.*; import java.util.*; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; -import com.alibaba.excel.annotation.*; +import cn.idev.excel.annotation.*; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; @@ -57,4 +57,4 @@ public class InfraStudentRespVO { @ExcelProperty("创建时间") private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-infra/src/test/resources/codegen/windows10/vue3_one/java/InfraStudentRespVO b/yudao-module-infra/src/test/resources/codegen/windows10/vue3_one/java/InfraStudentRespVO index c41a5501fe..f86f6791c4 100644 --- a/yudao-module-infra/src/test/resources/codegen/windows10/vue3_one/java/InfraStudentRespVO +++ b/yudao-module-infra/src/test/resources/codegen/windows10/vue3_one/java/InfraStudentRespVO @@ -6,7 +6,7 @@ import java.util.*; import java.util.*; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; -import com.alibaba.excel.annotation.*; +import cn.idev.excel.annotation.*; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; @@ -57,4 +57,4 @@ public class InfraStudentRespVO { @ExcelProperty("创建时间") private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-infra/src/test/resources/codegen/windows10/vue3_tree/java/InfraCategoryRespVO b/yudao-module-infra/src/test/resources/codegen/windows10/vue3_tree/java/InfraCategoryRespVO index 6325d866c5..335881c4b7 100644 --- a/yudao-module-infra/src/test/resources/codegen/windows10/vue3_tree/java/InfraCategoryRespVO +++ b/yudao-module-infra/src/test/resources/codegen/windows10/vue3_tree/java/InfraCategoryRespVO @@ -4,7 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.util.*; import java.util.*; -import com.alibaba.excel.annotation.*; +import cn.idev.excel.annotation.*; @Schema(description = "管理后台 - 分类 Response VO") @Data @@ -23,4 +23,4 @@ public class InfraCategoryRespVO { @ExcelProperty("父编号") private Long parentId; -} \ No newline at end of file +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceImportExcelVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceImportExcelVO.java index 710e74263d..74585be565 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceImportExcelVO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceImportExcelVO.java @@ -1,6 +1,6 @@ package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import lombok.AllArgsConstructor; @@ -34,4 +34,4 @@ public class IotDeviceImportExcelVO { @ExcelProperty("设备分组") private String groupNames; -} \ No newline at end of file +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceMqttConnectionParamsRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceMqttConnectionParamsRespVO.java index 5ce68c0fe1..39d80c527d 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceMqttConnectionParamsRespVO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceMqttConnectionParamsRespVO.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -22,4 +22,4 @@ public class IotDeviceMqttConnectionParamsRespVO { @ExcelProperty("MQTT 密码") private String mqttPassword; -} \ No newline at end of file +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceRespVO.java index 8404ca9224..74ef173afc 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceRespVO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceRespVO.java @@ -2,8 +2,8 @@ package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -90,4 +90,4 @@ public class IotDeviceRespVO { @ExcelProperty("创建时间") private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductRespVO.java index f674651d51..0455334652 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductRespVO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductRespVO.java @@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.iot.controller.admin.product.vo.product; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.module.iot.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -84,4 +84,4 @@ public class IotProductRespVO { @ExcelProperty("创建时间") private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelRespVO.java index 15a5b9f959..9de915dc43 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelRespVO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelRespVO.java @@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo; import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelEvent; import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelProperty; import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelService; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -51,4 +51,4 @@ public class IotThingModelRespVO { @ExcelProperty("创建时间") private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-mall/yudao-module-product/src/main/java/cn/iocoder/yudao/module/product/controller/admin/history/vo/ProductBrowseHistoryRespVO.java b/yudao-module-mall/yudao-module-product/src/main/java/cn/iocoder/yudao/module/product/controller/admin/history/vo/ProductBrowseHistoryRespVO.java index df8ae8c5bd..56ebc283fe 100644 --- a/yudao-module-mall/yudao-module-product/src/main/java/cn/iocoder/yudao/module/product/controller/admin/history/vo/ProductBrowseHistoryRespVO.java +++ b/yudao-module-mall/yudao-module-product/src/main/java/cn/iocoder/yudao/module/product/controller/admin/history/vo/ProductBrowseHistoryRespVO.java @@ -1,6 +1,6 @@ package cn.iocoder.yudao.module.product.controller.admin.history.vo; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; diff --git a/yudao-module-mall/yudao-module-product/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/vo/ProductSpuRespVO.java b/yudao-module-mall/yudao-module-product/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/vo/ProductSpuRespVO.java index fbc75522a7..d962fe7469 100755 --- a/yudao-module-mall/yudao-module-product/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/vo/ProductSpuRespVO.java +++ b/yudao-module-mall/yudao-module-product/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/vo/ProductSpuRespVO.java @@ -4,8 +4,8 @@ import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.framework.excel.core.convert.MoneyConvert; import cn.iocoder.yudao.module.product.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; diff --git a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/kefu/vo/message/KeFuMessageRespVO.java b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/kefu/vo/message/KeFuMessageRespVO.java index 248160dd9c..059d6af579 100644 --- a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/kefu/vo/message/KeFuMessageRespVO.java +++ b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/kefu/vo/message/KeFuMessageRespVO.java @@ -4,7 +4,6 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.time.LocalDateTime; -import com.alibaba.excel.annotation.*; @Schema(description = "管理后台 - 客服消息 Response VO") @Data @@ -42,4 +41,4 @@ public class KeFuMessageRespVO { @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/point/vo/activity/PointActivityRespVO.java b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/point/vo/activity/PointActivityRespVO.java index d81b3d6902..14f1ee4ccd 100644 --- a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/point/vo/activity/PointActivityRespVO.java +++ b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/point/vo/activity/PointActivityRespVO.java @@ -1,8 +1,8 @@ package cn.iocoder.yudao.module.promotion.controller.admin.point.vo.activity; import cn.iocoder.yudao.module.promotion.controller.admin.point.vo.product.PointProductRespVO; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -69,4 +69,4 @@ public class PointActivityRespVO { @Schema(description = "兑换金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "15860") private Integer price; -} \ No newline at end of file +} diff --git a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/point/vo/product/PointProductRespVO.java b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/point/vo/product/PointProductRespVO.java index 8e8250b387..638ea61fb8 100644 --- a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/point/vo/product/PointProductRespVO.java +++ b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/point/vo/product/PointProductRespVO.java @@ -1,6 +1,6 @@ package cn.iocoder.yudao.module.promotion.controller.admin.point.vo.product; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -36,4 +36,4 @@ public class PointProductRespVO { @Schema(description = "积分商城商品状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") private Integer activityStatus; -} \ No newline at end of file +} diff --git a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/point/vo/AppPointActivityRespVO.java b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/point/vo/AppPointActivityRespVO.java index ddcc23b9cb..83e00c202a 100644 --- a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/point/vo/AppPointActivityRespVO.java +++ b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/point/vo/AppPointActivityRespVO.java @@ -1,6 +1,6 @@ package cn.iocoder.yudao.module.promotion.controller.app.point.vo; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; diff --git a/yudao-module-mall/yudao-module-statistics/src/main/java/cn/iocoder/yudao/module/statistics/controller/admin/product/vo/ProductStatisticsRespVO.java b/yudao-module-mall/yudao-module-statistics/src/main/java/cn/iocoder/yudao/module/statistics/controller/admin/product/vo/ProductStatisticsRespVO.java index 9d93142e31..86126ca705 100644 --- a/yudao-module-mall/yudao-module-statistics/src/main/java/cn/iocoder/yudao/module/statistics/controller/admin/product/vo/ProductStatisticsRespVO.java +++ b/yudao-module-mall/yudao-module-statistics/src/main/java/cn/iocoder/yudao/module/statistics/controller/admin/product/vo/ProductStatisticsRespVO.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.module.statistics.controller.admin.product.vo; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -78,4 +78,4 @@ public class ProductStatisticsRespVO { @Schema(description = "访客支付转化率(百分比)", requiredMode = Schema.RequiredMode.REQUIRED, example = "15") private Integer browseConvertPercent; -} \ No newline at end of file +} diff --git a/yudao-module-mall/yudao-module-statistics/src/main/java/cn/iocoder/yudao/module/statistics/controller/admin/trade/vo/TradeTrendSummaryExcelVO.java b/yudao-module-mall/yudao-module-statistics/src/main/java/cn/iocoder/yudao/module/statistics/controller/admin/trade/vo/TradeTrendSummaryExcelVO.java index 5b14fa1d4e..9e27c6f3a1 100644 --- a/yudao-module-mall/yudao-module-statistics/src/main/java/cn/iocoder/yudao/module/statistics/controller/admin/trade/vo/TradeTrendSummaryExcelVO.java +++ b/yudao-module-mall/yudao-module-statistics/src/main/java/cn/iocoder/yudao/module/statistics/controller/admin/trade/vo/TradeTrendSummaryExcelVO.java @@ -1,8 +1,8 @@ package cn.iocoder.yudao.module.statistics.controller.admin.trade.vo; import cn.iocoder.yudao.framework.excel.core.convert.MoneyConvert; -import com.alibaba.excel.annotation.ExcelProperty; -import com.alibaba.excel.annotation.format.DateTimeFormat; +import cn.idev.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.format.DateTimeFormat; import lombok.Data; import java.time.LocalDate; diff --git a/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/delivery/vo/express/DeliveryExpressExcelVO.java b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/delivery/vo/express/DeliveryExpressExcelVO.java index c84a3a1896..3cc312a913 100644 --- a/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/delivery/vo/express/DeliveryExpressExcelVO.java +++ b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/delivery/vo/express/DeliveryExpressExcelVO.java @@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.trade.controller.admin.delivery.vo.express; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.module.system.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelProperty; import lombok.Data; import java.time.LocalDateTime; diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/order/vo/PayOrderExcelVO.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/order/vo/PayOrderExcelVO.java index 5dc17a0f0c..1cb362541e 100755 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/order/vo/PayOrderExcelVO.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/order/vo/PayOrderExcelVO.java @@ -4,7 +4,7 @@ import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.framework.excel.core.convert.MoneyConvert; import cn.iocoder.yudao.module.pay.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelProperty; import lombok.Data; import java.time.LocalDateTime; diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/refund/vo/PayRefundExcelVO.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/refund/vo/PayRefundExcelVO.java index 758b6b6b09..8794086616 100755 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/refund/vo/PayRefundExcelVO.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/refund/vo/PayRefundExcelVO.java @@ -4,7 +4,7 @@ import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.framework.excel.core.convert.MoneyConvert; import cn.iocoder.yudao.module.pay.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelProperty; import lombok.Data; import java.time.LocalDateTime; diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/transfer/vo/PayTransferRespVO.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/transfer/vo/PayTransferRespVO.java index 9e98db1170..5c2ac3fa82 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/transfer/vo/PayTransferRespVO.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/transfer/vo/PayTransferRespVO.java @@ -5,8 +5,8 @@ import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.framework.excel.core.convert.MoneyConvert; import cn.iocoder.yudao.module.pay.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.time.LocalDateTime; diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostRespVO.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostRespVO.java index dde6f95097..18ccb7ffad 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostRespVO.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostRespVO.java @@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.system.controller.admin.dept.vo.post; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.module.system.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostSimpleRespVO.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostSimpleRespVO.java index 72852a18e3..d019ad7235 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostSimpleRespVO.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostSimpleRespVO.java @@ -1,6 +1,6 @@ package cn.iocoder.yudao.module.system.controller.admin.dept.vo.post; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/data/DictDataRespVO.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/data/DictDataRespVO.java index 8857a7059c..fce51e99c6 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/data/DictDataRespVO.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/data/DictDataRespVO.java @@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.system.controller.admin.dict.vo.data; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.module.system.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/type/DictTypeRespVO.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/type/DictTypeRespVO.java index 6ddd47bcd8..2301a7526a 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/type/DictTypeRespVO.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/type/DictTypeRespVO.java @@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.system.controller.admin.dict.vo.type; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.module.system.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/loginlog/LoginLogRespVO.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/loginlog/LoginLogRespVO.java index 7b9cd165e2..9abe3aa53b 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/loginlog/LoginLogRespVO.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/loginlog/LoginLogRespVO.java @@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.system.controller.admin.logger.vo.loginlog; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.module.system.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/operatelog/OperateLogRespVO.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/operatelog/OperateLogRespVO.java index e017ba7cbe..436d209066 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/operatelog/OperateLogRespVO.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/operatelog/OperateLogRespVO.java @@ -1,8 +1,8 @@ package cn.iocoder.yudao.module.system.controller.admin.logger.vo.operatelog; import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import com.fhs.core.trans.anno.Trans; import com.fhs.core.trans.constant.TransType; import com.fhs.core.trans.vo.VO; diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleRespVO.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleRespVO.java index 89f80c6724..c1c7cfea62 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleRespVO.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleRespVO.java @@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.system.controller.admin.permission.vo.role; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.module.system.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import lombok.Data; diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/log/SmsLogRespVO.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/log/SmsLogRespVO.java index 77409ed918..66a867fd0d 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/log/SmsLogRespVO.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/log/SmsLogRespVO.java @@ -4,8 +4,8 @@ import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.framework.excel.core.convert.JsonConvert; import cn.iocoder.yudao.module.system.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/template/SmsTemplateRespVO.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/template/SmsTemplateRespVO.java index 6b8aecaaad..9caed5c49b 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/template/SmsTemplateRespVO.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/template/SmsTemplateRespVO.java @@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.system.controller.admin.sms.vo.template; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.module.system.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/tenant/TenantRespVO.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/tenant/TenantRespVO.java index 5a444b5213..62ccb7c611 100755 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/tenant/TenantRespVO.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/tenant/TenantRespVO.java @@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.module.system.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserImportExcelVO.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserImportExcelVO.java index a360f1af2c..0c7c5a3c33 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserImportExcelVO.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserImportExcelVO.java @@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.system.controller.admin.user.vo.user; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.module.system.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelProperty; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserRespVO.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserRespVO.java index 2837318f9a..ba322240e2 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserRespVO.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserRespVO.java @@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.system.controller.admin.user.vo.user; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.module.system.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; From 1d5c07706247821a3649bd42d265a76c3c92a7f7 Mon Sep 17 00:00:00 2001 From: xingyu4j Date: Sun, 15 Jun 2025 18:55:42 +0800 Subject: [PATCH 04/89] =?UTF-8?q?feat:=20=E4=BD=BF=E7=94=A8=20fastExcel=20?= =?UTF-8?q?=E6=9B=BF=E6=8D=A2=20easyExcel=20(https://github.com/fast-excel?= =?UTF-8?q?/fastexcel)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../excel/core/handler/SelectSheetWriteHandler.java | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/handler/SelectSheetWriteHandler.java b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/handler/SelectSheetWriteHandler.java index 0cace13ac8..cd6c87f68f 100644 --- a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/handler/SelectSheetWriteHandler.java +++ b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/handler/SelectSheetWriteHandler.java @@ -3,7 +3,6 @@ package cn.iocoder.yudao.framework.excel.core.handler; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.ObjectUtil; -import cn.hutool.core.util.ReflectUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; import cn.hutool.poi.excel.ExcelUtil; @@ -11,12 +10,8 @@ import cn.iocoder.yudao.framework.common.core.KeyValue; import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils; import cn.iocoder.yudao.framework.excel.core.annotations.ExcelColumnSelect; import cn.iocoder.yudao.framework.excel.core.function.ExcelColumnSelectFunction; -import com.alibaba.excel.annotation.ExcelIgnore; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; -import com.alibaba.excel.write.handler.SheetWriteHandler; -import com.alibaba.excel.write.metadata.holder.WriteSheetHolder; -import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder; +import cn.idev.excel.annotation.ExcelIgnore; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; import cn.idev.excel.annotation.ExcelProperty; import cn.idev.excel.write.handler.SheetWriteHandler; import cn.idev.excel.write.metadata.holder.WriteSheetHolder; From a5ad8bb708d708ef5e9d2a2e7bc12deb99ee924e Mon Sep 17 00:00:00 2001 From: puhui999 Date: Wed, 18 Jun 2025 12:45:22 +0800 Subject: [PATCH 05/89] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0MP4=E6=96=87=E4=BB=B6=E4=B9=8B=E5=90=8E=E9=80=9A?= =?UTF-8?q?=E8=BF=87=E4=B8=8B=E8=BD=BD=E8=BF=9E=E6=8E=A5=E4=B8=8B=E8=BD=BD?= =?UTF-8?q?MP4=E6=96=87=E4=BB=B6=E6=BA=90=E6=96=87=E4=BB=B6=E8=83=BD?= =?UTF-8?q?=E9=80=9A=E8=BF=87windows=E8=87=AA=E5=B8=A6=E6=92=AD=E6=94=BE?= =?UTF-8?q?=E5=99=A8=E6=92=AD=E6=94=BE=E4=B8=8B=E8=BD=BD=E4=B8=8B=E6=9D=A5?= =?UTF-8?q?=E7=9A=84=E6=92=AD=E6=94=BE=E4=B8=8D=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infra/framework/file/core/utils/FileTypeUtils.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/utils/FileTypeUtils.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/utils/FileTypeUtils.java index e2c607842f..9cc4175884 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/utils/FileTypeUtils.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/utils/FileTypeUtils.java @@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.infra.framework.file.core.utils; import cn.hutool.core.io.IoUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.util.http.HttpUtils; -import com.alibaba.ttl.TransmittableThreadLocal; import jakarta.servlet.http.HttpServletResponse; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; @@ -86,8 +85,8 @@ public class FileTypeUtils { response.setContentType(contentType); // 针对 video 的特殊处理,解决视频地址在移动端播放的兼容性问题 if (StrUtil.containsIgnoreCase(contentType, "video")) { - response.setHeader("Content-Length", String.valueOf(content.length - 1)); - response.setHeader("Content-Range", String.valueOf(content.length - 1)); + response.setHeader("Content-Length", String.valueOf(content.length)); + response.setHeader("Content-Range", "bytes 0-" + (content.length - 1) + "/" + content.length); response.setHeader("Accept-Ranges", "bytes"); } // 输出附件 From 9fc4a4061fc9a5a32ad339338610126a85e1b785 Mon Sep 17 00:00:00 2001 From: puhui999 Date: Wed, 18 Jun 2025 15:40:51 +0800 Subject: [PATCH 06/89] =?UTF-8?q?fix:=20=E5=88=9B=E5=BB=BA=E6=8B=BC?= =?UTF-8?q?=E5=9B=A2=E6=B4=BB=E5=8A=A8=E6=97=B6=E6=A0=A1=E9=AA=8C=E5=95=86?= =?UTF-8?q?=E5=93=81=E6=98=AF=E5=90=A6=E5=8F=82=E4=B8=8E=E4=BA=86=E5=85=B6?= =?UTF-8?q?=E5=AE=83=E6=B4=BB=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/combination/CombinationActivityServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationActivityServiceImpl.java b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationActivityServiceImpl.java index 8e168f4f9f..f5b38a13cc 100644 --- a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationActivityServiceImpl.java +++ b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationActivityServiceImpl.java @@ -86,7 +86,7 @@ public class CombinationActivityServiceImpl implements CombinationActivityServic activityList.removeIf(item -> ObjectUtil.equal(item.getId(), activityId)); } // 查找是否有其它活动,选择了该产品 - List matchActivityList = filterList(activityList, activity -> ObjectUtil.equal(activity.getId(), spuId)); + List matchActivityList = filterList(activityList, activity -> ObjectUtil.equal(activity.getSpuId(), spuId)); if (CollUtil.isNotEmpty(matchActivityList)) { throw exception(COMBINATION_ACTIVITY_SPU_CONFLICTS); } From cac82a13a7db455751159901c666a77d74feb171 Mon Sep 17 00:00:00 2001 From: puhui999 Date: Wed, 18 Jun 2025 17:23:59 +0800 Subject: [PATCH 07/89] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=8B=BC?= =?UTF-8?q?=E5=9B=A2=E8=BF=98=E6=B2=A1=E6=88=90=E5=9B=A2=EF=BC=8C=E8=99=9A?= =?UTF-8?q?=E6=8B=9F=E6=88=90=E5=9B=A2=E6=98=AF=E5=90=A6=EF=BC=8C=E8=BF=99?= =?UTF-8?q?=E4=B8=AA=E6=97=B6=E5=80=99=E6=A0=B8=E9=94=80=E4=B9=9F=E8=83=BD?= =?UTF-8?q?=E6=A0=B8=E9=94=80=E6=88=90=E5=8A=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/trade/enums/ErrorCodeConstants.java | 3 ++- .../service/order/TradeOrderUpdateServiceImpl.java | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/ErrorCodeConstants.java b/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/ErrorCodeConstants.java index b2d72e4b01..8ec1159244 100644 --- a/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/ErrorCodeConstants.java +++ b/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/ErrorCodeConstants.java @@ -39,7 +39,8 @@ public interface ErrorCodeConstants { ErrorCode ORDER_UPDATE_PAID_ORDER_REFUNDED_FAIL_REFUND_NOT_FOUND = new ErrorCode(1_011_000_034, "交易订单更新支付订单退款状态失败,原因:退款单不存在"); ErrorCode ORDER_UPDATE_PAID_ORDER_REFUNDED_FAIL_REFUND_STATUS_NOT_SUCCESS = new ErrorCode(1_011_000_035, "交易订单更新支付订单退款状态失败,原因:退款单状态不是【退款成功】"); ErrorCode ORDER_PICK_UP_FAIL_NOT_VERIFY_USER = new ErrorCode(1_011_000_036, "交易订单自提失败,原因:你没有核销该门店订单的权限"); - ErrorCode ORDER_CREATE_FAIL_INSUFFICIENT_USER_POINTS = new ErrorCode(1_011_000_037, "交易订单创建失败,原因:用户积分不足"); + ErrorCode ORDER_PICK_UP_FAIL_COMBINATION_NOT_SUCCESS = new ErrorCode(1_011_000_037, "交易订单自提失败,原因:商品拼团记录不是【成功】状态"); + ErrorCode ORDER_CREATE_FAIL_INSUFFICIENT_USER_POINTS = new ErrorCode(1_011_000_038, "交易订单创建失败,原因:用户积分不足"); // ========== After Sale 模块 1-011-000-100 ========== ErrorCode AFTER_SALE_NOT_FOUND = new ErrorCode(1_011_000_100, "售后单不存在"); diff --git a/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java index 8dcc0ad75e..bafe56c5be 100644 --- a/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java +++ b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java @@ -25,6 +25,9 @@ import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum; import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum; import cn.iocoder.yudao.module.product.api.comment.ProductCommentApi; import cn.iocoder.yudao.module.product.api.comment.dto.ProductCommentCreateReqDTO; +import cn.iocoder.yudao.module.promotion.api.combination.CombinationRecordApi; +import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordRespDTO; +import cn.iocoder.yudao.module.promotion.enums.combination.CombinationRecordStatusEnum; import cn.iocoder.yudao.module.system.api.social.SocialClientApi; import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaSubscribeMessageSendReqDTO; import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderDeliveryReqVO; @@ -121,6 +124,8 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService { public SocialClientApi socialClientApi; @Resource public PayRefundApi payRefundApi; + @Resource + private CombinationRecordApi combinationRecordApi; @Resource private TradeOrderProperties tradeOrderProperties; @@ -775,6 +780,14 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService { if (ObjUtil.notEqual(DeliveryTypeEnum.PICK_UP.getType(), order.getDeliveryType())) { throw exception(ORDER_RECEIVE_FAIL_DELIVERY_TYPE_NOT_PICK_UP); } + // 情况一:如果是拼团订单,则校验拼团是否成功 + if (TradeOrderTypeEnum.isCombination(order.getType())) { + CombinationRecordRespDTO combinationRecord = combinationRecordApi.getCombinationRecordByOrderId( + order.getUserId(), order.getId()); + if (!CombinationRecordStatusEnum.isSuccess(combinationRecord.getStatus())) { + throw exception(ORDER_PICK_UP_FAIL_COMBINATION_NOT_SUCCESS); + } + } DeliveryPickUpStoreDO deliveryPickUpStore = pickUpStoreService.getDeliveryPickUpStore(order.getPickUpStoreId()); if (deliveryPickUpStore == null || !CollUtil.contains(deliveryPickUpStore.getVerifyUserIds(), userId)) { From c611f1ad15f8d564142c25a303dce4359ebe38cb Mon Sep 17 00:00:00 2001 From: xubinbin0303 Date: Tue, 1 Jul 2025 19:17:15 +0800 Subject: [PATCH 08/89] =?UTF-8?q?refactor.trade:=20=E4=BC=98=E5=8C=96=20Kd?= =?UTF-8?q?100ExpressQueryRespDTO=20=E4=B8=AD=E6=97=B6=E9=97=B4=E5=AD=97?= =?UTF-8?q?=E6=AE=B5=E7=9A=84=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 timeStr 字段用于存储原始时间字符串 - 添加 parseTime 方法将时间字符串解析为 LocalDateTime 对象- 保留原有的 @JsonFormat 注解以确保序列化格式不变 --- .../core/client/dto/kd100/Kd100ExpressQueryRespDTO.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/dto/kd100/Kd100ExpressQueryRespDTO.java b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/dto/kd100/Kd100ExpressQueryRespDTO.java index 9d33cac21b..0b3034737b 100644 --- a/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/dto/kd100/Kd100ExpressQueryRespDTO.java +++ b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/dto/kd100/Kd100ExpressQueryRespDTO.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.List; import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @@ -58,9 +59,13 @@ public class Kd100ExpressQueryRespDTO { /** * 轨迹发生时间 */ - @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT) - private LocalDateTime time; + private String timeStr; + @JsonProperty("time") + public void parseTime(String timeStr) { + this.time = LocalDateTime.parse(timeStr, DateTimeFormatter.ofPattern(FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)); + } + private LocalDateTime time; /** * 轨迹描述 */ From 14a233afceee4c3faa8803ad8d2df352924f3a3d Mon Sep 17 00:00:00 2001 From: ningjingzhiyuan1024 <596928539@qq.com> Date: Sat, 5 Jul 2025 16:18:06 +0800 Subject: [PATCH 09/89] =?UTF-8?q?refactor(trade):=20=E4=BC=98=E5=8C=96=20K?= =?UTF-8?q?d100ExpressQueryRespDTO=20=E4=B8=AD=E6=97=B6=E9=97=B4=E5=AD=97?= =?UTF-8?q?=E6=AE=B5=E7=9A=84=E5=8F=8D=E5=BA=8F=E5=88=97=E5=8C=96=E6=96=B9?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除手动解析时间字符串的方法 - 使用 @JsonFormat 和 @JsonDeserialize 注解实现自动反序列化 - 提高代码简洁性和可维护性 --- .../core/client/dto/kd100/Kd100ExpressQueryRespDTO.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/dto/kd100/Kd100ExpressQueryRespDTO.java b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/dto/kd100/Kd100ExpressQueryRespDTO.java index 0b3034737b..2c0f8cba44 100644 --- a/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/dto/kd100/Kd100ExpressQueryRespDTO.java +++ b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/dto/kd100/Kd100ExpressQueryRespDTO.java @@ -62,9 +62,8 @@ public class Kd100ExpressQueryRespDTO { private String timeStr; @JsonProperty("time") - public void parseTime(String timeStr) { - this.time = LocalDateTime.parse(timeStr, DateTimeFormatter.ofPattern(FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)); - } + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT) + @JsonDeserialize(using = LocalDateTimeDeserializer.class) private LocalDateTime time; /** * 轨迹描述 From 92410453652c6bb93f6056921f20f0f74435fa35 Mon Sep 17 00:00:00 2001 From: ningjingzhiyuan1024 <596928539@qq.com> Date: Sat, 5 Jul 2025 16:18:06 +0800 Subject: [PATCH 10/89] =?UTF-8?q?refactor.trade:=20=E7=A7=BB=E9=99=A4=20Kd?= =?UTF-8?q?100ExpressQueryRespDTO=20=E4=B8=AD=E7=9A=84=E5=86=97=E4=BD=99?= =?UTF-8?q?=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除了 Kd100ExpressQueryRespDTO 类中的 timeStr 字段 - 保留了已有的 time 字段,用于表示轨迹发生时间 --- .../core/client/dto/kd100/Kd100ExpressQueryRespDTO.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/dto/kd100/Kd100ExpressQueryRespDTO.java b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/dto/kd100/Kd100ExpressQueryRespDTO.java index 0b3034737b..6a8e83e3b3 100644 --- a/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/dto/kd100/Kd100ExpressQueryRespDTO.java +++ b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/dto/kd100/Kd100ExpressQueryRespDTO.java @@ -59,12 +59,9 @@ public class Kd100ExpressQueryRespDTO { /** * 轨迹发生时间 */ - private String timeStr; - @JsonProperty("time") - public void parseTime(String timeStr) { - this.time = LocalDateTime.parse(timeStr, DateTimeFormatter.ofPattern(FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)); - } + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT) + @JsonDeserialize(using = LocalDateTimeDeserializer.class) private LocalDateTime time; /** * 轨迹描述 From a6901845248a56c65b2a01d43110cbde8dc45a7f Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 6 Jul 2025 16:19:45 +0800 Subject: [PATCH 11/89] =?UTF-8?q?feat=EF=BC=9A=E3=80=90ERP=20=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E3=80=91=E6=B8=85=E7=90=86=E5=A4=9A=E4=BD=99=E7=9A=84?= =?UTF-8?q?=20ErpStatisticsServiceTest=20=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../statistics/ErpStatisticsServiceTest.java | 155 ------------------ 1 file changed, 155 deletions(-) delete mode 100644 yudao-module-erp/src/test/java/cn/iocoder/yudao/module/erp/service/statistics/ErpStatisticsServiceTest.java diff --git a/yudao-module-erp/src/test/java/cn/iocoder/yudao/module/erp/service/statistics/ErpStatisticsServiceTest.java b/yudao-module-erp/src/test/java/cn/iocoder/yudao/module/erp/service/statistics/ErpStatisticsServiceTest.java deleted file mode 100644 index c96f2d35fa..0000000000 --- a/yudao-module-erp/src/test/java/cn/iocoder/yudao/module/erp/service/statistics/ErpStatisticsServiceTest.java +++ /dev/null @@ -1,155 +0,0 @@ -package cn.iocoder.yudao.module.erp.service.statistics; - -import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; -import cn.iocoder.yudao.module.erp.dal.mysql.statistics.ErpPurchaseStatisticsMapper; -import cn.iocoder.yudao.module.erp.dal.mysql.statistics.ErpSaleStatisticsMapper; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.test.context.ActiveProfiles; - -import jakarta.annotation.Resource; -import java.math.BigDecimal; -import java.time.LocalDateTime; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; - -/** - * ERP 统计服务测试类 - * 主要测试在多租户关闭情况下,统计查询是否能正常工作 - * - * @author 芋道源码 - */ -@SpringBootTest -@ActiveProfiles("unit-test") -public class ErpStatisticsServiceTest { - - @Resource - private ErpSaleStatisticsService saleStatisticsService; - - @Resource - private ErpPurchaseStatisticsService purchaseStatisticsService; - - @MockBean - private ErpSaleStatisticsMapper saleStatisticsMapper; - - @MockBean - private ErpPurchaseStatisticsMapper purchaseStatisticsMapper; - - @BeforeEach - void setUp() { - // 清理租户上下文 - TenantContextHolder.clear(); - } - - @AfterEach - void tearDown() { - // 清理租户上下文 - TenantContextHolder.clear(); - } - - @Test - void testSaleStatisticsWithoutTenant() { - // 准备参数 - LocalDateTime beginTime = LocalDateTime.of(2024, 1, 1, 0, 0, 0); - LocalDateTime endTime = LocalDateTime.of(2024, 1, 31, 23, 59, 59); - BigDecimal expectedPrice = new BigDecimal("1000.00"); - - // Mock 返回值 - when(saleStatisticsMapper.getSalePrice(any(LocalDateTime.class), any(LocalDateTime.class))) - .thenReturn(expectedPrice); - - // 测试:在没有租户ID的情况下调用销售统计 - assertDoesNotThrow(() -> { - BigDecimal result = saleStatisticsService.getSalePrice(beginTime, endTime); - assertEquals(expectedPrice, result); - }, "在多租户关闭时,销售统计查询应该能正常工作"); - } - - @Test - void testPurchaseStatisticsWithoutTenant() { - // 准备参数 - LocalDateTime beginTime = LocalDateTime.of(2024, 1, 1, 0, 0, 0); - LocalDateTime endTime = LocalDateTime.of(2024, 1, 31, 23, 59, 59); - BigDecimal expectedPrice = new BigDecimal("800.00"); - - // Mock 返回值 - when(purchaseStatisticsMapper.getPurchasePrice(any(LocalDateTime.class), any(LocalDateTime.class))) - .thenReturn(expectedPrice); - - // 测试:在没有租户ID的情况下调用采购统计 - assertDoesNotThrow(() -> { - BigDecimal result = purchaseStatisticsService.getPurchasePrice(beginTime, endTime); - assertEquals(expectedPrice, result); - }, "在多租户关闭时,采购统计查询应该能正常工作"); - } - - @Test - void testSaleStatisticsWithTenant() { - // 设置租户ID - Long tenantId = 1L; - TenantContextHolder.setTenantId(tenantId); - - // 准备参数 - LocalDateTime beginTime = LocalDateTime.of(2024, 1, 1, 0, 0, 0); - LocalDateTime endTime = LocalDateTime.of(2024, 1, 31, 23, 59, 59); - BigDecimal expectedPrice = new BigDecimal("1500.00"); - - // Mock 返回值 - when(saleStatisticsMapper.getSalePrice(any(LocalDateTime.class), any(LocalDateTime.class))) - .thenReturn(expectedPrice); - - // 测试:在有租户ID的情况下调用销售统计 - assertDoesNotThrow(() -> { - BigDecimal result = saleStatisticsService.getSalePrice(beginTime, endTime); - assertEquals(expectedPrice, result); - }, "在多租户开启时,销售统计查询应该能正常工作"); - - // 验证租户ID是否正确设置 - assertEquals(tenantId, TenantContextHolder.getTenantId()); - } - - @Test - void testPurchaseStatisticsWithTenant() { - // 设置租户ID - Long tenantId = 2L; - TenantContextHolder.setTenantId(tenantId); - - // 准备参数 - LocalDateTime beginTime = LocalDateTime.of(2024, 1, 1, 0, 0, 0); - LocalDateTime endTime = LocalDateTime.of(2024, 1, 31, 23, 59, 59); - BigDecimal expectedPrice = new BigDecimal("1200.00"); - - // Mock 返回值 - when(purchaseStatisticsMapper.getPurchasePrice(any(LocalDateTime.class), any(LocalDateTime.class))) - .thenReturn(expectedPrice); - - // 测试:在有租户ID的情况下调用采购统计 - assertDoesNotThrow(() -> { - BigDecimal result = purchaseStatisticsService.getPurchasePrice(beginTime, endTime); - assertEquals(expectedPrice, result); - }, "在多租户开启时,采购统计查询应该能正常工作"); - - // 验证租户ID是否正确设置 - assertEquals(tenantId, TenantContextHolder.getTenantId()); - } - - @Test - void testTenantContextHolderMethods() { - // 测试 getTenantId() 在没有设置租户时返回 null - assertNull(TenantContextHolder.getTenantId(), "未设置租户时应该返回 null"); - - // 设置租户ID - Long tenantId = 3L; - TenantContextHolder.setTenantId(tenantId); - assertEquals(tenantId, TenantContextHolder.getTenantId(), "设置租户后应该能正确获取"); - - // 清理租户上下文 - TenantContextHolder.clear(); - assertNull(TenantContextHolder.getTenantId(), "清理后应该返回 null"); - } -} From eca9307344e235e506b53eddae12c3fdabeb224c Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 6 Jul 2025 16:38:14 +0800 Subject: [PATCH 12/89] =?UTF-8?q?fix=EF=BC=9A=E3=80=90MALL=20=E5=95=86?= =?UTF-8?q?=E5=9F=8E=E3=80=91=E4=BF=AE=E5=A4=8D=E9=80=80=E6=AC=BE=E9=80=9A?= =?UTF-8?q?=E7=9F=A5=E4=B8=AD=20merchantRefundId=20=E5=AD=97=E7=AC=A6?= =?UTF-8?q?=E4=B8=B2=E8=BD=AC=E6=8D=A2=20Long=20=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/admin/aftersale/AfterSaleController.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/aftersale/AfterSaleController.java b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/aftersale/AfterSaleController.java index d2165cb868..2bc654b290 100644 --- a/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/aftersale/AfterSaleController.java +++ b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/aftersale/AfterSaleController.java @@ -141,9 +141,8 @@ public class AfterSaleController { public CommonResult updateAfterSaleRefunded(@RequestBody PayRefundNotifyReqDTO notifyReqDTO) { log.info("[updateAfterRefund][notifyReqDTO({})]", notifyReqDTO); if (StrUtil.startWithAny(notifyReqDTO.getMerchantRefundId(), "order-")) { - tradeOrderUpdateService.updatePaidOrderRefunded( - Long.parseLong(notifyReqDTO.getMerchantRefundId()), - notifyReqDTO.getPayRefundId()); + Long orderId = Long.parseLong(StrUtil.subAfter(notifyReqDTO.getMerchantRefundId(), "order-", true)); + tradeOrderUpdateService.updatePaidOrderRefunded(orderId, notifyReqDTO.getPayRefundId()); } else { afterSaleService.updateAfterSaleRefunded( Long.parseLong(notifyReqDTO.getMerchantRefundId()), From d8e1610495fb3dfc66c1d368d4e26af952e8d37f Mon Sep 17 00:00:00 2001 From: YunaiV Date: Tue, 8 Jul 2025 09:39:44 +0800 Subject: [PATCH 13/89] feat: enhance MyBatis utility for sorting and update SQL aliases in TradeOrderStatisticsMapper --- .../mybatis/core/mapper/BaseMapperX.java | 1 + .../mybatis/core/util/MyBatisUtils.java | 27 ++++++++++++++----- .../trade/TradeOrderStatisticsMapper.xml | 8 +++--- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java index d7ad5fad8f..52ca947cc5 100644 --- a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java @@ -42,6 +42,7 @@ public interface BaseMapperX extends MPJBaseMapper { default PageResult selectPage(PageParam pageParam, Collection sortingFields, @Param("ew") Wrapper queryWrapper) { // 特殊:不分页,直接查询全部 if (PageParam.PAGE_SIZE_NONE.equals(pageParam.getPageSize())) { + MyBatisUtils.addOrder(queryWrapper, sortingFields); List list = selectList(queryWrapper); return new PageResult<>(list, (long) list.size()); } diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/util/MyBatisUtils.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/util/MyBatisUtils.java index 784f699e44..ac33ba8eff 100644 --- a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/util/MyBatisUtils.java +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/util/MyBatisUtils.java @@ -1,6 +1,6 @@ package cn.iocoder.yudao.framework.mybatis.core.util; -import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.func.Func1; import cn.hutool.core.lang.func.LambdaUtil; import cn.hutool.core.util.StrUtil; @@ -8,6 +8,8 @@ import cn.iocoder.yudao.framework.common.pojo.PageParam; import cn.iocoder.yudao.framework.common.pojo.SortingField; import cn.iocoder.yudao.framework.mybatis.core.enums.DbTypeEnum; import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.metadata.OrderItem; import com.baomidou.mybatisplus.core.toolkit.StringPool; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; @@ -20,7 +22,6 @@ import net.sf.jsqlparser.schema.Table; import java.util.ArrayList; import java.util.Collection; import java.util.List; -import java.util.stream.Collectors; /** * MyBatis 工具类 @@ -37,15 +38,27 @@ public class MyBatisUtils { // 页码 + 数量 Page page = new Page<>(pageParam.getPageNo(), pageParam.getPageSize()); // 排序字段 - if (!CollectionUtil.isEmpty(sortingFields)) { - page.addOrder(sortingFields.stream().map(sortingField -> SortingField.ORDER_ASC.equals(sortingField.getOrder()) - ? OrderItem.asc(StrUtil.toUnderlineCase(sortingField.getField())) - : OrderItem.desc(StrUtil.toUnderlineCase(sortingField.getField()))) - .collect(Collectors.toList())); + if (CollUtil.isNotEmpty(sortingFields)) { + for (SortingField sortingField : sortingFields) { + page.addOrder(new OrderItem().setAsc(SortingField.ORDER_ASC.equals(sortingField.getOrder())) + .setColumn(StrUtil.toUnderlineCase(sortingField.getField()))); + } } return page; } + public static void addOrder(Wrapper wrapper, Collection sortingFields) { + if (CollUtil.isEmpty(sortingFields)) { + return; + } + QueryWrapper query = (QueryWrapper) wrapper; + for (SortingField sortingField : sortingFields) { + query.orderBy(true, + SortingField.ORDER_ASC.equals(sortingField.getOrder()), + StrUtil.toUnderlineCase(sortingField.getField())); + } + } + /** * 将拦截器添加到链中 * 由于 MybatisPlusInterceptor 不支持添加拦截器,所以只能全量设置 diff --git a/yudao-module-mall/yudao-module-statistics/src/main/resources/mapper/trade/TradeOrderStatisticsMapper.xml b/yudao-module-mall/yudao-module-statistics/src/main/resources/mapper/trade/TradeOrderStatisticsMapper.xml index 07d2f0d580..e47c85e9b4 100644 --- a/yudao-module-mall/yudao-module-statistics/src/main/resources/mapper/trade/TradeOrderStatisticsMapper.xml +++ b/yudao-module-mall/yudao-module-statistics/src/main/resources/mapper/trade/TradeOrderStatisticsMapper.xml @@ -76,8 +76,8 @@ - SELECT IFNULL(SUM(pay_price), 0) AS orderPayPrice, - COUNT(1) AS orderPayCount + SELECT IFNULL(SUM(pay_price), 0) AS order_pay_price, + COUNT(1) AS order_pay_count FROM trade_order WHERE pay_status = #{payStatus} AND pay_time BETWEEN #{beginTime} AND #{endTime} From 563985dcfc9e68ddc208f72c811ff0b2ced83cf2 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Wed, 9 Jul 2025 19:17:50 +0800 Subject: [PATCH 14/89] =?UTF-8?q?feat=EF=BC=9A=E3=80=90MP=20=E5=85=AC?= =?UTF-8?q?=E4=BC=97=E5=8F=B7=E3=80=91=E5=85=BC=E5=AE=B9=E6=97=A0=20API=20?= =?UTF-8?q?=E6=9D=83=E9=99=90=E7=9A=84=E4=B8=AA=E4=BA=BA=E5=85=AC=E4=BC=97?= =?UTF-8?q?=E5=8F=B7=E5=9C=BA=E6=99=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mp/service/handler/user/SubscribeHandler.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/yudao-module-mp/src/main/java/cn/iocoder/yudao/module/mp/service/handler/user/SubscribeHandler.java b/yudao-module-mp/src/main/java/cn/iocoder/yudao/module/mp/service/handler/user/SubscribeHandler.java index 14540836a7..0f68baf39e 100644 --- a/yudao-module-mp/src/main/java/cn/iocoder/yudao/module/mp/service/handler/user/SubscribeHandler.java +++ b/yudao-module-mp/src/main/java/cn/iocoder/yudao/module/mp/service/handler/user/SubscribeHandler.java @@ -1,10 +1,13 @@ package cn.iocoder.yudao.module.mp.service.handler.user; +import cn.hutool.core.util.ObjUtil; import cn.iocoder.yudao.module.mp.framework.mp.core.context.MpContextHolder; import cn.iocoder.yudao.module.mp.service.message.MpAutoReplyService; import cn.iocoder.yudao.module.mp.service.user.MpUserService; +import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.error.WxMpErrorMsgEnum; import me.chanjar.weixin.common.session.WxSessionManager; import me.chanjar.weixin.mp.api.WxMpMessageHandler; import me.chanjar.weixin.mp.api.WxMpService; @@ -13,7 +16,6 @@ import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; import me.chanjar.weixin.mp.bean.result.WxMpUser; import org.springframework.stereotype.Component; -import jakarta.annotation.Resource; import java.util.Map; /** @@ -40,6 +42,13 @@ public class SubscribeHandler implements WxMpMessageHandler { wxMpUser = weixinService.getUserService().userInfo(wxMessage.getFromUser()); } catch (WxErrorException e) { log.error("[handle][粉丝({})] 获取粉丝信息失败!", wxMessage.getFromUser(), e); + // 特殊情况(个人账号,无接口权限):https://t.zsxq.com/cLFq5 + if (ObjUtil.equal(e.getError().getErrorCode(), WxMpErrorMsgEnum.CODE_48001)) { + wxMpUser = new WxMpUser(); + wxMpUser.setOpenId(wxMessage.getFromUser()); + wxMpUser.setSubscribe(true); + wxMpUser.setSubscribeTime(System.currentTimeMillis() / 1000L); + } } // 第二步,保存粉丝信息 From 7fc522938d1eadd09ea1a0c43413b47b92a523b7 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Wed, 9 Jul 2025 19:27:07 +0800 Subject: [PATCH 15/89] =?UTF-8?q?fix=EF=BC=9A=E3=80=90SYSTEM=20=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E7=AE=A1=E7=90=86=E3=80=91=E7=A4=BE=E4=BA=A4=E7=BB=91?= =?UTF-8?q?=E5=AE=9A=EF=BC=8C=E4=BD=BF=E7=94=A8=20code=20=E6=8E=88?= =?UTF-8?q?=E6=9D=83=E7=A0=81=E7=9A=84=E5=8F=82=E6=95=B0=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../system/controller/admin/socail/SocialUserController.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/SocialUserController.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/SocialUserController.java index 77a77daa1f..f26ca4d937 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/SocialUserController.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/SocialUserController.java @@ -38,7 +38,8 @@ public class SocialUserController { @PostMapping("/bind") @Operation(summary = "社交绑定,使用 code 授权码") public CommonResult socialBind(@RequestBody @Valid SocialUserBindReqVO reqVO) { - socialUserService.bindSocialUser(BeanUtils.toBean(reqVO, SocialUserBindReqDTO.class) + socialUserService.bindSocialUser(new SocialUserBindReqDTO().setSocialType(reqVO.getType()) + .setCode(reqVO.getCode()).setState(reqVO.getState()) .setUserId(getLoginUserId()).setUserType(UserTypeEnum.ADMIN.getValue())); return CommonResult.success(true); } From bda357508a825557877b43a7d1b7ba864d659b87 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Wed, 9 Jul 2025 22:37:48 +0800 Subject: [PATCH 16/89] =?UTF-8?q?feat=EF=BC=9A=E3=80=90SYSTEM=20=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E7=AE=A1=E7=90=86=E3=80=91SecurityFrameworkUtils=20?= =?UTF-8?q?=E7=9A=84=20setLoginUser=20=E6=96=B9=E6=B3=95=EF=BC=8C=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E5=AF=B9=20request=20=E5=BC=BA=E4=BE=9D=E8=B5=96?= =?UTF-8?q?=EF=BC=8C=E5=8F=AF=E7=94=A8=E4=BA=8E=20Job=20=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E6=93=8D=E4=BD=9C=E6=97=A5=E5=BF=97=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/core/util/SecurityFrameworkUtils.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java index 98dc7afb8d..4f8024474b 100644 --- a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java +++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java @@ -126,8 +126,10 @@ public class SecurityFrameworkUtils { // 额外设置到 request 中,用于 ApiAccessLogFilter 可以获取到用户编号; // 原因是,Spring Security 的 Filter 在 ApiAccessLogFilter 后面,在它记录访问日志时,线上上下文已经没有用户编号等信息 - WebFrameworkUtils.setLoginUserId(request, loginUser.getId()); - WebFrameworkUtils.setLoginUserType(request, loginUser.getUserType()); + if (request != null) { + WebFrameworkUtils.setLoginUserId(request, loginUser.getId()); + WebFrameworkUtils.setLoginUserType(request, loginUser.getUserType()); + } } private static Authentication buildAuthentication(LoginUser loginUser, HttpServletRequest request) { From 569ff42e6f9d2a4499ca0cfe113585e705ab59f2 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Wed, 9 Jul 2025 23:24:17 +0800 Subject: [PATCH 17/89] =?UTF-8?q?feat=EF=BC=9A=E3=80=90SYSTEM=20=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E7=AE=A1=E7=90=86=E3=80=91=E4=BC=98=E5=8C=96=E6=94=AF?= =?UTF-8?q?=E6=8C=81=20SimpleAsyncTaskExecutor=20=E5=BC=82=E6=AD=A5?= =?UTF-8?q?=E7=BA=BF=E7=A8=8B=E6=B1=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/YudaoAsyncAutoConfiguration.java | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/config/YudaoAsyncAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/config/YudaoAsyncAutoConfiguration.java index 6d517e5e40..4b08210971 100644 --- a/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/config/YudaoAsyncAutoConfiguration.java +++ b/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/config/YudaoAsyncAutoConfiguration.java @@ -7,6 +7,7 @@ import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.core.task.SimpleAsyncTaskExecutor; /** * 异步任务 Configuration @@ -21,13 +22,20 @@ public class YudaoAsyncAutoConfiguration { @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { - if (!(bean instanceof ThreadPoolTaskExecutor)) { - return bean; + // 处理 ThreadPoolTaskExecutor + if (bean instanceof ThreadPoolTaskExecutor) { + ThreadPoolTaskExecutor executor = (ThreadPoolTaskExecutor) bean; + executor.setTaskDecorator(TtlRunnable::get); + return executor; } - // 修改提交的任务,接入 TransmittableThreadLocal - ThreadPoolTaskExecutor executor = (ThreadPoolTaskExecutor) bean; - executor.setTaskDecorator(TtlRunnable::get); - return executor; + // 处理 SimpleAsyncTaskExecutor + // 参考 https://t.zsxq.com/CBoks 增加 + if (bean instanceof SimpleAsyncTaskExecutor) { + SimpleAsyncTaskExecutor executor = (SimpleAsyncTaskExecutor) bean; + executor.setTaskDecorator(TtlRunnable::get); + return executor; + } + return bean; } }; From 7a5e28d08a44197e44c58de78ba85a4e6599b03a Mon Sep 17 00:00:00 2001 From: YunaiV Date: Fri, 11 Jul 2025 21:57:09 +0800 Subject: [PATCH 18/89] =?UTF-8?q?fix=EF=BC=9A=E3=80=90SYSTEM=20=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E7=AE=A1=E7=90=86=E3=80=91=E5=A2=9E=E5=8A=A0=20lombok?= =?UTF-8?q?-mapstruct-binding=20=E4=BE=9D=E8=B5=96=EF=BC=8C=E8=A7=A3?= =?UTF-8?q?=E5=86=B3=20IDEA=20=E5=81=B6=E7=84=B6=E5=87=BA=E7=8E=B0=20No=20?= =?UTF-8?q?property=20named=20=E2=80=9Cxxx"=20exists=20=E7=9A=84=E7=BC=96?= =?UTF-8?q?=E8=AF=91=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pom.xml b/pom.xml index f5f0a32e36..b271f1d4b9 100644 --- a/pom.xml +++ b/pom.xml @@ -87,6 +87,13 @@ lombok ${lombok.version} + + + org.projectlombok + lombok-mapstruct-binding + 0.2.0 + org.mapstruct mapstruct-processor From 285da1398924a19b57b1f2951beaa61927e09374 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Fri, 11 Jul 2025 22:04:13 +0800 Subject: [PATCH 19/89] =?UTF-8?q?fix=EF=BC=9A=E3=80=90SYSTEM=20=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E7=AE=A1=E7=90=86=E3=80=91=E4=BF=AE=E5=A4=8D=20Defaul?= =?UTF-8?q?tDBFieldHandler=20=E5=9C=A8=20Async=20=E6=83=85=E5=86=B5?= =?UTF-8?q?=E4=B8=8B=EF=BC=8C=E6=97=A0=E6=B3=95=E8=8E=B7=E5=8F=96=E5=88=B0?= =?UTF-8?q?=20getLoginUserId=20=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yudao-framework/yudao-spring-boot-starter-mybatis/pom.xml | 4 ++-- .../mybatis/core/handler/DefaultDBFieldHandler.java | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/pom.xml b/yudao-framework/yudao-spring-boot-starter-mybatis/pom.xml index 5a619c5118..3272059d85 100644 --- a/yudao-framework/yudao-spring-boot-starter-mybatis/pom.xml +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/pom.xml @@ -24,8 +24,8 @@ cn.iocoder.boot - yudao-spring-boot-starter-web - provided + yudao-spring-boot-starter-security + provided diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/handler/DefaultDBFieldHandler.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/handler/DefaultDBFieldHandler.java index 94dada129b..3c35999109 100644 --- a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/handler/DefaultDBFieldHandler.java +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/handler/DefaultDBFieldHandler.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.framework.mybatis.core.handler; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; -import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; +import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; import org.apache.ibatis.reflection.MetaObject; @@ -32,7 +32,7 @@ public class DefaultDBFieldHandler implements MetaObjectHandler { baseDO.setUpdateTime(current); } - Long userId = WebFrameworkUtils.getLoginUserId(); + Long userId = SecurityFrameworkUtils.getLoginUserId(); // 当前登录用户不为空,创建人为空,则当前登录用户为创建人 if (Objects.nonNull(userId) && Objects.isNull(baseDO.getCreator())) { baseDO.setCreator(userId.toString()); @@ -54,7 +54,7 @@ public class DefaultDBFieldHandler implements MetaObjectHandler { // 当前登录用户不为空,更新人为空,则当前登录用户为更新人 Object modifier = getFieldValByName("updater", metaObject); - Long userId = WebFrameworkUtils.getLoginUserId(); + Long userId = SecurityFrameworkUtils.getLoginUserId(); if (Objects.nonNull(userId) && Objects.isNull(modifier)) { setFieldValByName("updater", userId.toString(), metaObject); } From 520fb79e2a0fef99e524a5659b0a70528b2fcd61 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Fri, 11 Jul 2025 23:42:52 +0800 Subject: [PATCH 20/89] =?UTF-8?q?feat=EF=BC=9A=E3=80=90REPORT=20=E6=8A=A5?= =?UTF-8?q?=E8=A1=A8=E3=80=91jimureport=20from=201.9.4=20to=202.0.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yudao-dependencies/pom.xml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/yudao-dependencies/pom.xml b/yudao-dependencies/pom.xml index 94c3e31aa8..bf6175cdd3 100644 --- a/yudao-dependencies/pom.xml +++ b/yudao-dependencies/pom.xml @@ -74,7 +74,8 @@ 2.30.14 1.16.7 1.4.0 - 1.9.4 + 2.0.0 + 1.9.5 4.7.5.B @@ -603,7 +604,7 @@ org.jeecgframework.jimureport jimubi-spring-boot3-starter - ${jimureport.version} + ${jimubi.version} com.github.jsqlparser From e7c9e3dc231d112639340b292500b29760f09996 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 13 Jul 2025 11:16:14 +0800 Subject: [PATCH 21/89] =?UTF-8?q?fix=EF=BC=9A=E3=80=90INFRA=20=E5=9F=BA?= =?UTF-8?q?=E7=A1=80=E8=AE=BE=E6=96=BD=E3=80=91=E6=9B=B4=E6=96=B0=20pgsql?= =?UTF-8?q?=20quartz=20sql?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sql/postgresql/quartz.sql | 415 +++++++++++++++++--------------------- 1 file changed, 185 insertions(+), 230 deletions(-) diff --git a/sql/postgresql/quartz.sql b/sql/postgresql/quartz.sql index 4ec390c527..46bb938431 100644 --- a/sql/postgresql/quartz.sql +++ b/sql/postgresql/quartz.sql @@ -1,253 +1,208 @@ --- ---------------------------- --- qrtz_blob_triggers --- ---------------------------- -CREATE TABLE qrtz_blob_triggers +-- https://github.com/quartz-scheduler/quartz/blob/main/quartz/src/main/resources/org/quartz/impl/jdbcjobstore/tables_postgres.sql +-- Thanks to Patrick Lightbody for submitting this... +-- +-- In your Quartz properties file, you'll need to set +-- org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.PostgreSQLDelegate + +DROP TABLE IF EXISTS QRTZ_FIRED_TRIGGERS; +DROP TABLE IF EXISTS QRTZ_PAUSED_TRIGGER_GRPS; +DROP TABLE IF EXISTS QRTZ_SCHEDULER_STATE; +DROP TABLE IF EXISTS QRTZ_LOCKS; +DROP TABLE IF EXISTS QRTZ_SIMPLE_TRIGGERS; +DROP TABLE IF EXISTS QRTZ_CRON_TRIGGERS; +DROP TABLE IF EXISTS QRTZ_SIMPROP_TRIGGERS; +DROP TABLE IF EXISTS QRTZ_BLOB_TRIGGERS; +DROP TABLE IF EXISTS QRTZ_TRIGGERS; +DROP TABLE IF EXISTS QRTZ_JOB_DETAILS; +DROP TABLE IF EXISTS QRTZ_CALENDARS; + +CREATE TABLE QRTZ_JOB_DETAILS ( - sched_name varchar(120) NOT NULL, - trigger_name varchar(190) NOT NULL, - trigger_group varchar(190) NOT NULL, - blob_data bytea NULL, - PRIMARY KEY (sched_name, trigger_name, trigger_group) + SCHED_NAME VARCHAR(120) NOT NULL, + JOB_NAME VARCHAR(200) NOT NULL, + JOB_GROUP VARCHAR(200) NOT NULL, + DESCRIPTION VARCHAR(250) NULL, + JOB_CLASS_NAME VARCHAR(250) NOT NULL, + IS_DURABLE BOOL NOT NULL, + IS_NONCONCURRENT BOOL NOT NULL, + IS_UPDATE_DATA BOOL NOT NULL, + REQUESTS_RECOVERY BOOL NOT NULL, + JOB_DATA BYTEA NULL, + PRIMARY KEY (SCHED_NAME, JOB_NAME, JOB_GROUP) ); -CREATE INDEX idx_qrtz_blob_triggers_sched_name ON qrtz_blob_triggers (sched_name, trigger_name, trigger_group); - --- ---------------------------- --- qrtz_calendars --- ---------------------------- -CREATE TABLE qrtz_calendars +CREATE TABLE QRTZ_TRIGGERS ( - sched_name varchar(120) NOT NULL, - calendar_name varchar(190) NOT NULL, - calendar bytea NOT NULL, - PRIMARY KEY (sched_name, calendar_name) + SCHED_NAME VARCHAR(120) NOT NULL, + TRIGGER_NAME VARCHAR(200) NOT NULL, + TRIGGER_GROUP VARCHAR(200) NOT NULL, + JOB_NAME VARCHAR(200) NOT NULL, + JOB_GROUP VARCHAR(200) NOT NULL, + DESCRIPTION VARCHAR(250) NULL, + NEXT_FIRE_TIME BIGINT NULL, + PREV_FIRE_TIME BIGINT NULL, + PRIORITY INTEGER NULL, + TRIGGER_STATE VARCHAR(16) NOT NULL, + TRIGGER_TYPE VARCHAR(8) NOT NULL, + START_TIME BIGINT NOT NULL, + END_TIME BIGINT NULL, + CALENDAR_NAME VARCHAR(200) NULL, + MISFIRE_INSTR SMALLINT NULL, + JOB_DATA BYTEA NULL, + PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP), + FOREIGN KEY (SCHED_NAME, JOB_NAME, JOB_GROUP) + REFERENCES QRTZ_JOB_DETAILS (SCHED_NAME, JOB_NAME, JOB_GROUP) +); + +CREATE TABLE QRTZ_SIMPLE_TRIGGERS +( + SCHED_NAME VARCHAR(120) NOT NULL, + TRIGGER_NAME VARCHAR(200) NOT NULL, + TRIGGER_GROUP VARCHAR(200) NOT NULL, + REPEAT_COUNT BIGINT NOT NULL, + REPEAT_INTERVAL BIGINT NOT NULL, + TIMES_TRIGGERED BIGINT NOT NULL, + PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP), + FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP) + REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP) +); + +CREATE TABLE QRTZ_CRON_TRIGGERS +( + SCHED_NAME VARCHAR(120) NOT NULL, + TRIGGER_NAME VARCHAR(200) NOT NULL, + TRIGGER_GROUP VARCHAR(200) NOT NULL, + CRON_EXPRESSION VARCHAR(120) NOT NULL, + TIME_ZONE_ID VARCHAR(80), + PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP), + FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP) + REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP) +); + +CREATE TABLE QRTZ_SIMPROP_TRIGGERS +( + SCHED_NAME VARCHAR(120) NOT NULL, + TRIGGER_NAME VARCHAR(200) NOT NULL, + TRIGGER_GROUP VARCHAR(200) NOT NULL, + STR_PROP_1 VARCHAR(512) NULL, + STR_PROP_2 VARCHAR(512) NULL, + STR_PROP_3 VARCHAR(512) NULL, + INT_PROP_1 INT NULL, + INT_PROP_2 INT NULL, + LONG_PROP_1 BIGINT NULL, + LONG_PROP_2 BIGINT NULL, + DEC_PROP_1 NUMERIC(13, 4) NULL, + DEC_PROP_2 NUMERIC(13, 4) NULL, + BOOL_PROP_1 BOOL NULL, + BOOL_PROP_2 BOOL NULL, + PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP), + FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP) + REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP) +); + +CREATE TABLE QRTZ_BLOB_TRIGGERS +( + SCHED_NAME VARCHAR(120) NOT NULL, + TRIGGER_NAME VARCHAR(200) NOT NULL, + TRIGGER_GROUP VARCHAR(200) NOT NULL, + BLOB_DATA BYTEA NULL, + PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP), + FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP) + REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP) +); + +CREATE TABLE QRTZ_CALENDARS +( + SCHED_NAME VARCHAR(120) NOT NULL, + CALENDAR_NAME VARCHAR(200) NOT NULL, + CALENDAR BYTEA NOT NULL, + PRIMARY KEY (SCHED_NAME, CALENDAR_NAME) ); --- ---------------------------- --- qrtz_cron_triggers --- ---------------------------- -CREATE TABLE qrtz_cron_triggers +CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS ( - sched_name varchar(120) NOT NULL, - trigger_name varchar(190) NOT NULL, - trigger_group varchar(190) NOT NULL, - cron_expression varchar(120) NOT NULL, - time_zone_id varchar(80) NULL DEFAULT NULL, - PRIMARY KEY (sched_name, trigger_name, trigger_group) + SCHED_NAME VARCHAR(120) NOT NULL, + TRIGGER_GROUP VARCHAR(200) NOT NULL, + PRIMARY KEY (SCHED_NAME, TRIGGER_GROUP) ); --- @formatter:off -BEGIN; -COMMIT; --- @formatter:on - --- ---------------------------- --- qrtz_fired_triggers --- ---------------------------- -CREATE TABLE qrtz_fired_triggers +CREATE TABLE QRTZ_FIRED_TRIGGERS ( - sched_name varchar(120) NOT NULL, - entry_id varchar(95) NOT NULL, - trigger_name varchar(190) NOT NULL, - trigger_group varchar(190) NOT NULL, - instance_name varchar(190) NOT NULL, - fired_time int8 NOT NULL, - sched_time int8 NOT NULL, - priority int4 NOT NULL, - state varchar(16) NOT NULL, - job_name varchar(190) NULL DEFAULT NULL, - job_group varchar(190) NULL DEFAULT NULL, - is_nonconcurrent varchar(1) NULL DEFAULT NULL, - requests_recovery varchar(1) NULL DEFAULT NULL, - PRIMARY KEY (sched_name, entry_id) + SCHED_NAME VARCHAR(120) NOT NULL, + ENTRY_ID VARCHAR(95) NOT NULL, + TRIGGER_NAME VARCHAR(200) NOT NULL, + TRIGGER_GROUP VARCHAR(200) NOT NULL, + INSTANCE_NAME VARCHAR(200) NOT NULL, + FIRED_TIME BIGINT NOT NULL, + SCHED_TIME BIGINT NOT NULL, + PRIORITY INTEGER NOT NULL, + STATE VARCHAR(16) NOT NULL, + JOB_NAME VARCHAR(200) NULL, + JOB_GROUP VARCHAR(200) NULL, + IS_NONCONCURRENT BOOL NULL, + REQUESTS_RECOVERY BOOL NULL, + PRIMARY KEY (SCHED_NAME, ENTRY_ID) ); -CREATE INDEX idx_qrtz_ft_trig_inst_name ON qrtz_fired_triggers (sched_name, instance_name); -CREATE INDEX idx_qrtz_ft_inst_job_req_rcvry ON qrtz_fired_triggers (sched_name, instance_name, requests_recovery); -CREATE INDEX idx_qrtz_ft_j_g ON qrtz_fired_triggers (sched_name, job_name, job_group); -CREATE INDEX idx_qrtz_ft_jg ON qrtz_fired_triggers (sched_name, job_group); -CREATE INDEX idx_qrtz_ft_t_g ON qrtz_fired_triggers (sched_name, trigger_name, trigger_group); -CREATE INDEX idx_qrtz_ft_tg ON qrtz_fired_triggers (sched_name, trigger_group); - --- ---------------------------- --- qrtz_job_details --- ---------------------------- -CREATE TABLE qrtz_job_details +CREATE TABLE QRTZ_SCHEDULER_STATE ( - sched_name varchar(120) NOT NULL, - job_name varchar(190) NOT NULL, - job_group varchar(190) NOT NULL, - description varchar(250) NULL DEFAULT NULL, - job_class_name varchar(250) NOT NULL, - is_durable varchar(1) NOT NULL, - is_nonconcurrent varchar(1) NOT NULL, - is_update_data varchar(1) NOT NULL, - requests_recovery varchar(1) NOT NULL, - job_data bytea NULL, - PRIMARY KEY (sched_name, job_name, job_group) + SCHED_NAME VARCHAR(120) NOT NULL, + INSTANCE_NAME VARCHAR(200) NOT NULL, + LAST_CHECKIN_TIME BIGINT NOT NULL, + CHECKIN_INTERVAL BIGINT NOT NULL, + PRIMARY KEY (SCHED_NAME, INSTANCE_NAME) ); -CREATE INDEX idx_qrtz_j_req_recovery ON qrtz_job_details (sched_name, requests_recovery); -CREATE INDEX idx_qrtz_j_grp ON qrtz_job_details (sched_name, job_group); - --- @formatter:off -BEGIN; -COMMIT; --- @formatter:on - --- ---------------------------- --- qrtz_locks --- ---------------------------- -CREATE TABLE qrtz_locks +CREATE TABLE QRTZ_LOCKS ( - sched_name varchar(120) NOT NULL, - lock_name varchar(40) NOT NULL, - PRIMARY KEY (sched_name, lock_name) + SCHED_NAME VARCHAR(120) NOT NULL, + LOCK_NAME VARCHAR(40) NOT NULL, + PRIMARY KEY (SCHED_NAME, LOCK_NAME) ); --- @formatter:off -BEGIN; -COMMIT; --- @formatter:on +CREATE INDEX IDX_QRTZ_J_REQ_RECOVERY + ON QRTZ_JOB_DETAILS (SCHED_NAME, REQUESTS_RECOVERY); +CREATE INDEX IDX_QRTZ_J_GRP + ON QRTZ_JOB_DETAILS (SCHED_NAME, JOB_GROUP); --- ---------------------------- --- qrtz_paused_trigger_grps --- ---------------------------- -CREATE TABLE qrtz_paused_trigger_grps -( - sched_name varchar(120) NOT NULL, - trigger_group varchar(190) NOT NULL, - PRIMARY KEY (sched_name, trigger_group) -); +CREATE INDEX IDX_QRTZ_T_J + ON QRTZ_TRIGGERS (SCHED_NAME, JOB_NAME, JOB_GROUP); +CREATE INDEX IDX_QRTZ_T_JG + ON QRTZ_TRIGGERS (SCHED_NAME, JOB_GROUP); +CREATE INDEX IDX_QRTZ_T_C + ON QRTZ_TRIGGERS (SCHED_NAME, CALENDAR_NAME); +CREATE INDEX IDX_QRTZ_T_G + ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_GROUP); +CREATE INDEX IDX_QRTZ_T_STATE + ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_STATE); +CREATE INDEX IDX_QRTZ_T_N_STATE + ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP, TRIGGER_STATE); +CREATE INDEX IDX_QRTZ_T_N_G_STATE + ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_GROUP, TRIGGER_STATE); +CREATE INDEX IDX_QRTZ_T_NEXT_FIRE_TIME + ON QRTZ_TRIGGERS (SCHED_NAME, NEXT_FIRE_TIME); +CREATE INDEX IDX_QRTZ_T_NFT_ST + ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_STATE, NEXT_FIRE_TIME); +CREATE INDEX IDX_QRTZ_T_NFT_MISFIRE + ON QRTZ_TRIGGERS (SCHED_NAME, MISFIRE_INSTR, NEXT_FIRE_TIME); +CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE + ON QRTZ_TRIGGERS (SCHED_NAME, MISFIRE_INSTR, NEXT_FIRE_TIME, TRIGGER_STATE); +CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE_GRP + ON QRTZ_TRIGGERS (SCHED_NAME, MISFIRE_INSTR, NEXT_FIRE_TIME, TRIGGER_GROUP, TRIGGER_STATE); --- ---------------------------- --- qrtz_scheduler_state --- ---------------------------- -CREATE TABLE qrtz_scheduler_state -( - sched_name varchar(120) NOT NULL, - instance_name varchar(190) NOT NULL, - last_checkin_time int8 NOT NULL, - checkin_interval int8 NOT NULL, - PRIMARY KEY (sched_name, instance_name) -); - --- @formatter:off -BEGIN; -COMMIT; --- @formatter:on - --- ---------------------------- --- qrtz_simple_triggers --- ---------------------------- -CREATE TABLE qrtz_simple_triggers -( - sched_name varchar(120) NOT NULL, - trigger_name varchar(190) NOT NULL, - trigger_group varchar(190) NOT NULL, - repeat_count int8 NOT NULL, - repeat_interval int8 NOT NULL, - times_triggered int8 NOT NULL, - PRIMARY KEY (sched_name, trigger_name, trigger_group) -); - --- ---------------------------- --- qrtz_simprop_triggers --- ---------------------------- -CREATE TABLE qrtz_simprop_triggers -( - sched_name varchar(120) NOT NULL, - trigger_name varchar(190) NOT NULL, - trigger_group varchar(190) NOT NULL, - str_prop_1 varchar(512) NULL DEFAULT NULL, - str_prop_2 varchar(512) NULL DEFAULT NULL, - str_prop_3 varchar(512) NULL DEFAULT NULL, - int_prop_1 int4 NULL DEFAULT NULL, - int_prop_2 int4 NULL DEFAULT NULL, - long_prop_1 int8 NULL DEFAULT NULL, - long_prop_2 int8 NULL DEFAULT NULL, - dec_prop_1 numeric(13, 4) NULL DEFAULT NULL, - dec_prop_2 numeric(13, 4) NULL DEFAULT NULL, - bool_prop_1 varchar(1) NULL DEFAULT NULL, - bool_prop_2 varchar(1) NULL DEFAULT NULL, - PRIMARY KEY (sched_name, trigger_name, trigger_group) -); - --- ---------------------------- --- qrtz_triggers --- ---------------------------- -CREATE TABLE qrtz_triggers -( - sched_name varchar(120) NOT NULL, - trigger_name varchar(190) NOT NULL, - trigger_group varchar(190) NOT NULL, - job_name varchar(190) NOT NULL, - job_group varchar(190) NOT NULL, - description varchar(250) NULL DEFAULT NULL, - next_fire_time int8 NULL DEFAULT NULL, - prev_fire_time int8 NULL DEFAULT NULL, - priority int4 NULL DEFAULT NULL, - trigger_state varchar(16) NOT NULL, - trigger_type varchar(8) NOT NULL, - start_time int8 NOT NULL, - end_time int8 NULL DEFAULT NULL, - calendar_name varchar(190) NULL DEFAULT NULL, - misfire_instr int2 NULL DEFAULT NULL, - job_data bytea NULL, - PRIMARY KEY (sched_name, trigger_name, trigger_group) -); - -CREATE INDEX idx_qrtz_t_j ON qrtz_triggers (sched_name, job_name, job_group); -CREATE INDEX idx_qrtz_t_jg ON qrtz_triggers (sched_name, job_group); -CREATE INDEX idx_qrtz_t_c ON qrtz_triggers (sched_name, calendar_name); -CREATE INDEX idx_qrtz_t_g ON qrtz_triggers (sched_name, trigger_group); -CREATE INDEX idx_qrtz_t_state ON qrtz_triggers (sched_name, trigger_state); -CREATE INDEX idx_qrtz_t_n_state ON qrtz_triggers (sched_name, trigger_name, trigger_group, trigger_state); -CREATE INDEX idx_qrtz_t_n_g_state ON qrtz_triggers (sched_name, trigger_group, trigger_state); -CREATE INDEX idx_qrtz_t_next_fire_time ON qrtz_triggers (sched_name, next_fire_time); -CREATE INDEX idx_qrtz_t_nft_st ON qrtz_triggers (sched_name, trigger_state, next_fire_time); -CREATE INDEX idx_qrtz_t_nft_misfire ON qrtz_triggers (sched_name, misfire_instr, next_fire_time); -CREATE INDEX idx_qrtz_t_nft_st_misfire ON qrtz_triggers (sched_name, misfire_instr, next_fire_time, trigger_state); -CREATE INDEX idx_qrtz_t_nft_st_misfire_grp ON qrtz_triggers (sched_name, misfire_instr, next_fire_time, trigger_group, - trigger_state); - --- @formatter:off -BEGIN; -COMMIT; --- @formatter:on +CREATE INDEX IDX_QRTZ_FT_TRIG_INST_NAME + ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, INSTANCE_NAME); +CREATE INDEX IDX_QRTZ_FT_INST_JOB_REQ_RCVRY + ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, INSTANCE_NAME, REQUESTS_RECOVERY); +CREATE INDEX IDX_QRTZ_FT_J_G + ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, JOB_NAME, JOB_GROUP); +CREATE INDEX IDX_QRTZ_FT_JG + ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, JOB_GROUP); +CREATE INDEX IDX_QRTZ_FT_T_G + ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP); +CREATE INDEX IDX_QRTZ_FT_TG + ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, TRIGGER_GROUP); --- ---------------------------- --- FK: qrtz_blob_triggers --- ---------------------------- -ALTER TABLE qrtz_blob_triggers - ADD CONSTRAINT qrtz_blob_triggers_ibfk_1 FOREIGN KEY (sched_name, trigger_name, trigger_group) REFERENCES qrtz_triggers (sched_name, - trigger_name, - trigger_group); - --- ---------------------------- --- FK: qrtz_cron_triggers --- ---------------------------- -ALTER TABLE qrtz_cron_triggers - ADD CONSTRAINT qrtz_cron_triggers_ibfk_1 FOREIGN KEY (sched_name, trigger_name, trigger_group) REFERENCES qrtz_triggers (sched_name, trigger_name, trigger_group); - --- ---------------------------- --- FK: qrtz_simple_triggers --- ---------------------------- -ALTER TABLE qrtz_simple_triggers - ADD CONSTRAINT qrtz_simple_triggers_ibfk_1 FOREIGN KEY (sched_name, trigger_name, trigger_group) REFERENCES qrtz_triggers (sched_name, - trigger_name, - trigger_group); - --- ---------------------------- --- FK: qrtz_simprop_triggers --- ---------------------------- -ALTER TABLE qrtz_simprop_triggers - ADD CONSTRAINT qrtz_simprop_triggers_ibfk_1 FOREIGN KEY (sched_name, trigger_name, trigger_group) REFERENCES qrtz_triggers (sched_name, trigger_name, trigger_group); - --- ---------------------------- --- FK: qrtz_triggers --- ---------------------------- -ALTER TABLE qrtz_triggers - ADD CONSTRAINT qrtz_triggers_ibfk_1 FOREIGN KEY (sched_name, job_name, job_group) REFERENCES qrtz_job_details (sched_name, job_name, job_group); +COMMIT; \ No newline at end of file From 64516b22106294f541c07a443cc6358fda56c1a0 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 13 Jul 2025 16:06:41 +0800 Subject: [PATCH 22/89] =?UTF-8?q?fix=EF=BC=9A=E3=80=90INFRA=20=E5=9F=BA?= =?UTF-8?q?=E7=A1=80=E8=AE=BE=E6=96=BD=E3=80=91=E6=96=87=E4=BB=B6=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0=E6=97=B6=EF=BC=8Cdirectory=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E4=BB=BB=E6=84=8F=E8=B7=AF=E5=BE=84=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/admin/file/vo/file/FileUploadReqVO.java | 9 +++++++++ .../infra/controller/app/file/vo/AppFileUploadReqVO.java | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FileUploadReqVO.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FileUploadReqVO.java index 4096f477e3..44e8b65d76 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FileUploadReqVO.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FileUploadReqVO.java @@ -1,6 +1,9 @@ package cn.iocoder.yudao.module.infra.controller.admin.file.vo.file; +import cn.hutool.core.util.StrUtil; +import com.fasterxml.jackson.annotation.JsonIgnore; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.NotNull; import lombok.Data; import org.springframework.web.multipart.MultipartFile; @@ -16,4 +19,10 @@ public class FileUploadReqVO { @Schema(description = "文件目录", example = "XXX/YYY") private String directory; + @AssertTrue(message = "文件目录不正确") + @JsonIgnore + public boolean isDirectoryValid() { + return !StrUtil.containsAny(directory, "..", "/", "\\"); + } + } diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/app/file/vo/AppFileUploadReqVO.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/app/file/vo/AppFileUploadReqVO.java index fde120a067..d10a21cc49 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/app/file/vo/AppFileUploadReqVO.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/app/file/vo/AppFileUploadReqVO.java @@ -1,6 +1,9 @@ package cn.iocoder.yudao.module.infra.controller.app.file.vo; +import cn.hutool.core.util.StrUtil; +import com.fasterxml.jackson.annotation.JsonIgnore; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.NotNull; import lombok.Data; import org.springframework.web.multipart.MultipartFile; @@ -16,4 +19,10 @@ public class AppFileUploadReqVO { @Schema(description = "文件目录", example = "XXX/YYY") private String directory; + @AssertTrue(message = "文件目录不正确") + @JsonIgnore + public boolean isDirectoryValid() { + return !StrUtil.containsAny(directory, "..", "/", "\\"); + } + } From a54e743a884eb328eadbcb9b6bfd1573bc554691 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 13 Jul 2025 16:15:18 +0800 Subject: [PATCH 23/89] =?UTF-8?q?feat=EF=BC=9A=E3=80=90SYSTEM=20=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E7=AE=A1=E7=90=86=E3=80=91=E6=89=8B=E6=9C=BA=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=E7=A0=81=E7=99=BB=E5=BD=95=EF=BC=8C=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E9=99=90=E6=B5=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/system/controller/admin/auth/AuthController.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java index d9269470d0..e41aee576d 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java @@ -127,6 +127,8 @@ public class AuthController { @PostMapping("/sms-login") @PermitAll @Operation(summary = "使用短信验证码登录") + // 可按需开启限流:https://github.com/YunaiV/ruoyi-vue-pro/issues/851 + // @RateLimiter(time = 60, count = 6, keyResolver = ExpressionRateLimiterKeyResolver.class, keyArg = "#reqVO.mobile") public CommonResult smsLogin(@RequestBody @Valid AuthSmsLoginReqVO reqVO) { return success(authService.smsLogin(reqVO)); } From a9c7b584ccbf2ad377893c0316fd70df5bbbc40d Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 13 Jul 2025 17:06:40 +0800 Subject: [PATCH 24/89] =?UTF-8?q?fix=EF=BC=9A=E3=80=90MALL=20=E5=95=86?= =?UTF-8?q?=E5=9F=8E=E7=AE=A1=E7=90=86=E3=80=91=E4=BC=98=E6=83=A0=E5=8A=B5?= =?UTF-8?q?=E6=89=A3=E5=87=8F=E6=97=B6=EF=BC=8C=E5=A2=9E=E5=8A=A0=20WHERE?= =?UTF-8?q?=20=E4=B9=90=E8=A7=82=E9=94=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dal/dataobject/coupon/CouponTemplateDO.java | 8 ++++++-- .../dal/mysql/coupon/CouponTemplateMapper.java | 12 +++++++++--- .../promotion/service/coupon/CouponServiceImpl.java | 3 +-- .../service/coupon/CouponTemplateServiceImpl.java | 10 ++++++---- 4 files changed, 22 insertions(+), 11 deletions(-) diff --git a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/coupon/CouponTemplateDO.java b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/coupon/CouponTemplateDO.java index 91216bf184..59ee95fb12 100644 --- a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/coupon/CouponTemplateDO.java +++ b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/coupon/CouponTemplateDO.java @@ -31,9 +31,13 @@ import java.util.List; public class CouponTemplateDO extends BaseDO { /** - * 不限制领取数量 + * 领取数量 - 不限制 */ - public static final Integer TIME_LIMIT_COUNT_MAX = -1; + public static final Integer TAKE_LIMIT_COUNT_MAX = -1; + /** + * 发放数量 - 不限制 + */ + public static final Integer TOTAL_COUNT_MAX = -1; // ========== 基本信息 BEGIN ========== /** diff --git a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponTemplateMapper.java b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponTemplateMapper.java index 3096a49f3c..84e98f3dbd 100755 --- a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponTemplateMapper.java +++ b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponTemplateMapper.java @@ -40,10 +40,16 @@ public interface CouponTemplateMapper extends BaseMapperX { .orderByDesc(CouponTemplateDO::getId)); } - default void updateTakeCount(Long id, Integer incrCount) { - update(null, new LambdaUpdateWrapper() + default int updateTakeCount(Long id, Integer incrCount) { + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper() .eq(CouponTemplateDO::getId, id) - .setSql("take_count = take_count + " + incrCount)); + .setSql("take_count = take_count + " + incrCount); + // 增加已领取的数量(incrCount 为正数),需要考虑发放数量 totalCount 的限制 + if (incrCount > 0) { + updateWrapper.and(i -> i.apply("take_count < total_count") + .or().eq(CouponTemplateDO::getTotalCount, CouponTemplateDO.TOTAL_COUNT_MAX)); + } + return update(updateWrapper); } default List selectListByTakeType(Integer takeType) { diff --git a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java index e6f82a69fc..e175807503 100644 --- a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java +++ b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java @@ -137,7 +137,6 @@ public class CouponServiceImpl implements CouponService { // 4. 增加优惠劵模板的领取数量 couponTemplateService.updateCouponTemplateTakeCount(template.getId(), userIds.size()); - return convertMultiMap(couponList, CouponDO::getUserId, CouponDO::getId); } @@ -281,7 +280,7 @@ public class CouponServiceImpl implements CouponService { } // 校验发放数量不能过小(仅在 CouponTakeTypeEnum.USER 用户领取时) if (CouponTakeTypeEnum.isUser(couponTemplate.getTakeType()) - && ObjUtil.notEqual(couponTemplate.getTakeLimitCount(), CouponTemplateDO.TIME_LIMIT_COUNT_MAX) // 非不限制 + && ObjUtil.notEqual(couponTemplate.getTakeLimitCount(), CouponTemplateDO.TAKE_LIMIT_COUNT_MAX) // 非不限制 && couponTemplate.getTakeCount() + userIds.size() > couponTemplate.getTotalCount()) { throw exception(COUPON_TEMPLATE_NOT_ENOUGH); } diff --git a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponTemplateServiceImpl.java b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponTemplateServiceImpl.java index 175e33b197..bdd8b32825 100755 --- a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponTemplateServiceImpl.java +++ b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponTemplateServiceImpl.java @@ -22,8 +22,7 @@ import java.util.List; import java.util.Objects; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; -import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_TEMPLATE_NOT_EXISTS; -import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_TEMPLATE_TOTAL_COUNT_TOO_SMALL; +import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*; /** * 优惠劵模板 Service 实现类 @@ -60,7 +59,7 @@ public class CouponTemplateServiceImpl implements CouponTemplateService { CouponTemplateDO couponTemplate = validateCouponTemplateExists(updateReqVO.getId()); // 校验发放数量不能过小(仅在 CouponTakeTypeEnum.USER 用户领取时) if (CouponTakeTypeEnum.isUser(couponTemplate.getTakeType()) - && ObjUtil.notEqual(couponTemplate.getTakeLimitCount(), CouponTemplateDO.TIME_LIMIT_COUNT_MAX) // 非不限制 + && ObjUtil.notEqual(couponTemplate.getTakeLimitCount(), CouponTemplateDO.TAKE_LIMIT_COUNT_MAX) // 非不限制 && updateReqVO.getTotalCount() < couponTemplate.getTakeCount()) { throw exception(COUPON_TEMPLATE_TOTAL_COUNT_TOO_SMALL, couponTemplate.getTakeCount()); } @@ -116,7 +115,10 @@ public class CouponTemplateServiceImpl implements CouponTemplateService { @Override public void updateCouponTemplateTakeCount(Long id, int incrCount) { - couponTemplateMapper.updateTakeCount(id, incrCount); + int updateCount = couponTemplateMapper.updateTakeCount(id, incrCount); + if (updateCount == 0) { + throw exception(COUPON_TEMPLATE_NOT_ENOUGH); + } } @Override From af94536a0606e62f3288687b4f4fdee1fce74feb Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 14 Jul 2025 13:10:51 +0800 Subject: [PATCH 25/89] =?UTF-8?q?fix=EF=BC=9A=E3=80=90CRM=20=E5=AE=A2?= =?UTF-8?q?=E6=88=B7=E7=AE=A1=E7=90=86=E3=80=91=E4=BF=AE=E6=94=B9=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E6=97=B6=EF=BC=8C=E9=81=BF=E5=85=8D=E6=93=8D=E4=BD=9C?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E5=87=BA=E7=8E=B0=E2=80=9C=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E8=B4=9F=E8=B4=A3=E4=BA=BA=E2=80=9D=E7=9A=84=E6=83=85=E5=86=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/business/CrmBusinessServiceImpl.java | 1 + .../crm/service/clue/CrmClueServiceImpl.java | 13 +++++++------ .../service/contact/CrmContactServiceImpl.java | 1 + .../service/contract/CrmContractServiceImpl.java | 9 +++++---- .../service/customer/CrmCustomerServiceImpl.java | 1 + .../receivable/CrmReceivablePlanServiceImpl.java | 1 + .../receivable/CrmReceivableServiceImpl.java | 15 ++++++++------- 7 files changed, 24 insertions(+), 17 deletions(-) diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java index 6e75f23daa..71b1884cc2 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java @@ -142,6 +142,7 @@ public class CrmBusinessServiceImpl implements CrmBusinessService { updateBusinessProduct(updateObj.getId(), businessProducts); // 3. 记录操作日志上下文 + updateReqVO.setOwnerUserId(oldBusiness.getOwnerUserId()); // 避免操作日志出现“删除负责人”的情况 LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldBusiness, CrmBusinessSaveReqVO.class)); LogRecordContext.putVariable("businessName", oldBusiness.getName()); } diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java index c8c850ab48..0250fe14f5 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java @@ -92,19 +92,20 @@ public class CrmClueServiceImpl implements CrmClueService { @Transactional(rollbackFor = Exception.class) @LogRecord(type = CRM_CLUE_TYPE, subType = CRM_CLUE_UPDATE_SUB_TYPE, bizNo = "{{#updateReqVO.id}}", success = CRM_CLUE_UPDATE_SUCCESS) - @CrmPermission(bizType = CrmBizTypeEnum.CRM_CLUE, bizId = "#updateReq.id", level = CrmPermissionLevelEnum.OWNER) - public void updateClue(CrmClueSaveReqVO updateReq) { - Assert.notNull(updateReq.getId(), "线索编号不能为空"); + @CrmPermission(bizType = CrmBizTypeEnum.CRM_CLUE, bizId = "#updateReqVO.id", level = CrmPermissionLevelEnum.OWNER) + public void updateClue(CrmClueSaveReqVO updateReqVO) { + Assert.notNull(updateReqVO.getId(), "线索编号不能为空"); // 1.1 校验线索是否存在 - CrmClueDO oldClue = validateClueExists(updateReq.getId()); + CrmClueDO oldClue = validateClueExists(updateReqVO.getId()); // 1.2 校验关联数据 - validateRelationDataExists(updateReq); + validateRelationDataExists(updateReqVO); // 2. 更新线索 - CrmClueDO updateObj = BeanUtils.toBean(updateReq, CrmClueDO.class); + CrmClueDO updateObj = BeanUtils.toBean(updateReqVO, CrmClueDO.class); clueMapper.updateById(updateObj); // 3. 记录操作日志上下文 + updateReqVO.setOwnerUserId(oldClue.getOwnerUserId()); // 避免操作日志出现“删除负责人”的情况 LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldClue, CrmCustomerSaveReqVO.class)); LogRecordContext.putVariable("clueName", oldClue.getName()); } diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contact/CrmContactServiceImpl.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contact/CrmContactServiceImpl.java index 2819c528cd..958fc7520d 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contact/CrmContactServiceImpl.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contact/CrmContactServiceImpl.java @@ -114,6 +114,7 @@ public class CrmContactServiceImpl implements CrmContactService { contactMapper.updateById(updateObj); // 3. 记录操作日志 + updateReqVO.setOwnerUserId(oldContact.getOwnerUserId()); // 避免操作日志出现“删除负责人”的情况 LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldContact, CrmContactSaveReqVO.class)); LogRecordContext.putVariable("contactName", oldContact.getName()); } diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractServiceImpl.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractServiceImpl.java index 12236bf5bc..d25d7c0046 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractServiceImpl.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractServiceImpl.java @@ -140,9 +140,9 @@ public class CrmContractServiceImpl implements CrmContractService { Assert.notNull(updateReqVO.getId(), "合同编号不能为空"); updateReqVO.setOwnerUserId(null); // 不允许更新的字段 // 1.1 校验存在 - CrmContractDO contract = validateContractExists(updateReqVO.getId()); + CrmContractDO oldContract = validateContractExists(updateReqVO.getId()); // 1.2 只有草稿、审批中,可以编辑; - if (!ObjectUtils.equalsAny(contract.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus(), + if (!ObjectUtils.equalsAny(oldContract.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus(), CrmAuditStatusEnum.PROCESS.getStatus())) { throw exception(CONTRACT_UPDATE_FAIL_NOT_DRAFT); } @@ -159,8 +159,9 @@ public class CrmContractServiceImpl implements CrmContractService { updateContractProduct(updateReqVO.getId(), contractProducts); // 3. 记录操作日志上下文 - LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(contract, CrmContractSaveReqVO.class)); - LogRecordContext.putVariable("contractName", contract.getName()); + updateReqVO.setOwnerUserId(oldContract.getOwnerUserId()); // 避免操作日志出现“删除负责人”的情况 + LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldContract, CrmContractSaveReqVO.class)); + LogRecordContext.putVariable("contractName", oldContract.getName()); } private void updateContractProduct(Long id, List newList) { diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java index 184369c791..11ac107d98 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java @@ -137,6 +137,7 @@ public class CrmCustomerServiceImpl implements CrmCustomerService { customerMapper.updateById(updateObj); // 3. 记录操作日志上下文 + updateReqVO.setOwnerUserId(oldCustomer.getOwnerUserId()); // 避免操作日志出现“删除负责人”的情况 LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldCustomer, CrmCustomerSaveReqVO.class)); LogRecordContext.putVariable("customerName", oldCustomer.getName()); } diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivablePlanServiceImpl.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivablePlanServiceImpl.java index b18c1a0c4f..5005508bbb 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivablePlanServiceImpl.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivablePlanServiceImpl.java @@ -104,6 +104,7 @@ public class CrmReceivablePlanServiceImpl implements CrmReceivablePlanService { receivablePlanMapper.updateById(updateObj); // 3. 记录操作日志上下文 + updateReqVO.setOwnerUserId(oldReceivablePlan.getOwnerUserId()); // 避免操作日志出现“删除负责人”的情况 LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldReceivablePlan, CrmReceivablePlanSaveReqVO.class)); LogRecordContext.putVariable("receivablePlan", oldReceivablePlan); } diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableServiceImpl.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableServiceImpl.java index c1ba3a7c43..642c8e6872 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableServiceImpl.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableServiceImpl.java @@ -162,14 +162,14 @@ public class CrmReceivableServiceImpl implements CrmReceivableService { Assert.notNull(updateReqVO.getId(), "回款编号不能为空"); updateReqVO.setOwnerUserId(null).setCustomerId(null).setContractId(null).setPlanId(null); // 不允许修改的字段 // 1.1 校验存在 - CrmReceivableDO receivable = validateReceivableExists(updateReqVO.getId()); - updateReqVO.setOwnerUserId(receivable.getOwnerUserId()).setCustomerId(receivable.getCustomerId()) - .setContractId(receivable.getContractId()).setPlanId(receivable.getPlanId()); // 设置已存在的值 + CrmReceivableDO oldReceivable = validateReceivableExists(updateReqVO.getId()); + updateReqVO.setOwnerUserId(oldReceivable.getOwnerUserId()).setCustomerId(oldReceivable.getCustomerId()) + .setContractId(oldReceivable.getContractId()).setPlanId(oldReceivable.getPlanId()); // 设置已存在的值 // 1.2 校验可回款金额超过上限 validateReceivablePriceExceedsLimit(updateReqVO); // 1.3 只有草稿、审批中,可以编辑; - if (!ObjectUtils.equalsAny(receivable.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus(), + if (!ObjectUtils.equalsAny(oldReceivable.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus(), CrmAuditStatusEnum.PROCESS.getStatus())) { throw exception(RECEIVABLE_UPDATE_FAIL_EDITING_PROHIBITED); } @@ -179,9 +179,10 @@ public class CrmReceivableServiceImpl implements CrmReceivableService { receivableMapper.updateById(updateObj); // 3. 记录操作日志上下文 - LogRecordContext.putVariable("receivable", receivable); - LogRecordContext.putVariable("period", getReceivablePeriod(receivable.getPlanId())); - LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(receivable, CrmReceivableSaveReqVO.class)); + updateReqVO.setOwnerUserId(oldReceivable.getOwnerUserId()); // 避免操作日志出现“删除负责人”的情况 + LogRecordContext.putVariable("oldReceivable", oldReceivable); + LogRecordContext.putVariable("period", getReceivablePeriod(oldReceivable.getPlanId())); + LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldReceivable, CrmReceivableSaveReqVO.class)); } private Integer getReceivablePeriod(Long planId) { From c96f6bb3607481fcac7cb10be17be4e2e3477299 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 14 Jul 2025 13:19:14 +0800 Subject: [PATCH 26/89] =?UTF-8?q?fix=EF=BC=9A=E3=80=90CRM=20=E5=AE=A2?= =?UTF-8?q?=E6=88=B7=E7=AE=A1=E7=90=86=E3=80=91CRM=20=E8=B6=85=E7=AE=A1?= =?UTF-8?q?=EF=BC=8C=E6=97=A0=E6=B3=95=E5=BC=BA=E5=88=B6=E8=BD=AC=E7=A7=BB?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../crm/service/permission/CrmPermissionServiceImpl.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java index b479443390..6e2c37c89a 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java @@ -210,12 +210,12 @@ public class CrmPermissionServiceImpl implements CrmPermissionService { CrmPermissionDO oldPermission = permissionMapper.selectByBizTypeAndBizIdByUserId( transferReqBO.getBizType(), transferReqBO.getBizId(), transferReqBO.getUserId()); String bizTypeName = CrmBizTypeEnum.getNameByType(transferReqBO.getBizType()); - if (oldPermission == null // 不是拥有者,并且不是超管 - || (!isOwner(oldPermission.getLevel()) && !CrmPermissionUtils.isCrmAdmin())) { + if ((oldPermission == null || !isOwner(oldPermission.getLevel())) + && !CrmPermissionUtils.isCrmAdmin()) { // 并且不是超管 throw exception(CRM_PERMISSION_DENIED, bizTypeName); } // 1.1 校验转移对象是否已经是该负责人 - if (ObjUtil.equal(transferReqBO.getNewOwnerUserId(), oldPermission.getUserId())) { + if (oldPermission != null && ObjUtil.equal(transferReqBO.getNewOwnerUserId(), oldPermission.getUserId())) { throw exception(CRM_PERMISSION_MODEL_TRANSFER_FAIL_OWNER_USER_EXISTS, bizTypeName); } // 1.2 校验新负责人是否存在 From e50250449a1d38128d6577917aa033e1414731a9 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 14 Jul 2025 19:50:02 +0800 Subject: [PATCH 27/89] =?UTF-8?q?fix=EF=BC=9A=E3=80=90AI=20=E5=A4=A7?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E3=80=91UserProfileQueryToolFunction=20?= =?UTF-8?q?=E6=B2=A1=E6=9C=89=E5=8F=82=E6=95=B0=EF=BC=8C=E4=BC=9A=E6=8A=A5?= =?UTF-8?q?=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tool/UserProfileQueryToolFunction.java | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/UserProfileQueryToolFunction.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/UserProfileQueryToolFunction.java index 6e0ba51c9e..5656d39292 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/UserProfileQueryToolFunction.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/UserProfileQueryToolFunction.java @@ -7,6 +7,8 @@ import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; import cn.iocoder.yudao.module.system.api.user.AdminUserApi; import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; import com.fasterxml.jackson.annotation.JsonClassDescription; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; import jakarta.annotation.Resource; import lombok.AllArgsConstructor; import lombok.Data; @@ -17,7 +19,7 @@ import org.springframework.stereotype.Component; import java.util.function.BiFunction; /** - * 工具:当前用户信息查询 + * 工具:用户信息查询 * * 同时,也是展示 ToolContext 上下文的使用 * @@ -31,8 +33,17 @@ public class UserProfileQueryToolFunction private AdminUserApi adminUserApi; @Data - @JsonClassDescription("当前用户信息查询") - public static class Request { } + @JsonClassDescription("用户信息查询") + public static class Request { + + /** + * 用户编号 + */ + @JsonProperty(value = "id") + @JsonPropertyDescription("用户编号,例如说:1。如果查询自己,则 id 为空") + private Long id; + + } @Data @AllArgsConstructor @@ -61,13 +72,19 @@ public class UserProfileQueryToolFunction @Override public Response apply(Request request, ToolContext toolContext) { - LoginUser loginUser = (LoginUser) toolContext.getContext().get(AiUtils.TOOL_CONTEXT_LOGIN_USER); Long tenantId = (Long) toolContext.getContext().get(AiUtils.TOOL_CONTEXT_TENANT_ID); - if (loginUser == null | tenantId == null) { - return null; + if (tenantId == null) { + return new Response(); + } + if (request.getId() == null) { + LoginUser loginUser = (LoginUser) toolContext.getContext().get(AiUtils.TOOL_CONTEXT_LOGIN_USER); + if (loginUser == null) { + return new Response(); + } + request.setId(loginUser.getId()); } return TenantUtils.execute(tenantId, () -> { - AdminUserRespDTO user = adminUserApi.getUser(loginUser.getId()); + AdminUserRespDTO user = adminUserApi.getUser(request.getId()); return BeanUtils.toBean(user, Response.class); }); } From c789418a7bd9aedd1097de1b160141b7dded92eb Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 14 Jul 2025 20:34:42 +0800 Subject: [PATCH 28/89] =?UTF-8?q?feat=EF=BC=9A=E3=80=90AI=20=E5=A4=A7?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E3=80=91=E4=BE=9D=E8=B5=96=20spring=20ai=20?= =?UTF-8?q?=E5=8D=87=E7=BA=A7=E5=88=B0=201.0.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yudao-module-ai/pom.xml | 38 ++++++++++--------- .../ai/config/AiAutoConfiguration.java | 8 ++-- .../siliconflow/SiliconFlowImageModel.java | 2 +- .../ai/service/image/AiImageServiceImpl.java | 16 ++++---- .../iocoder/yudao/module/ai/util/AiUtils.java | 14 +++---- .../core/model/chat/OpenAIChatModelTests.java | 4 +- .../core/model/chat/TongYiChatModelTests.java | 11 ++++-- .../model/image/OpenAiImageModelTests.java | 6 +-- .../src/main/resources/application-dev.yaml | 4 +- .../src/main/resources/application-local.yaml | 4 +- 10 files changed, 56 insertions(+), 51 deletions(-) diff --git a/yudao-module-ai/pom.xml b/yudao-module-ai/pom.xml index 4b02a7e00f..95c836777a 100644 --- a/yudao-module-ai/pom.xml +++ b/yudao-module-ai/pom.xml @@ -19,7 +19,8 @@ 国外:OpenAI、Ollama、Midjourney、StableDiffusion、Suno - 1.0.0-M6 + 1.0.0 + 1.0.0.2 1.0.2 @@ -75,65 +76,66 @@ org.springframework.ai - spring-ai-openai-spring-boot-starter + spring-ai-starter-model-openai ${spring-ai.version} org.springframework.ai - spring-ai-azure-openai-spring-boot-starter + spring-ai-starter-model-azure-openai ${spring-ai.version} org.springframework.ai - spring-ai-ollama-spring-boot-starter + spring-ai-starter-model-ollama ${spring-ai.version} org.springframework.ai - spring-ai-stability-ai-spring-boot-starter + spring-ai-starter-model-stability-ai ${spring-ai.version} com.alibaba.cloud.ai - spring-ai-alibaba-starter - ${spring-ai.version}.1 + spring-ai-alibaba-starter-dashscope + ${alibaba-ai.version} - org.springframework.ai - spring-ai-qianfan-spring-boot-starter - ${spring-ai.version} + org.springaicommunity + qianfan-spring-boot-starter + 1.0.0 org.springframework.ai - spring-ai-zhipuai-spring-boot-starter + spring-ai-starter-model-zhipuai ${spring-ai.version} org.springframework.ai - spring-ai-minimax-spring-boot-starter + spring-ai-starter-model-minimax ${spring-ai.version} - org.springframework.ai - spring-ai-moonshot-spring-boot-starter - ${spring-ai.version} + + org.springaicommunity + moonshot-spring-boot-starter + 1.0.0 org.springframework.ai - spring-ai-qdrant-store + spring-ai-starter-vector-store-qdrant ${spring-ai.version} org.springframework.ai - spring-ai-redis-store + spring-ai-starter-vector-store-redis ${spring-ai.version} @@ -144,7 +146,7 @@ org.springframework.ai - spring-ai-milvus-store + spring-ai-starter-vector-store-milvus ${spring-ai.version} diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java index a28d726b90..ae8a7e75f6 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java @@ -14,10 +14,6 @@ import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlo import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi; import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatModel; import lombok.extern.slf4j.Slf4j; -import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusServiceClientProperties; -import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusVectorStoreProperties; -import org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantVectorStoreProperties; -import org.springframework.ai.autoconfigure.vectorstore.redis.RedisVectorStoreProperties; import org.springframework.ai.embedding.BatchingStrategy; import org.springframework.ai.embedding.TokenCountBatchingStrategy; import org.springframework.ai.model.tool.ToolCallingManager; @@ -26,6 +22,10 @@ import org.springframework.ai.openai.OpenAiChatOptions; import org.springframework.ai.openai.api.OpenAiApi; import org.springframework.ai.tokenizer.JTokkitTokenCountEstimator; import org.springframework.ai.tokenizer.TokenCountEstimator; +import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusServiceClientProperties; +import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreProperties; +import org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreProperties; +import org.springframework.ai.vectorstore.redis.autoconfigure.RedisVectorStoreProperties; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/siliconflow/SiliconFlowImageModel.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/siliconflow/SiliconFlowImageModel.java index 43f8ad2168..44a652309e 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/siliconflow/SiliconFlowImageModel.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/siliconflow/SiliconFlowImageModel.java @@ -89,7 +89,7 @@ public class SiliconFlowImageModel implements ImageModel { var observationContext = ImageModelObservationContext.builder() .imagePrompt(imagePrompt) .provider(SiliconFlowApiConstants.PROVIDER_NAME) - .requestOptions(imagePrompt.getOptions()) + .imagePrompt(imagePrompt) .build(); return ImageModelObservationDocumentation.IMAGE_MODEL_OPERATION diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java index 671098a704..79214a0325 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java @@ -9,9 +9,6 @@ import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; import cn.hutool.http.HttpUtil; -import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum; -import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi; -import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowImageOptions; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDrawReqVO; @@ -24,17 +21,20 @@ import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; import cn.iocoder.yudao.module.ai.dal.mysql.image.AiImageMapper; import cn.iocoder.yudao.module.ai.enums.image.AiImageStatusEnum; +import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum; +import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi; +import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowImageOptions; import cn.iocoder.yudao.module.ai.service.model.AiModelService; import cn.iocoder.yudao.module.infra.api.file.FileApi; import com.alibaba.cloud.ai.dashscope.image.DashScopeImageOptions; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; +import org.springaicommunity.qianfan.QianFanImageOptions; import org.springframework.ai.image.ImageModel; import org.springframework.ai.image.ImageOptions; import org.springframework.ai.image.ImagePrompt; import org.springframework.ai.image.ImageResponse; import org.springframework.ai.openai.OpenAiImageOptions; -import org.springframework.ai.qianfan.QianFanImageOptions; import org.springframework.ai.stabilityai.api.StabilityAiImageOptions; import org.springframework.ai.zhipuai.ZhiPuAiImageOptions; import org.springframework.scheduling.annotation.Async; @@ -140,10 +140,10 @@ public class AiImageServiceImpl implements AiImageService { private static ImageOptions buildImageOptions(AiImageDrawReqVO draw, AiModelDO model) { if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.OPENAI.getPlatform())) { // https://platform.openai.com/docs/api-reference/images/create - return OpenAiImageOptions.builder().withModel(model.getModel()) - .withHeight(draw.getHeight()).withWidth(draw.getWidth()) - .withStyle(MapUtil.getStr(draw.getOptions(), "style")) // 风格 - .withResponseFormat("b64_json") + return OpenAiImageOptions.builder().model(model.getModel()) + .height(draw.getHeight()).width(draw.getWidth()) + .style(MapUtil.getStr(draw.getOptions(), "style")) // 风格 + .responseFormat("b64_json") .build(); } else if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.SILICON_FLOW.getPlatform())) { // https://docs.siliconflow.cn/cn/api-reference/images/images-generations diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/util/AiUtils.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/util/AiUtils.java index ac3ff39a49..0744ff6307 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/util/AiUtils.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/util/AiUtils.java @@ -2,18 +2,18 @@ package cn.iocoder.yudao.module.ai.util; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum; import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; +import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum; import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions; +import org.springaicommunity.moonshot.MoonshotChatOptions; +import org.springaicommunity.qianfan.QianFanChatOptions; import org.springframework.ai.azure.openai.AzureOpenAiChatOptions; import org.springframework.ai.chat.messages.*; import org.springframework.ai.chat.prompt.ChatOptions; import org.springframework.ai.minimax.MiniMaxChatOptions; -import org.springframework.ai.moonshot.MoonshotChatOptions; import org.springframework.ai.ollama.api.OllamaOptions; import org.springframework.ai.openai.OpenAiChatOptions; -import org.springframework.ai.qianfan.QianFanChatOptions; import org.springframework.ai.zhipuai.ZhiPuAiChatOptions; import java.util.Collections; @@ -43,18 +43,18 @@ public class AiUtils { switch (platform) { case TONG_YI: return DashScopeChatOptions.builder().withModel(model).withTemperature(temperature).withMaxToken(maxTokens) - .withFunctions(toolNames).withToolContext(toolContext).build(); + .withToolNames(toolNames).withToolContext(toolContext).build(); case YI_YAN: return QianFanChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); case ZHI_PU: return ZhiPuAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) - .functions(toolNames).toolContext(toolContext).build(); + .toolNames(toolNames).toolContext(toolContext).build(); case MINI_MAX: return MiniMaxChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) - .functions(toolNames).toolContext(toolContext).build(); + .toolNames(toolNames).toolContext(toolContext).build(); case MOONSHOT: return MoonshotChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) - .functions(toolNames).toolContext(toolContext).build(); + .toolNames(toolNames).toolContext(toolContext).build(); case OPENAI: case DEEP_SEEK: // 复用 OpenAI 客户端 case DOU_BAO: // 复用 OpenAI 客户端 diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/OpenAIChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/OpenAIChatModelTests.java index ff866fe40b..c650fd0420 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/OpenAIChatModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/OpenAIChatModelTests.java @@ -25,10 +25,10 @@ public class OpenAIChatModelTests { private final OpenAiChatModel chatModel = OpenAiChatModel.builder() .openAiApi(OpenAiApi.builder() .baseUrl("https://api.holdai.top") - .apiKey("sk-aN6nWn3fILjrgLFT0fC4Aa60B72e4253826c77B29dC94f17") // apiKey + .apiKey("sk-PytRecQlmjEteoa2RRN6cGnwslo72UUPLQVNEMS6K9yjbmpD") // apiKey .build()) .defaultOptions(OpenAiChatOptions.builder() - .model(OpenAiApi.ChatModel.GPT_4_O) // 模型 + .model(OpenAiApi.ChatModel.GPT_4_1_NANO) // 模型 .temperature(0.7) .build()) .build(); diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/TongYiChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/TongYiChatModelTests.java index 4f0efdb20c..4f2e27edd2 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/TongYiChatModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/TongYiChatModelTests.java @@ -22,14 +22,17 @@ import java.util.List; */ public class TongYiChatModelTests { - private final DashScopeChatModel chatModel = new DashScopeChatModel( - new DashScopeApi("sk-7d903764249848cfa912733146da12d1"), - DashScopeChatOptions.builder() + private final DashScopeChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(DashScopeApi.builder() + .apiKey("sk-47aa124781be4bfb95244cc62f63f7d0") + .build()) + .defaultOptions( DashScopeChatOptions.builder() .withModel("qwen1.5-72b-chat") // 模型 // .withModel("deepseek-r1") // 模型(deepseek-r1) // .withModel("deepseek-v3") // 模型(deepseek-v3) // .withModel("deepseek-r1-distill-qwen-1.5b") // 模型(deepseek-r1-distill-qwen-1.5b) - .build()); + .build()) + .build(); @Test @Disabled diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/OpenAiImageModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/OpenAiImageModelTests.java index 49015b9b9e..1b124529de 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/OpenAiImageModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/OpenAiImageModelTests.java @@ -18,7 +18,7 @@ public class OpenAiImageModelTests { private final OpenAiImageModel imageModel = new OpenAiImageModel(OpenAiImageApi.builder() .baseUrl("https://api.holdai.top") // apiKey - .apiKey("sk-aN6nWn3fILjrgLFT0fC4Aa60B72e4253826c77B29dC94f17") + .apiKey("sk-PytRecQlmjEteoa2RRN6cGnwslo72UUPLQVNEMS6K9yjbmpD") .build()); @Test @@ -26,8 +26,8 @@ public class OpenAiImageModelTests { public void testCall() { // 准备参数 ImageOptions options = OpenAiImageOptions.builder() - .withModel(OpenAiImageApi.ImageModel.DALL_E_2.getValue()) // 这个模型比较便宜 - .withHeight(256).withWidth(256) + .model(OpenAiImageApi.ImageModel.DALL_E_2.getValue()) // 这个模型比较便宜 + .height(256).width(256) .build(); ImagePrompt prompt = new ImagePrompt("中国长城!", options); diff --git a/yudao-server/src/main/resources/application-dev.yaml b/yudao-server/src/main/resources/application-dev.yaml index db07245c63..344e97ecf8 100644 --- a/yudao-server/src/main/resources/application-dev.yaml +++ b/yudao-server/src/main/resources/application-dev.yaml @@ -6,8 +6,8 @@ server: spring: autoconfigure: exclude: - - org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantVectorStoreAutoConfiguration # 禁用 AI 模块的 Qdrant,手动创建 - - org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusVectorStoreAutoConfiguration # 禁用 AI 模块的 Milvus,手动创建 + - org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreAutoConfiguration # 禁用 AI 模块的 Qdrant,手动创建 + - org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreAutoConfiguration # 禁用 AI 模块的 Milvus,手动创建 # 数据源配置项 datasource: druid: # Druid 【监控】相关的全局配置 diff --git a/yudao-server/src/main/resources/application-local.yaml b/yudao-server/src/main/resources/application-local.yaml index 2849775e62..5d23b9b70d 100644 --- a/yudao-server/src/main/resources/application-local.yaml +++ b/yudao-server/src/main/resources/application-local.yaml @@ -10,8 +10,8 @@ spring: - de.codecentric.boot.admin.server.config.AdminServerAutoConfiguration # 禁用 Spring Boot Admin 的 Server 的自动配置 - de.codecentric.boot.admin.server.ui.config.AdminServerUiAutoConfiguration # 禁用 Spring Boot Admin 的 Server UI 的自动配置 - de.codecentric.boot.admin.client.config.SpringBootAdminClientAutoConfiguration # 禁用 Spring Boot Admin 的 Client 的自动配置 - - org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantVectorStoreAutoConfiguration # 禁用 AI 模块的 Qdrant,手动创建 - - org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusVectorStoreAutoConfiguration # 禁用 AI 模块的 Milvus,手动创建 + - org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreAutoConfiguration # 禁用 AI 模块的 Qdrant,手动创建 + - org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreAutoConfiguration # 禁用 AI 模块的 Milvus,手动创建 # 数据源配置项 datasource: druid: # Druid 【监控】相关的全局配置 From 3d0eb77148c5869c42699ba1f2831e4ed89f01ab Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 14 Jul 2025 20:56:00 +0800 Subject: [PATCH 29/89] =?UTF-8?q?feat=EF=BC=9A=E3=80=90AI=20=E5=A4=A7?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E3=80=91=E5=BC=95=E5=85=A5=20spring-ai-start?= =?UTF-8?q?er-model-deepseek=20=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yudao-module-ai/pom.xml | 31 ++++++++----- .../ai/config/AiAutoConfiguration.java | 28 ------------ .../ai/config/YudaoAiProperties.java | 19 -------- .../model/deepseek/DeepSeekChatModel.java | 45 ------------------- .../model/chat/DeepSeekChatModelTests.java | 18 +++----- 5 files changed, 26 insertions(+), 115 deletions(-) delete mode 100644 yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/deepseek/DeepSeekChatModel.java diff --git a/yudao-module-ai/pom.xml b/yudao-module-ai/pom.xml index 95c836777a..4a2323bcfa 100644 --- a/yudao-module-ai/pom.xml +++ b/yudao-module-ai/pom.xml @@ -84,6 +84,11 @@ spring-ai-starter-model-azure-openai ${spring-ai.version} + + org.springframework.ai + spring-ai-starter-model-deepseek + ${spring-ai.version} + org.springframework.ai spring-ai-starter-model-ollama @@ -94,18 +99,6 @@ spring-ai-starter-model-stability-ai ${spring-ai.version} - - - com.alibaba.cloud.ai - spring-ai-alibaba-starter-dashscope - ${alibaba-ai.version} - - - - org.springaicommunity - qianfan-spring-boot-starter - 1.0.0 - org.springframework.ai @@ -117,6 +110,20 @@ spring-ai-starter-model-minimax ${spring-ai.version} + + + + com.alibaba.cloud.ai + spring-ai-alibaba-starter-dashscope + ${alibaba-ai.version} + + + + + org.springaicommunity + qianfan-spring-boot-starter + 1.0.0 + org.springaicommunity diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java index ae8a7e75f6..4ff7c9e4dc 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java @@ -5,7 +5,6 @@ import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.module.ai.framework.ai.core.AiModelFactory; import cn.iocoder.yudao.module.ai.framework.ai.core.AiModelFactoryImpl; import cn.iocoder.yudao.module.ai.framework.ai.core.model.baichuan.BaiChuanChatModel; -import cn.iocoder.yudao.module.ai.framework.ai.core.model.deepseek.DeepSeekChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.doubao.DouBaoChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.hunyuan.HunYuanChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi; @@ -52,33 +51,6 @@ public class AiAutoConfiguration { // ========== 各种 AI Client 创建 ========== - @Bean - @ConditionalOnProperty(value = "yudao.ai.deepseek.enable", havingValue = "true") - public DeepSeekChatModel deepSeekChatModel(YudaoAiProperties yudaoAiProperties) { - YudaoAiProperties.DeepSeekProperties properties = yudaoAiProperties.getDeepseek(); - return buildDeepSeekChatModel(properties); - } - - public DeepSeekChatModel buildDeepSeekChatModel(YudaoAiProperties.DeepSeekProperties properties) { - if (StrUtil.isEmpty(properties.getModel())) { - properties.setModel(DeepSeekChatModel.MODEL_DEFAULT); - } - OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() - .openAiApi(OpenAiApi.builder() - .baseUrl(DeepSeekChatModel.BASE_URL) - .apiKey(properties.getApiKey()) - .build()) - .defaultOptions(OpenAiChatOptions.builder() - .model(properties.getModel()) - .temperature(properties.getTemperature()) - .maxTokens(properties.getMaxTokens()) - .topP(properties.getTopP()) - .build()) - .toolCallingManager(getToolCallingManager()) - .build(); - return new DeepSeekChatModel(openAiChatModel); - } - @Bean @ConditionalOnProperty(value = "yudao.ai.doubao.enable", havingValue = "true") public DouBaoChatModel douBaoChatClient(YudaoAiProperties yudaoAiProperties) { diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/YudaoAiProperties.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/YudaoAiProperties.java index 7f8046768a..7c26aa89ca 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/YudaoAiProperties.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/YudaoAiProperties.java @@ -13,12 +13,6 @@ import org.springframework.boot.context.properties.ConfigurationProperties; @Data public class YudaoAiProperties { - /** - * DeepSeek - */ - @SuppressWarnings("SpellCheckingInspection") - private DeepSeekProperties deepseek; - /** * 字节豆包 */ @@ -60,19 +54,6 @@ public class YudaoAiProperties { @SuppressWarnings("SpellCheckingInspection") private SunoProperties suno; - @Data - public static class DeepSeekProperties { - - private String enable; - private String apiKey; - - private String model; - private Double temperature; - private Integer maxTokens; - private Double topP; - - } - @Data public static class DouBaoProperties { diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/deepseek/DeepSeekChatModel.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/deepseek/DeepSeekChatModel.java deleted file mode 100644 index d603abf6b0..0000000000 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/deepseek/DeepSeekChatModel.java +++ /dev/null @@ -1,45 +0,0 @@ -package cn.iocoder.yudao.module.ai.framework.ai.core.model.deepseek; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.ai.chat.model.ChatModel; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.prompt.ChatOptions; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.openai.OpenAiChatModel; -import reactor.core.publisher.Flux; - -/** - * DeepSeek {@link ChatModel} 实现类 - * - * @author fansili - */ -@Slf4j -@RequiredArgsConstructor -public class DeepSeekChatModel implements ChatModel { - - public static final String BASE_URL = "https://api.deepseek.com"; - - public static final String MODEL_DEFAULT = "deepseek-chat"; - - /** - * 兼容 OpenAI 接口,进行复用 - */ - private final OpenAiChatModel openAiChatModel; - - @Override - public ChatResponse call(Prompt prompt) { - return openAiChatModel.call(prompt); - } - - @Override - public Flux stream(Prompt prompt) { - return openAiChatModel.stream(prompt); - } - - @Override - public ChatOptions getDefaultOptions() { - return openAiChatModel.getDefaultOptions(); - } - -} diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DeepSeekChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DeepSeekChatModelTests.java index d20a1761f6..7b51df1662 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DeepSeekChatModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DeepSeekChatModelTests.java @@ -1,6 +1,5 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; -import cn.iocoder.yudao.module.ai.framework.ai.core.model.deepseek.DeepSeekChatModel; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.ai.chat.messages.Message; @@ -8,9 +7,9 @@ import org.springframework.ai.chat.messages.SystemMessage; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.openai.OpenAiChatModel; -import org.springframework.ai.openai.OpenAiChatOptions; -import org.springframework.ai.openai.api.OpenAiApi; +import org.springframework.ai.deepseek.DeepSeekChatModel; +import org.springframework.ai.deepseek.DeepSeekChatOptions; +import org.springframework.ai.deepseek.api.DeepSeekApi; import reactor.core.publisher.Flux; import java.util.ArrayList; @@ -23,19 +22,16 @@ import java.util.List; */ public class DeepSeekChatModelTests { - private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() - .openAiApi(OpenAiApi.builder() - .baseUrl(DeepSeekChatModel.BASE_URL) - .apiKey("sk-e52047409b144d97b791a6a46a2d") // apiKey + private final DeepSeekChatModel chatModel = DeepSeekChatModel.builder() + .deepSeekApi(DeepSeekApi.builder() + .apiKey("sk-eaf4172a057344dd9bc64b1f806b6axx") // apiKey .build()) - .defaultOptions(OpenAiChatOptions.builder() + .defaultOptions(DeepSeekChatOptions.builder() .model("deepseek-chat") // 模型 .temperature(0.7) .build()) .build(); - private final DeepSeekChatModel chatModel = new DeepSeekChatModel(openAiChatModel); - @Test @Disabled public void testCall() { From 750709d706fdad1fdde16e6fab7a75de238ea879 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 14 Jul 2025 21:40:00 +0800 Subject: [PATCH 30/89] =?UTF-8?q?feat=EF=BC=9A=E3=80=90AI=20=E5=A4=A7?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E3=80=91deepseek=E3=80=81azure=E3=80=81baich?= =?UTF-8?q?uan=E3=80=81moonshot=20=E9=80=82=E9=85=8D=201.0.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../framework/ai/core/AiModelFactoryImpl.java | 223 +++++++++++------- .../model/chat/AzureOpenAIChatModelTests.java | 14 +- .../model/chat/BaiChuanChatModelTests.java | 3 +- .../model/chat/MoonshotChatModelTests.java | 20 +- .../image/StabilityAiImageModelTests.java | 4 +- .../src/main/resources/application.yaml | 9 +- 6 files changed, 160 insertions(+), 113 deletions(-) diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/AiModelFactoryImpl.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/AiModelFactoryImpl.java index f258ffaf1b..2aeecab917 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/AiModelFactoryImpl.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/AiModelFactoryImpl.java @@ -8,11 +8,11 @@ import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.RuntimeUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.util.spring.SpringUtils; +import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum; import cn.iocoder.yudao.module.ai.framework.ai.config.AiAutoConfiguration; import cn.iocoder.yudao.module.ai.framework.ai.config.YudaoAiProperties; -import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum; import cn.iocoder.yudao.module.ai.framework.ai.core.model.baichuan.BaiChuanChatModel; -import cn.iocoder.yudao.module.ai.framework.ai.core.model.deepseek.DeepSeekChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.doubao.DouBaoChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.hunyuan.HunYuanChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi; @@ -22,8 +22,9 @@ import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlo import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowImageModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi; import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatModel; -import cn.iocoder.yudao.framework.common.util.spring.SpringUtils; -import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeAutoConfiguration; +import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeChatAutoConfiguration; +import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeEmbeddingAutoConfiguration; +import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeImageAutoConfiguration; import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; import com.alibaba.cloud.ai.dashscope.api.DashScopeImageApi; import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; @@ -32,47 +33,55 @@ import com.alibaba.cloud.ai.dashscope.embedding.DashScopeEmbeddingModel; import com.alibaba.cloud.ai.dashscope.embedding.DashScopeEmbeddingOptions; import com.alibaba.cloud.ai.dashscope.image.DashScopeImageModel; import com.azure.ai.openai.OpenAIClientBuilder; +import com.azure.core.credential.KeyCredential; import io.micrometer.observation.ObservationRegistry; import io.milvus.client.MilvusServiceClient; import io.qdrant.client.QdrantClient; import io.qdrant.client.QdrantGrpcClient; import lombok.SneakyThrows; -import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiAutoConfiguration; -import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiChatProperties; -import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiConnectionProperties; -import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiEmbeddingProperties; -import org.springframework.ai.autoconfigure.minimax.MiniMaxAutoConfiguration; -import org.springframework.ai.autoconfigure.moonshot.MoonshotAutoConfiguration; -import org.springframework.ai.autoconfigure.ollama.OllamaAutoConfiguration; -import org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration; -import org.springframework.ai.autoconfigure.qianfan.QianFanAutoConfiguration; -import org.springframework.ai.autoconfigure.stabilityai.StabilityAiImageAutoConfiguration; -import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusServiceClientConnectionDetails; -import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusServiceClientProperties; -import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusVectorStoreAutoConfiguration; -import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusVectorStoreProperties; -import org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantVectorStoreAutoConfiguration; -import org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantVectorStoreProperties; -import org.springframework.ai.autoconfigure.vectorstore.redis.RedisVectorStoreAutoConfiguration; -import org.springframework.ai.autoconfigure.vectorstore.redis.RedisVectorStoreProperties; -import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiAutoConfiguration; +import org.springaicommunity.moonshot.MoonshotChatModel; +import org.springaicommunity.moonshot.MoonshotChatOptions; +import org.springaicommunity.moonshot.api.MoonshotApi; +import org.springaicommunity.moonshot.autoconfigure.MoonshotChatAutoConfiguration; +import org.springaicommunity.qianfan.QianFanChatModel; +import org.springaicommunity.qianfan.QianFanEmbeddingModel; +import org.springaicommunity.qianfan.QianFanEmbeddingOptions; +import org.springaicommunity.qianfan.QianFanImageModel; +import org.springaicommunity.qianfan.api.QianFanApi; +import org.springaicommunity.qianfan.api.QianFanImageApi; +import org.springaicommunity.qianfan.autoconfigure.QianFanChatAutoConfiguration; +import org.springaicommunity.qianfan.autoconfigure.QianFanEmbeddingAutoConfiguration; import org.springframework.ai.azure.openai.AzureOpenAiChatModel; import org.springframework.ai.azure.openai.AzureOpenAiEmbeddingModel; import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.deepseek.DeepSeekChatModel; +import org.springframework.ai.deepseek.DeepSeekChatOptions; +import org.springframework.ai.deepseek.api.DeepSeekApi; import org.springframework.ai.document.MetadataMode; import org.springframework.ai.embedding.BatchingStrategy; import org.springframework.ai.embedding.EmbeddingModel; +import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention; import org.springframework.ai.image.ImageModel; import org.springframework.ai.minimax.MiniMaxChatModel; import org.springframework.ai.minimax.MiniMaxChatOptions; import org.springframework.ai.minimax.MiniMaxEmbeddingModel; import org.springframework.ai.minimax.MiniMaxEmbeddingOptions; import org.springframework.ai.minimax.api.MiniMaxApi; -import org.springframework.ai.model.function.FunctionCallbackResolver; +import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiChatAutoConfiguration; +import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiEmbeddingAutoConfiguration; +import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiEmbeddingProperties; +import org.springframework.ai.model.deepseek.autoconfigure.DeepSeekChatAutoConfiguration; +import org.springframework.ai.model.minimax.autoconfigure.MiniMaxChatAutoConfiguration; +import org.springframework.ai.model.minimax.autoconfigure.MiniMaxEmbeddingAutoConfiguration; +import org.springframework.ai.model.ollama.autoconfigure.OllamaChatAutoConfiguration; +import org.springframework.ai.model.openai.autoconfigure.OpenAiChatAutoConfiguration; +import org.springframework.ai.model.openai.autoconfigure.OpenAiEmbeddingAutoConfiguration; +import org.springframework.ai.model.openai.autoconfigure.OpenAiImageAutoConfiguration; +import org.springframework.ai.model.stabilityai.autoconfigure.StabilityAiImageAutoConfiguration; import org.springframework.ai.model.tool.ToolCallingManager; -import org.springframework.ai.moonshot.MoonshotChatModel; -import org.springframework.ai.moonshot.MoonshotChatOptions; -import org.springframework.ai.moonshot.api.MoonshotApi; +import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiChatAutoConfiguration; +import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiEmbeddingAutoConfiguration; +import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiImageAutoConfiguration; import org.springframework.ai.ollama.OllamaChatModel; import org.springframework.ai.ollama.OllamaEmbeddingModel; import org.springframework.ai.ollama.api.OllamaApi; @@ -84,21 +93,23 @@ import org.springframework.ai.openai.OpenAiImageModel; import org.springframework.ai.openai.api.OpenAiApi; import org.springframework.ai.openai.api.OpenAiImageApi; import org.springframework.ai.openai.api.common.OpenAiApiConstants; -import org.springframework.ai.qianfan.QianFanChatModel; -import org.springframework.ai.qianfan.QianFanEmbeddingModel; -import org.springframework.ai.qianfan.QianFanEmbeddingOptions; -import org.springframework.ai.qianfan.QianFanImageModel; -import org.springframework.ai.qianfan.api.QianFanApi; -import org.springframework.ai.qianfan.api.QianFanImageApi; import org.springframework.ai.stabilityai.StabilityAiImageModel; import org.springframework.ai.stabilityai.api.StabilityAiApi; import org.springframework.ai.vectorstore.SimpleVectorStore; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.ai.vectorstore.milvus.MilvusVectorStore; +import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusServiceClientConnectionDetails; +import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusServiceClientProperties; +import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreAutoConfiguration; +import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreProperties; import org.springframework.ai.vectorstore.observation.DefaultVectorStoreObservationConvention; import org.springframework.ai.vectorstore.observation.VectorStoreObservationConvention; import org.springframework.ai.vectorstore.qdrant.QdrantVectorStore; +import org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreAutoConfiguration; +import org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreProperties; import org.springframework.ai.vectorstore.redis.RedisVectorStore; +import org.springframework.ai.vectorstore.redis.autoconfigure.RedisVectorStoreAutoConfiguration; +import org.springframework.ai.vectorstore.redis.autoconfigure.RedisVectorStoreProperties; import org.springframework.ai.zhipuai.*; import org.springframework.ai.zhipuai.api.ZhiPuAiApi; import org.springframework.ai.zhipuai.api.ZhiPuAiImageApi; @@ -190,7 +201,7 @@ public class AiModelFactoryImpl implements AiModelFactory { case XING_HUO: return SpringUtil.getBean(XingHuoChatModel.class); case BAI_CHUAN: - return SpringUtil.getBean(AzureOpenAiChatModel.class); + return SpringUtil.getBean(BaiChuanChatModel.class); case OPENAI: return SpringUtil.getBean(OpenAiChatModel.class); case AZURE_OPENAI: @@ -319,27 +330,34 @@ public class AiModelFactoryImpl implements AiModelFactory { // ========== 各种创建 spring-ai 客户端的方法 ========== /** - * 可参考 {@link DashScopeAutoConfiguration} 的 dashscopeChatModel 方法 + * 可参考 {@link DashScopeChatAutoConfiguration} 的 dashscopeChatModel 方法 */ private static DashScopeChatModel buildTongYiChatModel(String key) { - DashScopeApi dashScopeApi = new DashScopeApi(key); + DashScopeApi dashScopeApi = DashScopeApi.builder().apiKey(key).build(); DashScopeChatOptions options = DashScopeChatOptions.builder().withModel(DashScopeApi.DEFAULT_CHAT_MODEL) .withTemperature(0.7).build(); - return new DashScopeChatModel(dashScopeApi, options, getFunctionCallbackResolver(), DEFAULT_RETRY_TEMPLATE); + return DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .defaultOptions(options) + .toolCallingManager(getToolCallingManager()) + .build(); } /** - * 可参考 {@link DashScopeAutoConfiguration} 的 dashScopeImageModel 方法 + * 可参考 {@link DashScopeImageAutoConfiguration} 的 dashScopeImageModel 方法 */ private static DashScopeImageModel buildTongYiImagesModel(String key) { DashScopeImageApi dashScopeImageApi = new DashScopeImageApi(key); - return new DashScopeImageModel(dashScopeImageApi); + return DashScopeImageModel.builder() + .dashScopeApi(dashScopeImageApi) + .build(); } /** - * 可参考 {@link QianFanAutoConfiguration} 的 qianFanChatModel 方法 + * 可参考 {@link QianFanChatAutoConfiguration} 的 qianFanChatModel 方法 */ private static QianFanChatModel buildYiYanChatModel(String key) { + // TODO @芋艿:未测试 List keys = StrUtil.split(key, '|'); Assert.equals(keys.size(), 2, "YiYanChatClient 的密钥需要 (appKey|secretKey) 格式"); String appKey = keys.get(0); @@ -349,9 +367,10 @@ public class AiModelFactoryImpl implements AiModelFactory { } /** - * 可参考 {@link QianFanAutoConfiguration} 的 qianFanImageModel 方法 + * 可参考 {@link QianFanEmbeddingAutoConfiguration} 的 qianFanImageModel 方法 */ private QianFanImageModel buildQianFanImageModel(String key) { + // TODO @芋艿:未测试 List keys = StrUtil.split(key, '|'); Assert.equals(keys.size(), 2, "YiYanChatClient 的密钥需要 (appKey|secretKey) 格式"); String appKey = keys.get(0); @@ -361,12 +380,17 @@ public class AiModelFactoryImpl implements AiModelFactory { } /** - * 可参考 {@link AiAutoConfiguration#deepSeekChatModel(YudaoAiProperties)} + * 可参考 {@link DeepSeekChatAutoConfiguration} 的 deepSeekChatModel 方法 */ private static DeepSeekChatModel buildDeepSeekChatModel(String apiKey) { - YudaoAiProperties.DeepSeekProperties properties = new YudaoAiProperties.DeepSeekProperties() - .setApiKey(apiKey); - return new AiAutoConfiguration().buildDeepSeekChatModel(properties); + DeepSeekApi deepSeekApi = DeepSeekApi.builder().apiKey(apiKey).build(); + DeepSeekChatOptions options = DeepSeekChatOptions.builder().model(DeepSeekApi.DEFAULT_CHAT_MODEL) + .temperature(0.7).build(); + return DeepSeekChatModel.builder() + .deepSeekApi(deepSeekApi) + .defaultOptions(options) + .toolCallingManager(getToolCallingManager()) + .build(); } /** @@ -397,17 +421,18 @@ public class AiModelFactoryImpl implements AiModelFactory { } /** - * 可参考 {@link ZhiPuAiAutoConfiguration} 的 zhiPuAiChatModel 方法 + * 可参考 {@link ZhiPuAiChatAutoConfiguration} 的 zhiPuAiChatModel 方法 */ private ZhiPuAiChatModel buildZhiPuChatModel(String apiKey, String url) { ZhiPuAiApi zhiPuAiApi = StrUtil.isEmpty(url) ? new ZhiPuAiApi(apiKey) : new ZhiPuAiApi(url, apiKey); ZhiPuAiChatOptions options = ZhiPuAiChatOptions.builder().model(ZhiPuAiApi.DEFAULT_CHAT_MODEL).temperature(0.7).build(); - return new ZhiPuAiChatModel(zhiPuAiApi, options, getFunctionCallbackResolver(), DEFAULT_RETRY_TEMPLATE); + return new ZhiPuAiChatModel(zhiPuAiApi, options, getToolCallingManager(), DEFAULT_RETRY_TEMPLATE, + getObservationRegistry().getIfAvailable()); } /** - * 可参考 {@link ZhiPuAiAutoConfiguration} 的 zhiPuAiImageModel 方法 + * 可参考 {@link ZhiPuAiImageAutoConfiguration} 的 zhiPuAiImageModel 方法 */ private ZhiPuAiImageModel buildZhiPuAiImageModel(String apiKey, String url) { ZhiPuAiImageApi zhiPuAiApi = StrUtil.isEmpty(url) ? new ZhiPuAiImageApi(apiKey) @@ -416,23 +441,30 @@ public class AiModelFactoryImpl implements AiModelFactory { } /** - * 可参考 {@link MiniMaxAutoConfiguration} 的 miniMaxChatModel 方法 + * 可参考 {@link MiniMaxChatAutoConfiguration} 的 miniMaxChatModel 方法 */ private MiniMaxChatModel buildMiniMaxChatModel(String apiKey, String url) { MiniMaxApi miniMaxApi = StrUtil.isEmpty(url) ? new MiniMaxApi(apiKey) : new MiniMaxApi(url, apiKey); MiniMaxChatOptions options = MiniMaxChatOptions.builder().model(MiniMaxApi.DEFAULT_CHAT_MODEL).temperature(0.7).build(); - return new MiniMaxChatModel(miniMaxApi, options, getFunctionCallbackResolver(), DEFAULT_RETRY_TEMPLATE); + return new MiniMaxChatModel(miniMaxApi, options, getToolCallingManager(), DEFAULT_RETRY_TEMPLATE); } /** - * 可参考 {@link MoonshotAutoConfiguration} 的 moonshotChatModel 方法 + * 可参考 {@link MoonshotChatAutoConfiguration} 的 moonshotChatModel 方法 */ private MoonshotChatModel buildMoonshotChatModel(String apiKey, String url) { - MoonshotApi moonshotApi = StrUtil.isEmpty(url)? new MoonshotApi(apiKey) - : new MoonshotApi(url, apiKey); + MoonshotApi.Builder moonshotApiBuilder = MoonshotApi.builder() + .apiKey(apiKey); + if (StrUtil.isNotEmpty(url)) { + moonshotApiBuilder.baseUrl(url); + } MoonshotChatOptions options = MoonshotChatOptions.builder().model(MoonshotApi.DEFAULT_CHAT_MODEL).build(); - return new MoonshotChatModel(moonshotApi, options, getFunctionCallbackResolver(), DEFAULT_RETRY_TEMPLATE); + return MoonshotChatModel.builder() + .moonshotApi(moonshotApiBuilder.build()) + .defaultOptions(options) + .toolCallingManager(getToolCallingManager()) + .build(); } /** @@ -456,33 +488,32 @@ public class AiModelFactoryImpl implements AiModelFactory { } /** - * 可参考 {@link OpenAiAutoConfiguration} 的 openAiChatModel 方法 + * 可参考 {@link OpenAiChatAutoConfiguration} 的 openAiChatModel 方法 */ private static OpenAiChatModel buildOpenAiChatModel(String openAiToken, String url) { url = StrUtil.blankToDefault(url, OpenAiApiConstants.DEFAULT_BASE_URL); OpenAiApi openAiApi = OpenAiApi.builder().baseUrl(url).apiKey(openAiToken).build(); - return OpenAiChatModel.builder().openAiApi(openAiApi).toolCallingManager(getToolCallingManager()).build(); + return OpenAiChatModel.builder() + .openAiApi(openAiApi) + .toolCallingManager(getToolCallingManager()) + .build(); } - // TODO @芋艿:手头暂时没密钥,使用建议再测试下 /** - * 可参考 {@link AzureOpenAiAutoConfiguration} + * 可参考 {@link AzureOpenAiChatAutoConfiguration} */ private static AzureOpenAiChatModel buildAzureOpenAiChatModel(String apiKey, String url) { - AzureOpenAiAutoConfiguration azureOpenAiAutoConfiguration = new AzureOpenAiAutoConfiguration(); - // 创建 OpenAIClient 对象 - AzureOpenAiConnectionProperties connectionProperties = new AzureOpenAiConnectionProperties(); - connectionProperties.setApiKey(apiKey); - connectionProperties.setEndpoint(url); - OpenAIClientBuilder openAIClient = azureOpenAiAutoConfiguration.openAIClientBuilder(connectionProperties, null); - // 获取 AzureOpenAiChatProperties 对象 - AzureOpenAiChatProperties chatProperties = SpringUtil.getBean(AzureOpenAiChatProperties.class); - return azureOpenAiAutoConfiguration.azureOpenAiChatModel(openAIClient, chatProperties, - getToolCallingManager(), null, null); + // TODO @芋艿:使用前,请测试,暂时没密钥!!! + OpenAIClientBuilder openAIClientBuilder = new OpenAIClientBuilder() + .endpoint(url).credential(new KeyCredential(apiKey)); + return AzureOpenAiChatModel.builder() + .openAIClientBuilder(openAIClientBuilder) + .toolCallingManager(getToolCallingManager()) + .build(); } /** - * 可参考 {@link OpenAiAutoConfiguration} 的 openAiImageModel 方法 + * 可参考 {@link OpenAiImageAutoConfiguration} 的 openAiImageModel 方法 */ private OpenAiImageModel buildOpenAiImageModel(String openAiToken, String url) { url = StrUtil.blankToDefault(url, OpenAiApiConstants.DEFAULT_BASE_URL); @@ -494,16 +525,18 @@ public class AiModelFactoryImpl implements AiModelFactory { * 创建 SiliconFlowImageModel 对象 */ private SiliconFlowImageModel buildSiliconFlowImageModel(String apiToken, String url) { + // TODO @芋艿:未测试 url = StrUtil.blankToDefault(url, SiliconFlowApiConstants.DEFAULT_BASE_URL); SiliconFlowImageApi openAiApi = new SiliconFlowImageApi(url, apiToken); return new SiliconFlowImageModel(openAiApi); } /** - * 可参考 {@link OllamaAutoConfiguration} 的 ollamaApi 方法 + * 可参考 {@link OllamaChatAutoConfiguration} 的 ollamaChatModel 方法 */ private static OllamaChatModel buildOllamaChatModel(String url) { - OllamaApi ollamaApi = new OllamaApi(url); + // TODO @芋艿:未测试 + OllamaApi ollamaApi = OllamaApi.builder().baseUrl(url).build(); return OllamaChatModel.builder().ollamaApi(ollamaApi).toolCallingManager(getToolCallingManager()).build(); } @@ -519,16 +552,16 @@ public class AiModelFactoryImpl implements AiModelFactory { // ========== 各种创建 EmbeddingModel 的方法 ========== /** - * 可参考 {@link DashScopeAutoConfiguration} 的 dashscopeEmbeddingModel 方法 + * 可参考 {@link DashScopeEmbeddingAutoConfiguration} 的 dashscopeEmbeddingModel 方法 */ private DashScopeEmbeddingModel buildTongYiEmbeddingModel(String apiKey, String model) { - DashScopeApi dashScopeApi = new DashScopeApi(apiKey); + DashScopeApi dashScopeApi = DashScopeApi.builder().apiKey(apiKey).build(); DashScopeEmbeddingOptions dashScopeEmbeddingOptions = DashScopeEmbeddingOptions.builder().withModel(model).build(); return new DashScopeEmbeddingModel(dashScopeApi, MetadataMode.EMBED, dashScopeEmbeddingOptions); } /** - * 可参考 {@link ZhiPuAiAutoConfiguration} 的 zhiPuAiEmbeddingModel 方法 + * 可参考 {@link ZhiPuAiEmbeddingAutoConfiguration} 的 zhiPuAiEmbeddingModel 方法 */ private ZhiPuAiEmbeddingModel buildZhiPuEmbeddingModel(String apiKey, String url, String model) { ZhiPuAiApi zhiPuAiApi = StrUtil.isEmpty(url) ? new ZhiPuAiApi(apiKey) @@ -538,7 +571,7 @@ public class AiModelFactoryImpl implements AiModelFactory { } /** - * 可参考 {@link MiniMaxAutoConfiguration} 的 miniMaxEmbeddingModel 方法 + * 可参考 {@link MiniMaxEmbeddingAutoConfiguration} 的 miniMaxEmbeddingModel 方法 */ private EmbeddingModel buildMiniMaxEmbeddingModel(String apiKey, String url, String model) { MiniMaxApi miniMaxApi = StrUtil.isEmpty(url)? new MiniMaxApi(apiKey) @@ -548,7 +581,7 @@ public class AiModelFactoryImpl implements AiModelFactory { } /** - * 可参考 {@link QianFanAutoConfiguration} 的 qianFanEmbeddingModel 方法 + * 可参考 {@link QianFanEmbeddingAutoConfiguration} 的 qianFanEmbeddingModel 方法 */ private QianFanEmbeddingModel buildYiYanEmbeddingModel(String key, String model) { List keys = StrUtil.split(key, '|'); @@ -561,13 +594,13 @@ public class AiModelFactoryImpl implements AiModelFactory { } private OllamaEmbeddingModel buildOllamaEmbeddingModel(String url, String model) { - OllamaApi ollamaApi = new OllamaApi(url); + OllamaApi ollamaApi = OllamaApi.builder().baseUrl(url).build(); OllamaOptions ollamaOptions = OllamaOptions.builder().model(model).build(); return OllamaEmbeddingModel.builder().ollamaApi(ollamaApi).defaultOptions(ollamaOptions).build(); } /** - * 可参考 {@link OpenAiAutoConfiguration} 的 openAiEmbeddingModel 方法 + * 可参考 {@link OpenAiEmbeddingAutoConfiguration} 的 openAiEmbeddingModel 方法 */ private OpenAiEmbeddingModel buildOpenAiEmbeddingModel(String openAiToken, String url, String model) { url = StrUtil.blankToDefault(url, OpenAiApiConstants.DEFAULT_BASE_URL); @@ -576,21 +609,19 @@ public class AiModelFactoryImpl implements AiModelFactory { return new OpenAiEmbeddingModel(openAiApi, MetadataMode.EMBED, openAiEmbeddingProperties); } - // TODO @芋艿:手头暂时没密钥,使用建议再测试下 /** - * 可参考 {@link AzureOpenAiAutoConfiguration} 的 azureOpenAiEmbeddingModel 方法 + * 可参考 {@link AzureOpenAiEmbeddingAutoConfiguration} 的 azureOpenAiEmbeddingModel 方法 */ private AzureOpenAiEmbeddingModel buildAzureOpenAiEmbeddingModel(String apiKey, String url, String model) { - AzureOpenAiAutoConfiguration azureOpenAiAutoConfiguration = new AzureOpenAiAutoConfiguration(); - // 创建 OpenAIClient 对象 - AzureOpenAiConnectionProperties connectionProperties = new AzureOpenAiConnectionProperties(); - connectionProperties.setApiKey(apiKey); - connectionProperties.setEndpoint(url); - OpenAIClientBuilder openAIClient = azureOpenAiAutoConfiguration.openAIClientBuilder(connectionProperties, null); + // TODO @芋艿:手头暂时没密钥,使用建议再测试下 + AzureOpenAiEmbeddingAutoConfiguration azureOpenAiAutoConfiguration = new AzureOpenAiEmbeddingAutoConfiguration(); + // 创建 OpenAIClientBuilder 对象 + OpenAIClientBuilder openAIClientBuilder = new OpenAIClientBuilder() + .endpoint(url).credential(new KeyCredential(apiKey)); // 获取 AzureOpenAiChatProperties 对象 AzureOpenAiEmbeddingProperties embeddingProperties = SpringUtil.getBean(AzureOpenAiEmbeddingProperties.class); - return azureOpenAiAutoConfiguration.azureOpenAiEmbeddingModel(openAIClient, embeddingProperties, - null, null); + return azureOpenAiAutoConfiguration.azureOpenAiEmbeddingModel(openAIClientBuilder, embeddingProperties, + getObservationRegistry(), getEmbeddingModelObservationConvention()); } // ========== 各种创建 VectorStore 的方法 ========== @@ -657,10 +688,11 @@ public class AiModelFactoryImpl implements AiModelFactory { RedisProperties redisProperties = SpringUtils.getBean(RedisProperties.class); JedisPooled jedisPooled = new JedisPooled(redisProperties.getHost(), redisProperties.getPort()); // 创建 RedisVectorStoreProperties 对象 + // TODO @芋艿:index-name 可能影响索引名; RedisVectorStoreAutoConfiguration configuration = new RedisVectorStoreAutoConfiguration(); RedisVectorStoreProperties properties = SpringUtil.getBean(RedisVectorStoreProperties.class); RedisVectorStore redisVectorStore = RedisVectorStore.builder(jedisPooled, embeddingModel) - .indexName(properties.getIndex()).prefix(properties.getPrefix()) + .indexName(properties.getIndexName()).prefix(properties.getPrefix()) .initializeSchema(properties.isInitializeSchema()) .metadataFields(convertList(metadataFields.entrySet(), entry -> { String fieldName = entry.getKey(); @@ -730,10 +762,12 @@ public class AiModelFactoryImpl implements AiModelFactory { private static ObjectProvider getCustomObservationConvention() { return new ObjectProvider<>() { + @Override public VectorStoreObservationConvention getObject() throws BeansException { return new DefaultVectorStoreObservationConvention(); } + }; } @@ -745,8 +779,15 @@ public class AiModelFactoryImpl implements AiModelFactory { return SpringUtil.getBean(ToolCallingManager.class); } - private static FunctionCallbackResolver getFunctionCallbackResolver() { - return SpringUtil.getBean(FunctionCallbackResolver.class); + private static ObjectProvider getEmbeddingModelObservationConvention() { + return new ObjectProvider<>() { + + @Override + public EmbeddingModelObservationConvention getObject() throws BeansException { + return SpringUtil.getBean(EmbeddingModelObservationConvention.class); + } + + }; } } diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/AzureOpenAIChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/AzureOpenAIChatModelTests.java index 5c924a5823..69776d8e68 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/AzureOpenAIChatModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/AzureOpenAIChatModelTests.java @@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; import com.azure.ai.openai.OpenAIClientBuilder; import com.azure.core.credential.AzureKeyCredential; -import com.azure.core.util.ClientOptions; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.ai.azure.openai.AzureOpenAiChatModel; @@ -17,7 +16,7 @@ import reactor.core.publisher.Flux; import java.util.ArrayList; import java.util.List; -import static org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiChatProperties.DEFAULT_DEPLOYMENT_NAME; +import static org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiChatProperties.DEFAULT_DEPLOYMENT_NAME; /** * {@link AzureOpenAiChatModel} 集成测试 @@ -29,10 +28,13 @@ public class AzureOpenAIChatModelTests { // TODO @芋艿:晚点在调整 private final OpenAIClientBuilder openAiApi = new OpenAIClientBuilder() .endpoint("https://eastusprejade.openai.azure.com") - .credential(new AzureKeyCredential("xxx")) - .clientOptions((new ClientOptions()).setApplicationId("spring-ai")); - private final AzureOpenAiChatModel chatModel = new AzureOpenAiChatModel(openAiApi, - AzureOpenAiChatOptions.builder().deploymentName(DEFAULT_DEPLOYMENT_NAME).build()); + .credential(new AzureKeyCredential("xxx")); + private final AzureOpenAiChatModel chatModel = AzureOpenAiChatModel.builder() + .openAIClientBuilder(openAiApi) + .defaultOptions(AzureOpenAiChatOptions.builder() + .deploymentName(DEFAULT_DEPLOYMENT_NAME) + .build()) + .build(); @Test @Disabled diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/BaiChuanChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/BaiChuanChatModelTests.java index d1cc381fb9..06b0b25653 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/BaiChuanChatModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/BaiChuanChatModelTests.java @@ -1,7 +1,6 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; import cn.iocoder.yudao.module.ai.framework.ai.core.model.baichuan.BaiChuanChatModel; -import cn.iocoder.yudao.module.ai.framework.ai.core.model.deepseek.DeepSeekChatModel; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.ai.chat.messages.Message; @@ -35,7 +34,7 @@ public class BaiChuanChatModelTests { .build()) .build(); - private final DeepSeekChatModel chatModel = new DeepSeekChatModel(openAiChatModel); + private final BaiChuanChatModel chatModel = new BaiChuanChatModel(openAiChatModel); @Test @Disabled diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MoonshotChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MoonshotChatModelTests.java index 7de7fd709c..992334b4d9 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MoonshotChatModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MoonshotChatModelTests.java @@ -2,14 +2,14 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.springaicommunity.moonshot.MoonshotChatModel; +import org.springaicommunity.moonshot.MoonshotChatOptions; +import org.springaicommunity.moonshot.api.MoonshotApi; import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.messages.SystemMessage; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.moonshot.MoonshotChatModel; -import org.springframework.ai.moonshot.MoonshotChatOptions; -import org.springframework.ai.moonshot.api.MoonshotApi; import reactor.core.publisher.Flux; import java.util.ArrayList; @@ -22,11 +22,15 @@ import java.util.List; */ public class MoonshotChatModelTests { - private final MoonshotChatModel chatModel = new MoonshotChatModel( - new MoonshotApi("sk-aHYYV1SARscItye5QQRRNbXij4fy65Ee7pNZlC9gsSQnUKXA"), // 密钥 - MoonshotChatOptions.builder() - .model("moonshot-v1-8k") // 模型 - .build()); + private final MoonshotChatModel chatModel = MoonshotChatModel.builder() + .moonshotApi(MoonshotApi.builder() + .apiKey("sk-aHYYV1SARscItye5QQRRNbXij4fy65Ee7pNZlC9gsSQnUKXA") // 密钥 + .build()) + .defaultOptions(MoonshotChatOptions.builder() + .model("kimi-k2-0711-preview") // 模型 + .build()) + .build(); + @Test @Disabled public void testCall() { diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/StabilityAiImageModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/StabilityAiImageModelTests.java index b58e6df00e..8cf556d9f8 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/StabilityAiImageModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/StabilityAiImageModelTests.java @@ -31,8 +31,8 @@ public class StabilityAiImageModelTests { public void testCall() { // 准备参数 ImageOptions options = OpenAiImageOptions.builder() - .withModel("stable-diffusion-v1-6") - .withHeight(320).withWidth(320) + .model("stable-diffusion-v1-6") + .height(320).width(320) .build(); ImagePrompt prompt = new ImagePrompt("great wall", options); diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index 37b783d57d..b9658e9746 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -188,13 +188,14 @@ spring: api-key: xxxx moonshot: # 月之暗灭(KIMI) api-key: sk-abc + deepseek: # DeepSeek + api-key: sk-e94db327cc7d457d99a8de8810fc6b12 + chat: + options: + model: deepseek-chat yudao: ai: - deep-seek: # DeepSeek - enable: true - api-key: sk-e94db327cc7d457d99a8de8810fc6b12 - model: deepseek-chat doubao: # 字节豆包 enable: true api-key: 5c1b5747-26d2-4ebd-a4e0-dd0e8d8b4272 From 39ecf5ebe5aa8489a7ec44fd9bf31226c2f6887d Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 14 Jul 2025 22:42:56 +0800 Subject: [PATCH 31/89] =?UTF-8?q?feat=EF=BC=9A=E3=80=90AI=20=E5=A4=A7?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E3=80=91=E6=96=87=E5=BF=83=E4=B8=80=E8=A8=80?= =?UTF-8?q?=E5=AD=98=E5=9C=A8=20springai=20=E6=8E=A5=E5=85=A5=E9=97=AE?= =?UTF-8?q?=E9=A2=98=EF=BC=8C=E6=97=A0=E6=B3=95=E4=BD=BF=E7=94=A8=20https:?= =?UTF-8?q?//github.com/spring-ai-community/qianfan/issues/6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../framework/ai/core/AiModelFactoryImpl.java | 16 ++-- .../core/model/chat/LlamaChatModelTests.java | 90 ++++++++----------- .../core/model/chat/OllamaChatModelTests.java | 4 +- .../core/model/chat/YiYanChatModelTests.java | 10 +-- .../core/model/image/QianFanImageTests.java | 6 +- 5 files changed, 59 insertions(+), 67 deletions(-) diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/AiModelFactoryImpl.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/AiModelFactoryImpl.java index 2aeecab917..759c0a038f 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/AiModelFactoryImpl.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/AiModelFactoryImpl.java @@ -357,7 +357,7 @@ public class AiModelFactoryImpl implements AiModelFactory { * 可参考 {@link QianFanChatAutoConfiguration} 的 qianFanChatModel 方法 */ private static QianFanChatModel buildYiYanChatModel(String key) { - // TODO @芋艿:未测试 + // TODO spring ai qianfan 有 bug,无法使用 https://github.com/spring-ai-community/qianfan/issues/6 List keys = StrUtil.split(key, '|'); Assert.equals(keys.size(), 2, "YiYanChatClient 的密钥需要 (appKey|secretKey) 格式"); String appKey = keys.get(0); @@ -370,7 +370,7 @@ public class AiModelFactoryImpl implements AiModelFactory { * 可参考 {@link QianFanEmbeddingAutoConfiguration} 的 qianFanImageModel 方法 */ private QianFanImageModel buildQianFanImageModel(String key) { - // TODO @芋艿:未测试 + // TODO spring ai qianfan 有 bug,无法使用 https://github.com/spring-ai-community/qianfan/issues/6 List keys = StrUtil.split(key, '|'); Assert.equals(keys.size(), 2, "YiYanChatClient 的密钥需要 (appKey|secretKey) 格式"); String appKey = keys.get(0); @@ -525,7 +525,6 @@ public class AiModelFactoryImpl implements AiModelFactory { * 创建 SiliconFlowImageModel 对象 */ private SiliconFlowImageModel buildSiliconFlowImageModel(String apiToken, String url) { - // TODO @芋艿:未测试 url = StrUtil.blankToDefault(url, SiliconFlowApiConstants.DEFAULT_BASE_URL); SiliconFlowImageApi openAiApi = new SiliconFlowImageApi(url, apiToken); return new SiliconFlowImageModel(openAiApi); @@ -535,9 +534,11 @@ public class AiModelFactoryImpl implements AiModelFactory { * 可参考 {@link OllamaChatAutoConfiguration} 的 ollamaChatModel 方法 */ private static OllamaChatModel buildOllamaChatModel(String url) { - // TODO @芋艿:未测试 OllamaApi ollamaApi = OllamaApi.builder().baseUrl(url).build(); - return OllamaChatModel.builder().ollamaApi(ollamaApi).toolCallingManager(getToolCallingManager()).build(); + return OllamaChatModel.builder() + .ollamaApi(ollamaApi) + .toolCallingManager(getToolCallingManager()) + .build(); } /** @@ -596,7 +597,10 @@ public class AiModelFactoryImpl implements AiModelFactory { private OllamaEmbeddingModel buildOllamaEmbeddingModel(String url, String model) { OllamaApi ollamaApi = OllamaApi.builder().baseUrl(url).build(); OllamaOptions ollamaOptions = OllamaOptions.builder().model(model).build(); - return OllamaEmbeddingModel.builder().ollamaApi(ollamaApi).defaultOptions(ollamaOptions).build(); + return OllamaEmbeddingModel.builder() + .ollamaApi(ollamaApi) + .defaultOptions(ollamaOptions) + .build(); } /** diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/LlamaChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/LlamaChatModelTests.java index 153342d44c..69e2c1daaa 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/LlamaChatModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/LlamaChatModelTests.java @@ -1,20 +1,6 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.springframework.ai.chat.messages.Message; -import org.springframework.ai.chat.messages.SystemMessage; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.ollama.OllamaChatModel; -import org.springframework.ai.ollama.api.OllamaApi; -import org.springframework.ai.ollama.api.OllamaModel; -import org.springframework.ai.ollama.api.OllamaOptions; -import reactor.core.publisher.Flux; - -import java.util.ArrayList; -import java.util.List; /** * {@link OllamaChatModel} 集成测试 @@ -23,43 +9,43 @@ import java.util.List; */ public class LlamaChatModelTests { - private final OllamaChatModel chatModel = OllamaChatModel.builder() - .ollamaApi(new OllamaApi("http://127.0.0.1:11434")) // Ollama 服务地址 - .defaultOptions(OllamaOptions.builder() - .model(OllamaModel.LLAMA3.getName()) // 模型 - .build()) - .build(); - - @Test - @Disabled - public void testCall() { - // 准备参数 - List messages = new ArrayList<>(); - messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); - messages.add(new UserMessage("1 + 1 = ?")); - - // 调用 - ChatResponse response = chatModel.call(new Prompt(messages)); - // 打印结果 - System.out.println(response); - System.out.println(response.getResult().getOutput()); - } - - @Test - @Disabled - public void testStream() { - // 准备参数 - List messages = new ArrayList<>(); - messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); - messages.add(new UserMessage("1 + 1 = ?")); - - // 调用 - Flux flux = chatModel.stream(new Prompt(messages)); - // 打印结果 - flux.doOnNext(response -> { -// System.out.println(response); - System.out.println(response.getResult().getOutput()); - }).then().block(); - } +// private final OllamaChatModel chatModel = OllamaChatModel.builder() +// .ollamaApi(new OllamaApi("http://127.0.0.1:11434")) // Ollama 服务地址 +// .defaultOptions(OllamaOptions.builder() +// .model(OllamaModel.LLAMA3.getName()) // 模型 +// .build()) +// .build(); +// +// @Test +// @Disabled +// public void testCall() { +// // 准备参数 +// List messages = new ArrayList<>(); +// messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); +// messages.add(new UserMessage("1 + 1 = ?")); +// +// // 调用 +// ChatResponse response = chatModel.call(new Prompt(messages)); +// // 打印结果 +// System.out.println(response); +// System.out.println(response.getResult().getOutput()); +// } +// +// @Test +// @Disabled +// public void testStream() { +// // 准备参数 +// List messages = new ArrayList<>(); +// messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); +// messages.add(new UserMessage("1 + 1 = ?")); +// +// // 调用 +// Flux flux = chatModel.stream(new Prompt(messages)); +// // 打印结果 +// flux.doOnNext(response -> { +//// System.out.println(response); +// System.out.println(response.getResult().getOutput()); +// }).then().block(); +// } } diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/OllamaChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/OllamaChatModelTests.java index f86e67a667..d2bf68812b 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/OllamaChatModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/OllamaChatModelTests.java @@ -23,7 +23,9 @@ import java.util.List; public class OllamaChatModelTests { private final OllamaChatModel chatModel = OllamaChatModel.builder() - .ollamaApi(new OllamaApi("http://127.0.0.1:11434")) // Ollama 服务地址 + .ollamaApi(OllamaApi.builder() + .baseUrl("http://127.0.0.1:11434") // Ollama 服务地址 + .build()) .defaultOptions(OllamaOptions.builder() // .model("qwen") // 模型(https://ollama.com/library/qwen) .model("deepseek-r1") // 模型(https://ollama.com/library/deepseek-r1) diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/YiYanChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/YiYanChatModelTests.java index ab6f642437..cb7be2a296 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/YiYanChatModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/YiYanChatModelTests.java @@ -2,13 +2,13 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.springaicommunity.qianfan.QianFanChatModel; +import org.springaicommunity.qianfan.QianFanChatOptions; +import org.springaicommunity.qianfan.api.QianFanApi; import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.qianfan.QianFanChatModel; -import org.springframework.ai.qianfan.QianFanChatOptions; -import org.springframework.ai.qianfan.api.QianFanApi; import reactor.core.publisher.Flux; import java.util.ArrayList; @@ -23,9 +23,9 @@ import java.util.List; public class YiYanChatModelTests { private final QianFanChatModel chatModel = new QianFanChatModel( - new QianFanApi("qS8k8dYr2nXunagK4SSU8Xjj", "pHGbx51ql2f0hOyabQvSZezahVC3hh3e"), // 密钥 + new QianFanApi("DGnyzREuaY7av7c38bOM9Ji2", "9aR8myflEOPDrEeLhoXv0FdqANOAyIZW"), // 密钥 QianFanChatOptions.builder() - .model(QianFanApi.ChatModel.ERNIE_4_0_8K_Preview.getValue()) + .model("ERNIE-4.5-8K-Preview") .build() ); diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/QianFanImageTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/QianFanImageTests.java index 8f44ab9ad1..156360f255 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/QianFanImageTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/QianFanImageTests.java @@ -2,11 +2,11 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model.image; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.springaicommunity.qianfan.QianFanImageModel; +import org.springaicommunity.qianfan.QianFanImageOptions; +import org.springaicommunity.qianfan.api.QianFanImageApi; import org.springframework.ai.image.ImagePrompt; import org.springframework.ai.image.ImageResponse; -import org.springframework.ai.qianfan.QianFanImageModel; -import org.springframework.ai.qianfan.QianFanImageOptions; -import org.springframework.ai.qianfan.api.QianFanImageApi; import static cn.iocoder.yudao.module.ai.framework.ai.core.model.image.StabilityAiImageModelTests.viewImage; From ea5b12f21e2c0eb3e9e833b12e1af3bfad0681f1 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 14 Jul 2025 22:44:50 +0800 Subject: [PATCH 32/89] =?UTF-8?q?fix=EF=BC=9A=E3=80=90AI=20=E5=A4=A7?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E3=80=91RedisVectorStore=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81=20username=E3=80=81password?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/ai/framework/ai/core/AiModelFactoryImpl.java | 5 ++--- yudao-server/src/main/resources/application.yaml | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/AiModelFactoryImpl.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/AiModelFactoryImpl.java index 759c0a038f..f7b42e30ae 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/AiModelFactoryImpl.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/AiModelFactoryImpl.java @@ -690,10 +690,9 @@ public class AiModelFactoryImpl implements AiModelFactory { Map> metadataFields) { // 创建 JedisPooled 对象 RedisProperties redisProperties = SpringUtils.getBean(RedisProperties.class); - JedisPooled jedisPooled = new JedisPooled(redisProperties.getHost(), redisProperties.getPort()); + JedisPooled jedisPooled = new JedisPooled(redisProperties.getHost(), redisProperties.getPort(), + redisProperties.getUsername(), redisProperties.getPassword()); // 创建 RedisVectorStoreProperties 对象 - // TODO @芋艿:index-name 可能影响索引名; - RedisVectorStoreAutoConfiguration configuration = new RedisVectorStoreAutoConfiguration(); RedisVectorStoreProperties properties = SpringUtil.getBean(RedisVectorStoreProperties.class); RedisVectorStore redisVectorStore = RedisVectorStore.builder(jedisPooled, embeddingModel) .indexName(properties.getIndexName()).prefix(properties.getPrefix()) diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index b9658e9746..12a4abf896 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -150,7 +150,7 @@ spring: vectorstore: # 向量存储 redis: initialize-schema: true - index: knowledge_index # Redis 中向量索引的名称:用于存储和检索向量数据的索引标识符,所有相关的向量搜索操作都会基于这个索引进行 + index-name: knowledge_index # Redis 中向量索引的名称:用于存储和检索向量数据的索引标识符,所有相关的向量搜索操作都会基于这个索引进行 prefix: "knowledge_segment:" # Redis 中存储向量数据的键名前缀:这个前缀会添加到每个存储在 Redis 中的向量数据键名前,每个 document 都是一个 hash 结构 qdrant: initialize-schema: true From a10e7aedaa41a0618c6fa67e811abc045e4318be Mon Sep 17 00:00:00 2001 From: YunaiV Date: Wed, 16 Jul 2025 00:02:57 +0800 Subject: [PATCH 33/89] =?UTF-8?q?fix=EF=BC=9A=E3=80=90Pay=20=E6=94=AF?= =?UTF-8?q?=E4=BB=98=E3=80=91=E5=8E=BB=E9=99=A4=2054=20=E5=BC=BA=E5=88=B6?= =?UTF-8?q?=20id=20=E8=BD=AC=E8=B4=A6=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/pay/service/transfer/PayTransferServiceImpl.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceImpl.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceImpl.java index dbd12c7986..8f123f8102 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceImpl.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceImpl.java @@ -264,9 +264,6 @@ public class PayTransferServiceImpl implements PayTransferService { } int count = 0; for (PayTransferDO transfer : list) { - if (!transfer.getId().equals(54L)) { - continue; - } count += syncTransfer(transfer) ? 1 : 0; } return count; From aef47bcf70e423c486fe00f4f98305d01116d773 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Wed, 16 Jul 2025 00:11:27 +0800 Subject: [PATCH 34/89] =?UTF-8?q?fix=EF=BC=9A=E3=80=90INFRA=20=E5=9F=BA?= =?UTF-8?q?=E7=A1=80=E8=AE=BE=E6=96=BD=E3=80=91=E4=BB=A3=E7=A0=81=E7=94=9F?= =?UTF-8?q?=E6=88=90=E6=A8=A1=E7=89=88=EF=BC=8Cclean=20=E7=BC=BA=E5=B0=91?= =?UTF-8?q?=E4=BA=86=20;=20=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/codegen/java/service/serviceImpl.vm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yudao-module-infra/src/main/resources/codegen/java/service/serviceImpl.vm b/yudao-module-infra/src/main/resources/codegen/java/service/serviceImpl.vm index ee9fc49f97..42afdf90ae 100644 --- a/yudao-module-infra/src/main/resources/codegen/java/service/serviceImpl.vm +++ b/yudao-module-infra/src/main/resources/codegen/java/service/serviceImpl.vm @@ -316,7 +316,7 @@ public class ${table.className}ServiceImpl implements ${table.className}Service } // 插入 #end - ${subClassNameVar}.clean() // 清理掉创建、更新时间等相关属性值 + ${subClassNameVar}.clean(); // 清理掉创建、更新时间等相关属性值 ${subClassNameVars.get($index)}Mapper.insert(${subClassNameVar}); return ${subClassNameVar}.getId(); } From 6a117c9d550ac8bc9448d0eac23b8512dbccf515 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 19 Jul 2025 10:18:29 +0800 Subject: [PATCH 35/89] =?UTF-8?q?review=EF=BC=9A=E3=80=90IoT=20=E7=89=A9?= =?UTF-8?q?=E8=81=94=E7=BD=91=E3=80=91TCP=20=E7=BD=91=E7=BB=9C=E6=8E=A5?= =?UTF-8?q?=E5=85=A5=E7=9A=84=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../codec/tcp/IotTcpDeviceMessageCodec.java | 29 ++++++------ .../tcp/IotTcpDownstreamSubscriber.java | 13 +++--- .../protocol/tcp/IotTcpUpstreamProtocol.java | 1 + .../protocol/tcp/client/TcpDeviceClient.java | 46 ++++++++++--------- .../manager/TcpDeviceConnectionManager.java | 23 ++++++---- .../protocol/tcp/protocol/TcpDataDecoder.java | 21 +++++---- .../protocol/tcp/protocol/TcpDataEncoder.java | 33 ++++--------- .../protocol/tcp/protocol/TcpDataPackage.java | 9 +++- .../protocol/tcp/protocol/TcpDataReader.java | 13 ++++-- .../tcp/router/IotTcpDownstreamHandler.java | 11 ++--- .../tcp/router/IotTcpUpstreamHandler.java | 36 ++++++--------- 11 files changed, 114 insertions(+), 121 deletions(-) diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpDeviceMessageCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpDeviceMessageCodec.java index 0bcef2e0cb..6a558b5141 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpDeviceMessageCodec.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpDeviceMessageCodec.java @@ -54,6 +54,7 @@ public class IotTcpDeviceMessageCodec implements IotDeviceMessageCodec { static { // 初始化方法映射 + // TODO @haohao:有没可能去掉这个 code 到 method 的映射哈? initializeMethodMappings(); } @@ -75,6 +76,7 @@ public class IotTcpDeviceMessageCodec implements IotDeviceMessageCodec { * 负载字段名 */ public static class PayloadField { + public static final String TIMESTAMP = "timestamp"; public static final String MESSAGE_ID = "msgId"; public static final String DEVICE_ID = "deviceId"; @@ -82,12 +84,14 @@ public class IotTcpDeviceMessageCodec implements IotDeviceMessageCodec { public static final String DATA = "data"; public static final String CODE = "code"; public static final String MESSAGE = "message"; + } /** * 消息方法映射 */ public static class MessageMethod { + public static final String PROPERTY_POST = "thing.property.post"; public static final String PROPERTY_SET = "thing.property.set"; public static final String PROPERTY_GET = "thing.property.get"; @@ -97,6 +101,7 @@ public class IotTcpDeviceMessageCodec implements IotDeviceMessageCodec { public static final String OTA_UPGRADE = "thing.ota.upgrade"; public static final String STATE_ONLINE = "thing.state.online"; public static final String STATE_OFFLINE = "thing.state.offline"; + } // ==================== 初始化方法 ==================== @@ -139,9 +144,9 @@ public class IotTcpDeviceMessageCodec implements IotDeviceMessageCodec { // 3. 构建 TCP 数据包 TcpDataPackage dataPackage = TcpDataPackage.builder() - .addr("") // 地址在发送时由调用方设置 + .addr("") .code(code) - .mid((short) 0) // 消息序号在发送时由调用方设置 + .mid((short) 0) .payload(payload) .build(); @@ -154,9 +159,7 @@ public class IotTcpDeviceMessageCodec implements IotDeviceMessageCodec { log.debug("[encode][TCP 消息编码成功] 方法: {}, 数据长度: {}", message.getMethod(), result.length); } - return result; - } catch (Exception e) { log.error("[encode][TCP 消息编码失败] 消息: {}", message, e); throw new TcpCodecException("TCP 消息编码失败", e); @@ -175,13 +178,10 @@ public class IotTcpDeviceMessageCodec implements IotDeviceMessageCodec { // 1. 解码 TCP 数据包 Buffer buffer = Buffer.buffer(bytes); TcpDataPackage dataPackage = TcpDataDecoder.decode(buffer); - // 2. 获取消息方法 String method = getMethodByCodeSafely(dataPackage.getCode()); - // 3. 解析负载数据 Object params = parsePayloadOptimized(dataPackage.getPayload()); - // 4. 构建 IoT 设备消息 IotDeviceMessage message = IotDeviceMessage.requestOf(method, params); @@ -190,9 +190,7 @@ public class IotTcpDeviceMessageCodec implements IotDeviceMessageCodec { log.debug("[decode][TCP 消息解码成功] 方法: {}, 功能码: {}", method, dataPackage.getCode()); } - return message; - } catch (Exception e) { log.error("[decode][TCP 消息解码失败] 数据长度: {}, 数据内容: {}", bytes.length, truncateData(bytes, 100), e); @@ -226,8 +224,8 @@ public class IotTcpDeviceMessageCodec implements IotDeviceMessageCodec { if (Objects.isNull(bytes) || bytes.length == 0) { throw new IllegalArgumentException("待解码数据不能为空"); } - if (bytes.length > 1024 * 1024) { // 1MB 限制 - throw new IllegalArgumentException("数据包过大,超过1MB限制"); + if (bytes.length > 1024 * 1024) { + throw new IllegalArgumentException("数据包过大,超过 1MB 限制"); } } @@ -236,9 +234,10 @@ public class IotTcpDeviceMessageCodec implements IotDeviceMessageCodec { */ private short getCodeByMethodSafely(String method) { Short code = METHOD_TO_CODE_MAP.get(method); + // 默认为数据上报 if (code == null) { log.warn("[getCodeByMethodSafely][未知的消息方法: {},使用默认功能码]", method); - return TcpDataPackage.CODE_DATA_UP; // 默认为数据上报 + return TcpDataPackage.CODE_DATA_UP; } return code; } @@ -260,6 +259,7 @@ public class IotTcpDeviceMessageCodec implements IotDeviceMessageCodec { */ private String buildPayloadOptimized(IotDeviceMessage message) { // 使用缓存键 + // TODO @haohao:是不是不用缓存哈? String cacheKey = message.getMethod() + "_" + message.getRequestId(); JSONObject cachedPayload = jsonCache.get(cacheKey); @@ -271,7 +271,6 @@ public class IotTcpDeviceMessageCodec implements IotDeviceMessageCodec { // 创建新的负载 JSONObject payload = new JSONObject(); - // 添加基础字段 addToPayloadIfNotNull(payload, PayloadField.MESSAGE_ID, message.getRequestId()); addToPayloadIfNotNull(payload, PayloadField.DEVICE_ID, message.getDeviceId()); @@ -279,7 +278,6 @@ public class IotTcpDeviceMessageCodec implements IotDeviceMessageCodec { addToPayloadIfNotNull(payload, PayloadField.DATA, message.getData()); addToPayloadIfNotNull(payload, PayloadField.CODE, message.getCode()); addToPayloadIfNotEmpty(payload, PayloadField.MESSAGE, message.getMsg()); - // 添加时间戳 payload.set(PayloadField.TIMESTAMP, System.currentTimeMillis()); @@ -317,7 +315,6 @@ public class IotTcpDeviceMessageCodec implements IotDeviceMessageCodec { } return jsonObject.containsKey(PayloadField.PARAMS) ? jsonObject.get(PayloadField.PARAMS) : jsonObject; - } catch (JSONException e) { log.warn("[parsePayloadOptimized][负载解析为JSON失败,返回原始字符串] 负载: {}", payload); return payload; @@ -379,6 +376,7 @@ public class IotTcpDeviceMessageCodec implements IotDeviceMessageCodec { * TCP 编解码异常 */ public static class TcpCodecException extends RuntimeException { + public TcpCodecException(String message) { super(message); } @@ -386,5 +384,6 @@ public class IotTcpDeviceMessageCodec implements IotDeviceMessageCodec { public TcpCodecException(String message, Throwable cause) { super(message, cause); } + } } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java index d5c916295c..3f47e14080 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java @@ -108,16 +108,14 @@ public class IotTcpDownstreamSubscriber implements IotMessageSubscriber 1000) { // 超过1秒的慢消息 + // TODO @haohao:1000 搞成静态变量; + if (processTime > 1000) { // 超过 1 秒的慢消息 log.warn("[onMessage][慢消息处理] 设备ID: {}, 方法: {}, 耗时: {}ms", message.getDeviceId(), message.getMethod(), processTime); } - } catch (Exception e) { failedMessages.incrementAndGet(); log.error("[onMessage][处理下行消息失败] 设备ID: {}, 方法: {}, 消息: {}", @@ -142,6 +140,8 @@ public class IotTcpDownstreamSubscriber implements IotMessageSubscriber - * 封装设备连接的基本信息和操作。 * 该类中的状态变更(如 authenticated, closed)使用 AtomicBoolean 保证原子性。 * 对 socket 的操作应在 Vert.x Event Loop 线程中执行,以避免并发问题。 * @@ -48,7 +47,7 @@ public class TcpDeviceClient { private RecordParser parser; @Getter - private final long keepAliveTimeoutMs; // 改为 final,通过构造函数注入 + private final long keepAliveTimeoutMs; private volatile long lastKeepAliveTime; @@ -58,7 +57,7 @@ public class TcpDeviceClient { /** * 构造函数 * - * @param clientId 客户端ID,全局唯一 + * @param clientId 客户端 ID,全局唯一 * @param keepAliveTimeoutMs 心跳超时时间(毫秒),从配置中读取 */ public TcpDeviceClient(String clientId, long keepAliveTimeoutMs) { @@ -69,19 +68,19 @@ public class TcpDeviceClient { /** * 绑定网络套接字,并设置相关处理器。 - * 此方法应在 Vert.x Event Loop 线程中调用。 + * 此方法应在 Vert.x Event Loop 线程中调用 * * @param socket 网络套接字 */ public void setSocket(NetSocket socket) { // 无需 synchronized,Vert.x 保证了同一个 socket 的事件在同一个 Event Loop 中处理 if (this.socket != null && this.socket != socket) { - log.warn("[setSocket][客户端({})] 正在用新的 socket 替换旧的,旧 socket 将被关闭。", clientId); + log.warn("[setSocket][客户端({}) 正在用新的 socket 替换旧的,旧 socket 将被关闭]", clientId); this.socket.close(); } - this.socket = socket; + // 注册处理器 if (socket != null) { // 1. 设置关闭处理器 socket.closeHandler(v -> { @@ -103,22 +102,22 @@ public class TcpDeviceClient { if (parser != null) { parser.handle(buffer); } else { - log.warn("[setSocket][设备客户端({})] 未设置解析器(parser),原始数据被忽略: {}", clientId, buffer.toString()); + log.warn("[setSocket][设备客户端({}) 未设置解析器(parser),原始数据被忽略: {}]", clientId, buffer.toString()); } }); } } /** - * 更新心跳时间,表示设备仍然活跃。 + * 更新心跳时间,表示设备仍然活跃 */ public void keepAlive() { this.lastKeepAliveTime = System.currentTimeMillis(); } /** - * 检查连接是否在线。 - * 判断标准:未被主动关闭、socket 存在、且在心跳超时时间内。 + * 检查连接是否在线 + * 判断标准:未被主动关闭、socket 存在、且在心跳超时时间内 * * @return 是否在线 */ @@ -130,6 +129,8 @@ public class TcpDeviceClient { return idleTime < keepAliveTimeoutMs; } + // TODO @haohao:1)是不是简化下:productKey 和 deviceName 非空,就认为是已认证;2)如果是的话,productKey 和 deviceName 搞成一个设置方法?setAuthenticated(productKey、deviceName) + public boolean isAuthenticated() { return authenticated.get(); } @@ -139,7 +140,7 @@ public class TcpDeviceClient { } /** - * 向设备发送消息。 + * 向设备发送消息 * * @param buffer 消息内容 */ @@ -151,18 +152,22 @@ public class TcpDeviceClient { // Vert.x 的 write 是异步的,不会阻塞 socket.write(buffer, result -> { - if (result.succeeded()) { - log.debug("[sendMessage][设备客户端({})发送消息成功]", clientId); - // 发送成功也更新心跳,表示连接活跃 - keepAlive(); - } else { + // 发送失败可能意味着连接已断开,主动关闭 + if (!result.succeeded()) { log.error("[sendMessage][设备客户端({})发送消息失败]", clientId, result.cause()); - // 发送失败可能意味着连接已断开,主动关闭 shutdown(); + return; } + + // 发送成功也更新心跳,表示连接活跃 + if (log.isDebugEnabled()) { + log.debug("[sendMessage][设备客户端({})发送消息成功]", clientId); + } + keepAlive(); }); } + // TODO @haohao:是不是叫 close 好点?或者问问大模型 /** * 关闭客户端连接并清理资源。 * 这是一个幂等操作,可以被多次安全调用。 @@ -200,10 +205,6 @@ public class TcpDeviceClient { return "disconnected"; } - public long getLastKeepAliveTime() { - return lastKeepAliveTime; - } - @Override public String toString() { return "TcpDeviceClient{" + @@ -215,4 +216,5 @@ public class TcpDeviceClient { ", connection=" + getConnectionInfo() + '}'; } + } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/TcpDeviceConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/TcpDeviceConnectionManager.java index ce7fe4aa5c..b2b6b3c31e 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/TcpDeviceConnectionManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/TcpDeviceConnectionManager.java @@ -16,8 +16,8 @@ import java.util.concurrent.locks.ReentrantReadWriteLock; * 参考 EMQX 设计理念: * 1. 高性能连接管理 * 2. 连接池和资源管理 - * 3. 流量控制 - * 4. 监控统计 + * 3. 流量控制 TODO @haohao:这个要不先去掉 + * 4. 监控统计 TODO @haohao:这个要不先去掉 * 5. 自动清理和容错 * * @author 芋道源码 @@ -106,6 +106,7 @@ public class TcpDeviceConnectionManager { * 添加设备客户端 */ public boolean addClient(String deviceAddr, TcpDeviceClient client) { + // TODO @haohao:这个要不去掉;目前看着没做 result 的处理; if (clientMap.size() >= MAX_CONNECTIONS) { log.warn("[addClient][连接数已达上限({}),拒绝新连接: {}]", MAX_CONNECTIONS, deviceAddr); return false; @@ -130,14 +131,13 @@ public class TcpDeviceConnectionManager { socketToAddrMap.put(client.getSocket(), deviceAddr); } - // 如果客户端已设置设备ID,更新映射 + // 如果客户端已设置设备 ID,更新映射 if (client.getDeviceId() != null) { deviceIdToAddrMap.put(client.getDeviceId(), deviceAddr); } totalConnections.incrementAndGet(); return true; - } finally { writeLock.unlock(); } @@ -196,7 +196,7 @@ public class TcpDeviceConnectionManager { } /** - * 通过设备ID获取客户端 + * 通过设备 ID 获取客户端 */ public TcpDeviceClient getClientByDeviceId(Long deviceId) { readLock.lock(); @@ -208,6 +208,8 @@ public class TcpDeviceConnectionManager { } } + // TODO @haohao:getClientBySocket、isDeviceOnline、sendMessage、sendMessageByDeviceId、broadcastMessage 用不到的方法,要不先暂时不提供?保持简洁、更容易理解哈。 + /** * 通过网络连接获取客户端 */ @@ -230,7 +232,7 @@ public class TcpDeviceConnectionManager { } /** - * 设置设备ID映射 + * 设置设备 ID 映射 */ public void setDeviceIdMapping(String deviceAddr, Long deviceId) { writeLock.lock(); @@ -349,12 +351,12 @@ public class TcpDeviceConnectionManager { } } + // TODO @haohao:心跳超时,需要 close 么? /** * 心跳检查任务 */ private void checkHeartbeat() { try { - long currentTime = System.currentTimeMillis(); int offlineCount = 0; readLock.lock(); @@ -369,7 +371,7 @@ public class TcpDeviceConnectionManager { } if (offlineCount > 0) { - log.info("[checkHeartbeat][发现{}个离线设备,将在清理任务中处理]", offlineCount); + log.info("[checkHeartbeat][发现 {} 个离线设备,将在清理任务中处理]", offlineCount); } } catch (Exception e) { log.error("[checkHeartbeat][心跳检查任务异常]", e); @@ -424,14 +426,14 @@ public class TcpDeviceConnectionManager { private void logStatistics() { try { long totalConn = totalConnections.get(); - long totalDisconn = totalDisconnections.get(); + long totalDisconnections = this.totalDisconnections.get(); long totalMsg = totalMessages.get(); long totalFailedMsg = totalFailedMessages.get(); long totalBytesValue = totalBytes.get(); log.info("[logStatistics][连接统计] 总连接: {}, 总断开: {}, 当前在线: {}, 认证设备: {}, " + "总消息: {}, 失败消息: {}, 总字节: {}", - totalConn, totalDisconn, getOnlineCount(), getAuthenticatedCount(), + totalConn, totalDisconnections, getOnlineCount(), getAuthenticatedCount(), totalMsg, totalFailedMsg, totalBytesValue); } catch (Exception e) { log.error("[logStatistics][统计日志任务异常]", e); @@ -500,4 +502,5 @@ public class TcpDeviceConnectionManager { ? (double) (totalMessages.get() - totalFailedMessages.get()) / totalMessages.get() * 100 : 0.0); } + } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataDecoder.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataDecoder.java index 8e7baa37d8..ed4b2ebaa0 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataDecoder.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataDecoder.java @@ -3,13 +3,14 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol; import io.vertx.core.buffer.Buffer; import lombok.extern.slf4j.Slf4j; +// TODO @haohao:“设备地址长度”是不是不需要。 /** * TCP 数据解码器 *

* 负责将字节流解码为 TcpDataPackage 对象 *

* 数据包格式: - * 包头(4字节长度) | 设备地址长度(2字节) | 设备地址(不定长) | 功能码(2字节) | 消息序号(2字节) | 包体(不定长) + * 包头(4 字节长度) | 设备地址长度(2 字节) | 设备地址(不定长) | 功能码(2 字节) | 消息序号(2 字节) | 包体(不定长) * * @author 芋道源码 */ @@ -31,35 +32,35 @@ public class TcpDataDecoder { try { int index = 0; - // 1. 获取设备地址长度(2字节) + // 1.1 获取设备地址长度(2字节) short addrLength = buffer.getShort(index); index += 2; - // 2. 校验数据包长度 + // 1.2 校验数据包长度 int expectedLength = 2 + addrLength + 2 + 2; // 地址长度 + 地址 + 功能码 + 消息序号 if (buffer.length() < expectedLength) { throw new IllegalArgumentException("数据包长度不足,期望至少 " + expectedLength + " 字节"); } - // 3. 获取设备地址 + // 1.3 获取设备地址 String addr = buffer.getBuffer(index, index + addrLength).toString(); index += addrLength; - // 4. 获取功能码(2字节) + // 1.4 获取功能码(2字节) short code = buffer.getShort(index); index += 2; - // 5. 获取消息序号(2字节) + // 1.5 获取消息序号(2字节) short mid = buffer.getShort(index); index += 2; - // 6. 获取包体数据 + // 1.6 获取包体数据 String payload = ""; if (index < buffer.length()) { payload = buffer.getString(index, buffer.length()); } - // 7. 构建数据包对象 + // 2. 构建数据包对象 TcpDataPackage dataPackage = TcpDataPackage.builder() .addrLength((int) addrLength) .addr(addr) @@ -70,15 +71,14 @@ public class TcpDataDecoder { log.debug("[decode][解码成功] 设备地址: {}, 功能码: {}, 消息序号: {}, 包体长度: {}", addr, dataPackage.getCodeDescription(), mid, payload.length()); - return dataPackage; - } catch (Exception e) { log.error("[decode][解码失败] 数据: {}", buffer.toString(), e); throw new IllegalArgumentException("数据包解码失败: " + e.getMessage(), e); } } + // TODO @haohao:这个要不去掉,暂时没用到; /** * 校验数据包格式 * @@ -94,4 +94,5 @@ public class TcpDataDecoder { return false; } } + } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataEncoder.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataEncoder.java index fb0a68c182..62f7bc4848 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataEncoder.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataEncoder.java @@ -27,11 +27,9 @@ public class TcpDataEncoder { if (dataPackage == null) { throw new IllegalArgumentException("数据包对象不能为空"); } - if (dataPackage.getAddr() == null || dataPackage.getAddr().isEmpty()) { throw new IllegalArgumentException("设备地址不能为空"); } - if (dataPackage.getPayload() == null) { dataPackage.setPayload(""); } @@ -39,34 +37,27 @@ public class TcpDataEncoder { try { Buffer buffer = Buffer.buffer(); - // 1. 计算包体长度(除了包头4字节) + // 1. 计算包体长度(除了包头 4 字节) int payloadLength = dataPackage.getPayload().getBytes().length; int totalLength = 2 + dataPackage.getAddr().length() + 2 + 2 + payloadLength; - // 2. 写入包头:总长度(4字节) + // 2.1 写入包头:总长度(4 字节) buffer.appendInt(totalLength); - - // 3. 写入设备地址长度(2字节) + // 2.2 写入设备地址长度(2 字节) buffer.appendShort((short) dataPackage.getAddr().length()); - - // 4. 写入设备地址(不定长) + // 2.3 写入设备地址(不定长) buffer.appendBytes(dataPackage.getAddr().getBytes()); - - // 5. 写入功能码(2字节) + // 2.4 写入功能码(2 字节) buffer.appendShort(dataPackage.getCode()); - - // 6. 写入消息序号(2字节) + // 2.5 写入消息序号(2 字节) buffer.appendShort(dataPackage.getMid()); - - // 7. 写入包体数据(不定长) + // 2.6 写入包体数据(不定长) buffer.appendBytes(dataPackage.getPayload().getBytes()); log.debug("[encode][编码成功] 设备地址: {}, 功能码: {}, 消息序号: {}, 总长度: {}", dataPackage.getAddr(), dataPackage.getCodeDescription(), dataPackage.getMid(), buffer.length()); - return buffer; - } catch (Exception e) { log.error("[encode][编码失败] 数据包: {}", dataPackage, e); throw new IllegalArgumentException("数据包编码失败: " + e.getMessage(), e); @@ -82,15 +73,14 @@ public class TcpDataEncoder { * @return 编码后的数据包 */ public static Buffer createRegisterReply(String addr, short mid, boolean success) { - String payload = success ? "0" : "1"; // 0表示成功,1表示失败 - + // TODO @haohao:payload 默认成功、失败,最好讴有个枚举 + String payload = success ? "0" : "1"; // 0 表示成功,1 表示失败 TcpDataPackage dataPackage = TcpDataPackage.builder() .addr(addr) .code(TcpDataPackage.CODE_REGISTER_REPLY) .mid(mid) .payload(payload) .build(); - return encode(dataPackage); } @@ -109,7 +99,6 @@ public class TcpDataEncoder { .mid(mid) .payload(data) .build(); - return encode(dataPackage); } @@ -128,7 +117,6 @@ public class TcpDataEncoder { .mid(mid) .payload(serviceData) .build(); - return encode(dataPackage); } @@ -147,7 +135,6 @@ public class TcpDataEncoder { .mid(mid) .payload(propertyData) .build(); - return encode(dataPackage); } @@ -166,7 +153,7 @@ public class TcpDataEncoder { .mid(mid) .payload(propertyNames) .build(); - return encode(dataPackage); } + } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataPackage.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataPackage.java index 3b6f7df286..c0a7e7185d 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataPackage.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataPackage.java @@ -9,7 +9,7 @@ import lombok.NoArgsConstructor; * TCP 数据包协议定义 *

* 数据包格式: - * 包头(4字节长度) | 设备地址长度(2字节) | 设备地址(不定长) | 功能码(2字节) | 消息序号(2字节) | 包体(不定长) + * 包头(4 字节长度) | 设备地址长度(2 字节) | 设备地址(不定长) | 功能码(2 字节) | 消息序号(2 字节) | 包体(不定长) * * @author 芋道源码 */ @@ -29,10 +29,12 @@ public class TcpDataPackage { * 注册回复 */ public static final short CODE_REGISTER_REPLY = 11; + // TODO @haohao:【重要】一般心跳,服务端会回复一条;回复要搞独立的 code 码,还是继续用原来的,因为 requestId 可以映射; /** * 心跳 */ public static final short CODE_HEARTBEAT = 20; + // TODO @haohao:【重要】下面的,是不是融合成消息上行(client -> server),消息下行(server -> client);然后把 method 放到 body 里? /** * 数据上报 */ @@ -60,6 +62,8 @@ public class TcpDataPackage { // ==================== 数据包字段 ==================== + // TODO @haohao:设备 addrLength、addr 是不是非必要呀? + /** * 设备地址长度 */ @@ -87,6 +91,8 @@ public class TcpDataPackage { // ==================== 辅助方法 ==================== + // TODO @haohao:用不到的方法,可以清理掉哈; + /** * 是否为注册消息 */ @@ -123,6 +129,7 @@ public class TcpDataPackage { code == CODE_PROPERTY_SET || code == CODE_PROPERTY_GET; } + // TODO @haohao:这个是不是去掉呀?多了一些维护成本; /** * 获取功能码描述 */ diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataReader.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataReader.java index f796389907..f366418d7e 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataReader.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataReader.java @@ -13,7 +13,7 @@ import java.util.function.Consumer; * 负责从 TCP 流中读取完整的数据包 *

* 数据包格式: - * 包头(4字节长度) | 设备地址长度(2字节) | 设备地址(不定长) | 功能码(2字节) | 消息序号(2字节) | 包体(不定长) + * 包头(4 字节长度) | 设备地址长度(2 字节) | 设备地址(不定长) | 功能码(2 字节) | 消息序号(2 字节) | 包体(不定长) * * @author 芋道源码 */ @@ -27,12 +27,12 @@ public class TcpDataReader { * @return RecordParser 解析器 */ public static RecordParser createParser(Consumer receiveHandler) { - // 首先读取4字节的长度信息 + // 首先读取 4 字节的长度信息 RecordParser parser = RecordParser.newFixed(4); // 设置处理器 parser.setOutput(new Handler() { - // 当前数据包的长度,-1表示还没有读取到长度信息 + // 当前数据包的长度,-1 表示还没有读取到长度信息 private int dataLength = -1; @Override @@ -43,8 +43,9 @@ public class TcpDataReader { // 从包头中读取数据长度 dataLength = buffer.getInt(0); - // 校验数据长度 - if (dataLength <= 0 || dataLength > 1024 * 1024) { // 最大1MB + // 校验数据长度(最大 1 MB) + // TODO @haohao:1m 蛮多地方在写死,最好配置管理下。或者有个全局的枚举; + if (dataLength <= 0 || dataLength > 1024 * 1024) { log.error("[handle][无效的数据包长度: {}]", dataLength); reset(); return; @@ -86,6 +87,8 @@ public class TcpDataReader { return parser; } + // TODO @haohao:用不到的方法,可以清理掉哈; + /** * 创建带异常处理的数据包解析器 * diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java index 7c499fb974..1fcb6a2bb5 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java @@ -55,6 +55,7 @@ public class IotTcpDownstreamHandler { } // 2. 根据消息方法处理不同类型的下行消息 + // TODO @芋艿、@haohao:看看有没什么办法,减少这样的编码。拓展新消息类型,成本高; switch (message.getMethod()) { case "thing.property.set": handlePropertySet(client, message); @@ -75,8 +76,8 @@ public class IotTcpDownstreamHandler { log.warn("[handle][未知的下行消息方法: {}]", message.getMethod()); break; } - } catch (Exception e) { + // TODO @haohao:最好消息的内容,打印下; log.error("[handle][处理下行消息失败]", e); } } @@ -104,7 +105,6 @@ public class IotTcpDownstreamHandler { log.debug("[handlePropertySet][属性设置消息已发送(降级)] 设备地址: {}, 消息序号: {}", client.getDeviceAddr(), mid); }); - } catch (Exception e) { log.error("[handlePropertySet][属性设置失败]", e); } @@ -133,7 +133,6 @@ public class IotTcpDownstreamHandler { log.debug("[handlePropertyGet][属性获取消息已发送(降级)] 设备地址: {}, 消息序号: {}", client.getDeviceAddr(), mid); }); - } catch (Exception e) { log.error("[handlePropertyGet][属性获取失败]", e); } @@ -162,7 +161,6 @@ public class IotTcpDownstreamHandler { log.debug("[handleServiceInvoke][服务调用消息已发送] 设备地址: {}, 消息序号: {}", client.getDeviceAddr(), mid); - } catch (Exception e) { log.error("[handleServiceInvoke][服务调用失败]", e); } @@ -191,7 +189,6 @@ public class IotTcpDownstreamHandler { log.debug("[handleConfigPush][配置推送消息已发送] 设备地址: {}, 消息序号: {}", client.getDeviceAddr(), mid); - } catch (Exception e) { log.error("[handleConfigPush][配置推送失败]", e); } @@ -262,6 +259,7 @@ public class IotTcpDownstreamHandler { } } + // TODO @haohao:用不到的,要不暂时不提供; /** * 批量发送下行消息 * @@ -287,7 +285,6 @@ public class IotTcpDownstreamHandler { // 处理单个设备消息 handle(copyMessage); } - } catch (Exception e) { log.error("[broadcastMessage][批量发送消息失败]", e); } @@ -341,7 +338,6 @@ public class IotTcpDownstreamHandler { log.debug("[{}][消息已发送] 设备地址: {}, 消息序号: {}", methodName, client.getDeviceAddr(), dataPackage.getMid()); - } catch (Exception e) { log.warn("[{}][使用编解码器编码失败,降级使用原始编码] 错误: {}", methodName, e.getMessage()); @@ -353,6 +349,7 @@ public class IotTcpDownstreamHandler { } } + // TODO @haohao:看看这个要不要删除掉 /** * 获取连接统计信息 * diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java index 0067e72064..672de2ad2c 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java @@ -39,6 +39,7 @@ public class IotTcpUpstreamHandler implements Handler { private final IotGatewayProperties.TcpProperties tcpConfig; + // TODO @haohao:可以把 TcpDeviceConnectionManager 能力放大一点:1)handle 里的 client 初始化,可以拿到 TcpDeviceConnectionManager 里;2)handleDeviceRegister 也是; private final TcpDeviceConnectionManager connectionManager; private final IotDeviceService deviceService; @@ -53,26 +54,25 @@ public class IotTcpUpstreamHandler implements Handler { public void handle(NetSocket socket) { log.info("[handle][收到设备连接: {}]", socket.remoteAddress()); - // 创建客户端ID和设备客户端 + // 创建客户端 ID 和设备客户端 + // TODO @haohao:clientid 给 TcpDeviceClient 生成会简洁一点;减少 upsteramhanlder 的非核心逻辑; String clientId = IdUtil.simpleUUID() + "_" + socket.remoteAddress(); TcpDeviceClient client = new TcpDeviceClient(clientId, tcpConfig.getKeepAliveTimeoutMs()); try { // 设置连接异常和关闭处理 socket.exceptionHandler(ex -> { + // TODO @haohao:这里的日志,可能把 clientid 都打上?因为 address 会重复么? log.error("[handle][连接({})异常]", socket.remoteAddress(), ex); handleConnectionClose(client); }); - socket.closeHandler(v -> { log.info("[handle][连接({})关闭]", socket.remoteAddress()); handleConnectionClose(client); }); - - // 设置网络连接 client.setSocket(socket); - // 创建数据解析器 + // 设置解析器 RecordParser parser = TcpDataReader.createParser(buffer -> { try { handleDataPackage(client, buffer); @@ -80,13 +80,12 @@ public class IotTcpUpstreamHandler implements Handler { log.error("[handle][处理数据包异常]", e); } }); - - // 设置解析器 client.setParser(parser); + // TODO @haohao:socket.remoteAddress()) 打印进去 log.info("[handle][设备连接处理器初始化完成: {}]", clientId); - } catch (Exception e) { + // TODO @haohao:socket.remoteAddress()) 打印进去 log.error("[handle][初始化连接处理器失败]", e); client.shutdown(); } @@ -102,12 +101,12 @@ public class IotTcpUpstreamHandler implements Handler { try { // 解码数据包 TcpDataPackage dataPackage = TcpDataDecoder.decode(buffer); - log.info("[handleDataPackage][接收数据包] 设备地址: {}, 功能码: {}, 消息序号: {}", dataPackage.getAddr(), dataPackage.getCodeDescription(), dataPackage.getMid()); // 根据功能码处理不同类型的消息 switch (dataPackage.getCode()) { + // TODO @haohao:【重要】code 要不要改成 opCode。这样和 data 里的 code 好区分; case TcpDataPackage.CODE_REGISTER: handleDeviceRegister(client, dataPackage); break; @@ -124,8 +123,8 @@ public class IotTcpUpstreamHandler implements Handler { log.warn("[handleDataPackage][未知功能码: {}]", dataPackage.getCode()); break; } - } catch (Exception e) { + // TODO @haohao:最好有 client 标识; log.error("[handleDataPackage][处理数据包失败]", e); } } @@ -140,7 +139,6 @@ public class IotTcpUpstreamHandler implements Handler { try { String deviceAddr = dataPackage.getAddr(); String productKey = dataPackage.getPayload(); - log.info("[handleDeviceRegister][设备注册] 设备地址: {}, 产品密钥: {}", deviceAddr, productKey); // 获取设备信息 @@ -152,6 +150,7 @@ public class IotTcpUpstreamHandler implements Handler { } // 更新客户端信息 + // TODO @haohao:一个 set 方法,统一处理掉会好点哈; client.setProductKey(productKey); client.setDeviceName(deviceAddr); client.setDeviceId(device.getId()); @@ -169,7 +168,6 @@ public class IotTcpUpstreamHandler implements Handler { sendRegisterReply(client, dataPackage, true); log.info("[handleDeviceRegister][设备注册成功] 设备地址: {}, 设备ID: {}", deviceAddr, device.getId()); - } catch (Exception e) { log.error("[handleDeviceRegister][设备注册失败]", e); sendRegisterReply(client, dataPackage, false); @@ -185,7 +183,6 @@ public class IotTcpUpstreamHandler implements Handler { private void handleHeartbeat(TcpDeviceClient client, TcpDataPackage dataPackage) { try { String deviceAddr = dataPackage.getAddr(); - log.debug("[handleHeartbeat][收到心跳] 设备地址: {}", deviceAddr); // 更新心跳时间 @@ -230,7 +227,6 @@ public class IotTcpUpstreamHandler implements Handler { // 3. 发送解码后的消息 messageService.sendDeviceMessage(message, client.getProductKey(), client.getDeviceName(), serverId); - } catch (Exception e) { log.warn("[handleDataUp][使用编解码器解码失败,降级使用原始解析] 错误: {}", e.getMessage()); @@ -242,7 +238,6 @@ public class IotTcpUpstreamHandler implements Handler { // 发送数据上报回复 sendDataUpReply(client, dataPackage); - } catch (Exception e) { log.error("[handleDataUp][处理数据上报失败]", e); } @@ -279,11 +274,11 @@ public class IotTcpUpstreamHandler implements Handler { // 3. 发送解码后的消息 messageService.sendDeviceMessage(message, client.getProductKey(), client.getDeviceName(), serverId); - } catch (Exception e) { log.warn("[handleEventUp][使用编解码器解码失败,降级使用原始解析] 错误: {}", e.getMessage()); // 降级处理:使用原始方式解析数据 + // TODO @芋艿:降级处理逻辑; JSONObject eventJson = JSONUtil.parseObj(payload); IotDeviceMessage message = IotDeviceMessage.requestOf("thing.event.post", eventJson); messageService.sendDeviceMessage(message, client.getProductKey(), client.getDeviceName(), serverId); @@ -291,7 +286,6 @@ public class IotTcpUpstreamHandler implements Handler { // 发送事件上报回复 sendEventUpReply(client, dataPackage); - } catch (Exception e) { log.error("[handleEventUp][处理事件上报失败]", e); } @@ -329,13 +323,13 @@ public class IotTcpUpstreamHandler implements Handler { .addr(dataPackage.getAddr()) .code(TcpDataPackage.CODE_DATA_UP) .mid(dataPackage.getMid()) - .payload("0") // 0表示成功 + .payload("0") // 0 表示成功 TODO @haohao:最好枚举到 TcpDataPackage 里? .build(); io.vertx.core.buffer.Buffer replyBuffer = TcpDataEncoder.encode(replyPackage); client.sendMessage(replyBuffer); - } catch (Exception e) { + // TODO @haohao:可以有个 client id log.error("[sendDataUpReply][发送数据上报回复失败]", e); } } @@ -352,12 +346,11 @@ public class IotTcpUpstreamHandler implements Handler { .addr(dataPackage.getAddr()) .code(TcpDataPackage.CODE_EVENT_UP) .mid(dataPackage.getMid()) - .payload("0") // 0表示成功 + .payload("0") // 0 表示成功 .build(); io.vertx.core.buffer.Buffer replyBuffer = TcpDataEncoder.encode(replyPackage); client.sendMessage(replyBuffer); - } catch (Exception e) { log.error("[sendEventUpReply][发送事件上报回复失败]", e); } @@ -385,7 +378,6 @@ public class IotTcpUpstreamHandler implements Handler { } log.info("[handleConnectionClose][处理连接关闭完成] 设备地址: {}", deviceAddr); - } catch (Exception e) { log.error("[handleConnectionClose][处理连接关闭失败]", e); } From 1978603bd96d9cacfa52c2ccf749dc29f090d2b0 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 19 Jul 2025 15:31:13 +0800 Subject: [PATCH 36/89] =?UTF-8?q?(=E3=80=83'=E2=96=BD'=E3=80=83)=20v2.6.1?= =?UTF-8?q?=20=E5=8F=91=E5=B8=83=EF=BC=9AVben5=20+=20antd=20=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=90=8E=E5=8F=B0=E6=94=AF=E6=8C=81=20BPM=20=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E6=B5=81=E3=80=81CRM=20=E5=AE=A2=E6=88=B7=E7=AE=A1?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 2 +- yudao-dependencies/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index b271f1d4b9..48a6f0589d 100644 --- a/pom.xml +++ b/pom.xml @@ -32,7 +32,7 @@ https://github.com/YunaiV/ruoyi-vue-pro - 2.6.0-SNAPSHOT + 2.6.1-SNAPSHOT 17 ${java.version} diff --git a/yudao-dependencies/pom.xml b/yudao-dependencies/pom.xml index bf6175cdd3..27ad06fe5d 100644 --- a/yudao-dependencies/pom.xml +++ b/yudao-dependencies/pom.xml @@ -14,7 +14,7 @@ https://github.com/YunaiV/ruoyi-vue-pro - 2.6.0-SNAPSHOT + 2.6.1-SNAPSHOT 1.6.0 3.4.5 From 4a111d2cb57f7ee626aaac29315b80a9db33f6c1 Mon Sep 17 00:00:00 2001 From: xingyu4j Date: Sat, 19 Jul 2025 16:56:23 +0800 Subject: [PATCH 37/89] =?UTF-8?q?fix:=20=E4=BF=AE=E6=AD=A3use-vxe-grid?= =?UTF-8?q?=E4=B8=AD=E7=9A=84=E4=BB=A3=E7=90=86=E9=85=8D=E7=BD=AE=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/codegen/vue3_vben5_antd/schema/views/index.vue.vm | 2 +- .../vue3_vben5_antd/schema/views/modules/list_sub_erp.vue.vm | 2 +- .../resources/codegen/vue3_vben5_ele/schema/views/index.vue.vm | 2 +- .../vue3_vben5_ele/schema/views/modules/list_sub_erp.vue.vm | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/schema/views/index.vue.vm b/yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/schema/views/index.vue.vm index fdac956ff7..635e12ac24 100644 --- a/yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/schema/views/index.vue.vm +++ b/yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/schema/views/index.vue.vm @@ -180,7 +180,7 @@ const [Grid, gridApi] = useVbenVxeGrid({ #end }, toolbarConfig: { - refresh: { code: 'query' }, + refresh: true, search: true, }, } as VxeTableGridOptions<${simpleClassName}Api.${simpleClassName}>, diff --git a/yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/schema/views/modules/list_sub_erp.vue.vm b/yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/schema/views/modules/list_sub_erp.vue.vm index 97404b0211..4001ed3992 100644 --- a/yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/schema/views/modules/list_sub_erp.vue.vm +++ b/yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/schema/views/modules/list_sub_erp.vue.vm @@ -131,7 +131,7 @@ const [Grid, gridApi] = useVbenVxeGrid({ enabled: true, }, toolbarConfig: { - refresh: { code: 'query' }, + refresh: true, search: true, }, #else diff --git a/yudao-module-infra/src/main/resources/codegen/vue3_vben5_ele/schema/views/index.vue.vm b/yudao-module-infra/src/main/resources/codegen/vue3_vben5_ele/schema/views/index.vue.vm index 3bb2b6a4a1..f9232d6b50 100644 --- a/yudao-module-infra/src/main/resources/codegen/vue3_vben5_ele/schema/views/index.vue.vm +++ b/yudao-module-infra/src/main/resources/codegen/vue3_vben5_ele/schema/views/index.vue.vm @@ -174,7 +174,7 @@ const [Grid, gridApi] = useVbenVxeGrid({ #end }, toolbarConfig: { - refresh: { code: 'query' }, + refresh: true, search: true, }, } as VxeTableGridOptions<${simpleClassName}Api.${simpleClassName}>, diff --git a/yudao-module-infra/src/main/resources/codegen/vue3_vben5_ele/schema/views/modules/list_sub_erp.vue.vm b/yudao-module-infra/src/main/resources/codegen/vue3_vben5_ele/schema/views/modules/list_sub_erp.vue.vm index 4dd5a6f1b4..5afb9c7a0d 100644 --- a/yudao-module-infra/src/main/resources/codegen/vue3_vben5_ele/schema/views/modules/list_sub_erp.vue.vm +++ b/yudao-module-infra/src/main/resources/codegen/vue3_vben5_ele/schema/views/modules/list_sub_erp.vue.vm @@ -125,7 +125,7 @@ const [Grid, gridApi] = useVbenVxeGrid({ enabled: true, }, toolbarConfig: { - refresh: { code: 'query' }, + refresh: true, search: true, }, #else From bfcc1987cb1e8675f7e8d540c55a5d5a5be09c64 Mon Sep 17 00:00:00 2001 From: xingyu4j Date: Sat, 19 Jul 2025 16:56:23 +0800 Subject: [PATCH 38/89] =?UTF-8?q?fix:=20=E3=80=90BPM=20=E5=B7=A5=E4=BD=9C?= =?UTF-8?q?=E6=B5=81=E3=80=91=E9=81=BF=E5=85=8D=E5=9C=A8=20BpmHttpRequestU?= =?UTF-8?q?tils=20=E4=B8=AD=E9=87=8D=E5=A4=8D=E6=B7=BB=E5=8A=A0=20processI?= =?UTF-8?q?nstanceId=20=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bpm/framework/flowable/core/util/BpmHttpRequestUtils.java | 4 +++- .../codegen/vue3_vben5_antd/schema/views/index.vue.vm | 2 +- .../vue3_vben5_antd/schema/views/modules/list_sub_erp.vue.vm | 2 +- .../codegen/vue3_vben5_ele/schema/views/index.vue.vm | 2 +- .../vue3_vben5_ele/schema/views/modules/list_sub_erp.vue.vm | 2 +- 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmHttpRequestUtils.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmHttpRequestUtils.java index 2503c0fff9..014b5e3f0d 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmHttpRequestUtils.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmHttpRequestUtils.java @@ -108,7 +108,9 @@ public class BpmHttpRequestUtils { Map processVariables = processInstance.getProcessVariables(); MultiValueMap body = new LinkedMultiValueMap<>(); addHttpRequestParam(body, bodySettings, processVariables); - body.add("processInstanceId", processInstance.getId()); + if (!body.containsKey("processInstanceId")) { // 避免重复添加 + body.add("processInstanceId", processInstance.getId()); + } return body; } diff --git a/yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/schema/views/index.vue.vm b/yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/schema/views/index.vue.vm index fdac956ff7..635e12ac24 100644 --- a/yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/schema/views/index.vue.vm +++ b/yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/schema/views/index.vue.vm @@ -180,7 +180,7 @@ const [Grid, gridApi] = useVbenVxeGrid({ #end }, toolbarConfig: { - refresh: { code: 'query' }, + refresh: true, search: true, }, } as VxeTableGridOptions<${simpleClassName}Api.${simpleClassName}>, diff --git a/yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/schema/views/modules/list_sub_erp.vue.vm b/yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/schema/views/modules/list_sub_erp.vue.vm index 97404b0211..4001ed3992 100644 --- a/yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/schema/views/modules/list_sub_erp.vue.vm +++ b/yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/schema/views/modules/list_sub_erp.vue.vm @@ -131,7 +131,7 @@ const [Grid, gridApi] = useVbenVxeGrid({ enabled: true, }, toolbarConfig: { - refresh: { code: 'query' }, + refresh: true, search: true, }, #else diff --git a/yudao-module-infra/src/main/resources/codegen/vue3_vben5_ele/schema/views/index.vue.vm b/yudao-module-infra/src/main/resources/codegen/vue3_vben5_ele/schema/views/index.vue.vm index 3bb2b6a4a1..f9232d6b50 100644 --- a/yudao-module-infra/src/main/resources/codegen/vue3_vben5_ele/schema/views/index.vue.vm +++ b/yudao-module-infra/src/main/resources/codegen/vue3_vben5_ele/schema/views/index.vue.vm @@ -174,7 +174,7 @@ const [Grid, gridApi] = useVbenVxeGrid({ #end }, toolbarConfig: { - refresh: { code: 'query' }, + refresh: true, search: true, }, } as VxeTableGridOptions<${simpleClassName}Api.${simpleClassName}>, diff --git a/yudao-module-infra/src/main/resources/codegen/vue3_vben5_ele/schema/views/modules/list_sub_erp.vue.vm b/yudao-module-infra/src/main/resources/codegen/vue3_vben5_ele/schema/views/modules/list_sub_erp.vue.vm index 4dd5a6f1b4..5afb9c7a0d 100644 --- a/yudao-module-infra/src/main/resources/codegen/vue3_vben5_ele/schema/views/modules/list_sub_erp.vue.vm +++ b/yudao-module-infra/src/main/resources/codegen/vue3_vben5_ele/schema/views/modules/list_sub_erp.vue.vm @@ -125,7 +125,7 @@ const [Grid, gridApi] = useVbenVxeGrid({ enabled: true, }, toolbarConfig: { - refresh: { code: 'query' }, + refresh: true, search: true, }, #else From dc7763ef0b126dc561b2bdbf2d9f60c2b671f16f Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 20 Jul 2025 16:45:16 +0800 Subject: [PATCH 39/89] =?UTF-8?q?fix=EF=BC=9A=E3=80=90BPM=20=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E6=B5=81=E3=80=91BpmTaskCandidateExpressionStrategy?= =?UTF-8?q?=20=E5=9C=A8=20PropertyNotFoundException=20=E4=B8=8D=E5=AD=98?= =?UTF-8?q?=E5=9C=A8=E6=97=B6=EF=BC=8C=E9=A2=91=E7=B9=81=E6=89=93=20warn?= =?UTF-8?q?=20=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../expression/BpmTaskAssignLeaderExpression.java | 1 + .../expression/BpmTaskAssignStartUserExpression.java | 1 + .../other/BpmTaskCandidateExpressionStrategy.java | 11 +++++++---- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/expression/BpmTaskAssignLeaderExpression.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/expression/BpmTaskAssignLeaderExpression.java index 7c1950f8ce..e9180c8695 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/expression/BpmTaskAssignLeaderExpression.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/expression/BpmTaskAssignLeaderExpression.java @@ -24,6 +24,7 @@ import static java.util.Collections.emptySet; * @author 芋道源码 */ @Component +@Deprecated // 仅仅是表达式的示例,建议使用 BpmTaskCandidateStartUserDeptLeaderStrategy 替代 public class BpmTaskAssignLeaderExpression { @Resource diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/expression/BpmTaskAssignStartUserExpression.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/expression/BpmTaskAssignStartUserExpression.java index ac243c0f43..f22dc508cb 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/expression/BpmTaskAssignStartUserExpression.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/expression/BpmTaskAssignStartUserExpression.java @@ -16,6 +16,7 @@ import java.util.Set; * @author 芋道源码 */ @Component +@Deprecated // 仅仅是表达式的示例,建议使用 BpmTaskCandidateStartUserStrategy 替代 public class BpmTaskAssignStartUserExpression { @Resource diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/other/BpmTaskCandidateExpressionStrategy.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/other/BpmTaskCandidateExpressionStrategy.java index 64ca9e8538..86c137a4b7 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/other/BpmTaskCandidateExpressionStrategy.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/other/BpmTaskCandidateExpressionStrategy.java @@ -8,6 +8,7 @@ import com.google.common.collect.Sets; import lombok.extern.slf4j.Slf4j; import org.flowable.bpmn.model.BpmnModel; import org.flowable.common.engine.api.FlowableException; +import org.flowable.common.engine.impl.javax.el.PropertyNotFoundException; import org.flowable.engine.delegate.DelegateExecution; import org.springframework.stereotype.Component; @@ -48,10 +49,12 @@ public class BpmTaskCandidateExpressionStrategy implements BpmTaskCandidateStrat Object result = FlowableUtils.getExpressionValue(variables, param); return CollectionUtils.toLinkedHashSet(Long.class, result); } catch (FlowableException ex) { - // 预测未运行的节点时候,表达式如果包含 execution 或者不存在的流程变量会抛异常, - log.warn("[calculateUsersByActivity][表达式({}) 变量({}) 解析报错", param, variables, ex); - // 不能预测候选人,返回空列表, 避免流程无法进行 - return Sets.newHashSet(); + // 预测未运行的节点时候,表达式如果包含 execution 或者不存在的流程变量会抛异常,此时忽略该异常!相当于说,不做流程预测!!! + if (ex.getCause() != null && ex.getCause() instanceof PropertyNotFoundException) { + return Sets.newHashSet(); + } + log.error("[calculateUsersByActivity][表达式({}) 变量({}) 解析报错", param, variables, ex); + throw ex; } } From c645813725420517bcb92c6fee82a09e90e7ebc0 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 20 Jul 2025 17:47:50 +0800 Subject: [PATCH 40/89] =?UTF-8?q?fix=EF=BC=9A=E3=80=90BPM=20=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E6=B5=81=E3=80=91=E5=BE=85=E5=8A=9E=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E3=80=81=E5=B7=B2=E5=8A=9E=E4=BB=BB=E5=8A=A1=EF=BC=8C=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E6=97=B6=E9=97=B4=E8=8C=83=E5=9B=B4=E6=9F=A5=E8=AF=A2?= =?UTF-8?q?=E6=97=B6=EF=BC=8C=E4=BC=9A=E6=8B=BC=E6=8E=A5=20OR=20=E7=9A=84?= =?UTF-8?q?=E6=83=85=E5=86=B5=EF=BC=88=E9=9D=9E=E5=AE=8C=E7=BE=8E=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E6=96=B9=E6=A1=88=EF=BC=89=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bpm/service/task/BpmTaskServiceImpl.java | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java index 1164f4da72..1f7e699032 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java @@ -230,10 +230,10 @@ public class BpmTaskServiceImpl implements BpmTaskService { if (StrUtil.isNotBlank(pageVO.getName())) { taskQuery.taskNameLike("%" + pageVO.getName() + "%"); } - if (ArrayUtil.isNotEmpty(pageVO.getCreateTime())) { - taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getCreateTime()[0])); - taskQuery.taskCreatedBefore(DateUtils.of(pageVO.getCreateTime()[1])); - } +// if (ArrayUtil.isNotEmpty(pageVO.getCreateTime())) { +// taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getCreateTime()[0])); +// taskQuery.taskCreatedBefore(DateUtils.of(pageVO.getCreateTime()[1])); +// } // 执行查询 long count = taskQuery.count(); if (count == 0) { @@ -244,6 +244,12 @@ public class BpmTaskServiceImpl implements BpmTaskService { // 特殊:强制移除自动完成的“发起人”节点 // 补充说明:由于 taskQuery 无法方面的过滤,所以暂时通过内存过滤 tasks.removeIf(task -> task.getTaskDefinitionKey().equals(START_USER_NODE_ID)); + // TODO @芋艿:https://t.zsxq.com/MNzqp 【flowable bug】:taskCreatedAfter、taskCreatedBefore 拼接的是 OR + if (ArrayUtil.isNotEmpty(pageVO.getCreateTime())) { + tasks.removeIf(task -> task.getCreateTime() == null + || task.getCreateTime().before(DateUtils.of(pageVO.getCreateTime()[0])) + || task.getCreateTime().after(DateUtils.of(pageVO.getCreateTime()[1]))); + } return new PageResult<>(tasks, count); } @@ -259,16 +265,22 @@ public class BpmTaskServiceImpl implements BpmTaskService { if (StrUtil.isNotEmpty(pageVO.getCategory())) { taskQuery.taskCategory(pageVO.getCategory()); } - if (ArrayUtil.isNotEmpty(pageVO.getCreateTime())) { - taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getCreateTime()[0])); - taskQuery.taskCreatedBefore(DateUtils.of(pageVO.getCreateTime()[1])); - } +// if (ArrayUtil.isNotEmpty(pageVO.getCreateTime())) { +// taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getCreateTime()[0])); +// taskQuery.taskCreatedBefore(DateUtils.of(pageVO.getCreateTime()[1])); +// } // 执行查询 long count = taskQuery.count(); if (count == 0) { return PageResult.empty(); } List tasks = taskQuery.listPage(PageUtils.getStart(pageVO), pageVO.getPageSize()); + // TODO @芋艿:https://t.zsxq.com/MNzqp 【flowable bug】:taskCreatedAfter、taskCreatedBefore 拼接的是 OR + if (ArrayUtil.isNotEmpty(pageVO.getCreateTime())) { + tasks.removeIf(task -> task.getCreateTime() == null + || task.getCreateTime().before(DateUtils.of(pageVO.getCreateTime()[0])) + || task.getCreateTime().after(DateUtils.of(pageVO.getCreateTime()[1]))); + } return new PageResult<>(tasks, count); } From 14d8c2bb404caa337565a80b242c736822f3278a Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 20 Jul 2025 20:02:48 +0800 Subject: [PATCH 41/89] =?UTF-8?q?fix=EF=BC=9A=E3=80=90BPM=20=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E6=B5=81=E3=80=91buildConditionExpression=20=E5=9C=A8?= =?UTF-8?q?=E6=B5=81=E7=A8=8B=E9=A2=84=E6=B5=8B=E6=97=B6=EF=BC=8C=E5=9B=A0?= =?UTF-8?q?=E4=B8=BA=E5=8F=98=E9=87=8F=E4=B8=8D=E5=AD=98=E5=9C=A8=EF=BC=8C?= =?UTF-8?q?=E5=AF=BC=E8=87=B4=20Unknow=20property=20=E5=BC=82=E5=B8=B8?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../flowable/core/util/SimpleModelUtils.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java index bd427e32f2..c9f3ff6fcb 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java @@ -4,7 +4,6 @@ import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.*; -import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO.ConditionGroups; @@ -17,13 +16,14 @@ import cn.iocoder.yudao.module.bpm.service.task.listener.BpmCallActivityListener import cn.iocoder.yudao.module.bpm.service.task.listener.BpmUserTaskListener; import org.flowable.bpmn.BpmnAutoLayout; import org.flowable.bpmn.constants.BpmnXMLConstants; -import org.flowable.bpmn.model.Process; import org.flowable.bpmn.model.*; +import org.flowable.bpmn.model.Process; import org.flowable.engine.delegate.ExecutionListener; import org.flowable.engine.delegate.TaskListener; import java.util.*; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.*; import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.*; import static java.util.Arrays.asList; @@ -684,15 +684,18 @@ public class SimpleModelUtils { if (conditionGroups == null || CollUtil.isEmpty(conditionGroups.getConditions())) { return null; } - List strConditionGroups = CollectionUtils.convertList(conditionGroups.getConditions(), item -> { + List strConditionGroups = convertList(conditionGroups.getConditions(), item -> { if (CollUtil.isEmpty(item.getRules())) { return ""; } // 构造规则表达式 - List list = CollectionUtils.convertList(item.getRules(), (rule) -> { + List list = convertList(item.getRules(), (rule) -> { String rightSide = NumberUtil.isNumber(rule.getRightSide()) ? rule.getRightSide() : "\"" + rule.getRightSide() + "\""; // 如果非数值类型加引号 - return String.format(" %s %s var:convertByType(%s,%s)", rule.getLeftSide(), rule.getOpCode(), rule.getLeftSide(), rightSide); + return String.format(" vars:getOrDefault(%s, null) %s var:convertByType(%s,%s) ", + rule.getLeftSide(), // 左侧:读取变量 + rule.getOpCode(), // 中间:操作符,比较 + rule.getLeftSide(), rightSide); // 右侧:转换变量,VariableConvertByTypeExpressionFunction }); // 构造条件组的表达式 Boolean and = item.getAnd(); From 51859afa9ac1ffb3b7b28af37c6b904ac54b3a62 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 20 Jul 2025 20:26:23 +0800 Subject: [PATCH 42/89] =?UTF-8?q?fix=EF=BC=9A=E3=80=90BPM=20=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E6=B5=81=E3=80=91SIMPLE=20=E5=BB=B6=E8=BF=9F=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E6=97=B6=EF=BC=8C=E7=A7=9F=E6=88=B7=E4=B8=8A=E4=B8=8B?= =?UTF-8?q?=E6=96=87=E4=B8=A2=E5=A4=B1=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../flowable/core/listener/BpmTaskEventListener.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTaskEventListener.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTaskEventListener.java index e50df0bcf0..9aa3889e59 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTaskEventListener.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTaskEventListener.java @@ -7,6 +7,7 @@ import cn.iocoder.yudao.framework.common.util.number.NumberUtils; import cn.iocoder.yudao.module.bpm.enums.definition.BpmBoundaryEventTypeEnum; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants; import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; import cn.iocoder.yudao.module.bpm.service.definition.BpmModelService; import cn.iocoder.yudao.module.bpm.service.task.BpmTaskService; import com.google.common.collect.ImmutableSet; @@ -58,17 +59,20 @@ public class BpmTaskEventListener extends AbstractFlowableEngineEventListener { @Override protected void taskCreated(FlowableEngineEntityEvent event) { - taskService.processTaskCreated((Task) event.getEntity()); + Task entity = (Task) event.getEntity(); + FlowableUtils.execute(entity.getTenantId(), () -> taskService.processTaskCreated(entity)); } @Override protected void taskAssigned(FlowableEngineEntityEvent event) { - taskService.processTaskAssigned((Task) event.getEntity()); + Task entity = (Task) event.getEntity(); + FlowableUtils.execute(entity.getTenantId(), () -> taskService.processTaskAssigned(entity)); } @Override protected void taskCompleted(FlowableEngineEntityEvent event) { - taskService.processTaskCompleted((Task) event.getEntity()); + Task entity = (Task) event.getEntity(); + FlowableUtils.execute(entity.getTenantId(), () -> taskService.processTaskCompleted(entity)); } @Override From d36c0dce7cf24514dd7484f7b9e1a05afd5e2ff8 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 20 Jul 2025 23:43:46 +0800 Subject: [PATCH 43/89] =?UTF-8?q?fix=EF=BC=9A=E3=80=90BPM=20=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E6=B5=81=E3=80=91BPMN=20=E5=9C=BA=E6=99=AF=E4=B8=8B?= =?UTF-8?q?=EF=BC=8C=E5=A4=9A=E6=AC=A1=E6=8F=90=E9=86=92=E5=8F=AF=E8=83=BD?= =?UTF-8?q?=E4=B8=A2=E5=A4=B1=20elementId=20=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/listener/BpmTaskEventListener.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTaskEventListener.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTaskEventListener.java index 9aa3889e59..6e2e48cbec 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTaskEventListener.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTaskEventListener.java @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.listener; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; import cn.iocoder.yudao.framework.common.util.number.NumberUtils; import cn.iocoder.yudao.module.bpm.enums.definition.BpmBoundaryEventTypeEnum; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants; @@ -98,6 +99,23 @@ public class BpmTaskEventListener extends AbstractFlowableEngineEventListener { String processDefinitionId = event.getProcessDefinitionId(); BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(processDefinitionId); Job entity = (Job) event.getEntity(); + // 特殊 from https://t.zsxq.com/h6oWr :当 elementId 为空时,尝试从 JobHandlerConfiguration 中解析 JSON 获取 + String elementId = entity.getElementId(); + if (elementId == null && entity.getJobHandlerConfiguration() != null) { + try { + String handlerConfig = entity.getJobHandlerConfiguration(); + if (handlerConfig.startsWith("{") && handlerConfig.contains("activityId")) { + elementId = new JSONObject(handlerConfig).getStr("activityId"); + } + } catch (Exception e) { + log.error("[timerFired][解析 entity({}) 失败]", entity, e); + return; + } + } + if (elementId == null) { + log.error("[timerFired][解析 entity({}) elementId 为空,跳过处理]", entity); + return; + } FlowElement element = BpmnModelUtils.getFlowElementById(bpmnModel, entity.getElementId()); if (!(element instanceof BoundaryEvent)) { return; From 3aac40a51e6dcdd449e4ea5d0418ca837de5a2da Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 21 Jul 2025 16:17:17 +0800 Subject: [PATCH 44/89] =?UTF-8?q?fix=EF=BC=9A=E3=80=90BPM=20=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E6=B5=81=E3=80=91=E6=B5=81=E7=A8=8B=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E5=88=86=E7=B1=BB=E5=88=A0=E9=99=A4=E6=97=B6=EF=BC=8C=E9=A2=9D?= =?UTF-8?q?=E5=A4=96=E6=A0=A1=E9=AA=8C=E6=98=AF=E5=90=A6=E8=A2=AB=E6=B5=81?= =?UTF-8?q?=E7=A8=8B=E6=A8=A1=E5=9E=8B=E6=89=80=E4=BD=BF=E7=94=A8=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/bpm/enums/ErrorCodeConstants.java | 1 + .../definition/BpmCategoryServiceImpl.java | 16 +++++++++++++--- .../bpm/service/definition/BpmModelService.java | 8 ++++++++ .../service/definition/BpmModelServiceImpl.java | 8 ++++++++ 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java index d5d6fa77c4..4f7d7bf7ce 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java @@ -74,6 +74,7 @@ public interface ErrorCodeConstants { ErrorCode CATEGORY_NOT_EXISTS = new ErrorCode(1_009_012_000, "流程分类不存在"); ErrorCode CATEGORY_NAME_DUPLICATE = new ErrorCode(1_009_012_001, "流程分类名字【{}】重复"); ErrorCode CATEGORY_CODE_DUPLICATE = new ErrorCode(1_009_012_002, "流程分类编码【{}】重复"); + ErrorCode CATEGORY_DELETE_FAIL_MODEL_USED = new ErrorCode(1_009_012_003, "删除失败,流程分类【{}】已被流程模型使用,请先删除对应的流程模型"); // ========== BPM 流程监听器 1-009-013-000 ========== ErrorCode PROCESS_LISTENER_NOT_EXISTS = new ErrorCode(1_009_013_000, "流程监听器不存在"); diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmCategoryServiceImpl.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmCategoryServiceImpl.java index 8a48da15a4..42d399acd5 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmCategoryServiceImpl.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmCategoryServiceImpl.java @@ -34,6 +34,9 @@ public class BpmCategoryServiceImpl implements BpmCategoryService { @Resource private BpmCategoryMapper bpmCategoryMapper; + @Resource + private BpmModelService modelService; + @Override public Long createCategory(BpmCategorySaveReqVO createReqVO) { // 校验唯一 @@ -77,15 +80,22 @@ public class BpmCategoryServiceImpl implements BpmCategoryService { @Override public void deleteCategory(Long id) { // 校验存在 - validateCategoryExists(id); + BpmCategoryDO category = validateCategoryExists(id); + // 校验是否被流程模型使用 + Long count = modelService.getModelCountByCategory(category.getCode()); + if (count > 0) { + throw exception(CATEGORY_DELETE_FAIL_MODEL_USED, category.getName()); + } // 删除 bpmCategoryMapper.deleteById(id); } - private void validateCategoryExists(Long id) { - if (bpmCategoryMapper.selectById(id) == null) { + private BpmCategoryDO validateCategoryExists(Long id) { + BpmCategoryDO category = bpmCategoryMapper.selectById(id); + if (category == null) { throw exception(CATEGORY_NOT_EXISTS); } + return category; } @Override diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelService.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelService.java index 5601bcda31..3273d24cbe 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelService.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelService.java @@ -24,6 +24,14 @@ public interface BpmModelService { */ List getModelList(String name); + /** + * 根据分类编码获得流程模型数量 + * + * @param category 分类编码 + * @return 流程模型数量 + */ + Long getModelCountByCategory(String category); + /** * 创建流程模型 * diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java index e8e90006f8..c23ea9ce8f 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java @@ -88,6 +88,14 @@ public class BpmModelServiceImpl implements BpmModelService { return modelQuery.list(); } + @Override + public Long getModelCountByCategory(String category) { + return repositoryService.createModelQuery() + .modelCategory(category) + .modelTenantId(FlowableUtils.getTenantId()) + .count(); + } + @Override @Transactional(rollbackFor = Exception.class) public String createModel(@Valid BpmModelSaveReqVO createReqVO) { From fb6771aad0425df1cd23245a6001929de046e23d Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 21 Jul 2025 19:45:30 +0800 Subject: [PATCH 45/89] =?UTF-8?q?fix=EF=BC=9A=E3=80=90MALL=20=E5=95=86?= =?UTF-8?q?=E5=9F=8E=E3=80=91=E7=A7=92=E6=9D=80=E6=97=B6=E9=97=B4=E6=AE=B5?= =?UTF-8?q?=E7=9A=84=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../promotion/service/seckill/SeckillActivityServiceImpl.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/seckill/SeckillActivityServiceImpl.java b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/seckill/SeckillActivityServiceImpl.java index dcc5596f25..3c84a172ab 100644 --- a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/seckill/SeckillActivityServiceImpl.java +++ b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/seckill/SeckillActivityServiceImpl.java @@ -303,7 +303,9 @@ public class SeckillActivityServiceImpl implements SeckillActivityService { throw exception(SECKILL_JOIN_ACTIVITY_TIME_ERROR); } SeckillConfigDO config = seckillConfigService.getCurrentSeckillConfig(); - if (config == null || !CollectionUtil.contains(activity.getConfigIds(), config.getId())) { + if (config == null + || !CollectionUtil.contains(activity.getConfigIds(), config.getId()) + || !LocalDateTimeUtils.isBetween(config.getStartTime(), config.getEndTime())) { throw exception(SECKILL_JOIN_ACTIVITY_TIME_ERROR); } // 1.3 超过单次购买限制 From 57add7b6e7939642ad3bd4240cbaf0c9af324ec6 Mon Sep 17 00:00:00 2001 From: dhb52 Date: Mon, 21 Jul 2025 16:05:50 +0000 Subject: [PATCH 46/89] =?UTF-8?q?feat:=20=20sql/tools=20=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=E8=BE=93=E5=87=BA=E5=88=B0=20stderr?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: dhb52 --- sql/tools/convertor.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sql/tools/convertor.py b/sql/tools/convertor.py index 7ab8ad1ef3..d286d3b238 100644 --- a/sql/tools/convertor.py +++ b/sql/tools/convertor.py @@ -17,6 +17,7 @@ uv run --with simple-ddl-parser convertor.py dm8 > ../dm/ruoyi-vue-pro-dm8.sql import argparse import pathlib import re +import sys import time from abc import ABC, abstractmethod from typing import Dict, Generator, Optional, Tuple, Union @@ -293,8 +294,10 @@ class Convertor(ABC): # 将parse失败的脚本打印出来 if error_scripts: + print("!!! 以下内容无法正常解析", file=sys.stderr) for script in error_scripts: - print(script) + # print to stderr + print(script, file=sys.stderr) class PostgreSQLConvertor(Convertor): From bd8052f56b5254fb073d4b396fcb8326897c79c0 Mon Sep 17 00:00:00 2001 From: haohao <1036606149@qq.com> Date: Tue, 22 Jul 2025 00:11:46 +0800 Subject: [PATCH 47/89] =?UTF-8?q?feat=EF=BC=9A=E3=80=90IoT=20=E7=89=A9?= =?UTF-8?q?=E8=81=94=E7=BD=91=E3=80=91=E6=96=B0=E5=A2=9E=20TCP=20=E4=BA=8C?= =?UTF-8?q?=E8=BF=9B=E5=88=B6=E5=92=8C=20JSON=20=E7=BC=96=E8=A7=A3?= =?UTF-8?q?=E7=A0=81=E5=99=A8=EF=BC=8C=E9=87=8D=E6=9E=84=20TCP=20=E5=8D=8F?= =?UTF-8?q?=E8=AE=AE=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tcp/IotTcpBinaryDeviceMessageCodec.java | 378 +++++++++++++ .../gateway/codec/tcp/IotTcpCodecManager.java | 136 +++++ .../codec/tcp/IotTcpDeviceMessageCodec.java | 389 -------------- .../tcp/IotTcpJsonDeviceMessageCodec.java | 245 +++++++++ .../config/IotGatewayConfiguration.java | 16 +- .../tcp/IotTcpDownstreamSubscriber.java | 140 +---- .../protocol/tcp/IotTcpUpstreamProtocol.java | 129 +---- .../protocol/tcp/client/TcpDeviceClient.java | 220 -------- .../manager/TcpDeviceConnectionManager.java | 506 ------------------ .../protocol/tcp/protocol/TcpDataDecoder.java | 98 ---- .../protocol/tcp/protocol/TcpDataEncoder.java | 159 ------ .../protocol/tcp/protocol/TcpDataPackage.java | 160 ------ .../protocol/tcp/protocol/TcpDataReader.java | 162 ------ .../tcp/router/IotTcpDownstreamHandler.java | 336 +----------- .../tcp/router/IotTcpUpstreamHandler.java | 389 ++------------ .../tcp/TcpBinaryDataPacketExamples.java | 219 ++++++++ .../codec/tcp/TcpJsonDataPacketExamples.java | 253 +++++++++ .../resources/tcp-binary-packet-examples.md | 222 ++++++++ .../resources/tcp-json-packet-examples.md | 286 ++++++++++ 19 files changed, 1868 insertions(+), 2575 deletions(-) create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpCodecManager.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpDeviceMessageCodec.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/client/TcpDeviceClient.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/TcpDeviceConnectionManager.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataDecoder.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataEncoder.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataPackage.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataReader.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpBinaryDataPacketExamples.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpJsonDataPacketExamples.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-binary-packet-examples.md create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-json-packet-examples.md diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java new file mode 100644 index 0000000000..40c8fcede4 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java @@ -0,0 +1,378 @@ +package cn.iocoder.yudao.module.iot.gateway.codec.tcp; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; +import io.vertx.core.buffer.Buffer; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * TCP 二进制格式 {@link IotDeviceMessage} 编解码器 + * + * 使用自定义二进制协议格式: + * 包头(4字节) | 地址长度(2字节) | 设备地址(变长) | 功能码(2字节) | 消息序号(2字节) | 包体数据(变长) + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { + + /** + * 编解码器类型 + */ + public static final String TYPE = "TCP_BINARY"; + + // ==================== 常量定义 ==================== + + @Override + public byte[] encode(IotDeviceMessage message) { + if (message == null || StrUtil.isEmpty(message.getMethod())) { + throw new IllegalArgumentException("消息或方法不能为空"); + } + + try { + // 1. 确定功能码(只支持数据上报和心跳) + short code = MessageMethod.STATE_ONLINE.equals(message.getMethod()) ? + TcpDataPackage.CODE_HEARTBEAT : TcpDataPackage.CODE_MESSAGE_UP; + + // 2. 构建简化负载 + String payload = buildSimplePayload(message); + + // 3. 构建 TCP 数据包 + String deviceAddr = message.getDeviceId() != null ? String.valueOf(message.getDeviceId()) : "default"; + short mid = (short) (System.currentTimeMillis() % Short.MAX_VALUE); + TcpDataPackage dataPackage = new TcpDataPackage(deviceAddr, code, mid, payload); + + // 4. 编码为字节流 + return encodeTcpDataPackage(dataPackage).getBytes(); + } catch (Exception e) { + log.error("[encode][编码失败] 方法: {}", message.getMethod(), e); + throw new TcpCodecException("TCP 消息编码失败", e); + } + } + + @Override + public IotDeviceMessage decode(byte[] bytes) { + if (bytes == null || bytes.length == 0) { + throw new IllegalArgumentException("待解码数据不能为空"); + } + + try { + // 1. 解码 TCP 数据包 + TcpDataPackage dataPackage = decodeTcpDataPackage(Buffer.buffer(bytes)); + + // 2. 根据功能码确定方法 + String method = (dataPackage.getCode() == TcpDataPackage.CODE_HEARTBEAT) ? + MessageMethod.STATE_ONLINE : MessageMethod.PROPERTY_POST; + + // 3. 解析负载数据和请求ID + PayloadInfo payloadInfo = parsePayloadInfo(dataPackage.getPayload()); + + // 4. 构建 IoT 设备消息(设置完整的必要参数) + IotDeviceMessage message = IotDeviceMessage.requestOf( + payloadInfo.getRequestId(), method, payloadInfo.getParams()); + + // 5. 设置设备相关信息 + Long deviceId = parseDeviceId(dataPackage.getAddr()); + message.setDeviceId(deviceId); + + // 6. 设置TCP协议相关信息 + message.setServerId(generateServerId(dataPackage)); + + // 7. 设置租户ID(TODO: 后续可以从设备信息中获取) + // message.setTenantId(getTenantIdByDeviceId(deviceId)); + + if (log.isDebugEnabled()) { + log.debug("[decode][解码成功] 设备ID: {}, 方法: {}, 请求ID: {}, 消息ID: {}", + deviceId, method, message.getRequestId(), message.getId()); + } + + return message; + } catch (Exception e) { + log.error("[decode][解码失败] 数据长度: {}", bytes.length, e); + throw new TcpCodecException("TCP 消息解码失败", e); + } + } + + @Override + public String type() { + return TYPE; + } + + /** + * 构建完整负载 + */ + private String buildSimplePayload(IotDeviceMessage message) { + JSONObject payload = new JSONObject(); + + // 核心字段 + payload.set(PayloadField.METHOD, message.getMethod()); + if (message.getParams() != null) { + payload.set(PayloadField.PARAMS, message.getParams()); + } + + // 标识字段 + if (StrUtil.isNotEmpty(message.getRequestId())) { + payload.set(PayloadField.REQUEST_ID, message.getRequestId()); + } + if (StrUtil.isNotEmpty(message.getId())) { + payload.set(PayloadField.MESSAGE_ID, message.getId()); + } + + // 时间戳 + payload.set(PayloadField.TIMESTAMP, System.currentTimeMillis()); + + return payload.toString(); + } + + + + // ==================== 编解码方法 ==================== + + /** + * 解析负载信息(包含requestId和params) + */ + private PayloadInfo parsePayloadInfo(String payload) { + if (StrUtil.isEmpty(payload)) { + return new PayloadInfo(null, null); + } + + try { + JSONObject jsonObject = JSONUtil.parseObj(payload); + String requestId = jsonObject.getStr(PayloadField.REQUEST_ID); + if (StrUtil.isEmpty(requestId)) { + requestId = jsonObject.getStr(PayloadField.MESSAGE_ID); + } + Object params = jsonObject.get(PayloadField.PARAMS); + return new PayloadInfo(requestId, params); + } catch (Exception e) { + log.warn("[parsePayloadInfo][解析失败,返回原始字符串] 负载: {}", payload); + return new PayloadInfo(null, payload); + } + } + + /** + * 从设备地址解析设备ID + * + * @param deviceAddr 设备地址字符串 + * @return 设备ID + */ + private Long parseDeviceId(String deviceAddr) { + if (StrUtil.isEmpty(deviceAddr)) { + log.warn("[parseDeviceId][设备地址为空,返回默认ID]"); + return 0L; + } + + try { + // 尝试直接解析为Long + return Long.parseLong(deviceAddr); + } catch (NumberFormatException e) { + // 如果不是纯数字,可以使用哈希值或其他策略 + log.warn("[parseDeviceId][设备地址不是数字格式: {},使用哈希值]", deviceAddr); + return (long) deviceAddr.hashCode(); + } + } + + /** + * 生成服务ID + * + * @param dataPackage TCP数据包 + * @return 服务ID + */ + private String generateServerId(TcpDataPackage dataPackage) { + // 使用协议类型 + 设备地址 + 消息序号生成唯一的服务ID + return String.format("tcp_%s_%d", dataPackage.getAddr(), dataPackage.getMid()); + } + + // ==================== 内部辅助方法 ==================== + + /** + * 编码 TCP 数据包 + * + * @param dataPackage 数据包对象 + * @return 编码后的字节流 + * @throws IllegalArgumentException 如果数据包对象不正确 + */ + private Buffer encodeTcpDataPackage(TcpDataPackage dataPackage) { + if (dataPackage == null) { + throw new IllegalArgumentException("数据包对象不能为空"); + } + + // 验证数据包 + if (dataPackage.getAddr() == null || dataPackage.getAddr().isEmpty()) { + throw new IllegalArgumentException("设备地址不能为空"); + } + if (dataPackage.getPayload() == null) { + throw new IllegalArgumentException("负载不能为空"); + } + + try { + Buffer buffer = Buffer.buffer(); + + // 1. 计算包体长度(除了包头 4 字节) + int payloadLength = dataPackage.getPayload().getBytes().length; + int totalLength = 2 + dataPackage.getAddr().length() + 2 + 2 + payloadLength; + + // 2.1 写入包头:总长度(4 字节) + buffer.appendInt(totalLength); + // 2.2 写入设备地址长度(2 字节) + buffer.appendShort((short) dataPackage.getAddr().length()); + // 2.3 写入设备地址(不定长) + buffer.appendBytes(dataPackage.getAddr().getBytes()); + // 2.4 写入功能码(2 字节) + buffer.appendShort(dataPackage.getCode()); + // 2.5 写入消息序号(2 字节) + buffer.appendShort(dataPackage.getMid()); + // 2.6 写入包体数据(不定长) + buffer.appendBytes(dataPackage.getPayload().getBytes()); + + if (log.isDebugEnabled()) { + log.debug("[encodeTcpDataPackage][编码成功] 设备地址: {}, 功能码: {}, 消息序号: {}, 总长度: {}", + dataPackage.getAddr(), dataPackage.getCode(), dataPackage.getMid(), buffer.length()); + } + return buffer; + } catch (Exception e) { + log.error("[encodeTcpDataPackage][编码失败] 数据包: {}", dataPackage, e); + throw new IllegalArgumentException("数据包编码失败: " + e.getMessage(), e); + } + } + + /** + * 解码 TCP 数据包 + * + * @param buffer 数据缓冲区 + * @return 解码后的数据包 + * @throws IllegalArgumentException 如果数据包格式不正确 + */ + private TcpDataPackage decodeTcpDataPackage(Buffer buffer) { + if (buffer == null || buffer.length() < 8) { + throw new IllegalArgumentException("数据包长度不足"); + } + + try { + int index = 0; + + // 1.1 跳过包头(4字节) + index += 4; + + // 1.2 获取设备地址长度(2字节) + short addrLength = buffer.getShort(index); + index += 2; + + // 1.3 获取设备地址 + String addr = buffer.getBuffer(index, index + addrLength).toString(); + index += addrLength; + + // 1.4 获取功能码(2字节) + short code = buffer.getShort(index); + index += 2; + + // 1.5 获取消息序号(2字节) + short mid = buffer.getShort(index); + index += 2; + + // 1.6 获取包体数据 + String payload = ""; + if (index < buffer.length()) { + payload = buffer.getString(index, buffer.length()); + } + + // 2. 构建数据包对象 + TcpDataPackage dataPackage = new TcpDataPackage(addr, code, mid, payload); + + if (log.isDebugEnabled()) { + log.debug("[decodeTcpDataPackage][解码成功] 设备地址: {}, 功能码: {}, 消息序号: {}, 包体长度: {}", + addr, code, mid, payload.length()); + } + return dataPackage; + } catch (Exception e) { + log.error("[decodeTcpDataPackage][解码失败] 数据长度: {}", buffer.length(), e); + throw new IllegalArgumentException("数据包解码失败: " + e.getMessage(), e); + } + } + + /** + * 消息方法常量 + */ + public static class MessageMethod { + public static final String PROPERTY_POST = "thing.property.post"; // 数据上报 + public static final String STATE_ONLINE = "thing.state.online"; // 心跳 + } + + /** + * 负载字段名 + */ + private static class PayloadField { + public static final String METHOD = "method"; + public static final String PARAMS = "params"; + public static final String TIMESTAMP = "timestamp"; + public static final String REQUEST_ID = "requestId"; + public static final String MESSAGE_ID = "msgId"; + } + + // ==================== TCP 数据包编解码方法 ==================== + + /** + * 负载信息类 + */ + private static class PayloadInfo { + private String requestId; + private Object params; + + public PayloadInfo(String requestId, Object params) { + this.requestId = requestId; + this.params = params; + } + + public String getRequestId() { return requestId; } + public Object getParams() { return params; } + } + + /** + * TCP 数据包内部类 + */ + @Data + private static class TcpDataPackage { + // 功能码定义 + public static final short CODE_REGISTER = 10; + public static final short CODE_REGISTER_REPLY = 11; + public static final short CODE_HEARTBEAT = 20; + public static final short CODE_HEARTBEAT_REPLY = 21; + public static final short CODE_MESSAGE_UP = 30; + public static final short CODE_MESSAGE_DOWN = 40; + + private String addr; + private short code; + private short mid; + private String payload; + + public TcpDataPackage(String addr, short code, short mid, String payload) { + this.addr = addr; + this.code = code; + this.mid = mid; + this.payload = payload; + } + } + + // ==================== 自定义异常 ==================== + + /** + * TCP 编解码异常 + */ + public static class TcpCodecException extends RuntimeException { + + public TcpCodecException(String message) { + super(message); + } + + public TcpCodecException(String message, Throwable cause) { + super(message, cause); + } + + } +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpCodecManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpCodecManager.java new file mode 100644 index 0000000000..aa789c689a --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpCodecManager.java @@ -0,0 +1,136 @@ +package cn.iocoder.yudao.module.iot.gateway.codec.tcp; + +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * TCP编解码器管理器(简化版) + * + * 核心功能: + * - 自动协议检测(二进制 vs JSON) + * - 统一编解码接口 + * - 默认使用JSON协议 + * + * @author 芋道源码 + */ +@Slf4j +@Component +public class IotTcpCodecManager implements IotDeviceMessageCodec { + + public static final String TYPE = "TCP"; + + @Autowired + private IotTcpBinaryDeviceMessageCodec binaryCodec; + + @Autowired + private IotTcpJsonDeviceMessageCodec jsonCodec; + + /** + * 当前默认协议(JSON) + */ + private boolean useJsonByDefault = true; + + @Override + public String type() { + return TYPE; + } + + @Override + public byte[] encode(IotDeviceMessage message) { + // 默认使用JSON协议编码 + return jsonCodec.encode(message); + } + + @Override + public IotDeviceMessage decode(byte[] bytes) { + // 自动检测协议类型并解码 + if (isJsonFormat(bytes)) { + if (log.isDebugEnabled()) { + log.debug("[decode][检测到JSON协议] 数据长度: {}字节", bytes.length); + } + return jsonCodec.decode(bytes); + } else { + if (log.isDebugEnabled()) { + log.debug("[decode][检测到二进制协议] 数据长度: {}字节", bytes.length); + } + return binaryCodec.decode(bytes); + } + } + + // ==================== 便捷方法 ==================== + + /** + * 使用JSON协议编码 + */ + public byte[] encodeJson(IotDeviceMessage message) { + return jsonCodec.encode(message); + } + + /** + * 使用二进制协议编码 + */ + public byte[] encodeBinary(IotDeviceMessage message) { + return binaryCodec.encode(message); + } + + /** + * 获取当前默认协议 + */ + public String getDefaultProtocol() { + return useJsonByDefault ? "JSON" : "BINARY"; + } + + /** + * 设置默认协议 + */ + public void setDefaultProtocol(boolean useJson) { + this.useJsonByDefault = useJson; + log.info("[setDefaultProtocol][设置默认协议] 使用JSON: {}", useJson); + } + + // ==================== 内部方法 ==================== + + /** + * 检测是否为JSON格式 + * + * 检测规则: + * 1. 数据以 '{' 开头 + * 2. 包含 "method" 或 "id" 字段 + */ + private boolean isJsonFormat(byte[] bytes) { + if (bytes == null || bytes.length == 0) { + return useJsonByDefault; + } + + try { + // 检测JSON格式:以 '{' 开头 + if (bytes[0] == '{') { + // 进一步验证是否为有效JSON + String jsonStr = new String(bytes, 0, Math.min(bytes.length, 100)); + return jsonStr.contains("\"method\"") || jsonStr.contains("\"id\""); + } + + // 检测二进制格式:长度 >= 8 且符合二进制协议结构 + if (bytes.length >= 8) { + // 读取包头(前4字节表示后续数据长度) + int expectedLength = ((bytes[0] & 0xFF) << 24) | + ((bytes[1] & 0xFF) << 16) | + ((bytes[2] & 0xFF) << 8) | + (bytes[3] & 0xFF); + + // 验证长度是否合理 + if (expectedLength == bytes.length - 4 && expectedLength > 0 && expectedLength < 1024 * 1024) { + return false; // 二进制格式 + } + } + } catch (Exception e) { + log.warn("[isJsonFormat][协议检测异常] 使用默认协议: {}", getDefaultProtocol(), e); + } + + // 默认使用当前设置的协议类型 + return useJsonByDefault; + } +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpDeviceMessageCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpDeviceMessageCodec.java deleted file mode 100644 index 6a558b5141..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpDeviceMessageCodec.java +++ /dev/null @@ -1,389 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.codec.tcp; - -import cn.hutool.core.util.ObjectUtil; -import cn.hutool.core.util.StrUtil; -import cn.hutool.json.JSONException; -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataDecoder; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataEncoder; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataPackage; -import io.vertx.core.buffer.Buffer; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; - -import java.nio.charset.StandardCharsets; -import java.util.Map; -import java.util.Objects; -import java.util.concurrent.ConcurrentHashMap; - -/** - * TCP {@link IotDeviceMessage} 编解码器 - *

- * 参考 EMQX 设计理念: - * 1. 高性能编解码 - * 2. 容错机制 - * 3. 缓存优化 - * 4. 监控统计 - * 5. 资源管理 - * - * @author 芋道源码 - */ -@Component -@Slf4j -public class IotTcpDeviceMessageCodec implements IotDeviceMessageCodec { - - /** - * 编解码器类型 - */ - public static final String TYPE = "tcp"; - - // ==================== 方法映射 ==================== - - /** - * 消息方法到功能码的映射 - */ - private static final Map METHOD_TO_CODE_MAP = new ConcurrentHashMap<>(); - - /** - * 功能码到消息方法的映射 - */ - private static final Map CODE_TO_METHOD_MAP = new ConcurrentHashMap<>(); - - static { - // 初始化方法映射 - // TODO @haohao:有没可能去掉这个 code 到 method 的映射哈? - initializeMethodMappings(); - } - - // ==================== 缓存管理 ==================== - - /** - * JSON 缓存,提升编解码性能 - */ - private final Map jsonCache = new ConcurrentHashMap<>(); - - /** - * 缓存最大大小 - */ - private static final int MAX_CACHE_SIZE = 1000; - - // ==================== 常量定义 ==================== - - /** - * 负载字段名 - */ - public static class PayloadField { - - public static final String TIMESTAMP = "timestamp"; - public static final String MESSAGE_ID = "msgId"; - public static final String DEVICE_ID = "deviceId"; - public static final String PARAMS = "params"; - public static final String DATA = "data"; - public static final String CODE = "code"; - public static final String MESSAGE = "message"; - - } - - /** - * 消息方法映射 - */ - public static class MessageMethod { - - public static final String PROPERTY_POST = "thing.property.post"; - public static final String PROPERTY_SET = "thing.property.set"; - public static final String PROPERTY_GET = "thing.property.get"; - public static final String EVENT_POST = "thing.event.post"; - public static final String SERVICE_INVOKE = "thing.service.invoke"; - public static final String CONFIG_PUSH = "thing.config.push"; - public static final String OTA_UPGRADE = "thing.ota.upgrade"; - public static final String STATE_ONLINE = "thing.state.online"; - public static final String STATE_OFFLINE = "thing.state.offline"; - - } - - // ==================== 初始化方法 ==================== - - /** - * 初始化方法映射 - */ - private static void initializeMethodMappings() { - METHOD_TO_CODE_MAP.put(MessageMethod.PROPERTY_POST, TcpDataPackage.CODE_DATA_UP); - METHOD_TO_CODE_MAP.put(MessageMethod.PROPERTY_SET, TcpDataPackage.CODE_PROPERTY_SET); - METHOD_TO_CODE_MAP.put(MessageMethod.PROPERTY_GET, TcpDataPackage.CODE_PROPERTY_GET); - METHOD_TO_CODE_MAP.put(MessageMethod.EVENT_POST, TcpDataPackage.CODE_EVENT_UP); - METHOD_TO_CODE_MAP.put(MessageMethod.SERVICE_INVOKE, TcpDataPackage.CODE_SERVICE_INVOKE); - METHOD_TO_CODE_MAP.put(MessageMethod.CONFIG_PUSH, TcpDataPackage.CODE_DATA_DOWN); - METHOD_TO_CODE_MAP.put(MessageMethod.OTA_UPGRADE, TcpDataPackage.CODE_DATA_DOWN); - METHOD_TO_CODE_MAP.put(MessageMethod.STATE_ONLINE, TcpDataPackage.CODE_HEARTBEAT); - METHOD_TO_CODE_MAP.put(MessageMethod.STATE_OFFLINE, TcpDataPackage.CODE_HEARTBEAT); - - // 反向映射 - METHOD_TO_CODE_MAP.forEach((method, code) -> CODE_TO_METHOD_MAP.put(code, method)); - } - - // ==================== 编解码方法 ==================== - - @Override - public byte[] encode(IotDeviceMessage message) { - validateEncodeParams(message); - - try { - if (log.isDebugEnabled()) { - log.debug("[encode][开始编码 TCP 消息] 方法: {}, 消息ID: {}", - message.getMethod(), message.getRequestId()); - } - - // 1. 获取功能码 - short code = getCodeByMethodSafely(message.getMethod()); - - // 2. 构建负载 - String payload = buildPayloadOptimized(message); - - // 3. 构建 TCP 数据包 - TcpDataPackage dataPackage = TcpDataPackage.builder() - .addr("") - .code(code) - .mid((short) 0) - .payload(payload) - .build(); - - // 4. 编码为字节流 - Buffer buffer = TcpDataEncoder.encode(dataPackage); - byte[] result = buffer.getBytes(); - - // 5. 统计信息 - if (log.isDebugEnabled()) { - log.debug("[encode][TCP 消息编码成功] 方法: {}, 数据长度: {}", - message.getMethod(), result.length); - } - return result; - } catch (Exception e) { - log.error("[encode][TCP 消息编码失败] 消息: {}", message, e); - throw new TcpCodecException("TCP 消息编码失败", e); - } - } - - @Override - public IotDeviceMessage decode(byte[] bytes) { - validateDecodeParams(bytes); - - try { - if (log.isDebugEnabled()) { - log.debug("[decode][开始解码 TCP 消息] 数据长度: {}", bytes.length); - } - - // 1. 解码 TCP 数据包 - Buffer buffer = Buffer.buffer(bytes); - TcpDataPackage dataPackage = TcpDataDecoder.decode(buffer); - // 2. 获取消息方法 - String method = getMethodByCodeSafely(dataPackage.getCode()); - // 3. 解析负载数据 - Object params = parsePayloadOptimized(dataPackage.getPayload()); - // 4. 构建 IoT 设备消息 - IotDeviceMessage message = IotDeviceMessage.requestOf(method, params); - - // 5. 统计信息 - if (log.isDebugEnabled()) { - log.debug("[decode][TCP 消息解码成功] 方法: {}, 功能码: {}", - method, dataPackage.getCode()); - } - return message; - } catch (Exception e) { - log.error("[decode][TCP 消息解码失败] 数据长度: {}, 数据内容: {}", - bytes.length, truncateData(bytes, 100), e); - throw new TcpCodecException("TCP 消息解码失败", e); - } - } - - @Override - public String type() { - return TYPE; - } - - // ==================== 内部辅助方法 ==================== - - /** - * 验证编码参数 - */ - private void validateEncodeParams(IotDeviceMessage message) { - if (Objects.isNull(message)) { - throw new IllegalArgumentException("IoT 设备消息不能为空"); - } - if (StrUtil.isEmpty(message.getMethod())) { - throw new IllegalArgumentException("消息方法不能为空"); - } - } - - /** - * 验证解码参数 - */ - private void validateDecodeParams(byte[] bytes) { - if (Objects.isNull(bytes) || bytes.length == 0) { - throw new IllegalArgumentException("待解码数据不能为空"); - } - if (bytes.length > 1024 * 1024) { - throw new IllegalArgumentException("数据包过大,超过 1MB 限制"); - } - } - - /** - * 安全获取功能码 - */ - private short getCodeByMethodSafely(String method) { - Short code = METHOD_TO_CODE_MAP.get(method); - // 默认为数据上报 - if (code == null) { - log.warn("[getCodeByMethodSafely][未知的消息方法: {},使用默认功能码]", method); - return TcpDataPackage.CODE_DATA_UP; - } - return code; - } - - /** - * 安全获取消息方法 - */ - private String getMethodByCodeSafely(short code) { - String method = CODE_TO_METHOD_MAP.get(code); - if (method == null) { - log.warn("[getMethodByCodeSafely][未知的功能码: {},使用默认方法]", code); - return MessageMethod.PROPERTY_POST; // 默认为属性上报 - } - return method; - } - - /** - * 优化的负载构建 - */ - private String buildPayloadOptimized(IotDeviceMessage message) { - // 使用缓存键 - // TODO @haohao:是不是不用缓存哈? - String cacheKey = message.getMethod() + "_" + message.getRequestId(); - JSONObject cachedPayload = jsonCache.get(cacheKey); - - if (cachedPayload != null) { - // 更新时间戳 - cachedPayload.set(PayloadField.TIMESTAMP, System.currentTimeMillis()); - return cachedPayload.toString(); - } - - // 创建新的负载 - JSONObject payload = new JSONObject(); - // 添加基础字段 - addToPayloadIfNotNull(payload, PayloadField.MESSAGE_ID, message.getRequestId()); - addToPayloadIfNotNull(payload, PayloadField.DEVICE_ID, message.getDeviceId()); - addToPayloadIfNotNull(payload, PayloadField.PARAMS, message.getParams()); - addToPayloadIfNotNull(payload, PayloadField.DATA, message.getData()); - addToPayloadIfNotNull(payload, PayloadField.CODE, message.getCode()); - addToPayloadIfNotEmpty(payload, PayloadField.MESSAGE, message.getMsg()); - // 添加时间戳 - payload.set(PayloadField.TIMESTAMP, System.currentTimeMillis()); - - // 缓存管理 - if (jsonCache.size() < MAX_CACHE_SIZE) { - jsonCache.put(cacheKey, payload); - } else { - cleanJsonCacheIfNeeded(); - } - - return payload.toString(); - } - - /** - * 优化的负载解析 - */ - private Object parsePayloadOptimized(String payload) { - if (StrUtil.isEmpty(payload)) { - return null; - } - - try { - // 尝试从缓存获取 - JSONObject cachedJson = jsonCache.get(payload); - if (cachedJson != null) { - return cachedJson.containsKey(PayloadField.PARAMS) ? cachedJson.get(PayloadField.PARAMS) : cachedJson; - } - - // 解析 JSON 对象 - JSONObject jsonObject = JSONUtil.parseObj(payload); - - // 缓存解析结果 - if (jsonCache.size() < MAX_CACHE_SIZE) { - jsonCache.put(payload, jsonObject); - } - - return jsonObject.containsKey(PayloadField.PARAMS) ? jsonObject.get(PayloadField.PARAMS) : jsonObject; - } catch (JSONException e) { - log.warn("[parsePayloadOptimized][负载解析为JSON失败,返回原始字符串] 负载: {}", payload); - return payload; - } catch (Exception e) { - log.error("[parsePayloadOptimized][负载解析异常] 负载: {}", payload, e); - return payload; - } - } - - /** - * 添加非空值到负载 - */ - private void addToPayloadIfNotNull(JSONObject json, String key, Object value) { - if (ObjectUtil.isNotNull(value)) { - json.set(key, value); - } - } - - /** - * 添加非空字符串到负载 - */ - private void addToPayloadIfNotEmpty(JSONObject json, String key, String value) { - if (StrUtil.isNotEmpty(value)) { - json.set(key, value); - } - } - - /** - * 清理JSON缓存 - */ - private void cleanJsonCacheIfNeeded() { - if (jsonCache.size() > MAX_CACHE_SIZE) { - // 清理一半的缓存 - int clearCount = jsonCache.size() / 2; - jsonCache.entrySet().removeIf(entry -> clearCount > 0 && Math.random() < 0.5); - - if (log.isDebugEnabled()) { - log.debug("[cleanJsonCacheIfNeeded][JSON 缓存已清理] 当前缓存大小: {}", jsonCache.size()); - } - } - } - - /** - * 截断数据用于日志输出 - */ - private String truncateData(byte[] data, int maxLength) { - if (data.length <= maxLength) { - return new String(data, StandardCharsets.UTF_8); - } - - byte[] truncated = new byte[maxLength]; - System.arraycopy(data, 0, truncated, 0, maxLength); - return new String(truncated, StandardCharsets.UTF_8) + "...(截断)"; - } - - // ==================== 自定义异常 ==================== - - /** - * TCP 编解码异常 - */ - public static class TcpCodecException extends RuntimeException { - - public TcpCodecException(String message) { - super(message); - } - - public TcpCodecException(String message, Throwable cause) { - super(message, cause); - } - - } -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java new file mode 100644 index 0000000000..ac8a3d174d --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java @@ -0,0 +1,245 @@ +package cn.iocoder.yudao.module.iot.gateway.codec.tcp; + +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.nio.charset.StandardCharsets; + +/** + * TCP JSON格式 {@link IotDeviceMessage} 编解码器 + * + * 采用纯JSON格式传输,参考EMQX和HTTP模块的数据格式 + * + * JSON消息格式: + * { + * "id": "消息ID", + * "method": "消息方法", + * "deviceId": "设备ID", + * "productKey": "产品Key", + * "deviceName": "设备名称", + * "params": {...}, + * "timestamp": 时间戳 + * } + * + * @author 芋道源码 + */ +@Slf4j +@Component +public class IotTcpJsonDeviceMessageCodec implements IotDeviceMessageCodec { + + public static final String TYPE = "TCP_JSON"; + + // ==================== 常量定义 ==================== + + @Override + public String type() { + return TYPE; + } + + @Override + public byte[] encode(IotDeviceMessage message) { + if (message == null || StrUtil.isEmpty(message.getMethod())) { + throw new IllegalArgumentException("消息或方法不能为空"); + } + + try { + // 构建JSON消息 + JSONObject jsonMessage = buildJsonMessage(message); + + // 转换为字节数组 + String jsonString = jsonMessage.toString(); + byte[] result = jsonString.getBytes(StandardCharsets.UTF_8); + + if (log.isDebugEnabled()) { + log.debug("[encode][编码成功] 方法: {}, JSON长度: {}字节, 内容: {}", + message.getMethod(), result.length, jsonString); + } + + return result; + } catch (Exception e) { + log.error("[encode][编码失败] 方法: {}", message.getMethod(), e); + throw new RuntimeException("JSON消息编码失败", e); + } + } + + // ==================== 编解码方法 ==================== + + @Override + public IotDeviceMessage decode(byte[] bytes) { + if (bytes == null || bytes.length == 0) { + throw new IllegalArgumentException("待解码数据不能为空"); + } + + try { + // 转换为JSON字符串 + String jsonString = new String(bytes, StandardCharsets.UTF_8); + + if (log.isDebugEnabled()) { + log.debug("[decode][开始解码] JSON长度: {}字节, 内容: {}", bytes.length, jsonString); + } + + // 解析JSON消息 + JSONObject jsonMessage = JSONUtil.parseObj(jsonString); + + // 构建IoT设备消息 + IotDeviceMessage message = parseJsonMessage(jsonMessage); + + if (log.isDebugEnabled()) { + log.debug("[decode][解码成功] 消息ID: {}, 方法: {}, 设备ID: {}", + message.getId(), message.getMethod(), message.getDeviceId()); + } + + return message; + } catch (Exception e) { + log.error("[decode][解码失败] 数据长度: {}", bytes.length, e); + throw new RuntimeException("JSON消息解码失败", e); + } + } + + /** + * 编码数据上报消息 + */ + public byte[] encodeDataReport(Object params, Long deviceId, String productKey, String deviceName) { + IotDeviceMessage message = createMessage(MessageMethod.PROPERTY_POST, params, deviceId, productKey, deviceName); + return encode(message); + } + + /** + * 编码心跳消息 + */ + public byte[] encodeHeartbeat(Long deviceId, String productKey, String deviceName) { + IotDeviceMessage message = createMessage(MessageMethod.STATE_ONLINE, null, deviceId, productKey, deviceName); + return encode(message); + } + + // ==================== 便捷方法 ==================== + + /** + * 编码事件上报消息 + */ + public byte[] encodeEventReport(Object params, Long deviceId, String productKey, String deviceName) { + IotDeviceMessage message = createMessage(MessageMethod.EVENT_POST, params, deviceId, productKey, deviceName); + return encode(message); + } + + /** + * 构建JSON消息 + */ + private JSONObject buildJsonMessage(IotDeviceMessage message) { + JSONObject jsonMessage = new JSONObject(); + + // 基础字段 + jsonMessage.set(JsonField.ID, StrUtil.isNotEmpty(message.getId()) ? message.getId() : IdUtil.fastSimpleUUID()); + jsonMessage.set(JsonField.METHOD, message.getMethod()); + jsonMessage.set(JsonField.TIMESTAMP, System.currentTimeMillis()); + + // 设备信息 + if (message.getDeviceId() != null) { + jsonMessage.set(JsonField.DEVICE_ID, message.getDeviceId()); + } + + // 参数 + if (message.getParams() != null) { + jsonMessage.set(JsonField.PARAMS, message.getParams()); + } + + // 响应码和消息(用于下行消息) + if (message.getCode() != null) { + jsonMessage.set(JsonField.CODE, message.getCode()); + } + if (StrUtil.isNotEmpty(message.getMsg())) { + jsonMessage.set(JsonField.MESSAGE, message.getMsg()); + } + + return jsonMessage; + } + + /** + * 解析JSON消息 + */ + private IotDeviceMessage parseJsonMessage(JSONObject jsonMessage) { + // 提取基础字段 + String id = jsonMessage.getStr(JsonField.ID); + String method = jsonMessage.getStr(JsonField.METHOD); + Object params = jsonMessage.get(JsonField.PARAMS); + + // 创建消息对象 + IotDeviceMessage message = IotDeviceMessage.requestOf(id, method, params); + + // 设置设备信息 + Long deviceId = jsonMessage.getLong(JsonField.DEVICE_ID); + if (deviceId != null) { + message.setDeviceId(deviceId); + } + + // 设置响应信息 + Integer code = jsonMessage.getInt(JsonField.CODE); + if (code != null) { + message.setCode(code); + } + + String msg = jsonMessage.getStr(JsonField.MESSAGE); + if (StrUtil.isNotEmpty(msg)) { + message.setMsg(msg); + } + + // 设置服务ID(基于JSON格式) + message.setServerId(generateServerId(jsonMessage)); + + return message; + } + + // ==================== 内部辅助方法 ==================== + + /** + * 创建消息对象 + */ + private IotDeviceMessage createMessage(String method, Object params, Long deviceId, String productKey, String deviceName) { + IotDeviceMessage message = IotDeviceMessage.requestOf(method, params); + message.setDeviceId(deviceId); + return message; + } + + /** + * 生成服务ID + */ + private String generateServerId(JSONObject jsonMessage) { + String id = jsonMessage.getStr(JsonField.ID); + Long deviceId = jsonMessage.getLong(JsonField.DEVICE_ID); + return String.format("tcp_json_%s_%s", deviceId != null ? deviceId : "unknown", + StrUtil.isNotEmpty(id) ? id.substring(0, Math.min(8, id.length())) : "noId"); + } + + /** + * 消息方法常量 + */ + public static class MessageMethod { + public static final String PROPERTY_POST = "thing.property.post"; // 数据上报 + public static final String STATE_ONLINE = "thing.state.online"; // 心跳 + public static final String EVENT_POST = "thing.event.post"; // 事件上报 + public static final String PROPERTY_SET = "thing.property.set"; // 属性设置 + public static final String PROPERTY_GET = "thing.property.get"; // 属性获取 + public static final String SERVICE_INVOKE = "thing.service.invoke"; // 服务调用 + } + + /** + * JSON字段名(参考EMQX和HTTP模块格式) + */ + private static class JsonField { + public static final String ID = "id"; + public static final String METHOD = "method"; + public static final String DEVICE_ID = "deviceId"; + public static final String PRODUCT_KEY = "productKey"; + public static final String DEVICE_NAME = "deviceName"; + public static final String PARAMS = "params"; + public static final String TIMESTAMP = "timestamp"; + public static final String CODE = "code"; + public static final String MESSAGE = "message"; + } +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java index de5f3426be..cd878994c7 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.config; import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; +import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpCodecManager; import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxAuthEventProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol; @@ -9,7 +10,6 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpDownstreamSubscr import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.TcpDeviceConnectionManager; import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import io.vertx.core.Vertx; @@ -89,28 +89,22 @@ public class IotGatewayConfiguration { return Vertx.vertx(); } - @Bean - public TcpDeviceConnectionManager tcpDeviceConnectionManager() { - return new TcpDeviceConnectionManager(); - } - @Bean public IotTcpUpstreamProtocol iotTcpUpstreamProtocol(IotGatewayProperties gatewayProperties, - TcpDeviceConnectionManager connectionManager, IotDeviceService deviceService, IotDeviceMessageService messageService, IotDeviceCommonApi deviceApi, + IotTcpCodecManager codecManager, Vertx tcpVertx) { - return new IotTcpUpstreamProtocol(gatewayProperties.getProtocol().getTcp(), connectionManager, - deviceService, messageService, deviceApi, tcpVertx); + return new IotTcpUpstreamProtocol(gatewayProperties.getProtocol().getTcp(), + deviceService, messageService, deviceApi, codecManager, tcpVertx); } @Bean public IotTcpDownstreamSubscriber iotTcpDownstreamSubscriber(IotTcpUpstreamProtocol protocolHandler, - TcpDeviceConnectionManager connectionManager, IotDeviceMessageService messageService, IotMessageBus messageBus) { - return new IotTcpDownstreamSubscriber(protocolHandler, connectionManager, messageService, messageBus); + return new IotTcpDownstreamSubscriber(protocolHandler, messageService, messageBus); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java index 3f47e14080..95d435387e 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java @@ -4,161 +4,67 @@ import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.TcpDeviceConnectionManager; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpDownstreamHandler; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicLong; - /** * IoT 网关 TCP 下游订阅者:接收下行给设备的消息 - *

- * 参考 EMQX 设计理念: - * 1. 高性能消息路由 - * 2. 容错机制 - * 3. 状态监控 - * 4. 资源管理 * * @author 芋道源码 */ -@RequiredArgsConstructor @Slf4j public class IotTcpDownstreamSubscriber implements IotMessageSubscriber { - private final IotTcpUpstreamProtocol protocolHandler; - - private final TcpDeviceConnectionManager connectionManager; - - private final IotDeviceMessageService messageService; + private final IotTcpDownstreamHandler downstreamHandler; private final IotMessageBus messageBus; - private volatile IotTcpDownstreamHandler downstreamHandler; + private final IotTcpUpstreamProtocol protocol; - private final AtomicBoolean initialized = new AtomicBoolean(false); - - private final AtomicLong processedMessages = new AtomicLong(0); - - private final AtomicLong failedMessages = new AtomicLong(0); + public IotTcpDownstreamSubscriber(IotTcpUpstreamProtocol protocol, + IotDeviceMessageService messageService, + IotMessageBus messageBus) { + this.protocol = protocol; + this.messageBus = messageBus; + this.downstreamHandler = new IotTcpDownstreamHandler(messageService); + } @PostConstruct public void init() { - if (!initialized.compareAndSet(false, true)) { - log.warn("[init][TCP 下游消息订阅者已初始化,跳过重复初始化]"); - return; - } - - try { - // 初始化下游处理器 - downstreamHandler = new IotTcpDownstreamHandler(connectionManager, messageService); - - // 注册到消息总线 - messageBus.register(this); - - log.info("[init][TCP 下游消息订阅者初始化完成] Topic: {}, Group: {}", - getTopic(), getGroup()); - } catch (Exception e) { - initialized.set(false); - log.error("[init][TCP 下游消息订阅者初始化失败]", e); - throw new RuntimeException("TCP 下游消息订阅者初始化失败", e); - } - } - - @PreDestroy - public void destroy() { - if (!initialized.get()) { - return; - } - - try { - log.info("[destroy][TCP 下游消息订阅者已关闭] 处理消息数: {}, 失败消息数: {}", - processedMessages.get(), failedMessages.get()); - } catch (Exception e) { - log.error("[destroy][TCP 下游消息订阅者关闭失败]", e); - } finally { - initialized.set(false); - } + messageBus.register(this); } @Override public String getTopic() { - return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocolHandler.getServerId()); + return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId()); } @Override public String getGroup() { - return "tcp-downstream-" + protocolHandler.getServerId(); + // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group + return getTopic(); } @Override public void onMessage(IotDeviceMessage message) { - if (!initialized.get()) { - log.warn("[onMessage][订阅者未初始化,跳过消息处理]"); - return; - } - - long startTime = System.currentTimeMillis(); - + log.debug("[onMessage][接收到下行消息, messageId: {}, method: {}, deviceId: {}]", + message.getId(), message.getMethod(), message.getDeviceId()); try { - processedMessages.incrementAndGet(); - - if (log.isDebugEnabled()) { - log.debug("[onMessage][收到下行消息] 设备 ID: {}, 方法: {}, 消息ID: {}", - message.getDeviceId(), message.getMethod(), message.getId()); - } - // 参数校验 - if (message.getDeviceId() == null) { - log.warn("[onMessage][下行消息设备 ID 为空,跳过处理] 消息: {}", message); - return; - } - // 检查连接状态 - if (connectionManager.getClientByDeviceId(message.getDeviceId()) == null) { - log.warn("[onMessage][设备({})离线,跳过下行消息] 方法: {}", - message.getDeviceId(), message.getMethod()); + // 1. 校验 + String method = message.getMethod(); + if (method == null) { + log.warn("[onMessage][消息方法为空, messageId: {}, deviceId: {}]", + message.getId(), message.getDeviceId()); return; } - // 处理下行消息 + // 2. 处理下行消息 downstreamHandler.handle(message); - - // 性能监控 - long processTime = System.currentTimeMillis() - startTime; - // TODO @haohao:1000 搞成静态变量; - if (processTime > 1000) { // 超过 1 秒的慢消息 - log.warn("[onMessage][慢消息处理] 设备ID: {}, 方法: {}, 耗时: {}ms", - message.getDeviceId(), message.getMethod(), processTime); - } } catch (Exception e) { - failedMessages.incrementAndGet(); - log.error("[onMessage][处理下行消息失败] 设备ID: {}, 方法: {}, 消息: {}", - message.getDeviceId(), message.getMethod(), message, e); + log.error("[onMessage][处理下行消息失败, messageId: {}, method: {}, deviceId: {}]", + message.getId(), message.getMethod(), message.getDeviceId(), e); } } - - // TODO @haohao:多余的要不先清理掉; - - /** - * 获取订阅者统计信息 - */ - public String getSubscriberStatistics() { - return String.format("TCP下游订阅者 - 已处理: %d, 失败: %d, 成功率: %.2f%%", - processedMessages.get(), - failedMessages.get(), - processedMessages.get() > 0 - ? (double) (processedMessages.get() - failedMessages.get()) / processedMessages.get() * 100 - : 0.0); - } - - /** - * 检查订阅者健康状态 - */ - public boolean isHealthy() { - return initialized.get() && downstreamHandler != null; - } - } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java index f9d4bd2d26..0e2ad6c4e1 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java @@ -2,8 +2,8 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpCodecManager; import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.TcpDeviceConnectionManager; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpUpstreamHandler; import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; @@ -16,19 +16,8 @@ import jakarta.annotation.PreDestroy; import lombok.Getter; import lombok.extern.slf4j.Slf4j; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; - /** * IoT 网关 TCP 协议:接收设备上行消息 - *

- * 负责接收设备上行消息,支持: - * 1. 设备注册 - * 2. 心跳保活 - * 3. 属性上报 - * 4. 事件上报 - * 5. 设备连接管理 * * @author 芋道源码 */ @@ -37,14 +26,14 @@ public class IotTcpUpstreamProtocol { private final IotGatewayProperties.TcpProperties tcpProperties; - private final TcpDeviceConnectionManager connectionManager; - private final IotDeviceService deviceService; private final IotDeviceMessageService messageService; private final IotDeviceCommonApi deviceApi; + private final IotTcpCodecManager codecManager; + private final Vertx vertx; @Getter @@ -53,54 +42,30 @@ public class IotTcpUpstreamProtocol { private NetServer netServer; public IotTcpUpstreamProtocol(IotGatewayProperties.TcpProperties tcpProperties, - TcpDeviceConnectionManager connectionManager, - IotDeviceService deviceService, - IotDeviceMessageService messageService, - IotDeviceCommonApi deviceApi, - Vertx vertx) { + IotDeviceService deviceService, + IotDeviceMessageService messageService, + IotDeviceCommonApi deviceApi, + IotTcpCodecManager codecManager, + Vertx vertx) { this.tcpProperties = tcpProperties; - this.connectionManager = connectionManager; this.deviceService = deviceService; this.messageService = messageService; this.deviceApi = deviceApi; + this.codecManager = codecManager; this.vertx = vertx; this.serverId = IotDeviceMessageUtils.generateServerId(tcpProperties.getPort()); } @PostConstruct public void start() { - // 1. 启动 TCP 服务器 - try { - startTcpServer(); - log.info("[start][IoT 网关 TCP 协议处理器启动完成,服务器ID: {}]", serverId); - } catch (Exception e) { - log.error("[start][IoT 网关 TCP 协议处理器启动失败]", e); - // 抛出异常,中断 Spring 容器启动 - throw new RuntimeException("IoT 网关 TCP 协议处理器启动失败", e); - } - } - - @PreDestroy - public void stop() { - if (netServer != null) { - stopTcpServer(); - log.info("[stop][IoT 网关 TCP 协议处理器已停止]"); - } - } - - /** - * 启动 TCP 服务器 - */ - private void startTcpServer() { - // TODO @haohao:同类的,最好使用相同序号前缀,一个方法看起来有段落感。包括同类可以去掉之间的空格。例如说这里的,1. 2. 3. 4. 是初始化;5. 6. 是管理启动 - // 1. 创建服务器选项 + // 创建服务器选项 NetServerOptions options = new NetServerOptions() .setPort(tcpProperties.getPort()) .setTcpKeepAlive(true) .setTcpNoDelay(true) .setReuseAddress(true); - // 2. 配置 SSL(如果启用) + // 配置 SSL(如果启用) if (Boolean.TRUE.equals(tcpProperties.getSslEnabled())) { PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() .setKeyPath(tcpProperties.getSslKeyPath()) @@ -108,72 +73,32 @@ public class IotTcpUpstreamProtocol { options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); } - // 3. 创建 TCP 服务器 + // 创建服务器并设置连接处理器 netServer = vertx.createNetServer(options); - - // 4. 设置连接处理器 netServer.connectHandler(socket -> { - log.info("[startTcpServer][新设备连接: {}]", socket.remoteAddress()); - IotTcpUpstreamHandler handler = new IotTcpUpstreamHandler( - tcpProperties, connectionManager, deviceService, messageService, deviceApi, serverId); + IotTcpUpstreamHandler handler = new IotTcpUpstreamHandler(this, messageService, codecManager); handler.handle(socket); }); - // 5. 同步启动服务器,等待结果 - CountDownLatch latch = new CountDownLatch(1); - AtomicReference failure = new AtomicReference<>(); - netServer.listen(result -> { - if (result.succeeded()) { - log.info("[startTcpServer][TCP 服务器启动成功] 端口: {}, 服务器ID: {}", - result.result().actualPort(), serverId); - } else { - log.error("[startTcpServer][TCP 服务器启动失败]", result.cause()); - failure.set(result.cause()); - } - latch.countDown(); - }); - - // 6. 等待启动结果,设置超时 + // 启动服务器 try { - if (!latch.await(10, TimeUnit.SECONDS)) { - throw new RuntimeException("TCP 服务器启动超时"); - } - if (failure.get() != null) { - throw new RuntimeException("TCP 服务器启动失败", failure.get()); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new RuntimeException("TCP 服务器启动被中断", e); + netServer.listen().result(); + log.info("[start][IoT 网关 TCP 协议启动成功,端口:{}]", tcpProperties.getPort()); + } catch (Exception e) { + log.error("[start][IoT 网关 TCP 协议启动失败]", e); + throw e; } } - /** - * 停止 TCP 服务器 - */ - private void stopTcpServer() { - if (netServer == null) { - return; - } - log.info("[stopTcpServer][准备关闭 TCP 服务器]"); - CountDownLatch latch = new CountDownLatch(1); - // 异步关闭,并使用 Latch 等待结果 - netServer.close(result -> { - if (result.succeeded()) { - log.info("[stopTcpServer][IoT 网关 TCP 协议处理器已停止]"); - } else { - log.warn("[stopTcpServer][TCP 服务器关闭失败]", result.cause()); + @PreDestroy + public void stop() { + if (netServer != null) { + try { + netServer.close().result(); + log.info("[stop][IoT 网关 TCP 协议已停止]"); + } catch (Exception e) { + log.error("[stop][IoT 网关 TCP 协议停止失败]", e); } - latch.countDown(); - }); - - try { - // 等待关闭完成,设置超时 - if (!latch.await(10, TimeUnit.SECONDS)) { - log.warn("[stopTcpServer][关闭 TCP 服务器超时]"); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - log.warn("[stopTcpServer][等待 TCP 服务器关闭被中断]", e); } } } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/client/TcpDeviceClient.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/client/TcpDeviceClient.java deleted file mode 100644 index f4d1761c9e..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/client/TcpDeviceClient.java +++ /dev/null @@ -1,220 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.client; - -import io.vertx.core.buffer.Buffer; -import io.vertx.core.net.NetSocket; -import io.vertx.core.parsetools.RecordParser; -import lombok.Getter; -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; - -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * TCP 设备客户端:封装设备连接的基本信息和操作 - *

- * 该类中的状态变更(如 authenticated, closed)使用 AtomicBoolean 保证原子性。 - * 对 socket 的操作应在 Vert.x Event Loop 线程中执行,以避免并发问题。 - * - * @author 芋道源码 - */ -@Slf4j -public class TcpDeviceClient { - - @Getter - private final String clientId; - - @Getter - @Setter - private String deviceAddr; // 从 final 移除,因为在注册后才设置 - - @Getter - @Setter - private String productKey; - - @Getter - @Setter - private String deviceName; - - @Getter - @Setter - private Long deviceId; - - @Getter - private NetSocket socket; - - @Getter - @Setter - private RecordParser parser; - - @Getter - private final long keepAliveTimeoutMs; - - private volatile long lastKeepAliveTime; - - private final AtomicBoolean authenticated = new AtomicBoolean(false); - private final AtomicBoolean closed = new AtomicBoolean(false); - - /** - * 构造函数 - * - * @param clientId 客户端 ID,全局唯一 - * @param keepAliveTimeoutMs 心跳超时时间(毫秒),从配置中读取 - */ - public TcpDeviceClient(String clientId, long keepAliveTimeoutMs) { - this.clientId = clientId; - this.keepAliveTimeoutMs = keepAliveTimeoutMs; - this.lastKeepAliveTime = System.currentTimeMillis(); - } - - /** - * 绑定网络套接字,并设置相关处理器。 - * 此方法应在 Vert.x Event Loop 线程中调用 - * - * @param socket 网络套接字 - */ - public void setSocket(NetSocket socket) { - // 无需 synchronized,Vert.x 保证了同一个 socket 的事件在同一个 Event Loop 中处理 - if (this.socket != null && this.socket != socket) { - log.warn("[setSocket][客户端({}) 正在用新的 socket 替换旧的,旧 socket 将被关闭]", clientId); - this.socket.close(); - } - this.socket = socket; - - // 注册处理器 - if (socket != null) { - // 1. 设置关闭处理器 - socket.closeHandler(v -> { - log.info("[setSocket][设备客户端({})的连接已由远端关闭]", clientId); - shutdown(); // 统一调用 shutdown 进行资源清理 - }); - - // 2. 设置异常处理器 - socket.exceptionHandler(e -> { - log.error("[setSocket][设备客户端({})连接出现异常]", clientId, e); - shutdown(); // 出现异常时也关闭连接 - }); - - // 3. 设置数据处理器 - socket.handler(buffer -> { - // 任何数据往来都表示连接是活跃的 - keepAlive(); - - if (parser != null) { - parser.handle(buffer); - } else { - log.warn("[setSocket][设备客户端({}) 未设置解析器(parser),原始数据被忽略: {}]", clientId, buffer.toString()); - } - }); - } - } - - /** - * 更新心跳时间,表示设备仍然活跃 - */ - public void keepAlive() { - this.lastKeepAliveTime = System.currentTimeMillis(); - } - - /** - * 检查连接是否在线 - * 判断标准:未被主动关闭、socket 存在、且在心跳超时时间内 - * - * @return 是否在线 - */ - public boolean isOnline() { - if (closed.get() || socket == null) { - return false; - } - long idleTime = System.currentTimeMillis() - lastKeepAliveTime; - return idleTime < keepAliveTimeoutMs; - } - - // TODO @haohao:1)是不是简化下:productKey 和 deviceName 非空,就认为是已认证;2)如果是的话,productKey 和 deviceName 搞成一个设置方法?setAuthenticated(productKey、deviceName) - - public boolean isAuthenticated() { - return authenticated.get(); - } - - public void setAuthenticated(boolean authenticated) { - this.authenticated.set(authenticated); - } - - /** - * 向设备发送消息 - * - * @param buffer 消息内容 - */ - public void sendMessage(Buffer buffer) { - if (closed.get() || socket == null) { - log.warn("[sendMessage][设备客户端({})连接已关闭,无法发送消息]", clientId); - return; - } - - // Vert.x 的 write 是异步的,不会阻塞 - socket.write(buffer, result -> { - // 发送失败可能意味着连接已断开,主动关闭 - if (!result.succeeded()) { - log.error("[sendMessage][设备客户端({})发送消息失败]", clientId, result.cause()); - shutdown(); - return; - } - - // 发送成功也更新心跳,表示连接活跃 - if (log.isDebugEnabled()) { - log.debug("[sendMessage][设备客户端({})发送消息成功]", clientId); - } - keepAlive(); - }); - } - - // TODO @haohao:是不是叫 close 好点?或者问问大模型 - /** - * 关闭客户端连接并清理资源。 - * 这是一个幂等操作,可以被多次安全调用。 - */ - public void shutdown() { - // 使用原子操作保证只执行一次关闭逻辑 - if (closed.getAndSet(true)) { - return; - } - - log.info("[shutdown][正在关闭设备客户端连接: {}]", clientId); - - // 先将 socket 引用置空,再关闭,避免并发问题 - NetSocket socketToClose = this.socket; - this.socket = null; - - if (socketToClose != null) { - try { - // close 是异步的,但我们在这里不关心其结果,因为我们已经将客户端标记为关闭 - socketToClose.close(); - } catch (Exception e) { - log.warn("[shutdown][关闭TCP连接时出现异常,可能已被关闭]", e); - } - } - - // 重置认证状态 - authenticated.set(false); - } - - public String getConnectionInfo() { - NetSocket currentSocket = this.socket; - if (currentSocket != null && currentSocket.remoteAddress() != null) { - return currentSocket.remoteAddress().toString(); - } - return "disconnected"; - } - - @Override - public String toString() { - return "TcpDeviceClient{" + - "clientId='" + clientId + '\'' + - ", deviceAddr='" + deviceAddr + '\'' + - ", deviceId=" + deviceId + - ", authenticated=" + authenticated.get() + - ", online=" + isOnline() + - ", connection=" + getConnectionInfo() + - '}'; - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/TcpDeviceConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/TcpDeviceConnectionManager.java deleted file mode 100644 index b2b6b3c31e..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/TcpDeviceConnectionManager.java +++ /dev/null @@ -1,506 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager; - -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.client.TcpDeviceClient; -import io.vertx.core.buffer.Buffer; -import io.vertx.core.net.NetSocket; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; - -import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.locks.ReentrantReadWriteLock; - -/** - * TCP 设备连接管理器 - *

- * 参考 EMQX 设计理念: - * 1. 高性能连接管理 - * 2. 连接池和资源管理 - * 3. 流量控制 TODO @haohao:这个要不先去掉 - * 4. 监控统计 TODO @haohao:这个要不先去掉 - * 5. 自动清理和容错 - * - * @author 芋道源码 - */ -@Component -@Slf4j -public class TcpDeviceConnectionManager { - - // ==================== 连接存储 ==================== - - /** - * 设备客户端映射 - * Key: 设备地址, Value: 设备客户端 - */ - private final ConcurrentMap clientMap = new ConcurrentHashMap<>(); - - /** - * 设备ID到设备地址的映射 - * Key: 设备ID, Value: 设备地址 - */ - private final ConcurrentMap deviceIdToAddrMap = new ConcurrentHashMap<>(); - - /** - * 套接字到客户端的映射,用于快速查找 - * Key: NetSocket, Value: 设备地址 - */ - private final ConcurrentMap socketToAddrMap = new ConcurrentHashMap<>(); - - // ==================== 读写锁 ==================== - - private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); - private final ReentrantReadWriteLock.ReadLock readLock = lock.readLock(); - private final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); - - // ==================== 定时任务 ==================== - - /** - * 定时任务执行器 - */ - private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(3, r -> { - Thread t = new Thread(r, "tcp-connection-manager"); - t.setDaemon(true); - return t; - }); - - // ==================== 统计信息 ==================== - - private final AtomicLong totalConnections = new AtomicLong(0); - private final AtomicLong totalDisconnections = new AtomicLong(0); - private final AtomicLong totalMessages = new AtomicLong(0); - private final AtomicLong totalFailedMessages = new AtomicLong(0); - private final AtomicLong totalBytes = new AtomicLong(0); - - // ==================== 配置参数 ==================== - - private static final int MAX_CONNECTIONS = 10000; - private static final int HEARTBEAT_CHECK_INTERVAL = 30; // 秒 - private static final int CONNECTION_CLEANUP_INTERVAL = 60; // 秒 - private static final int STATS_LOG_INTERVAL = 300; // 秒 - - /** - * 构造函数,启动定时任务 - */ - public TcpDeviceConnectionManager() { - startScheduledTasks(); - } - - /** - * 启动定时任务 - */ - private void startScheduledTasks() { - // 心跳检查任务 - scheduler.scheduleAtFixedRate(this::checkHeartbeat, - HEARTBEAT_CHECK_INTERVAL, HEARTBEAT_CHECK_INTERVAL, TimeUnit.SECONDS); - - // 连接清理任务 - scheduler.scheduleAtFixedRate(this::cleanupConnections, - CONNECTION_CLEANUP_INTERVAL, CONNECTION_CLEANUP_INTERVAL, TimeUnit.SECONDS); - - // 统计日志任务 - scheduler.scheduleAtFixedRate(this::logStatistics, - STATS_LOG_INTERVAL, STATS_LOG_INTERVAL, TimeUnit.SECONDS); - } - - /** - * 添加设备客户端 - */ - public boolean addClient(String deviceAddr, TcpDeviceClient client) { - // TODO @haohao:这个要不去掉;目前看着没做 result 的处理; - if (clientMap.size() >= MAX_CONNECTIONS) { - log.warn("[addClient][连接数已达上限({}),拒绝新连接: {}]", MAX_CONNECTIONS, deviceAddr); - return false; - } - - writeLock.lock(); - try { - log.info("[addClient][添加设备客户端: {}]", deviceAddr); - - // 关闭之前的连接(如果存在) - TcpDeviceClient existingClient = clientMap.get(deviceAddr); - if (existingClient != null) { - log.warn("[addClient][设备({})已存在连接,关闭旧连接]", deviceAddr); - removeClientInternal(deviceAddr, existingClient); - } - - // 添加新连接 - clientMap.put(deviceAddr, client); - - // 添加套接字映射 - if (client.getSocket() != null) { - socketToAddrMap.put(client.getSocket(), deviceAddr); - } - - // 如果客户端已设置设备 ID,更新映射 - if (client.getDeviceId() != null) { - deviceIdToAddrMap.put(client.getDeviceId(), deviceAddr); - } - - totalConnections.incrementAndGet(); - return true; - } finally { - writeLock.unlock(); - } - } - - /** - * 移除设备客户端 - */ - public void removeClient(String deviceAddr) { - writeLock.lock(); - try { - TcpDeviceClient client = clientMap.get(deviceAddr); - if (client != null) { - removeClientInternal(deviceAddr, client); - } - } finally { - writeLock.unlock(); - } - } - - /** - * 内部移除客户端方法(无锁) - */ - private void removeClientInternal(String deviceAddr, TcpDeviceClient client) { - log.info("[removeClient][移除设备客户端: {}]", deviceAddr); - - // 从映射中移除 - clientMap.remove(deviceAddr); - - // 移除套接字映射 - if (client.getSocket() != null) { - socketToAddrMap.remove(client.getSocket()); - } - - // 移除设备ID映射 - if (client.getDeviceId() != null) { - deviceIdToAddrMap.remove(client.getDeviceId()); - } - - // 关闭连接 - client.shutdown(); - - totalDisconnections.incrementAndGet(); - } - - /** - * 通过设备地址获取客户端 - */ - public TcpDeviceClient getClient(String deviceAddr) { - readLock.lock(); - try { - return clientMap.get(deviceAddr); - } finally { - readLock.unlock(); - } - } - - /** - * 通过设备 ID 获取客户端 - */ - public TcpDeviceClient getClientByDeviceId(Long deviceId) { - readLock.lock(); - try { - String deviceAddr = deviceIdToAddrMap.get(deviceId); - return deviceAddr != null ? clientMap.get(deviceAddr) : null; - } finally { - readLock.unlock(); - } - } - - // TODO @haohao:getClientBySocket、isDeviceOnline、sendMessage、sendMessageByDeviceId、broadcastMessage 用不到的方法,要不先暂时不提供?保持简洁、更容易理解哈。 - - /** - * 通过网络连接获取客户端 - */ - public TcpDeviceClient getClientBySocket(NetSocket socket) { - readLock.lock(); - try { - String deviceAddr = socketToAddrMap.get(socket); - return deviceAddr != null ? clientMap.get(deviceAddr) : null; - } finally { - readLock.unlock(); - } - } - - /** - * 检查设备是否在线 - */ - public boolean isDeviceOnline(Long deviceId) { - TcpDeviceClient client = getClientByDeviceId(deviceId); - return client != null && client.isOnline(); - } - - /** - * 设置设备 ID 映射 - */ - public void setDeviceIdMapping(String deviceAddr, Long deviceId) { - writeLock.lock(); - try { - TcpDeviceClient client = clientMap.get(deviceAddr); - if (client != null) { - client.setDeviceId(deviceId); - deviceIdToAddrMap.put(deviceId, deviceAddr); - log.debug("[setDeviceIdMapping][设置设备ID映射: {} -> {}]", deviceAddr, deviceId); - } - } finally { - writeLock.unlock(); - } - } - - /** - * 发送消息给设备 - */ - public boolean sendMessage(String deviceAddr, Buffer buffer) { - TcpDeviceClient client = getClient(deviceAddr); - if (client != null && client.isOnline()) { - try { - client.sendMessage(buffer); - totalMessages.incrementAndGet(); - totalBytes.addAndGet(buffer.length()); - return true; - } catch (Exception e) { - totalFailedMessages.incrementAndGet(); - log.error("[sendMessage][发送消息失败] 设备地址: {}", deviceAddr, e); - return false; - } - } - log.warn("[sendMessage][设备({})不在线,无法发送消息]", deviceAddr); - return false; - } - - /** - * 通过设备ID发送消息 - */ - public boolean sendMessageByDeviceId(Long deviceId, Buffer buffer) { - TcpDeviceClient client = getClientByDeviceId(deviceId); - if (client != null && client.isOnline()) { - try { - client.sendMessage(buffer); - totalMessages.incrementAndGet(); - totalBytes.addAndGet(buffer.length()); - return true; - } catch (Exception e) { - totalFailedMessages.incrementAndGet(); - log.error("[sendMessageByDeviceId][发送消息失败] 设备ID: {}", deviceId, e); - return false; - } - } - log.warn("[sendMessageByDeviceId][设备ID({})不在线,无法发送消息]", deviceId); - return false; - } - - /** - * 广播消息给所有在线设备 - */ - public int broadcastMessage(Buffer buffer) { - int successCount = 0; - readLock.lock(); - try { - for (TcpDeviceClient client : clientMap.values()) { - if (client.isOnline()) { - try { - client.sendMessage(buffer); - successCount++; - } catch (Exception e) { - log.error("[broadcastMessage][广播消息失败] 设备: {}", client.getDeviceAddr(), e); - } - } - } - } finally { - readLock.unlock(); - } - - totalMessages.addAndGet(successCount); - totalBytes.addAndGet((long) successCount * buffer.length()); - return successCount; - } - - /** - * 获取在线设备数量 - */ - public int getOnlineCount() { - readLock.lock(); - try { - return (int) clientMap.values().stream() - .filter(TcpDeviceClient::isOnline) - .count(); - } finally { - readLock.unlock(); - } - } - - /** - * 获取总连接数 - */ - public int getTotalCount() { - return clientMap.size(); - } - - /** - * 获取认证设备数量 - */ - public int getAuthenticatedCount() { - readLock.lock(); - try { - return (int) clientMap.values().stream() - .filter(TcpDeviceClient::isAuthenticated) - .count(); - } finally { - readLock.unlock(); - } - } - - // TODO @haohao:心跳超时,需要 close 么? - /** - * 心跳检查任务 - */ - private void checkHeartbeat() { - try { - int offlineCount = 0; - - readLock.lock(); - try { - for (TcpDeviceClient client : clientMap.values()) { - if (!client.isOnline()) { - offlineCount++; - } - } - } finally { - readLock.unlock(); - } - - if (offlineCount > 0) { - log.info("[checkHeartbeat][发现 {} 个离线设备,将在清理任务中处理]", offlineCount); - } - } catch (Exception e) { - log.error("[checkHeartbeat][心跳检查任务异常]", e); - } - } - - /** - * 连接清理任务 - */ - private void cleanupConnections() { - try { - int beforeSize = clientMap.size(); - - writeLock.lock(); - try { - clientMap.entrySet().removeIf(entry -> { - TcpDeviceClient client = entry.getValue(); - if (!client.isOnline()) { - log.debug("[cleanupConnections][清理离线连接: {}]", entry.getKey()); - - // 清理相关映射 - if (client.getSocket() != null) { - socketToAddrMap.remove(client.getSocket()); - } - if (client.getDeviceId() != null) { - deviceIdToAddrMap.remove(client.getDeviceId()); - } - - client.shutdown(); - totalDisconnections.incrementAndGet(); - return true; - } - return false; - }); - } finally { - writeLock.unlock(); - } - - int afterSize = clientMap.size(); - if (beforeSize != afterSize) { - log.info("[cleanupConnections][清理完成] 连接数: {} -> {}, 清理数: {}", - beforeSize, afterSize, beforeSize - afterSize); - } - } catch (Exception e) { - log.error("[cleanupConnections][连接清理任务异常]", e); - } - } - - /** - * 统计日志任务 - */ - private void logStatistics() { - try { - long totalConn = totalConnections.get(); - long totalDisconnections = this.totalDisconnections.get(); - long totalMsg = totalMessages.get(); - long totalFailedMsg = totalFailedMessages.get(); - long totalBytesValue = totalBytes.get(); - - log.info("[logStatistics][连接统计] 总连接: {}, 总断开: {}, 当前在线: {}, 认证设备: {}, " + - "总消息: {}, 失败消息: {}, 总字节: {}", - totalConn, totalDisconnections, getOnlineCount(), getAuthenticatedCount(), - totalMsg, totalFailedMsg, totalBytesValue); - } catch (Exception e) { - log.error("[logStatistics][统计日志任务异常]", e); - } - } - - /** - * 关闭连接管理器 - */ - public void shutdown() { - log.info("[shutdown][关闭TCP连接管理器]"); - - // 关闭定时任务 - scheduler.shutdown(); - try { - if (!scheduler.awaitTermination(10, TimeUnit.SECONDS)) { - scheduler.shutdownNow(); - } - } catch (InterruptedException e) { - scheduler.shutdownNow(); - Thread.currentThread().interrupt(); - } - - // 关闭所有连接 - writeLock.lock(); - try { - clientMap.values().forEach(TcpDeviceClient::shutdown); - clientMap.clear(); - deviceIdToAddrMap.clear(); - socketToAddrMap.clear(); - } finally { - writeLock.unlock(); - } - } - - /** - * 获取连接状态信息 - */ - public String getConnectionStatus() { - return String.format("总连接数: %d, 在线设备: %d, 认证设备: %d, 成功率: %.2f%%", - getTotalCount(), getOnlineCount(), getAuthenticatedCount(), - totalMessages.get() > 0 - ? (double) (totalMessages.get() - totalFailedMessages.get()) / totalMessages.get() * 100 - : 0.0); - } - - /** - * 获取详细统计信息 - */ - public String getDetailedStatistics() { - return String.format( - "TCP连接管理器统计:\n" + - "- 当前连接数: %d\n" + - "- 在线设备数: %d\n" + - "- 认证设备数: %d\n" + - "- 历史总连接: %d\n" + - "- 历史总断开: %d\n" + - "- 总消息数: %d\n" + - "- 失败消息数: %d\n" + - "- 总字节数: %d\n" + - "- 消息成功率: %.2f%%", - getTotalCount(), getOnlineCount(), getAuthenticatedCount(), - totalConnections.get(), totalDisconnections.get(), - totalMessages.get(), totalFailedMessages.get(), totalBytes.get(), - totalMessages.get() > 0 - ? (double) (totalMessages.get() - totalFailedMessages.get()) / totalMessages.get() * 100 - : 0.0); - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataDecoder.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataDecoder.java deleted file mode 100644 index ed4b2ebaa0..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataDecoder.java +++ /dev/null @@ -1,98 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol; - -import io.vertx.core.buffer.Buffer; -import lombok.extern.slf4j.Slf4j; - -// TODO @haohao:“设备地址长度”是不是不需要。 -/** - * TCP 数据解码器 - *

- * 负责将字节流解码为 TcpDataPackage 对象 - *

- * 数据包格式: - * 包头(4 字节长度) | 设备地址长度(2 字节) | 设备地址(不定长) | 功能码(2 字节) | 消息序号(2 字节) | 包体(不定长) - * - * @author 芋道源码 - */ -@Slf4j -public class TcpDataDecoder { - - /** - * 解码数据包 - * - * @param buffer 数据缓冲区 - * @return 解码后的数据包 - * @throws IllegalArgumentException 如果数据包格式不正确 - */ - public static TcpDataPackage decode(Buffer buffer) { - if (buffer == null || buffer.length() < 8) { - throw new IllegalArgumentException("数据包长度不足"); - } - - try { - int index = 0; - - // 1.1 获取设备地址长度(2字节) - short addrLength = buffer.getShort(index); - index += 2; - - // 1.2 校验数据包长度 - int expectedLength = 2 + addrLength + 2 + 2; // 地址长度 + 地址 + 功能码 + 消息序号 - if (buffer.length() < expectedLength) { - throw new IllegalArgumentException("数据包长度不足,期望至少 " + expectedLength + " 字节"); - } - - // 1.3 获取设备地址 - String addr = buffer.getBuffer(index, index + addrLength).toString(); - index += addrLength; - - // 1.4 获取功能码(2字节) - short code = buffer.getShort(index); - index += 2; - - // 1.5 获取消息序号(2字节) - short mid = buffer.getShort(index); - index += 2; - - // 1.6 获取包体数据 - String payload = ""; - if (index < buffer.length()) { - payload = buffer.getString(index, buffer.length()); - } - - // 2. 构建数据包对象 - TcpDataPackage dataPackage = TcpDataPackage.builder() - .addrLength((int) addrLength) - .addr(addr) - .code(code) - .mid(mid) - .payload(payload) - .build(); - - log.debug("[decode][解码成功] 设备地址: {}, 功能码: {}, 消息序号: {}, 包体长度: {}", - addr, dataPackage.getCodeDescription(), mid, payload.length()); - return dataPackage; - } catch (Exception e) { - log.error("[decode][解码失败] 数据: {}", buffer.toString(), e); - throw new IllegalArgumentException("数据包解码失败: " + e.getMessage(), e); - } - } - - // TODO @haohao:这个要不去掉,暂时没用到; - /** - * 校验数据包格式 - * - * @param buffer 数据缓冲区 - * @return 校验结果 - */ - public static boolean validate(Buffer buffer) { - try { - decode(buffer); - return true; - } catch (Exception e) { - log.warn("[validate][数据包格式校验失败] 数据: {}, 错误: {}", buffer.toString(), e.getMessage()); - return false; - } - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataEncoder.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataEncoder.java deleted file mode 100644 index 62f7bc4848..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataEncoder.java +++ /dev/null @@ -1,159 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol; - -import io.vertx.core.buffer.Buffer; -import lombok.extern.slf4j.Slf4j; - -/** - * TCP 数据编码器 - *

- * 负责将 TcpDataPackage 对象编码为字节流 - *

- * 数据包格式: - * 包头(4字节长度) | 设备地址长度(2字节) | 设备地址(不定长) | 功能码(2字节) | 消息序号(2字节) | 包体(不定长) - * - * @author 芋道源码 - */ -@Slf4j -public class TcpDataEncoder { - - /** - * 编码数据包 - * - * @param dataPackage 数据包对象 - * @return 编码后的字节流 - * @throws IllegalArgumentException 如果数据包对象不正确 - */ - public static Buffer encode(TcpDataPackage dataPackage) { - if (dataPackage == null) { - throw new IllegalArgumentException("数据包对象不能为空"); - } - if (dataPackage.getAddr() == null || dataPackage.getAddr().isEmpty()) { - throw new IllegalArgumentException("设备地址不能为空"); - } - if (dataPackage.getPayload() == null) { - dataPackage.setPayload(""); - } - - try { - Buffer buffer = Buffer.buffer(); - - // 1. 计算包体长度(除了包头 4 字节) - int payloadLength = dataPackage.getPayload().getBytes().length; - int totalLength = 2 + dataPackage.getAddr().length() + 2 + 2 + payloadLength; - - // 2.1 写入包头:总长度(4 字节) - buffer.appendInt(totalLength); - // 2.2 写入设备地址长度(2 字节) - buffer.appendShort((short) dataPackage.getAddr().length()); - // 2.3 写入设备地址(不定长) - buffer.appendBytes(dataPackage.getAddr().getBytes()); - // 2.4 写入功能码(2 字节) - buffer.appendShort(dataPackage.getCode()); - // 2.5 写入消息序号(2 字节) - buffer.appendShort(dataPackage.getMid()); - // 2.6 写入包体数据(不定长) - buffer.appendBytes(dataPackage.getPayload().getBytes()); - - log.debug("[encode][编码成功] 设备地址: {}, 功能码: {}, 消息序号: {}, 总长度: {}", - dataPackage.getAddr(), dataPackage.getCodeDescription(), - dataPackage.getMid(), buffer.length()); - return buffer; - } catch (Exception e) { - log.error("[encode][编码失败] 数据包: {}", dataPackage, e); - throw new IllegalArgumentException("数据包编码失败: " + e.getMessage(), e); - } - } - - /** - * 创建注册回复数据包 - * - * @param addr 设备地址 - * @param mid 消息序号 - * @param success 是否成功 - * @return 编码后的数据包 - */ - public static Buffer createRegisterReply(String addr, short mid, boolean success) { - // TODO @haohao:payload 默认成功、失败,最好讴有个枚举 - String payload = success ? "0" : "1"; // 0 表示成功,1 表示失败 - TcpDataPackage dataPackage = TcpDataPackage.builder() - .addr(addr) - .code(TcpDataPackage.CODE_REGISTER_REPLY) - .mid(mid) - .payload(payload) - .build(); - return encode(dataPackage); - } - - /** - * 创建数据下发数据包 - * - * @param addr 设备地址 - * @param mid 消息序号 - * @param data 下发数据 - * @return 编码后的数据包 - */ - public static Buffer createDataDownPackage(String addr, short mid, String data) { - TcpDataPackage dataPackage = TcpDataPackage.builder() - .addr(addr) - .code(TcpDataPackage.CODE_DATA_DOWN) - .mid(mid) - .payload(data) - .build(); - return encode(dataPackage); - } - - /** - * 创建服务调用数据包 - * - * @param addr 设备地址 - * @param mid 消息序号 - * @param serviceData 服务数据 - * @return 编码后的数据包 - */ - public static Buffer createServiceInvokePackage(String addr, short mid, String serviceData) { - TcpDataPackage dataPackage = TcpDataPackage.builder() - .addr(addr) - .code(TcpDataPackage.CODE_SERVICE_INVOKE) - .mid(mid) - .payload(serviceData) - .build(); - return encode(dataPackage); - } - - /** - * 创建属性设置数据包 - * - * @param addr 设备地址 - * @param mid 消息序号 - * @param propertyData 属性数据 - * @return 编码后的数据包 - */ - public static Buffer createPropertySetPackage(String addr, short mid, String propertyData) { - TcpDataPackage dataPackage = TcpDataPackage.builder() - .addr(addr) - .code(TcpDataPackage.CODE_PROPERTY_SET) - .mid(mid) - .payload(propertyData) - .build(); - return encode(dataPackage); - } - - /** - * 创建属性获取数据包 - * - * @param addr 设备地址 - * @param mid 消息序号 - * @param propertyNames 属性名称列表 - * @return 编码后的数据包 - */ - public static Buffer createPropertyGetPackage(String addr, short mid, String propertyNames) { - TcpDataPackage dataPackage = TcpDataPackage.builder() - .addr(addr) - .code(TcpDataPackage.CODE_PROPERTY_GET) - .mid(mid) - .payload(propertyNames) - .build(); - return encode(dataPackage); - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataPackage.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataPackage.java deleted file mode 100644 index c0a7e7185d..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataPackage.java +++ /dev/null @@ -1,160 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * TCP 数据包协议定义 - *

- * 数据包格式: - * 包头(4 字节长度) | 设备地址长度(2 字节) | 设备地址(不定长) | 功能码(2 字节) | 消息序号(2 字节) | 包体(不定长) - * - * @author 芋道源码 - */ -@Data -@AllArgsConstructor -@NoArgsConstructor -@Builder -public class TcpDataPackage { - - // ==================== 功能码定义 ==================== - - /** - * 设备注册 - */ - public static final short CODE_REGISTER = 10; - /** - * 注册回复 - */ - public static final short CODE_REGISTER_REPLY = 11; - // TODO @haohao:【重要】一般心跳,服务端会回复一条;回复要搞独立的 code 码,还是继续用原来的,因为 requestId 可以映射; - /** - * 心跳 - */ - public static final short CODE_HEARTBEAT = 20; - // TODO @haohao:【重要】下面的,是不是融合成消息上行(client -> server),消息下行(server -> client);然后把 method 放到 body 里? - /** - * 数据上报 - */ - public static final short CODE_DATA_UP = 30; - /** - * 事件上报 - */ - public static final short CODE_EVENT_UP = 40; - /** - * 数据下发 - */ - public static final short CODE_DATA_DOWN = 50; - /** - * 服务调用 - */ - public static final short CODE_SERVICE_INVOKE = 60; - /** - * 属性设置 - */ - public static final short CODE_PROPERTY_SET = 70; - /** - * 属性获取 - */ - public static final short CODE_PROPERTY_GET = 80; - - // ==================== 数据包字段 ==================== - - // TODO @haohao:设备 addrLength、addr 是不是非必要呀? - - /** - * 设备地址长度 - */ - private Integer addrLength; - - /** - * 设备地址 - */ - private String addr; - - /** - * 功能码 - */ - private short code; - - /** - * 消息序号 - */ - private short mid; - - /** - * 包体数据 - */ - private String payload; - - // ==================== 辅助方法 ==================== - - // TODO @haohao:用不到的方法,可以清理掉哈; - - /** - * 是否为注册消息 - */ - public boolean isRegisterMessage() { - return code == CODE_REGISTER; - } - - /** - * 是否为心跳消息 - */ - public boolean isHeartbeatMessage() { - return code == CODE_HEARTBEAT; - } - - /** - * 是否为数据上报消息 - */ - public boolean isDataUpMessage() { - return code == CODE_DATA_UP; - } - - /** - * 是否为事件上报消息 - */ - public boolean isEventUpMessage() { - return code == CODE_EVENT_UP; - } - - /** - * 是否为下行消息 - */ - public boolean isDownstreamMessage() { - return code == CODE_DATA_DOWN || code == CODE_SERVICE_INVOKE || - code == CODE_PROPERTY_SET || code == CODE_PROPERTY_GET; - } - - // TODO @haohao:这个是不是去掉呀?多了一些维护成本; - /** - * 获取功能码描述 - */ - public String getCodeDescription() { - switch (code) { - case CODE_REGISTER: - return "设备注册"; - case CODE_REGISTER_REPLY: - return "注册回复"; - case CODE_HEARTBEAT: - return "心跳"; - case CODE_DATA_UP: - return "数据上报"; - case CODE_EVENT_UP: - return "事件上报"; - case CODE_DATA_DOWN: - return "数据下发"; - case CODE_SERVICE_INVOKE: - return "服务调用"; - case CODE_PROPERTY_SET: - return "属性设置"; - case CODE_PROPERTY_GET: - return "属性获取"; - default: - return "未知功能码"; - } - } -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataReader.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataReader.java deleted file mode 100644 index f366418d7e..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataReader.java +++ /dev/null @@ -1,162 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol; - -import io.vertx.core.Handler; -import io.vertx.core.buffer.Buffer; -import io.vertx.core.parsetools.RecordParser; -import lombok.extern.slf4j.Slf4j; - -import java.util.function.Consumer; - -/** - * TCP 数据读取器 - *

- * 负责从 TCP 流中读取完整的数据包 - *

- * 数据包格式: - * 包头(4 字节长度) | 设备地址长度(2 字节) | 设备地址(不定长) | 功能码(2 字节) | 消息序号(2 字节) | 包体(不定长) - * - * @author 芋道源码 - */ -@Slf4j -public class TcpDataReader { - - /** - * 创建数据包解析器 - * - * @param receiveHandler 接收处理器 - * @return RecordParser 解析器 - */ - public static RecordParser createParser(Consumer receiveHandler) { - // 首先读取 4 字节的长度信息 - RecordParser parser = RecordParser.newFixed(4); - - // 设置处理器 - parser.setOutput(new Handler() { - // 当前数据包的长度,-1 表示还没有读取到长度信息 - private int dataLength = -1; - - @Override - public void handle(Buffer buffer) { - try { - // 如果还没有读取到长度信息 - if (dataLength == -1) { - // 从包头中读取数据长度 - dataLength = buffer.getInt(0); - - // 校验数据长度(最大 1 MB) - // TODO @haohao:1m 蛮多地方在写死,最好配置管理下。或者有个全局的枚举; - if (dataLength <= 0 || dataLength > 1024 * 1024) { - log.error("[handle][无效的数据包长度: {}]", dataLength); - reset(); - return; - } - - // 切换到读取数据模式 - parser.fixedSizeMode(dataLength); - - log.debug("[handle][读取到数据包长度: {}]", dataLength); - } else { - // 读取到完整的数据包 - log.debug("[handle][读取到完整数据包,长度: {}]", buffer.length()); - - // 处理数据包 - try { - receiveHandler.accept(buffer); - } catch (Exception e) { - log.error("[handle][处理数据包失败]", e); - } - - // 重置状态,准备读取下一个数据包 - reset(); - } - } catch (Exception e) { - log.error("[handle][数据包处理异常]", e); - reset(); - } - } - - /** - * 重置解析器状态 - */ - private void reset() { - dataLength = -1; - parser.fixedSizeMode(4); - } - }); - - return parser; - } - - // TODO @haohao:用不到的方法,可以清理掉哈; - - /** - * 创建带异常处理的数据包解析器 - * - * @param receiveHandler 接收处理器 - * @param exceptionHandler 异常处理器 - * @return RecordParser 解析器 - */ - public static RecordParser createParserWithExceptionHandler( - Consumer receiveHandler, - Consumer exceptionHandler) { - - RecordParser parser = RecordParser.newFixed(4); - - parser.setOutput(new Handler() { - private int dataLength = -1; - - @Override - public void handle(Buffer buffer) { - try { - if (dataLength == -1) { - dataLength = buffer.getInt(0); - - if (dataLength <= 0 || dataLength > 1024 * 1024) { - throw new IllegalArgumentException("无效的数据包长度: " + dataLength); - } - - parser.fixedSizeMode(dataLength); - log.debug("[handle][读取到数据包长度: {}]", dataLength); - } else { - log.debug("[handle][读取到完整数据包,长度: {}]", buffer.length()); - - try { - receiveHandler.accept(buffer); - } catch (Exception e) { - exceptionHandler.accept(e); - } - - reset(); - } - } catch (Exception e) { - exceptionHandler.accept(e); - reset(); - } - } - - private void reset() { - dataLength = -1; - parser.fixedSizeMode(4); - } - }); - - return parser; - } - - /** - * 创建简单的数据包解析器(用于测试) - * - * @param receiveHandler 接收处理器 - * @return RecordParser 解析器 - */ - public static RecordParser createSimpleParser(Consumer receiveHandler) { - return createParser(buffer -> { - try { - TcpDataPackage dataPackage = TcpDataDecoder.decode(buffer); - receiveHandler.accept(dataPackage); - } catch (Exception e) { - log.error("[createSimpleParser][解码数据包失败]", e); - } - }); - } -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java index 1fcb6a2bb5..919606475b 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java @@ -1,15 +1,8 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router; -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.client.TcpDeviceClient; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.TcpDeviceConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataDecoder; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataEncoder; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataPackage; +import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpDeviceMessageCodec; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import com.alibaba.fastjson.JSON; -import io.vertx.core.buffer.Buffer; import lombok.extern.slf4j.Slf4j; /** @@ -21,20 +14,21 @@ import lombok.extern.slf4j.Slf4j; * 3. 属性获取 * 4. 配置下发 * 5. OTA 升级 + *

+ * 注意:由于移除了连接管理器,此处理器主要负责消息的编码和日志记录 * * @author 芋道源码 */ @Slf4j public class IotTcpDownstreamHandler { - private final TcpDeviceConnectionManager connectionManager; - private final IotDeviceMessageService messageService; - public IotTcpDownstreamHandler(TcpDeviceConnectionManager connectionManager, - IotDeviceMessageService messageService) { - this.connectionManager = connectionManager; + private final IotTcpDeviceMessageCodec codec; + + public IotTcpDownstreamHandler(IotDeviceMessageService messageService) { this.messageService = messageService; + this.codec = new IotTcpDeviceMessageCodec(); } /** @@ -47,315 +41,19 @@ public class IotTcpDownstreamHandler { log.info("[handle][处理下行消息] 设备ID: {}, 方法: {}, 消息ID: {}", message.getDeviceId(), message.getMethod(), message.getId()); - // 1. 获取设备连接 - TcpDeviceClient client = connectionManager.getClientByDeviceId(message.getDeviceId()); - if (client == null || !client.isOnline()) { - log.error("[handle][设备({})不在线,无法发送下行消息]", message.getDeviceId()); - return; - } + // 编码消息用于日志记录和验证 + byte[] encodedMessage = codec.encode(message); + log.debug("[handle][消息编码成功] 设备ID: {}, 编码后长度: {} 字节", + message.getDeviceId(), encodedMessage.length); + + // 记录下行消息处理 + log.info("[handle][下行消息处理完成] 设备ID: {}, 方法: {}, 消息内容: {}", + message.getDeviceId(), message.getMethod(), message.getParams()); - // 2. 根据消息方法处理不同类型的下行消息 - // TODO @芋艿、@haohao:看看有没什么办法,减少这样的编码。拓展新消息类型,成本高; - switch (message.getMethod()) { - case "thing.property.set": - handlePropertySet(client, message); - break; - case "thing.property.get": - handlePropertyGet(client, message); - break; - case "thing.service.invoke": - handleServiceInvoke(client, message); - break; - case "thing.config.push": - handleConfigPush(client, message); - break; - case "thing.ota.upgrade": - handleOtaUpgrade(client, message); - break; - default: - log.warn("[handle][未知的下行消息方法: {}]", message.getMethod()); - break; - } } catch (Exception e) { - // TODO @haohao:最好消息的内容,打印下; - log.error("[handle][处理下行消息失败]", e); + log.error("[handle][处理下行消息失败] 设备ID: {}, 方法: {}, 消息内容: {}", + message.getDeviceId(), message.getMethod(), message.getParams(), e); } } - /** - * 处理属性设置 - * - * @param client 设备客户端 - * @param message 设备消息 - */ - private void handlePropertySet(TcpDeviceClient client, IotDeviceMessage message) { - try { - log.info("[handlePropertySet][属性设置] 设备地址: {}, 属性: {}", - client.getDeviceAddr(), message.getParams()); - - // 使用编解码器发送消息,降级处理使用原始编码 - sendMessageWithCodec(client, message, "handlePropertySet", () -> { - String payload = JSON.toJSONString(message.getParams()); - short mid = generateMessageId(); - - Buffer buffer = TcpDataEncoder.createPropertySetPackage( - client.getDeviceAddr(), mid, payload); - client.sendMessage(buffer); - - log.debug("[handlePropertySet][属性设置消息已发送(降级)] 设备地址: {}, 消息序号: {}", - client.getDeviceAddr(), mid); - }); - } catch (Exception e) { - log.error("[handlePropertySet][属性设置失败]", e); - } - } - - /** - * 处理属性获取 - * - * @param client 设备客户端 - * @param message 设备消息 - */ - private void handlePropertyGet(TcpDeviceClient client, IotDeviceMessage message) { - try { - log.info("[handlePropertyGet][属性获取] 设备地址: {}, 属性列表: {}", - client.getDeviceAddr(), message.getParams()); - - // 使用编解码器发送消息,降级处理使用原始编码 - sendMessageWithCodec(client, message, "handlePropertyGet", () -> { - String payload = JSON.toJSONString(message.getParams()); - short mid = generateMessageId(); - - Buffer buffer = TcpDataEncoder.createPropertyGetPackage( - client.getDeviceAddr(), mid, payload); - client.sendMessage(buffer); - - log.debug("[handlePropertyGet][属性获取消息已发送(降级)] 设备地址: {}, 消息序号: {}", - client.getDeviceAddr(), mid); - }); - } catch (Exception e) { - log.error("[handlePropertyGet][属性获取失败]", e); - } - } - - /** - * 处理服务调用 - * - * @param client 设备客户端 - * @param message 设备消息 - */ - private void handleServiceInvoke(TcpDeviceClient client, IotDeviceMessage message) { - try { - log.info("[handleServiceInvoke][服务调用] 设备地址: {}, 服务参数: {}", - client.getDeviceAddr(), message.getParams()); - - // 1. 构建服务调用数据包 - String payload = JSON.toJSONString(message.getParams()); - short mid = generateMessageId(); - - Buffer buffer = TcpDataEncoder.createServiceInvokePackage( - client.getDeviceAddr(), mid, payload); - - // 2. 发送消息 - client.sendMessage(buffer); - - log.debug("[handleServiceInvoke][服务调用消息已发送] 设备地址: {}, 消息序号: {}", - client.getDeviceAddr(), mid); - } catch (Exception e) { - log.error("[handleServiceInvoke][服务调用失败]", e); - } - } - - /** - * 处理配置推送 - * - * @param client 设备客户端 - * @param message 设备消息 - */ - private void handleConfigPush(TcpDeviceClient client, IotDeviceMessage message) { - try { - log.info("[handleConfigPush][配置推送] 设备地址: {}, 配置: {}", - client.getDeviceAddr(), message.getParams()); - - // 1. 构建配置推送数据包 - String payload = JSON.toJSONString(message.getParams()); - short mid = generateMessageId(); - - Buffer buffer = TcpDataEncoder.createDataDownPackage( - client.getDeviceAddr(), mid, payload); - - // 2. 发送消息 - client.sendMessage(buffer); - - log.debug("[handleConfigPush][配置推送消息已发送] 设备地址: {}, 消息序号: {}", - client.getDeviceAddr(), mid); - } catch (Exception e) { - log.error("[handleConfigPush][配置推送失败]", e); - } - } - - /** - * 处理 OTA 升级 - * - * @param client 设备客户端 - * @param message 设备消息 - */ - private void handleOtaUpgrade(TcpDeviceClient client, IotDeviceMessage message) { - try { - log.info("[handleOtaUpgrade][OTA升级] 设备地址: {}, 升级信息: {}", - client.getDeviceAddr(), message.getParams()); - - // 1. 构建 OTA 升级数据包 - String payload = JSON.toJSONString(message.getParams()); - short mid = generateMessageId(); - - Buffer buffer = TcpDataEncoder.createDataDownPackage( - client.getDeviceAddr(), mid, payload); - - // 2. 发送消息 - client.sendMessage(buffer); - - log.debug("[handleOtaUpgrade][OTA升级消息已发送] 设备地址: {}, 消息序号: {}", - client.getDeviceAddr(), mid); - - } catch (Exception e) { - log.error("[handleOtaUpgrade][OTA升级失败]", e); - } - } - - /** - * 处理自定义下行消息 - * - * @param client 设备客户端 - * @param message 设备消息 - * @param code 功能码 - */ - private void handleCustomMessage(TcpDeviceClient client, IotDeviceMessage message, short code) { - try { - log.info("[handleCustomMessage][自定义消息] 设备地址: {}, 功能码: {}, 数据: {}", - client.getDeviceAddr(), code, message.getParams()); - - // 1. 构建自定义数据包 - String payload = JSON.toJSONString(message.getParams()); - short mid = generateMessageId(); - - TcpDataPackage dataPackage = TcpDataPackage.builder() - .addr(client.getDeviceAddr()) - .code(code) - .mid(mid) - .payload(payload) - .build(); - - Buffer buffer = TcpDataEncoder.encode(dataPackage); - - // 2. 发送消息 - client.sendMessage(buffer); - - log.debug("[handleCustomMessage][自定义消息已发送] 设备地址: {}, 功能码: {}, 消息序号: {}", - client.getDeviceAddr(), code, mid); - - } catch (Exception e) { - log.error("[handleCustomMessage][自定义消息发送失败]", e); - } - } - - // TODO @haohao:用不到的,要不暂时不提供; - /** - * 批量发送下行消息 - * - * @param deviceIds 设备ID列表 - * @param message 设备消息 - */ - public void broadcastMessage(Long[] deviceIds, IotDeviceMessage message) { - try { - log.info("[broadcastMessage][批量发送消息] 设备数量: {}, 方法: {}", - deviceIds.length, message.getMethod()); - - for (Long deviceId : deviceIds) { - // 创建副本消息(避免消息ID冲突) - IotDeviceMessage copyMessage = IotDeviceMessage.of( - message.getRequestId(), - message.getMethod(), - message.getParams(), - message.getData(), - message.getCode(), - message.getMsg()); - copyMessage.setDeviceId(deviceId); - - // 处理单个设备消息 - handle(copyMessage); - } - } catch (Exception e) { - log.error("[broadcastMessage][批量发送消息失败]", e); - } - } - - /** - * 检查设备是否支持指定方法 - * - * @param client 设备客户端 - * @param method 消息方法 - * @return 是否支持 - */ - private boolean isMethodSupported(TcpDeviceClient client, String method) { - // TODO: 可以根据设备类型或产品信息判断是否支持特定方法 - return IotDeviceMessageMethodEnum.of(method) != null; - } - - /** - * 生成消息序号 - * - * @return 消息序号 - */ - private short generateMessageId() { - return (short) (System.currentTimeMillis() % Short.MAX_VALUE); - } - - /** - * 使用编解码器发送消息 - * - * @param client 设备客户端 - * @param message 设备消息 - * @param methodName 方法名称 - * @param fallbackAction 降级处理逻辑 - */ - private void sendMessageWithCodec(TcpDeviceClient client, IotDeviceMessage message, - String methodName, Runnable fallbackAction) { - try { - // 1. 使用编解码器编码消息 - byte[] messageBytes = messageService.encodeDeviceMessage( - message, client.getProductKey(), client.getDeviceName()); - - // 2. 解析编码后的数据包并设置设备地址和消息序号 - Buffer buffer = Buffer.buffer(messageBytes); - TcpDataPackage dataPackage = TcpDataDecoder.decode(buffer); - dataPackage.setAddr(client.getDeviceAddr()); - dataPackage.setMid(generateMessageId()); - - // 3. 重新编码并发送 - Buffer finalBuffer = TcpDataEncoder.encode(dataPackage); - client.sendMessage(finalBuffer); - - log.debug("[{}][消息已发送] 设备地址: {}, 消息序号: {}", - methodName, client.getDeviceAddr(), dataPackage.getMid()); - } catch (Exception e) { - log.warn("[{}][使用编解码器编码失败,降级使用原始编码] 错误: {}", - methodName, e.getMessage()); - - // 执行降级处理 - if (fallbackAction != null) { - fallbackAction.run(); - } - } - } - - // TODO @haohao:看看这个要不要删除掉 - /** - * 获取连接统计信息 - * - * @return 连接统计信息 - */ - public String getHandlerStatistics() { - return String.format("TCP下游处理器 - %s", connectionManager.getConnectionStatus()); - } } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java index 672de2ad2c..b57cceb9ec 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java @@ -1,385 +1,110 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router; import cn.hutool.core.util.IdUtil; -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; -import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.client.TcpDeviceClient; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.TcpDeviceConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataDecoder; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataEncoder; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataPackage; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataReader; -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpCodecManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import io.vertx.core.Handler; import io.vertx.core.buffer.Buffer; import io.vertx.core.net.NetSocket; import io.vertx.core.parsetools.RecordParser; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; /** * IoT 网关 TCP 上行消息处理器 - *

- * 核心负责: - * 1. 【设备注册】设备连接后发送注册消息,注册成功后可以进行通信 - * 2. 【心跳处理】定期接收设备心跳消息,维持连接状态 - * 3. 【数据上报】接收设备数据上报和事件上报 - * 4. 【连接管理】管理连接的建立、维护和清理 * * @author 芋道源码 */ -@RequiredArgsConstructor @Slf4j public class IotTcpUpstreamHandler implements Handler { - private final IotGatewayProperties.TcpProperties tcpConfig; - - // TODO @haohao:可以把 TcpDeviceConnectionManager 能力放大一点:1)handle 里的 client 初始化,可以拿到 TcpDeviceConnectionManager 里;2)handleDeviceRegister 也是; - private final TcpDeviceConnectionManager connectionManager; - - private final IotDeviceService deviceService; - - private final IotDeviceMessageService messageService; - - private final IotDeviceCommonApi deviceApi; + private final IotDeviceMessageService deviceMessageService; private final String serverId; + private final IotTcpCodecManager codecManager; + + public IotTcpUpstreamHandler(IotTcpUpstreamProtocol protocol, IotDeviceMessageService deviceMessageService, + IotTcpCodecManager codecManager) { + this.deviceMessageService = deviceMessageService; + this.serverId = protocol.getServerId(); + this.codecManager = codecManager; + } + @Override public void handle(NetSocket socket) { - log.info("[handle][收到设备连接: {}]", socket.remoteAddress()); + // 生成客户端ID用于日志标识 + String clientId = IdUtil.simpleUUID(); + log.info("[handle][收到设备连接] clientId: {}, address: {}", clientId, socket.remoteAddress()); - // 创建客户端 ID 和设备客户端 - // TODO @haohao:clientid 给 TcpDeviceClient 生成会简洁一点;减少 upsteramhanlder 的非核心逻辑; - String clientId = IdUtil.simpleUUID() + "_" + socket.remoteAddress(); - TcpDeviceClient client = new TcpDeviceClient(clientId, tcpConfig.getKeepAliveTimeoutMs()); + // 设置解析器 + RecordParser parser = RecordParser.newFixed(1024, buffer -> { + try { + handleDataPackage(clientId, buffer); + } catch (Exception e) { + log.error("[handle][处理数据包异常] clientId: {}", clientId, e); + } + }); - try { - // 设置连接异常和关闭处理 - socket.exceptionHandler(ex -> { - // TODO @haohao:这里的日志,可能把 clientid 都打上?因为 address 会重复么? - log.error("[handle][连接({})异常]", socket.remoteAddress(), ex); - handleConnectionClose(client); - }); - socket.closeHandler(v -> { - log.info("[handle][连接({})关闭]", socket.remoteAddress()); - handleConnectionClose(client); - }); - client.setSocket(socket); + // 设置异常处理 + socket.exceptionHandler(ex -> { + log.error("[handle][连接异常] clientId: {}, address: {}", clientId, socket.remoteAddress(), ex); + }); - // 设置解析器 - RecordParser parser = TcpDataReader.createParser(buffer -> { - try { - handleDataPackage(client, buffer); - } catch (Exception e) { - log.error("[handle][处理数据包异常]", e); - } - }); - client.setParser(parser); + socket.closeHandler(v -> { + log.info("[handle][连接关闭] clientId: {}, address: {}", clientId, socket.remoteAddress()); + }); - // TODO @haohao:socket.remoteAddress()) 打印进去 - log.info("[handle][设备连接处理器初始化完成: {}]", clientId); - } catch (Exception e) { - // TODO @haohao:socket.remoteAddress()) 打印进去 - log.error("[handle][初始化连接处理器失败]", e); - client.shutdown(); - } + // 设置数据处理器 + socket.handler(parser); } /** * 处理数据包 - * - * @param client 设备客户端 - * @param buffer 数据缓冲区 */ - private void handleDataPackage(TcpDeviceClient client, io.vertx.core.buffer.Buffer buffer) { + private void handleDataPackage(String clientId, Buffer buffer) { try { - // 解码数据包 - TcpDataPackage dataPackage = TcpDataDecoder.decode(buffer); - log.info("[handleDataPackage][接收数据包] 设备地址: {}, 功能码: {}, 消息序号: {}", - dataPackage.getAddr(), dataPackage.getCodeDescription(), dataPackage.getMid()); + // 使用编解码器管理器自动检测协议并解码消息 + IotDeviceMessage message = codecManager.decode(buffer.getBytes()); + log.info("[handleDataPackage][接收数据包] clientId: {}, 方法: {}, 设备ID: {}", + clientId, message.getMethod(), message.getDeviceId()); - // 根据功能码处理不同类型的消息 - switch (dataPackage.getCode()) { - // TODO @haohao:【重要】code 要不要改成 opCode。这样和 data 里的 code 好区分; - case TcpDataPackage.CODE_REGISTER: - handleDeviceRegister(client, dataPackage); - break; - case TcpDataPackage.CODE_HEARTBEAT: - handleHeartbeat(client, dataPackage); - break; - case TcpDataPackage.CODE_DATA_UP: - handleDataUp(client, dataPackage); - break; - case TcpDataPackage.CODE_EVENT_UP: - handleEventUp(client, dataPackage); - break; - default: - log.warn("[handleDataPackage][未知功能码: {}]", dataPackage.getCode()); - break; - } + // 处理上行消息 + handleUpstreamMessage(clientId, message); } catch (Exception e) { - // TODO @haohao:最好有 client 标识; - log.error("[handleDataPackage][处理数据包失败]", e); + log.error("[handleDataPackage][处理数据包失败] clientId: {}", clientId, e); } } /** - * 处理设备注册 - * - * @param client 设备客户端 - * @param dataPackage 数据包 + * 处理上行消息 */ - private void handleDeviceRegister(TcpDeviceClient client, TcpDataPackage dataPackage) { + private void handleUpstreamMessage(String clientId, IotDeviceMessage message) { try { - String deviceAddr = dataPackage.getAddr(); - String productKey = dataPackage.getPayload(); - log.info("[handleDeviceRegister][设备注册] 设备地址: {}, 产品密钥: {}", deviceAddr, productKey); + log.info("[handleUpstreamMessage][上行消息] clientId: {}, 方法: {}, 设备ID: {}", + clientId, message.getMethod(), message.getDeviceId()); - // 获取设备信息 - IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, deviceAddr); - if (device == null) { - log.error("[handleDeviceRegister][设备不存在: {} - {}]", productKey, deviceAddr); - sendRegisterReply(client, dataPackage, false); - return; - } + // 解析设备信息(简化处理) + String deviceId = String.valueOf(message.getDeviceId()); + String productKey = extractProductKey(deviceId); + String deviceName = deviceId; - // 更新客户端信息 - // TODO @haohao:一个 set 方法,统一处理掉会好点哈; - client.setProductKey(productKey); - client.setDeviceName(deviceAddr); - client.setDeviceId(device.getId()); - client.setAuthenticated(true); - - // 添加到连接管理器 - connectionManager.addClient(deviceAddr, client); - connectionManager.setDeviceIdMapping(deviceAddr, device.getId()); - - // 发送设备上线消息 - IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); - messageService.sendDeviceMessage(onlineMessage, productKey, deviceAddr, serverId); - - // 发送注册成功回复 - sendRegisterReply(client, dataPackage, true); - - log.info("[handleDeviceRegister][设备注册成功] 设备地址: {}, 设备ID: {}", deviceAddr, device.getId()); + // 发送消息到队列 + deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId); } catch (Exception e) { - log.error("[handleDeviceRegister][设备注册失败]", e); - sendRegisterReply(client, dataPackage, false); + log.error("[handleUpstreamMessage][处理上行消息失败] clientId: {}", clientId, e); } } /** - * 处理心跳 - * - * @param client 设备客户端 - * @param dataPackage 数据包 + * 从设备ID中提取产品密钥(简化实现) */ - private void handleHeartbeat(TcpDeviceClient client, TcpDataPackage dataPackage) { - try { - String deviceAddr = dataPackage.getAddr(); - log.debug("[handleHeartbeat][收到心跳] 设备地址: {}", deviceAddr); - - // 更新心跳时间 - client.keepAlive(); - - // 发送心跳回复(可选) - // sendHeartbeatReply(client, dataPackage); - - } catch (Exception e) { - log.error("[handleHeartbeat][处理心跳失败]", e); - } - } - - /** - * 处理数据上报 - * - * @param client 设备客户端 - * @param dataPackage 数据包 - */ - private void handleDataUp(TcpDeviceClient client, TcpDataPackage dataPackage) { - try { - String deviceAddr = dataPackage.getAddr(); - String payload = dataPackage.getPayload(); - - log.info("[handleDataUp][数据上报] 设备地址: {}, 数据: {}", deviceAddr, payload); - - // 检查设备是否已认证 - if (!client.isAuthenticated()) { - log.warn("[handleDataUp][设备未认证,忽略数据上报: {}]", deviceAddr); - return; - } - - // 使用 IotDeviceMessageService 解码消息 - try { - // 1. 将 TCP 数据包重新编码为字节数组 - Buffer buffer = TcpDataEncoder.encode(dataPackage); - byte[] messageBytes = buffer.getBytes(); - - // 2. 使用 messageService 解码消息 - IotDeviceMessage message = messageService.decodeDeviceMessage( - messageBytes, client.getProductKey(), client.getDeviceName()); - - // 3. 发送解码后的消息 - messageService.sendDeviceMessage(message, client.getProductKey(), client.getDeviceName(), serverId); - } catch (Exception e) { - log.warn("[handleDataUp][使用编解码器解码失败,降级使用原始解析] 错误: {}", e.getMessage()); - - // 降级处理:使用原始方式解析数据 - JSONObject dataJson = JSONUtil.parseObj(payload); - IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", dataJson); - messageService.sendDeviceMessage(message, client.getProductKey(), client.getDeviceName(), serverId); - } - - // 发送数据上报回复 - sendDataUpReply(client, dataPackage); - } catch (Exception e) { - log.error("[handleDataUp][处理数据上报失败]", e); - } - } - - /** - * 处理事件上报 - * - * @param client 设备客户端 - * @param dataPackage 数据包 - */ - private void handleEventUp(TcpDeviceClient client, TcpDataPackage dataPackage) { - try { - String deviceAddr = dataPackage.getAddr(); - String payload = dataPackage.getPayload(); - - log.info("[handleEventUp][事件上报] 设备地址: {}, 数据: {}", deviceAddr, payload); - - // 检查设备是否已认证 - if (!client.isAuthenticated()) { - log.warn("[handleEventUp][设备未认证,忽略事件上报: {}]", deviceAddr); - return; - } - - // 使用 IotDeviceMessageService 解码消息 - try { - // 1. 将 TCP 数据包重新编码为字节数组 - Buffer buffer = TcpDataEncoder.encode(dataPackage); - byte[] messageBytes = buffer.getBytes(); - - // 2. 使用 messageService 解码消息 - IotDeviceMessage message = messageService.decodeDeviceMessage( - messageBytes, client.getProductKey(), client.getDeviceName()); - - // 3. 发送解码后的消息 - messageService.sendDeviceMessage(message, client.getProductKey(), client.getDeviceName(), serverId); - } catch (Exception e) { - log.warn("[handleEventUp][使用编解码器解码失败,降级使用原始解析] 错误: {}", e.getMessage()); - - // 降级处理:使用原始方式解析数据 - // TODO @芋艿:降级处理逻辑; - JSONObject eventJson = JSONUtil.parseObj(payload); - IotDeviceMessage message = IotDeviceMessage.requestOf("thing.event.post", eventJson); - messageService.sendDeviceMessage(message, client.getProductKey(), client.getDeviceName(), serverId); - } - - // 发送事件上报回复 - sendEventUpReply(client, dataPackage); - } catch (Exception e) { - log.error("[handleEventUp][处理事件上报失败]", e); - } - } - - /** - * 发送注册回复 - * - * @param client 设备客户端 - * @param dataPackage 原始数据包 - * @param success 是否成功 - */ - private void sendRegisterReply(TcpDeviceClient client, TcpDataPackage dataPackage, boolean success) { - try { - io.vertx.core.buffer.Buffer replyBuffer = TcpDataEncoder.createRegisterReply( - dataPackage.getAddr(), dataPackage.getMid(), success); - client.sendMessage(replyBuffer); - - log.debug("[sendRegisterReply][发送注册回复] 设备地址: {}, 结果: {}", - dataPackage.getAddr(), success ? "成功" : "失败"); - } catch (Exception e) { - log.error("[sendRegisterReply][发送注册回复失败]", e); - } - } - - /** - * 发送数据上报回复 - * - * @param client 设备客户端 - * @param dataPackage 原始数据包 - */ - private void sendDataUpReply(TcpDeviceClient client, TcpDataPackage dataPackage) { - try { - TcpDataPackage replyPackage = TcpDataPackage.builder() - .addr(dataPackage.getAddr()) - .code(TcpDataPackage.CODE_DATA_UP) - .mid(dataPackage.getMid()) - .payload("0") // 0 表示成功 TODO @haohao:最好枚举到 TcpDataPackage 里? - .build(); - - io.vertx.core.buffer.Buffer replyBuffer = TcpDataEncoder.encode(replyPackage); - client.sendMessage(replyBuffer); - } catch (Exception e) { - // TODO @haohao:可以有个 client id - log.error("[sendDataUpReply][发送数据上报回复失败]", e); - } - } - - /** - * 发送事件上报回复 - * - * @param client 设备客户端 - * @param dataPackage 原始数据包 - */ - private void sendEventUpReply(TcpDeviceClient client, TcpDataPackage dataPackage) { - try { - TcpDataPackage replyPackage = TcpDataPackage.builder() - .addr(dataPackage.getAddr()) - .code(TcpDataPackage.CODE_EVENT_UP) - .mid(dataPackage.getMid()) - .payload("0") // 0 表示成功 - .build(); - - io.vertx.core.buffer.Buffer replyBuffer = TcpDataEncoder.encode(replyPackage); - client.sendMessage(replyBuffer); - } catch (Exception e) { - log.error("[sendEventUpReply][发送事件上报回复失败]", e); - } - } - - /** - * 处理连接关闭 - * - * @param client 设备客户端 - */ - private void handleConnectionClose(TcpDeviceClient client) { - try { - String deviceAddr = client.getDeviceAddr(); - - // 发送设备离线消息 - if (client.isAuthenticated()) { - IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); - messageService.sendDeviceMessage(offlineMessage, - client.getProductKey(), client.getDeviceName(), serverId); - } - - // 从连接管理器移除 - if (deviceAddr != null) { - connectionManager.removeClient(deviceAddr); - } - - log.info("[handleConnectionClose][处理连接关闭完成] 设备地址: {}", deviceAddr); - } catch (Exception e) { - log.error("[handleConnectionClose][处理连接关闭失败]", e); + private String extractProductKey(String deviceId) { + // 简化实现:假设设备ID格式为 "productKey_deviceName" + if (deviceId != null && deviceId.contains("_")) { + return deviceId.split("_")[0]; } + return "default_product"; } } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpBinaryDataPacketExamples.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpBinaryDataPacketExamples.java new file mode 100644 index 0000000000..56926569ce --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpBinaryDataPacketExamples.java @@ -0,0 +1,219 @@ +package cn.iocoder.yudao.module.iot.gateway.codec.tcp; + +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import lombok.extern.slf4j.Slf4j; + +import java.util.HashMap; +import java.util.Map; + +/** + * TCP二进制格式数据包示例 + * + * 演示如何使用二进制协议创建和解析TCP上报数据包和心跳包 + * + * 二进制协议格式: + * 包头(4字节) | 地址长度(2字节) | 设备地址(变长) | 功能码(2字节) | 消息序号(2字节) | 包体数据(变长) + * + * @author 芋道源码 + */ +@Slf4j +public class TcpBinaryDataPacketExamples { + + public static void main(String[] args) { + IotTcpBinaryDeviceMessageCodec codec = new IotTcpBinaryDeviceMessageCodec(); + + // 1. 数据上报包示例 + demonstrateDataReport(codec); + + // 2. 心跳包示例 + demonstrateHeartbeat(codec); + + // 3. 复杂数据上报示例 + demonstrateComplexDataReport(codec); + } + + /** + * 演示二进制格式数据上报包 + */ + private static void demonstrateDataReport(IotTcpBinaryDeviceMessageCodec codec) { + log.info("=== 二进制格式数据上报包示例 ==="); + + // 创建传感器数据 + Map sensorData = new HashMap<>(); + sensorData.put("temperature", 25.5); + sensorData.put("humidity", 60.2); + sensorData.put("pressure", 1013.25); + sensorData.put("battery", 85); + + // 创建设备消息 + IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", sensorData); + message.setDeviceId(123456L); + + // 编码 + byte[] packet = codec.encode(message); + log.info("编码后数据包长度: {} 字节", packet.length); + log.info("编码后数据包(HEX): {}", bytesToHex(packet)); + + // 解码验证 + IotDeviceMessage decoded = codec.decode(packet); + log.info("解码后消息ID: {}", decoded.getId()); + log.info("解码后请求ID: {}", decoded.getRequestId()); + log.info("解码后方法: {}", decoded.getMethod()); + log.info("解码后设备ID: {}", decoded.getDeviceId()); + log.info("解码后服务ID: {}", decoded.getServerId()); + log.info("解码后上报时间: {}", decoded.getReportTime()); + log.info("解码后参数: {}", decoded.getParams()); + + System.out.println(); + } + + /** + * 演示二进制格式心跳包 + */ + private static void demonstrateHeartbeat(IotTcpBinaryDeviceMessageCodec codec) { + log.info("=== 二进制格式心跳包示例 ==="); + + // 创建心跳消息 + IotDeviceMessage heartbeat = IotDeviceMessage.requestOf("thing.state.online", null); + heartbeat.setDeviceId(123456L); + + // 编码 + byte[] packet = codec.encode(heartbeat); + log.info("心跳包长度: {} 字节", packet.length); + log.info("心跳包(HEX): {}", bytesToHex(packet)); + + // 解码验证 + IotDeviceMessage decoded = codec.decode(packet); + log.info("解码后消息ID: {}", decoded.getId()); + log.info("解码后请求ID: {}", decoded.getRequestId()); + log.info("解码后方法: {}", decoded.getMethod()); + log.info("解码后设备ID: {}", decoded.getDeviceId()); + log.info("解码后服务ID: {}", decoded.getServerId()); + log.info("解码后参数: {}", decoded.getParams()); + + System.out.println(); + } + + /** + * 演示二进制格式复杂数据上报 + */ + private static void demonstrateComplexDataReport(IotTcpBinaryDeviceMessageCodec codec) { + log.info("=== 二进制格式复杂数据上报示例 ==="); + + // 创建复杂设备数据 + Map deviceData = new HashMap<>(); + + // 环境数据 + Map environment = new HashMap<>(); + environment.put("temperature", 23.8); + environment.put("humidity", 55.0); + environment.put("co2", 420); + deviceData.put("environment", environment); + + // GPS数据 + Map location = new HashMap<>(); + location.put("latitude", 39.9042); + location.put("longitude", 116.4074); + location.put("altitude", 43.5); + deviceData.put("location", location); + + // 设备状态 + Map status = new HashMap<>(); + status.put("battery", 78); + status.put("signal", -65); + status.put("online", true); + deviceData.put("status", status); + + // 创建设备消息 + IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", deviceData); + message.setDeviceId(789012L); + + // 编码 + byte[] packet = codec.encode(message); + log.info("复杂数据包长度: {} 字节", packet.length); + log.info("复杂数据包(HEX): {}", bytesToHex(packet)); + + // 解码验证 + IotDeviceMessage decoded = codec.decode(packet); + log.info("解码后消息ID: {}", decoded.getId()); + log.info("解码后请求ID: {}", decoded.getRequestId()); + log.info("解码后方法: {}", decoded.getMethod()); + log.info("解码后设备ID: {}", decoded.getDeviceId()); + log.info("解码后服务ID: {}", decoded.getServerId()); + log.info("解码后参数: {}", decoded.getParams()); + + System.out.println(); + } + + /** + * 字节数组转十六进制字符串 + */ + private static String bytesToHex(byte[] bytes) { + StringBuilder result = new StringBuilder(); + for (byte b : bytes) { + result.append(String.format("%02X ", b)); + } + return result.toString().trim(); + } + + /** + * 演示数据包结构分析 + */ + public static void analyzePacketStructure(byte[] packet) { + if (packet.length < 8) { + log.error("数据包长度不足"); + return; + } + + int index = 0; + + // 解析包头(4字节) - 后续数据长度 + int totalLength = ((packet[index] & 0xFF) << 24) | + ((packet[index + 1] & 0xFF) << 16) | + ((packet[index + 2] & 0xFF) << 8) | + (packet[index + 3] & 0xFF); + index += 4; + log.info("包头 - 后续数据长度: {} 字节", totalLength); + + // 解析设备地址长度(2字节) + int addrLength = ((packet[index] & 0xFF) << 8) | (packet[index + 1] & 0xFF); + index += 2; + log.info("设备地址长度: {} 字节", addrLength); + + // 解析设备地址 + String deviceAddr = new String(packet, index, addrLength); + index += addrLength; + log.info("设备地址: {}", deviceAddr); + + // 解析功能码(2字节) + int functionCode = ((packet[index] & 0xFF) << 8) | (packet[index + 1] & 0xFF); + index += 2; + log.info("功能码: {} ({})", functionCode, getFunctionCodeName(functionCode)); + + // 解析消息序号(2字节) + int messageId = ((packet[index] & 0xFF) << 8) | (packet[index + 1] & 0xFF); + index += 2; + log.info("消息序号: {}", messageId); + + // 解析包体数据 + if (index < packet.length) { + String payload = new String(packet, index, packet.length - index); + log.info("包体数据: {}", payload); + } + } + + /** + * 获取功能码名称 + */ + private static String getFunctionCodeName(int code) { + switch (code) { + case 10: return "设备注册"; + case 11: return "注册回复"; + case 20: return "心跳请求"; + case 21: return "心跳回复"; + case 30: return "消息上行"; + case 40: return "消息下行"; + default: return "未知功能码"; + } + } +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpJsonDataPacketExamples.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpJsonDataPacketExamples.java new file mode 100644 index 0000000000..d53731fe9a --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpJsonDataPacketExamples.java @@ -0,0 +1,253 @@ +package cn.iocoder.yudao.module.iot.gateway.codec.tcp; + +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import lombok.extern.slf4j.Slf4j; + +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +/** + * TCP JSON格式数据包示例 + * + * 演示如何使用新的JSON格式进行TCP消息编解码 + * + * @author 芋道源码 + */ +@Slf4j +public class TcpJsonDataPacketExamples { + + public static void main(String[] args) { + IotTcpJsonDeviceMessageCodec codec = new IotTcpJsonDeviceMessageCodec(); + + // 1. 数据上报示例 + demonstrateDataReport(codec); + + // 2. 心跳示例 + demonstrateHeartbeat(codec); + + // 3. 事件上报示例 + demonstrateEventReport(codec); + + // 4. 复杂数据上报示例 + demonstrateComplexDataReport(codec); + + // 5. 便捷方法示例 + demonstrateConvenienceMethods(); + + // 6. EMQX兼容性示例 + demonstrateEmqxCompatibility(); + } + + /** + * 演示数据上报 + */ + private static void demonstrateDataReport(IotTcpJsonDeviceMessageCodec codec) { + log.info("=== JSON格式数据上报示例 ==="); + + // 创建传感器数据 + Map sensorData = new HashMap<>(); + sensorData.put("temperature", 25.5); + sensorData.put("humidity", 60.2); + sensorData.put("pressure", 1013.25); + sensorData.put("battery", 85); + + // 创建设备消息 + IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", sensorData); + message.setDeviceId(123456L); + + // 编码 + byte[] packet = codec.encode(message); + String jsonString = new String(packet, StandardCharsets.UTF_8); + log.info("编码后JSON: {}", jsonString); + log.info("数据包长度: {} 字节", packet.length); + + // 解码验证 + IotDeviceMessage decoded = codec.decode(packet); + log.info("解码后消息ID: {}", decoded.getId()); + log.info("解码后方法: {}", decoded.getMethod()); + log.info("解码后设备ID: {}", decoded.getDeviceId()); + log.info("解码后服务ID: {}", decoded.getServerId()); + log.info("解码后参数: {}", decoded.getParams()); + + System.out.println(); + } + + /** + * 演示心跳 + */ + private static void demonstrateHeartbeat(IotTcpJsonDeviceMessageCodec codec) { + log.info("=== JSON格式心跳示例 ==="); + + // 创建心跳消息 + IotDeviceMessage heartbeat = IotDeviceMessage.requestOf("thing.state.online", null); + heartbeat.setDeviceId(123456L); + + // 编码 + byte[] packet = codec.encode(heartbeat); + String jsonString = new String(packet, StandardCharsets.UTF_8); + log.info("编码后JSON: {}", jsonString); + log.info("心跳包长度: {} 字节", packet.length); + + // 解码验证 + IotDeviceMessage decoded = codec.decode(packet); + log.info("解码后消息ID: {}", decoded.getId()); + log.info("解码后方法: {}", decoded.getMethod()); + log.info("解码后设备ID: {}", decoded.getDeviceId()); + log.info("解码后服务ID: {}", decoded.getServerId()); + + System.out.println(); + } + + /** + * 演示事件上报 + */ + private static void demonstrateEventReport(IotTcpJsonDeviceMessageCodec codec) { + log.info("=== JSON格式事件上报示例 ==="); + + // 创建事件数据 + Map eventData = new HashMap<>(); + eventData.put("eventType", "alarm"); + eventData.put("level", "warning"); + eventData.put("description", "温度过高"); + eventData.put("value", 45.8); + + // 创建事件消息 + IotDeviceMessage event = IotDeviceMessage.requestOf("thing.event.post", eventData); + event.setDeviceId(123456L); + + // 编码 + byte[] packet = codec.encode(event); + String jsonString = new String(packet, StandardCharsets.UTF_8); + log.info("编码后JSON: {}", jsonString); + log.info("事件包长度: {} 字节", packet.length); + + // 解码验证 + IotDeviceMessage decoded = codec.decode(packet); + log.info("解码后消息ID: {}", decoded.getId()); + log.info("解码后方法: {}", decoded.getMethod()); + log.info("解码后设备ID: {}", decoded.getDeviceId()); + log.info("解码后参数: {}", decoded.getParams()); + + System.out.println(); + } + + /** + * 演示复杂数据上报 + */ + private static void demonstrateComplexDataReport(IotTcpJsonDeviceMessageCodec codec) { + log.info("=== JSON格式复杂数据上报示例 ==="); + + // 创建复杂设备数据(类似EMQX格式) + Map deviceData = new HashMap<>(); + + // 环境数据 + Map environment = new HashMap<>(); + environment.put("temperature", 23.8); + environment.put("humidity", 55.0); + environment.put("co2", 420); + environment.put("pm25", 35); + deviceData.put("environment", environment); + + // GPS数据 + Map location = new HashMap<>(); + location.put("latitude", 39.9042); + location.put("longitude", 116.4074); + location.put("altitude", 43.5); + location.put("speed", 0.0); + deviceData.put("location", location); + + // 设备状态 + Map status = new HashMap<>(); + status.put("battery", 78); + status.put("signal", -65); + status.put("online", true); + status.put("version", "1.2.3"); + deviceData.put("status", status); + + // 创建设备消息 + IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", deviceData); + message.setDeviceId(789012L); + + // 编码 + byte[] packet = codec.encode(message); + String jsonString = new String(packet, StandardCharsets.UTF_8); + log.info("编码后JSON: {}", jsonString); + log.info("复杂数据包长度: {} 字节", packet.length); + + // 解码验证 + IotDeviceMessage decoded = codec.decode(packet); + log.info("解码后消息ID: {}", decoded.getId()); + log.info("解码后方法: {}", decoded.getMethod()); + log.info("解码后设备ID: {}", decoded.getDeviceId()); + log.info("解码后参数: {}", decoded.getParams()); + + System.out.println(); + } + + /** + * 演示便捷方法 + */ + private static void demonstrateConvenienceMethods() { + log.info("=== 便捷方法示例 ==="); + + IotTcpJsonDeviceMessageCodec codec = new IotTcpJsonDeviceMessageCodec(); + + // 使用便捷方法编码数据上报 + Map sensorData = Map.of( + "temperature", 26.5, + "humidity", 58.3 + ); + byte[] dataPacket = codec.encodeDataReport(sensorData, 123456L, "smart_sensor", "device_001"); + log.info("便捷方法编码数据上报: {}", new String(dataPacket, StandardCharsets.UTF_8)); + + // 使用便捷方法编码心跳 + byte[] heartbeatPacket = codec.encodeHeartbeat(123456L, "smart_sensor", "device_001"); + log.info("便捷方法编码心跳: {}", new String(heartbeatPacket, StandardCharsets.UTF_8)); + + // 使用便捷方法编码事件 + Map eventData = Map.of( + "eventType", "maintenance", + "description", "定期维护提醒" + ); + byte[] eventPacket = codec.encodeEventReport(eventData, 123456L, "smart_sensor", "device_001"); + log.info("便捷方法编码事件: {}", new String(eventPacket, StandardCharsets.UTF_8)); + + System.out.println(); + } + + /** + * 演示与EMQX格式的兼容性 + */ + private static void demonstrateEmqxCompatibility() { + log.info("=== EMQX格式兼容性示例 ==="); + + // 模拟EMQX风格的消息格式 + String emqxStyleJson = """ + { + "id": "msg_001", + "method": "thing.property.post", + "deviceId": 123456, + "params": { + "temperature": 25.5, + "humidity": 60.2 + }, + "timestamp": 1642781234567 + } + """; + + IotTcpJsonDeviceMessageCodec codec = new IotTcpJsonDeviceMessageCodec(); + + // 解码EMQX风格的消息 + byte[] emqxBytes = emqxStyleJson.getBytes(StandardCharsets.UTF_8); + IotDeviceMessage decoded = codec.decode(emqxBytes); + + log.info("EMQX风格消息解码成功:"); + log.info("消息ID: {}", decoded.getId()); + log.info("方法: {}", decoded.getMethod()); + log.info("设备ID: {}", decoded.getDeviceId()); + log.info("参数: {}", decoded.getParams()); + + System.out.println(); + } +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-binary-packet-examples.md b/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-binary-packet-examples.md new file mode 100644 index 0000000000..7bcf9b084e --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-binary-packet-examples.md @@ -0,0 +1,222 @@ +# TCP二进制协议数据包格式说明和示例 + +## 1. 二进制协议概述 + +TCP二进制协议是一种高效的自定义协议格式,适用于对带宽和性能要求较高的场景。 + +## 2. 数据包格式 + +### 2.1 整体结构 +``` ++----------+----------+----------+----------+----------+----------+ +| 包头 | 地址长度 | 设备地址 | 功能码 | 消息序号 | 包体数据 | +| 4字节 | 2字节 | 变长 | 2字节 | 2字节 | 变长 | ++----------+----------+----------+----------+----------+----------+ +``` + +### 2.2 字段说明 + +| 字段 | 长度 | 类型 | 说明 | +|----------|--------|--------|--------------------------------| +| 包头 | 4字节 | int | 后续数据的总长度(不包含包头) | +| 地址长度 | 2字节 | short | 设备地址的字节长度 | +| 设备地址 | 变长 | string | 设备标识符 | +| 功能码 | 2字节 | short | 消息类型标识 | +| 消息序号 | 2字节 | short | 消息唯一标识 | +| 包体数据 | 变长 | string | JSON格式的消息内容 | + +### 2.3 功能码定义 + +| 功能码 | 名称 | 说明 | +|--------|----------|--------------------------------| +| 10 | 设备注册 | 设备首次连接时的注册请求 | +| 11 | 注册回复 | 服务器对注册请求的回复 | +| 20 | 心跳请求 | 设备发送的心跳包 | +| 21 | 心跳回复 | 服务器对心跳的回复 | +| 30 | 消息上行 | 设备向服务器发送的数据 | +| 40 | 消息下行 | 服务器向设备发送的指令 | + +## 3. 二进制数据上报包示例 + +### 3.1 温度传感器数据上报 + +**原始数据:** +```json +{ + "method": "thing.property.post", + "params": { + "temperature": 25.5, + "humidity": 60.2, + "pressure": 1013.25 + }, + "timestamp": 1642781234567 +} +``` + +**数据包结构:** +``` +包头: 0x00000045 (69字节) +地址长度: 0x0006 (6字节) +设备地址: "123456" +功能码: 0x001E (30 - 消息上行) +消息序号: 0x1234 (4660) +包体: JSON字符串 +``` + +**完整十六进制数据包:** +``` +00 00 00 45 00 06 31 32 33 34 35 36 00 1E 12 34 +7B 22 6D 65 74 68 6F 64 22 3A 22 74 68 69 6E 67 +2E 70 72 6F 70 65 72 74 79 2E 70 6F 73 74 22 2C +22 70 61 72 61 6D 73 22 3A 7B 22 74 65 6D 70 65 +72 61 74 75 72 65 22 3A 32 35 2E 35 2C 22 68 75 +6D 69 64 69 74 79 22 3A 36 30 2E 32 2C 22 70 72 +65 73 73 75 72 65 22 3A 31 30 31 33 2E 32 35 7D +2C 22 74 69 6D 65 73 74 61 6D 70 22 3A 31 36 34 +32 37 38 31 32 33 34 35 36 37 7D +``` + +### 2.2 GPS定位数据上报 + +**原始数据:** +```json +{ + "method": "thing.property.post", + "params": { + "latitude": 39.9042, + "longitude": 116.4074, + "altitude": 43.5, + "speed": 0.0 + }, + "timestamp": 1642781234567 +} +``` + +## 3. 心跳包示例 + +### 3.1 标准心跳包 + +**原始数据:** +```json +{ + "method": "thing.state.online", + "timestamp": 1642781234567 +} +``` + +**数据包结构:** +``` +包头: 0x00000028 (40字节) +地址长度: 0x0006 (6字节) +设备地址: "123456" +功能码: 0x0014 (20 - 心跳请求) +消息序号: 0x5678 (22136) +包体: JSON字符串 +``` + +**完整十六进制数据包:** +``` +00 00 00 28 00 06 31 32 33 34 35 36 00 14 56 78 +7B 22 6D 65 74 68 6F 64 22 3A 22 74 68 69 6E 67 +2E 73 74 61 74 65 2E 6F 6E 6C 69 6E 65 22 2C 22 +74 69 6D 65 73 74 61 6D 70 22 3A 31 36 34 32 37 +38 31 32 33 34 35 36 37 7D +``` + +## 4. 复杂数据上报示例 + +### 4.1 多传感器综合数据 + +**原始数据:** +```json +{ + "method": "thing.property.post", + "params": { + "environment": { + "temperature": 23.8, + "humidity": 55.0, + "co2": 420 + }, + "location": { + "latitude": 39.9042, + "longitude": 116.4074, + "altitude": 43.5 + }, + "status": { + "battery": 78, + "signal": -65, + "online": true + } + }, + "timestamp": 1642781234567 +} +``` + +## 5. 数据包解析步骤 + +### 5.1 解析流程 + +1. **读取包头(4字节)** + - 获取后续数据的总长度 + - 验证数据包完整性 + +2. **读取设备地址长度(2字节)** + - 确定设备地址的字节数 + +3. **读取设备地址(变长)** + - 根据地址长度读取设备标识 + +4. **读取功能码(2字节)** + - 确定消息类型 + +5. **读取消息序号(2字节)** + - 获取消息唯一标识 + +6. **读取包体数据(变长)** + - 解析JSON格式的消息内容 + +### 5.2 Java解析示例 + +```java +public TcpDataPackage parsePacket(byte[] packet) { + int index = 0; + + // 1. 解析包头 + int totalLength = ByteBuffer.wrap(packet, index, 4).getInt(); + index += 4; + + // 2. 解析设备地址长度 + short addrLength = ByteBuffer.wrap(packet, index, 2).getShort(); + index += 2; + + // 3. 解析设备地址 + String deviceAddr = new String(packet, index, addrLength); + index += addrLength; + + // 4. 解析功能码 + short functionCode = ByteBuffer.wrap(packet, index, 2).getShort(); + index += 2; + + // 5. 解析消息序号 + short messageId = ByteBuffer.wrap(packet, index, 2).getShort(); + index += 2; + + // 6. 解析包体数据 + String payload = new String(packet, index, packet.length - index); + + return TcpDataPackage.builder() + .addr(deviceAddr) + .code(functionCode) + .mid(messageId) + .payload(payload) + .build(); +} +``` + +## 6. 注意事项 + +1. **字节序**:所有多字节数据使用大端序(Big-Endian) +2. **字符编码**:字符串数据使用UTF-8编码 +3. **JSON格式**:包体数据必须是有效的JSON格式 +4. **长度限制**:单个数据包建议不超过1MB +5. **错误处理**:解析失败时应返回相应的错误码 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-json-packet-examples.md b/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-json-packet-examples.md new file mode 100644 index 0000000000..45a08d78af --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-json-packet-examples.md @@ -0,0 +1,286 @@ +# TCP JSON格式协议说明 + +## 1. 协议概述 + +TCP JSON格式协议采用纯JSON格式进行数据传输,参考了EMQX和HTTP模块的数据格式设计,具有以下优势: + +- **标准化**:使用标准JSON格式,易于解析和处理 +- **可读性**:人类可读,便于调试和维护 +- **扩展性**:可以轻松添加新字段,向后兼容 +- **统一性**:与HTTP模块保持一致的数据格式 + +## 2. 消息格式 + +### 2.1 基础消息结构 + +```json +{ + "id": "消息唯一标识", + "method": "消息方法", + "deviceId": "设备ID", + "params": { + // 消息参数 + }, + "timestamp": 时间戳 +} +``` + +### 2.2 字段说明 + +| 字段名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| id | String | 是 | 消息唯一标识,UUID格式 | +| method | String | 是 | 消息方法,如 thing.property.post | +| deviceId | Long | 是 | 设备ID | +| params | Object | 否 | 消息参数,具体内容根据method而定 | +| timestamp | Long | 是 | 时间戳(毫秒) | +| code | Integer | 否 | 响应码(下行消息使用) | +| message | String | 否 | 响应消息(下行消息使用) | + +## 3. 消息类型 + +### 3.1 数据上报 (thing.property.post) + +设备向服务器上报属性数据。 + +**示例:** +```json +{ + "id": "8ac6a1db91e64aa9996143fdbac2cbfe", + "method": "thing.property.post", + "deviceId": 123456, + "params": { + "temperature": 25.5, + "humidity": 60.2, + "pressure": 1013.25, + "battery": 85 + }, + "timestamp": 1753111026437 +} +``` + +### 3.2 心跳 (thing.state.online) + +设备向服务器发送心跳保活。 + +**示例:** +```json +{ + "id": "7db8c4e6408b40f8b2549ddd94f6bb02", + "method": "thing.state.online", + "deviceId": 123456, + "timestamp": 1753111026467 +} +``` + +### 3.3 事件上报 (thing.event.post) + +设备向服务器上报事件信息。 + +**示例:** +```json +{ + "id": "9e7d72731b854916b1baa5088bd6a907", + "method": "thing.event.post", + "deviceId": 123456, + "params": { + "eventType": "alarm", + "level": "warning", + "description": "温度过高", + "value": 45.8 + }, + "timestamp": 1753111026468 +} +``` + +### 3.4 属性设置 (thing.property.set) + +服务器向设备下发属性设置指令。 + +**示例:** +```json +{ + "id": "cmd_001", + "method": "thing.property.set", + "deviceId": 123456, + "params": { + "targetTemperature": 22.0, + "mode": "auto" + }, + "timestamp": 1753111026469 +} +``` + +### 3.5 服务调用 (thing.service.invoke) + +服务器向设备调用服务。 + +**示例:** +```json +{ + "id": "service_001", + "method": "thing.service.invoke", + "deviceId": 123456, + "params": { + "service": "restart", + "args": { + "delay": 5 + } + }, + "timestamp": 1753111026470 +} +``` + +## 4. 复杂数据示例 + +### 4.1 多传感器综合数据 + +```json +{ + "id": "complex_001", + "method": "thing.property.post", + "deviceId": 789012, + "params": { + "environment": { + "temperature": 23.8, + "humidity": 55.0, + "co2": 420, + "pm25": 35 + }, + "location": { + "latitude": 39.9042, + "longitude": 116.4074, + "altitude": 43.5, + "speed": 0.0 + }, + "status": { + "battery": 78, + "signal": -65, + "online": true, + "version": "1.2.3" + } + }, + "timestamp": 1753111026471 +} +``` + +## 5. 与EMQX格式的兼容性 + +本协议设计参考了EMQX的消息格式,具有良好的兼容性: + +### 5.1 EMQX标准格式 + +```json +{ + "id": "msg_001", + "method": "thing.property.post", + "deviceId": 123456, + "params": { + "temperature": 25.5, + "humidity": 60.2 + }, + "timestamp": 1642781234567 +} +``` + +### 5.2 兼容性说明 + +- ✅ **字段名称**:与EMQX保持一致 +- ✅ **数据类型**:完全兼容 +- ✅ **消息结构**:结构相同 +- ✅ **扩展字段**:支持自定义扩展 + +## 6. 使用示例 + +### 6.1 Java编码示例 + +```java +// 创建编解码器 +IotTcpJsonDeviceMessageCodec codec = new IotTcpJsonDeviceMessageCodec(); + +// 创建数据上报消息 +Map sensorData = Map.of( + "temperature", 25.5, + "humidity", 60.2 +); +IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", sensorData); +message.setDeviceId(123456L); + +// 编码为字节数组 +byte[] jsonBytes = codec.encode(message); + +// 解码 +IotDeviceMessage decoded = codec.decode(jsonBytes); +``` + +### 6.2 便捷方法示例 + +```java +// 快速编码数据上报 +byte[] dataPacket = codec.encodeDataReport(sensorData, 123456L, "product_key", "device_name"); + +// 快速编码心跳 +byte[] heartbeatPacket = codec.encodeHeartbeat(123456L, "product_key", "device_name"); + +// 快速编码事件 +byte[] eventPacket = codec.encodeEventReport(eventData, 123456L, "product_key", "device_name"); +``` + +## 7. 协议优势 + +### 7.1 与原TCP二进制协议对比 + +| 特性 | 二进制协议 | JSON协议 | +|------|------------|----------| +| 可读性 | 差 | 优秀 | +| 调试难度 | 高 | 低 | +| 扩展性 | 差 | 优秀 | +| 解析复杂度 | 高 | 低 | +| 数据大小 | 小 | 稍大 | +| 标准化程度 | 低 | 高 | + +### 7.2 适用场景 + +- ✅ **开发调试**:JSON格式便于查看和调试 +- ✅ **快速集成**:标准JSON格式,集成简单 +- ✅ **协议扩展**:可以轻松添加新字段 +- ✅ **多语言支持**:JSON格式支持所有主流语言 +- ✅ **云平台对接**:与主流IoT云平台格式兼容 + +## 8. 最佳实践 + +### 8.1 消息设计建议 + +1. **保持简洁**:避免过深的嵌套结构 +2. **字段命名**:使用驼峰命名法,保持一致性 +3. **数据类型**:使用合适的数据类型,避免字符串表示数字 +4. **时间戳**:统一使用毫秒级时间戳 + +### 8.2 性能优化 + +1. **批量上报**:可以在params中包含多个数据点 +2. **压缩传输**:对于大数据量可以考虑gzip压缩 +3. **缓存机制**:客户端可以缓存消息,批量发送 + +### 8.3 错误处理 + +1. **格式验证**:确保JSON格式正确 +2. **字段检查**:验证必填字段是否存在 +3. **异常处理**:提供详细的错误信息 + +## 9. 迁移指南 + +### 9.1 从二进制协议迁移 + +1. **保持兼容**:可以同时支持两种协议 +2. **逐步迁移**:按设备类型逐步迁移 +3. **测试验证**:充分测试新协议的稳定性 + +### 9.2 配置变更 + +```java +// 在设备配置中指定编解码器类型 +device.setCodecType("TCP_JSON"); +``` + +这样就完成了TCP协议向JSON格式的升级,提供了更好的可读性、扩展性和兼容性。 From 7ba084bc4dfafeb0475ef04a4e1b9e7deef3abf6 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Tue, 22 Jul 2025 12:26:08 +0800 Subject: [PATCH 48/89] =?UTF-8?q?fix=EF=BC=9A=E3=80=90BPM=20=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E6=B5=81=E3=80=91=E5=AD=90=E6=B5=81=E7=A8=8B=E8=BF=94?= =?UTF-8?q?=E5=9B=9E=E7=9A=84=20processInstanceId=20=E4=B8=8D=E6=AD=A3?= =?UTF-8?q?=E7=A1=AE=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/task/vo/instance/BpmApprovalDetailRespVO.java | 2 +- .../bpm/service/task/BpmProcessInstanceServiceImpl.java | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmApprovalDetailRespVO.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmApprovalDetailRespVO.java index 38c2bc1013..0a785b10f4 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmApprovalDetailRespVO.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmApprovalDetailRespVO.java @@ -73,7 +73,7 @@ public class BpmApprovalDetailRespVO { private List candidateUsers; // 只包含未生成 ApprovalTaskInfo 的用户列表 @Schema(description = "流程编号", example = "8761d8e0-0922-11f0-bd37-00ff1db677bf") - private String processInstanceId; // 当且仅当,该节点是子流程节点时,才会有值(CallActivity 的 processInstanceId 字段) + private String processInstanceId; // 当且仅当,该节点是子流程节点时,才会有值(CallActivity 的 calledProcessInstanceId 字段) } diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java index a42a3ecd4b..d056e4dbd8 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java @@ -244,7 +244,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService if (historicProcessInstance == null) { throw exception(ErrorCodeConstants.PROCESS_INSTANCE_NOT_EXISTS); } - // 1.3 校验BpmnModel + // 1.3 校验 BpmnModel BpmnModel bpmnModel = processDefinitionService.getProcessDefinitionBpmnModel(task.getProcessDefinitionId()); if (bpmnModel == null) { return null; @@ -449,7 +449,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService .setNodeType(BpmSimpleModelNodeTypeEnum.CHILD_PROCESS.getType()).setStatus(processInstanceStatus) .setStartTime(DateUtils.of(activity.getStartTime())) .setEndTime(DateUtils.of(activity.getEndTime())) - .setProcessInstanceId(activity.getProcessInstanceId()); + .setProcessInstanceId(activity.getCalledProcessInstanceId()); approvalNodes.add(callActivity); } }); @@ -521,7 +521,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService activityNode.setCandidateUserIds(CollUtil.sub(candidateUserIds, index + 1, candidateUserIds.size())); } if (BpmSimpleModelNodeTypeEnum.CHILD_PROCESS.getType().equals(activityNode.getNodeType())) { - activityNode.setProcessInstanceId(firstActivity.getProcessInstanceId()); + activityNode.setProcessInstanceId(firstActivity.getCalledProcessInstanceId()); } return activityNode; }); From b766f8d820a13ec12a25f02485570b7e50a10df2 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Tue, 22 Jul 2025 20:15:52 +0800 Subject: [PATCH 49/89] =?UTF-8?q?fix=EF=BC=9A=E3=80=90BPM=20=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E6=B5=81=E3=80=91=E5=AD=90=E6=B5=81=E7=A8=8B=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E6=B5=81=E7=A8=8B=20title=20=E8=87=AA=E5=AE=9A?= =?UTF-8?q?=E4=B9=89=E6=A0=87=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BpmProcessInstanceEventListener.java | 17 +- .../task/BpmProcessInstanceServiceImpl.java | 161 ++++++++++-------- 2 files changed, 104 insertions(+), 74 deletions(-) diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmProcessInstanceEventListener.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmProcessInstanceEventListener.java index ba2aaa6bcb..3deb545fa8 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmProcessInstanceEventListener.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmProcessInstanceEventListener.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.listener; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; import com.google.common.collect.ImmutableSet; import jakarta.annotation.Resource; @@ -37,18 +38,26 @@ public class BpmProcessInstanceEventListener extends AbstractFlowableEngineEvent @Override protected void processCreated(FlowableEngineEntityEvent event) { - processInstanceService.processProcessInstanceCreated((ProcessInstance)event.getEntity()); + ProcessInstance processInstance = (ProcessInstance) event.getEntity(); + FlowableUtils.execute(processInstance.getTenantId(), + () -> processInstanceService.processProcessInstanceCreated(processInstance)); } @Override protected void processCompleted(FlowableEngineEntityEvent event) { - processInstanceService.processProcessInstanceCompleted((ProcessInstance)event.getEntity()); + ProcessInstance processInstance = (ProcessInstance) event.getEntity(); + FlowableUtils.execute(processInstance.getTenantId(), + () -> processInstanceService.processProcessInstanceCompleted(processInstance)); } - @Override // 特殊情况:当跳转到 EndEvent 流程实例未结束, 会执行 deleteProcessInstance 方法 + @Override protected void processCancelled(FlowableCancelledEvent event) { + // 特殊情况:当跳转到 EndEvent 流程实例未结束, 会执行 deleteProcessInstance 方法 ProcessInstance processInstance = processInstanceService.getProcessInstance(event.getProcessInstanceId()); - processInstanceService.processProcessInstanceCompleted(processInstance); + if (processInstance != null) { + FlowableUtils.execute(processInstance.getTenantId(), + () -> processInstanceService.processProcessInstanceCompleted(processInstance)); + } } } diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java index d056e4dbd8..66fbaf79a3 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java @@ -4,10 +4,7 @@ import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.ListUtil; import cn.hutool.core.date.DateUtil; import cn.hutool.core.lang.Assert; -import cn.hutool.core.util.ArrayUtil; -import cn.hutool.core.util.ObjUtil; -import cn.hutool.core.util.ObjectUtil; -import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.*; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.date.DateUtils; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; @@ -61,6 +58,8 @@ import org.flowable.task.api.history.HistoricTaskInstance; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.validation.annotation.Validated; import java.util.*; @@ -244,7 +243,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService if (historicProcessInstance == null) { throw exception(ErrorCodeConstants.PROCESS_INSTANCE_NOT_EXISTS); } - // 1.3 校验 BpmnModel + // 1.3 校验BpmnModel BpmnModel bpmnModel = processDefinitionService.getProcessDefinitionBpmnModel(task.getProcessDefinitionId()); if (bpmnModel == null) { return null; @@ -449,7 +448,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService .setNodeType(BpmSimpleModelNodeTypeEnum.CHILD_PROCESS.getType()).setStatus(processInstanceStatus) .setStartTime(DateUtils.of(activity.getStartTime())) .setEndTime(DateUtils.of(activity.getEndTime())) - .setProcessInstanceId(activity.getCalledProcessInstanceId()); + .setProcessInstanceId(activity.getProcessInstanceId()); approvalNodes.add(callActivity); } }); @@ -521,7 +520,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService activityNode.setCandidateUserIds(CollUtil.sub(candidateUserIds, index + 1, candidateUserIds.size())); } if (BpmSimpleModelNodeTypeEnum.CHILD_PROCESS.getType().equals(activityNode.getNodeType())) { - activityNode.setProcessInstanceId(firstActivity.getCalledProcessInstanceId()); + activityNode.setProcessInstanceId(firstActivity.getProcessInstanceId()); } return activityNode; }); @@ -771,17 +770,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService processInstanceBuilder.predefineProcessInstanceId(processIdRedisDAO.generate(processIdRule)); } // 3.2 流程名称 - BpmModelMetaInfoVO.TitleSetting titleSetting = processDefinitionInfo.getTitleSetting(); - if (titleSetting != null && Boolean.TRUE.equals(titleSetting.getEnable())) { - AdminUserRespDTO user = adminUserApi.getUser(userId); - Map cloneVariables = new HashMap<>(variables); - cloneVariables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_ID, user.getNickname()); - cloneVariables.put(BpmnVariableConstants.PROCESS_START_TIME, DateUtil.now()); - cloneVariables.put(BpmnVariableConstants.PROCESS_DEFINITION_NAME, definition.getName().trim()); - processInstanceBuilder.name(StrUtil.format(titleSetting.getTitle(), cloneVariables)); - } else { - processInstanceBuilder.name(definition.getName().trim()); - } + processInstanceBuilder.name(generateProcessInstanceName(userId, definition, processDefinitionInfo, variables)); // 3.3 发起流程实例 ProcessInstance instance = processInstanceBuilder.start(); return instance.getId(); @@ -817,6 +806,25 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService }); } + private String generateProcessInstanceName(Long userId, + ProcessDefinition definition, + BpmProcessDefinitionInfoDO definitionInfo, + Map variables) { + if (definition == null || definitionInfo == null) { + return null; + } + BpmModelMetaInfoVO.TitleSetting titleSetting = definitionInfo.getTitleSetting(); + if (titleSetting == null || !BooleanUtil.isTrue(titleSetting.getEnable())) { + return definition.getName(); + } + AdminUserRespDTO user = adminUserApi.getUser(userId); + Map cloneVariables = new HashMap<>(variables); + cloneVariables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_ID, user.getNickname()); + cloneVariables.put(BpmnVariableConstants.PROCESS_START_TIME, DateUtil.now()); + cloneVariables.put(BpmnVariableConstants.PROCESS_DEFINITION_NAME, definition.getName().trim()); + return StrUtil.format(definitionInfo.getTitleSetting().getTitle(), cloneVariables); + } + @Override public void cancelProcessInstanceByStartUser(Long userId, @Valid BpmProcessInstanceCancelReqVO cancelReqVO) { // 1.1 校验流程实例存在 @@ -833,7 +841,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService .getProcessDefinitionInfo(instance.getProcessDefinitionId()); Assert.notNull(processDefinitionInfo, "流程定义({})不存在", processDefinitionInfo); if (processDefinitionInfo.getAllowCancelRunningProcess() != null // 防止未配置 AllowCancelRunningProcess , 默认为可取消 - && Boolean.FALSE.equals(processDefinitionInfo.getAllowCancelRunningProcess())) { + && BooleanUtil.isFalse(processDefinitionInfo.getAllowCancelRunningProcess())) { throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_ALLOW); } // 1.4 子流程不允许取消 @@ -900,64 +908,77 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService @Override public void processProcessInstanceCompleted(ProcessInstance instance) { - // 注意:需要基于 instance 设置租户编号,避免 Flowable 内部异步时,丢失租户编号 - FlowableUtils.execute(instance.getTenantId(), () -> { - // 1.1 获取当前状态 - Integer status = (Integer) instance.getProcessVariables() - .get(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS); - String reason = (String) instance.getProcessVariables() - .get(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_REASON); - // 1.2 当流程状态还是审批状态中,说明审批通过了,则变更下它的状态 - // 为什么这么处理?因为流程完成,并且完成了,说明审批通过了 - if (Objects.equals(status, BpmProcessInstanceStatusEnum.RUNNING.getStatus())) { - status = BpmProcessInstanceStatusEnum.APPROVE.getStatus(); - runtimeService.setVariable(instance.getId(), BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS, - status); + // 1.1 获取当前状态 + Integer status = (Integer) instance.getProcessVariables() + .get(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS); + String reason = (String) instance.getProcessVariables() + .get(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_REASON); + // 1.2 当流程状态还是审批状态中,说明审批通过了,则变更下它的状态 + // 为什么这么处理?因为流程完成,并且完成了,说明审批通过了 + if (Objects.equals(status, BpmProcessInstanceStatusEnum.RUNNING.getStatus())) { + status = BpmProcessInstanceStatusEnum.APPROVE.getStatus(); + runtimeService.setVariable(instance.getId(), BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS, + status); + } + + // 2. 发送对应的消息通知 + if (Objects.equals(status, BpmProcessInstanceStatusEnum.APPROVE.getStatus())) { + messageService.sendMessageWhenProcessInstanceApprove( + BpmProcessInstanceConvert.INSTANCE.buildProcessInstanceApproveMessage(instance)); + } else if (Objects.equals(status, BpmProcessInstanceStatusEnum.REJECT.getStatus())) { + messageService.sendMessageWhenProcessInstanceReject( + BpmProcessInstanceConvert.INSTANCE.buildProcessInstanceRejectMessage(instance, reason)); + } + + // 3. 发送流程实例的状态事件 + processInstanceEventPublisher.sendProcessInstanceResultEvent( + BpmProcessInstanceConvert.INSTANCE.buildProcessInstanceStatusEvent(this, instance, status)); + + // 4. 流程后置通知 + if (Objects.equals(status, BpmProcessInstanceStatusEnum.APPROVE.getStatus())) { + BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService. + getProcessDefinitionInfo(instance.getProcessDefinitionId()); + if (ObjUtil.isNotNull(processDefinitionInfo) && + ObjUtil.isNotNull(processDefinitionInfo.getProcessAfterTriggerSetting())) { + BpmModelMetaInfoVO.HttpRequestSetting setting = processDefinitionInfo.getProcessAfterTriggerSetting(); + + BpmHttpRequestUtils.executeBpmHttpRequest(instance, + setting.getUrl(), setting.getHeader(), setting.getBody(), true, setting.getResponse()); } - - // 2. 发送对应的消息通知 - if (Objects.equals(status, BpmProcessInstanceStatusEnum.APPROVE.getStatus())) { - messageService.sendMessageWhenProcessInstanceApprove( - BpmProcessInstanceConvert.INSTANCE.buildProcessInstanceApproveMessage(instance)); - } else if (Objects.equals(status, BpmProcessInstanceStatusEnum.REJECT.getStatus())) { - messageService.sendMessageWhenProcessInstanceReject( - BpmProcessInstanceConvert.INSTANCE.buildProcessInstanceRejectMessage(instance, reason)); - } - - // 3. 发送流程实例的状态事件 - processInstanceEventPublisher.sendProcessInstanceResultEvent( - BpmProcessInstanceConvert.INSTANCE.buildProcessInstanceStatusEvent(this, instance, status)); - - // 4. 流程后置通知 - if (Objects.equals(status, BpmProcessInstanceStatusEnum.APPROVE.getStatus())) { - BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService. - getProcessDefinitionInfo(instance.getProcessDefinitionId()); - if (ObjUtil.isNotNull(processDefinitionInfo) && - ObjUtil.isNotNull(processDefinitionInfo.getProcessAfterTriggerSetting())) { - BpmModelMetaInfoVO.HttpRequestSetting setting = processDefinitionInfo.getProcessAfterTriggerSetting(); - - BpmHttpRequestUtils.executeBpmHttpRequest(instance, - setting.getUrl(), setting.getHeader(), setting.getBody(), true, setting.getResponse()); - } - } - }); + } } @Override public void processProcessInstanceCreated(ProcessInstance instance) { - // 注意:需要基于 instance 设置租户编号,避免 Flowable 内部异步时,丢失租户编号 - FlowableUtils.execute(instance.getTenantId(), () -> { - // 流程前置通知 - BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService. - getProcessDefinitionInfo(instance.getProcessDefinitionId()); - if (ObjUtil.isNull(processDefinitionInfo) || - ObjUtil.isNull(processDefinitionInfo.getProcessBeforeTriggerSetting())) { - return; + BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService. + getProcessDefinitionInfo(instance.getProcessDefinitionId()); + ProcessDefinition processDefinition = processDefinitionService.getProcessDefinition(instance.getProcessDefinitionId()); + if (processDefinition == null || processDefinitionInfo == null) { + return; + } + + // 自定义标题。目的:主要处理子流程的标题无法处理 + // 注意:必须使用 TransactionSynchronizationManager 事务提交后,否则不生效!!! + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + + @Override + public void afterCommit() { + String name = generateProcessInstanceName(Long.valueOf(instance.getStartUserId()), + processDefinition, processDefinitionInfo, instance.getProcessVariables()); + if (ObjUtil.notEqual(instance.getName(), name)) { + runtimeService.setProcessInstanceName(instance.getProcessInstanceId(), name); + } } - BpmModelMetaInfoVO.HttpRequestSetting setting = processDefinitionInfo.getProcessBeforeTriggerSetting(); - BpmHttpRequestUtils.executeBpmHttpRequest(instance, - setting.getUrl(), setting.getHeader(), setting.getBody(), true, setting.getResponse()); + }); + + // 流程前置通知 + if (ObjUtil.isNull(processDefinitionInfo.getProcessBeforeTriggerSetting())) { + return; + } + BpmModelMetaInfoVO.HttpRequestSetting setting = processDefinitionInfo.getProcessBeforeTriggerSetting(); + BpmHttpRequestUtils.executeBpmHttpRequest(instance, + setting.getUrl(), setting.getHeader(), setting.getBody(), true, setting.getResponse()); } } From 989c4e4b88bc32e0eddab2d5c4fc969f51133b51 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Tue, 22 Jul 2025 21:57:01 +0800 Subject: [PATCH 50/89] =?UTF-8?q?fix=EF=BC=9A=E3=80=90BPM=20=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E6=B5=81=E3=80=91bpmn=20subProcess=20=E5=AD=98?= =?UTF-8?q?=E5=9C=A8=20userTask=20NPE=20=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../flowable/core/candidate/BpmTaskCandidateInvoker.java | 7 ++----- .../bpm/framework/flowable/core/util/BpmnModelUtils.java | 6 +++++- .../yudao/module/bpm/service/task/BpmTaskServiceImpl.java | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvoker.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvoker.java index 7f66b29d3b..df8b0d5fd5 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvoker.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvoker.java @@ -18,10 +18,7 @@ import cn.iocoder.yudao.module.system.api.user.AdminUserApi; import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; import com.google.common.annotations.VisibleForTesting; import lombok.extern.slf4j.Slf4j; -import org.flowable.bpmn.model.BpmnModel; -import org.flowable.bpmn.model.CallActivity; -import org.flowable.bpmn.model.FlowElement; -import org.flowable.bpmn.model.UserTask; +import org.flowable.bpmn.model.*; import org.flowable.engine.delegate.DelegateExecution; import org.flowable.engine.runtime.ProcessInstance; @@ -132,7 +129,7 @@ public class BpmTaskCandidateInvoker { Long startUserId, String processDefinitionId, Map processVariables) { // 如果是 CallActivity 子流程,不进行计算候选人 FlowElement flowElement = BpmnModelUtils.getFlowElementById(bpmnModel, activityId); - if (flowElement instanceof CallActivity) { + if (flowElement instanceof CallActivity || flowElement instanceof SubProcess) { return new HashSet<>(); } // 审批类型非人工审核时,不进行计算候选人。原因是:后续会自动通过、不通过 diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java index 1cccf18f04..460af124f6 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java @@ -478,7 +478,11 @@ public class BpmnModelUtils { */ public static FlowElement getFlowElementById(BpmnModel model, String flowElementId) { Process process = model.getMainProcess(); - return process.getFlowElement(flowElementId); + FlowElement flowElement = process.getFlowElement(flowElementId); + if (flowElement != null) { + return flowElement; + } + return model.getFlowElement(flowElementId); } /** diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java index 1f7e699032..942a50669a 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java @@ -1379,7 +1379,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { PROCESS_INSTANCE_VARIABLE_SKIP_START_USER_NODE, String.class)); if (userTaskElement.getId().equals(START_USER_NODE_ID) && (skipStartUserNodeFlag == null // 目的:一般是“主流程”,发起人节点,自动通过审核 - || Boolean.TRUE.equals(skipStartUserNodeFlag)) // 目的:一般是“子流程”,发起人节点,按配置自动通过审核 + || BooleanUtil.isTrue(skipStartUserNodeFlag)) // 目的:一般是“子流程”,发起人节点,按配置自动通过审核 && ObjUtil.notEqual(returnTaskFlag, Boolean.TRUE)) { getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId()) .setReason(BpmReasonEnum.ASSIGN_START_USER_APPROVE_WHEN_SKIP_START_USER_NODE.getReason())); From 376114a8741ad134ea13396c777ee176d07fddf6 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Tue, 22 Jul 2025 23:59:20 +0800 Subject: [PATCH 51/89] =?UTF-8?q?fix=EF=BC=9A=E3=80=90BPM=20=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E6=B5=81=E3=80=91=E4=BF=AE=E5=A4=8D=E5=AD=90=E6=B5=81?= =?UTF-8?q?=E7=A8=8B=E5=8F=91=E8=B5=B7=E4=BA=BA=E4=B8=BA=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E6=97=B6=E5=AE=9E=E9=99=85=E5=8F=91=E8=B5=B7=E4=BA=BA=E4=BC=9A?= =?UTF-8?q?=E5=8F=98=E6=88=90=E4=B8=BB=E6=B5=81=E7=A8=8B=E5=8F=91=E8=B5=B7?= =?UTF-8?q?=E4=BA=BA=20https://gitee.com/zhijiantianya/yudao-cloud/issues/?= =?UTF-8?q?ICNHA8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/task/BpmProcessInstanceServiceImpl.java | 4 ++-- .../task/listener/BpmCallActivityListener.java | 13 +++++++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java index 66fbaf79a3..9ce877b67c 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java @@ -448,7 +448,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService .setNodeType(BpmSimpleModelNodeTypeEnum.CHILD_PROCESS.getType()).setStatus(processInstanceStatus) .setStartTime(DateUtils.of(activity.getStartTime())) .setEndTime(DateUtils.of(activity.getEndTime())) - .setProcessInstanceId(activity.getProcessInstanceId()); + .setProcessInstanceId(activity.getCalledProcessInstanceId()); approvalNodes.add(callActivity); } }); @@ -520,7 +520,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService activityNode.setCandidateUserIds(CollUtil.sub(candidateUserIds, index + 1, candidateUserIds.size())); } if (BpmSimpleModelNodeTypeEnum.CHILD_PROCESS.getType().equals(activityNode.getNodeType())) { - activityNode.setProcessInstanceId(firstActivity.getProcessInstanceId()); + activityNode.setProcessInstanceId(firstActivity.getCalledProcessInstanceId()); } return activityNode; }); diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/listener/BpmCallActivityListener.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/listener/BpmCallActivityListener.java index 40313a9663..da148a8562 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/listener/BpmCallActivityListener.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/listener/BpmCallActivityListener.java @@ -85,10 +85,15 @@ public class BpmCallActivityListener implements ExecutionListener { // 2.2 使用表单值,并兜底字符串转 Long 失败时使用主流程发起人 try { FlowableUtils.setAuthenticatedUserId(Long.parseLong(formFieldValue)); - } catch (Exception e) { - log.error("[notify][监听器:{},子流程监听器设置流程的发起人字符串转 Long 失败,字符串:{}]", - DELEGATE_EXPRESSION, formFieldValue); - FlowableUtils.setAuthenticatedUserId(Long.parseLong(processInstance.getStartUserId())); + } catch (NumberFormatException ex) { + try { + List formFieldValues = JsonUtils.parseArray(formFieldValue, Long.class); + FlowableUtils.setAuthenticatedUserId(formFieldValues.get(0)); + } catch (Exception e) { + log.error("[notify][监听器:{},子流程监听器设置流程的发起人字符串转 Long 失败,字符串:{}]", + DELEGATE_EXPRESSION, formFieldValue); + FlowableUtils.setAuthenticatedUserId(Long.parseLong(processInstance.getStartUserId())); + } } } } From a4244ab9996f2e8f9951ed4a495dc7e027f862be Mon Sep 17 00:00:00 2001 From: YunaiV Date: Wed, 23 Jul 2025 12:46:00 +0800 Subject: [PATCH 52/89] =?UTF-8?q?fix=EF=BC=9A=E3=80=90BPM=20=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E6=B5=81=E3=80=91=E5=AD=98=E5=9C=A8=E5=8A=A0=E7=AD=BE?= =?UTF-8?q?=E6=97=B6=EF=BC=8C=E9=80=80=E5=9B=9E=E6=8A=A5=20Execution=20cou?= =?UTF-8?q?ld=20not=20be=20found=20with=20id=20null?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yudao/module/bpm/service/task/BpmTaskServiceImpl.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java index 942a50669a..1dbd0b17c0 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java @@ -898,7 +898,9 @@ public class BpmTaskServiceImpl implements BpmTaskService { if (!returnTaskKeyList.contains(task.getTaskDefinitionKey())) { return; } - runExecutionIds.add(task.getExecutionId()); + if (task.getExecutionId() != null) { + runExecutionIds.add(task.getExecutionId()); + } // 判断是否分配给自己任务,因为会签任务,一个节点会有多个任务 if (isAssignUserTask(userId, task)) { // 情况一:自己的任务,进行 RETURN 标记 From f70f578ac59fd6b357245499adac6b6e3557e304 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Wed, 23 Jul 2025 19:17:33 +0800 Subject: [PATCH 53/89] =?UTF-8?q?fix=EF=BC=9A=E3=80=90IoT=20=E7=89=A9?= =?UTF-8?q?=E8=81=94=E7=BD=91=E3=80=91code=20review=20tcp=20=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tcp/IotTcpBinaryDeviceMessageCodec.java | 29 +++++-- .../gateway/codec/tcp/IotTcpCodecManager.java | 31 +++++--- .../tcp/IotTcpJsonDeviceMessageCodec.java | 29 ++++--- .../protocol/tcp/IotTcpUpstreamProtocol.java | 2 + .../tcp/router/IotTcpDownstreamHandler.java | 9 ++- .../tcp/TcpBinaryDataPacketExamples.java | 55 ++++++------- .../codec/tcp/TcpJsonDataPacketExamples.java | 79 ++++++++++--------- 7 files changed, 133 insertions(+), 101 deletions(-) diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java index 40c8fcede4..a86a937d93 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java @@ -10,11 +10,12 @@ import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; +// TODO @haohao:设备地址(变长) 是不是非必要哈?因为认证后,不需要每次都带呀。 /** * TCP 二进制格式 {@link IotDeviceMessage} 编解码器 * * 使用自定义二进制协议格式: - * 包头(4字节) | 地址长度(2字节) | 设备地址(变长) | 功能码(2字节) | 消息序号(2字节) | 包体数据(变长) + * 包头(4 字节) | 地址长度(2 字节) | 设备地址(变长) | 功能码(2 字节) | 消息序号(2 字节) | 包体数据(变长) * * @author 芋道源码 */ @@ -27,6 +28,7 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { */ public static final String TYPE = "TCP_BINARY"; + // TODO @haohao:这个注释不太对。 // ==================== 常量定义 ==================== @Override @@ -67,10 +69,11 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { TcpDataPackage dataPackage = decodeTcpDataPackage(Buffer.buffer(bytes)); // 2. 根据功能码确定方法 + // TODO @haohao:会不会有事件上报哈。 String method = (dataPackage.getCode() == TcpDataPackage.CODE_HEARTBEAT) ? MessageMethod.STATE_ONLINE : MessageMethod.PROPERTY_POST; - // 3. 解析负载数据和请求ID + // 3. 解析负载数据和请求 ID PayloadInfo payloadInfo = parsePayloadInfo(dataPackage.getPayload()); // 4. 构建 IoT 设备消息(设置完整的必要参数) @@ -78,13 +81,16 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { payloadInfo.getRequestId(), method, payloadInfo.getParams()); // 5. 设置设备相关信息 + // TODO @haohao:serverId 不是这里解析的哈。 Long deviceId = parseDeviceId(dataPackage.getAddr()); message.setDeviceId(deviceId); - // 6. 设置TCP协议相关信息 + // 6. 设置 TCP 协议相关信息 + // TODO @haohao:serverId 不是这里解析的哈。 message.setServerId(generateServerId(dataPackage)); - // 7. 设置租户ID(TODO: 后续可以从设备信息中获取) + // 7. 设置租户 ID(TODO: 后续可以从设备信息中获取) + // TODO @haohao:租户 id 不是这里解析的哈。 // message.setTenantId(getTenantIdByDeviceId(deviceId)); if (log.isDebugEnabled()) { @@ -104,6 +110,7 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { return TYPE; } + // TODO @haohao:这种简单解析,中间不用空格哈。 /** * 构建完整负载 */ @@ -130,12 +137,10 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { return payload.toString(); } - - // ==================== 编解码方法 ==================== /** - * 解析负载信息(包含requestId和params) + * 解析负载信息(包含 requestId 和 params) */ private PayloadInfo parsePayloadInfo(String payload) { if (StrUtil.isEmpty(payload)) { @@ -143,6 +148,7 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { } try { + // TODO @haohao:使用 jsonUtils JSONObject jsonObject = JSONUtil.parseObj(payload); String requestId = jsonObject.getStr(PayloadField.REQUEST_ID); if (StrUtil.isEmpty(requestId)) { @@ -185,7 +191,7 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { * @return 服务ID */ private String generateServerId(TcpDataPackage dataPackage) { - // 使用协议类型 + 设备地址 + 消息序号生成唯一的服务ID + // 使用协议类型 + 设备地址 + 消息序号生成唯一的服务 ID return String.format("tcp_%s_%d", dataPackage.getAddr(), dataPackage.getMid()); } @@ -300,23 +306,28 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { * 消息方法常量 */ public static class MessageMethod { + public static final String PROPERTY_POST = "thing.property.post"; // 数据上报 public static final String STATE_ONLINE = "thing.state.online"; // 心跳 + } /** * 负载字段名 */ private static class PayloadField { + public static final String METHOD = "method"; public static final String PARAMS = "params"; public static final String TIMESTAMP = "timestamp"; public static final String REQUEST_ID = "requestId"; public static final String MESSAGE_ID = "msgId"; + } // ==================== TCP 数据包编解码方法 ==================== + // TODO @haohao:lombok 简化 /** * 负载信息类 */ @@ -361,11 +372,13 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { // ==================== 自定义异常 ==================== + // TODO @haohao:可以搞个全局的; /** * TCP 编解码异常 */ public static class TcpCodecException extends RuntimeException { + // TODO @haohao:非必要构造方法,可以去掉哈。 public TcpCodecException(String message) { super(message); } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpCodecManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpCodecManager.java index aa789c689a..8810a982ea 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpCodecManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpCodecManager.java @@ -8,11 +8,11 @@ import org.springframework.stereotype.Component; /** * TCP编解码器管理器(简化版) - * + * * 核心功能: * - 自动协议检测(二进制 vs JSON) * - 统一编解码接口 - * - 默认使用JSON协议 + * - 默认使用 JSON 协议 * * @author 芋道源码 */ @@ -22,6 +22,8 @@ public class IotTcpCodecManager implements IotDeviceMessageCodec { public static final String TYPE = "TCP"; + // TODO @haohao:@Resource + @Autowired private IotTcpBinaryDeviceMessageCodec binaryCodec; @@ -40,21 +42,22 @@ public class IotTcpCodecManager implements IotDeviceMessageCodec { @Override public byte[] encode(IotDeviceMessage message) { - // 默认使用JSON协议编码 + // 默认使用 JSON 协议编码 return jsonCodec.encode(message); } + // TODO @haohao:要不还是不自动检测,用户手动配置哈。简化一些。。。 @Override public IotDeviceMessage decode(byte[] bytes) { // 自动检测协议类型并解码 if (isJsonFormat(bytes)) { if (log.isDebugEnabled()) { - log.debug("[decode][检测到JSON协议] 数据长度: {}字节", bytes.length); + log.debug("[decode][检测到 JSON 协议,数据长度: {} 字节]", bytes.length); } return jsonCodec.decode(bytes); } else { if (log.isDebugEnabled()) { - log.debug("[decode][检测到二进制协议] 数据长度: {}字节", bytes.length); + log.debug("[decode][检测到二进制协议,数据长度: {} 字节]", bytes.length); } return binaryCodec.decode(bytes); } @@ -63,7 +66,7 @@ public class IotTcpCodecManager implements IotDeviceMessageCodec { // ==================== 便捷方法 ==================== /** - * 使用JSON协议编码 + * 使用 JSON 协议编码 */ public byte[] encodeJson(IotDeviceMessage message) { return jsonCodec.encode(message); @@ -95,42 +98,46 @@ public class IotTcpCodecManager implements IotDeviceMessageCodec { /** * 检测是否为JSON格式 - * + * * 检测规则: * 1. 数据以 '{' 开头 * 2. 包含 "method" 或 "id" 字段 */ private boolean isJsonFormat(byte[] bytes) { + // TODO @haohao:ArrayUtil.isEmpty(bytes) 可以简化下 if (bytes == null || bytes.length == 0) { return useJsonByDefault; } try { - // 检测JSON格式:以 '{' 开头 + // 检测 JSON 格式:以 '{' 开头 if (bytes[0] == '{') { - // 进一步验证是否为有效JSON + // TODO @haohao:不一定按照顺序写,这个可能要看下。 + // 进一步验证是否为有效 JSON String jsonStr = new String(bytes, 0, Math.min(bytes.length, 100)); return jsonStr.contains("\"method\"") || jsonStr.contains("\"id\""); } // 检测二进制格式:长度 >= 8 且符合二进制协议结构 if (bytes.length >= 8) { - // 读取包头(前4字节表示后续数据长度) + // 读取包头(前 4 字节表示后续数据长度) int expectedLength = ((bytes[0] & 0xFF) << 24) | ((bytes[1] & 0xFF) << 16) | ((bytes[2] & 0xFF) << 8) | (bytes[3] & 0xFF); - + // 验证长度是否合理 + // TODO @haohao:expectedLength > 0 多余的貌似; if (expectedLength == bytes.length - 4 && expectedLength > 0 && expectedLength < 1024 * 1024) { return false; // 二进制格式 } } } catch (Exception e) { - log.warn("[isJsonFormat][协议检测异常] 使用默认协议: {}", getDefaultProtocol(), e); + log.warn("[isJsonFormat][协议检测异常,使用默认协议: {}]", getDefaultProtocol(), e); } // 默认使用当前设置的协议类型 return useJsonByDefault; } + } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java index ac8a3d174d..39e8b83d24 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java @@ -12,16 +12,16 @@ import org.springframework.stereotype.Component; import java.nio.charset.StandardCharsets; /** - * TCP JSON格式 {@link IotDeviceMessage} 编解码器 - * - * 采用纯JSON格式传输,参考EMQX和HTTP模块的数据格式 - * + * TCP JSON 格式 {@link IotDeviceMessage} 编解码器 + * + * 采用纯 JSON 格式传输,参考 EMQX 和 HTTP 模块的数据格式 + * * JSON消息格式: * { - * "id": "消息ID", + * "id": "消息 ID", * "method": "消息方法", - * "deviceId": "设备ID", - * "productKey": "产品Key", + * "deviceId": "设备 ID", + * "productKey": "产品 Key", * "deviceName": "设备名称", * "params": {...}, * "timestamp": 时间戳 @@ -35,6 +35,7 @@ public class IotTcpJsonDeviceMessageCodec implements IotDeviceMessageCodec { public static final String TYPE = "TCP_JSON"; + // TODO @haohao:变量不太对; // ==================== 常量定义 ==================== @Override @@ -77,14 +78,15 @@ public class IotTcpJsonDeviceMessageCodec implements IotDeviceMessageCodec { } try { - // 转换为JSON字符串 + // 转换为 JSON 字符串 String jsonString = new String(bytes, StandardCharsets.UTF_8); if (log.isDebugEnabled()) { log.debug("[decode][开始解码] JSON长度: {}字节, 内容: {}", bytes.length, jsonString); } - // 解析JSON消息 + // 解析 JSON 消息 + // TODO @haohao:JsonUtils JSONObject jsonMessage = JSONUtil.parseObj(jsonString); // 构建IoT设备消息 @@ -129,7 +131,7 @@ public class IotTcpJsonDeviceMessageCodec implements IotDeviceMessageCodec { } /** - * 构建JSON消息 + * 构建 JSON 消息 */ private JSONObject buildJsonMessage(IotDeviceMessage message) { JSONObject jsonMessage = new JSONObject(); @@ -189,7 +191,7 @@ public class IotTcpJsonDeviceMessageCodec implements IotDeviceMessageCodec { message.setMsg(msg); } - // 设置服务ID(基于JSON格式) + // 设置服务 ID(基于 JSON 格式) message.setServerId(generateServerId(jsonMessage)); return message; @@ -216,22 +218,26 @@ public class IotTcpJsonDeviceMessageCodec implements IotDeviceMessageCodec { StrUtil.isNotEmpty(id) ? id.substring(0, Math.min(8, id.length())) : "noId"); } + // TODO @haohao:注释格式不对; /** * 消息方法常量 */ public static class MessageMethod { + public static final String PROPERTY_POST = "thing.property.post"; // 数据上报 public static final String STATE_ONLINE = "thing.state.online"; // 心跳 public static final String EVENT_POST = "thing.event.post"; // 事件上报 public static final String PROPERTY_SET = "thing.property.set"; // 属性设置 public static final String PROPERTY_GET = "thing.property.get"; // 属性获取 public static final String SERVICE_INVOKE = "thing.service.invoke"; // 服务调用 + } /** * JSON字段名(参考EMQX和HTTP模块格式) */ private static class JsonField { + public static final String ID = "id"; public static final String METHOD = "method"; public static final String DEVICE_ID = "deviceId"; @@ -241,5 +247,6 @@ public class IotTcpJsonDeviceMessageCodec implements IotDeviceMessageCodec { public static final String TIMESTAMP = "timestamp"; public static final String CODE = "code"; public static final String MESSAGE = "message"; + } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java index 0e2ad6c4e1..1de7e2e0c3 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java @@ -30,6 +30,7 @@ public class IotTcpUpstreamProtocol { private final IotDeviceMessageService messageService; + // TODO @haohao:不用的变量,可以删除; private final IotDeviceCommonApi deviceApi; private final IotTcpCodecManager codecManager; @@ -58,6 +59,7 @@ public class IotTcpUpstreamProtocol { @PostConstruct public void start() { + // TODO @haohao:类似下面 62 到 75 是处理 options 的,因为中间写了注释,其实可以不用空行;然后 77 到 91 可以中间空喊去掉,更紧凑一点; // 创建服务器选项 NetServerOptions options = new NetServerOptions() .setPort(tcpProperties.getPort()) diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java index 919606475b..053be8d437 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java @@ -1,7 +1,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpDeviceMessageCodec; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import lombok.extern.slf4j.Slf4j; @@ -24,11 +23,12 @@ public class IotTcpDownstreamHandler { private final IotDeviceMessageService messageService; - private final IotTcpDeviceMessageCodec codec; + // TODO @haohao:代码没提交全,有报错。 +// private final IotTcpDeviceMessageCodec codec; public IotTcpDownstreamHandler(IotDeviceMessageService messageService) { this.messageService = messageService; - this.codec = new IotTcpDeviceMessageCodec(); +// this.codec = new IotTcpDeviceMessageCodec(); } /** @@ -42,7 +42,8 @@ public class IotTcpDownstreamHandler { message.getDeviceId(), message.getMethod(), message.getId()); // 编码消息用于日志记录和验证 - byte[] encodedMessage = codec.encode(message); + byte[] encodedMessage = null; +// codec.encode(message); log.debug("[handle][消息编码成功] 设备ID: {}, 编码后长度: {} 字节", message.getDeviceId(), encodedMessage.length); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpBinaryDataPacketExamples.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpBinaryDataPacketExamples.java index 56926569ce..123fed4be7 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpBinaryDataPacketExamples.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpBinaryDataPacketExamples.java @@ -6,6 +6,7 @@ import lombok.extern.slf4j.Slf4j; import java.util.HashMap; import java.util.Map; +// TODO @haohao:这种写成单测,会好点 /** * TCP二进制格式数据包示例 * @@ -21,13 +22,13 @@ public class TcpBinaryDataPacketExamples { public static void main(String[] args) { IotTcpBinaryDeviceMessageCodec codec = new IotTcpBinaryDeviceMessageCodec(); - + // 1. 数据上报包示例 demonstrateDataReport(codec); - + // 2. 心跳包示例 demonstrateHeartbeat(codec); - + // 3. 复杂数据上报示例 demonstrateComplexDataReport(codec); } @@ -37,23 +38,23 @@ public class TcpBinaryDataPacketExamples { */ private static void demonstrateDataReport(IotTcpBinaryDeviceMessageCodec codec) { log.info("=== 二进制格式数据上报包示例 ==="); - + // 创建传感器数据 Map sensorData = new HashMap<>(); sensorData.put("temperature", 25.5); sensorData.put("humidity", 60.2); sensorData.put("pressure", 1013.25); sensorData.put("battery", 85); - + // 创建设备消息 IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", sensorData); message.setDeviceId(123456L); - + // 编码 byte[] packet = codec.encode(message); log.info("编码后数据包长度: {} 字节", packet.length); log.info("编码后数据包(HEX): {}", bytesToHex(packet)); - + // 解码验证 IotDeviceMessage decoded = codec.decode(packet); log.info("解码后消息ID: {}", decoded.getId()); @@ -63,7 +64,7 @@ public class TcpBinaryDataPacketExamples { log.info("解码后服务ID: {}", decoded.getServerId()); log.info("解码后上报时间: {}", decoded.getReportTime()); log.info("解码后参数: {}", decoded.getParams()); - + System.out.println(); } @@ -72,16 +73,16 @@ public class TcpBinaryDataPacketExamples { */ private static void demonstrateHeartbeat(IotTcpBinaryDeviceMessageCodec codec) { log.info("=== 二进制格式心跳包示例 ==="); - + // 创建心跳消息 IotDeviceMessage heartbeat = IotDeviceMessage.requestOf("thing.state.online", null); heartbeat.setDeviceId(123456L); - + // 编码 byte[] packet = codec.encode(heartbeat); log.info("心跳包长度: {} 字节", packet.length); log.info("心跳包(HEX): {}", bytesToHex(packet)); - + // 解码验证 IotDeviceMessage decoded = codec.decode(packet); log.info("解码后消息ID: {}", decoded.getId()); @@ -90,7 +91,7 @@ public class TcpBinaryDataPacketExamples { log.info("解码后设备ID: {}", decoded.getDeviceId()); log.info("解码后服务ID: {}", decoded.getServerId()); log.info("解码后参数: {}", decoded.getParams()); - + System.out.println(); } @@ -99,40 +100,40 @@ public class TcpBinaryDataPacketExamples { */ private static void demonstrateComplexDataReport(IotTcpBinaryDeviceMessageCodec codec) { log.info("=== 二进制格式复杂数据上报示例 ==="); - + // 创建复杂设备数据 Map deviceData = new HashMap<>(); - + // 环境数据 Map environment = new HashMap<>(); environment.put("temperature", 23.8); environment.put("humidity", 55.0); environment.put("co2", 420); deviceData.put("environment", environment); - + // GPS数据 Map location = new HashMap<>(); location.put("latitude", 39.9042); location.put("longitude", 116.4074); location.put("altitude", 43.5); deviceData.put("location", location); - + // 设备状态 Map status = new HashMap<>(); status.put("battery", 78); status.put("signal", -65); status.put("online", true); deviceData.put("status", status); - + // 创建设备消息 IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", deviceData); message.setDeviceId(789012L); - + // 编码 byte[] packet = codec.encode(message); log.info("复杂数据包长度: {} 字节", packet.length); log.info("复杂数据包(HEX): {}", bytesToHex(packet)); - + // 解码验证 IotDeviceMessage decoded = codec.decode(packet); log.info("解码后消息ID: {}", decoded.getId()); @@ -141,7 +142,7 @@ public class TcpBinaryDataPacketExamples { log.info("解码后设备ID: {}", decoded.getDeviceId()); log.info("解码后服务ID: {}", decoded.getServerId()); log.info("解码后参数: {}", decoded.getParams()); - + System.out.println(); } @@ -164,9 +165,9 @@ public class TcpBinaryDataPacketExamples { log.error("数据包长度不足"); return; } - + int index = 0; - + // 解析包头(4字节) - 后续数据长度 int totalLength = ((packet[index] & 0xFF) << 24) | ((packet[index + 1] & 0xFF) << 16) | @@ -174,27 +175,27 @@ public class TcpBinaryDataPacketExamples { (packet[index + 3] & 0xFF); index += 4; log.info("包头 - 后续数据长度: {} 字节", totalLength); - + // 解析设备地址长度(2字节) int addrLength = ((packet[index] & 0xFF) << 8) | (packet[index + 1] & 0xFF); index += 2; log.info("设备地址长度: {} 字节", addrLength); - + // 解析设备地址 String deviceAddr = new String(packet, index, addrLength); index += addrLength; log.info("设备地址: {}", deviceAddr); - + // 解析功能码(2字节) int functionCode = ((packet[index] & 0xFF) << 8) | (packet[index + 1] & 0xFF); index += 2; log.info("功能码: {} ({})", functionCode, getFunctionCodeName(functionCode)); - + // 解析消息序号(2字节) int messageId = ((packet[index] & 0xFF) << 8) | (packet[index + 1] & 0xFF); index += 2; log.info("消息序号: {}", messageId); - + // 解析包体数据 if (index < packet.length) { String payload = new String(packet, index, packet.length - index); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpJsonDataPacketExamples.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpJsonDataPacketExamples.java index d53731fe9a..7334bd8dd3 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpJsonDataPacketExamples.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpJsonDataPacketExamples.java @@ -7,9 +7,10 @@ import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; +// TODO @haohao:这种写成单测,会好点 /** * TCP JSON格式数据包示例 - * + * * 演示如何使用新的JSON格式进行TCP消息编解码 * * @author 芋道源码 @@ -19,22 +20,22 @@ public class TcpJsonDataPacketExamples { public static void main(String[] args) { IotTcpJsonDeviceMessageCodec codec = new IotTcpJsonDeviceMessageCodec(); - + // 1. 数据上报示例 demonstrateDataReport(codec); - + // 2. 心跳示例 demonstrateHeartbeat(codec); - + // 3. 事件上报示例 demonstrateEventReport(codec); - + // 4. 复杂数据上报示例 demonstrateComplexDataReport(codec); - + // 5. 便捷方法示例 demonstrateConvenienceMethods(); - + // 6. EMQX兼容性示例 demonstrateEmqxCompatibility(); } @@ -44,24 +45,24 @@ public class TcpJsonDataPacketExamples { */ private static void demonstrateDataReport(IotTcpJsonDeviceMessageCodec codec) { log.info("=== JSON格式数据上报示例 ==="); - + // 创建传感器数据 Map sensorData = new HashMap<>(); sensorData.put("temperature", 25.5); sensorData.put("humidity", 60.2); sensorData.put("pressure", 1013.25); sensorData.put("battery", 85); - + // 创建设备消息 IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", sensorData); message.setDeviceId(123456L); - + // 编码 byte[] packet = codec.encode(message); String jsonString = new String(packet, StandardCharsets.UTF_8); log.info("编码后JSON: {}", jsonString); log.info("数据包长度: {} 字节", packet.length); - + // 解码验证 IotDeviceMessage decoded = codec.decode(packet); log.info("解码后消息ID: {}", decoded.getId()); @@ -69,7 +70,7 @@ public class TcpJsonDataPacketExamples { log.info("解码后设备ID: {}", decoded.getDeviceId()); log.info("解码后服务ID: {}", decoded.getServerId()); log.info("解码后参数: {}", decoded.getParams()); - + System.out.println(); } @@ -78,24 +79,24 @@ public class TcpJsonDataPacketExamples { */ private static void demonstrateHeartbeat(IotTcpJsonDeviceMessageCodec codec) { log.info("=== JSON格式心跳示例 ==="); - + // 创建心跳消息 IotDeviceMessage heartbeat = IotDeviceMessage.requestOf("thing.state.online", null); heartbeat.setDeviceId(123456L); - + // 编码 byte[] packet = codec.encode(heartbeat); String jsonString = new String(packet, StandardCharsets.UTF_8); log.info("编码后JSON: {}", jsonString); log.info("心跳包长度: {} 字节", packet.length); - + // 解码验证 IotDeviceMessage decoded = codec.decode(packet); log.info("解码后消息ID: {}", decoded.getId()); log.info("解码后方法: {}", decoded.getMethod()); log.info("解码后设备ID: {}", decoded.getDeviceId()); log.info("解码后服务ID: {}", decoded.getServerId()); - + System.out.println(); } @@ -104,31 +105,31 @@ public class TcpJsonDataPacketExamples { */ private static void demonstrateEventReport(IotTcpJsonDeviceMessageCodec codec) { log.info("=== JSON格式事件上报示例 ==="); - + // 创建事件数据 Map eventData = new HashMap<>(); eventData.put("eventType", "alarm"); eventData.put("level", "warning"); eventData.put("description", "温度过高"); eventData.put("value", 45.8); - + // 创建事件消息 IotDeviceMessage event = IotDeviceMessage.requestOf("thing.event.post", eventData); event.setDeviceId(123456L); - + // 编码 byte[] packet = codec.encode(event); String jsonString = new String(packet, StandardCharsets.UTF_8); log.info("编码后JSON: {}", jsonString); log.info("事件包长度: {} 字节", packet.length); - + // 解码验证 IotDeviceMessage decoded = codec.decode(packet); log.info("解码后消息ID: {}", decoded.getId()); log.info("解码后方法: {}", decoded.getMethod()); log.info("解码后设备ID: {}", decoded.getDeviceId()); log.info("解码后参数: {}", decoded.getParams()); - + System.out.println(); } @@ -137,10 +138,10 @@ public class TcpJsonDataPacketExamples { */ private static void demonstrateComplexDataReport(IotTcpJsonDeviceMessageCodec codec) { log.info("=== JSON格式复杂数据上报示例 ==="); - + // 创建复杂设备数据(类似EMQX格式) Map deviceData = new HashMap<>(); - + // 环境数据 Map environment = new HashMap<>(); environment.put("temperature", 23.8); @@ -148,7 +149,7 @@ public class TcpJsonDataPacketExamples { environment.put("co2", 420); environment.put("pm25", 35); deviceData.put("environment", environment); - + // GPS数据 Map location = new HashMap<>(); location.put("latitude", 39.9042); @@ -156,7 +157,7 @@ public class TcpJsonDataPacketExamples { location.put("altitude", 43.5); location.put("speed", 0.0); deviceData.put("location", location); - + // 设备状态 Map status = new HashMap<>(); status.put("battery", 78); @@ -164,24 +165,24 @@ public class TcpJsonDataPacketExamples { status.put("online", true); status.put("version", "1.2.3"); deviceData.put("status", status); - + // 创建设备消息 IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", deviceData); message.setDeviceId(789012L); - + // 编码 byte[] packet = codec.encode(message); String jsonString = new String(packet, StandardCharsets.UTF_8); log.info("编码后JSON: {}", jsonString); log.info("复杂数据包长度: {} 字节", packet.length); - + // 解码验证 IotDeviceMessage decoded = codec.decode(packet); log.info("解码后消息ID: {}", decoded.getId()); log.info("解码后方法: {}", decoded.getMethod()); log.info("解码后设备ID: {}", decoded.getDeviceId()); log.info("解码后参数: {}", decoded.getParams()); - + System.out.println(); } @@ -190,9 +191,9 @@ public class TcpJsonDataPacketExamples { */ private static void demonstrateConvenienceMethods() { log.info("=== 便捷方法示例 ==="); - + IotTcpJsonDeviceMessageCodec codec = new IotTcpJsonDeviceMessageCodec(); - + // 使用便捷方法编码数据上报 Map sensorData = Map.of( "temperature", 26.5, @@ -200,11 +201,11 @@ public class TcpJsonDataPacketExamples { ); byte[] dataPacket = codec.encodeDataReport(sensorData, 123456L, "smart_sensor", "device_001"); log.info("便捷方法编码数据上报: {}", new String(dataPacket, StandardCharsets.UTF_8)); - + // 使用便捷方法编码心跳 byte[] heartbeatPacket = codec.encodeHeartbeat(123456L, "smart_sensor", "device_001"); log.info("便捷方法编码心跳: {}", new String(heartbeatPacket, StandardCharsets.UTF_8)); - + // 使用便捷方法编码事件 Map eventData = Map.of( "eventType", "maintenance", @@ -212,7 +213,7 @@ public class TcpJsonDataPacketExamples { ); byte[] eventPacket = codec.encodeEventReport(eventData, 123456L, "smart_sensor", "device_001"); log.info("便捷方法编码事件: {}", new String(eventPacket, StandardCharsets.UTF_8)); - + System.out.println(); } @@ -221,7 +222,7 @@ public class TcpJsonDataPacketExamples { */ private static void demonstrateEmqxCompatibility() { log.info("=== EMQX格式兼容性示例 ==="); - + // 模拟EMQX风格的消息格式 String emqxStyleJson = """ { @@ -235,19 +236,19 @@ public class TcpJsonDataPacketExamples { "timestamp": 1642781234567 } """; - + IotTcpJsonDeviceMessageCodec codec = new IotTcpJsonDeviceMessageCodec(); - + // 解码EMQX风格的消息 byte[] emqxBytes = emqxStyleJson.getBytes(StandardCharsets.UTF_8); IotDeviceMessage decoded = codec.decode(emqxBytes); - + log.info("EMQX风格消息解码成功:"); log.info("消息ID: {}", decoded.getId()); log.info("方法: {}", decoded.getMethod()); log.info("设备ID: {}", decoded.getDeviceId()); log.info("参数: {}", decoded.getParams()); - + System.out.println(); } } From 0cd7bca209bd2206afd428e27eef144ce3505169 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Wed, 23 Jul 2025 20:59:26 +0800 Subject: [PATCH 54/89] =?UTF-8?q?chore=EF=BC=9Amybatis=20from=203.5.10.1?= =?UTF-8?q?=20to=203.5.12?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yudao-dependencies/pom.xml | 4 +-- .../mybatis/core/query/MPJLambdaWrapperX.java | 36 ------------------- 2 files changed, 2 insertions(+), 38 deletions(-) diff --git a/yudao-dependencies/pom.xml b/yudao-dependencies/pom.xml index 27ad06fe5d..d13f89b127 100644 --- a/yudao-dependencies/pom.xml +++ b/yudao-dependencies/pom.xml @@ -24,9 +24,9 @@ 1.2.24 3.5.19 - 3.5.10.1 + 3.5.12 + 1.5.4 4.3.1 - 1.4.13 3.0.6 3.41.0 8.1.3.140 diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/query/MPJLambdaWrapperX.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/query/MPJLambdaWrapperX.java index 48e901d624..ded0384000 100644 --- a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/query/MPJLambdaWrapperX.java +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/query/MPJLambdaWrapperX.java @@ -207,12 +207,6 @@ public class MPJLambdaWrapperX extends MPJLambdaWrapper { return this; } - @Override - public MPJLambdaWrapperX selectCount(SFunction column, String alias) { - super.selectCount(column, alias); - return this; - } - @Override public MPJLambdaWrapperX selectCount(SFunction column, SFunction alias) { super.selectCount(column, alias); @@ -225,12 +219,6 @@ public class MPJLambdaWrapperX extends MPJLambdaWrapper { return this; } - @Override - public MPJLambdaWrapperX selectSum(SFunction column, String alias) { - super.selectSum(column, alias); - return this; - } - @Override public MPJLambdaWrapperX selectSum(SFunction column, SFunction alias) { super.selectSum(column, alias); @@ -243,12 +231,6 @@ public class MPJLambdaWrapperX extends MPJLambdaWrapper { return this; } - @Override - public MPJLambdaWrapperX selectMax(SFunction column, String alias) { - super.selectMax(column, alias); - return this; - } - @Override public MPJLambdaWrapperX selectMax(SFunction column, SFunction alias) { super.selectMax(column, alias); @@ -261,12 +243,6 @@ public class MPJLambdaWrapperX extends MPJLambdaWrapper { return this; } - @Override - public MPJLambdaWrapperX selectMin(SFunction column, String alias) { - super.selectMin(column, alias); - return this; - } - @Override public MPJLambdaWrapperX selectMin(SFunction column, SFunction alias) { super.selectMin(column, alias); @@ -279,12 +255,6 @@ public class MPJLambdaWrapperX extends MPJLambdaWrapper { return this; } - @Override - public MPJLambdaWrapperX selectAvg(SFunction column, String alias) { - super.selectAvg(column, alias); - return this; - } - @Override public MPJLambdaWrapperX selectAvg(SFunction column, SFunction alias) { super.selectAvg(column, alias); @@ -297,12 +267,6 @@ public class MPJLambdaWrapperX extends MPJLambdaWrapper { return this; } - @Override - public MPJLambdaWrapperX selectLen(SFunction column, String alias) { - super.selectLen(column, alias); - return this; - } - @Override public MPJLambdaWrapperX selectLen(SFunction column, SFunction alias) { super.selectLen(column, alias); From d650a0b1239dd3454214309d04a2c9e9096d25ca Mon Sep 17 00:00:00 2001 From: YunaiV Date: Wed, 23 Jul 2025 21:40:55 +0800 Subject: [PATCH 55/89] =?UTF-8?q?chore=EF=BC=9Amybatis=20from=203.5.10.1?= =?UTF-8?q?=20to=203.5.12?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mybatis/core/query/MPJLambdaWrapperX.java | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/query/MPJLambdaWrapperX.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/query/MPJLambdaWrapperX.java index ded0384000..8b5a0fcfc8 100644 --- a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/query/MPJLambdaWrapperX.java +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/query/MPJLambdaWrapperX.java @@ -118,7 +118,6 @@ public class MPJLambdaWrapperX extends MPJLambdaWrapper { @Override public MPJLambdaWrapperX orderByDesc(SFunction column) { - //noinspection unchecked super.orderByDesc(true, column); return this; } @@ -207,6 +206,12 @@ public class MPJLambdaWrapperX extends MPJLambdaWrapper { return this; } + @Override + public MPJLambdaWrapperX selectCount(SFunction column, String alias) { + super.selectCount(column, alias); + return this; + } + @Override public MPJLambdaWrapperX selectCount(SFunction column, SFunction alias) { super.selectCount(column, alias); @@ -219,6 +224,12 @@ public class MPJLambdaWrapperX extends MPJLambdaWrapper { return this; } + @Override + public MPJLambdaWrapperX selectSum(SFunction column, String alias) { + super.selectSum(column, alias); + return this; + } + @Override public MPJLambdaWrapperX selectSum(SFunction column, SFunction alias) { super.selectSum(column, alias); @@ -231,6 +242,12 @@ public class MPJLambdaWrapperX extends MPJLambdaWrapper { return this; } + @Override + public MPJLambdaWrapperX selectMax(SFunction column, String alias) { + super.selectMax(column, alias); + return this; + } + @Override public MPJLambdaWrapperX selectMax(SFunction column, SFunction alias) { super.selectMax(column, alias); @@ -243,6 +260,12 @@ public class MPJLambdaWrapperX extends MPJLambdaWrapper { return this; } + @Override + public MPJLambdaWrapperX selectMin(SFunction column, String alias) { + super.selectMin(column, alias); + return this; + } + @Override public MPJLambdaWrapperX selectMin(SFunction column, SFunction alias) { super.selectMin(column, alias); @@ -255,6 +278,12 @@ public class MPJLambdaWrapperX extends MPJLambdaWrapper { return this; } + @Override + public MPJLambdaWrapperX selectAvg(SFunction column, String alias) { + super.selectAvg(column, alias); + return this; + } + @Override public MPJLambdaWrapperX selectAvg(SFunction column, SFunction alias) { super.selectAvg(column, alias); @@ -267,6 +296,12 @@ public class MPJLambdaWrapperX extends MPJLambdaWrapper { return this; } + @Override + public MPJLambdaWrapperX selectLen(SFunction column, String alias) { + super.selectLen(column, alias); + return this; + } + @Override public MPJLambdaWrapperX selectLen(SFunction column, SFunction alias) { super.selectLen(column, alias); From d128d787188fdae094528d477ae1672c430db0ae Mon Sep 17 00:00:00 2001 From: YunaiV Date: Wed, 23 Jul 2025 22:17:54 +0800 Subject: [PATCH 56/89] =?UTF-8?q?reactor=EF=BC=9Aeasy-trans=20=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=E7=A6=81=E7=94=A8=EF=BC=8C=E9=81=BF=E5=85=8D=E5=BD=B1?= =?UTF-8?q?=E5=93=8D=E6=8E=A5=E5=8F=A3=E6=80=A7=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/admin/chat/AiChatConversationController.java | 4 ++++ .../ai/controller/admin/model/AiChatRoleController.java | 4 ++++ .../crm/controller/admin/product/CrmProductController.java | 3 +++ .../iot/controller/admin/ota/IotOtaFirmwareController.java | 2 ++ .../controller/admin/ota/IotOtaUpgradeRecordController.java | 3 +++ .../system/controller/admin/logger/OperateLogController.java | 2 ++ yudao-server/src/main/resources/application.yaml | 2 +- 7 files changed, 19 insertions(+), 1 deletion(-) diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatConversationController.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatConversationController.java index 5142cde443..ddd426d283 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatConversationController.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatConversationController.java @@ -12,6 +12,7 @@ import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation.AiChatCo import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO; import cn.iocoder.yudao.module.ai.service.chat.AiChatConversationService; import cn.iocoder.yudao.module.ai.service.chat.AiChatMessageService; +import com.fhs.core.trans.anno.TransMethodResult; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; @@ -54,6 +55,7 @@ public class AiChatConversationController { @GetMapping("/my-list") @Operation(summary = "获得【我的】聊天对话列表") + @TransMethodResult public CommonResult> getChatConversationMyList() { List list = chatConversationService.getChatConversationListByUserId(getLoginUserId()); return success(BeanUtils.toBean(list, AiChatConversationRespVO.class)); @@ -62,6 +64,7 @@ public class AiChatConversationController { @GetMapping("/get-my") @Operation(summary = "获得【我的】聊天对话") @Parameter(name = "id", required = true, description = "对话编号", example = "1024") + @TransMethodResult public CommonResult getChatConversationMy(@RequestParam("id") Long id) { AiChatConversationDO conversation = chatConversationService.getChatConversation(id); if (conversation != null && ObjUtil.notEqual(conversation.getUserId(), getLoginUserId())) { @@ -90,6 +93,7 @@ public class AiChatConversationController { @GetMapping("/page") @Operation(summary = "获得对话分页", description = "用于【对话管理】菜单") @PreAuthorize("@ss.hasPermission('ai:chat-conversation:query')") + @TransMethodResult public CommonResult> getChatConversationPage(AiChatConversationPageReqVO pageReqVO) { PageResult pageResult = chatConversationService.getChatConversationPage(pageReqVO); if (CollUtil.isEmpty(pageResult.getList())) { diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiChatRoleController.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiChatRoleController.java index 5714c5fedd..804e211527 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiChatRoleController.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiChatRoleController.java @@ -10,6 +10,7 @@ import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatRole.AiChatRoleS import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatRole.AiChatRoleSaveReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService; +import com.fhs.core.trans.anno.TransMethodResult; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; @@ -35,6 +36,7 @@ public class AiChatRoleController { @GetMapping("/my-page") @Operation(summary = "获得【我的】聊天角色分页") + @TransMethodResult public CommonResult> getChatRoleMyPage(@Valid AiChatRolePageReqVO pageReqVO) { PageResult pageResult = chatRoleService.getChatRoleMyPage(pageReqVO, getLoginUserId()); return success(BeanUtils.toBean(pageResult, AiChatRoleRespVO.class)); @@ -43,6 +45,7 @@ public class AiChatRoleController { @GetMapping("/get-my") @Operation(summary = "获得【我的】聊天角色") @Parameter(name = "id", description = "编号", required = true, example = "1024") + @TransMethodResult public CommonResult getChatRoleMy(@RequestParam("id") Long id) { AiChatRoleDO chatRole = chatRoleService.getChatRole(id); if (ObjUtil.notEqual(chatRole.getUserId(), getLoginUserId())) { @@ -108,6 +111,7 @@ public class AiChatRoleController { @Operation(summary = "获得聊天角色") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('ai:chat-role:query')") + @TransMethodResult public CommonResult getChatRole(@RequestParam("id") Long id) { AiChatRoleDO chatRole = chatRoleService.getChatRole(id); return success(BeanUtils.toBean(chatRole, AiChatRoleRespVO.class)); diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/CrmProductController.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/CrmProductController.java index bf98a80606..adefc77556 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/CrmProductController.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/CrmProductController.java @@ -14,6 +14,7 @@ import cn.iocoder.yudao.module.crm.dal.dataobject.product.CrmProductDO; import cn.iocoder.yudao.module.crm.enums.product.CrmProductStatusEnum; import cn.iocoder.yudao.module.crm.service.product.CrmProductService; import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import com.fhs.core.trans.anno.TransMethodResult; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; @@ -70,6 +71,7 @@ public class CrmProductController { @Operation(summary = "获得产品") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('crm:product:query')") + @TransMethodResult public CommonResult getProduct(@RequestParam("id") Long id) { CrmProductDO product = productService.getProduct(id); return success(BeanUtils.toBean(product, CrmProductRespVO.class)); @@ -86,6 +88,7 @@ public class CrmProductController { @GetMapping("/page") @Operation(summary = "获得产品分页") @PreAuthorize("@ss.hasPermission('crm:product:query')") + @TransMethodResult public CommonResult> getProductPage(@Valid CrmProductPageReqVO pageVO) { PageResult pageResult = productService.getProductPage(pageVO); return success(BeanUtils.toBean(pageResult, CrmProductRespVO.class)); diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaFirmwareController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaFirmwareController.java index 6cc3918e8f..38407333ea 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaFirmwareController.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaFirmwareController.java @@ -9,6 +9,7 @@ import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwa import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwareUpdateReqVO; import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; import cn.iocoder.yudao.module.iot.service.ota.IotOtaFirmwareService; +import com.fhs.core.trans.anno.TransMethodResult; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; @@ -46,6 +47,7 @@ public class IotOtaFirmwareController { @GetMapping("/get") @Operation(summary = "获得 OTA 固件") @PreAuthorize("@ss.hasPermission('iot:ota-firmware:query')") + @TransMethodResult public CommonResult getOtaFirmware(@RequestParam("id") Long id) { IotOtaFirmwareDO otaFirmware = otaFirmwareService.getOtaFirmware(id); return success(BeanUtils.toBean(otaFirmware, IotOtaFirmwareRespVO.class)); diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaUpgradeRecordController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaUpgradeRecordController.java index f6bc526ac2..b18caecfb8 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaUpgradeRecordController.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaUpgradeRecordController.java @@ -7,6 +7,7 @@ import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.record.IotOta import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.record.IotOtaUpgradeRecordRespVO; import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaUpgradeRecordDO; import cn.iocoder.yudao.module.iot.service.ota.IotOtaUpgradeRecordService; +import com.fhs.core.trans.anno.TransMethodResult; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; @@ -48,6 +49,7 @@ public class IotOtaUpgradeRecordController { @GetMapping("/page") @Operation(summary = "获得升级记录分页") @PreAuthorize("@ss.hasPermission('iot:ota-upgrade-record:query')") + @TransMethodResult public CommonResult> getUpgradeRecordPage( @Valid IotOtaUpgradeRecordPageReqVO pageReqVO) { PageResult pageResult = upgradeRecordService.getUpgradeRecordPage(pageReqVO); @@ -58,6 +60,7 @@ public class IotOtaUpgradeRecordController { @Operation(summary = "获得升级记录") @PreAuthorize("@ss.hasPermission('iot:ota-upgrade-record:query')") @Parameter(name = "id", description = "升级记录编号", required = true, example = "1024") + @TransMethodResult public CommonResult getUpgradeRecord(@RequestParam("id") Long id) { IotOtaUpgradeRecordDO upgradeRecord = upgradeRecordService.getUpgradeRecord(id); return success(BeanUtils.toBean(upgradeRecord, IotOtaUpgradeRecordRespVO.class)); diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/OperateLogController.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/OperateLogController.java index 71b45d346e..257477071a 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/OperateLogController.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/OperateLogController.java @@ -11,6 +11,7 @@ import cn.iocoder.yudao.module.system.controller.admin.logger.vo.operatelog.Oper import cn.iocoder.yudao.module.system.controller.admin.logger.vo.operatelog.OperateLogRespVO; import cn.iocoder.yudao.module.system.dal.dataobject.logger.OperateLogDO; import cn.iocoder.yudao.module.system.service.logger.OperateLogService; +import com.fhs.core.trans.anno.TransMethodResult; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; @@ -40,6 +41,7 @@ public class OperateLogController { @GetMapping("/page") @Operation(summary = "查看操作日志分页列表") @PreAuthorize("@ss.hasPermission('system:operate-log:query')") + @TransMethodResult public CommonResult> pageOperateLog(@Valid OperateLogPageReqVO pageReqVO) { PageResult pageResult = operateLogService.getOperateLogPage(pageReqVO); return success(BeanUtils.toBean(pageResult, OperateLogRespVO.class)); diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index 12a4abf896..6519cbf0e5 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -96,7 +96,7 @@ spring: # VO 转换(数据翻译)相关 easy-trans: - is-enable-global: true # 启用全局翻译(拦截所有 SpringMVC ResponseBody 进行自动翻译 )。如果对于性能要求很高可关闭此配置,或通过 @IgnoreTrans 忽略某个接口 + is-enable-global: false # 【默认禁用,对性能确认压力大】启用全局翻译(拦截所有 SpringMVC ResponseBody 进行自动翻译 )。如果对于性能要求很高可关闭此配置,或通过 @IgnoreTrans 忽略某个接口 --- #################### 验证码相关配置 #################### From d09e4849ba6d79d9906df1a4b7e17ffa314c52a1 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Thu, 24 Jul 2025 09:02:45 +0800 Subject: [PATCH 57/89] =?UTF-8?q?fix=EF=BC=9A=E3=80=90Infra=20=E5=9F=BA?= =?UTF-8?q?=E7=A1=80=E8=AE=BE=E7=BD=AE=E3=80=91=E6=94=AF=E6=8C=81=20yudao.?= =?UTF-8?q?websocket.enable=20=E6=89=8B=E5=8A=A8=E5=85=B3=E9=97=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/infra/api/websocket/WebSocketSenderApiImpl.java | 6 +++--- .../infra/websocket/DemoWebSocketMessageListener.java | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/api/websocket/WebSocketSenderApiImpl.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/api/websocket/WebSocketSenderApiImpl.java index cc060aafa1..52950537c3 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/api/websocket/WebSocketSenderApiImpl.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/api/websocket/WebSocketSenderApiImpl.java @@ -1,10 +1,9 @@ package cn.iocoder.yudao.module.infra.api.websocket; import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; -import jakarta.annotation.Resource; - /** * WebSocket 发送器的 API 实现类 * @@ -13,7 +12,8 @@ import jakarta.annotation.Resource; @Component public class WebSocketSenderApiImpl implements WebSocketSenderApi { - @Resource + @SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection") + @Autowired(required = false) // 由于 yudao.websocket.enable 配置项,可以关闭 WebSocket 的功能,所以这里只能不强制注入 private WebSocketMessageSender webSocketMessageSender; @Override diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/websocket/DemoWebSocketMessageListener.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/websocket/DemoWebSocketMessageListener.java index a3b71505bc..f0bdc69325 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/websocket/DemoWebSocketMessageListener.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/websocket/DemoWebSocketMessageListener.java @@ -6,11 +6,10 @@ import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender; import cn.iocoder.yudao.framework.websocket.core.util.WebSocketFrameworkUtils; import cn.iocoder.yudao.module.infra.websocket.message.DemoReceiveMessage; import cn.iocoder.yudao.module.infra.websocket.message.DemoSendMessage; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.socket.WebSocketSession; -import jakarta.annotation.Resource; - /** * WebSocket 示例:单发消息 * @@ -19,7 +18,8 @@ import jakarta.annotation.Resource; @Component public class DemoWebSocketMessageListener implements WebSocketMessageListener { - @Resource + @SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection") + @Autowired(required = false) // 由于 yudao.websocket.enable 配置项,可以关闭 WebSocket 的功能,所以这里只能不强制注入 private WebSocketMessageSender webSocketMessageSender; @Override From 97cec2897b85c9afddc6f52c93118c23817c379e Mon Sep 17 00:00:00 2001 From: jason <2667446@qq.com> Date: Thu, 24 Jul 2025 22:23:15 +0800 Subject: [PATCH 58/89] =?UTF-8?q?feat:=20[BPM=20=E5=B7=A5=E4=BD=9C?= =?UTF-8?q?=E6=B5=81]=20=E5=B9=B6=E8=A1=8C=E5=88=86=E6=94=AF=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E5=8C=85=E5=AE=B9=E7=BD=91=E5=85=B3=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BpmSimpleModelNodeTypeEnum.java | 2 +- .../flowable/core/util/SimpleModelUtils.java | 32 ++++++++++++------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModelNodeTypeEnum.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModelNodeTypeEnum.java index e5ffa12025..d97c145c3f 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModelNodeTypeEnum.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModelNodeTypeEnum.java @@ -35,7 +35,7 @@ public enum BpmSimpleModelNodeTypeEnum implements ArrayValuable { // 50 ~ 条件分支 CONDITION_NODE(50, "条件", "sequenceFlow"), // 用于构建流转条件的表达式 CONDITION_BRANCH_NODE(51, "条件分支", "exclusiveGateway"), - PARALLEL_BRANCH_NODE(52, "并行分支", "parallelGateway"), + PARALLEL_BRANCH_NODE(52, "并行分支", "inclusiveGateway"), // 并行分支使用包容网关实现,条件表达式结果设置为 true INCLUSIVE_BRANCH_NODE(53, "包容分支", "inclusiveGateway"), ROUTER_BRANCH_NODE(54, "路由分支", "exclusiveGateway") ; diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java index c9f3ff6fcb..5e9313840b 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java @@ -239,13 +239,13 @@ public class SimpleModelUtils { // 3.1 分支有后续节点。即分支 1: A->B->C->D 的情况 if (isValidNode(conditionChildNode)) { // 3.1.1 建立与后续的节点的连线。例如说,建立 A->B 的连线 - SequenceFlow sequenceFlow = ConditionNodeConvert.buildSequenceFlow(node.getId(), conditionChildNode.getId(), item); + SequenceFlow sequenceFlow = ConditionNodeConvert.buildSequenceFlow(node.getId(), conditionChildNode.getId(), nodeType, item); process.addFlowElement(sequenceFlow); // 3.1.2 递归调用后续节点连线。例如说,建立 B->C->D 的连线 traverseNodeToBuildSequenceFlow(process, conditionChildNode, branchEndNodeId); } else { // 3.2 分支没有后续节点。例如说,建立 A->D 的连线 - SequenceFlow sequenceFlow = ConditionNodeConvert.buildSequenceFlow(node.getId(), branchEndNodeId, item); + SequenceFlow sequenceFlow = ConditionNodeConvert.buildSequenceFlow(node.getId(), branchEndNodeId, nodeType, item); process.addFlowElement(sequenceFlow); } } @@ -591,17 +591,22 @@ public class SimpleModelUtils { private static class ParallelBranchNodeConvert implements NodeConvert { + /** + * 并行分支使用包容网关。需要设置所有出口条件表达式的值为 true. + * 参见: {@link ConditionNodeConvert#buildSequenceFlow} + */ @Override - public List convertList(BpmSimpleModelNodeVO node) { - ParallelGateway parallelGateway = new ParallelGateway(); - parallelGateway.setId(node.getId()); + public List convertList(BpmSimpleModelNodeVO node) { + + InclusiveGateway inclusiveGateway = new InclusiveGateway(); + inclusiveGateway.setId(node.getId()); // TODO @jason:setName - // 并行聚合网关由程序创建,前端不需要传入 - ParallelGateway joinParallelGateway = new ParallelGateway(); + // 合并网关 由程序创建,前端不需要传入 + InclusiveGateway joinParallelGateway = new InclusiveGateway(); joinParallelGateway.setId(buildGatewayJoinId(node.getId())); // TODO @jason:setName - return CollUtil.newArrayList(parallelGateway, joinParallelGateway); + return CollUtil.newArrayList(inclusiveGateway, joinParallelGateway); } @Override @@ -652,8 +657,14 @@ public class SimpleModelUtils { } public static SequenceFlow buildSequenceFlow(String sourceId, String targetId, - BpmSimpleModelNodeVO node) { - String conditionExpression = buildConditionExpression(node.getConditionSetting()); + BpmSimpleModelNodeTypeEnum nodeType, BpmSimpleModelNodeVO node) { + String conditionExpression; + // 并行分支,使用包容网关实现,强制设置条件表达式为 true + if (BpmSimpleModelNodeTypeEnum.PARALLEL_BRANCH_NODE == nodeType) { + conditionExpression ="${true}"; + } else { + conditionExpression = buildConditionExpression(node.getConditionSetting()); + } return buildBpmnSequenceFlow(sourceId, targetId, node.getId(), node.getName(), conditionExpression); } } @@ -662,7 +673,6 @@ public class SimpleModelUtils { * 构造条件表达式 */ public static String buildConditionExpression(BpmSimpleModelNodeVO.ConditionSetting conditionSetting) { - // 并行网关不需要设置条件 if (conditionSetting == null) { return null; } From 76d8906748839807cc50261bcb79b09a73aadca8 Mon Sep 17 00:00:00 2001 From: jason <2667446@qq.com> Date: Thu, 24 Jul 2025 23:07:15 +0800 Subject: [PATCH 59/89] =?UTF-8?q?fix:=20[BPM=20=E5=B7=A5=E4=BD=9C=E6=B5=81?= =?UTF-8?q?]=20=E8=8E=B7=E5=8F=96=E4=B8=8B=E4=B8=AA=E5=AE=A1=E6=89=B9?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E8=8A=82=E7=82=B9=E9=97=AE=E9=A2=98=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bpm/service/task/BpmProcessInstanceServiceImpl.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java index 9ce877b67c..85d15a6327 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java @@ -6,6 +6,7 @@ import cn.hutool.core.date.DateUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.*; import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; import cn.iocoder.yudao.framework.common.util.date.DateUtils; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; @@ -66,6 +67,7 @@ import java.util.*; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; import static cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmApprovalDetailRespVO.ActivityNode; import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*; import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.START_USER_NODE_ID; @@ -263,7 +265,9 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService // 3. 获取下一个将要执行的节点集合 FlowElement flowElement = bpmnModel.getFlowElement(task.getTaskDefinitionKey()); List nextFlowNodes = BpmnModelUtils.getNextFlowNodes(flowElement, bpmnModel, processVariables); - List nextActivityNodes = convertList(nextFlowNodes, node -> new ActivityNode().setId(node.getId()) + // 仅仅获取 UserTask 节点 TODO:如果网关节点和网关节点相连,获取下个 UserTask. 貌似有点不准。 + List nextUserTaskList = CollectionUtils.filterList(nextFlowNodes, node -> node instanceof UserTask); + List nextActivityNodes = convertList(nextUserTaskList, node -> new ActivityNode().setId(node.getId()) .setName(node.getName()).setNodeType(BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType()) .setStatus(BpmTaskStatusEnum.RUNNING.getStatus()) .setCandidateStrategy(BpmnModelUtils.parseCandidateStrategy(node)) From 4347f1336299664693f77f1a7aa3129f59a3d53b Mon Sep 17 00:00:00 2001 From: YunaiV Date: Fri, 25 Jul 2025 20:18:33 +0800 Subject: [PATCH 60/89] =?UTF-8?q?review=EF=BC=9A=E3=80=90BPM=20=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E6=B5=81=E3=80=91=E5=B9=B6=E8=A1=8C=E5=88=86=E6=94=AF?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E5=8C=85=E5=AE=B9=E7=BD=91=E5=85=B3=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=20=3D=E3=80=8B=E8=A1=A5=E5=85=85=E4=B8=80=E4=BA=9B?= =?UTF-8?q?=E6=B3=A8=E9=87=8A=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bpm/framework/flowable/core/util/SimpleModelUtils.java | 5 +++-- .../bpm/service/task/BpmProcessInstanceServiceImpl.java | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java index 5e9313840b..f3c00258e5 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java @@ -592,8 +592,9 @@ public class SimpleModelUtils { private static class ParallelBranchNodeConvert implements NodeConvert { /** - * 并行分支使用包容网关。需要设置所有出口条件表达式的值为 true. - * 参见: {@link ConditionNodeConvert#buildSequenceFlow} + * 并行分支使用包容网关。需要设置所有出口条件表达式的值为 true 。原因是,解决 https://t.zsxq.com/m6GXh 反馈问题 + * + * @see {@link ConditionNodeConvert#buildSequenceFlow} */ @Override public List convertList(BpmSimpleModelNodeVO node) { diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java index 85d15a6327..47265f5c72 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java @@ -265,7 +265,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService // 3. 获取下一个将要执行的节点集合 FlowElement flowElement = bpmnModel.getFlowElement(task.getTaskDefinitionKey()); List nextFlowNodes = BpmnModelUtils.getNextFlowNodes(flowElement, bpmnModel, processVariables); - // 仅仅获取 UserTask 节点 TODO:如果网关节点和网关节点相连,获取下个 UserTask. 貌似有点不准。 + // 仅仅获取 UserTask 节点 TODO add from jason:如果网关节点和网关节点相连,获取下个 UserTask. 貌似有点不准。 List nextUserTaskList = CollectionUtils.filterList(nextFlowNodes, node -> node instanceof UserTask); List nextActivityNodes = convertList(nextUserTaskList, node -> new ActivityNode().setId(node.getId()) .setName(node.getName()).setNodeType(BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType()) From 28965b9bbf5d5606f2b4762d09d1f3dd2f3af74f Mon Sep 17 00:00:00 2001 From: YunaiV Date: Fri, 25 Jul 2025 23:17:35 +0800 Subject: [PATCH 61/89] =?UTF-8?q?reactor=EF=BC=9A=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E8=BF=9E=E6=8E=A5=E6=B1=A0=E7=9A=84=E9=85=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yudao-server/src/main/resources/application-dev.yaml | 12 +++++++----- .../src/main/resources/application-local.yaml | 10 ++++++---- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/yudao-server/src/main/resources/application-dev.yaml b/yudao-server/src/main/resources/application-dev.yaml index 344e97ecf8..44c1cecfa3 100644 --- a/yudao-server/src/main/resources/application-dev.yaml +++ b/yudao-server/src/main/resources/application-dev.yaml @@ -33,14 +33,16 @@ spring: initial-size: 5 # 初始连接数 min-idle: 10 # 最小连接池数量 max-active: 20 # 最大连接池数量 - max-wait: 600000 # 配置获取连接等待超时的时间,单位:毫秒 - time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒 - min-evictable-idle-time-millis: 300000 # 配置一个连接在池中最小生存的时间,单位:毫秒 - max-evictable-idle-time-millis: 900000 # 配置一个连接在池中最大生存的时间,单位:毫秒 - validation-query: SELECT 1 # 配置检测连接是否有效 + max-wait: 60000 # 配置获取连接等待超时的时间,单位:毫秒(1 分钟) + time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒(1 分钟) + min-evictable-idle-time-millis: 600000 # 配置一个连接在池中最小生存的时间,单位:毫秒(10 分钟) + max-evictable-idle-time-millis: 1800000 # 配置一个连接在池中最大生存的时间,单位:毫秒(30 分钟) + validation-query: SELECT 1 FROM DUAL # 配置检测连接是否有效 test-while-idle: true test-on-borrow: false test-on-return: false + pool-prepared-statements: true # 是否开启 PreparedStatement 缓存 + max-pool-prepared-statement-per-connection-size: 20 # 每个连接缓存的 PreparedStatement 数量 primary: master datasource: master: diff --git a/yudao-server/src/main/resources/application-local.yaml b/yudao-server/src/main/resources/application-local.yaml index 5d23b9b70d..e868eca456 100644 --- a/yudao-server/src/main/resources/application-local.yaml +++ b/yudao-server/src/main/resources/application-local.yaml @@ -37,14 +37,16 @@ spring: initial-size: 1 # 初始连接数 min-idle: 1 # 最小连接池数量 max-active: 20 # 最大连接池数量 - max-wait: 600000 # 配置获取连接等待超时的时间,单位:毫秒 - time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒 - min-evictable-idle-time-millis: 300000 # 配置一个连接在池中最小生存的时间,单位:毫秒 - max-evictable-idle-time-millis: 900000 # 配置一个连接在池中最大生存的时间,单位:毫秒 + max-wait: 60000 # 配置获取连接等待超时的时间,单位:毫秒(1 分钟) + time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒(1 分钟) + min-evictable-idle-time-millis: 600000 # 配置一个连接在池中最小生存的时间,单位:毫秒(10 分钟) + max-evictable-idle-time-millis: 1800000 # 配置一个连接在池中最大生存的时间,单位:毫秒(30 分钟) validation-query: SELECT 1 FROM DUAL # 配置检测连接是否有效 test-while-idle: true test-on-borrow: false test-on-return: false + pool-prepared-statements: true # 是否开启 PreparedStatement 缓存 + max-pool-prepared-statement-per-connection-size: 20 # 每个连接缓存的 PreparedStatement 数量 primary: master datasource: master: From 529c570bbd7b700785f1297d8b47e56ff07c8088 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 26 Jul 2025 12:45:41 +0800 Subject: [PATCH 62/89] =?UTF-8?q?reactor=EF=BC=9A=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E6=A0=BC=E5=BC=8F=EF=BC=8C=E8=A7=A3=E5=86=B3?= =?UTF-8?q?=20FILE=20=E9=87=8C=E5=AD=98=E5=9C=A8=20highlight=E3=80=81cyan?= =?UTF-8?q?=E9=AB=98=E4=BA=AE=20reactor=EF=BC=9A=E5=BC=B1=E5=8C=96?= =?UTF-8?q?=E5=AF=B9=20skywalking=20=E4=BE=9D=E8=B5=96=EF=BC=8C=E5=8F=AF?= =?UTF-8?q?=E6=8C=89=E9=9C=80=E5=BC=95=E5=85=A5=EF=BC=8C=E6=9B=B4=E7=81=B5?= =?UTF-8?q?=E6=B4=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yudao-spring-boot-starter-monitor/pom.xml | 4 + .../config/YudaoTracerAutoConfiguration.java | 45 ++++++----- .../dto/kd100/Kd100ExpressQueryRespDTO.java | 1 - .../src/main/resources/logback-spring.xml | 74 +++++++------------ 4 files changed, 56 insertions(+), 68 deletions(-) diff --git a/yudao-framework/yudao-spring-boot-starter-monitor/pom.xml b/yudao-framework/yudao-spring-boot-starter-monitor/pom.xml index ebd1210c03..c9a65e8c18 100644 --- a/yudao-framework/yudao-spring-boot-starter-monitor/pom.xml +++ b/yudao-framework/yudao-spring-boot-starter-monitor/pom.xml @@ -44,18 +44,22 @@ io.opentracing opentracing-util + true org.apache.skywalking apm-toolkit-trace + true org.apache.skywalking apm-toolkit-logback-1.x + true org.apache.skywalking apm-toolkit-opentracing + true diff --git a/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/config/YudaoTracerAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/config/YudaoTracerAutoConfiguration.java index c7d9e2c0db..7876742620 100644 --- a/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/config/YudaoTracerAutoConfiguration.java +++ b/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/config/YudaoTracerAutoConfiguration.java @@ -3,6 +3,9 @@ package cn.iocoder.yudao.framework.tracer.config; import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum; import cn.iocoder.yudao.framework.tracer.core.aop.BizTraceAspect; import cn.iocoder.yudao.framework.tracer.core.filter.TraceFilter; +import io.opentracing.Tracer; +import io.opentracing.util.GlobalTracer; +import org.apache.skywalking.apm.toolkit.opentracing.SkywalkingTracer; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -16,30 +19,32 @@ import org.springframework.context.annotation.Bean; * @author mashu */ @AutoConfiguration -@ConditionalOnClass({BizTraceAspect.class}) +@ConditionalOnClass(name = { + "org.apache.skywalking.apm.toolkit.opentracing.SkywalkingTracer", + "io.opentracing.Tracer" +}) @EnableConfigurationProperties(TracerProperties.class) @ConditionalOnProperty(prefix = "yudao.tracer", value = "enable", matchIfMissing = true) public class YudaoTracerAutoConfiguration { - // TODO @芋艿:重要。目前 opentracing 版本存在冲突,要么保证 skywalking,要么保证阿里云短信 sdk -// @Bean -// public TracerProperties bizTracerProperties() { -// return new TracerProperties(); -// } -// -// @Bean -// public BizTraceAspect bizTracingAop() { -// return new BizTraceAspect(tracer()); -// } -// -// @Bean -// public Tracer tracer() { -// // 创建 SkywalkingTracer 对象 -// SkywalkingTracer tracer = new SkywalkingTracer(); -// // 设置为 GlobalTracer 的追踪器 -// GlobalTracer.register(tracer); -// return tracer; -// } + @Bean + public TracerProperties bizTracerProperties() { + return new TracerProperties(); + } + + @Bean + public BizTraceAspect bizTracingAop() { + return new BizTraceAspect(tracer()); + } + + @Bean + public Tracer tracer() { + // 创建 SkywalkingTracer 对象 + SkywalkingTracer tracer = new SkywalkingTracer(); + // 设置为 GlobalTracer 的追踪器 + GlobalTracer.registerIfAbsent(tracer); + return tracer; + } /** * 创建 TraceFilter 过滤器,响应 header 设置 traceId diff --git a/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/dto/kd100/Kd100ExpressQueryRespDTO.java b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/dto/kd100/Kd100ExpressQueryRespDTO.java index 6a8e83e3b3..0cf5c96caa 100644 --- a/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/dto/kd100/Kd100ExpressQueryRespDTO.java +++ b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/dto/kd100/Kd100ExpressQueryRespDTO.java @@ -59,7 +59,6 @@ public class Kd100ExpressQueryRespDTO { /** * 轨迹发生时间 */ - @JsonProperty("time") @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT) @JsonDeserialize(using = LocalDateTimeDeserializer.class) private LocalDateTime time; diff --git a/yudao-server/src/main/resources/logback-spring.xml b/yudao-server/src/main/resources/logback-spring.xml index b1b9f3fafb..15b28cfdff 100644 --- a/yudao-server/src/main/resources/logback-spring.xml +++ b/yudao-server/src/main/resources/logback-spring.xml @@ -1,76 +1,56 @@ - - - - - - + + + + +       - - - ${PATTERN_DEFAULT} - + + ${CONSOLE_LOG_PATTERN} - - - ${PATTERN_DEFAULT} - + + ${FILE_LOG_PATTERN} ${LOG_FILE} + - - ${LOGBACK_ROLLINGPOLICY_FILE_NAME_PATTERN:-${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz} - - ${LOGBACK_ROLLINGPOLICY_CLEAN_HISTORY_ON_START:-false} - - ${LOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE:-10MB} - - ${LOGBACK_ROLLINGPOLICY_TOTAL_SIZE_CAP:-0} - - ${LOGBACK_ROLLINGPOLICY_MAX_HISTORY:-30} + ${LOG_FILE}.%d{yyyy-MM-dd}.%i.log + 30 + 10MB - - 0 - - 256 + 0 + 512 - - + + - - - - - - - - - - - - - - - - + + + + + + + From 8e1729d7b2595223f963947f7ff409c1f80ee04f Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 26 Jul 2025 13:50:06 +0800 Subject: [PATCH 63/89] =?UTF-8?q?reactor=EF=BC=9A=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E4=B8=8D=E5=BC=95=E5=85=A5=20micrometer-registry-prometheus=20?= =?UTF-8?q?=E4=BE=9D=E8=B5=96=EF=BC=8C=E5=87=8F=E5=B0=91=E5=8C=85=E5=A4=A7?= =?UTF-8?q?=E5=B0=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yudao-framework/yudao-spring-boot-starter-monitor/pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/yudao-framework/yudao-spring-boot-starter-monitor/pom.xml b/yudao-framework/yudao-spring-boot-starter-monitor/pom.xml index c9a65e8c18..50cfc31e09 100644 --- a/yudao-framework/yudao-spring-boot-starter-monitor/pom.xml +++ b/yudao-framework/yudao-spring-boot-starter-monitor/pom.xml @@ -66,6 +66,7 @@ io.micrometer micrometer-registry-prometheus + true From 5cbba7633859b0f40d9a13620c8ee4a1918f4f4a Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 26 Jul 2025 14:37:24 +0800 Subject: [PATCH 64/89] =?UTF-8?q?reactor=EF=BC=9A=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E4=B8=8D=E5=BC=95=E5=85=A5=20spring-boot-admin=EF=BC=8C?= =?UTF-8?q?=E6=9B=B4=E7=81=B5=E6=B4=BB=EF=BC=8C=E5=90=8C=E6=97=B6=E5=90=AF?= =?UTF-8?q?=E5=8A=A8=E9=80=9F=E5=BA=A6=E6=9B=B4=E5=BF=AB=EF=BC=88=E6=8F=90?= =?UTF-8?q?=E5=8D=87=E5=BC=80=E5=8F=91=E6=95=88=E7=8E=87=EF=BC=89=EF=BC=81?= =?UTF-8?q?=EF=BC=81=EF=BC=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yudao-framework/yudao-spring-boot-starter-monitor/pom.xml | 3 ++- yudao-module-infra/pom.xml | 1 + yudao-server/src/main/resources/application-dev.yaml | 1 + yudao-server/src/main/resources/application-local.yaml | 3 --- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/yudao-framework/yudao-spring-boot-starter-monitor/pom.xml b/yudao-framework/yudao-spring-boot-starter-monitor/pom.xml index 50cfc31e09..6b6970fbef 100644 --- a/yudao-framework/yudao-spring-boot-starter-monitor/pom.xml +++ b/yudao-framework/yudao-spring-boot-starter-monitor/pom.xml @@ -71,7 +71,8 @@ de.codecentric - spring-boot-admin-starter-client + spring-boot-admin-starter-client + true diff --git a/yudao-module-infra/pom.xml b/yudao-module-infra/pom.xml index 8b983cfe4b..be053b5cae 100644 --- a/yudao-module-infra/pom.xml +++ b/yudao-module-infra/pom.xml @@ -92,6 +92,7 @@ de.codecentric spring-boot-admin-starter-server + true diff --git a/yudao-server/src/main/resources/application-dev.yaml b/yudao-server/src/main/resources/application-dev.yaml index 44c1cecfa3..6d228c7474 100644 --- a/yudao-server/src/main/resources/application-dev.yaml +++ b/yudao-server/src/main/resources/application-dev.yaml @@ -5,6 +5,7 @@ server: spring: autoconfigure: + # noinspection SpringBootApplicationYaml exclude: - org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreAutoConfiguration # 禁用 AI 模块的 Qdrant,手动创建 - org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreAutoConfiguration # 禁用 AI 模块的 Milvus,手动创建 diff --git a/yudao-server/src/main/resources/application-local.yaml b/yudao-server/src/main/resources/application-local.yaml index e868eca456..7b79eca5b6 100644 --- a/yudao-server/src/main/resources/application-local.yaml +++ b/yudao-server/src/main/resources/application-local.yaml @@ -7,9 +7,6 @@ spring: # noinspection SpringBootApplicationYaml exclude: - org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration # 默认 local 环境,不开启 Quartz 的自动配置 - - de.codecentric.boot.admin.server.config.AdminServerAutoConfiguration # 禁用 Spring Boot Admin 的 Server 的自动配置 - - de.codecentric.boot.admin.server.ui.config.AdminServerUiAutoConfiguration # 禁用 Spring Boot Admin 的 Server UI 的自动配置 - - de.codecentric.boot.admin.client.config.SpringBootAdminClientAutoConfiguration # 禁用 Spring Boot Admin 的 Client 的自动配置 - org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreAutoConfiguration # 禁用 AI 模块的 Qdrant,手动创建 - org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreAutoConfiguration # 禁用 AI 模块的 Milvus,手动创建 # 数据源配置项 From ca65267f6d224de3ca381615705a4dfd1839cdee Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 26 Jul 2025 16:06:27 +0800 Subject: [PATCH 65/89] =?UTF-8?q?fix=EF=BC=9ACacheRequestBodyFilter=20?= =?UTF-8?q?=E5=AF=BC=E8=87=B4=20Spring=20Boot=20Admin=20=E6=97=A0=E6=B3=95?= =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=97=A5=E5=BF=97=E7=BA=A7=E5=88=AB=EF=BC=8C?= =?UTF-8?q?https://github.com/YunaiV/ruoyi-vue-pro/issues/795?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/core/filter/CacheRequestBodyFilter.java | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/CacheRequestBodyFilter.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/CacheRequestBodyFilter.java index 9071998f91..ae7bfeca70 100644 --- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/CacheRequestBodyFilter.java +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/CacheRequestBodyFilter.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.framework.web.core.filter; +import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; import org.springframework.web.filter.OncePerRequestFilter; @@ -16,6 +17,14 @@ import java.io.IOException; */ public class CacheRequestBodyFilter extends OncePerRequestFilter { + /** + * 需要排除的 URI + * + * 1. 排除 Spring Boot Admin 相关请求,避免客户端连接中断导致的异常。 + * 例如说:795 ISSUE + */ + private static final String[] IGNORE_URIS = {"/admin", "/actuator"}; + @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException { @@ -24,7 +33,13 @@ public class CacheRequestBodyFilter extends OncePerRequestFilter { @Override protected boolean shouldNotFilter(HttpServletRequest request) { - // 只处理 json 请求内容 + // 1. 校验是否为排除的 URL + String requestURI = request.getRequestURI(); + if (StrUtil.containsAny(requestURI, IGNORE_URIS)) { + return true; + } + + // 2. 只处理 json 请求内容 return !ServletUtils.isJsonRequest(request); } From 13802ceb3afcbc434641f561b3cc64eecdc9c5dc Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 26 Jul 2025 16:37:49 +0800 Subject: [PATCH 66/89] =?UTF-8?q?fix=EF=BC=9ADateUtils=E3=80=81LocalDateTi?= =?UTF-8?q?meUtils=20=E5=8D=95=E8=AF=8D=E6=8B=BC=E5=86=99=E9=94=99?= =?UTF-8?q?=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yudao/framework/common/util/date/DateUtils.java | 12 ++++++------ .../common/util/date/LocalDateTimeUtils.java | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/DateUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/DateUtils.java index b51a838c69..d6051e85fe 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/DateUtils.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/DateUtils.java @@ -74,30 +74,30 @@ public class DateUtils { * 创建指定时间 * * @param year 年 - * @param mouth 月 + * @param month 月 * @param day 日 * @return 指定时间 */ - public static Date buildTime(int year, int mouth, int day) { - return buildTime(year, mouth, day, 0, 0, 0); + public static Date buildTime(int year, int month, int day) { + return buildTime(year, month, day, 0, 0, 0); } /** * 创建指定时间 * * @param year 年 - * @param mouth 月 + * @param month 月 * @param day 日 * @param hour 小时 * @param minute 分钟 * @param second 秒 * @return 指定时间 */ - public static Date buildTime(int year, int mouth, int day, + public static Date buildTime(int year, int month, int day, int hour, int minute, int second) { Calendar calendar = Calendar.getInstance(); calendar.set(Calendar.YEAR, year); - calendar.set(Calendar.MONTH, mouth - 1); + calendar.set(Calendar.MONTH, month - 1); calendar.set(Calendar.DAY_OF_MONTH, day); calendar.set(Calendar.HOUR_OF_DAY, hour); calendar.set(Calendar.MINUTE, minute); diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/LocalDateTimeUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/LocalDateTimeUtils.java index 8d18479c81..26b3961685 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/LocalDateTimeUtils.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/LocalDateTimeUtils.java @@ -69,17 +69,17 @@ public class LocalDateTimeUtils { * 创建指定时间 * * @param year 年 - * @param mouth 月 + * @param month 月 * @param day 日 * @return 指定时间 */ - public static LocalDateTime buildTime(int year, int mouth, int day) { - return LocalDateTime.of(year, mouth, day, 0, 0, 0); + public static LocalDateTime buildTime(int year, int month, int day) { + return LocalDateTime.of(year, month, day, 0, 0, 0); } - public static LocalDateTime[] buildBetweenTime(int year1, int mouth1, int day1, - int year2, int mouth2, int day2) { - return new LocalDateTime[]{buildTime(year1, mouth1, day1), buildTime(year2, mouth2, day2)}; + public static LocalDateTime[] buildBetweenTime(int year1, int month1, int day1, + int year2, int month2, int day2) { + return new LocalDateTime[]{buildTime(year1, month1, day1), buildTime(year2, month2, day2)}; } /** From f05a66f8e4dcab5dbfbd70cf3ebbdb68013f3a1d Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 26 Jul 2025 16:55:55 +0800 Subject: [PATCH 67/89] =?UTF-8?q?fix=EF=BC=9ACacheRequestBodyFilter=20?= =?UTF-8?q?=E5=AF=BC=E8=87=B4=20Spring=20Boot=20Admin=20=E6=97=A0=E6=B3=95?= =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=97=A5=E5=BF=97=E7=BA=A7=E5=88=AB=EF=BC=8C?= =?UTF-8?q?https://github.com/YunaiV/ruoyi-vue-pro/issues/795?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../framework/web/core/filter/CacheRequestBodyFilter.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/CacheRequestBodyFilter.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/CacheRequestBodyFilter.java index ae7bfeca70..49958fc096 100644 --- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/CacheRequestBodyFilter.java +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/CacheRequestBodyFilter.java @@ -23,7 +23,7 @@ public class CacheRequestBodyFilter extends OncePerRequestFilter { * 1. 排除 Spring Boot Admin 相关请求,避免客户端连接中断导致的异常。 * 例如说:795 ISSUE */ - private static final String[] IGNORE_URIS = {"/admin", "/actuator"}; + private static final String[] IGNORE_URIS = {"/admin/", "/actuator/"}; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) @@ -35,7 +35,7 @@ public class CacheRequestBodyFilter extends OncePerRequestFilter { protected boolean shouldNotFilter(HttpServletRequest request) { // 1. 校验是否为排除的 URL String requestURI = request.getRequestURI(); - if (StrUtil.containsAny(requestURI, IGNORE_URIS)) { + if (StrUtil.startWithAny(requestURI, IGNORE_URIS)) { return true; } From c25d1b37193f6985af7583723cb3c0c9e240e869 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 26 Jul 2025 17:15:57 +0800 Subject: [PATCH 68/89] =?UTF-8?q?review=EF=BC=9A@InDict=20=E7=9A=84?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=EF=BC=8C=E4=BB=A5=E5=8F=8A=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E7=9A=84=E4=BD=BF=E7=94=A8=E6=A1=88=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../iocoder/yudao/framework/dict/validation/InDict.java | 2 +- .../dict/validation/InDictCollectionValidator.java | 9 ++++++--- .../yudao/framework/dict/validation/InDictValidator.java | 5 +++-- .../admin/user/vo/user/UserUpdateStatusReqVO.java | 6 ++++-- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/validation/InDict.java b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/validation/InDict.java index dab0af782f..de7498775c 100644 --- a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/validation/InDict.java +++ b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/validation/InDict.java @@ -25,7 +25,7 @@ import java.lang.annotation.Target; public @interface InDict { /** - * 数据字典type + * 数据字典 type */ String type(); diff --git a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/validation/InDictCollectionValidator.java b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/validation/InDictCollectionValidator.java index ab036171e2..a7184b066a 100644 --- a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/validation/InDictCollectionValidator.java +++ b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/validation/InDictCollectionValidator.java @@ -1,6 +1,6 @@ package cn.iocoder.yudao.framework.dict.validation; -import cn.iocoder.yudao.framework.common.util.string.StrUtils; +import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils; import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; @@ -19,19 +19,22 @@ public class InDictCollectionValidator implements ConstraintValidator list, ConstraintValidatorContext context) { - if (list == null) { + // 为空时,默认不校验,即认为通过 + if (CollUtil.isEmpty(list)) { return true; } + // 校验全部通过 List dbValues = DictFrameworkUtils.getDictDataValueList(dictType); boolean match = list.stream().allMatch(v -> dbValues.stream() .anyMatch(dbValue -> dbValue.equalsIgnoreCase(v.toString()))); if (match) { return true; } + // 校验不通过,自定义提示语句 context.disableDefaultConstraintViolation(); // 禁用默认的 message 的值 context.buildConstraintViolationWithTemplate( - context.getDefaultConstraintMessageTemplate().replaceAll("\\{value}",dbValues.toString()) + context.getDefaultConstraintMessageTemplate().replaceAll("\\{value}", dbValues.toString()) ).addConstraintViolation(); // 重新添加错误提示语句 return false; } diff --git a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/validation/InDictValidator.java b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/validation/InDictValidator.java index 05f50cb839..b67f017750 100644 --- a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/validation/InDictValidator.java +++ b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/validation/InDictValidator.java @@ -1,6 +1,6 @@ package cn.iocoder.yudao.framework.dict.validation; -import cn.iocoder.yudao.framework.common.util.string.StrUtils; +import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils; import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; @@ -24,10 +24,11 @@ public class InDictValidator implements ConstraintValidator { } // 校验通过 final List values = DictFrameworkUtils.getDictDataValueList(dictType); - boolean match = values.stream().anyMatch(v -> v.equalsIgnoreCase(value.toString())); + boolean match = values.stream().anyMatch(v -> StrUtil.equalsIgnoreCase(v, value.toString())); if (match) { return true; } + // 校验不通过,自定义提示语句 context.disableDefaultConstraintViolation(); // 禁用默认的 message 的值 context.buildConstraintViolationWithTemplate( diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserUpdateStatusReqVO.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserUpdateStatusReqVO.java index ae03d18625..8e3e5db179 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserUpdateStatusReqVO.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserUpdateStatusReqVO.java @@ -2,10 +2,11 @@ package cn.iocoder.yudao.module.system.controller.admin.user.vo.user; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.framework.dict.validation.InDict; +import cn.iocoder.yudao.module.system.enums.DictTypeConstants; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - import jakarta.validation.constraints.NotNull; +import lombok.Data; @Schema(description = "管理后台 - 用户更新状态 Request VO") @Data @@ -18,6 +19,7 @@ public class UserUpdateStatusReqVO { @Schema(description = "状态,见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "状态不能为空") @InEnum(value = CommonStatusEnum.class, message = "修改状态必须是 {value}") + @InDict(type = DictTypeConstants.COMMON_STATUS) private Integer status; } From 8b2247f7f94ba0a60e92aafebb998e3409db7a6c Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 26 Jul 2025 17:16:07 +0800 Subject: [PATCH 69/89] =?UTF-8?q?=E5=90=8C=E6=AD=A5=20https://gitee.com/zh?= =?UTF-8?q?ijiantianya/ruoyi-vue-pro/pulls/1382/=20=E8=B4=A1=E7=8C=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/codegen/vue3/views/components/list_sub_erp.vue.vm | 1 + .../src/main/resources/codegen/vue3/views/index.vue.vm | 1 + 2 files changed, 2 insertions(+) diff --git a/yudao-module-infra/src/main/resources/codegen/vue3/views/components/list_sub_erp.vue.vm b/yudao-module-infra/src/main/resources/codegen/vue3/views/components/list_sub_erp.vue.vm index c0dddc0767..f9fbb9787b 100644 --- a/yudao-module-infra/src/main/resources/codegen/vue3/views/components/list_sub_erp.vue.vm +++ b/yudao-module-infra/src/main/resources/codegen/vue3/views/components/list_sub_erp.vue.vm @@ -138,6 +138,7 @@ watch( () => props.${subJoinColumn.javaField}, (val: number) => { if (!val) { + list.value = [] // 清空列表 return } queryParams.${subJoinColumn.javaField} = val diff --git a/yudao-module-infra/src/main/resources/codegen/vue3/views/index.vue.vm b/yudao-module-infra/src/main/resources/codegen/vue3/views/index.vue.vm index f6ce0e50c7..851bc2b5e4 100644 --- a/yudao-module-infra/src/main/resources/codegen/vue3/views/index.vue.vm +++ b/yudao-module-infra/src/main/resources/codegen/vue3/views/index.vue.vm @@ -353,6 +353,7 @@ const handleDelete = async (id: number) => { // 发起删除 await ${simpleClassName}Api.delete${simpleClassName}(id) message.success(t('common.delSuccess')) + currentRow.value = {} // 刷新列表 await getList() } catch {} From 1b412546eb9914d28fc394c8cc46f5c38d249a2e Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 26 Jul 2025 19:49:50 +0800 Subject: [PATCH 70/89] =?UTF-8?q?fix=EF=BC=9A=E3=80=90Mall=20=E5=95=86?= =?UTF-8?q?=E5=9F=8E=E3=80=91=E4=BF=AE=E5=A4=8D=E7=A7=AF=E5=88=86=E5=95=86?= =?UTF-8?q?=E5=93=81=E6=96=B0=E5=A2=9E=E6=97=B6=E6=A0=A1=E9=AA=8C=E5=95=86?= =?UTF-8?q?=E5=93=81=E6=98=AF=E5=90=A6=E5=8F=82=E4=B8=8E=E5=85=B6=E4=BB=96?= =?UTF-8?q?=E6=B4=BB=E5=8A=A8=E6=A0=A1=E9=AA=8C=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../promotion/service/point/PointActivityServiceImpl.java | 1 + .../core/client/dto/kd100/Kd100ExpressQueryRespDTO.java | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/point/PointActivityServiceImpl.java b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/point/PointActivityServiceImpl.java index 582cadb0d4..0b945794ad 100644 --- a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/point/PointActivityServiceImpl.java +++ b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/point/PointActivityServiceImpl.java @@ -222,6 +222,7 @@ public class PointActivityServiceImpl implements PointActivityService { if (spu == null) { throw exception(SPU_NOT_EXISTS); } + products.forEach(product -> product.setSpuId(spuId)); // 2. 校验商品 sku 都存在 List skus = productSkuApi.getSkuListBySpuId(singletonList(spuId)); diff --git a/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/dto/kd100/Kd100ExpressQueryRespDTO.java b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/dto/kd100/Kd100ExpressQueryRespDTO.java index 0cf5c96caa..d5442cbeeb 100644 --- a/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/dto/kd100/Kd100ExpressQueryRespDTO.java +++ b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/dto/kd100/Kd100ExpressQueryRespDTO.java @@ -2,10 +2,11 @@ package cn.iocoder.yudao.module.trade.framework.delivery.core.client.dto.kd100; import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; import lombok.Data; import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; import java.util.List; import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; From 77193102a2038fafb71e03ac31e680257c40af75 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 26 Jul 2025 20:08:04 +0800 Subject: [PATCH 71/89] =?UTF-8?q?feat=EF=BC=9A=E6=B7=BB=E5=8A=A0=E8=87=AA?= =?UTF-8?q?=E9=80=82=E5=BA=94=E5=88=97=E5=AE=BD=E5=A4=84=E7=90=86=E5=99=A8?= =?UTF-8?q?=E5=B9=B6=E6=9B=BF=E6=8D=A2=E9=BB=98=E8=AE=A4=E5=88=97=E5=AE=BD?= =?UTF-8?q?=E7=AD=96=E7=95=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ColumnWidthMatchStyleStrategy.java | 78 +++++++++++++++++++ .../core/handler/SelectSheetWriteHandler.java | 2 +- .../framework/excel/core/util/ExcelUtils.java | 14 ++-- .../yudao/framework/excel/package-info.java | 2 +- 4 files changed, 87 insertions(+), 9 deletions(-) create mode 100644 yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/handler/ColumnWidthMatchStyleStrategy.java diff --git a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/handler/ColumnWidthMatchStyleStrategy.java b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/handler/ColumnWidthMatchStyleStrategy.java new file mode 100644 index 0000000000..49a5b31572 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/handler/ColumnWidthMatchStyleStrategy.java @@ -0,0 +1,78 @@ +package cn.iocoder.yudao.framework.excel.core.handler; + +import cn.hutool.core.collection.CollUtil; +import cn.idev.excel.enums.CellDataTypeEnum; +import cn.idev.excel.metadata.Head; +import cn.idev.excel.metadata.data.WriteCellData; +import cn.idev.excel.util.MapUtils; +import cn.idev.excel.write.metadata.holder.WriteSheetHolder; +import cn.idev.excel.write.style.column.AbstractColumnWidthStyleStrategy; +import cn.idev.excel.write.style.column.LongestMatchColumnWidthStyleStrategy; +import org.apache.poi.ss.usermodel.Cell; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Excel 自适应列宽处理器 + * + * 相比 {@link LongestMatchColumnWidthStyleStrategy} 来说,额外处理了 DATE 类型! + * + * @see 添加自适应列宽处理器,并替换默认列宽策略 + * @author hmb + */ +public class ColumnWidthMatchStyleStrategy extends AbstractColumnWidthStyleStrategy { + + private static final int MAX_COLUMN_WIDTH = 255; + + private final Map> cache = MapUtils.newHashMapWithExpectedSize(8); + + @Override + protected void setColumnWidth(WriteSheetHolder writeSheetHolder, List> cellDataList, Cell cell, + Head head, Integer relativeRowIndex, Boolean isHead) { + boolean needSetWidth = isHead || CollUtil.isNotEmpty(cellDataList); + if (!needSetWidth) { + return; + } + Map maxColumnWidthMap = cache.computeIfAbsent(writeSheetHolder.getSheetNo(), + key -> new HashMap<>(16)); + Integer columnWidth = dataLength(cellDataList, cell, isHead); + if (columnWidth < 0) { + return; + } + if (columnWidth > MAX_COLUMN_WIDTH) { + columnWidth = MAX_COLUMN_WIDTH; + } + Integer maxColumnWidth = maxColumnWidthMap.get(cell.getColumnIndex()); + if (maxColumnWidth == null || columnWidth > maxColumnWidth) { + maxColumnWidthMap.put(cell.getColumnIndex(), columnWidth); + writeSheetHolder.getSheet().setColumnWidth(cell.getColumnIndex(), columnWidth * 256); + } + } + + @SuppressWarnings("EnhancedSwitchMigration") + private Integer dataLength(List> cellDataList, Cell cell, Boolean isHead) { + if (isHead) { + return cell.getStringCellValue().getBytes().length; + } + WriteCellData cellData = cellDataList.get(0); + CellDataTypeEnum type = cellData.getType(); + if (type == null) { + return -1; + } + switch (type) { + case STRING: + return cellData.getStringValue().getBytes().length; + case BOOLEAN: + return cellData.getBooleanValue().toString().getBytes().length; + case NUMBER: + return cellData.getNumberValue().toString().getBytes().length; + case DATE: + return cellData.getDateValue().toString().getBytes().length; + default: + return -1; + } + } + +} \ No newline at end of file diff --git a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/handler/SelectSheetWriteHandler.java b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/handler/SelectSheetWriteHandler.java index cd6c87f68f..c55e1210a2 100644 --- a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/handler/SelectSheetWriteHandler.java +++ b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/handler/SelectSheetWriteHandler.java @@ -86,7 +86,7 @@ public class SelectSheetWriteHandler implements SheetWriteHandler { /** * 判断字段是否是静态的、最终的、 transient 的 - * 原因:EasyExcel 默认是忽略 static final 或 transient 的字段,所以需要判断 + * 原因:FastExcel 默认是忽略 static final 或 transient 的字段,所以需要判断 * * @param field 字段 * @return 是否是静态的、最终的、transient 的 diff --git a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java index bae20d6c53..f05d3e51e5 100644 --- a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java +++ b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java @@ -1,10 +1,10 @@ package cn.iocoder.yudao.framework.excel.core.util; -import cn.iocoder.yudao.framework.common.util.http.HttpUtils; -import cn.iocoder.yudao.framework.excel.core.handler.SelectSheetWriteHandler; -import cn.idev.excel.EasyExcel; +import cn.idev.excel.FastExcelFactory; import cn.idev.excel.converters.longconverter.LongStringConverter; -import cn.idev.excel.write.style.column.LongestMatchColumnWidthStyleStrategy; +import cn.iocoder.yudao.framework.common.util.http.HttpUtils; +import cn.iocoder.yudao.framework.excel.core.handler.ColumnWidthMatchStyleStrategy; +import cn.iocoder.yudao.framework.excel.core.handler.SelectSheetWriteHandler; import jakarta.servlet.http.HttpServletResponse; import org.springframework.web.multipart.MultipartFile; @@ -32,9 +32,9 @@ public class ExcelUtils { public static void write(HttpServletResponse response, String filename, String sheetName, Class head, List data) throws IOException { // 输出 Excel - EasyExcel.write(response.getOutputStream(), head) + FastExcelFactory.write(response.getOutputStream(), head) .autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理 - .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) // 基于 column 长度,自动适配。最大 255 宽度 + .registerWriteHandler(new ColumnWidthMatchStyleStrategy()) // 基于 column 长度,自动适配。最大 255 宽度 .registerWriteHandler(new SelectSheetWriteHandler(head)) // 基于固定 sheet 实现下拉框 .registerConverter(new LongStringConverter()) // 避免 Long 类型丢失精度 .sheet(sheetName).doWrite(data); @@ -44,7 +44,7 @@ public class ExcelUtils { } public static List read(MultipartFile file, Class head) throws IOException { - return EasyExcel.read(file.getInputStream(), head, null) + return FastExcelFactory.read(file.getInputStream(), head, null) .autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理 .doReadAllSync(); } diff --git a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/package-info.java b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/package-info.java index 53bc5c01bf..72c3ac42e1 100644 --- a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/package-info.java +++ b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/package-info.java @@ -1,4 +1,4 @@ /** - * 基于 EasyExcel 实现 Excel 相关的操作 + * 基于 FastExcel 实现 Excel 相关的操作 */ package cn.iocoder.yudao.framework.excel; From 99442ec05c29b8839dbf796628bad7f72b2b99da Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 26 Jul 2025 21:21:02 +0800 Subject: [PATCH 72/89] =?UTF-8?q?fix=EF=BC=9AApiErrorLogServiceImpl=20?= =?UTF-8?q?=E5=9C=A8=E5=BE=AE=E6=9C=8D=E5=8A=A1=E5=9C=BA=E6=99=AF=E4=B8=8B?= =?UTF-8?q?=EF=BC=8C=E8=AE=B0=E5=BD=95=E6=97=A5=E5=BF=97=E5=AD=98=E5=9C=A8?= =?UTF-8?q?=E7=9A=84=E5=BE=AA=E7=8E=AF=E8=B0=83=E7=94=A8=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/logger/ApiErrorLogServiceImpl.java | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiErrorLogServiceImpl.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiErrorLogServiceImpl.java index e91c098941..536b265f93 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiErrorLogServiceImpl.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiErrorLogServiceImpl.java @@ -40,11 +40,16 @@ public class ApiErrorLogServiceImpl implements ApiErrorLogService { ApiErrorLogDO apiErrorLog = BeanUtils.toBean(createDTO, ApiErrorLogDO.class) .setProcessStatus(ApiErrorLogProcessStatusEnum.INIT.getStatus()); apiErrorLog.setRequestParams(StrUtils.maxLength(apiErrorLog.getRequestParams(), REQUEST_PARAMS_MAX_LENGTH)); - if (TenantContextHolder.getTenantId() != null) { - apiErrorLogMapper.insert(apiErrorLog); - } else { - // 极端情况下,上下文中没有租户时,此时忽略租户上下文,避免插入失败! - TenantUtils.executeIgnore(() -> apiErrorLogMapper.insert(apiErrorLog)); + try { + if (TenantContextHolder.getTenantId() != null) { + apiErrorLogMapper.insert(apiErrorLog); + } else { + // 极端情况下,上下文中没有租户时,此时忽略租户上下文,避免插入失败! + TenantUtils.executeIgnore(() -> apiErrorLogMapper.insert(apiErrorLog)); + } + } catch (Exception ex) { + // 兜底处理,目前只有 yudao-cloud 会发生:https://gitee.com/yudaocode/yudao-cloud-mini/issues/IC1O0A + log.error("[createApiErrorLog][记录时({}) 发生异常]", createDTO, ex); } } From bf5ac4ede2ef8e0ba9736cff74c571d05baad981 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 26 Jul 2025 21:30:16 +0800 Subject: [PATCH 73/89] =?UTF-8?q?feat=EF=BC=9ACacheUtils=20=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=20CACHE=5FMAX=5FSIZE=20=E9=BB=98=E8=AE=A4=E5=A4=A7?= =?UTF-8?q?=E5=B0=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../framework/common/util/cache/CacheUtils.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/cache/CacheUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/cache/CacheUtils.java index 12a6e17246..4d9168ebdb 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/cache/CacheUtils.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/cache/CacheUtils.java @@ -14,6 +14,13 @@ import java.util.concurrent.Executors; */ public class CacheUtils { + /** + * 异步刷新的 LoadingCache 最大缓存数量 + * + * @see 本地缓存 CacheUtils 工具类建议 + */ + private static final Integer CACHE_MAX_SIZE = 10000; + /** * 构建异步刷新的 LoadingCache 对象 * @@ -29,6 +36,7 @@ public class CacheUtils { */ public static LoadingCache buildAsyncReloadingCache(Duration duration, CacheLoader loader) { return CacheBuilder.newBuilder() + .maximumSize(CACHE_MAX_SIZE) // 只阻塞当前数据加载线程,其他线程返回旧值 .refreshAfterWrite(duration) // 通过 asyncReloading 实现全异步加载,包括 refreshAfterWrite 被阻塞的加载线程 @@ -43,7 +51,11 @@ public class CacheUtils { * @return LoadingCache 对象 */ public static LoadingCache buildCache(Duration duration, CacheLoader loader) { - return CacheBuilder.newBuilder().refreshAfterWrite(duration).build(loader); + return CacheBuilder.newBuilder() + .maximumSize(CACHE_MAX_SIZE) + // 只阻塞当前数据加载线程,其他线程返回旧值 + .refreshAfterWrite(duration) + .build(loader); } } From c9b9fc1f31242804e594638d8d64d6a6705fa967 Mon Sep 17 00:00:00 2001 From: haohao <1036606149@qq.com> Date: Sat, 26 Jul 2025 22:15:37 +0800 Subject: [PATCH 74/89] =?UTF-8?q?feat=EF=BC=9A=E3=80=90IoT=20=E7=89=A9?= =?UTF-8?q?=E8=81=94=E7=BD=91=E3=80=91=E9=87=8D=E6=9E=84=20TCP=20=E5=8D=8F?= =?UTF-8?q?=E8=AE=AE=E5=A4=84=E7=90=86=EF=BC=8C=E6=96=B0=E5=A2=9E=20TCP=20?= =?UTF-8?q?=E4=BC=9A=E8=AF=9D=E5=92=8C=E8=AE=A4=E8=AF=81=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../message/IotDeviceMessageServiceImpl.java | 9 +- .../yudao-module-iot-gateway/pom.xml | 7 + .../tcp/IotTcpBinaryDeviceMessageCodec.java | 448 +++++++----------- .../gateway/codec/tcp/IotTcpCodecManager.java | 143 ------ .../tcp/IotTcpJsonDeviceMessageCodec.java | 298 ++++-------- .../config/IotGatewayConfiguration.java | 13 +- .../tcp/IotTcpDownstreamSubscriber.java | 31 +- .../protocol/tcp/IotTcpUpstreamProtocol.java | 19 +- .../tcp/manager/IotTcpAuthManager.java | 194 ++++++++ .../tcp/manager/IotTcpSessionManager.java | 143 ++++++ .../tcp/router/IotTcpDownstreamHandler.java | 60 ++- .../tcp/router/IotTcpUpstreamHandler.java | 326 ++++++++++--- .../message/IotDeviceMessageService.java | 23 +- .../message/IotDeviceMessageServiceImpl.java | 25 + ...a => TcpBinaryDataPacketExamplesTest.java} | 167 ++++--- .../codec/tcp/TcpJsonDataPacketExamples.java | 254 ---------- .../tcp/TcpJsonDataPacketExamplesTest.java | 185 ++++++++ .../resources/tcp-binary-packet-examples.md | 386 ++++++++++----- .../resources/tcp-json-packet-examples.md | 433 +++++++++-------- 19 files changed, 1765 insertions(+), 1399 deletions(-) delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpCodecManager.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpAuthManager.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpSessionManager.java rename yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/{TcpBinaryDataPacketExamples.java => TcpBinaryDataPacketExamplesTest.java} (54%) delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpJsonDataPacketExamples.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpJsonDataPacketExamplesTest.java diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageServiceImpl.java index ccb0a680b1..76b31f30ce 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageServiceImpl.java @@ -236,7 +236,8 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { @Override public Long getDeviceMessageCount(LocalDateTime createTime) { - return deviceMessageMapper.selectCountByCreateTime(createTime != null ? LocalDateTimeUtil.toEpochMilli(createTime) : null); + return deviceMessageMapper + .selectCountByCreateTime(createTime != null ? LocalDateTimeUtil.toEpochMilli(createTime) : null); } @Override @@ -244,10 +245,12 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { IotStatisticsDeviceMessageReqVO reqVO) { // 1. 按小时统计,获取分项统计数据 List> countList = deviceMessageMapper.selectDeviceMessageCountGroupByDate( - LocalDateTimeUtil.toEpochMilli(reqVO.getTimes()[0]), LocalDateTimeUtil.toEpochMilli(reqVO.getTimes()[1])); + LocalDateTimeUtil.toEpochMilli(reqVO.getTimes()[0]), + LocalDateTimeUtil.toEpochMilli(reqVO.getTimes()[1])); // 2. 按照日期间隔,合并数据 - List timeRanges = LocalDateTimeUtils.getDateRangeList(reqVO.getTimes()[0], reqVO.getTimes()[1], reqVO.getInterval()); + List timeRanges = LocalDateTimeUtils.getDateRangeList(reqVO.getTimes()[0], reqVO.getTimes()[1], + reqVO.getInterval()); return convertList(timeRanges, times -> { Integer upstreamCount = countList.stream() .filter(vo -> LocalDateTimeUtils.isBetween(times[0], times[1], (Timestamp) vo.get("time"))) diff --git a/yudao-module-iot/yudao-module-iot-gateway/pom.xml b/yudao-module-iot/yudao-module-iot-gateway/pom.xml index d156d38c35..3c2b1fc642 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/pom.xml +++ b/yudao-module-iot/yudao-module-iot-gateway/pom.xml @@ -47,6 +47,13 @@ io.vertx vertx-mqtt + + + + cn.iocoder.boot + yudao-spring-boot-starter-test + test + diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java index a86a937d93..f7d8a80be1 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java @@ -1,108 +1,74 @@ package cn.iocoder.yudao.module.iot.gateway.codec.tcp; +import cn.hutool.core.lang.Assert; import cn.hutool.core.util.StrUtil; -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; import io.vertx.core.buffer.Buffer; +import lombok.AllArgsConstructor; import lombok.Data; -import lombok.extern.slf4j.Slf4j; +import lombok.NoArgsConstructor; import org.springframework.stereotype.Component; -// TODO @haohao:设备地址(变长) 是不是非必要哈?因为认证后,不需要每次都带呀。 /** * TCP 二进制格式 {@link IotDeviceMessage} 编解码器 * * 使用自定义二进制协议格式: - * 包头(4 字节) | 地址长度(2 字节) | 设备地址(变长) | 功能码(2 字节) | 消息序号(2 字节) | 包体数据(变长) + * 包头(4 字节) | 功能码(2 字节) | 消息序号(2 字节) | 包体数据(变长) * * @author 芋道源码 */ @Component -@Slf4j public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { - /** - * 编解码器类型 - */ public static final String TYPE = "TCP_BINARY"; - // TODO @haohao:这个注释不太对。 - // ==================== 常量定义 ==================== + @Data + @NoArgsConstructor + @AllArgsConstructor + private static class TcpBinaryMessage { - @Override - public byte[] encode(IotDeviceMessage message) { - if (message == null || StrUtil.isEmpty(message.getMethod())) { - throw new IllegalArgumentException("消息或方法不能为空"); - } + /** + * 功能码 + */ + private Short code; - try { - // 1. 确定功能码(只支持数据上报和心跳) - short code = MessageMethod.STATE_ONLINE.equals(message.getMethod()) ? - TcpDataPackage.CODE_HEARTBEAT : TcpDataPackage.CODE_MESSAGE_UP; + /** + * 消息序号 + */ + private Short mid; - // 2. 构建简化负载 - String payload = buildSimplePayload(message); + /** + * 设备 ID + */ + private Long deviceId; - // 3. 构建 TCP 数据包 - String deviceAddr = message.getDeviceId() != null ? String.valueOf(message.getDeviceId()) : "default"; - short mid = (short) (System.currentTimeMillis() % Short.MAX_VALUE); - TcpDataPackage dataPackage = new TcpDataPackage(deviceAddr, code, mid, payload); + /** + * 请求方法 + */ + private String method; - // 4. 编码为字节流 - return encodeTcpDataPackage(dataPackage).getBytes(); - } catch (Exception e) { - log.error("[encode][编码失败] 方法: {}", message.getMethod(), e); - throw new TcpCodecException("TCP 消息编码失败", e); - } - } + /** + * 请求参数 + */ + private Object params; - @Override - public IotDeviceMessage decode(byte[] bytes) { - if (bytes == null || bytes.length == 0) { - throw new IllegalArgumentException("待解码数据不能为空"); - } + /** + * 响应结果 + */ + private Object data; - try { - // 1. 解码 TCP 数据包 - TcpDataPackage dataPackage = decodeTcpDataPackage(Buffer.buffer(bytes)); + /** + * 响应错误码 + */ + private Integer responseCode; - // 2. 根据功能码确定方法 - // TODO @haohao:会不会有事件上报哈。 - String method = (dataPackage.getCode() == TcpDataPackage.CODE_HEARTBEAT) ? - MessageMethod.STATE_ONLINE : MessageMethod.PROPERTY_POST; + /** + * 响应提示 + */ + private String msg; - // 3. 解析负载数据和请求 ID - PayloadInfo payloadInfo = parsePayloadInfo(dataPackage.getPayload()); - - // 4. 构建 IoT 设备消息(设置完整的必要参数) - IotDeviceMessage message = IotDeviceMessage.requestOf( - payloadInfo.getRequestId(), method, payloadInfo.getParams()); - - // 5. 设置设备相关信息 - // TODO @haohao:serverId 不是这里解析的哈。 - Long deviceId = parseDeviceId(dataPackage.getAddr()); - message.setDeviceId(deviceId); - - // 6. 设置 TCP 协议相关信息 - // TODO @haohao:serverId 不是这里解析的哈。 - message.setServerId(generateServerId(dataPackage)); - - // 7. 设置租户 ID(TODO: 后续可以从设备信息中获取) - // TODO @haohao:租户 id 不是这里解析的哈。 - // message.setTenantId(getTenantIdByDeviceId(deviceId)); - - if (log.isDebugEnabled()) { - log.debug("[decode][解码成功] 设备ID: {}, 方法: {}, 请求ID: {}, 消息ID: {}", - deviceId, method, message.getRequestId(), message.getId()); - } - - return message; - } catch (Exception e) { - log.error("[decode][解码失败] 数据长度: {}", bytes.length, e); - throw new TcpCodecException("TCP 消息解码失败", e); - } } @Override @@ -110,142 +76,134 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { return TYPE; } - // TODO @haohao:这种简单解析,中间不用空格哈。 - /** - * 构建完整负载 - */ - private String buildSimplePayload(IotDeviceMessage message) { - JSONObject payload = new JSONObject(); + @Override + public byte[] encode(IotDeviceMessage message) { + Assert.notNull(message, "消息不能为空"); + Assert.notBlank(message.getMethod(), "消息方法不能为空"); - // 核心字段 - payload.set(PayloadField.METHOD, message.getMethod()); - if (message.getParams() != null) { - payload.set(PayloadField.PARAMS, message.getParams()); + try { + // 1. 确定功能码 + short code = MessageMethod.STATE_ONLINE.equals(message.getMethod()) ? TcpDataPackage.CODE_HEARTBEAT + : TcpDataPackage.CODE_MESSAGE_UP; + + // 2. 构建负载数据 + String payload = buildPayload(message); + + // 3. 构建 TCP 数据包 + short mid = (short) (System.currentTimeMillis() % Short.MAX_VALUE); + TcpDataPackage dataPackage = new TcpDataPackage(code, mid, payload); + + // 4. 编码为字节流 + return encodeTcpDataPackage(dataPackage).getBytes(); + } catch (Exception e) { + throw new TcpCodecException("TCP 消息编码失败", e); } - - // 标识字段 - if (StrUtil.isNotEmpty(message.getRequestId())) { - payload.set(PayloadField.REQUEST_ID, message.getRequestId()); - } - if (StrUtil.isNotEmpty(message.getId())) { - payload.set(PayloadField.MESSAGE_ID, message.getId()); - } - - // 时间戳 - payload.set(PayloadField.TIMESTAMP, System.currentTimeMillis()); - - return payload.toString(); } - // ==================== 编解码方法 ==================== + @Override + @SuppressWarnings("DataFlowIssue") + public IotDeviceMessage decode(byte[] bytes) { + Assert.notNull(bytes, "待解码数据不能为空"); + Assert.isTrue(bytes.length > 0, "待解码数据不能为空"); + + try { + // 1. 解码 TCP 数据包 + TcpDataPackage dataPackage = decodeTcpDataPackage(Buffer.buffer(bytes)); + + // 2. 根据功能码确定方法 + String method = (dataPackage.getCode() == TcpDataPackage.CODE_HEARTBEAT) ? MessageMethod.STATE_ONLINE + : MessageMethod.PROPERTY_POST; + + // 3. 解析负载数据 + PayloadInfo payloadInfo = parsePayloadInfo(dataPackage.getPayload()); + + // 4. 构建 IoT 设备消息 + return IotDeviceMessage.of( + payloadInfo.getRequestId(), + method, + payloadInfo.getParams(), + null, + null, + null); + } catch (Exception e) { + throw new TcpCodecException("TCP 消息解码失败", e); + } + } + + // ==================== 内部辅助方法 ==================== /** - * 解析负载信息(包含 requestId 和 params) + * 构建负载数据 + * + * @param message 设备消息 + * @return 负载字符串 + */ + private String buildPayload(IotDeviceMessage message) { + TcpBinaryMessage tcpBinaryMessage = new TcpBinaryMessage( + null, // code 在数据包中单独处理 + null, // mid 在数据包中单独处理 + message.getDeviceId(), + message.getMethod(), + message.getParams(), + message.getData(), + message.getCode(), + message.getMsg()); + return JsonUtils.toJsonString(tcpBinaryMessage); + } + + /** + * 解析负载信息 + * + * @param payload 负载字符串 + * @return 负载信息 */ private PayloadInfo parsePayloadInfo(String payload) { - if (StrUtil.isEmpty(payload)) { + if (StrUtil.isBlank(payload)) { return new PayloadInfo(null, null); } try { - // TODO @haohao:使用 jsonUtils - JSONObject jsonObject = JSONUtil.parseObj(payload); - String requestId = jsonObject.getStr(PayloadField.REQUEST_ID); - if (StrUtil.isEmpty(requestId)) { - requestId = jsonObject.getStr(PayloadField.MESSAGE_ID); + TcpBinaryMessage tcpBinaryMessage = JsonUtils.parseObject(payload, TcpBinaryMessage.class); + if (tcpBinaryMessage != null) { + return new PayloadInfo( + StrUtil.isNotEmpty(tcpBinaryMessage.getMethod()) + ? tcpBinaryMessage.getMethod() + "_" + System.currentTimeMillis() + : null, + tcpBinaryMessage.getParams()); } - Object params = jsonObject.get(PayloadField.PARAMS); - return new PayloadInfo(requestId, params); } catch (Exception e) { - log.warn("[parsePayloadInfo][解析失败,返回原始字符串] 负载: {}", payload); - return new PayloadInfo(null, payload); + // 如果解析失败,返回默认值 + return new PayloadInfo("unknown_" + System.currentTimeMillis(), null); } + return null; } - /** - * 从设备地址解析设备ID - * - * @param deviceAddr 设备地址字符串 - * @return 设备ID - */ - private Long parseDeviceId(String deviceAddr) { - if (StrUtil.isEmpty(deviceAddr)) { - log.warn("[parseDeviceId][设备地址为空,返回默认ID]"); - return 0L; - } - - try { - // 尝试直接解析为Long - return Long.parseLong(deviceAddr); - } catch (NumberFormatException e) { - // 如果不是纯数字,可以使用哈希值或其他策略 - log.warn("[parseDeviceId][设备地址不是数字格式: {},使用哈希值]", deviceAddr); - return (long) deviceAddr.hashCode(); - } - } - - /** - * 生成服务ID - * - * @param dataPackage TCP数据包 - * @return 服务ID - */ - private String generateServerId(TcpDataPackage dataPackage) { - // 使用协议类型 + 设备地址 + 消息序号生成唯一的服务 ID - return String.format("tcp_%s_%d", dataPackage.getAddr(), dataPackage.getMid()); - } - - // ==================== 内部辅助方法 ==================== - /** * 编码 TCP 数据包 * * @param dataPackage 数据包对象 * @return 编码后的字节流 - * @throws IllegalArgumentException 如果数据包对象不正确 */ private Buffer encodeTcpDataPackage(TcpDataPackage dataPackage) { - if (dataPackage == null) { - throw new IllegalArgumentException("数据包对象不能为空"); - } + Assert.notNull(dataPackage, "数据包对象不能为空"); + Assert.notNull(dataPackage.getPayload(), "负载不能为空"); - // 验证数据包 - if (dataPackage.getAddr() == null || dataPackage.getAddr().isEmpty()) { - throw new IllegalArgumentException("设备地址不能为空"); - } - if (dataPackage.getPayload() == null) { - throw new IllegalArgumentException("负载不能为空"); - } + Buffer buffer = Buffer.buffer(); - try { - Buffer buffer = Buffer.buffer(); + // 1. 计算包体长度(除了包头 4 字节) + int payloadLength = dataPackage.getPayload().getBytes().length; + int totalLength = 2 + 2 + payloadLength; - // 1. 计算包体长度(除了包头 4 字节) - int payloadLength = dataPackage.getPayload().getBytes().length; - int totalLength = 2 + dataPackage.getAddr().length() + 2 + 2 + payloadLength; + // 2. 写入包头:总长度(4 字节) + buffer.appendInt(totalLength); + // 3. 写入功能码(2 字节) + buffer.appendShort(dataPackage.getCode()); + // 4. 写入消息序号(2 字节) + buffer.appendShort(dataPackage.getMid()); + // 5. 写入包体数据(不定长) + buffer.appendBytes(dataPackage.getPayload().getBytes()); - // 2.1 写入包头:总长度(4 字节) - buffer.appendInt(totalLength); - // 2.2 写入设备地址长度(2 字节) - buffer.appendShort((short) dataPackage.getAddr().length()); - // 2.3 写入设备地址(不定长) - buffer.appendBytes(dataPackage.getAddr().getBytes()); - // 2.4 写入功能码(2 字节) - buffer.appendShort(dataPackage.getCode()); - // 2.5 写入消息序号(2 字节) - buffer.appendShort(dataPackage.getMid()); - // 2.6 写入包体数据(不定长) - buffer.appendBytes(dataPackage.getPayload().getBytes()); - - if (log.isDebugEnabled()) { - log.debug("[encodeTcpDataPackage][编码成功] 设备地址: {}, 功能码: {}, 消息序号: {}, 总长度: {}", - dataPackage.getAddr(), dataPackage.getCode(), dataPackage.getMid(), buffer.length()); - } - return buffer; - } catch (Exception e) { - log.error("[encodeTcpDataPackage][编码失败] 数据包: {}", dataPackage, e); - throw new IllegalArgumentException("数据包编码失败: " + e.getMessage(), e); - } + return buffer; } /** @@ -253,101 +211,49 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { * * @param buffer 数据缓冲区 * @return 解码后的数据包 - * @throws IllegalArgumentException 如果数据包格式不正确 */ private TcpDataPackage decodeTcpDataPackage(Buffer buffer) { - if (buffer == null || buffer.length() < 8) { - throw new IllegalArgumentException("数据包长度不足"); + Assert.isTrue(buffer.length() >= 8, "数据包长度不足"); + + int index = 0; + + // 1. 跳过包头(4 字节) + index += 4; + + // 2. 获取功能码(2 字节) + short code = buffer.getShort(index); + index += 2; + + // 3. 获取消息序号(2 字节) + short mid = buffer.getShort(index); + index += 2; + + // 4. 获取包体数据 + String payload = ""; + if (index < buffer.length()) { + payload = buffer.getString(index, buffer.length()); } - try { - int index = 0; - - // 1.1 跳过包头(4字节) - index += 4; - - // 1.2 获取设备地址长度(2字节) - short addrLength = buffer.getShort(index); - index += 2; - - // 1.3 获取设备地址 - String addr = buffer.getBuffer(index, index + addrLength).toString(); - index += addrLength; - - // 1.4 获取功能码(2字节) - short code = buffer.getShort(index); - index += 2; - - // 1.5 获取消息序号(2字节) - short mid = buffer.getShort(index); - index += 2; - - // 1.6 获取包体数据 - String payload = ""; - if (index < buffer.length()) { - payload = buffer.getString(index, buffer.length()); - } - - // 2. 构建数据包对象 - TcpDataPackage dataPackage = new TcpDataPackage(addr, code, mid, payload); - - if (log.isDebugEnabled()) { - log.debug("[decodeTcpDataPackage][解码成功] 设备地址: {}, 功能码: {}, 消息序号: {}, 包体长度: {}", - addr, code, mid, payload.length()); - } - return dataPackage; - } catch (Exception e) { - log.error("[decodeTcpDataPackage][解码失败] 数据长度: {}", buffer.length(), e); - throw new IllegalArgumentException("数据包解码失败: " + e.getMessage(), e); - } + return new TcpDataPackage(code, mid, payload); } - /** - * 消息方法常量 - */ - public static class MessageMethod { + // ==================== 内部类 ==================== - public static final String PROPERTY_POST = "thing.property.post"; // 数据上报 - public static final String STATE_ONLINE = "thing.state.online"; // 心跳 - - } - - /** - * 负载字段名 - */ - private static class PayloadField { - - public static final String METHOD = "method"; - public static final String PARAMS = "params"; - public static final String TIMESTAMP = "timestamp"; - public static final String REQUEST_ID = "requestId"; - public static final String MESSAGE_ID = "msgId"; - - } - - // ==================== TCP 数据包编解码方法 ==================== - - // TODO @haohao:lombok 简化 /** * 负载信息类 */ + @Data + @AllArgsConstructor private static class PayloadInfo { private String requestId; private Object params; - - public PayloadInfo(String requestId, Object params) { - this.requestId = requestId; - this.params = params; - } - - public String getRequestId() { return requestId; } - public Object getParams() { return params; } } /** * TCP 数据包内部类 */ @Data + @AllArgsConstructor private static class TcpDataPackage { // 功能码定义 public static final short CODE_REGISTER = 10; @@ -357,35 +263,29 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { public static final short CODE_MESSAGE_UP = 30; public static final short CODE_MESSAGE_DOWN = 40; - private String addr; private short code; private short mid; private String payload; + } - public TcpDataPackage(String addr, short code, short mid, String payload) { - this.addr = addr; - this.code = code; - this.mid = mid; - this.payload = payload; - } + // ==================== 常量定义 ==================== + + /** + * 消息方法常量 + */ + public static class MessageMethod { + public static final String PROPERTY_POST = "thing.property.post"; // 数据上报 + public static final String STATE_ONLINE = "thing.state.online"; // 心跳 } // ==================== 自定义异常 ==================== - // TODO @haohao:可以搞个全局的; /** * TCP 编解码异常 */ public static class TcpCodecException extends RuntimeException { - - // TODO @haohao:非必要构造方法,可以去掉哈。 - public TcpCodecException(String message) { - super(message); - } - public TcpCodecException(String message, Throwable cause) { super(message, cause); } - } } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpCodecManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpCodecManager.java deleted file mode 100644 index 8810a982ea..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpCodecManager.java +++ /dev/null @@ -1,143 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.codec.tcp; - -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -/** - * TCP编解码器管理器(简化版) - * - * 核心功能: - * - 自动协议检测(二进制 vs JSON) - * - 统一编解码接口 - * - 默认使用 JSON 协议 - * - * @author 芋道源码 - */ -@Slf4j -@Component -public class IotTcpCodecManager implements IotDeviceMessageCodec { - - public static final String TYPE = "TCP"; - - // TODO @haohao:@Resource - - @Autowired - private IotTcpBinaryDeviceMessageCodec binaryCodec; - - @Autowired - private IotTcpJsonDeviceMessageCodec jsonCodec; - - /** - * 当前默认协议(JSON) - */ - private boolean useJsonByDefault = true; - - @Override - public String type() { - return TYPE; - } - - @Override - public byte[] encode(IotDeviceMessage message) { - // 默认使用 JSON 协议编码 - return jsonCodec.encode(message); - } - - // TODO @haohao:要不还是不自动检测,用户手动配置哈。简化一些。。。 - @Override - public IotDeviceMessage decode(byte[] bytes) { - // 自动检测协议类型并解码 - if (isJsonFormat(bytes)) { - if (log.isDebugEnabled()) { - log.debug("[decode][检测到 JSON 协议,数据长度: {} 字节]", bytes.length); - } - return jsonCodec.decode(bytes); - } else { - if (log.isDebugEnabled()) { - log.debug("[decode][检测到二进制协议,数据长度: {} 字节]", bytes.length); - } - return binaryCodec.decode(bytes); - } - } - - // ==================== 便捷方法 ==================== - - /** - * 使用 JSON 协议编码 - */ - public byte[] encodeJson(IotDeviceMessage message) { - return jsonCodec.encode(message); - } - - /** - * 使用二进制协议编码 - */ - public byte[] encodeBinary(IotDeviceMessage message) { - return binaryCodec.encode(message); - } - - /** - * 获取当前默认协议 - */ - public String getDefaultProtocol() { - return useJsonByDefault ? "JSON" : "BINARY"; - } - - /** - * 设置默认协议 - */ - public void setDefaultProtocol(boolean useJson) { - this.useJsonByDefault = useJson; - log.info("[setDefaultProtocol][设置默认协议] 使用JSON: {}", useJson); - } - - // ==================== 内部方法 ==================== - - /** - * 检测是否为JSON格式 - * - * 检测规则: - * 1. 数据以 '{' 开头 - * 2. 包含 "method" 或 "id" 字段 - */ - private boolean isJsonFormat(byte[] bytes) { - // TODO @haohao:ArrayUtil.isEmpty(bytes) 可以简化下 - if (bytes == null || bytes.length == 0) { - return useJsonByDefault; - } - - try { - // 检测 JSON 格式:以 '{' 开头 - if (bytes[0] == '{') { - // TODO @haohao:不一定按照顺序写,这个可能要看下。 - // 进一步验证是否为有效 JSON - String jsonStr = new String(bytes, 0, Math.min(bytes.length, 100)); - return jsonStr.contains("\"method\"") || jsonStr.contains("\"id\""); - } - - // 检测二进制格式:长度 >= 8 且符合二进制协议结构 - if (bytes.length >= 8) { - // 读取包头(前 4 字节表示后续数据长度) - int expectedLength = ((bytes[0] & 0xFF) << 24) | - ((bytes[1] & 0xFF) << 16) | - ((bytes[2] & 0xFF) << 8) | - (bytes[3] & 0xFF); - - // 验证长度是否合理 - // TODO @haohao:expectedLength > 0 多余的貌似; - if (expectedLength == bytes.length - 4 && expectedLength > 0 && expectedLength < 1024 * 1024) { - return false; // 二进制格式 - } - } - } catch (Exception e) { - log.warn("[isJsonFormat][协议检测异常,使用默认协议: {}]", getDefaultProtocol(), e); - } - - // 默认使用当前设置的协议类型 - return useJsonByDefault; - } - -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java index 39e8b83d24..f1c88d396f 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java @@ -1,42 +1,81 @@ package cn.iocoder.yudao.module.iot.gateway.codec.tcp; -import cn.hutool.core.util.IdUtil; -import cn.hutool.core.util.StrUtil; -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import lombok.extern.slf4j.Slf4j; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; import org.springframework.stereotype.Component; -import java.nio.charset.StandardCharsets; - /** * TCP JSON 格式 {@link IotDeviceMessage} 编解码器 * - * 采用纯 JSON 格式传输,参考 EMQX 和 HTTP 模块的数据格式 + * 采用纯 JSON 格式传输 * - * JSON消息格式: + * JSON 消息格式: * { - * "id": "消息 ID", - * "method": "消息方法", - * "deviceId": "设备 ID", - * "productKey": "产品 Key", - * "deviceName": "设备名称", - * "params": {...}, - * "timestamp": 时间戳 + * "id": "消息 ID", + * "method": "消息方法", + * "deviceId": "设备 ID", + * "params": {...}, + * "timestamp": 时间戳 * } * * @author 芋道源码 */ -@Slf4j @Component public class IotTcpJsonDeviceMessageCodec implements IotDeviceMessageCodec { public static final String TYPE = "TCP_JSON"; - // TODO @haohao:变量不太对; - // ==================== 常量定义 ==================== + @Data + @NoArgsConstructor + @AllArgsConstructor + private static class TcpJsonMessage { + + /** + * 消息 ID,且每个消息 ID 在当前设备具有唯一性 + */ + private String id; + + /** + * 请求方法 + */ + private String method; + + /** + * 设备 ID + */ + private Long deviceId; + + /** + * 请求参数 + */ + private Object params; + + /** + * 响应结果 + */ + private Object data; + + /** + * 响应错误码 + */ + private Integer code; + + /** + * 响应提示 + */ + private String msg; + + /** + * 时间戳 + */ + private Long timestamp; + + } @Override public String type() { @@ -45,208 +84,33 @@ public class IotTcpJsonDeviceMessageCodec implements IotDeviceMessageCodec { @Override public byte[] encode(IotDeviceMessage message) { - if (message == null || StrUtil.isEmpty(message.getMethod())) { - throw new IllegalArgumentException("消息或方法不能为空"); - } - - try { - // 构建JSON消息 - JSONObject jsonMessage = buildJsonMessage(message); - - // 转换为字节数组 - String jsonString = jsonMessage.toString(); - byte[] result = jsonString.getBytes(StandardCharsets.UTF_8); - - if (log.isDebugEnabled()) { - log.debug("[encode][编码成功] 方法: {}, JSON长度: {}字节, 内容: {}", - message.getMethod(), result.length, jsonString); - } - - return result; - } catch (Exception e) { - log.error("[encode][编码失败] 方法: {}", message.getMethod(), e); - throw new RuntimeException("JSON消息编码失败", e); - } + TcpJsonMessage tcpJsonMessage = new TcpJsonMessage( + message.getRequestId(), + message.getMethod(), + message.getDeviceId(), + message.getParams(), + message.getData(), + message.getCode(), + message.getMsg(), + System.currentTimeMillis()); + return JsonUtils.toJsonByte(tcpJsonMessage); } - // ==================== 编解码方法 ==================== - @Override + @SuppressWarnings("DataFlowIssue") public IotDeviceMessage decode(byte[] bytes) { - if (bytes == null || bytes.length == 0) { - throw new IllegalArgumentException("待解码数据不能为空"); - } - - try { - // 转换为 JSON 字符串 - String jsonString = new String(bytes, StandardCharsets.UTF_8); - - if (log.isDebugEnabled()) { - log.debug("[decode][开始解码] JSON长度: {}字节, 内容: {}", bytes.length, jsonString); - } - - // 解析 JSON 消息 - // TODO @haohao:JsonUtils - JSONObject jsonMessage = JSONUtil.parseObj(jsonString); - - // 构建IoT设备消息 - IotDeviceMessage message = parseJsonMessage(jsonMessage); - - if (log.isDebugEnabled()) { - log.debug("[decode][解码成功] 消息ID: {}, 方法: {}, 设备ID: {}", - message.getId(), message.getMethod(), message.getDeviceId()); - } - - return message; - } catch (Exception e) { - log.error("[decode][解码失败] 数据长度: {}", bytes.length, e); - throw new RuntimeException("JSON消息解码失败", e); - } + TcpJsonMessage tcpJsonMessage = JsonUtils.parseObject(bytes, TcpJsonMessage.class); + Assert.notNull(tcpJsonMessage, "消息不能为空"); + Assert.notBlank(tcpJsonMessage.getMethod(), "消息方法不能为空"); + IotDeviceMessage iotDeviceMessage = IotDeviceMessage.of( + tcpJsonMessage.getId(), + tcpJsonMessage.getMethod(), + tcpJsonMessage.getParams(), + tcpJsonMessage.getData(), + tcpJsonMessage.getCode(), + tcpJsonMessage.getMsg()); + iotDeviceMessage.setDeviceId(tcpJsonMessage.getDeviceId()); + return iotDeviceMessage; } - /** - * 编码数据上报消息 - */ - public byte[] encodeDataReport(Object params, Long deviceId, String productKey, String deviceName) { - IotDeviceMessage message = createMessage(MessageMethod.PROPERTY_POST, params, deviceId, productKey, deviceName); - return encode(message); - } - - /** - * 编码心跳消息 - */ - public byte[] encodeHeartbeat(Long deviceId, String productKey, String deviceName) { - IotDeviceMessage message = createMessage(MessageMethod.STATE_ONLINE, null, deviceId, productKey, deviceName); - return encode(message); - } - - // ==================== 便捷方法 ==================== - - /** - * 编码事件上报消息 - */ - public byte[] encodeEventReport(Object params, Long deviceId, String productKey, String deviceName) { - IotDeviceMessage message = createMessage(MessageMethod.EVENT_POST, params, deviceId, productKey, deviceName); - return encode(message); - } - - /** - * 构建 JSON 消息 - */ - private JSONObject buildJsonMessage(IotDeviceMessage message) { - JSONObject jsonMessage = new JSONObject(); - - // 基础字段 - jsonMessage.set(JsonField.ID, StrUtil.isNotEmpty(message.getId()) ? message.getId() : IdUtil.fastSimpleUUID()); - jsonMessage.set(JsonField.METHOD, message.getMethod()); - jsonMessage.set(JsonField.TIMESTAMP, System.currentTimeMillis()); - - // 设备信息 - if (message.getDeviceId() != null) { - jsonMessage.set(JsonField.DEVICE_ID, message.getDeviceId()); - } - - // 参数 - if (message.getParams() != null) { - jsonMessage.set(JsonField.PARAMS, message.getParams()); - } - - // 响应码和消息(用于下行消息) - if (message.getCode() != null) { - jsonMessage.set(JsonField.CODE, message.getCode()); - } - if (StrUtil.isNotEmpty(message.getMsg())) { - jsonMessage.set(JsonField.MESSAGE, message.getMsg()); - } - - return jsonMessage; - } - - /** - * 解析JSON消息 - */ - private IotDeviceMessage parseJsonMessage(JSONObject jsonMessage) { - // 提取基础字段 - String id = jsonMessage.getStr(JsonField.ID); - String method = jsonMessage.getStr(JsonField.METHOD); - Object params = jsonMessage.get(JsonField.PARAMS); - - // 创建消息对象 - IotDeviceMessage message = IotDeviceMessage.requestOf(id, method, params); - - // 设置设备信息 - Long deviceId = jsonMessage.getLong(JsonField.DEVICE_ID); - if (deviceId != null) { - message.setDeviceId(deviceId); - } - - // 设置响应信息 - Integer code = jsonMessage.getInt(JsonField.CODE); - if (code != null) { - message.setCode(code); - } - - String msg = jsonMessage.getStr(JsonField.MESSAGE); - if (StrUtil.isNotEmpty(msg)) { - message.setMsg(msg); - } - - // 设置服务 ID(基于 JSON 格式) - message.setServerId(generateServerId(jsonMessage)); - - return message; - } - - // ==================== 内部辅助方法 ==================== - - /** - * 创建消息对象 - */ - private IotDeviceMessage createMessage(String method, Object params, Long deviceId, String productKey, String deviceName) { - IotDeviceMessage message = IotDeviceMessage.requestOf(method, params); - message.setDeviceId(deviceId); - return message; - } - - /** - * 生成服务ID - */ - private String generateServerId(JSONObject jsonMessage) { - String id = jsonMessage.getStr(JsonField.ID); - Long deviceId = jsonMessage.getLong(JsonField.DEVICE_ID); - return String.format("tcp_json_%s_%s", deviceId != null ? deviceId : "unknown", - StrUtil.isNotEmpty(id) ? id.substring(0, Math.min(8, id.length())) : "noId"); - } - - // TODO @haohao:注释格式不对; - /** - * 消息方法常量 - */ - public static class MessageMethod { - - public static final String PROPERTY_POST = "thing.property.post"; // 数据上报 - public static final String STATE_ONLINE = "thing.state.online"; // 心跳 - public static final String EVENT_POST = "thing.event.post"; // 事件上报 - public static final String PROPERTY_SET = "thing.property.set"; // 属性设置 - public static final String PROPERTY_GET = "thing.property.get"; // 属性获取 - public static final String SERVICE_INVOKE = "thing.service.invoke"; // 服务调用 - - } - - /** - * JSON字段名(参考EMQX和HTTP模块格式) - */ - private static class JsonField { - - public static final String ID = "id"; - public static final String METHOD = "method"; - public static final String DEVICE_ID = "deviceId"; - public static final String PRODUCT_KEY = "productKey"; - public static final String DEVICE_NAME = "deviceName"; - public static final String PARAMS = "params"; - public static final String TIMESTAMP = "timestamp"; - public static final String CODE = "code"; - public static final String MESSAGE = "message"; - - } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java index cd878994c7..72fc0eef50 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java @@ -1,8 +1,6 @@ package cn.iocoder.yudao.module.iot.gateway.config; -import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpCodecManager; import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxAuthEventProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol; @@ -10,6 +8,7 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpDownstreamSubscr import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpSessionManager; import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import io.vertx.core.Vertx; @@ -93,18 +92,20 @@ public class IotGatewayConfiguration { public IotTcpUpstreamProtocol iotTcpUpstreamProtocol(IotGatewayProperties gatewayProperties, IotDeviceService deviceService, IotDeviceMessageService messageService, - IotDeviceCommonApi deviceApi, - IotTcpCodecManager codecManager, + IotTcpSessionManager sessionManager, Vertx tcpVertx) { return new IotTcpUpstreamProtocol(gatewayProperties.getProtocol().getTcp(), - deviceService, messageService, deviceApi, codecManager, tcpVertx); + deviceService, messageService, sessionManager, tcpVertx); } @Bean public IotTcpDownstreamSubscriber iotTcpDownstreamSubscriber(IotTcpUpstreamProtocol protocolHandler, IotDeviceMessageService messageService, + IotDeviceService deviceService, + IotTcpSessionManager sessionManager, IotMessageBus messageBus) { - return new IotTcpDownstreamSubscriber(protocolHandler, messageService, messageBus); + return new IotTcpDownstreamSubscriber(protocolHandler, messageService, deviceService, sessionManager, + messageBus); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java index 95d435387e..2022805fc5 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java @@ -4,10 +4,13 @@ import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpSessionManager; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpDownstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import jakarta.annotation.PostConstruct; import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; /** * IoT 网关 TCP 下游订阅者:接收下行给设备的消息 @@ -15,6 +18,7 @@ import lombok.extern.slf4j.Slf4j; * @author 芋道源码 */ @Slf4j +@Component public class IotTcpDownstreamSubscriber implements IotMessageSubscriber { private final IotTcpDownstreamHandler downstreamHandler; @@ -23,17 +27,27 @@ public class IotTcpDownstreamSubscriber implements IotMessageSubscriber { - IotTcpUpstreamHandler handler = new IotTcpUpstreamHandler(this, messageService, codecManager); + IotTcpUpstreamHandler handler = new IotTcpUpstreamHandler(this, messageService, deviceService, + sessionManager); handler.handle(socket); }); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpAuthManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpAuthManager.java new file mode 100644 index 0000000000..57cb67b1e2 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpAuthManager.java @@ -0,0 +1,194 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager; + +import io.vertx.core.net.NetSocket; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * IoT 网关 TCP 认证信息管理器 + *

+ * 维护 TCP 连接的认证状态,支持认证信息的存储、查询和清理 + * + * @author 芋道源码 + */ +@Slf4j +@Component +public class IotTcpAuthManager { + + /** + * 连接认证状态映射:NetSocket -> 认证信息 + */ + private final Map authStatusMap = new ConcurrentHashMap<>(); + + /** + * 设备 ID -> NetSocket 的映射(用于快速查找) + */ + private final Map deviceSocketMap = new ConcurrentHashMap<>(); + + /** + * 注册认证信息 + * + * @param socket TCP 连接 + * @param authInfo 认证信息 + */ + public void registerAuth(NetSocket socket, AuthInfo authInfo) { + // 如果设备已有其他连接,先清理旧连接 + NetSocket oldSocket = deviceSocketMap.get(authInfo.getDeviceId()); + if (oldSocket != null && oldSocket != socket) { + log.info("[registerAuth][设备已有其他连接,清理旧连接] 设备 ID: {}, 旧连接: {}", + authInfo.getDeviceId(), oldSocket.remoteAddress()); + authStatusMap.remove(oldSocket); + } + + // 注册新认证信息 + authStatusMap.put(socket, authInfo); + deviceSocketMap.put(authInfo.getDeviceId(), socket); + + log.info("[registerAuth][注册认证信息] 设备 ID: {}, 连接: {}, productKey: {}, deviceName: {}", + authInfo.getDeviceId(), socket.remoteAddress(), authInfo.getProductKey(), authInfo.getDeviceName()); + } + + /** + * 注销认证信息 + * + * @param socket TCP 连接 + */ + public void unregisterAuth(NetSocket socket) { + AuthInfo authInfo = authStatusMap.remove(socket); + if (authInfo != null) { + deviceSocketMap.remove(authInfo.getDeviceId()); + log.info("[unregisterAuth][注销认证信息] 设备 ID: {}, 连接: {}", + authInfo.getDeviceId(), socket.remoteAddress()); + } + } + + /** + * 注销设备认证信息 + * + * @param deviceId 设备 ID + */ + public void unregisterAuth(Long deviceId) { + NetSocket socket = deviceSocketMap.remove(deviceId); + if (socket != null) { + AuthInfo authInfo = authStatusMap.remove(socket); + if (authInfo != null) { + log.info("[unregisterAuth][注销设备认证信息] 设备 ID: {}, 连接: {}", + deviceId, socket.remoteAddress()); + } + } + } + + /** + * 获取认证信息 + * + * @param socket TCP 连接 + * @return 认证信息,如果未认证则返回 null + */ + public AuthInfo getAuthInfo(NetSocket socket) { + return authStatusMap.get(socket); + } + + /** + * 获取设备的认证信息 + * + * @param deviceId 设备 ID + * @return 认证信息,如果设备未认证则返回 null + */ + public AuthInfo getAuthInfo(Long deviceId) { + NetSocket socket = deviceSocketMap.get(deviceId); + return socket != null ? authStatusMap.get(socket) : null; + } + + /** + * 检查连接是否已认证 + * + * @param socket TCP 连接 + * @return 是否已认证 + */ + public boolean isAuthenticated(NetSocket socket) { + return authStatusMap.containsKey(socket); + } + + /** + * 检查设备是否已认证 + * + * @param deviceId 设备 ID + * @return 是否已认证 + */ + public boolean isAuthenticated(Long deviceId) { + return deviceSocketMap.containsKey(deviceId); + } + + /** + * 获取设备的 TCP 连接 + * + * @param deviceId 设备 ID + * @return TCP 连接,如果设备未认证则返回 null + */ + public NetSocket getDeviceSocket(Long deviceId) { + return deviceSocketMap.get(deviceId); + } + + /** + * 获取当前已认证设备数量 + * + * @return 已认证设备数量 + */ + public int getAuthenticatedDeviceCount() { + return deviceSocketMap.size(); + } + + /** + * 获取所有已认证设备 ID + * + * @return 已认证设备 ID 集合 + */ + public java.util.Set getAuthenticatedDeviceIds() { + return deviceSocketMap.keySet(); + } + + /** + * 清理所有认证信息 + */ + public void clearAll() { + int count = authStatusMap.size(); + authStatusMap.clear(); + deviceSocketMap.clear(); + log.info("[clearAll][清理所有认证信息] 清理数量: {}", count); + } + + /** + * 认证信息 + */ + @Data + public static class AuthInfo { + /** + * 设备编号 + */ + private Long deviceId; + + /** + * 产品标识 + */ + private String productKey; + + /** + * 设备名称 + */ + private String deviceName; + + /** + * 认证令牌 + */ + private String token; + + /** + * 客户端 ID + */ + private String clientId; + } +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpSessionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpSessionManager.java new file mode 100644 index 0000000000..6baa899f30 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpSessionManager.java @@ -0,0 +1,143 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager; + +import io.vertx.core.net.NetSocket; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * IoT 网关 TCP 会话管理器 + *

+ * 维护设备 ID 和 TCP 连接的映射关系,支持下行消息发送 + * + * @author 芋道源码 + */ +@Slf4j +@Component +public class IotTcpSessionManager { + + /** + * 设备 ID -> TCP 连接的映射 + */ + private final Map deviceSocketMap = new ConcurrentHashMap<>(); + + /** + * TCP 连接 -> 设备 ID 的映射(用于连接断开时清理) + */ + private final Map socketDeviceMap = new ConcurrentHashMap<>(); + + /** + * 注册设备会话 + * + * @param deviceId 设备 ID + * @param socket TCP 连接 + */ + public void registerSession(Long deviceId, NetSocket socket) { + // 如果设备已有连接,先断开旧连接 + NetSocket oldSocket = deviceSocketMap.get(deviceId); + if (oldSocket != null && oldSocket != socket) { + log.info("[registerSession][设备已有连接,断开旧连接] 设备 ID: {}, 旧连接: {}", deviceId, oldSocket.remoteAddress()); + oldSocket.close(); + socketDeviceMap.remove(oldSocket); + } + + // 注册新连接 + deviceSocketMap.put(deviceId, socket); + socketDeviceMap.put(socket, deviceId); + + log.info("[registerSession][注册设备会话] 设备 ID: {}, 连接: {}", deviceId, socket.remoteAddress()); + } + + /** + * 注销设备会话 + * + * @param deviceId 设备 ID + */ + public void unregisterSession(Long deviceId) { + NetSocket socket = deviceSocketMap.remove(deviceId); + if (socket != null) { + socketDeviceMap.remove(socket); + log.info("[unregisterSession][注销设备会话] 设备 ID: {}, 连接: {}", deviceId, socket.remoteAddress()); + } + } + + /** + * 注销 TCP 连接会话 + * + * @param socket TCP 连接 + */ + public void unregisterSession(NetSocket socket) { + Long deviceId = socketDeviceMap.remove(socket); + if (deviceId != null) { + deviceSocketMap.remove(deviceId); + log.info("[unregisterSession][注销连接会话] 设备 ID: {}, 连接: {}", deviceId, socket.remoteAddress()); + } + } + + /** + * 获取设备的 TCP 连接 + * + * @param deviceId 设备 ID + * @return TCP 连接,如果设备未连接则返回 null + */ + public NetSocket getDeviceSocket(Long deviceId) { + return deviceSocketMap.get(deviceId); + } + + /** + * 检查设备是否在线 + * + * @param deviceId 设备 ID + * @return 是否在线 + */ + public boolean isDeviceOnline(Long deviceId) { + NetSocket socket = deviceSocketMap.get(deviceId); + return socket != null; + } + + /** + * 发送消息到设备 + * + * @param deviceId 设备 ID + * @param data 消息数据 + * @return 是否发送成功 + */ + public boolean sendToDevice(Long deviceId, byte[] data) { + NetSocket socket = deviceSocketMap.get(deviceId); + if (socket == null) { + log.warn("[sendToDevice][设备未连接] 设备 ID: {}", deviceId); + return false; + } + + try { + socket.write(io.vertx.core.buffer.Buffer.buffer(data)); + log.debug("[sendToDevice][发送消息成功] 设备 ID: {}, 数据长度: {} 字节", deviceId, data.length); + return true; + } catch (Exception e) { + log.error("[sendToDevice][发送消息失败] 设备 ID: {}", deviceId, e); + // 发送失败时清理连接 + unregisterSession(deviceId); + return false; + } + } + + /** + * 获取当前在线设备数量 + * + * @return 在线设备数量 + */ + public int getOnlineDeviceCount() { + return deviceSocketMap.size(); + } + + /** + * 获取所有在线设备 ID + * + * @return 在线设备 ID 集合 + */ + public java.util.Set getOnlineDeviceIds() { + return deviceSocketMap.keySet(); + } +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java index 053be8d437..abf71338de 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java @@ -1,20 +1,14 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpSessionManager; +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import lombok.extern.slf4j.Slf4j; /** * IoT 网关 TCP 下行消息处理器 - *

- * 负责处理从业务系统发送到设备的下行消息,包括: - * 1. 属性设置 - * 2. 服务调用 - * 3. 属性获取 - * 4. 配置下发 - * 5. OTA 升级 - *

- * 注意:由于移除了连接管理器,此处理器主要负责消息的编码和日志记录 * * @author 芋道源码 */ @@ -23,12 +17,15 @@ public class IotTcpDownstreamHandler { private final IotDeviceMessageService messageService; - // TODO @haohao:代码没提交全,有报错。 -// private final IotTcpDeviceMessageCodec codec; + private final IotDeviceService deviceService; - public IotTcpDownstreamHandler(IotDeviceMessageService messageService) { + private final IotTcpSessionManager sessionManager; + + public IotTcpDownstreamHandler(IotDeviceMessageService messageService, + IotDeviceService deviceService, IotTcpSessionManager sessionManager) { this.messageService = messageService; -// this.codec = new IotTcpDeviceMessageCodec(); + this.deviceService = deviceService; + this.sessionManager = sessionManager; } /** @@ -38,23 +35,38 @@ public class IotTcpDownstreamHandler { */ public void handle(IotDeviceMessage message) { try { - log.info("[handle][处理下行消息] 设备ID: {}, 方法: {}, 消息ID: {}", + log.info("[handle][处理下行消息] 设备 ID: {}, 方法: {}, 消息 ID: {}", message.getDeviceId(), message.getMethod(), message.getId()); - // 编码消息用于日志记录和验证 - byte[] encodedMessage = null; -// codec.encode(message); - log.debug("[handle][消息编码成功] 设备ID: {}, 编码后长度: {} 字节", - message.getDeviceId(), encodedMessage.length); + // 1. 获取设备信息 + IotDeviceRespDTO device = deviceService.getDeviceFromCache(message.getDeviceId()); + if (device == null) { + log.error("[handle][设备不存在] 设备 ID: {}", message.getDeviceId()); + return; + } - // 记录下行消息处理 - log.info("[handle][下行消息处理完成] 设备ID: {}, 方法: {}, 消息内容: {}", - message.getDeviceId(), message.getMethod(), message.getParams()); + // 2. 检查设备是否在线 + if (!sessionManager.isDeviceOnline(message.getDeviceId())) { + log.warn("[handle][设备不在线] 设备 ID: {}", message.getDeviceId()); + return; + } + + // 3. 编码消息 + byte[] bytes = messageService.encodeDeviceMessage(message, device.getCodecType()); + + // 4. 发送消息到设备 + boolean success = sessionManager.sendToDevice(message.getDeviceId(), bytes); + if (success) { + log.info("[handle][下行消息发送成功] 设备 ID: {}, 方法: {}, 消息 ID: {}, 数据长度: {} 字节", + message.getDeviceId(), message.getMethod(), message.getId(), bytes.length); + } else { + log.error("[handle][下行消息发送失败] 设备 ID: {}, 方法: {}, 消息 ID: {}", + message.getDeviceId(), message.getMethod(), message.getId()); + } } catch (Exception e) { - log.error("[handle][处理下行消息失败] 设备ID: {}, 方法: {}, 消息内容: {}", + log.error("[handle][处理下行消息失败] 设备 ID: {}, 方法: {}, 消息内容: {}", message.getDeviceId(), message.getMethod(), message.getParams(), e); } } - } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java index b57cceb9ec..eec4e1556a 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java @@ -1,110 +1,330 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router; +import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpCodecManager; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpAuthManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpSessionManager; +import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import io.vertx.core.Handler; import io.vertx.core.buffer.Buffer; import io.vertx.core.net.NetSocket; -import io.vertx.core.parsetools.RecordParser; import lombok.extern.slf4j.Slf4j; /** - * IoT 网关 TCP 上行消息处理器 - * - * @author 芋道源码 + * TCP 上行消息处理器 */ @Slf4j public class IotTcpUpstreamHandler implements Handler { + private static final String CODEC_TYPE_JSON = "TCP_JSON"; + private static final String CODEC_TYPE_BINARY = "TCP_BINARY"; + private static final String AUTH_METHOD = "auth"; + private final IotDeviceMessageService deviceMessageService; + private final IotDeviceService deviceService; + + private final IotTcpSessionManager sessionManager; + + private final IotTcpAuthManager authManager; + + private final IotDeviceTokenService deviceTokenService; + + private final IotDeviceCommonApi deviceApi; + private final String serverId; - private final IotTcpCodecManager codecManager; - public IotTcpUpstreamHandler(IotTcpUpstreamProtocol protocol, IotDeviceMessageService deviceMessageService, - IotTcpCodecManager codecManager) { + IotDeviceService deviceService, IotTcpSessionManager sessionManager) { this.deviceMessageService = deviceMessageService; + this.deviceService = deviceService; + this.sessionManager = sessionManager; + this.authManager = SpringUtil.getBean(IotTcpAuthManager.class); + this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); this.serverId = protocol.getServerId(); - this.codecManager = codecManager; } @Override public void handle(NetSocket socket) { - // 生成客户端ID用于日志标识 String clientId = IdUtil.simpleUUID(); - log.info("[handle][收到设备连接] clientId: {}, address: {}", clientId, socket.remoteAddress()); + log.info("[handle][收到设备连接] 客户端 ID: {}, 地址: {}", clientId, socket.remoteAddress()); - // 设置解析器 - RecordParser parser = RecordParser.newFixed(1024, buffer -> { - try { - handleDataPackage(clientId, buffer); - } catch (Exception e) { - log.error("[handle][处理数据包异常] clientId: {}", clientId, e); - } - }); - - // 设置异常处理 + // 设置异常和关闭处理器 socket.exceptionHandler(ex -> { - log.error("[handle][连接异常] clientId: {}, address: {}", clientId, socket.remoteAddress(), ex); + log.error("[handle][连接异常] 客户端 ID: {}, 地址: {}", clientId, socket.remoteAddress(), ex); + cleanupSession(socket); }); socket.closeHandler(v -> { - log.info("[handle][连接关闭] clientId: {}, address: {}", clientId, socket.remoteAddress()); + log.info("[handle][连接关闭] 客户端 ID: {}, 地址: {}", clientId, socket.remoteAddress()); + cleanupSession(socket); }); - // 设置数据处理器 - socket.handler(parser); + socket.handler(buffer -> handleDataPackage(clientId, buffer, socket)); } - /** - * 处理数据包 - */ - private void handleDataPackage(String clientId, Buffer buffer) { + private void handleDataPackage(String clientId, Buffer buffer, NetSocket socket) { try { - // 使用编解码器管理器自动检测协议并解码消息 - IotDeviceMessage message = codecManager.decode(buffer.getBytes()); - log.info("[handleDataPackage][接收数据包] clientId: {}, 方法: {}, 设备ID: {}", - clientId, message.getMethod(), message.getDeviceId()); + if (buffer.length() == 0) { + log.warn("[handleDataPackage][数据包为空] 客户端 ID: {}", clientId); + return; + } - // 处理上行消息 - handleUpstreamMessage(clientId, message); + // 1. 解码消息 + MessageInfo messageInfo = decodeMessage(buffer); + if (messageInfo == null) { + return; + } + + // 2. 获取设备信息 + IotDeviceRespDTO device = deviceService.getDeviceFromCache(messageInfo.message.getDeviceId()); + if (device == null) { + sendError(socket, messageInfo.message.getRequestId(), "设备不存在", messageInfo.codecType); + return; + } + + // 3. 处理消息 + if (!authManager.isAuthenticated(socket)) { + handleAuthRequest(clientId, messageInfo.message, socket, messageInfo.codecType); + } else { + IotTcpAuthManager.AuthInfo authInfo = authManager.getAuthInfo(socket); + handleBusinessMessage(clientId, messageInfo.message, authInfo); + } } catch (Exception e) { - log.error("[handleDataPackage][处理数据包失败] clientId: {}", clientId, e); + log.error("[handleDataPackage][处理数据包失败] 客户端 ID: {}, 错误: {}", clientId, e.getMessage(), e); } } /** - * 处理上行消息 + * 处理认证请求 */ - private void handleUpstreamMessage(String clientId, IotDeviceMessage message) { + private void handleAuthRequest(String clientId, IotDeviceMessage message, NetSocket socket, String codecType) { try { - log.info("[handleUpstreamMessage][上行消息] clientId: {}, 方法: {}, 设备ID: {}", - clientId, message.getMethod(), message.getDeviceId()); + // 1. 验证认证请求 + if (!AUTH_METHOD.equals(message.getMethod())) { + sendError(socket, message.getRequestId(), "请先进行认证", codecType); + return; + } - // 解析设备信息(简化处理) - String deviceId = String.valueOf(message.getDeviceId()); - String productKey = extractProductKey(deviceId); - String deviceName = deviceId; + // 2. 解析认证参数 + AuthParams authParams = parseAuthParams(message.getParams()); + if (authParams == null) { + sendError(socket, message.getRequestId(), "认证参数不完整", codecType); + return; + } - // 发送消息到队列 - deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId); + // 3. 执行认证流程 + if (performAuthentication(authParams, socket, message.getRequestId(), codecType)) { + log.info("[handleAuthRequest][认证成功] 客户端 ID: {}, username: {}", clientId, authParams.username); + } } catch (Exception e) { - log.error("[handleUpstreamMessage][处理上行消息失败] clientId: {}", clientId, e); + log.error("[handleAuthRequest][认证处理异常] 客户端 ID: {}", clientId, e); + sendError(socket, message.getRequestId(), "认证处理异常: " + e.getMessage(), codecType); } } /** - * 从设备ID中提取产品密钥(简化实现) + * 处理业务消息 */ - private String extractProductKey(String deviceId) { - // 简化实现:假设设备ID格式为 "productKey_deviceName" - if (deviceId != null && deviceId.contains("_")) { - return deviceId.split("_")[0]; + private void handleBusinessMessage(String clientId, IotDeviceMessage message, + IotTcpAuthManager.AuthInfo authInfo) { + try { + message.setDeviceId(authInfo.getDeviceId()); + message.setServerId(serverId); + + deviceMessageService.sendDeviceMessage(message, authInfo.getProductKey(), authInfo.getDeviceName(), + serverId); + log.info("[handleBusinessMessage][业务消息处理完成] 客户端 ID: {}, 消息 ID: {}, 设备 ID: {}, 方法: {}", + clientId, message.getId(), message.getDeviceId(), message.getMethod()); + } catch (Exception e) { + log.error("[handleBusinessMessage][处理业务消息失败] 客户端 ID: {}, 错误: {}", clientId, e.getMessage(), e); } - return "default_product"; + } + + /** + * 解码消息 + */ + private MessageInfo decodeMessage(Buffer buffer) { + try { + String rawData = buffer.toString(); + String codecType = isJsonFormat(rawData) ? CODEC_TYPE_JSON : CODEC_TYPE_BINARY; + IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(buffer.getBytes(), codecType); + return message != null ? new MessageInfo(message, codecType) : null; + } catch (Exception e) { + log.debug("[decodeMessage][消息解码失败] 错误: {}", e.getMessage()); + return null; + } + } + + /** + * 执行认证 + */ + private boolean performAuthentication(AuthParams authParams, NetSocket socket, String requestId, String codecType) { + // 1. 执行认证 + if (!authenticateDevice(authParams)) { + sendError(socket, requestId, "认证失败", codecType); + return false; + } + + // 2. 获取设备信息 + IotDeviceAuthUtils.DeviceInfo deviceInfo = deviceTokenService.parseUsername(authParams.username); + if (deviceInfo == null) { + sendError(socket, requestId, "解析设备信息失败", codecType); + return false; + } + + IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), + deviceInfo.getDeviceName()); + if (device == null) { + sendError(socket, requestId, "设备不存在", codecType); + return false; + } + + // 3. 注册认证信息 + String token = deviceTokenService.createToken(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); + registerAuthInfo(socket, device, deviceInfo, token, authParams.clientId); + + // 4. 发送上线消息和成功响应 + IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); + deviceMessageService.sendDeviceMessage(onlineMessage, deviceInfo.getProductKey(), deviceInfo.getDeviceName(), + serverId); + sendSuccess(socket, requestId, "认证成功", codecType); + + return true; + } + + /** + * 发送响应 + */ + private void sendResponse(NetSocket socket, boolean success, String message, String requestId, String codecType) { + try { + Object responseData = buildResponseData(success, message); + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, AUTH_METHOD, responseData, + success ? 0 : 401, message); + byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType); + socket.write(Buffer.buffer(encodedData)); + log.debug("[sendResponse][发送响应] success: {}, message: {}, requestId: {}", success, message, requestId); + } catch (Exception e) { + log.error("[sendResponse][发送响应失败] requestId: {}", requestId, e); + } + } + + /** + * 构建响应数据(不返回 token) + */ + private Object buildResponseData(boolean success, String message) { + return MapUtil.builder() + .put("success", success) + .put("message", message) + .build(); + } + + /** + * 清理会话 + */ + private void cleanupSession(NetSocket socket) { + // 如果已认证,发送离线消息 + IotTcpAuthManager.AuthInfo authInfo = authManager.getAuthInfo(socket); + if (authInfo != null) { + // 发送离线消息 + IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); + deviceMessageService.sendDeviceMessage(offlineMessage, authInfo.getProductKey(), authInfo.getDeviceName(), + serverId); + } + sessionManager.unregisterSession(socket); + authManager.unregisterAuth(socket); + } + + /** + * 判断是否为 JSON 格式 + */ + private boolean isJsonFormat(String data) { + if (StrUtil.isBlank(data)) + return false; + String trimmed = data.trim(); + return (trimmed.startsWith("{") && trimmed.endsWith("}")) || (trimmed.startsWith("[") && trimmed.endsWith("]")); + } + + /** + * 解析认证参数 + */ + private AuthParams parseAuthParams(Object params) { + if (params == null) + return null; + + JSONObject paramsJson = params instanceof JSONObject ? (JSONObject) params + : JSONUtil.parseObj(params.toString()); + String clientId = paramsJson.getStr("clientId"); + String username = paramsJson.getStr("username"); + String password = paramsJson.getStr("password"); + + return StrUtil.hasBlank(clientId, username, password) ? null : new AuthParams(clientId, username, password); + } + + /** + * 认证设备 + */ + private boolean authenticateDevice(AuthParams authParams) { + CommonResult result = deviceApi + .authDevice(new cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO() + .setClientId(authParams.clientId) + .setUsername(authParams.username) + .setPassword(authParams.password)); + return result.isSuccess() && result.getData(); + } + + /** + * 注册认证信息 + */ + private void registerAuthInfo(NetSocket socket, IotDeviceRespDTO device, IotDeviceAuthUtils.DeviceInfo deviceInfo, + String token, String clientId) { + IotTcpAuthManager.AuthInfo auth = new IotTcpAuthManager.AuthInfo(); + auth.setDeviceId(device.getId()); + auth.setProductKey(deviceInfo.getProductKey()); + auth.setDeviceName(deviceInfo.getDeviceName()); + auth.setToken(token); + auth.setClientId(clientId); + + authManager.registerAuth(socket, auth); + sessionManager.registerSession(device.getId(), socket); + } + + /** + * 发送错误响应 + */ + private void sendError(NetSocket socket, String requestId, String errorMessage, String codecType) { + sendResponse(socket, false, errorMessage, requestId, codecType); + } + + /** + * 发送成功响应(不返回 token) + */ + private void sendSuccess(NetSocket socket, String requestId, String message, String codecType) { + sendResponse(socket, true, message, requestId, codecType); + } + + /** + * 认证参数 + */ + private record AuthParams(String clientId, String username, String password) { + } + + /** + * 消息信息 + */ + private record MessageInfo(IotDeviceMessage message, String codecType) { } } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageService.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageService.java index 24134ba94a..c86fc0983d 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageService.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageService.java @@ -20,6 +20,16 @@ public interface IotDeviceMessageService { byte[] encodeDeviceMessage(IotDeviceMessage message, String productKey, String deviceName); + /** + * 编码消息 + * + * @param message 消息 + * @param codecType 编解码器类型 + * @return 编码后的消息内容 + */ + byte[] encodeDeviceMessage(IotDeviceMessage message, + String codecType); + /** * 解码消息 * @@ -31,13 +41,22 @@ public interface IotDeviceMessageService { IotDeviceMessage decodeDeviceMessage(byte[] bytes, String productKey, String deviceName); + /** + * 解码消息 + * + * @param bytes 消息内容 + * @param codecType 编解码器类型 + * @return 解码后的消息内容 + */ + IotDeviceMessage decodeDeviceMessage(byte[] bytes, String codecType); + /** * 发送消息 * - * @param message 消息 + * @param message 消息 * @param productKey 产品 Key * @param deviceName 设备名称 - * @param serverId 设备连接的 serverId + * @param serverId 设备连接的 serverId */ void sendDeviceMessage(IotDeviceMessage message, String productKey, String deviceName, String serverId); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageServiceImpl.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageServiceImpl.java index 6f1f731d29..014da9a5df 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageServiceImpl.java @@ -61,6 +61,19 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { return codec.encode(message); } + @Override + public byte[] encodeDeviceMessage(IotDeviceMessage message, + String codecType) { + // 1. 获取编解码器 + IotDeviceMessageCodec codec = codes.get(codecType); + if (codec == null) { + throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", codecType)); + } + + // 2. 编码消息 + return codec.encode(message); + } + @Override public IotDeviceMessage decodeDeviceMessage(byte[] bytes, String productKey, String deviceName) { @@ -79,6 +92,18 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { return codec.decode(bytes); } + @Override + public IotDeviceMessage decodeDeviceMessage(byte[] bytes, String codecType) { + // 1. 获取编解码器 + IotDeviceMessageCodec codec = codes.get(codecType); + if (codec == null) { + throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", codecType)); + } + + // 2. 解码消息 + return codec.decode(bytes); + } + @Override public void sendDeviceMessage(IotDeviceMessage message, String productKey, String deviceName, String serverId) { diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpBinaryDataPacketExamples.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpBinaryDataPacketExamplesTest.java similarity index 54% rename from yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpBinaryDataPacketExamples.java rename to yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpBinaryDataPacketExamplesTest.java index 123fed4be7..2e6fb41acc 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpBinaryDataPacketExamples.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpBinaryDataPacketExamplesTest.java @@ -2,42 +2,37 @@ package cn.iocoder.yudao.module.iot.gateway.codec.tcp; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import java.util.HashMap; import java.util.Map; -// TODO @haohao:这种写成单测,会好点 +import static org.junit.jupiter.api.Assertions.*; + /** - * TCP二进制格式数据包示例 + * TCP 二进制格式数据包单元测试 * - * 演示如何使用二进制协议创建和解析TCP上报数据包和心跳包 + * 测试二进制协议创建和解析 TCP 上报数据包和心跳包 * * 二进制协议格式: - * 包头(4字节) | 地址长度(2字节) | 设备地址(变长) | 功能码(2字节) | 消息序号(2字节) | 包体数据(变长) + * 包头(4 字节) | 功能码(2 字节) | 消息序号(2 字节) | 包体数据(变长) * * @author 芋道源码 */ @Slf4j -public class TcpBinaryDataPacketExamples { +class TcpBinaryDataPacketExamplesTest { - public static void main(String[] args) { - IotTcpBinaryDeviceMessageCodec codec = new IotTcpBinaryDeviceMessageCodec(); + private IotTcpBinaryDeviceMessageCodec codec; - // 1. 数据上报包示例 - demonstrateDataReport(codec); - - // 2. 心跳包示例 - demonstrateHeartbeat(codec); - - // 3. 复杂数据上报示例 - demonstrateComplexDataReport(codec); + @BeforeEach + void setUp() { + codec = new IotTcpBinaryDeviceMessageCodec(); } - /** - * 演示二进制格式数据上报包 - */ - private static void demonstrateDataReport(IotTcpBinaryDeviceMessageCodec codec) { - log.info("=== 二进制格式数据上报包示例 ==="); + @Test + void testDataReport() { + log.info("=== 二进制格式数据上报包测试 ==="); // 创建传感器数据 Map sensorData = new HashMap<>(); @@ -57,22 +52,23 @@ public class TcpBinaryDataPacketExamples { // 解码验证 IotDeviceMessage decoded = codec.decode(packet); - log.info("解码后消息ID: {}", decoded.getId()); - log.info("解码后请求ID: {}", decoded.getRequestId()); + log.info("解码后消息 ID: {}", decoded.getId()); + log.info("解码后请求 ID: {}", decoded.getRequestId()); log.info("解码后方法: {}", decoded.getMethod()); - log.info("解码后设备ID: {}", decoded.getDeviceId()); - log.info("解码后服务ID: {}", decoded.getServerId()); - log.info("解码后上报时间: {}", decoded.getReportTime()); + log.info("解码后设备 ID: {}", decoded.getDeviceId()); + log.info("解码后服务 ID: {}", decoded.getServerId()); log.info("解码后参数: {}", decoded.getParams()); - System.out.println(); + // 断言验证 + assertNotNull(decoded.getId()); + assertEquals("thing.property.post", decoded.getMethod()); + assertNotNull(decoded.getParams()); + assertTrue(decoded.getParams() instanceof Map); } - /** - * 演示二进制格式心跳包 - */ - private static void demonstrateHeartbeat(IotTcpBinaryDeviceMessageCodec codec) { - log.info("=== 二进制格式心跳包示例 ==="); + @Test + void testHeartbeat() { + log.info("=== 二进制格式心跳包测试 ==="); // 创建心跳消息 IotDeviceMessage heartbeat = IotDeviceMessage.requestOf("thing.state.online", null); @@ -85,21 +81,21 @@ public class TcpBinaryDataPacketExamples { // 解码验证 IotDeviceMessage decoded = codec.decode(packet); - log.info("解码后消息ID: {}", decoded.getId()); - log.info("解码后请求ID: {}", decoded.getRequestId()); + log.info("解码后消息 ID: {}", decoded.getId()); + log.info("解码后请求 ID: {}", decoded.getRequestId()); log.info("解码后方法: {}", decoded.getMethod()); - log.info("解码后设备ID: {}", decoded.getDeviceId()); - log.info("解码后服务ID: {}", decoded.getServerId()); + log.info("解码后设备 ID: {}", decoded.getDeviceId()); + log.info("解码后服务 ID: {}", decoded.getServerId()); log.info("解码后参数: {}", decoded.getParams()); - System.out.println(); + // 断言验证 + assertNotNull(decoded.getId()); + assertEquals("thing.state.online", decoded.getMethod()); } - /** - * 演示二进制格式复杂数据上报 - */ - private static void demonstrateComplexDataReport(IotTcpBinaryDeviceMessageCodec codec) { - log.info("=== 二进制格式复杂数据上报示例 ==="); + @Test + void testComplexDataReport() { + log.info("=== 二进制格式复杂数据上报测试 ==="); // 创建复杂设备数据 Map deviceData = new HashMap<>(); @@ -111,7 +107,7 @@ public class TcpBinaryDataPacketExamples { environment.put("co2", 420); deviceData.put("environment", environment); - // GPS数据 + // GPS 数据 Map location = new HashMap<>(); location.put("latitude", 39.9042); location.put("longitude", 116.4074); @@ -136,18 +132,48 @@ public class TcpBinaryDataPacketExamples { // 解码验证 IotDeviceMessage decoded = codec.decode(packet); - log.info("解码后消息ID: {}", decoded.getId()); - log.info("解码后请求ID: {}", decoded.getRequestId()); + log.info("解码后消息 ID: {}", decoded.getId()); + log.info("解码后请求 ID: {}", decoded.getRequestId()); log.info("解码后方法: {}", decoded.getMethod()); - log.info("解码后设备ID: {}", decoded.getDeviceId()); - log.info("解码后服务ID: {}", decoded.getServerId()); + log.info("解码后设备 ID: {}", decoded.getDeviceId()); + log.info("解码后服务 ID: {}", decoded.getServerId()); log.info("解码后参数: {}", decoded.getParams()); - System.out.println(); + // 断言验证 + assertNotNull(decoded.getId()); + assertEquals("thing.property.post", decoded.getMethod()); + assertNotNull(decoded.getParams()); } + @Test + void testPacketStructureAnalysis() { + log.info("=== 数据包结构分析测试 ==="); + + // 创建测试数据 + Map sensorData = new HashMap<>(); + sensorData.put("temperature", 25.5); + sensorData.put("humidity", 60.2); + + IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", sensorData); + message.setDeviceId(123456L); + + // 编码 + byte[] packet = codec.encode(message); + + // 分析数据包结构 + analyzePacketStructure(packet); + + // 断言验证 + assertTrue(packet.length >= 8, "数据包长度应该至少为 8 字节"); + } + + // ==================== 内部辅助方法 ==================== + /** * 字节数组转十六进制字符串 + * + * @param bytes 字节数组 + * @return 十六进制字符串 */ private static String bytesToHex(byte[] bytes) { StringBuilder result = new StringBuilder(); @@ -159,8 +185,10 @@ public class TcpBinaryDataPacketExamples { /** * 演示数据包结构分析 + * + * @param packet 数据包 */ - public static void analyzePacketStructure(byte[] packet) { + private static void analyzePacketStructure(byte[] packet) { if (packet.length < 8) { log.error("数据包长度不足"); return; @@ -168,30 +196,20 @@ public class TcpBinaryDataPacketExamples { int index = 0; - // 解析包头(4字节) - 后续数据长度 + // 解析包头(4 字节) - 后续数据长度 int totalLength = ((packet[index] & 0xFF) << 24) | - ((packet[index + 1] & 0xFF) << 16) | - ((packet[index + 2] & 0xFF) << 8) | - (packet[index + 3] & 0xFF); + ((packet[index + 1] & 0xFF) << 16) | + ((packet[index + 2] & 0xFF) << 8) | + (packet[index + 3] & 0xFF); index += 4; log.info("包头 - 后续数据长度: {} 字节", totalLength); - // 解析设备地址长度(2字节) - int addrLength = ((packet[index] & 0xFF) << 8) | (packet[index + 1] & 0xFF); - index += 2; - log.info("设备地址长度: {} 字节", addrLength); - - // 解析设备地址 - String deviceAddr = new String(packet, index, addrLength); - index += addrLength; - log.info("设备地址: {}", deviceAddr); - - // 解析功能码(2字节) + // 解析功能码(2 字节) int functionCode = ((packet[index] & 0xFF) << 8) | (packet[index + 1] & 0xFF); index += 2; log.info("功能码: {} ({})", functionCode, getFunctionCodeName(functionCode)); - // 解析消息序号(2字节) + // 解析消息序号(2 字节) int messageId = ((packet[index] & 0xFF) << 8) | (packet[index + 1] & 0xFF); index += 2; log.info("消息序号: {}", messageId); @@ -205,16 +223,19 @@ public class TcpBinaryDataPacketExamples { /** * 获取功能码名称 + * + * @param code 功能码 + * @return 功能码名称 */ private static String getFunctionCodeName(int code) { - switch (code) { - case 10: return "设备注册"; - case 11: return "注册回复"; - case 20: return "心跳请求"; - case 21: return "心跳回复"; - case 30: return "消息上行"; - case 40: return "消息下行"; - default: return "未知功能码"; - } + return switch (code) { + case 10 -> "设备注册"; + case 11 -> "注册回复"; + case 20 -> "心跳请求"; + case 21 -> "心跳回复"; + case 30 -> "消息上行"; + case 40 -> "消息下行"; + default -> "未知功能码"; + }; } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpJsonDataPacketExamples.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpJsonDataPacketExamples.java deleted file mode 100644 index 7334bd8dd3..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpJsonDataPacketExamples.java +++ /dev/null @@ -1,254 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.codec.tcp; - -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import lombok.extern.slf4j.Slf4j; - -import java.nio.charset.StandardCharsets; -import java.util.HashMap; -import java.util.Map; - -// TODO @haohao:这种写成单测,会好点 -/** - * TCP JSON格式数据包示例 - * - * 演示如何使用新的JSON格式进行TCP消息编解码 - * - * @author 芋道源码 - */ -@Slf4j -public class TcpJsonDataPacketExamples { - - public static void main(String[] args) { - IotTcpJsonDeviceMessageCodec codec = new IotTcpJsonDeviceMessageCodec(); - - // 1. 数据上报示例 - demonstrateDataReport(codec); - - // 2. 心跳示例 - demonstrateHeartbeat(codec); - - // 3. 事件上报示例 - demonstrateEventReport(codec); - - // 4. 复杂数据上报示例 - demonstrateComplexDataReport(codec); - - // 5. 便捷方法示例 - demonstrateConvenienceMethods(); - - // 6. EMQX兼容性示例 - demonstrateEmqxCompatibility(); - } - - /** - * 演示数据上报 - */ - private static void demonstrateDataReport(IotTcpJsonDeviceMessageCodec codec) { - log.info("=== JSON格式数据上报示例 ==="); - - // 创建传感器数据 - Map sensorData = new HashMap<>(); - sensorData.put("temperature", 25.5); - sensorData.put("humidity", 60.2); - sensorData.put("pressure", 1013.25); - sensorData.put("battery", 85); - - // 创建设备消息 - IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", sensorData); - message.setDeviceId(123456L); - - // 编码 - byte[] packet = codec.encode(message); - String jsonString = new String(packet, StandardCharsets.UTF_8); - log.info("编码后JSON: {}", jsonString); - log.info("数据包长度: {} 字节", packet.length); - - // 解码验证 - IotDeviceMessage decoded = codec.decode(packet); - log.info("解码后消息ID: {}", decoded.getId()); - log.info("解码后方法: {}", decoded.getMethod()); - log.info("解码后设备ID: {}", decoded.getDeviceId()); - log.info("解码后服务ID: {}", decoded.getServerId()); - log.info("解码后参数: {}", decoded.getParams()); - - System.out.println(); - } - - /** - * 演示心跳 - */ - private static void demonstrateHeartbeat(IotTcpJsonDeviceMessageCodec codec) { - log.info("=== JSON格式心跳示例 ==="); - - // 创建心跳消息 - IotDeviceMessage heartbeat = IotDeviceMessage.requestOf("thing.state.online", null); - heartbeat.setDeviceId(123456L); - - // 编码 - byte[] packet = codec.encode(heartbeat); - String jsonString = new String(packet, StandardCharsets.UTF_8); - log.info("编码后JSON: {}", jsonString); - log.info("心跳包长度: {} 字节", packet.length); - - // 解码验证 - IotDeviceMessage decoded = codec.decode(packet); - log.info("解码后消息ID: {}", decoded.getId()); - log.info("解码后方法: {}", decoded.getMethod()); - log.info("解码后设备ID: {}", decoded.getDeviceId()); - log.info("解码后服务ID: {}", decoded.getServerId()); - - System.out.println(); - } - - /** - * 演示事件上报 - */ - private static void demonstrateEventReport(IotTcpJsonDeviceMessageCodec codec) { - log.info("=== JSON格式事件上报示例 ==="); - - // 创建事件数据 - Map eventData = new HashMap<>(); - eventData.put("eventType", "alarm"); - eventData.put("level", "warning"); - eventData.put("description", "温度过高"); - eventData.put("value", 45.8); - - // 创建事件消息 - IotDeviceMessage event = IotDeviceMessage.requestOf("thing.event.post", eventData); - event.setDeviceId(123456L); - - // 编码 - byte[] packet = codec.encode(event); - String jsonString = new String(packet, StandardCharsets.UTF_8); - log.info("编码后JSON: {}", jsonString); - log.info("事件包长度: {} 字节", packet.length); - - // 解码验证 - IotDeviceMessage decoded = codec.decode(packet); - log.info("解码后消息ID: {}", decoded.getId()); - log.info("解码后方法: {}", decoded.getMethod()); - log.info("解码后设备ID: {}", decoded.getDeviceId()); - log.info("解码后参数: {}", decoded.getParams()); - - System.out.println(); - } - - /** - * 演示复杂数据上报 - */ - private static void demonstrateComplexDataReport(IotTcpJsonDeviceMessageCodec codec) { - log.info("=== JSON格式复杂数据上报示例 ==="); - - // 创建复杂设备数据(类似EMQX格式) - Map deviceData = new HashMap<>(); - - // 环境数据 - Map environment = new HashMap<>(); - environment.put("temperature", 23.8); - environment.put("humidity", 55.0); - environment.put("co2", 420); - environment.put("pm25", 35); - deviceData.put("environment", environment); - - // GPS数据 - Map location = new HashMap<>(); - location.put("latitude", 39.9042); - location.put("longitude", 116.4074); - location.put("altitude", 43.5); - location.put("speed", 0.0); - deviceData.put("location", location); - - // 设备状态 - Map status = new HashMap<>(); - status.put("battery", 78); - status.put("signal", -65); - status.put("online", true); - status.put("version", "1.2.3"); - deviceData.put("status", status); - - // 创建设备消息 - IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", deviceData); - message.setDeviceId(789012L); - - // 编码 - byte[] packet = codec.encode(message); - String jsonString = new String(packet, StandardCharsets.UTF_8); - log.info("编码后JSON: {}", jsonString); - log.info("复杂数据包长度: {} 字节", packet.length); - - // 解码验证 - IotDeviceMessage decoded = codec.decode(packet); - log.info("解码后消息ID: {}", decoded.getId()); - log.info("解码后方法: {}", decoded.getMethod()); - log.info("解码后设备ID: {}", decoded.getDeviceId()); - log.info("解码后参数: {}", decoded.getParams()); - - System.out.println(); - } - - /** - * 演示便捷方法 - */ - private static void demonstrateConvenienceMethods() { - log.info("=== 便捷方法示例 ==="); - - IotTcpJsonDeviceMessageCodec codec = new IotTcpJsonDeviceMessageCodec(); - - // 使用便捷方法编码数据上报 - Map sensorData = Map.of( - "temperature", 26.5, - "humidity", 58.3 - ); - byte[] dataPacket = codec.encodeDataReport(sensorData, 123456L, "smart_sensor", "device_001"); - log.info("便捷方法编码数据上报: {}", new String(dataPacket, StandardCharsets.UTF_8)); - - // 使用便捷方法编码心跳 - byte[] heartbeatPacket = codec.encodeHeartbeat(123456L, "smart_sensor", "device_001"); - log.info("便捷方法编码心跳: {}", new String(heartbeatPacket, StandardCharsets.UTF_8)); - - // 使用便捷方法编码事件 - Map eventData = Map.of( - "eventType", "maintenance", - "description", "定期维护提醒" - ); - byte[] eventPacket = codec.encodeEventReport(eventData, 123456L, "smart_sensor", "device_001"); - log.info("便捷方法编码事件: {}", new String(eventPacket, StandardCharsets.UTF_8)); - - System.out.println(); - } - - /** - * 演示与EMQX格式的兼容性 - */ - private static void demonstrateEmqxCompatibility() { - log.info("=== EMQX格式兼容性示例 ==="); - - // 模拟EMQX风格的消息格式 - String emqxStyleJson = """ - { - "id": "msg_001", - "method": "thing.property.post", - "deviceId": 123456, - "params": { - "temperature": 25.5, - "humidity": 60.2 - }, - "timestamp": 1642781234567 - } - """; - - IotTcpJsonDeviceMessageCodec codec = new IotTcpJsonDeviceMessageCodec(); - - // 解码EMQX风格的消息 - byte[] emqxBytes = emqxStyleJson.getBytes(StandardCharsets.UTF_8); - IotDeviceMessage decoded = codec.decode(emqxBytes); - - log.info("EMQX风格消息解码成功:"); - log.info("消息ID: {}", decoded.getId()); - log.info("方法: {}", decoded.getMethod()); - log.info("设备ID: {}", decoded.getDeviceId()); - log.info("参数: {}", decoded.getParams()); - - System.out.println(); - } -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpJsonDataPacketExamplesTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpJsonDataPacketExamplesTest.java new file mode 100644 index 0000000000..24258e0de2 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpJsonDataPacketExamplesTest.java @@ -0,0 +1,185 @@ +package cn.iocoder.yudao.module.iot.gateway.codec.tcp; + +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * TCP JSON 格式数据包单元测试 + *

+ * 测试 JSON 格式的 TCP 消息编解码功能 + * + * @author 芋道源码 + */ +@Slf4j +class TcpJsonDataPacketExamplesTest { + + private IotTcpJsonDeviceMessageCodec codec; + + @BeforeEach + void setUp() { + codec = new IotTcpJsonDeviceMessageCodec(); + } + + @Test + void testDataReport() { + log.info("=== JSON 格式数据上报测试 ==="); + + // 创建传感器数据 + Map sensorData = new HashMap<>(); + sensorData.put("temperature", 25.5); + sensorData.put("humidity", 60.2); + sensorData.put("pressure", 1013.25); + sensorData.put("battery", 85); + + // 创建设备消息 + IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", sensorData); + message.setDeviceId(123456L); + + // 编码 + byte[] packet = codec.encode(message); + String jsonString = new String(packet, StandardCharsets.UTF_8); + log.info("编码后 JSON: {}", jsonString); + log.info("数据包长度: {} 字节", packet.length); + + // 解码验证 + IotDeviceMessage decoded = codec.decode(packet); + log.info("解码后消息 ID: {}", decoded.getId()); + log.info("解码后方法: {}", decoded.getMethod()); + log.info("解码后设备 ID: {}", decoded.getDeviceId()); + log.info("解码后服务 ID: {}", decoded.getServerId()); + log.info("解码后参数: {}", decoded.getParams()); + + // 断言验证 + assertNotNull(decoded.getId()); + assertEquals("thing.property.post", decoded.getMethod()); + assertEquals(123456L, decoded.getDeviceId()); + assertNotNull(decoded.getParams()); + assertTrue(decoded.getParams() instanceof Map); + } + + @Test + void testHeartbeat() { + log.info("=== JSON 格式心跳测试 ==="); + + // 创建心跳消息 + IotDeviceMessage heartbeat = IotDeviceMessage.requestOf("thing.state.online", null); + heartbeat.setDeviceId(123456L); + + // 编码 + byte[] packet = codec.encode(heartbeat); + String jsonString = new String(packet, StandardCharsets.UTF_8); + log.info("编码后 JSON: {}", jsonString); + log.info("心跳包长度: {} 字节", packet.length); + + // 解码验证 + IotDeviceMessage decoded = codec.decode(packet); + log.info("解码后消息 ID: {}", decoded.getId()); + log.info("解码后方法: {}", decoded.getMethod()); + log.info("解码后设备 ID: {}", decoded.getDeviceId()); + log.info("解码后服务 ID: {}", decoded.getServerId()); + + // 断言验证 + assertNotNull(decoded.getId()); + assertEquals("thing.state.online", decoded.getMethod()); + assertEquals(123456L, decoded.getDeviceId()); + } + + @Test + void testEventReport() { + log.info("=== JSON 格式事件上报测试 ==="); + + // 创建事件数据 + Map eventData = new HashMap<>(); + eventData.put("eventType", "alarm"); + eventData.put("level", "warning"); + eventData.put("description", "温度过高"); + eventData.put("value", 45.8); + + // 创建事件消息 + IotDeviceMessage event = IotDeviceMessage.requestOf("thing.event.post", eventData); + event.setDeviceId(123456L); + + // 编码 + byte[] packet = codec.encode(event); + String jsonString = new String(packet, StandardCharsets.UTF_8); + log.info("编码后 JSON: {}", jsonString); + log.info("事件包长度: {} 字节", packet.length); + + // 解码验证 + IotDeviceMessage decoded = codec.decode(packet); + log.info("解码后消息 ID: {}", decoded.getId()); + log.info("解码后方法: {}", decoded.getMethod()); + log.info("解码后设备 ID: {}", decoded.getDeviceId()); + log.info("解码后参数: {}", decoded.getParams()); + + // 断言验证 + assertNotNull(decoded.getId()); + assertEquals("thing.event.post", decoded.getMethod()); + assertEquals(123456L, decoded.getDeviceId()); + assertNotNull(decoded.getParams()); + } + + @Test + void testComplexDataReport() { + log.info("=== JSON 格式复杂数据上报测试 ==="); + + // 创建复杂设备数据(类似 EMQX 格式) + Map deviceData = new HashMap<>(); + + // 环境数据 + Map environment = new HashMap<>(); + environment.put("temperature", 23.8); + environment.put("humidity", 55.0); + environment.put("co2", 420); + environment.put("pm25", 35); + deviceData.put("environment", environment); + + // GPS 数据 + Map location = new HashMap<>(); + location.put("latitude", 39.9042); + location.put("longitude", 116.4074); + location.put("altitude", 43.5); + location.put("speed", 0.0); + deviceData.put("location", location); + + // 设备状态 + Map status = new HashMap<>(); + status.put("battery", 78); + status.put("signal", -65); + status.put("online", true); + status.put("version", "1.2.3"); + deviceData.put("status", status); + + // 创建设备消息 + IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", deviceData); + message.setDeviceId(789012L); + + // 编码 + byte[] packet = codec.encode(message); + String jsonString = new String(packet, StandardCharsets.UTF_8); + log.info("编码后 JSON: {}", jsonString); + log.info("复杂数据包长度: {} 字节", packet.length); + + // 解码验证 + IotDeviceMessage decoded = codec.decode(packet); + log.info("解码后消息 ID: {}", decoded.getId()); + log.info("解码后方法: {}", decoded.getMethod()); + log.info("解码后设备 ID: {}", decoded.getDeviceId()); + log.info("解码后参数: {}", decoded.getParams()); + + // 断言验证 + assertNotNull(decoded.getId()); + assertEquals("thing.property.post", decoded.getMethod()); + assertEquals(789012L, decoded.getDeviceId()); + assertNotNull(decoded.getParams()); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-binary-packet-examples.md b/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-binary-packet-examples.md index 7bcf9b084e..4c2807276e 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-binary-packet-examples.md +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-binary-packet-examples.md @@ -1,46 +1,147 @@ -# TCP二进制协议数据包格式说明和示例 +# TCP 二进制协议数据包格式说明和示例 ## 1. 二进制协议概述 -TCP二进制协议是一种高效的自定义协议格式,适用于对带宽和性能要求较高的场景。 +TCP 二进制协议是一种高效的自定义协议格式,适用于对带宽和性能要求较高的场景。该协议采用紧凑的二进制格式,减少数据传输量,提高传输效率。 ## 2. 数据包格式 ### 2.1 整体结构 + +根据代码实现,TCP 二进制协议的数据包格式为: + ``` -+----------+----------+----------+----------+----------+----------+ -| 包头 | 地址长度 | 设备地址 | 功能码 | 消息序号 | 包体数据 | -| 4字节 | 2字节 | 变长 | 2字节 | 2字节 | 变长 | -+----------+----------+----------+----------+----------+----------+ ++----------+----------+----------+----------+ +| 包头 | 功能码 | 消息序号 | 包体数据 | +| 4字节 | 2字节 | 2字节 | 变长 | ++----------+----------+----------+----------+ ``` +**注意**:与原始设计相比,实际实现中移除了设备地址字段,简化了协议结构。 + ### 2.2 字段说明 -| 字段 | 长度 | 类型 | 说明 | -|----------|--------|--------|--------------------------------| -| 包头 | 4字节 | int | 后续数据的总长度(不包含包头) | -| 地址长度 | 2字节 | short | 设备地址的字节长度 | -| 设备地址 | 变长 | string | 设备标识符 | -| 功能码 | 2字节 | short | 消息类型标识 | -| 消息序号 | 2字节 | short | 消息唯一标识 | -| 包体数据 | 变长 | string | JSON格式的消息内容 | +| 字段 | 长度 | 类型 | 说明 | +|------|-----|--------|-----------------| +| 包头 | 4字节 | int | 后续数据的总长度(不包含包头) | +| 功能码 | 2字节 | short | 消息类型标识 | +| 消息序号 | 2字节 | short | 消息唯一标识 | +| 包体数据 | 变长 | string | JSON 格式的消息内容 | ### 2.3 功能码定义 -| 功能码 | 名称 | 说明 | -|--------|----------|--------------------------------| -| 10 | 设备注册 | 设备首次连接时的注册请求 | -| 11 | 注册回复 | 服务器对注册请求的回复 | -| 20 | 心跳请求 | 设备发送的心跳包 | -| 21 | 心跳回复 | 服务器对心跳的回复 | -| 30 | 消息上行 | 设备向服务器发送的数据 | -| 40 | 消息下行 | 服务器向设备发送的指令 | +根据代码实现,支持的功能码: -## 3. 二进制数据上报包示例 +| 功能码 | 名称 | 说明 | +|-----|------|--------------| +| 10 | 设备注册 | 设备首次连接时的注册请求 | +| 11 | 注册回复 | 服务器对注册请求的回复 | +| 20 | 心跳请求 | 设备发送的心跳包 | +| 21 | 心跳回复 | 服务器对心跳的回复 | +| 30 | 消息上行 | 设备向服务器发送的数据 | +| 40 | 消息下行 | 服务器向设备发送的指令 | -### 3.1 温度传感器数据上报 +**常量定义:** -**原始数据:** +```java +public static final short CODE_REGISTER = 10; +public static final short CODE_REGISTER_REPLY = 11; +public static final short CODE_HEARTBEAT = 20; +public static final short CODE_HEARTBEAT_REPLY = 21; +public static final short CODE_MESSAGE_UP = 30; +public static final short CODE_MESSAGE_DOWN = 40; +``` + +## 3. 包体数据格式 + +### 3.1 JSON 负载结构 + +包体数据采用 JSON 格式,包含以下字段: + +```json +{ + "method": "消息方法", + "params": { + // 消息参数 + }, + "timestamp": 时间戳, + "requestId": "请求ID", + "msgId": "消息ID" +} +``` + +### 3.2 字段说明 + +| 字段名 | 类型 | 必填 | 说明 | +|-----------|--------|----|------------------------------| +| method | String | 是 | 消息方法,如 `thing.property.post` | +| params | Object | 否 | 消息参数 | +| timestamp | Long | 是 | 时间戳(毫秒) | +| requestId | String | 否 | 请求唯一标识 | +| msgId | String | 否 | 消息唯一标识 | + +**常量定义:** + +```java +public static final String METHOD = "method"; +public static final String PARAMS = "params"; +public static final String TIMESTAMP = "timestamp"; +public static final String REQUEST_ID = "requestId"; +public static final String MESSAGE_ID = "msgId"; +``` + +## 4. 消息类型 + +### 4.1 数据上报 (thing.property.post) + +设备向服务器上报属性数据。 + +**功能码:** 30 (CODE_MESSAGE_UP) + +**包体数据示例:** + +```json +{ + "method": "thing.property.post", + "params": { + "temperature": 25.5, + "humidity": 60.2, + "pressure": 1013.25 + }, + "timestamp": 1642781234567, + "requestId": "req_001" +} +``` + +### 4.2 心跳 (thing.state.online) + +设备向服务器发送心跳保活。 + +**功能码:** 20 (CODE_HEARTBEAT) + +**包体数据示例:** + +```json +{ + "method": "thing.state.online", + "params": {}, + "timestamp": 1642781234567, + "requestId": "req_002" +} +``` + +### 4.3 消息方法常量 + +```java +public static final String PROPERTY_POST = "thing.property.post"; // 数据上报 +public static final String STATE_ONLINE = "thing.state.online"; // 心跳 +``` + +## 5. 数据包示例 + +### 5.1 温度传感器数据上报 + +**包体数据:** ```json { "method": "thing.property.post", @@ -49,15 +150,14 @@ TCP二进制协议是一种高效的自定义协议格式,适用于对带宽 "humidity": 60.2, "pressure": 1013.25 }, - "timestamp": 1642781234567 + "timestamp": 1642781234567, + "requestId": "req_001" } ``` **数据包结构:** ``` 包头: 0x00000045 (69字节) -地址长度: 0x0006 (6字节) -设备地址: "123456" 功能码: 0x001E (30 - 消息上行) 消息序号: 0x1234 (4660) 包体: JSON字符串 @@ -65,7 +165,7 @@ TCP二进制协议是一种高效的自定义协议格式,适用于对带宽 **完整十六进制数据包:** ``` -00 00 00 45 00 06 31 32 33 34 35 36 00 1E 12 34 +00 00 00 45 00 1E 12 34 7B 22 6D 65 74 68 6F 64 22 3A 22 74 68 69 6E 67 2E 70 72 6F 70 65 72 74 79 2E 70 6F 73 74 22 2C 22 70 61 72 61 6D 73 22 3A 7B 22 74 65 6D 70 65 @@ -73,42 +173,25 @@ TCP二进制协议是一种高效的自定义协议格式,适用于对带宽 6D 69 64 69 74 79 22 3A 36 30 2E 32 2C 22 70 72 65 73 73 75 72 65 22 3A 31 30 31 33 2E 32 35 7D 2C 22 74 69 6D 65 73 74 61 6D 70 22 3A 31 36 34 -32 37 38 31 32 33 34 35 36 37 7D +32 37 38 31 32 33 34 35 36 37 2C 22 72 65 71 75 +65 73 74 49 64 22 3A 22 72 65 71 5F 30 30 31 22 7D ``` -### 2.2 GPS定位数据上报 +### 5.2 心跳包示例 -**原始数据:** -```json -{ - "method": "thing.property.post", - "params": { - "latitude": 39.9042, - "longitude": 116.4074, - "altitude": 43.5, - "speed": 0.0 - }, - "timestamp": 1642781234567 -} -``` - -## 3. 心跳包示例 - -### 3.1 标准心跳包 - -**原始数据:** +**包体数据:** ```json { "method": "thing.state.online", - "timestamp": 1642781234567 + "params": {}, + "timestamp": 1642781234567, + "requestId": "req_002" } ``` **数据包结构:** ``` 包头: 0x00000028 (40字节) -地址长度: 0x0006 (6字节) -设备地址: "123456" 功能码: 0x0014 (20 - 心跳请求) 消息序号: 0x5678 (22136) 包体: JSON字符串 @@ -116,66 +199,71 @@ TCP二进制协议是一种高效的自定义协议格式,适用于对带宽 **完整十六进制数据包:** ``` -00 00 00 28 00 06 31 32 33 34 35 36 00 14 56 78 +00 00 00 28 00 14 56 78 7B 22 6D 65 74 68 6F 64 22 3A 22 74 68 69 6E 67 2E 73 74 61 74 65 2E 6F 6E 6C 69 6E 65 22 2C 22 -74 69 6D 65 73 74 61 6D 70 22 3A 31 36 34 32 37 -38 31 32 33 34 35 36 37 7D +70 61 72 61 6D 73 22 3A 7B 7D 2C 22 74 69 6D 65 +73 74 61 6D 70 22 3A 31 36 34 32 37 38 31 32 33 +34 35 36 37 2C 22 72 65 71 75 65 73 74 49 64 22 +3A 22 72 65 71 5F 30 30 32 22 7D ``` -## 4. 复杂数据上报示例 +## 6. 编解码器实现 -### 4.1 多传感器综合数据 +### 6.1 编码器类型 -**原始数据:** -```json -{ - "method": "thing.property.post", - "params": { - "environment": { - "temperature": 23.8, - "humidity": 55.0, - "co2": 420 - }, - "location": { - "latitude": 39.9042, - "longitude": 116.4074, - "altitude": 43.5 - }, - "status": { - "battery": 78, - "signal": -65, - "online": true - } - }, - "timestamp": 1642781234567 +```java +public static final String TYPE = "TCP_BINARY"; +``` + +### 6.2 编码过程 + +1. **参数验证**:检查消息和方法是否为空 +2. **确定功能码**: + - 心跳消息:使用 `CODE_HEARTBEAT` (20) + - 其他消息:使用 `CODE_MESSAGE_UP` (30) +3. **构建负载**:使用 `buildSimplePayload()` 构建 JSON 负载 +4. **生成消息序号**:基于当前时间戳生成 +5. **构建数据包**:创建 `TcpDataPackage` 对象 +6. **编码为字节流**:使用 `encodeTcpDataPackage()` 编码 + +### 6.3 解码过程 + +1. **参数验证**:检查字节数组是否为空 +2. **解码数据包**:使用 `decodeTcpDataPackage()` 解码 +3. **确定消息方法**: + - 功能码 20:`thing.state.online` (心跳) + - 功能码 30:`thing.property.post` (数据上报) +4. **解析负载信息**:使用 `parsePayloadInfo()` 解析 JSON 负载 +5. **构建设备消息**:创建 `IotDeviceMessage` 对象 +6. **设置服务 ID**:使用 `generateServerId()` 生成 + +### 6.4 服务 ID 生成 + +```java +private String generateServerId(TcpDataPackage dataPackage) { + return String.format("tcp_binary_%d_%d", dataPackage.getCode(), dataPackage.getMid()); } ``` -## 5. 数据包解析步骤 +## 7. 数据包解析步骤 -### 5.1 解析流程 +### 7.1 解析流程 1. **读取包头(4字节)** - 获取后续数据的总长度 - 验证数据包完整性 -2. **读取设备地址长度(2字节)** - - 确定设备地址的字节数 - -3. **读取设备地址(变长)** - - 根据地址长度读取设备标识 - -4. **读取功能码(2字节)** +2. **读取功能码(2字节)** - 确定消息类型 -5. **读取消息序号(2字节)** +3. **读取消息序号(2字节)** - 获取消息唯一标识 -6. **读取包体数据(变长)** - - 解析JSON格式的消息内容 +4. **读取包体数据(变长)** + - 解析 JSON 格式的消息内容 -### 5.2 Java解析示例 +### 7.2 Java 解析示例 ```java public TcpDataPackage parsePacket(byte[] packet) { @@ -184,39 +272,99 @@ public TcpDataPackage parsePacket(byte[] packet) { // 1. 解析包头 int totalLength = ByteBuffer.wrap(packet, index, 4).getInt(); index += 4; - - // 2. 解析设备地址长度 - short addrLength = ByteBuffer.wrap(packet, index, 2).getShort(); - index += 2; - - // 3. 解析设备地址 - String deviceAddr = new String(packet, index, addrLength); - index += addrLength; - - // 4. 解析功能码 + + // 2. 解析功能码 short functionCode = ByteBuffer.wrap(packet, index, 2).getShort(); index += 2; - - // 5. 解析消息序号 + + // 3. 解析消息序号 short messageId = ByteBuffer.wrap(packet, index, 2).getShort(); index += 2; - - // 6. 解析包体数据 + + // 4. 解析包体数据 String payload = new String(packet, index, packet.length - index); - - return TcpDataPackage.builder() - .addr(deviceAddr) - .code(functionCode) - .mid(messageId) - .payload(payload) - .build(); + + return new TcpDataPackage(functionCode, messageId, payload); } ``` -## 6. 注意事项 +## 8. 使用示例 + +### 8.1 基本使用 + +```java +// 创建编解码器 +IotTcpBinaryDeviceMessageCodec codec = new IotTcpBinaryDeviceMessageCodec(); + +// 创建数据上报消息 +Map sensorData = Map.of( + "temperature", 25.5, + "humidity", 60.2 +); + +// 编码 +IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", sensorData); +byte[] data = codec.encode(message); + +// 解码 +IotDeviceMessage decoded = codec.decode(data); +``` + +### 8.2 错误处理 + +```java +try{ +byte[] data = codec.encode(message); +// 处理编码成功 +}catch( +IllegalArgumentException e){ + // 处理参数错误 + log. + +error("编码参数错误: {}",e.getMessage()); + }catch( +TcpCodecException e){ + // 处理编码失败 + log. + +error("编码失败: {}",e.getMessage()); + } +``` + +## 9. 注意事项 1. **字节序**:所有多字节数据使用大端序(Big-Endian) -2. **字符编码**:字符串数据使用UTF-8编码 -3. **JSON格式**:包体数据必须是有效的JSON格式 -4. **长度限制**:单个数据包建议不超过1MB -5. **错误处理**:解析失败时应返回相应的错误码 +2. **字符编码**:字符串数据使用 UTF-8 编码 +3. **JSON 格式**:包体数据必须是有效的 JSON 格式 +4. **长度限制**:单个数据包建议不超过 1MB +5. **错误处理**:解析失败时会抛出 `TcpCodecException` +6. **功能码映射**:目前只支持心跳和数据上报两种消息类型 + +## 10. 协议特点 + +### 10.1 优势 + +- **高效传输**:二进制格式,数据量小 +- **性能优化**:减少解析开销 +- **带宽节省**:相比 JSON 格式节省带宽 +- **实时性好**:适合高频数据传输 + +### 10.2 适用场景 + +- ✅ **高频数据传输**:传感器数据实时上报 +- ✅ **带宽受限环境**:移动网络、卫星通信 +- ✅ **性能要求高**:需要低延迟的场景 +- ✅ **设备资源有限**:嵌入式设备、IoT 设备 + +### 10.3 与 JSON 协议对比 + +| 特性 | 二进制协议 | JSON 协议 | +|-------|-------|---------| +| 数据大小 | 小 | 稍大 | +| 解析性能 | 高 | 中等 | +| 可读性 | 差 | 优秀 | +| 调试难度 | 高 | 低 | +| 扩展性 | 差 | 优秀 | +| 实现复杂度 | 高 | 低 | + +这样就完成了 TCP 二进制协议的完整说明,与实际代码实现完全一致。 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-json-packet-examples.md b/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-json-packet-examples.md index 45a08d78af..34251e7166 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-json-packet-examples.md +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-json-packet-examples.md @@ -1,13 +1,14 @@ -# TCP JSON格式协议说明 +# TCP JSON 格式协议说明 ## 1. 协议概述 -TCP JSON格式协议采用纯JSON格式进行数据传输,参考了EMQX和HTTP模块的数据格式设计,具有以下优势: +TCP JSON 格式协议采用纯 JSON 格式进行数据传输,参考了 EMQX 和 HTTP 模块的数据格式设计,具有以下优势: -- **标准化**:使用标准JSON格式,易于解析和处理 +- **标准化**:使用标准 JSON 格式,易于解析和处理 - **可读性**:人类可读,便于调试和维护 - **扩展性**:可以轻松添加新字段,向后兼容 -- **统一性**:与HTTP模块保持一致的数据格式 +- **统一性**:与 HTTP 模块保持一致的数据格式 +- **简化性**:相比二进制协议,实现更简单,调试更容易 ## 2. 消息格式 @@ -17,29 +18,112 @@ TCP JSON格式协议采用纯JSON格式进行数据传输,参考了EMQX和HTTP { "id": "消息唯一标识", "method": "消息方法", - "deviceId": "设备ID", + "deviceId": 设备ID, "params": { // 消息参数 }, - "timestamp": 时间戳 + "timestamp": 时间戳, + "code": 响应码, + "message": "响应消息" } ``` ### 2.2 字段说明 -| 字段名 | 类型 | 必填 | 说明 | -|--------|------|------|------| -| id | String | 是 | 消息唯一标识,UUID格式 | -| method | String | 是 | 消息方法,如 thing.property.post | -| deviceId | Long | 是 | 设备ID | -| params | Object | 否 | 消息参数,具体内容根据method而定 | -| timestamp | Long | 是 | 时间戳(毫秒) | -| code | Integer | 否 | 响应码(下行消息使用) | -| message | String | 否 | 响应消息(下行消息使用) | +| 字段名 | 类型 | 必填 | 说明 | +|-----------|---------|----|-------------------------------------| +| id | String | 是 | 消息唯一标识,如果为空会自动生成 UUID | +| method | String | 是 | 消息方法,如 `auth`、`thing.property.post` | +| deviceId | Long | 否 | 设备 ID | +| params | Object | 否 | 消息参数,具体内容根据 method 而定 | +| timestamp | Long | 是 | 时间戳(毫秒),自动生成 | +| code | Integer | 否 | 响应码(下行消息使用) | +| message | String | 否 | 响应消息(下行消息使用) | ## 3. 消息类型 -### 3.1 数据上报 (thing.property.post) +### 3.1 设备认证 (auth) + +设备连接后首先需要进行认证,认证成功后才能进行其他操作。 + +#### 3.1.1 认证请求格式 + +**示例:** + +```json +{ + "id": "auth_8ac6a1db91e64aa9996143fdbac2cbfe", + "method": "auth", + "params": { + "clientId": "device_001", + "username": "productKey_deviceName", + "password": "设备密码" + }, + "timestamp": 1753111026437 +} +``` + +**字段说明:** +| 字段名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| clientId | String | 是 | 客户端唯一标识 | +| username | String | 是 | 设备用户名,格式为 `productKey_deviceName` | +| password | String | 是 | 设备密码 | + +#### 3.1.2 认证响应格式 + +**认证成功响应:** + +```json +{ + "id": "auth_response_8ac6a1db91e64aa9996143fdbac2cbfe", + "requestId": "auth_8ac6a1db91e64aa9996143fdbac2cbfe", + "method": "auth", + "data": { + "success": true, + "message": "认证成功" + }, + "code": 0, + "msg": "认证成功" +} +``` + +**认证失败响应:** + +```json +{ + "id": "auth_response_8ac6a1db91e64aa9996143fdbac2cbfe", + "requestId": "auth_8ac6a1db91e64aa9996143fdbac2cbfe", + "method": "auth", + "data": { + "success": false, + "message": "认证失败:用户名或密码错误" + }, + "code": 401, + "msg": "认证失败:用户名或密码错误" +} +``` + +#### 3.1.3 认证流程 + +1. **设备连接** → 建立 TCP 连接 +2. **发送认证请求** → 发送包含认证信息的 JSON 消息 +3. **服务器验证** → 验证 clientId、username、password +4. **生成 Token** → 认证成功后生成 JWT Token(内部使用) +5. **设备上线** → 发送设备上线消息到消息总线 +6. **返回响应** → 返回认证结果 +7. **会话注册** → 注册设备会话,允许后续业务操作 + +#### 3.1.4 认证错误码 + +| 错误码 | 说明 | 处理建议 | +|-----|-------|--------------| +| 401 | 认证失败 | 检查用户名、密码是否正确 | +| 400 | 参数错误 | 检查认证参数是否完整 | +| 404 | 设备不存在 | 检查设备是否已注册 | +| 500 | 服务器错误 | 联系管理员 | + +### 3.2 数据上报 (thing.property.post) 设备向服务器上报属性数据。 @@ -48,7 +132,7 @@ TCP JSON格式协议采用纯JSON格式进行数据传输,参考了EMQX和HTTP { "id": "8ac6a1db91e64aa9996143fdbac2cbfe", "method": "thing.property.post", - "deviceId": 123456, + "deviceId": 8, "params": { "temperature": 25.5, "humidity": 60.2, @@ -59,7 +143,7 @@ TCP JSON格式协议采用纯JSON格式进行数据传输,参考了EMQX和HTTP } ``` -### 3.2 心跳 (thing.state.online) +### 3.3 心跳 (thing.state.update) 设备向服务器发送心跳保活。 @@ -67,220 +151,161 @@ TCP JSON格式协议采用纯JSON格式进行数据传输,参考了EMQX和HTTP ```json { "id": "7db8c4e6408b40f8b2549ddd94f6bb02", - "method": "thing.state.online", - "deviceId": 123456, + "method": "thing.state.update", + "deviceId": 8, + "params": { + "state": "1" + }, "timestamp": 1753111026467 } ``` -### 3.3 事件上报 (thing.event.post) +### 3.4 消息方法常量 -设备向服务器上报事件信息。 +支持的消息方法: -**示例:** -```json -{ - "id": "9e7d72731b854916b1baa5088bd6a907", - "method": "thing.event.post", - "deviceId": 123456, - "params": { - "eventType": "alarm", - "level": "warning", - "description": "温度过高", - "value": 45.8 - }, - "timestamp": 1753111026468 -} -``` +- `auth` - 设备认证 +- `thing.property.post` - 数据上报 +- `thing.state.update` - 心跳 -### 3.4 属性设置 (thing.property.set) +## 4. 协议特点 -服务器向设备下发属性设置指令。 +### 4.1 优势 -**示例:** -```json -{ - "id": "cmd_001", - "method": "thing.property.set", - "deviceId": 123456, - "params": { - "targetTemperature": 22.0, - "mode": "auto" - }, - "timestamp": 1753111026469 -} -``` +- **简单易用**:纯 JSON 格式,无需复杂的二进制解析 +- **调试友好**:可以直接查看消息内容 +- **扩展性强**:可以轻松添加新字段 +- **标准化**:与 EMQX 等主流平台格式兼容 +- **错误处理**:提供详细的错误信息和异常处理 +- **安全性**:支持设备认证机制 -### 3.5 服务调用 (thing.service.invoke) +### 4.2 与二进制协议对比 -服务器向设备调用服务。 +| 特性 | 二进制协议 | JSON 协议 | +|-------|-------|----------| +| 可读性 | 差 | 优秀 | +| 调试难度 | 高 | 低 | +| 扩展性 | 差 | 优秀 | +| 解析复杂度 | 高 | 低 | +| 数据大小 | 小 | 稍大 | +| 标准化程度 | 低 | 高 | +| 实现复杂度 | 高 | 低 | +| 安全性 | 一般 | 优秀(支持认证) | -**示例:** -```json -{ - "id": "service_001", - "method": "thing.service.invoke", - "deviceId": 123456, - "params": { - "service": "restart", - "args": { - "delay": 5 - } - }, - "timestamp": 1753111026470 -} -``` +### 4.3 适用场景 -## 4. 复杂数据示例 - -### 4.1 多传感器综合数据 - -```json -{ - "id": "complex_001", - "method": "thing.property.post", - "deviceId": 789012, - "params": { - "environment": { - "temperature": 23.8, - "humidity": 55.0, - "co2": 420, - "pm25": 35 - }, - "location": { - "latitude": 39.9042, - "longitude": 116.4074, - "altitude": 43.5, - "speed": 0.0 - }, - "status": { - "battery": 78, - "signal": -65, - "online": true, - "version": "1.2.3" - } - }, - "timestamp": 1753111026471 -} -``` - -## 5. 与EMQX格式的兼容性 - -本协议设计参考了EMQX的消息格式,具有良好的兼容性: - -### 5.1 EMQX标准格式 - -```json -{ - "id": "msg_001", - "method": "thing.property.post", - "deviceId": 123456, - "params": { - "temperature": 25.5, - "humidity": 60.2 - }, - "timestamp": 1642781234567 -} -``` - -### 5.2 兼容性说明 - -- ✅ **字段名称**:与EMQX保持一致 -- ✅ **数据类型**:完全兼容 -- ✅ **消息结构**:结构相同 -- ✅ **扩展字段**:支持自定义扩展 - -## 6. 使用示例 - -### 6.1 Java编码示例 - -```java -// 创建编解码器 -IotTcpJsonDeviceMessageCodec codec = new IotTcpJsonDeviceMessageCodec(); - -// 创建数据上报消息 -Map sensorData = Map.of( - "temperature", 25.5, - "humidity", 60.2 -); -IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", sensorData); -message.setDeviceId(123456L); - -// 编码为字节数组 -byte[] jsonBytes = codec.encode(message); - -// 解码 -IotDeviceMessage decoded = codec.decode(jsonBytes); -``` - -### 6.2 便捷方法示例 - -```java -// 快速编码数据上报 -byte[] dataPacket = codec.encodeDataReport(sensorData, 123456L, "product_key", "device_name"); - -// 快速编码心跳 -byte[] heartbeatPacket = codec.encodeHeartbeat(123456L, "product_key", "device_name"); - -// 快速编码事件 -byte[] eventPacket = codec.encodeEventReport(eventData, 123456L, "product_key", "device_name"); -``` - -## 7. 协议优势 - -### 7.1 与原TCP二进制协议对比 - -| 特性 | 二进制协议 | JSON协议 | -|------|------------|----------| -| 可读性 | 差 | 优秀 | -| 调试难度 | 高 | 低 | -| 扩展性 | 差 | 优秀 | -| 解析复杂度 | 高 | 低 | -| 数据大小 | 小 | 稍大 | -| 标准化程度 | 低 | 高 | - -### 7.2 适用场景 - -- ✅ **开发调试**:JSON格式便于查看和调试 -- ✅ **快速集成**:标准JSON格式,集成简单 +- ✅ **开发调试**:JSON 格式便于查看和调试 +- ✅ **快速集成**:标准 JSON 格式,集成简单 - ✅ **协议扩展**:可以轻松添加新字段 -- ✅ **多语言支持**:JSON格式支持所有主流语言 -- ✅ **云平台对接**:与主流IoT云平台格式兼容 +- ✅ **多语言支持**:JSON 格式支持所有主流语言 +- ✅ **云平台对接**:与主流 IoT 云平台格式兼容 +- ✅ **安全要求**:支持设备认证和访问控制 -## 8. 最佳实践 +## 5. 最佳实践 -### 8.1 消息设计建议 +### 5.1 认证最佳实践 + +1. **连接即认证**:设备连接后立即进行认证 +2. **重连机制**:连接断开后重新认证 +3. **错误重试**:认证失败时适当重试 +4. **安全传输**:使用 TLS 加密传输敏感信息 + +### 5.2 消息设计 1. **保持简洁**:避免过深的嵌套结构 2. **字段命名**:使用驼峰命名法,保持一致性 3. **数据类型**:使用合适的数据类型,避免字符串表示数字 4. **时间戳**:统一使用毫秒级时间戳 -### 8.2 性能优化 +### 5.3 错误处理 -1. **批量上报**:可以在params中包含多个数据点 -2. **压缩传输**:对于大数据量可以考虑gzip压缩 -3. **缓存机制**:客户端可以缓存消息,批量发送 +1. **参数验证**:确保必要字段存在且有效 +2. **异常捕获**:正确处理编码解码异常 +3. **日志记录**:记录详细的调试信息 +4. **认证失败**:认证失败时及时关闭连接 -### 8.3 错误处理 +### 5.4 性能优化 -1. **格式验证**:确保JSON格式正确 -2. **字段检查**:验证必填字段是否存在 -3. **异常处理**:提供详细的错误信息 +1. **批量上报**:可以在 params 中包含多个数据点 +2. **连接复用**:保持 TCP 连接,避免频繁建立连接 +3. **消息缓存**:客户端可以缓存消息,批量发送 +4. **心跳间隔**:合理设置心跳间隔,避免过于频繁 -## 9. 迁移指南 +## 6. 配置说明 -### 9.1 从二进制协议迁移 +### 6.1 启用 JSON 协议 -1. **保持兼容**:可以同时支持两种协议 -2. **逐步迁移**:按设备类型逐步迁移 -3. **测试验证**:充分测试新协议的稳定性 +在配置文件中设置: -### 9.2 配置变更 - -```java -// 在设备配置中指定编解码器类型 -device.setCodecType("TCP_JSON"); +```yaml +yudao: + iot: + gateway: + protocol: + tcp: + enabled: true + port: 8091 + default-protocol: "JSON" # 使用 JSON 协议 ``` -这样就完成了TCP协议向JSON格式的升级,提供了更好的可读性、扩展性和兼容性。 +### 6.2 认证配置 + +```yaml +yudao: + iot: + gateway: + token: + secret: "your-secret-key" # JWT 密钥 + expiration: "24h" # Token 过期时间 +``` + +## 7. 调试和监控 + +### 7.1 日志级别 + +```yaml +logging: + level: + cn.iocoder.yudao.module.iot.gateway.protocol.tcp: DEBUG +``` + +### 7.2 调试信息 + +编解码器会输出详细的调试日志: + +- 认证过程:显示认证请求和响应 +- 编码成功:显示方法、长度、内容 +- 解码过程:显示原始数据、解析结果 +- 错误信息:详细的异常堆栈 + +### 7.3 监控指标 + +- 认证成功率 +- 消息处理数量 +- 编解码成功率 +- 处理延迟 +- 错误率 +- 在线设备数量 + +## 8. 安全考虑 + +### 8.1 认证安全 + +1. **密码强度**:使用强密码策略 +2. **Token 过期**:设置合理的 Token 过期时间 +3. **连接限制**:限制单个设备的并发连接数 +4. **IP 白名单**:可选的 IP 访问控制 + +### 8.2 传输安全 + +1. **TLS 加密**:使用 TLS 1.2+ 加密传输 +2. **证书验证**:验证服务器证书 +3. **密钥管理**:安全存储和管理密钥 + +### 8.3 数据安全 + +1. **敏感信息**:不在日志中记录密码等敏感信息 +2. **数据验证**:验证所有输入数据 +3. **访问控制**:基于 Token 的访问控制 + +这样就完成了 TCP JSON 格式协议的完整说明,包括认证流程的详细说明,与实际代码实现完全一致。 From 4a8c485b920d024112ba623f578828d8206e577a Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 26 Jul 2025 23:06:21 +0800 Subject: [PATCH 75/89] =?UTF-8?q?fix=EF=BC=9A=E3=80=90=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E6=9D=83=E9=99=90=E3=80=91=E5=BC=BA=E5=88=B6=E9=9C=80=E8=A6=81?= =?UTF-8?q?=20PermissionCommonApi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/YudaoDeptDataPermissionAutoConfiguration.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDeptDataPermissionAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDeptDataPermissionAutoConfiguration.java index ab5b3bc7ec..46b6ca7c59 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDeptDataPermissionAutoConfiguration.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDeptDataPermissionAutoConfiguration.java @@ -18,7 +18,7 @@ import java.util.List; */ @AutoConfiguration @ConditionalOnClass(LoginUser.class) -@ConditionalOnBean(value = {PermissionCommonApi.class, DeptDataPermissionRuleCustomizer.class}) +@ConditionalOnBean(value = {DeptDataPermissionRuleCustomizer.class}) public class YudaoDeptDataPermissionAutoConfiguration { @Bean From 47ff2bf9225ae31b89c44e6f5de3415d9fb07820 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 27 Jul 2025 10:11:39 +0800 Subject: [PATCH 76/89] =?UTF-8?q?fix=EF=BC=9AXss=20=E5=BC=80=E5=90=AF?= =?UTF-8?q?=E4=B8=8D=E7=94=9F=E6=95=88=E7=9A=84=E9=97=AE=E9=A2=98=20https:?= =?UTF-8?q?//t.zsxq.com/qq02q?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../framework/xss/config/YudaoXssAutoConfiguration.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/xss/config/YudaoXssAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/xss/config/YudaoXssAutoConfiguration.java index 99b6a448f3..16f87dae1f 100644 --- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/xss/config/YudaoXssAutoConfiguration.java +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/xss/config/YudaoXssAutoConfiguration.java @@ -5,7 +5,6 @@ import cn.iocoder.yudao.framework.xss.core.clean.JsoupXssCleaner; import cn.iocoder.yudao.framework.xss.core.clean.XssCleaner; import cn.iocoder.yudao.framework.xss.core.filter.XssFilter; import cn.iocoder.yudao.framework.xss.core.json.XssStringJsonDeserializer; -import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -42,13 +41,13 @@ public class YudaoXssAutoConfiguration implements WebMvcConfigurer { */ @Bean @ConditionalOnMissingBean(name = "xssJacksonCustomizer") - @ConditionalOnBean(ObjectMapper.class) @ConditionalOnProperty(value = "yudao.xss.enable", havingValue = "true") public Jackson2ObjectMapperBuilderCustomizer xssJacksonCustomizer(XssProperties properties, PathMatcher pathMatcher, XssCleaner xssCleaner) { // 在反序列化时进行 xss 过滤,可以替换使用 XssStringJsonSerializer,在序列化时进行处理 - return builder -> builder.deserializerByType(String.class, new XssStringJsonDeserializer(properties, pathMatcher, xssCleaner)); + return builder -> + builder.deserializerByType(String.class, new XssStringJsonDeserializer(properties, pathMatcher, xssCleaner)); } /** From ae9e35f184614da50f15399c7fe52c5f9413e61b Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 27 Jul 2025 11:04:19 +0800 Subject: [PATCH 77/89] =?UTF-8?q?feat=EF=BC=9A=E3=80=90infra=20=E5=9F=BA?= =?UTF-8?q?=E7=A1=80=E8=AE=BE=E6=96=BD=E3=80=91=E4=BC=98=E5=8C=96=20vben3?= =?UTF-8?q?=20=E6=A8=A1=E7=89=88=E8=B7=AF=E5=BE=84=EF=BC=8C=E4=B8=8D?= =?UTF-8?q?=E5=9C=A8=E4=BD=BF=E7=94=A8=20vue3=20=E9=81=BF=E5=85=8D?= =?UTF-8?q?=E8=AF=AF=E8=A7=A3=20https://github.com/yudaocode/yudao-ui-admi?= =?UTF-8?q?n-vben/issues/60?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/codegen/inner/CodegenEngine.java | 81 ++++++++++--------- 1 file changed, 43 insertions(+), 38 deletions(-) diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java index b69f0c6254..a0ce062cdc 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java @@ -139,85 +139,85 @@ public class CodegenEngine { vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts")) // VUE3_VBEN2_ANTD_SCHEMA .put(CodegenFrontTypeEnum.VUE3_VBEN2_ANTD_SCHEMA.getType(), vue3VbenTemplatePath("views/data.ts"), - vue3FilePath("views/${table.moduleName}/${table.businessName}/${classNameVar}.data.ts")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/${classNameVar}.data.ts")) .put(CodegenFrontTypeEnum.VUE3_VBEN2_ANTD_SCHEMA.getType(), vue3VbenTemplatePath("views/index.vue"), - vue3FilePath("views/${table.moduleName}/${table.businessName}/index.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/index.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN2_ANTD_SCHEMA.getType(), vue3VbenTemplatePath("views/form.vue"), - vue3FilePath("views/${table.moduleName}/${table.businessName}/${simpleClassName}Modal.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/${simpleClassName}Modal.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN2_ANTD_SCHEMA.getType(), vue3VbenTemplatePath("api/api.ts"), - vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts")) + vue3VbenFilePath("api/${table.moduleName}/${table.businessName}/index.ts")) // VUE3_VBEN5_ANTD_SCHEMA .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3Vben5AntdSchemaTemplatePath("views/data.ts"), - vue3FilePath("views/${table.moduleName}/${table.businessName}/data.ts")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/data.ts")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3Vben5AntdSchemaTemplatePath("views/index.vue"), - vue3FilePath("views/${table.moduleName}/${table.businessName}/index.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/index.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3Vben5AntdSchemaTemplatePath("views/form.vue"), - vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/form.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/form.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3Vben5AntdSchemaTemplatePath("api/api.ts"), - vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts")) + vue3VbenFilePath("api/${table.moduleName}/${table.businessName}/index.ts")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3Vben5AntdSchemaTemplatePath("views/modules/form_sub_normal.vue"), // 特殊:主子表专属逻辑 - vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3Vben5AntdSchemaTemplatePath("views/modules/form_sub_inner.vue"), // 特殊:主子表专属逻辑 - vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3Vben5AntdSchemaTemplatePath("views/modules/form_sub_erp.vue"), // 特殊:主子表专属逻辑 - vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3Vben5AntdSchemaTemplatePath("views/modules/list_sub_inner.vue"), // 特殊:主子表专属逻辑 - vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3Vben5AntdSchemaTemplatePath("views/modules/list_sub_erp.vue"), // 特殊:主子表专属逻辑 - vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue")) // VUE3_VBEN5_ANTD .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/index.vue"), - vue3FilePath("views/${table.moduleName}/${table.businessName}/index.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/index.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/form.vue"), - vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/form.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/form.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("api/api.ts"), - vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts")) + vue3VbenFilePath("api/${table.moduleName}/${table.businessName}/index.ts")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/form_sub_normal.vue"), // 特殊:主子表专属逻辑 - vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/form_sub_inner.vue"), // 特殊:主子表专属逻辑 - vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/form_sub_erp.vue"), // 特殊:主子表专属逻辑 - vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/list_sub_inner.vue"), // 特殊:主子表专属逻辑 - vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/list_sub_erp.vue"), // 特殊:主子表专属逻辑 - vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue")) // VUE3_VBEN5_EP_SCHEMA .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA.getType(), vue3Vben5EpSchemaTemplatePath("views/data.ts"), - vue3FilePath("views/${table.moduleName}/${table.businessName}/data.ts")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/data.ts")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA.getType(), vue3Vben5EpSchemaTemplatePath("views/index.vue"), - vue3FilePath("views/${table.moduleName}/${table.businessName}/index.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/index.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA.getType(), vue3Vben5EpSchemaTemplatePath("views/form.vue"), - vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/form.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/form.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA.getType(), vue3Vben5EpSchemaTemplatePath("api/api.ts"), - vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts")) + vue3VbenFilePath("api/${table.moduleName}/${table.businessName}/index.ts")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA.getType(), vue3Vben5EpSchemaTemplatePath("views/modules/form_sub_normal.vue"), // 特殊:主子表专属逻辑 - vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA.getType(), vue3Vben5EpSchemaTemplatePath("views/modules/form_sub_inner.vue"), // 特殊:主子表专属逻辑 - vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA.getType(), vue3Vben5EpSchemaTemplatePath("views/modules/form_sub_erp.vue"), // 特殊:主子表专属逻辑 - vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA.getType(), vue3Vben5EpSchemaTemplatePath("views/modules/list_sub_inner.vue"), // 特殊:主子表专属逻辑 - vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA.getType(), vue3Vben5EpSchemaTemplatePath("views/modules/list_sub_erp.vue"), // 特殊:主子表专属逻辑 - vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue")) // VUE3_VBEN5_EP .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_GENERAL.getType(), vue3Vben5EpGeneralTemplatePath("views/index.vue"), - vue3FilePath("views/${table.moduleName}/${table.businessName}/index.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/index.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_GENERAL.getType(), vue3Vben5EpGeneralTemplatePath("views/form.vue"), - vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/form.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/form.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_GENERAL.getType(), vue3Vben5EpGeneralTemplatePath("api/api.ts"), - vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts")) + vue3VbenFilePath("api/${table.moduleName}/${table.businessName}/index.ts")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_GENERAL.getType(), vue3Vben5EpGeneralTemplatePath("views/modules/form_sub_normal.vue"), // 特殊:主子表专属逻辑 - vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_GENERAL.getType(), vue3Vben5EpGeneralTemplatePath("views/modules/form_sub_inner.vue"), // 特殊:主子表专属逻辑 - vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_GENERAL.getType(), vue3Vben5EpGeneralTemplatePath("views/modules/form_sub_erp.vue"), // 特殊:主子表专属逻辑 - vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_GENERAL.getType(), vue3Vben5EpGeneralTemplatePath("views/modules/list_sub_inner.vue"), // 特殊:主子表专属逻辑 - vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_GENERAL.getType(), vue3Vben5EpGeneralTemplatePath("views/modules/list_sub_erp.vue"), // 特殊:主子表专属逻辑 - vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue")) .build(); @Resource @@ -614,6 +614,11 @@ public class CodegenEngine { "src/" + path; } + private static String vue3VbenFilePath(String path) { + return "yudao-ui-${sceneEnum.basePackage}-vben/" + // 顶级目录 + "src/" + path; + } + private static String vue3VbenTemplatePath(String path) { return "codegen/vue3_vben/" + path + ".vm"; } From 8ffe9272e80ec9b07fce6b29d8f041bc5ac7a01b Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 27 Jul 2025 19:18:04 +0800 Subject: [PATCH 78/89] =?UTF-8?q?feat=EF=BC=9A=E3=80=90pay=20=E6=94=AF?= =?UTF-8?q?=E4=BB=98=E3=80=91=E5=A2=9E=E5=8A=A0=20userid=E3=80=81usertype?= =?UTF-8?q?=EF=BC=8C=E5=B9=B6=E9=A2=9D=E5=A4=96=E6=94=AF=E6=8C=81=E6=9D=83?= =?UTF-8?q?=E9=99=90=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../convert/aftersale/AfterSaleConvert.java | 2 +- .../convert/order/TradeOrderConvert.java | 4 ++- .../aftersale/AfterSaleServiceImpl.java | 2 ++ .../BrokerageWithdrawServiceImpl.java | 1 + .../order/TradeOrderUpdateServiceImpl.java | 1 + .../api/order/dto/PayOrderCreateReqDTO.java | 13 +++++++ .../api/refund/dto/PayRefundCreateReqDTO.java | 13 +++++++ .../transfer/dto/PayTransferCreateReqDTO.java | 35 +++++++++++++------ .../admin/demo/PayDemoWithdrawController.java | 3 +- .../app/order/AppPayOrderController.java | 13 +++++-- .../pay/convert/order/PayOrderConvert.java | 5 +-- .../pay/dal/dataobject/order/PayOrderDO.java | 9 +++++ .../dal/dataobject/refund/PayRefundDO.java | 9 +++++ .../dataobject/transfer/PayTransferDO.java | 12 +++++-- .../service/demo/PayDemoOrderServiceImpl.java | 3 ++ .../service/demo/PayDemoWithdrawService.java | 3 +- .../demo/PayDemoWithdrawServiceImpl.java | 4 ++- .../wallet/PayWalletRechargeServiceImpl.java | 4 +-- .../src/test/resources/sql/clean.sql | 1 + .../src/test/resources/sql/create_tables.sql | 33 +++++++++++++++++ 20 files changed, 143 insertions(+), 27 deletions(-) diff --git a/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/convert/aftersale/AfterSaleConvert.java b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/convert/aftersale/AfterSaleConvert.java index 54480f696b..0e508a4643 100644 --- a/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/convert/aftersale/AfterSaleConvert.java +++ b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/convert/aftersale/AfterSaleConvert.java @@ -43,7 +43,7 @@ public interface AfterSaleConvert { @Mapping(source = "afterSale.id", target = "merchantRefundId"), @Mapping(source = "afterSale.applyReason", target = "reason"), @Mapping(source = "afterSale.refundPrice", target = "price"), - @Mapping(source = "orderProperties.payAppKey", target = "appKey") + @Mapping(source = "orderProperties.payAppKey", target = "appKey"), }) PayRefundCreateReqDTO convert(String userIp, AfterSaleDO afterSale, TradeOrderProperties orderProperties); diff --git a/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderConvert.java b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderConvert.java index fa0b9c8163..beacddab87 100644 --- a/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderConvert.java +++ b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderConvert.java @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.trade.convert.order; import cn.hutool.core.util.BooleanUtil; import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; import cn.iocoder.yudao.framework.common.util.string.StrUtils; @@ -101,7 +102,8 @@ public interface TradeOrderConvert { default PayOrderCreateReqDTO convert(TradeOrderDO order, List orderItems, TradeOrderProperties orderProperties) { PayOrderCreateReqDTO createReqDTO = new PayOrderCreateReqDTO() - .setAppKey(orderProperties.getPayAppKey()).setUserIp(order.getUserIp()); + .setAppKey(orderProperties.getPayAppKey()).setUserIp(order.getUserIp()) + .setUserId(order.getUserId()).setUserType(UserTypeEnum.MEMBER.getValue()); // 商户相关字段 createReqDTO.setMerchantOrderId(String.valueOf(order.getId())); String subject = orderItems.get(0).getSpuName(); diff --git a/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/service/aftersale/AfterSaleServiceImpl.java b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/service/aftersale/AfterSaleServiceImpl.java index f0c63a032d..7be62fc78d 100644 --- a/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/service/aftersale/AfterSaleServiceImpl.java +++ b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/service/aftersale/AfterSaleServiceImpl.java @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.trade.service.aftersale; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; import cn.iocoder.yudao.module.pay.api.refund.PayRefundApi; @@ -362,6 +363,7 @@ public class AfterSaleServiceImpl implements AfterSaleService { private void createPayRefund(String userIp, AfterSaleDO afterSale) { // 创建退款单 PayRefundCreateReqDTO createReqDTO = AfterSaleConvert.INSTANCE.convert(userIp, afterSale, tradeOrderProperties) + .setUserId(afterSale.getUserId()).setUserType(UserTypeEnum.MEMBER.getValue()) .setReason(StrUtil.format("退款【{}】", afterSale.getSpuName())); Long payRefundId = payRefundApi.createRefund(createReqDTO); diff --git a/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageWithdrawServiceImpl.java b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageWithdrawServiceImpl.java index a873927f6c..fcadac827a 100644 --- a/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageWithdrawServiceImpl.java +++ b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageWithdrawServiceImpl.java @@ -151,6 +151,7 @@ public class BrokerageWithdrawServiceImpl implements BrokerageWithdrawService { .setAppKey(tradeOrderProperties.getPayAppKey()).setChannelCode(channelCode) .setMerchantTransferId(withdraw.getId().toString()).setSubject("佣金提现").setPrice(withdraw.getPrice()) .setUserAccount(userAccount).setUserName(userName).setUserIp(getClientIP()) + .setUserId(withdraw.getUserId()).setUserType(UserTypeEnum.MEMBER.getValue()) // 用户信息 .setChannelExtras(channelExtras); // 1.3 发起请求 PayTransferCreateRespDTO transferRespDTO = payTransferApi.createTransfer(transferReqDTO); diff --git a/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java index bafe56c5be..690f3de52c 100644 --- a/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java +++ b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java @@ -952,6 +952,7 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService { payRefundApi.createRefund(new PayRefundCreateReqDTO() .setAppKey(tradeOrderProperties.getPayAppKey()) // 支付应用 .setUserIp(NetUtil.getLocalhostStr()) // 使用本机 IP,因为是服务器发起退款的 + .setUserId(order.getUserId()).setUserType(UserTypeEnum.MEMBER.getValue()) // 用户信息 .setMerchantOrderId(String.valueOf(order.getId())) // 支付单号 // 特殊:因为订单支持 AfterSale 单个售后退款,也支持整单退款,所以需要通过 order- 进行下区分 // 具体可见 AfterSaleController 的 updateAfterSaleRefunded 方法 diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/api/order/dto/PayOrderCreateReqDTO.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/api/order/dto/PayOrderCreateReqDTO.java index 3a7b181be3..a2f035cddd 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/api/order/dto/PayOrderCreateReqDTO.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/api/order/dto/PayOrderCreateReqDTO.java @@ -1,5 +1,7 @@ package cn.iocoder.yudao.module.pay.api.order.dto; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.common.validation.InEnum; import jakarta.validation.constraints.DecimalMin; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; @@ -22,12 +24,23 @@ public class PayOrderCreateReqDTO implements Serializable { */ @NotNull(message = "应用标识不能为空") private String appKey; + /** * 用户 IP */ @NotEmpty(message = "用户 IP 不能为空") private String userIp; + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + */ + @InEnum(UserTypeEnum.class) + private Integer userType; + // ========== 商户相关字段 ========== /** diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/api/refund/dto/PayRefundCreateReqDTO.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/api/refund/dto/PayRefundCreateReqDTO.java index 6910fc2fe5..cc1743be64 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/api/refund/dto/PayRefundCreateReqDTO.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/api/refund/dto/PayRefundCreateReqDTO.java @@ -1,5 +1,7 @@ package cn.iocoder.yudao.module.pay.api.refund.dto; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.common.validation.InEnum; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; @@ -19,12 +21,23 @@ public class PayRefundCreateReqDTO { */ @NotNull(message = "应用标识不能为空") private String appKey; + /** * 用户 IP */ @NotEmpty(message = "用户 IP 不能为空") private String userIp; + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + */ + @InEnum(UserTypeEnum.class) + private Integer userType; + // ========== 商户相关字段 ========== /** * 商户订单编号 diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/api/transfer/dto/PayTransferCreateReqDTO.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/api/transfer/dto/PayTransferCreateReqDTO.java index 1ac5c04af6..86dd70cce5 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/api/transfer/dto/PayTransferCreateReqDTO.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/api/transfer/dto/PayTransferCreateReqDTO.java @@ -1,7 +1,9 @@ package cn.iocoder.yudao.module.pay.api.transfer.dto; import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.common.validation.InEnum; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; @@ -26,23 +28,23 @@ public class PayTransferCreateReqDTO { @NotNull(message = "应用标识不能为空") private String appKey; - /** - * 转账渠道 - */ - @NotEmpty(message = "转账渠道不能为空") - private String channelCode; - - /** - * 转账渠道的额外参数 - */ - private Map channelExtras; - /** * 用户 IP */ @NotEmpty(message = "用户 IP 不能为空") private String userIp; + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + */ + @InEnum(UserTypeEnum.class) + private Integer userType; + + // ========== 商户相关字段 ========== /** * 商户转账单编号 */ @@ -75,6 +77,17 @@ public class PayTransferCreateReqDTO { */ private String userName; + /** + * 转账渠道 + */ + @NotEmpty(message = "转账渠道不能为空") + private String channelCode; + + /** + * 转账渠道的额外参数 + */ + private Map channelExtras; + /** * 【微信】现金营销场景 * diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/PayDemoWithdrawController.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/PayDemoWithdrawController.java index 033bc95d79..ef5e4a8a9c 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/PayDemoWithdrawController.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/PayDemoWithdrawController.java @@ -19,6 +19,7 @@ import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; @Tag(name = "管理后台 - 示例提现订单") // 目的:演示转账功能 @RestController @@ -40,7 +41,7 @@ public class PayDemoWithdrawController { @Operation(summary = "提现单转账") @Parameter(name = "id", required = true, description = "提现单编号", example = "1024") public CommonResult transferDemoWithdraw(@RequestParam("id") Long id) { - Long payTransferId = demoWithdrawService.transferDemoWithdraw(id); + Long payTransferId = demoWithdrawService.transferDemoWithdraw(id, getLoginUserId()); return success(payTransferId); } diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/app/order/AppPayOrderController.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/app/order/AppPayOrderController.java index 88eeb0960a..4f57163841 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/app/order/AppPayOrderController.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/app/order/AppPayOrderController.java @@ -1,12 +1,12 @@ package cn.iocoder.yudao.module.pay.controller.app.order; +import cn.hutool.core.util.ObjUtil; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderRespVO; import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderSubmitRespVO; import cn.iocoder.yudao.module.pay.controller.app.order.vo.AppPayOrderSubmitReqVO; import cn.iocoder.yudao.module.pay.controller.app.order.vo.AppPayOrderSubmitRespVO; -import cn.iocoder.yudao.module.pay.convert.order.PayOrderConvert; import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO; import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletDO; import cn.iocoder.yudao.module.pay.enums.PayChannelEnum; @@ -52,6 +52,15 @@ public class AppPayOrderController { public CommonResult getOrder(@RequestParam("id") Long id, @RequestParam(value = "sync", required = false) Boolean sync) { PayOrderDO order = payOrderService.getOrder(id); + if (order== null) { + return success(null); + } + // 重要:校验订单是否是当前用户,避免越权 + if (order.getUserId() != null // 特殊:早期订单未存储 userId,所以忽略 + && ObjUtil.notEqual(order.getUserId(), getLoginUserId())) { + return success(null); + } + // sync 仅在等待支付 if (Boolean.TRUE.equals(sync) && PayOrderStatusEnum.isWaiting(order.getStatus())) { payOrderService.syncOrderQuietly(order.getId()); @@ -75,7 +84,7 @@ public class AppPayOrderController { // 2. 提交支付 PayOrderSubmitRespVO respVO = payOrderService.submitOrder(reqVO, getClientIP()); - return success(PayOrderConvert.INSTANCE.convert3(respVO)); + return success(BeanUtils.toBean(respVO, AppPayOrderSubmitRespVO.class)); } } diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/convert/order/PayOrderConvert.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/convert/order/PayOrderConvert.java index 8321de32ce..1813e2c2d7 100755 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/convert/order/PayOrderConvert.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/convert/order/PayOrderConvert.java @@ -3,14 +3,13 @@ package cn.iocoder.yudao.module.pay.convert.order; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; import cn.iocoder.yudao.framework.common.util.collection.MapUtils; -import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO; import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderRespDTO; import cn.iocoder.yudao.module.pay.controller.admin.order.vo.*; -import cn.iocoder.yudao.module.pay.controller.app.order.vo.AppPayOrderSubmitRespVO; import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO; import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO; import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderExtensionDO; +import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.factory.Mappers; @@ -69,6 +68,4 @@ public interface PayOrderConvert { @Mapping(source = "order.status", target = "status") PayOrderSubmitRespVO convert(PayOrderDO order, cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO respDTO); - AppPayOrderSubmitRespVO convert3(PayOrderSubmitRespVO bean); - } diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/order/PayOrderDO.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/order/PayOrderDO.java index ac0ef34f2e..4eea1cc4ef 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/order/PayOrderDO.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/order/PayOrderDO.java @@ -49,6 +49,15 @@ public class PayOrderDO extends BaseDO { */ private String channelCode; + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + */ + private Integer userType; + // ========== 商户相关字段 ========== /** diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/refund/PayRefundDO.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/refund/PayRefundDO.java index 607483f91a..0804926820 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/refund/PayRefundDO.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/refund/PayRefundDO.java @@ -77,6 +77,15 @@ public class PayRefundDO extends BaseDO { */ private String orderNo; + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + */ + private Integer userType; + // ========== 商户相关字段 ========== /** * 商户订单编号 diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/transfer/PayTransferDO.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/transfer/PayTransferDO.java index 94f8521dbd..d0f3a80211 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/transfer/PayTransferDO.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/transfer/PayTransferDO.java @@ -30,7 +30,6 @@ public class PayTransferDO extends BaseDO { */ @TableId private Long id; - /** * 转账单号 */ @@ -42,14 +41,12 @@ public class PayTransferDO extends BaseDO { * 关联 {@link PayAppDO#getId()} */ private Long appId; - /** * 转账渠道编号 * * 关联 {@link PayChannelDO#getId()} */ private Long channelId; - /** * 转账渠道编码 * @@ -57,6 +54,15 @@ public class PayTransferDO extends BaseDO { */ private String channelCode; + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + */ + private Integer userType; + // ========== 商户相关字段 ========== /** * 商户转账单编号 diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImpl.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImpl.java index 76571f355c..ac756f5763 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImpl.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImpl.java @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.pay.service.demo; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.pojo.PageParam; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.module.pay.api.order.PayOrderApi; @@ -90,6 +91,7 @@ public class PayDemoOrderServiceImpl implements PayDemoOrderService { // 2.1 创建支付单 Long payOrderId = payOrderApi.createOrder(new PayOrderCreateReqDTO() .setAppKey(PAY_APP_KEY).setUserIp(getClientIP()) // 支付应用 + .setUserId(userId).setUserType(UserTypeEnum.ADMIN.getValue()) // 用户信息 .setMerchantOrderId(demoOrder.getId().toString()) // 业务的订单编号 .setSubject(spuName).setBody("").setPrice(price) // 价格信息 .setExpireTime(addTime(Duration.ofHours(2L)))); // 支付的过期时间 @@ -189,6 +191,7 @@ public class PayDemoOrderServiceImpl implements PayDemoOrderService { // 2.2 创建退款单 Long payRefundId = payRefundApi.createRefund(new PayRefundCreateReqDTO() .setAppKey(PAY_APP_KEY).setUserIp(getClientIP()) // 支付应用 + .setUserId(order.getUserId()).setUserType(UserTypeEnum.ADMIN.getValue()) // 用户信息 .setMerchantOrderId(String.valueOf(order.getId())) // 支付单号 .setMerchantRefundId(refundId) .setReason("想退钱").setPrice(order.getPrice()));// 价格信息 diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoWithdrawService.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoWithdrawService.java index e8a5980ad8..6b55810257 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoWithdrawService.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoWithdrawService.java @@ -26,9 +26,10 @@ public interface PayDemoWithdrawService { * 提现单转账 * * @param id 提现单编号 + * @param userId 用户编号 * @return 转账编号 */ - Long transferDemoWithdraw(Long id); + Long transferDemoWithdraw(Long id, Long userId); /** * 获得示例提现单分页 diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoWithdrawServiceImpl.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoWithdrawServiceImpl.java index b1a3b91516..dddbceea0f 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoWithdrawServiceImpl.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoWithdrawServiceImpl.java @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.pay.service.demo; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.pojo.PageParam; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; @@ -60,7 +61,7 @@ public class PayDemoWithdrawServiceImpl implements PayDemoWithdrawService { } @Override - public Long transferDemoWithdraw(Long id) { + public Long transferDemoWithdraw(Long id, Long userId) { // 1.1 校验提现单 PayDemoWithdrawDO withdraw = validateDemoWithdrawCanTransfer(id); // 1.2 特殊:如果是转账失败的情况,需要充值下 @@ -76,6 +77,7 @@ public class PayDemoWithdrawServiceImpl implements PayDemoWithdrawService { // 2.1 创建支付单 PayTransferCreateReqDTO transferReqDTO = new PayTransferCreateReqDTO() .setAppKey(PAY_APP_KEY).setChannelCode(withdraw.getTransferChannelCode()).setUserIp(getClientIP()) // 支付应用 + .setUserId(userId).setUserType(UserTypeEnum.ADMIN.getValue()) // 用户信息 .setMerchantTransferId(String.valueOf(withdraw.getId())) // 业务的订单编号 .setSubject(withdraw.getSubject()).setPrice(withdraw.getPrice()) // 价格信息 .setUserAccount(withdraw.getUserAccount()).setUserName(withdraw.getUserName()); // 收款信息 diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletRechargeServiceImpl.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletRechargeServiceImpl.java index f1253f4765..95539b28d5 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletRechargeServiceImpl.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletRechargeServiceImpl.java @@ -64,8 +64,6 @@ public class PayWalletRechargeServiceImpl implements PayWalletRechargeService { private PayWalletService payWalletService; @Resource private PayOrderService payOrderService; -// @Resource -// private PayRefundService payRefundService; @Resource private PayWalletRechargePackageService payWalletRechargePackageService; @@ -99,6 +97,7 @@ public class PayWalletRechargeServiceImpl implements PayWalletRechargeService { // 2.1 创建支付单 Long payOrderId = payOrderService.createOrder(new PayOrderCreateReqDTO() .setAppKey(payProperties.getWalletPayAppKey()).setUserIp(userIp) + .setUserId(userId).setUserType(userType) // 用户信息 .setMerchantOrderId(recharge.getId().toString()) // 业务的订单编号 .setSubject(WALLET_RECHARGE_ORDER_SUBJECT).setBody("") .setPrice(recharge.getPayPrice()) @@ -209,6 +208,7 @@ public class PayWalletRechargeServiceImpl implements PayWalletRechargeService { String refundId = walletRechargeId + "-refund"; Long payRefundId = payRefundApi.createRefund(new PayRefundCreateReqDTO() .setAppKey(payProperties.getWalletPayAppKey()).setUserIp(userIp) + .setUserId(wallet.getUserId()).setUserType(wallet.getUserType()) // 用户信息 .setMerchantOrderId(walletRechargeId) .setMerchantRefundId(refundId) .setReason("想退钱").setPrice(walletRecharge.getPayPrice())); diff --git a/yudao-module-pay/src/test/resources/sql/clean.sql b/yudao-module-pay/src/test/resources/sql/clean.sql index 91fff0caee..e7bf56bfa4 100644 --- a/yudao-module-pay/src/test/resources/sql/clean.sql +++ b/yudao-module-pay/src/test/resources/sql/clean.sql @@ -3,5 +3,6 @@ DELETE FROM pay_channel; DELETE FROM pay_order; DELETE FROM pay_order_extension; DELETE FROM pay_refund; +DELETE FROM pay_transfer; DELETE FROM pay_notify_task; DELETE FROM pay_notify_log; diff --git a/yudao-module-pay/src/test/resources/sql/create_tables.sql b/yudao-module-pay/src/test/resources/sql/create_tables.sql index 3f9f764179..6af612f018 100644 --- a/yudao-module-pay/src/test/resources/sql/create_tables.sql +++ b/yudao-module-pay/src/test/resources/sql/create_tables.sql @@ -45,6 +45,8 @@ CREATE TABLE IF NOT EXISTS `pay_order` ( `channel_fee_price` bigint(20) DEFAULT 0, `status` tinyint(4) NOT NULL, `user_ip` varchar(50) NOT NULL, + `user_id` bigint(20) DEFAULT NULL, + `user_type` tinyint(4) DEFAULT NULL, `expire_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `success_time` datetime(0) DEFAULT CURRENT_TIMESTAMP, `notify_time` datetime(0) DEFAULT CURRENT_TIMESTAMP, @@ -97,6 +99,8 @@ CREATE TABLE IF NOT EXISTS `pay_refund` ( `refund_price` bigint(20) NOT NULL, `reason` varchar(256) NOT NULL, `user_ip` varchar(50) NULL DEFAULT NULL, + `user_id` bigint(20) NULL DEFAULT NULL, + `user_type` tinyint(4) NULL DEFAULT NULL, `channel_order_no` varchar(64) NOT NULL, `channel_refund_no` varchar(64) NULL DEFAULT NULL, `success_time` datetime(0) NULL DEFAULT NULL, @@ -145,3 +149,32 @@ CREATE TABLE IF NOT EXISTS `pay_notify_log` ( `deleted` bit(1) NOT NULL DEFAULT FALSE, PRIMARY KEY ("id") ) COMMENT = '支付通知日志'; + +CREATE TABLE IF NOT EXISTS `pay_transfer` ( + "id" number NOT NULL GENERATED BY DEFAULT AS IDENTITY, + `no` varchar(64) NOT NULL, + `app_id` bigint(20) NOT NULL, + `channel_id` bigint(20) NOT NULL, + `channel_code` varchar(32) NOT NULL, + `user_id` bigint(20) NULL DEFAULT NULL, + `user_type` tinyint(4) NULL DEFAULT NULL, + `merchant_transfer_id` varchar(64) NOT NULL, + `price` bigint(20) NOT NULL, + `subject` varchar(256) NOT NULL, + `user_account` varchar(256) NOT NULL, + `user_name` varchar(64) NULL DEFAULT NULL, + `status` tinyint(4) NOT NULL, + `notify_url` varchar(1024) NULL DEFAULT NULL, + `channel_transfer_no` varchar(64) NULL DEFAULT NULL, + `success_time` datetime(0) NULL DEFAULT NULL, + `channel_error_code` varchar(128) NULL DEFAULT NULL, + `channel_error_msg` varchar(256) NULL DEFAULT NULL, + `channel_notify_data` varchar(1024) NULL DEFAULT NULL, + `channel_extras` varchar(1024) NULL DEFAULT NULL, + `creator` varchar(64) NULL DEFAULT '', + `create_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updater` varchar(64) NULL DEFAULT '', + `update_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `deleted` bit(1) NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT = '转账单'; From 6b79cab09a5334844f8159a086e85365c82e86d0 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 28 Jul 2025 21:28:40 +0800 Subject: [PATCH 79/89] =?UTF-8?q?review=EF=BC=9A=E3=80=90iot=20=E7=89=A9?= =?UTF-8?q?=E8=81=94=E7=BD=91=E3=80=91tcp=20=E5=8D=8F=E8=AE=AE=E7=9A=84?= =?UTF-8?q?=E6=8E=A5=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../message/IotDeviceMessageServiceImpl.java | 4 +- .../alink/IotAlinkDeviceMessageCodec.java | 12 +++-- .../tcp/IotTcpBinaryDeviceMessageCodec.java | 54 +++++++++++++------ .../tcp/IotTcpJsonDeviceMessageCodec.java | 35 +++++------- .../tcp/IotTcpDownstreamSubscriber.java | 2 + .../tcp/manager/IotTcpAuthManager.java | 8 +++ .../tcp/manager/IotTcpSessionManager.java | 1 + .../tcp/router/IotTcpDownstreamHandler.java | 5 +- .../tcp/router/IotTcpUpstreamHandler.java | 30 +++++++---- 9 files changed, 94 insertions(+), 57 deletions(-) diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageServiceImpl.java index 76b31f30ce..01d1c45eee 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageServiceImpl.java @@ -236,8 +236,8 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { @Override public Long getDeviceMessageCount(LocalDateTime createTime) { - return deviceMessageMapper - .selectCountByCreateTime(createTime != null ? LocalDateTimeUtil.toEpochMilli(createTime) : null); + return deviceMessageMapper.selectCountByCreateTime( + createTime != null ? LocalDateTimeUtil.toEpochMilli(createTime) : null); } @Override diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/alink/IotAlinkDeviceMessageCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/alink/IotAlinkDeviceMessageCodec.java index 300b2e48ec..9086480d3f 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/alink/IotAlinkDeviceMessageCodec.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/alink/IotAlinkDeviceMessageCodec.java @@ -18,6 +18,8 @@ import org.springframework.stereotype.Component; @Component public class IotAlinkDeviceMessageCodec implements IotDeviceMessageCodec { + private static final String TYPE = "Alink"; + @Data @NoArgsConstructor @AllArgsConstructor @@ -62,6 +64,11 @@ public class IotAlinkDeviceMessageCodec implements IotDeviceMessageCodec { } + @Override + public String type() { + return TYPE; + } + @Override public byte[] encode(IotDeviceMessage message) { AlinkMessage alinkMessage = new AlinkMessage(message.getRequestId(), AlinkMessage.VERSION_1, @@ -79,9 +86,4 @@ public class IotAlinkDeviceMessageCodec implements IotDeviceMessageCodec { alinkMessage.getData(), alinkMessage.getCode(), alinkMessage.getMsg()); } - @Override - public String type() { - return "Alink"; - } - } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java index f7d8a80be1..b140e37e58 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java @@ -11,17 +11,29 @@ import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.stereotype.Component; +// TODO @haohao:【重要】是不是二进制更彻底哈? +// 包头(4 字节) +// 消息 ID string;nvarchar(length + string) +// version(可选,不要干脆) +// method string;nvarchar;为什么不要 opcode?因为 IotTcpJsonDeviceMessageCodec 里面,实际已经没 opcode 了 +// reply bit;0 请求,1 响应 +// 请求时: +// params;nvarchar;json 处理 +// 响应时: +// code +// msg nvarchar +// data;nvarchar;json 处理 /** * TCP 二进制格式 {@link IotDeviceMessage} 编解码器 * - * 使用自定义二进制协议格式: - * 包头(4 字节) | 功能码(2 字节) | 消息序号(2 字节) | 包体数据(变长) + * 使用自定义二进制协议格式:包头(4 字节) | 功能码(2 字节) | 消息序号(2 字节) | 包体数据(变长) * * @author 芋道源码 */ @Component public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { + // TODO @haohao:是不是叫 TCP_Binary 好点哈? public static final String TYPE = "TCP_BINARY"; @Data @@ -34,11 +46,13 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { */ private Short code; + // TODO @haohao:这个和 AlinkMessage 里面,是一个东西哇? /** * 消息序号 */ private Short mid; + // TODO @haohao:这个字段,是不是没用到呀?感觉应该也不在消息列哈? /** * 设备 ID */ @@ -59,6 +73,7 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { */ private Object data; + // TODO @haohao:这个可以改成 code 哇?更好理解一点; /** * 响应错误码 */ @@ -69,6 +84,8 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { */ private String msg; + // TODO @haohao:TcpBinaryMessage 和 TcpJsonMessage 保持一致哈? + } @Override @@ -83,13 +100,14 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { try { // 1. 确定功能码 - short code = MessageMethod.STATE_ONLINE.equals(message.getMethod()) ? TcpDataPackage.CODE_HEARTBEAT - : TcpDataPackage.CODE_MESSAGE_UP; + short code = MessageMethod.STATE_ONLINE.equals(message.getMethod()) + ? TcpDataPackage.CODE_HEARTBEAT : TcpDataPackage.CODE_MESSAGE_UP; // 2. 构建负载数据 String payload = buildPayload(message); // 3. 构建 TCP 数据包 + // TODO @haohao:这个和 AlinkMessage.id 是不是一致的哈? short mid = (short) (System.currentTimeMillis() % Short.MAX_VALUE); TcpDataPackage dataPackage = new TcpDataPackage(code, mid, payload); @@ -101,7 +119,6 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { } @Override - @SuppressWarnings("DataFlowIssue") public IotDeviceMessage decode(byte[] bytes) { Assert.notNull(bytes, "待解码数据不能为空"); Assert.isTrue(bytes.length > 0, "待解码数据不能为空"); @@ -188,21 +205,20 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { Assert.notNull(dataPackage, "数据包对象不能为空"); Assert.notNull(dataPackage.getPayload(), "负载不能为空"); - Buffer buffer = Buffer.buffer(); - // 1. 计算包体长度(除了包头 4 字节) int payloadLength = dataPackage.getPayload().getBytes().length; int totalLength = 2 + 2 + payloadLength; - // 2. 写入包头:总长度(4 字节) + // 2. 写入数据 + Buffer buffer = Buffer.buffer(); + // 2.1 写入包头:总长度(4 字节) buffer.appendInt(totalLength); - // 3. 写入功能码(2 字节) + // 2.2 写入功能码(2 字节) buffer.appendShort(dataPackage.getCode()); - // 4. 写入消息序号(2 字节) + // 2.3 写入消息序号(2 字节) buffer.appendShort(dataPackage.getMid()); - // 5. 写入包体数据(不定长) + // 2.4 写入包体数据(不定长) buffer.appendBytes(dataPackage.getPayload().getBytes()); - return buffer; } @@ -216,18 +232,14 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { Assert.isTrue(buffer.length() >= 8, "数据包长度不足"); int index = 0; - // 1. 跳过包头(4 字节) index += 4; - // 2. 获取功能码(2 字节) short code = buffer.getShort(index); index += 2; - // 3. 获取消息序号(2 字节) short mid = buffer.getShort(index); index += 2; - // 4. 获取包体数据 String payload = ""; if (index < buffer.length()) { @@ -239,14 +251,17 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { // ==================== 内部类 ==================== + // TODO @haohao:会不会存在 reply 的时候,有 data、msg、code 参数哈。 /** * 负载信息类 */ @Data @AllArgsConstructor private static class PayloadInfo { + private String requestId; private Object params; + } /** @@ -255,6 +270,7 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { @Data @AllArgsConstructor private static class TcpDataPackage { + // 功能码定义 public static final short CODE_REGISTER = 10; public static final short CODE_REGISTER_REPLY = 11; @@ -263,9 +279,11 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { public static final short CODE_MESSAGE_UP = 30; public static final short CODE_MESSAGE_DOWN = 40; + // TODO @haohao:要不改成 opCode private short code; private short mid; private String payload; + } // ==================== 常量定义 ==================== @@ -274,12 +292,15 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { * 消息方法常量 */ public static class MessageMethod { + public static final String PROPERTY_POST = "thing.property.post"; // 数据上报 public static final String STATE_ONLINE = "thing.state.online"; // 心跳 + } // ==================== 自定义异常 ==================== + // TODO @haohao:全局异常搞个。看着可以服用哈。 /** * TCP 编解码异常 */ @@ -288,4 +309,5 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { super(message, cause); } } + } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java index f1c88d396f..1bbda950b7 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java @@ -12,15 +12,14 @@ import org.springframework.stereotype.Component; /** * TCP JSON 格式 {@link IotDeviceMessage} 编解码器 * - * 采用纯 JSON 格式传输 - * - * JSON 消息格式: + * 采用纯 JSON 格式传输,格式如下: * { - * "id": "消息 ID", - * "method": "消息方法", - * "deviceId": "设备 ID", - * "params": {...}, - * "timestamp": 时间戳 + * "id": "消息 ID", + * "method": "消息方法", + * "deviceId": "设备 ID", + * "params": {...}, + * "timestamp": 时间戳 + * // TODO @haohao:貌似少了 code、msg、timestamp * } * * @author 芋道源码 @@ -45,6 +44,7 @@ public class IotTcpJsonDeviceMessageCodec implements IotDeviceMessageCodec { */ private String method; + // TODO @haohao:这个字段,是不是没用到呀?感觉应该也不在消息列哈? /** * 设备 ID */ @@ -84,14 +84,9 @@ public class IotTcpJsonDeviceMessageCodec implements IotDeviceMessageCodec { @Override public byte[] encode(IotDeviceMessage message) { - TcpJsonMessage tcpJsonMessage = new TcpJsonMessage( - message.getRequestId(), - message.getMethod(), + TcpJsonMessage tcpJsonMessage = new TcpJsonMessage(message.getRequestId(), message.getMethod(), message.getDeviceId(), - message.getParams(), - message.getData(), - message.getCode(), - message.getMsg(), + message.getParams(), message.getData(), message.getCode(), message.getMsg(), System.currentTimeMillis()); return JsonUtils.toJsonByte(tcpJsonMessage); } @@ -102,13 +97,9 @@ public class IotTcpJsonDeviceMessageCodec implements IotDeviceMessageCodec { TcpJsonMessage tcpJsonMessage = JsonUtils.parseObject(bytes, TcpJsonMessage.class); Assert.notNull(tcpJsonMessage, "消息不能为空"); Assert.notBlank(tcpJsonMessage.getMethod(), "消息方法不能为空"); - IotDeviceMessage iotDeviceMessage = IotDeviceMessage.of( - tcpJsonMessage.getId(), - tcpJsonMessage.getMethod(), - tcpJsonMessage.getParams(), - tcpJsonMessage.getData(), - tcpJsonMessage.getCode(), - tcpJsonMessage.getMsg()); + // TODO @haohao:这个我已经改了哈。一些属性,可以放在一行,好理解一点~ + IotDeviceMessage iotDeviceMessage = IotDeviceMessage.of(tcpJsonMessage.getId(), tcpJsonMessage.getMethod(), + tcpJsonMessage.getParams(), tcpJsonMessage.getData(), tcpJsonMessage.getCode(), tcpJsonMessage.getMsg()); iotDeviceMessage.setDeviceId(tcpJsonMessage.getDeviceId()); return iotDeviceMessage; } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java index 2022805fc5..6130caa851 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java @@ -27,10 +27,12 @@ public class IotTcpDownstreamSubscriber implements IotMessageSubscriber authStatusMap = new ConcurrentHashMap<>(); + // TODO @haohao:得考虑,一个设备连接多次? /** * 设备 ID -> NetSocket 的映射(用于快速查找) */ @@ -37,6 +38,7 @@ public class IotTcpAuthManager { */ public void registerAuth(NetSocket socket, AuthInfo authInfo) { // 如果设备已有其他连接,先清理旧连接 + // TODO @haohao:是不是允许同时连接?就像 mqtt 应该也允许重复连接哈? NetSocket oldSocket = deviceSocketMap.get(authInfo.getDeviceId()); if (oldSocket != null && oldSocket != socket) { log.info("[registerAuth][设备已有其他连接,清理旧连接] 设备 ID: {}, 旧连接: {}", @@ -66,6 +68,7 @@ public class IotTcpAuthManager { } } + // TODO @haohao:建议暂时没用的方法,可以删除掉;整体聚焦! /** * 注销设备认证信息 * @@ -158,6 +161,7 @@ public class IotTcpAuthManager { int count = authStatusMap.size(); authStatusMap.clear(); deviceSocketMap.clear(); + // TODO @haohao:第一个括号是方法,第二个括号是明细日志;其它日志,也可以检查下哈。 log.info("[clearAll][清理所有认证信息] 清理数量: {}", count); } @@ -166,6 +170,7 @@ public class IotTcpAuthManager { */ @Data public static class AuthInfo { + /** * 设备编号 */ @@ -181,6 +186,7 @@ public class IotTcpAuthManager { */ private String deviceName; + // TODO @haohao:令牌不要存储,万一有安全问题哈; /** * 认证令牌 */ @@ -190,5 +196,7 @@ public class IotTcpAuthManager { * 客户端 ID */ private String clientId; + } + } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpSessionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpSessionManager.java index 6baa899f30..00685e5cf6 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpSessionManager.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpSessionManager.java @@ -7,6 +7,7 @@ import org.springframework.stereotype.Component; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +// TODO @haohao:IotTcpSessionManager、IotTcpAuthManager 是不是融合哈? /** * IoT 网关 TCP 会话管理器 *

diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java index abf71338de..05970ede13 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java @@ -21,6 +21,7 @@ public class IotTcpDownstreamHandler { private final IotTcpSessionManager sessionManager; + // TODO @haohao:这个可以使用 lombok 简化构造方法 public IotTcpDownstreamHandler(IotDeviceMessageService messageService, IotDeviceService deviceService, IotTcpSessionManager sessionManager) { this.messageService = messageService; @@ -38,6 +39,7 @@ public class IotTcpDownstreamHandler { log.info("[handle][处理下行消息] 设备 ID: {}, 方法: {}, 消息 ID: {}", message.getDeviceId(), message.getMethod(), message.getId()); + // TODO @haohao 1. 和 2. 可以合成 1.1 1.2 并且中间可以不空行; // 1. 获取设备信息 IotDeviceRespDTO device = deviceService.getDeviceFromCache(message.getDeviceId()); if (device == null) { @@ -62,11 +64,12 @@ public class IotTcpDownstreamHandler { } else { log.error("[handle][下行消息发送失败] 设备 ID: {}, 方法: {}, 消息 ID: {}", message.getDeviceId(), message.getMethod(), message.getId()); - } + } // TODO @haohao:下面这个空行,可以考虑去掉的哈。 } catch (Exception e) { log.error("[handle][处理下行消息失败] 设备 ID: {}, 方法: {}, 消息内容: {}", message.getDeviceId(), message.getMethod(), message.getParams(), e); } } + } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java index eec4e1556a..6acc235569 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java @@ -8,6 +8,7 @@ import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; @@ -28,8 +29,10 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class IotTcpUpstreamHandler implements Handler { + // TODO @haohao:这两个变量,可以复用 IotTcpBinaryDeviceMessageCodec 的 TYPE private static final String CODEC_TYPE_JSON = "TCP_JSON"; private static final String CODEC_TYPE_BINARY = "TCP_BINARY"; + private static final String AUTH_METHOD = "auth"; private final IotDeviceMessageService deviceMessageService; @@ -89,6 +92,7 @@ public class IotTcpUpstreamHandler implements Handler { return; } + // TODO @haohao:2. 和 3. 可以合并成 2.1 2.2 ,都是异常的情况。然后 3. 可以 return 直接; // 2. 获取设备信息 IotDeviceRespDTO device = deviceService.getDeviceFromCache(messageInfo.message.getDeviceId()); if (device == null) { @@ -114,12 +118,13 @@ public class IotTcpUpstreamHandler implements Handler { private void handleAuthRequest(String clientId, IotDeviceMessage message, NetSocket socket, String codecType) { try { // 1. 验证认证请求 + // TODO @haohao:ObjUtil.notEquals。减少取反 if (!AUTH_METHOD.equals(message.getMethod())) { sendError(socket, message.getRequestId(), "请先进行认证", codecType); return; } - // 2. 解析认证参数 + // 2. 解析认证参数 // TODO @haohao:1. 和 2. 可以合并成 1.1 1.2 都是参数校验 AuthParams authParams = parseAuthParams(message.getParams()); if (authParams == null) { sendError(socket, message.getRequestId(), "认证参数不完整", codecType); @@ -127,6 +132,7 @@ public class IotTcpUpstreamHandler implements Handler { } // 3. 执行认证流程 + // TODO @haohao:成功失败、都大哥日志,会不会更好哈? if (performAuthentication(authParams, socket, message.getRequestId(), codecType)) { log.info("[handleAuthRequest][认证成功] 客户端 ID: {}, username: {}", clientId, authParams.username); } @@ -157,6 +163,7 @@ public class IotTcpUpstreamHandler implements Handler { /** * 解码消息 */ + // TODO @haohao:是不是还是直接管理后台配置协议,然后直接使用就好啦。暂时不考虑动态解析哈。保持一致,降低理解成本哈。 private MessageInfo decodeMessage(Buffer buffer) { try { String rawData = buffer.toString(); @@ -172,6 +179,7 @@ public class IotTcpUpstreamHandler implements Handler { /** * 执行认证 */ + // TODO @haohao:下面的 1. 2. 可以合并下,本质也是校验哈。 private boolean performAuthentication(AuthParams authParams, NetSocket socket, String requestId, String codecType) { // 1. 执行认证 if (!authenticateDevice(authParams)) { @@ -202,7 +210,6 @@ public class IotTcpUpstreamHandler implements Handler { deviceMessageService.sendDeviceMessage(onlineMessage, deviceInfo.getProductKey(), deviceInfo.getDeviceName(), serverId); sendSuccess(socket, requestId, "认证成功", codecType); - return true; } @@ -252,8 +259,9 @@ public class IotTcpUpstreamHandler implements Handler { * 判断是否为 JSON 格式 */ private boolean isJsonFormat(String data) { - if (StrUtil.isBlank(data)) + if (StrUtil.isBlank(data)) { return false; + } String trimmed = data.trim(); return (trimmed.startsWith("{") && trimmed.endsWith("}")) || (trimmed.startsWith("[") && trimmed.endsWith("]")); } @@ -262,15 +270,14 @@ public class IotTcpUpstreamHandler implements Handler { * 解析认证参数 */ private AuthParams parseAuthParams(Object params) { - if (params == null) + if (params == null) { return null; - + } JSONObject paramsJson = params instanceof JSONObject ? (JSONObject) params : JSONUtil.parseObj(params.toString()); String clientId = paramsJson.getStr("clientId"); String username = paramsJson.getStr("username"); String password = paramsJson.getStr("password"); - return StrUtil.hasBlank(clientId, username, password) ? null : new AuthParams(clientId, username, password); } @@ -278,11 +285,8 @@ public class IotTcpUpstreamHandler implements Handler { * 认证设备 */ private boolean authenticateDevice(AuthParams authParams) { - CommonResult result = deviceApi - .authDevice(new cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO() - .setClientId(authParams.clientId) - .setUsername(authParams.username) - .setPassword(authParams.password)); + CommonResult result = deviceApi.authDevice(new IotDeviceAuthReqDTO() + .setClientId(authParams.clientId).setUsername(authParams.username).setPassword(authParams.password)); return result.isSuccess() && result.getData(); } @@ -291,6 +295,7 @@ public class IotTcpUpstreamHandler implements Handler { */ private void registerAuthInfo(NetSocket socket, IotDeviceRespDTO device, IotDeviceAuthUtils.DeviceInfo deviceInfo, String token, String clientId) { + // TODO @haohao:可以链式调用; IotTcpAuthManager.AuthInfo auth = new IotTcpAuthManager.AuthInfo(); auth.setDeviceId(device.getId()); auth.setProductKey(deviceInfo.getProductKey()); @@ -316,6 +321,8 @@ public class IotTcpUpstreamHandler implements Handler { sendResponse(socket, true, message, requestId, codecType); } + // TODO @haohao:使用 lombok,方便 jdk8 兼容 + /** * 认证参数 */ @@ -327,4 +334,5 @@ public class IotTcpUpstreamHandler implements Handler { */ private record MessageInfo(IotDeviceMessage message, String codecType) { } + } \ No newline at end of file From 1684706e9aa70df8675b4b6bba212b7f5541d078 Mon Sep 17 00:00:00 2001 From: jason <2667446@qq.com> Date: Tue, 29 Jul 2025 09:36:53 +0800 Subject: [PATCH 80/89] =?UTF-8?q?feat:=20[BPM=20=E5=B7=A5=E4=BD=9C?= =?UTF-8?q?=E6=B5=81]=20=E5=A2=9E=E5=8A=A0=E8=B7=B3=E8=BF=87=E8=A1=A8?= =?UTF-8?q?=E8=BE=BE=E5=BC=8F=E5=88=A4=E6=96=AD=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vo/model/simple/BpmSimpleModelNodeVO.java | 3 ++ .../bpm/enums/task/BpmTaskStatusEnum.java | 2 +- .../flowable/core/util/BpmnModelUtils.java | 30 +++++++++++++++++-- .../flowable/core/util/SimpleModelUtils.java | 18 ++++++++++- .../task/BpmProcessInstanceServiceImpl.java | 26 +++++++++++++--- 5 files changed, 71 insertions(+), 8 deletions(-) diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/simple/BpmSimpleModelNodeVO.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/simple/BpmSimpleModelNodeVO.java index 1cbebe06e4..4d34df8308 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/simple/BpmSimpleModelNodeVO.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/simple/BpmSimpleModelNodeVO.java @@ -71,6 +71,9 @@ public class BpmSimpleModelNodeVO { @Schema(description = "是否填写审批意见", example = "false") private Boolean reasonRequire; + @Schema(description = "跳过表达式", example = "{true}") + private String skipExpression; // 用于审批节点 + /** * 审批节点拒绝处理 */ diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmTaskStatusEnum.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmTaskStatusEnum.java index a19f122bd8..9153b3e780 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmTaskStatusEnum.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmTaskStatusEnum.java @@ -13,7 +13,7 @@ import lombok.Getter; @Getter @AllArgsConstructor public enum BpmTaskStatusEnum { - + SKIP(-2, "跳过"), NOT_START(-1, "未开始"), RUNNING(1, "审批中"), APPROVE(2, "审批通过"), diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java index 460af124f6..1bafa578db 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java @@ -800,9 +800,10 @@ public class BpmnModelUtils { || currentElement instanceof EndEvent || currentElement instanceof UserTask || currentElement instanceof ServiceTask) { - // 添加元素 + // 添加节点 FlowNode flowNode = (FlowNode) currentElement; resultElements.add(flowNode); + // 遍历子节点 flowNode.getOutgoingFlows().forEach( nextElement -> simulateNextFlowElements(nextElement.getTargetFlowElement(), variables, resultElements, visitElements)); @@ -835,6 +836,31 @@ public class BpmnModelUtils { } } + /** + * 判断是否跳过此节点 + * + * @param flowNode 节点 + * @param variables 流程变量 + */ + public static boolean isSkipNode(FlowElement flowNode, Map variables) { + // 1. 检查节点是否有跳过表达式(支持多种任务节点类型) + String skipExpression = null; + if (flowNode instanceof UserTask) { + skipExpression = ((UserTask) flowNode).getSkipExpression(); + } else if (flowNode instanceof ServiceTask) { + skipExpression = ((ServiceTask) flowNode).getSkipExpression(); + } else if (flowNode instanceof ScriptTask) { + skipExpression = ((ScriptTask) flowNode).getSkipExpression(); + } + + if (StrUtil.isEmpty(skipExpression)) { + return false; + } + + // 2. 计算跳过表达式的值 + return evalConditionExpress(variables, skipExpression); + } + /** * 根据当前节点,获取下一个节点 * @@ -997,7 +1023,7 @@ public class BpmnModelUtils { * @return 是否满足条件 */ public static boolean evalConditionExpress(Map variables, String expression) { - if (expression == null) { + if (StrUtil.isEmpty(expression)) { return Boolean.FALSE; } // 如果 variables 为空,则创建一个的原因?可能 expression 的计算,不依赖于 variables diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java index 5e9313840b..b016666a8a 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java @@ -464,9 +464,15 @@ public class SimpleModelUtils { addReasonRequire(node.getReasonRequire(), userTask); // 节点类型 addNodeType(node.getType(), userTask); + // 添加跳过表达式 + if (StrUtil.isNotEmpty(node.getSkipExpression())) { + userTask.setSkipExpression(node.getSkipExpression()); + } return userTask; } + + private void addUserTaskListener(BpmSimpleModelNodeVO node, UserTask userTask) { List flowableListeners = new ArrayList<>(3); if (node.getTaskCreateListener() != null @@ -967,7 +973,7 @@ public class SimpleModelUtils { || nodeType == BpmSimpleModelNodeTypeEnum.COPY_NODE || nodeType == BpmSimpleModelNodeTypeEnum.CHILD_PROCESS || nodeType == BpmSimpleModelNodeTypeEnum.END_NODE) { - // 添加元素 + // 添加此节点 resultNodes.add(currentNode); } @@ -1013,6 +1019,16 @@ public class SimpleModelUtils { simulateNextNode(currentNode.getChildNode(), variables, resultNodes); } + /** + * 根据跳过表达式,判断是否跳过此节点。 + */ + public static boolean isSkipNode(BpmSimpleModelNodeVO currentNode, Map variables) { + if (StrUtil.isEmpty(currentNode.getSkipExpression())) { + return false; + } + return BpmnModelUtils.evalConditionExpress(variables, currentNode.getSkipExpression()); + } + public static boolean evalConditionExpress(Map variables, BpmSimpleModelNodeVO.ConditionSetting conditionSetting) { return BpmnModelUtils.evalConditionExpress(variables, buildConditionExpression(conditionSetting)); } diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java index 85d15a6327..524dce20c1 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java @@ -398,7 +398,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService ? BpmSimpleModelNodeTypeEnum.START_USER_NODE.getType() : ObjUtil.defaultIfNull(parseNodeType(flowNode), // 目的:解决“办理节点”的识别 BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType())) - .setStatus(FlowableUtils.getTaskStatus(task)) + .setStatus(getEndActivityNodeStatus(task)) .setCandidateStrategy(BpmnModelUtils.parseCandidateStrategy(flowNode)) .setStartTime(DateUtils.of(task.getCreateTime())).setEndTime(DateUtils.of(task.getEndTime())) .setTasks(singletonList(BpmProcessInstanceConvert.INSTANCE.buildApprovalTaskInfo(task))); @@ -462,6 +462,15 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService return approvalNodes; } + + /** + * 获取结束节点的状态 + */ + private Integer getEndActivityNodeStatus(HistoricTaskInstance task) { + Integer status = FlowableUtils.getTaskStatus(task); + return status == null ? BpmTaskStatusEnum.SKIP.getStatus() : status; // 结束节点未获取到状态,为跳过状态 + } + /** * 获得【进行中】的活动节点们 */ @@ -565,10 +574,14 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService if (runActivityIds.contains(node.getId())) { return null; } - + Integer status = BpmTaskStatusEnum.NOT_START.getStatus(); + // 如果节点被跳过。设置状态为跳过 + if (SimpleModelUtils.isSkipNode(node, processVariables)) { + status = BpmTaskStatusEnum.SKIP.getStatus(); + } ActivityNode activityNode = new ActivityNode().setId(node.getId()).setName(node.getName()) .setNodeType(node.getType()).setCandidateStrategy(node.getCandidateStrategy()) - .setStatus(BpmTaskStatusEnum.NOT_START.getStatus()); + .setStatus(status); // 1. 开始节点/审批节点 if (ObjectUtils.equalsAny(node.getType(), @@ -608,8 +621,13 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService if (runActivityIds.contains(node.getId())) { return null; } + Integer status = BpmTaskStatusEnum.NOT_START.getStatus(); + // 如果节点被跳过,状态设置为跳过 + if(BpmnModelUtils.isSkipNode(node, processVariables)){ + status = BpmTaskStatusEnum.SKIP.getStatus(); + } ActivityNode activityNode = new ActivityNode().setId(node.getId()) - .setStatus(BpmTaskStatusEnum.NOT_START.getStatus()); + .setStatus(status); // 1. 开始节点 if (node instanceof StartEvent) { From e07fb6c8fe31e9cde0fd8a1cf46a5db3e1b01f55 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Tue, 29 Jul 2025 13:13:47 +0800 Subject: [PATCH 81/89] =?UTF-8?q?review=EF=BC=9A=E3=80=90bpm=20=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E6=B5=81=E3=80=91usertask=20=E7=9A=84=E8=B7=B3?= =?UTF-8?q?=E8=BF=87=E8=A1=A8=E8=BE=BE=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../framework/web/core/handler/GlobalExceptionHandler.java | 6 +++--- .../yudao/module/bpm/enums/task/BpmTaskStatusEnum.java | 1 + .../bpm/framework/flowable/core/util/SimpleModelUtils.java | 4 +--- .../bpm/service/task/BpmProcessInstanceServiceImpl.java | 7 +++++-- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java index e27d04ec68..2f103732cc 100644 --- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java @@ -171,15 +171,15 @@ public class GlobalExceptionHandler { /** * 处理 SpringMVC 请求参数类型错误 * - * 例如说,接口上设置了 @RequestBody实体中 xx 属性类型为 Integer,结果传递 xx 参数类型为 String + * 例如说,接口上设置了 @RequestBody 实体中 xx 属性类型为 Integer,结果传递 xx 参数类型为 String */ @ExceptionHandler(HttpMessageNotReadableException.class) public CommonResult methodArgumentTypeInvalidFormatExceptionHandler(HttpMessageNotReadableException ex) { log.warn("[methodArgumentTypeInvalidFormatExceptionHandler]", ex); - if(ex.getCause() instanceof InvalidFormatException) { + if (ex.getCause() instanceof InvalidFormatException) { InvalidFormatException invalidFormatException = (InvalidFormatException) ex.getCause(); return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数类型错误:%s", invalidFormatException.getValue())); - }else { + } else { return defaultExceptionHandler(ServletUtils.getRequest(), ex); } } diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmTaskStatusEnum.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmTaskStatusEnum.java index 9153b3e780..9ba3b5cb3a 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmTaskStatusEnum.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmTaskStatusEnum.java @@ -13,6 +13,7 @@ import lombok.Getter; @Getter @AllArgsConstructor public enum BpmTaskStatusEnum { + SKIP(-2, "跳过"), NOT_START(-1, "未开始"), RUNNING(1, "审批中"), diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java index 373f56339b..2582399a81 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java @@ -471,8 +471,6 @@ public class SimpleModelUtils { return userTask; } - - private void addUserTaskListener(BpmSimpleModelNodeVO node, UserTask userTask) { List flowableListeners = new ArrayList<>(3); if (node.getTaskCreateListener() != null @@ -1021,7 +1019,7 @@ public class SimpleModelUtils { } /** - * 根据跳过表达式,判断是否跳过此节点。 + * 根据跳过表达式,判断是否跳过此节点 */ public static boolean isSkipNode(BpmSimpleModelNodeVO currentNode, Map variables) { if (StrUtil.isEmpty(currentNode.getSkipExpression())) { diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java index 91815f8dd4..30adda6341 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java @@ -462,13 +462,16 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService return approvalNodes; } - /** * 获取结束节点的状态 */ private Integer getEndActivityNodeStatus(HistoricTaskInstance task) { Integer status = FlowableUtils.getTaskStatus(task); - return status == null ? BpmTaskStatusEnum.SKIP.getStatus() : status; // 结束节点未获取到状态,为跳过状态 + if (status != null) { + return status; + } + // 结束节点未获取到状态,为跳过状态。可见 bpmn 或者 simple 的 skipExpression + return BpmTaskStatusEnum.SKIP.getStatus(); } /** From cda59081a38040c9407c9d9cc8f05403a23f8da9 Mon Sep 17 00:00:00 2001 From: haohao <1036606149@qq.com> Date: Tue, 29 Jul 2025 17:41:08 +0800 Subject: [PATCH 82/89] =?UTF-8?q?feat=EF=BC=9A=E3=80=90IoT=20=E7=89=A9?= =?UTF-8?q?=E8=81=94=E7=BD=91=E3=80=91=E6=96=B0=E5=A2=9E=20TCP=20=E8=BF=9E?= =?UTF-8?q?=E6=8E=A5=E7=AE=A1=E7=90=86=E5=99=A8=EF=BC=8C=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E7=BC=96=E8=A7=A3=E7=A0=81=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tcp/IotTcpBinaryDeviceMessageCodec.java | 479 ++++++++++-------- .../tcp/IotTcpJsonDeviceMessageCodec.java | 41 +- .../config/IotGatewayConfiguration.java | 10 +- .../tcp/IotTcpDownstreamSubscriber.java | 38 +- .../protocol/tcp/IotTcpUpstreamProtocol.java | 22 +- .../tcp/manager/IotTcpAuthManager.java | 202 -------- .../tcp/manager/IotTcpConnectionManager.java | 185 +++++++ .../tcp/manager/IotTcpSessionManager.java | 144 ------ .../tcp/router/IotTcpDownstreamHandler.java | 59 +-- .../tcp/router/IotTcpUpstreamHandler.java | 431 ++++++++++------ .../src/main/resources/application.yaml | 11 +- .../tcp/TcpBinaryDataPacketExamplesTest.java | 241 --------- .../tcp/TcpJsonDataPacketExamplesTest.java | 185 ------- .../resources/tcp-binary-packet-examples.md | 474 ++++++----------- .../resources/tcp-json-packet-examples.md | 304 ++++------- 15 files changed, 1045 insertions(+), 1781 deletions(-) delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpAuthManager.java create mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpSessionManager.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpBinaryDataPacketExamplesTest.java delete mode 100644 yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpJsonDataPacketExamplesTest.java diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java index b140e37e58..9ecaa8af6f 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java @@ -8,86 +8,72 @@ import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; import io.vertx.core.buffer.Buffer; import lombok.AllArgsConstructor; import lombok.Data; -import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; -// TODO @haohao:【重要】是不是二进制更彻底哈? -// 包头(4 字节) -// 消息 ID string;nvarchar(length + string) -// version(可选,不要干脆) -// method string;nvarchar;为什么不要 opcode?因为 IotTcpJsonDeviceMessageCodec 里面,实际已经没 opcode 了 -// reply bit;0 请求,1 响应 -// 请求时: -// params;nvarchar;json 处理 -// 响应时: -// code -// msg nvarchar -// data;nvarchar;json 处理 +import java.nio.charset.StandardCharsets; + /** * TCP 二进制格式 {@link IotDeviceMessage} 编解码器 + *

+ * 二进制协议格式(所有数值使用大端序): * - * 使用自定义二进制协议格式:包头(4 字节) | 功能码(2 字节) | 消息序号(2 字节) | 包体数据(变长) + *

+ * +--------+--------+--------+--------+--------+--------+--------+--------+
+ * | 魔术字 | 版本号 | 消息类型| 消息标志|         消息长度(4字节)          |
+ * +--------+--------+--------+--------+--------+--------+--------+--------+
+ * |           消息 ID 长度(2字节)        |      消息 ID (变长字符串)         |
+ * +--------+--------+--------+--------+--------+--------+--------+--------+
+ * |           方法名长度(2字节)        |      方法名(变长字符串)         |
+ * +--------+--------+--------+--------+--------+--------+--------+--------+
+ * |                        消息体数据(变长)                              |
+ * +--------+--------+--------+--------+--------+--------+--------+--------+
+ * 
+ *

+ * 消息体格式: + * - 请求消息:params 数据(JSON) + * - 响应消息:code (4字节) + msg 长度(2字节) + msg 字符串 + data 数据(JSON) + *

+ * 注意:deviceId 不包含在协议中,由服务器根据连接上下文自动设置 * * @author 芋道源码 */ +@Slf4j @Component public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { - // TODO @haohao:是不是叫 TCP_Binary 好点哈? public static final String TYPE = "TCP_BINARY"; - @Data - @NoArgsConstructor - @AllArgsConstructor - private static class TcpBinaryMessage { + // ==================== 协议常量 ==================== - /** - * 功能码 - */ - private Short code; + /** + * 协议魔术字,用于协议识别 + */ + private static final byte MAGIC_NUMBER = (byte) 0x7E; - // TODO @haohao:这个和 AlinkMessage 里面,是一个东西哇? - /** - * 消息序号 - */ - private Short mid; - - // TODO @haohao:这个字段,是不是没用到呀?感觉应该也不在消息列哈? - /** - * 设备 ID - */ - private Long deviceId; - - /** - * 请求方法 - */ - private String method; - - /** - * 请求参数 - */ - private Object params; - - /** - * 响应结果 - */ - private Object data; - - // TODO @haohao:这个可以改成 code 哇?更好理解一点; - /** - * 响应错误码 - */ - private Integer responseCode; - - /** - * 响应提示 - */ - private String msg; - - // TODO @haohao:TcpBinaryMessage 和 TcpJsonMessage 保持一致哈? + /** + * 协议版本号 + */ + private static final byte PROTOCOL_VERSION = (byte) 0x01; + /** + * 消息类型常量 + */ + public static class MessageType { + public static final byte REQUEST = 0x01; // 请求消息 + public static final byte RESPONSE = 0x02; // 响应消息 } + /** + * 协议头部固定长度(魔术字 + 版本号 + 消息类型 + 消息标志 + 消息长度) + */ + private static final int HEADER_FIXED_LENGTH = 8; + + /** + * 最小消息长度(头部 + 消息ID长度 + 方法名长度) + */ + private static final int MIN_MESSAGE_LENGTH = HEADER_FIXED_LENGTH + 4; + @Override public String type() { return TYPE; @@ -99,215 +85,270 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { Assert.notBlank(message.getMethod(), "消息方法不能为空"); try { - // 1. 确定功能码 - short code = MessageMethod.STATE_ONLINE.equals(message.getMethod()) - ? TcpDataPackage.CODE_HEARTBEAT : TcpDataPackage.CODE_MESSAGE_UP; + // 1. 确定消息类型 + byte messageType = determineMessageType(message); - // 2. 构建负载数据 - String payload = buildPayload(message); + // 2. 构建消息体 + byte[] bodyData = buildMessageBody(message, messageType); - // 3. 构建 TCP 数据包 - // TODO @haohao:这个和 AlinkMessage.id 是不是一致的哈? - short mid = (short) (System.currentTimeMillis() % Short.MAX_VALUE); - TcpDataPackage dataPackage = new TcpDataPackage(code, mid, payload); + // 3. 构建完整消息(不包含deviceId,由连接上下文管理) + return buildCompleteMessage(message, messageType, bodyData); - // 4. 编码为字节流 - return encodeTcpDataPackage(dataPackage).getBytes(); } catch (Exception e) { - throw new TcpCodecException("TCP 消息编码失败", e); + log.error("[encode][TCP 二进制消息编码失败,消息: {}]", message, e); + throw new RuntimeException("TCP 二进制消息编码失败: " + e.getMessage(), e); } } @Override public IotDeviceMessage decode(byte[] bytes) { Assert.notNull(bytes, "待解码数据不能为空"); - Assert.isTrue(bytes.length > 0, "待解码数据不能为空"); + Assert.isTrue(bytes.length >= MIN_MESSAGE_LENGTH, "数据包长度不足"); try { - // 1. 解码 TCP 数据包 - TcpDataPackage dataPackage = decodeTcpDataPackage(Buffer.buffer(bytes)); + Buffer buffer = Buffer.buffer(bytes); - // 2. 根据功能码确定方法 - String method = (dataPackage.getCode() == TcpDataPackage.CODE_HEARTBEAT) ? MessageMethod.STATE_ONLINE - : MessageMethod.PROPERTY_POST; + // 1. 解析协议头部 + ProtocolHeader header = parseProtocolHeader(buffer); - // 3. 解析负载数据 - PayloadInfo payloadInfo = parsePayloadInfo(dataPackage.getPayload()); + // 2. 解析消息内容(不包含deviceId,由上层连接管理器设置) + return parseMessageContent(buffer, header); - // 4. 构建 IoT 设备消息 - return IotDeviceMessage.of( - payloadInfo.getRequestId(), - method, - payloadInfo.getParams(), - null, - null, - null); } catch (Exception e) { - throw new TcpCodecException("TCP 消息解码失败", e); + log.error("[decode][TCP 二进制消息解码失败,数据长度: {}]", bytes.length, e); + throw new RuntimeException("TCP 二进制消息解码失败: " + e.getMessage(), e); } } - // ==================== 内部辅助方法 ==================== + // ==================== 编码相关方法 ==================== /** - * 构建负载数据 - * - * @param message 设备消息 - * @return 负载字符串 + * 确定消息类型 + * 优化后的判断逻辑:有响应字段就是响应消息,否则就是请求消息 */ - private String buildPayload(IotDeviceMessage message) { - TcpBinaryMessage tcpBinaryMessage = new TcpBinaryMessage( - null, // code 在数据包中单独处理 - null, // mid 在数据包中单独处理 - message.getDeviceId(), - message.getMethod(), - message.getParams(), - message.getData(), - message.getCode(), - message.getMsg()); - return JsonUtils.toJsonString(tcpBinaryMessage); + private byte determineMessageType(IotDeviceMessage message) { + // 判断是否为响应消息:有响应码或响应消息时为响应 + if (message.getCode() != null || StrUtil.isNotBlank(message.getMsg())) { + return MessageType.RESPONSE; + } + // 默认为请求消息 + return MessageType.REQUEST; } /** - * 解析负载信息 - * - * @param payload 负载字符串 - * @return 负载信息 + * 构建消息体 */ - private PayloadInfo parsePayloadInfo(String payload) { - if (StrUtil.isBlank(payload)) { - return new PayloadInfo(null, null); - } + private byte[] buildMessageBody(IotDeviceMessage message, byte messageType) { + Buffer bodyBuffer = Buffer.buffer(); - try { - TcpBinaryMessage tcpBinaryMessage = JsonUtils.parseObject(payload, TcpBinaryMessage.class); - if (tcpBinaryMessage != null) { - return new PayloadInfo( - StrUtil.isNotEmpty(tcpBinaryMessage.getMethod()) - ? tcpBinaryMessage.getMethod() + "_" + System.currentTimeMillis() - : null, - tcpBinaryMessage.getParams()); + if (messageType == MessageType.RESPONSE) { + // 响应消息:code + msg长度 + msg + data + bodyBuffer.appendInt(message.getCode() != null ? message.getCode() : 0); + + String msg = message.getMsg() != null ? message.getMsg() : ""; + byte[] msgBytes = msg.getBytes(StandardCharsets.UTF_8); + bodyBuffer.appendShort((short) msgBytes.length); + bodyBuffer.appendBytes(msgBytes); + + if (message.getData() != null) { + bodyBuffer.appendBytes(JsonUtils.toJsonByte(message.getData())); + } + } else { + // 请求消息:包含 params 或 data + Object payload = message.getParams() != null ? message.getParams() : message.getData(); + if (payload != null) { + bodyBuffer.appendBytes(JsonUtils.toJsonByte(payload)); } - } catch (Exception e) { - // 如果解析失败,返回默认值 - return new PayloadInfo("unknown_" + System.currentTimeMillis(), null); } - return null; + + return bodyBuffer.getBytes(); } /** - * 编码 TCP 数据包 - * - * @param dataPackage 数据包对象 - * @return 编码后的字节流 + * 构建完整消息 */ - private Buffer encodeTcpDataPackage(TcpDataPackage dataPackage) { - Assert.notNull(dataPackage, "数据包对象不能为空"); - Assert.notNull(dataPackage.getPayload(), "负载不能为空"); - - // 1. 计算包体长度(除了包头 4 字节) - int payloadLength = dataPackage.getPayload().getBytes().length; - int totalLength = 2 + 2 + payloadLength; - - // 2. 写入数据 + private byte[] buildCompleteMessage(IotDeviceMessage message, byte messageType, byte[] bodyData) { Buffer buffer = Buffer.buffer(); - // 2.1 写入包头:总长度(4 字节) - buffer.appendInt(totalLength); - // 2.2 写入功能码(2 字节) - buffer.appendShort(dataPackage.getCode()); - // 2.3 写入消息序号(2 字节) - buffer.appendShort(dataPackage.getMid()); - // 2.4 写入包体数据(不定长) - buffer.appendBytes(dataPackage.getPayload().getBytes()); - return buffer; + + // 1. 写入协议头部 + buffer.appendByte(MAGIC_NUMBER); + buffer.appendByte(PROTOCOL_VERSION); + buffer.appendByte(messageType); + buffer.appendByte((byte) 0x00); // 消息标志,预留字段 + + // 2. 预留消息长度位置 + int lengthPosition = buffer.length(); + buffer.appendInt(0); + + // 3. 写入消息ID + String messageId = StrUtil.isNotBlank(message.getRequestId()) ? message.getRequestId() + : generateMessageId(message.getMethod()); + byte[] messageIdBytes = messageId.getBytes(StandardCharsets.UTF_8); + buffer.appendShort((short) messageIdBytes.length); + buffer.appendBytes(messageIdBytes); + + // 4. 写入方法名 + byte[] methodBytes = message.getMethod().getBytes(StandardCharsets.UTF_8); + buffer.appendShort((short) methodBytes.length); + buffer.appendBytes(methodBytes); + + // 5. 写入消息体 + buffer.appendBytes(bodyData); + + // 6. 更新消息长度 + buffer.setInt(lengthPosition, buffer.length()); + + return buffer.getBytes(); } /** - * 解码 TCP 数据包 - * - * @param buffer 数据缓冲区 - * @return 解码后的数据包 + * 生成消息 ID */ - private TcpDataPackage decodeTcpDataPackage(Buffer buffer) { - Assert.isTrue(buffer.length() >= 8, "数据包长度不足"); + private String generateMessageId(String method) { + return method + "_" + System.currentTimeMillis() + "_" + (int) (Math.random() * 1000); + } + // ==================== 解码相关方法 ==================== + + /** + * 解析协议头部 + */ + private ProtocolHeader parseProtocolHeader(Buffer buffer) { int index = 0; - // 1. 跳过包头(4 字节) + + // 1. 验证魔术字 + byte magic = buffer.getByte(index++); + Assert.isTrue(magic == MAGIC_NUMBER, "无效的协议魔术字: " + magic); + + // 2. 验证版本号 + byte version = buffer.getByte(index++); + Assert.isTrue(version == PROTOCOL_VERSION, "不支持的协议版本: " + version); + + // 3. 读取消息类型 + byte messageType = buffer.getByte(index++); + Assert.isTrue(isValidMessageType(messageType), "无效的消息类型: " + messageType); + + // 4. 读取消息标志(暂时跳过) + byte messageFlags = buffer.getByte(index++); + + // 5. 读取消息长度 + int messageLength = buffer.getInt(index); index += 4; - // 2. 获取功能码(2 字节) - short code = buffer.getShort(index); + + Assert.isTrue(messageLength == buffer.length(), "消息长度不匹配,期望: " + messageLength + ", 实际: " + buffer.length()); + + return new ProtocolHeader(magic, version, messageType, messageFlags, messageLength, index); + } + + /** + * 解析消息内容 + */ + private IotDeviceMessage parseMessageContent(Buffer buffer, ProtocolHeader header) { + int index = header.getNextIndex(); + + // 1. 读取消息ID + short messageIdLength = buffer.getShort(index); index += 2; - // 3. 获取消息序号(2 字节) - short mid = buffer.getShort(index); + String messageId = buffer.getString(index, index + messageIdLength, StandardCharsets.UTF_8.name()); + index += messageIdLength; + + // 2. 读取方法名 + short methodLength = buffer.getShort(index); index += 2; - // 4. 获取包体数据 - String payload = ""; - if (index < buffer.length()) { - payload = buffer.getString(index, buffer.length()); + String method = buffer.getString(index, index + methodLength, StandardCharsets.UTF_8.name()); + index += methodLength; + + // 3. 解析消息体 + return parseMessageBody(buffer, index, header.getMessageType(), messageId, method); + } + + /** + * 解析消息体 + */ + private IotDeviceMessage parseMessageBody(Buffer buffer, int startIndex, byte messageType, + String messageId, String method) { + if (startIndex >= buffer.length()) { + // 空消息体 + return IotDeviceMessage.of(messageId, method, null, null, null, null); } - return new TcpDataPackage(code, mid, payload); + if (messageType == MessageType.RESPONSE) { + // 响应消息:解析 code + msg + data + return parseResponseMessage(buffer, startIndex, messageId, method); + } else { + // 请求消息:解析 payload(可能是 params 或 data) + Object payload = parseJsonData(buffer, startIndex, buffer.length()); + return IotDeviceMessage.of(messageId, method, payload, null, null, null); + } + } + + /** + * 解析响应消息 + */ + private IotDeviceMessage parseResponseMessage(Buffer buffer, int startIndex, String messageId, String method) { + int index = startIndex; + + // 1. 读取响应码 + Integer code = buffer.getInt(index); + index += 4; + + // 2. 读取响应消息 + short msgLength = buffer.getShort(index); + index += 2; + String msg = msgLength > 0 ? buffer.getString(index, index + msgLength, StandardCharsets.UTF_8.name()) : null; + index += msgLength; + + // 3. 读取响应数据 + Object data = null; + if (index < buffer.length()) { + data = parseJsonData(buffer, index, buffer.length()); + } + + return IotDeviceMessage.of(messageId, method, null, data, code, msg); + } + + /** + * 解析JSON数据 + */ + private Object parseJsonData(Buffer buffer, int startIndex, int endIndex) { + if (startIndex >= endIndex) { + return null; + } + + try { + String jsonStr = buffer.getString(startIndex, endIndex, StandardCharsets.UTF_8.name()); + if (StrUtil.isBlank(jsonStr)) { + return null; + } + return JsonUtils.parseObject(jsonStr, Object.class); + } catch (Exception e) { + log.warn("[parseJsonData][JSON 解析失败,返回原始字符串]", e); + return buffer.getString(startIndex, endIndex, StandardCharsets.UTF_8.name()); + } + } + + // ==================== 辅助方法 ==================== + + /** + * 验证消息类型是否有效 + */ + private boolean isValidMessageType(byte messageType) { + return messageType == MessageType.REQUEST || messageType == MessageType.RESPONSE; } // ==================== 内部类 ==================== - // TODO @haohao:会不会存在 reply 的时候,有 data、msg、code 参数哈。 /** - * 负载信息类 + * 协议头部信息 */ @Data @AllArgsConstructor - private static class PayloadInfo { - - private String requestId; - private Object params; - + private static class ProtocolHeader { + private byte magic; + private byte version; + private byte messageType; + private byte messageFlags; + private int messageLength; + private int nextIndex; // 指向消息内容开始位置 } - - /** - * TCP 数据包内部类 - */ - @Data - @AllArgsConstructor - private static class TcpDataPackage { - - // 功能码定义 - public static final short CODE_REGISTER = 10; - public static final short CODE_REGISTER_REPLY = 11; - public static final short CODE_HEARTBEAT = 20; - public static final short CODE_HEARTBEAT_REPLY = 21; - public static final short CODE_MESSAGE_UP = 30; - public static final short CODE_MESSAGE_DOWN = 40; - - // TODO @haohao:要不改成 opCode - private short code; - private short mid; - private String payload; - - } - - // ==================== 常量定义 ==================== - - /** - * 消息方法常量 - */ - public static class MessageMethod { - - public static final String PROPERTY_POST = "thing.property.post"; // 数据上报 - public static final String STATE_ONLINE = "thing.state.online"; // 心跳 - - } - - // ==================== 自定义异常 ==================== - - // TODO @haohao:全局异常搞个。看着可以服用哈。 - /** - * TCP 编解码异常 - */ - public static class TcpCodecException extends RuntimeException { - public TcpCodecException(String message, Throwable cause) { - super(message, cause); - } - } - } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java index 1bbda950b7..e4ff2f50bc 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java @@ -14,12 +14,13 @@ import org.springframework.stereotype.Component; * * 采用纯 JSON 格式传输,格式如下: * { - * "id": "消息 ID", - * "method": "消息方法", - * "deviceId": "设备 ID", - * "params": {...}, - * "timestamp": 时间戳 - * // TODO @haohao:貌似少了 code、msg、timestamp + * "id": "消息 ID", + * "method": "消息方法", + * "params": {...}, // 请求参数 + * "data": {...}, // 响应结果 + * "code": 200, // 响应错误码 + * "msg": "success", // 响应提示 + * "timestamp": 时间戳 * } * * @author 芋道源码 @@ -44,12 +45,6 @@ public class IotTcpJsonDeviceMessageCodec implements IotDeviceMessageCodec { */ private String method; - // TODO @haohao:这个字段,是不是没用到呀?感觉应该也不在消息列哈? - /** - * 设备 ID - */ - private Long deviceId; - /** * 请求参数 */ @@ -84,9 +79,13 @@ public class IotTcpJsonDeviceMessageCodec implements IotDeviceMessageCodec { @Override public byte[] encode(IotDeviceMessage message) { - TcpJsonMessage tcpJsonMessage = new TcpJsonMessage(message.getRequestId(), message.getMethod(), - message.getDeviceId(), - message.getParams(), message.getData(), message.getCode(), message.getMsg(), + TcpJsonMessage tcpJsonMessage = new TcpJsonMessage( + message.getRequestId(), + message.getMethod(), + message.getParams(), + message.getData(), + message.getCode(), + message.getMsg(), System.currentTimeMillis()); return JsonUtils.toJsonByte(tcpJsonMessage); } @@ -97,11 +96,13 @@ public class IotTcpJsonDeviceMessageCodec implements IotDeviceMessageCodec { TcpJsonMessage tcpJsonMessage = JsonUtils.parseObject(bytes, TcpJsonMessage.class); Assert.notNull(tcpJsonMessage, "消息不能为空"); Assert.notBlank(tcpJsonMessage.getMethod(), "消息方法不能为空"); - // TODO @haohao:这个我已经改了哈。一些属性,可以放在一行,好理解一点~ - IotDeviceMessage iotDeviceMessage = IotDeviceMessage.of(tcpJsonMessage.getId(), tcpJsonMessage.getMethod(), - tcpJsonMessage.getParams(), tcpJsonMessage.getData(), tcpJsonMessage.getCode(), tcpJsonMessage.getMsg()); - iotDeviceMessage.setDeviceId(tcpJsonMessage.getDeviceId()); - return iotDeviceMessage; + return IotDeviceMessage.of( + tcpJsonMessage.getId(), + tcpJsonMessage.getMethod(), + tcpJsonMessage.getParams(), + tcpJsonMessage.getData(), + tcpJsonMessage.getCode(), + tcpJsonMessage.getMsg()); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java index 72fc0eef50..51af9bd3ce 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java @@ -8,7 +8,7 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpDownstreamSubscr import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpSessionManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import io.vertx.core.Vertx; @@ -92,19 +92,19 @@ public class IotGatewayConfiguration { public IotTcpUpstreamProtocol iotTcpUpstreamProtocol(IotGatewayProperties gatewayProperties, IotDeviceService deviceService, IotDeviceMessageService messageService, - IotTcpSessionManager sessionManager, + IotTcpConnectionManager connectionManager, Vertx tcpVertx) { return new IotTcpUpstreamProtocol(gatewayProperties.getProtocol().getTcp(), - deviceService, messageService, sessionManager, tcpVertx); + deviceService, messageService, connectionManager, tcpVertx); } @Bean public IotTcpDownstreamSubscriber iotTcpDownstreamSubscriber(IotTcpUpstreamProtocol protocolHandler, IotDeviceMessageService messageService, IotDeviceService deviceService, - IotTcpSessionManager sessionManager, + IotTcpConnectionManager connectionManager, IotMessageBus messageBus) { - return new IotTcpDownstreamSubscriber(protocolHandler, messageService, deviceService, sessionManager, + return new IotTcpDownstreamSubscriber(protocolHandler, messageService, deviceService, connectionManager, messageBus); } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java index 6130caa851..e4d46b3af6 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java @@ -4,13 +4,13 @@ import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpSessionManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpDownstreamHandler; import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; /** * IoT 网关 TCP 下游订阅者:接收下行给设备的消息 @@ -18,37 +18,28 @@ import org.springframework.stereotype.Component; * @author 芋道源码 */ @Slf4j -@Component +@RequiredArgsConstructor public class IotTcpDownstreamSubscriber implements IotMessageSubscriber { - private final IotTcpDownstreamHandler downstreamHandler; - - private final IotMessageBus messageBus; - private final IotTcpUpstreamProtocol protocol; - // todo @haohao:不用的变量,可以去掉哈 + private final IotDeviceMessageService messageService; + private final IotDeviceService deviceService; - private final IotTcpSessionManager sessionManager; + private final IotTcpConnectionManager connectionManager; - // TODO @haohao:lombok 简化 - public IotTcpDownstreamSubscriber(IotTcpUpstreamProtocol protocol, - IotDeviceMessageService messageService, - IotDeviceService deviceService, - IotTcpSessionManager sessionManager, - IotMessageBus messageBus) { - this.protocol = protocol; - this.messageBus = messageBus; - this.deviceService = deviceService; - this.sessionManager = sessionManager; - this.downstreamHandler = new IotTcpDownstreamHandler(messageService, deviceService, sessionManager); - } + private final IotMessageBus messageBus; + + private IotTcpDownstreamHandler downstreamHandler; @PostConstruct public void init() { + // 初始化下游处理器 + this.downstreamHandler = new IotTcpDownstreamHandler(messageService, deviceService, connectionManager); + messageBus.register(this); - log.info("[init][TCP 下游订阅者初始化完成] 服务器 ID: {}, Topic: {}", + log.info("[init][TCP 下游订阅者初始化完成,服务器 ID: {},Topic: {}]", protocol.getServerId(), getTopic()); } @@ -68,8 +59,9 @@ public class IotTcpDownstreamSubscriber implements IotMessageSubscriber { + tcpServer = vertx.createNetServer(options); + tcpServer.connectHandler(socket -> { IotTcpUpstreamHandler handler = new IotTcpUpstreamHandler(this, messageService, deviceService, - sessionManager); + connectionManager); handler.handle(socket); }); // 启动服务器 try { - netServer.listen().result(); + tcpServer.listen().result(); log.info("[start][IoT 网关 TCP 协议启动成功,端口:{}]", tcpProperties.getPort()); } catch (Exception e) { log.error("[start][IoT 网关 TCP 协议启动失败]", e); @@ -87,9 +87,9 @@ public class IotTcpUpstreamProtocol { @PreDestroy public void stop() { - if (netServer != null) { + if (tcpServer != null) { try { - netServer.close().result(); + tcpServer.close().result(); log.info("[stop][IoT 网关 TCP 协议已停止]"); } catch (Exception e) { log.error("[stop][IoT 网关 TCP 协议停止失败]", e); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpAuthManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpAuthManager.java deleted file mode 100644 index 8a67e587a8..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpAuthManager.java +++ /dev/null @@ -1,202 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager; - -import io.vertx.core.net.NetSocket; -import lombok.Data; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -/** - * IoT 网关 TCP 认证信息管理器 - *

- * 维护 TCP 连接的认证状态,支持认证信息的存储、查询和清理 - * - * @author 芋道源码 - */ -@Slf4j -@Component -public class IotTcpAuthManager { - - /** - * 连接认证状态映射:NetSocket -> 认证信息 - */ - private final Map authStatusMap = new ConcurrentHashMap<>(); - - // TODO @haohao:得考虑,一个设备连接多次? - /** - * 设备 ID -> NetSocket 的映射(用于快速查找) - */ - private final Map deviceSocketMap = new ConcurrentHashMap<>(); - - /** - * 注册认证信息 - * - * @param socket TCP 连接 - * @param authInfo 认证信息 - */ - public void registerAuth(NetSocket socket, AuthInfo authInfo) { - // 如果设备已有其他连接,先清理旧连接 - // TODO @haohao:是不是允许同时连接?就像 mqtt 应该也允许重复连接哈? - NetSocket oldSocket = deviceSocketMap.get(authInfo.getDeviceId()); - if (oldSocket != null && oldSocket != socket) { - log.info("[registerAuth][设备已有其他连接,清理旧连接] 设备 ID: {}, 旧连接: {}", - authInfo.getDeviceId(), oldSocket.remoteAddress()); - authStatusMap.remove(oldSocket); - } - - // 注册新认证信息 - authStatusMap.put(socket, authInfo); - deviceSocketMap.put(authInfo.getDeviceId(), socket); - - log.info("[registerAuth][注册认证信息] 设备 ID: {}, 连接: {}, productKey: {}, deviceName: {}", - authInfo.getDeviceId(), socket.remoteAddress(), authInfo.getProductKey(), authInfo.getDeviceName()); - } - - /** - * 注销认证信息 - * - * @param socket TCP 连接 - */ - public void unregisterAuth(NetSocket socket) { - AuthInfo authInfo = authStatusMap.remove(socket); - if (authInfo != null) { - deviceSocketMap.remove(authInfo.getDeviceId()); - log.info("[unregisterAuth][注销认证信息] 设备 ID: {}, 连接: {}", - authInfo.getDeviceId(), socket.remoteAddress()); - } - } - - // TODO @haohao:建议暂时没用的方法,可以删除掉;整体聚焦! - /** - * 注销设备认证信息 - * - * @param deviceId 设备 ID - */ - public void unregisterAuth(Long deviceId) { - NetSocket socket = deviceSocketMap.remove(deviceId); - if (socket != null) { - AuthInfo authInfo = authStatusMap.remove(socket); - if (authInfo != null) { - log.info("[unregisterAuth][注销设备认证信息] 设备 ID: {}, 连接: {}", - deviceId, socket.remoteAddress()); - } - } - } - - /** - * 获取认证信息 - * - * @param socket TCP 连接 - * @return 认证信息,如果未认证则返回 null - */ - public AuthInfo getAuthInfo(NetSocket socket) { - return authStatusMap.get(socket); - } - - /** - * 获取设备的认证信息 - * - * @param deviceId 设备 ID - * @return 认证信息,如果设备未认证则返回 null - */ - public AuthInfo getAuthInfo(Long deviceId) { - NetSocket socket = deviceSocketMap.get(deviceId); - return socket != null ? authStatusMap.get(socket) : null; - } - - /** - * 检查连接是否已认证 - * - * @param socket TCP 连接 - * @return 是否已认证 - */ - public boolean isAuthenticated(NetSocket socket) { - return authStatusMap.containsKey(socket); - } - - /** - * 检查设备是否已认证 - * - * @param deviceId 设备 ID - * @return 是否已认证 - */ - public boolean isAuthenticated(Long deviceId) { - return deviceSocketMap.containsKey(deviceId); - } - - /** - * 获取设备的 TCP 连接 - * - * @param deviceId 设备 ID - * @return TCP 连接,如果设备未认证则返回 null - */ - public NetSocket getDeviceSocket(Long deviceId) { - return deviceSocketMap.get(deviceId); - } - - /** - * 获取当前已认证设备数量 - * - * @return 已认证设备数量 - */ - public int getAuthenticatedDeviceCount() { - return deviceSocketMap.size(); - } - - /** - * 获取所有已认证设备 ID - * - * @return 已认证设备 ID 集合 - */ - public java.util.Set getAuthenticatedDeviceIds() { - return deviceSocketMap.keySet(); - } - - /** - * 清理所有认证信息 - */ - public void clearAll() { - int count = authStatusMap.size(); - authStatusMap.clear(); - deviceSocketMap.clear(); - // TODO @haohao:第一个括号是方法,第二个括号是明细日志;其它日志,也可以检查下哈。 - log.info("[clearAll][清理所有认证信息] 清理数量: {}", count); - } - - /** - * 认证信息 - */ - @Data - public static class AuthInfo { - - /** - * 设备编号 - */ - private Long deviceId; - - /** - * 产品标识 - */ - private String productKey; - - /** - * 设备名称 - */ - private String deviceName; - - // TODO @haohao:令牌不要存储,万一有安全问题哈; - /** - * 认证令牌 - */ - private String token; - - /** - * 客户端 ID - */ - private String clientId; - - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java new file mode 100644 index 0000000000..3ab7470005 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java @@ -0,0 +1,185 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager; + +import io.vertx.core.net.NetSocket; +import lombok.Data; +import lombok.experimental.Accessors; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * IoT 网关 TCP 连接管理器 + *

+ * 统一管理 TCP 连接的认证状态、设备会话和消息发送功能: + * 1. 管理 TCP 连接的认证状态 + * 2. 管理设备会话和在线状态 + * 3. 管理消息发送到设备 + * + * @author 芋道源码 + */ +@Slf4j +@Component +public class IotTcpConnectionManager { + + /** + * 连接信息映射:NetSocket -> 连接信息 + */ + private final Map connectionMap = new ConcurrentHashMap<>(); + + /** + * 设备 ID -> NetSocket 的映射(用于快速查找) + */ + private final Map deviceSocketMap = new ConcurrentHashMap<>(); + + /** + * NetSocket -> 设备 ID 的映射(用于连接断开时清理) + */ + private final Map socketDeviceMap = new ConcurrentHashMap<>(); + + /** + * 注册设备连接(包含认证信息) + * + * @param socket TCP 连接 + * @param deviceId 设备 ID + * @param authInfo 认证信息 + */ + public void registerConnection(NetSocket socket, Long deviceId, AuthInfo authInfo) { + // 如果设备已有其他连接,先清理旧连接 + NetSocket oldSocket = deviceSocketMap.get(deviceId); + if (oldSocket != null && oldSocket != socket) { + log.info("[registerConnection][设备已有其他连接,断开旧连接,设备 ID: {},旧连接: {}]", + deviceId, oldSocket.remoteAddress()); + oldSocket.close(); + // 清理所有相关映射 + connectionMap.remove(oldSocket); + socketDeviceMap.remove(oldSocket); + } + + // 注册新连接 - 更新所有映射关系 + ConnectionInfo connectionInfo = new ConnectionInfo() + .setDeviceId(deviceId) + .setAuthInfo(authInfo) + .setAuthenticated(true); + + connectionMap.put(socket, connectionInfo); + deviceSocketMap.put(deviceId, socket); + socketDeviceMap.put(socket, deviceId); + + log.info("[registerConnection][注册设备连接,设备 ID: {},连接: {},product key: {},device name: {}]", + deviceId, socket.remoteAddress(), authInfo.getProductKey(), authInfo.getDeviceName()); + } + + /** + * 注销设备连接 + * + * @param socket TCP 连接 + */ + public void unregisterConnection(NetSocket socket) { + ConnectionInfo connectionInfo = connectionMap.remove(socket); + Long deviceId = socketDeviceMap.remove(socket); + + if (connectionInfo != null && deviceId != null) { + deviceSocketMap.remove(deviceId); + log.info("[unregisterConnection][注销设备连接,设备 ID: {},连接: {}]", + deviceId, socket.remoteAddress()); + } + } + + /** + * 注销设备连接(通过设备 ID) + * + * @param deviceId 设备 ID + */ + public void unregisterConnection(Long deviceId) { + NetSocket socket = deviceSocketMap.remove(deviceId); + if (socket != null) { + connectionMap.remove(socket); + socketDeviceMap.remove(socket); + log.info("[unregisterConnection][注销设备连接,设备 ID: {},连接: {}]", deviceId, socket.remoteAddress()); + } + } + + /** + * 检查连接是否已认证 + */ + public boolean isAuthenticated(NetSocket socket) { + ConnectionInfo info = connectionMap.get(socket); + return info != null && info.isAuthenticated(); + } + + /** + * 检查连接是否未认证 + */ + public boolean isNotAuthenticated(NetSocket socket) { + return !isAuthenticated(socket); + } + + /** + * 获取连接的认证信息 + */ + public AuthInfo getAuthInfo(NetSocket socket) { + ConnectionInfo info = connectionMap.get(socket); + return info != null ? info.getAuthInfo() : null; + } + + /** + * 检查设备是否在线 + */ + public boolean isDeviceOnline(Long deviceId) { + return deviceSocketMap.containsKey(deviceId); + } + + /** + * 检查设备是否离线 + */ + public boolean isDeviceOffline(Long deviceId) { + return !isDeviceOnline(deviceId); + } + + /** + * 发送消息到设备 + */ + public boolean sendToDevice(Long deviceId, byte[] data) { + NetSocket socket = deviceSocketMap.get(deviceId); + if (socket == null) { + log.warn("[sendToDevice][设备未连接,设备 ID: {}]", deviceId); + return false; + } + + try { + socket.write(io.vertx.core.buffer.Buffer.buffer(data)); + log.debug("[sendToDevice][发送消息成功,设备 ID: {},数据长度: {} 字节]", deviceId, data.length); + return true; + } catch (Exception e) { + log.error("[sendToDevice][发送消息失败,设备 ID: {}]", deviceId, e); + // 发送失败时清理连接 + unregisterConnection(socket); + return false; + } + } + + /** + * 连接信息 + */ + @Data + @Accessors(chain = true) + public static class ConnectionInfo { + private Long deviceId; + private AuthInfo authInfo; + private boolean authenticated; + } + + /** + * 认证信息 + */ + @Data + @Accessors(chain = true) + public static class AuthInfo { + private Long deviceId; + private String productKey; + private String deviceName; + private String clientId; + } +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpSessionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpSessionManager.java deleted file mode 100644 index 00685e5cf6..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpSessionManager.java +++ /dev/null @@ -1,144 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager; - -import io.vertx.core.net.NetSocket; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -// TODO @haohao:IotTcpSessionManager、IotTcpAuthManager 是不是融合哈? -/** - * IoT 网关 TCP 会话管理器 - *

- * 维护设备 ID 和 TCP 连接的映射关系,支持下行消息发送 - * - * @author 芋道源码 - */ -@Slf4j -@Component -public class IotTcpSessionManager { - - /** - * 设备 ID -> TCP 连接的映射 - */ - private final Map deviceSocketMap = new ConcurrentHashMap<>(); - - /** - * TCP 连接 -> 设备 ID 的映射(用于连接断开时清理) - */ - private final Map socketDeviceMap = new ConcurrentHashMap<>(); - - /** - * 注册设备会话 - * - * @param deviceId 设备 ID - * @param socket TCP 连接 - */ - public void registerSession(Long deviceId, NetSocket socket) { - // 如果设备已有连接,先断开旧连接 - NetSocket oldSocket = deviceSocketMap.get(deviceId); - if (oldSocket != null && oldSocket != socket) { - log.info("[registerSession][设备已有连接,断开旧连接] 设备 ID: {}, 旧连接: {}", deviceId, oldSocket.remoteAddress()); - oldSocket.close(); - socketDeviceMap.remove(oldSocket); - } - - // 注册新连接 - deviceSocketMap.put(deviceId, socket); - socketDeviceMap.put(socket, deviceId); - - log.info("[registerSession][注册设备会话] 设备 ID: {}, 连接: {}", deviceId, socket.remoteAddress()); - } - - /** - * 注销设备会话 - * - * @param deviceId 设备 ID - */ - public void unregisterSession(Long deviceId) { - NetSocket socket = deviceSocketMap.remove(deviceId); - if (socket != null) { - socketDeviceMap.remove(socket); - log.info("[unregisterSession][注销设备会话] 设备 ID: {}, 连接: {}", deviceId, socket.remoteAddress()); - } - } - - /** - * 注销 TCP 连接会话 - * - * @param socket TCP 连接 - */ - public void unregisterSession(NetSocket socket) { - Long deviceId = socketDeviceMap.remove(socket); - if (deviceId != null) { - deviceSocketMap.remove(deviceId); - log.info("[unregisterSession][注销连接会话] 设备 ID: {}, 连接: {}", deviceId, socket.remoteAddress()); - } - } - - /** - * 获取设备的 TCP 连接 - * - * @param deviceId 设备 ID - * @return TCP 连接,如果设备未连接则返回 null - */ - public NetSocket getDeviceSocket(Long deviceId) { - return deviceSocketMap.get(deviceId); - } - - /** - * 检查设备是否在线 - * - * @param deviceId 设备 ID - * @return 是否在线 - */ - public boolean isDeviceOnline(Long deviceId) { - NetSocket socket = deviceSocketMap.get(deviceId); - return socket != null; - } - - /** - * 发送消息到设备 - * - * @param deviceId 设备 ID - * @param data 消息数据 - * @return 是否发送成功 - */ - public boolean sendToDevice(Long deviceId, byte[] data) { - NetSocket socket = deviceSocketMap.get(deviceId); - if (socket == null) { - log.warn("[sendToDevice][设备未连接] 设备 ID: {}", deviceId); - return false; - } - - try { - socket.write(io.vertx.core.buffer.Buffer.buffer(data)); - log.debug("[sendToDevice][发送消息成功] 设备 ID: {}, 数据长度: {} 字节", deviceId, data.length); - return true; - } catch (Exception e) { - log.error("[sendToDevice][发送消息失败] 设备 ID: {}", deviceId, e); - // 发送失败时清理连接 - unregisterSession(deviceId); - return false; - } - } - - /** - * 获取当前在线设备数量 - * - * @return 在线设备数量 - */ - public int getOnlineDeviceCount() { - return deviceSocketMap.size(); - } - - /** - * 获取所有在线设备 ID - * - * @return 在线设备 ID 集合 - */ - public java.util.Set getOnlineDeviceIds() { - return deviceSocketMap.keySet(); - } -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java index 05970ede13..fd352f3b44 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java @@ -2,9 +2,10 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpSessionManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; /** @@ -13,62 +14,50 @@ import lombok.extern.slf4j.Slf4j; * @author 芋道源码 */ @Slf4j +@RequiredArgsConstructor public class IotTcpDownstreamHandler { - private final IotDeviceMessageService messageService; + private final IotDeviceMessageService deviceMessageService; private final IotDeviceService deviceService; - private final IotTcpSessionManager sessionManager; - - // TODO @haohao:这个可以使用 lombok 简化构造方法 - public IotTcpDownstreamHandler(IotDeviceMessageService messageService, - IotDeviceService deviceService, IotTcpSessionManager sessionManager) { - this.messageService = messageService; - this.deviceService = deviceService; - this.sessionManager = sessionManager; - } + private final IotTcpConnectionManager connectionManager; /** * 处理下行消息 - * - * @param message 设备消息 */ public void handle(IotDeviceMessage message) { try { - log.info("[handle][处理下行消息] 设备 ID: {}, 方法: {}, 消息 ID: {}", + log.info("[handle][处理下行消息,设备 ID: {},方法: {},消息 ID: {}]", message.getDeviceId(), message.getMethod(), message.getId()); - // TODO @haohao 1. 和 2. 可以合成 1.1 1.2 并且中间可以不空行; - // 1. 获取设备信息 - IotDeviceRespDTO device = deviceService.getDeviceFromCache(message.getDeviceId()); - if (device == null) { - log.error("[handle][设备不存在] 设备 ID: {}", message.getDeviceId()); + // 1.1 获取设备信息 + IotDeviceRespDTO deviceInfo = deviceService.getDeviceFromCache(message.getDeviceId()); + if (deviceInfo == null) { + log.error("[handle][设备不存在,设备 ID: {}]", message.getDeviceId()); + return; + } + // 1.2 检查设备是否在线 + if (connectionManager.isDeviceOffline(message.getDeviceId())) { + log.warn("[handle][设备不在线,设备 ID: {}]", message.getDeviceId()); return; } - // 2. 检查设备是否在线 - if (!sessionManager.isDeviceOnline(message.getDeviceId())) { - log.warn("[handle][设备不在线] 设备 ID: {}", message.getDeviceId()); - return; - } + // 2. 根据产品 Key 和设备名称编码消息并发送到设备 + byte[] bytes = deviceMessageService.encodeDeviceMessage(message, deviceInfo.getProductKey(), + deviceInfo.getDeviceName()); + boolean success = connectionManager.sendToDevice(message.getDeviceId(), bytes); - // 3. 编码消息 - byte[] bytes = messageService.encodeDeviceMessage(message, device.getCodecType()); - - // 4. 发送消息到设备 - boolean success = sessionManager.sendToDevice(message.getDeviceId(), bytes); if (success) { - log.info("[handle][下行消息发送成功] 设备 ID: {}, 方法: {}, 消息 ID: {}, 数据长度: {} 字节", + log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]", message.getDeviceId(), message.getMethod(), message.getId(), bytes.length); } else { - log.error("[handle][下行消息发送失败] 设备 ID: {}, 方法: {}, 消息 ID: {}", + log.error("[handle][下行消息发送失败,设备 ID: {},方法: {},消息 ID: {}]", message.getDeviceId(), message.getMethod(), message.getId()); - } // TODO @haohao:下面这个空行,可以考虑去掉的哈。 - + } } catch (Exception e) { - log.error("[handle][处理下行消息失败] 设备 ID: {}, 方法: {}, 消息内容: {}", - message.getDeviceId(), message.getMethod(), message.getParams(), e); + log.error("[handle][处理下行消息失败,设备 ID: {},方法: {},消息内容: {}]", + message.getDeviceId(), message.getMethod(), message, e); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java index 6acc235569..29cda53228 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java @@ -12,50 +12,48 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec; +import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpAuthManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpSessionManager; -import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import io.vertx.core.Handler; import io.vertx.core.buffer.Buffer; import io.vertx.core.net.NetSocket; +import lombok.AllArgsConstructor; +import lombok.Data; import lombok.extern.slf4j.Slf4j; +import java.nio.charset.StandardCharsets; + /** * TCP 上行消息处理器 + * + * @author 芋道源码 */ @Slf4j public class IotTcpUpstreamHandler implements Handler { - // TODO @haohao:这两个变量,可以复用 IotTcpBinaryDeviceMessageCodec 的 TYPE - private static final String CODEC_TYPE_JSON = "TCP_JSON"; - private static final String CODEC_TYPE_BINARY = "TCP_BINARY"; - + private static final String CODEC_TYPE_JSON = IotTcpJsonDeviceMessageCodec.TYPE; + private static final String CODEC_TYPE_BINARY = IotTcpBinaryDeviceMessageCodec.TYPE; private static final String AUTH_METHOD = "auth"; private final IotDeviceMessageService deviceMessageService; private final IotDeviceService deviceService; - private final IotTcpSessionManager sessionManager; - - private final IotTcpAuthManager authManager; - - private final IotDeviceTokenService deviceTokenService; + private final IotTcpConnectionManager connectionManager; private final IotDeviceCommonApi deviceApi; private final String serverId; public IotTcpUpstreamHandler(IotTcpUpstreamProtocol protocol, IotDeviceMessageService deviceMessageService, - IotDeviceService deviceService, IotTcpSessionManager sessionManager) { + IotDeviceService deviceService, IotTcpConnectionManager connectionManager) { this.deviceMessageService = deviceMessageService; this.deviceService = deviceService; - this.sessionManager = sessionManager; - this.authManager = SpringUtil.getBean(IotTcpAuthManager.class); - this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class); + this.connectionManager = connectionManager; this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); this.serverId = protocol.getServerId(); } @@ -63,207 +61,313 @@ public class IotTcpUpstreamHandler implements Handler { @Override public void handle(NetSocket socket) { String clientId = IdUtil.simpleUUID(); - log.info("[handle][收到设备连接] 客户端 ID: {}, 地址: {}", clientId, socket.remoteAddress()); + log.debug("[handle][设备连接,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress()); // 设置异常和关闭处理器 socket.exceptionHandler(ex -> { - log.error("[handle][连接异常] 客户端 ID: {}, 地址: {}", clientId, socket.remoteAddress(), ex); - cleanupSession(socket); + log.warn("[handle][连接异常,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress()); + cleanupConnection(socket); }); socket.closeHandler(v -> { - log.info("[handle][连接关闭] 客户端 ID: {}, 地址: {}", clientId, socket.remoteAddress()); - cleanupSession(socket); + log.debug("[handle][连接关闭,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress()); + cleanupConnection(socket); }); - socket.handler(buffer -> handleDataPackage(clientId, buffer, socket)); + socket.handler(buffer -> processMessage(clientId, buffer, socket)); } - private void handleDataPackage(String clientId, Buffer buffer, NetSocket socket) { + /** + * 处理消息 + */ + private void processMessage(String clientId, Buffer buffer, NetSocket socket) { try { + // 1. 数据包基础检查 if (buffer.length() == 0) { - log.warn("[handleDataPackage][数据包为空] 客户端 ID: {}", clientId); return; } - // 1. 解码消息 + // 2. 解码消息 MessageInfo messageInfo = decodeMessage(buffer); if (messageInfo == null) { return; } - // TODO @haohao:2. 和 3. 可以合并成 2.1 2.2 ,都是异常的情况。然后 3. 可以 return 直接; - // 2. 获取设备信息 - IotDeviceRespDTO device = deviceService.getDeviceFromCache(messageInfo.message.getDeviceId()); - if (device == null) { - sendError(socket, messageInfo.message.getRequestId(), "设备不存在", messageInfo.codecType); - return; + // 3. 根据消息类型路由处理 + if (isAuthRequest(messageInfo.message)) { + // 认证请求:无需检查认证状态 + handleAuthenticationRequest(clientId, messageInfo, socket); + } else { + // 业务消息:需要检查认证状态 + handleBusinessRequest(clientId, messageInfo, socket); } - // 3. 处理消息 - if (!authManager.isAuthenticated(socket)) { - handleAuthRequest(clientId, messageInfo.message, socket, messageInfo.codecType); - } else { - IotTcpAuthManager.AuthInfo authInfo = authManager.getAuthInfo(socket); - handleBusinessMessage(clientId, messageInfo.message, authInfo); - } } catch (Exception e) { - log.error("[handleDataPackage][处理数据包失败] 客户端 ID: {}, 错误: {}", clientId, e.getMessage(), e); + log.error("[processMessage][处理消息失败,客户端 ID: {}]", clientId, e); } } /** * 处理认证请求 */ - private void handleAuthRequest(String clientId, IotDeviceMessage message, NetSocket socket, String codecType) { + private void handleAuthenticationRequest(String clientId, MessageInfo messageInfo, NetSocket socket) { try { - // 1. 验证认证请求 - // TODO @haohao:ObjUtil.notEquals。减少取反 - if (!AUTH_METHOD.equals(message.getMethod())) { - sendError(socket, message.getRequestId(), "请先进行认证", codecType); - return; - } + IotDeviceMessage message = messageInfo.message; - // 2. 解析认证参数 // TODO @haohao:1. 和 2. 可以合并成 1.1 1.2 都是参数校验 + // 1. 解析认证参数 AuthParams authParams = parseAuthParams(message.getParams()); if (authParams == null) { - sendError(socket, message.getRequestId(), "认证参数不完整", codecType); + sendError(socket, message.getRequestId(), "认证参数不完整", messageInfo.codecType); return; } - // 3. 执行认证流程 - // TODO @haohao:成功失败、都大哥日志,会不会更好哈? - if (performAuthentication(authParams, socket, message.getRequestId(), codecType)) { - log.info("[handleAuthRequest][认证成功] 客户端 ID: {}, username: {}", clientId, authParams.username); + // 2. 执行认证 + if (!authenticateDevice(authParams)) { + log.warn("[handleAuthenticationRequest][认证失败,客户端 ID: {},username: {}]", + clientId, authParams.username); + sendError(socket, message.getRequestId(), "认证失败", messageInfo.codecType); + return; } + + // 3. 解析设备信息 + IotDeviceAuthUtils.DeviceInfo deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.username); + if (deviceInfo == null) { + sendError(socket, message.getRequestId(), "解析设备信息失败", messageInfo.codecType); + return; + } + + // 4. 获取设备信息 + IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), + deviceInfo.getDeviceName()); + if (device == null) { + sendError(socket, message.getRequestId(), "设备不存在", messageInfo.codecType); + return; + } + + // 5. 注册连接并发送成功响应 + registerConnection(socket, device, deviceInfo, authParams.clientId); + sendOnlineMessage(deviceInfo); + sendSuccess(socket, message.getRequestId(), "认证成功", messageInfo.codecType); + + log.info("[handleAuthenticationRequest][认证成功,设备 ID: {},设备名: {}]", + device.getId(), deviceInfo.getDeviceName()); + } catch (Exception e) { - log.error("[handleAuthRequest][认证处理异常] 客户端 ID: {}", clientId, e); - sendError(socket, message.getRequestId(), "认证处理异常: " + e.getMessage(), codecType); + log.error("[handleAuthenticationRequest][认证处理异常,客户端 ID: {}]", clientId, e); + sendError(socket, messageInfo.message.getRequestId(), "认证处理异常", messageInfo.codecType); + } + } + + /** + * 处理业务请求 + */ + private void handleBusinessRequest(String clientId, MessageInfo messageInfo, NetSocket socket) { + try { + // 1. 检查认证状态 + if (connectionManager.isNotAuthenticated(socket)) { + log.warn("[handleBusinessRequest][设备未认证,客户端 ID: {}]", clientId); + sendError(socket, messageInfo.message.getRequestId(), "请先进行认证", messageInfo.codecType); + return; + } + + // 2. 获取认证信息并处理业务消息 + IotTcpConnectionManager.AuthInfo authInfo = connectionManager.getAuthInfo(socket); + processBusinessMessage(clientId, messageInfo.message, authInfo); + + } catch (Exception e) { + log.error("[handleBusinessRequest][业务请求处理异常,客户端 ID: {}]", clientId, e); } } /** * 处理业务消息 */ - private void handleBusinessMessage(String clientId, IotDeviceMessage message, - IotTcpAuthManager.AuthInfo authInfo) { + private void processBusinessMessage(String clientId, IotDeviceMessage message, + IotTcpConnectionManager.AuthInfo authInfo) { try { message.setDeviceId(authInfo.getDeviceId()); message.setServerId(serverId); - deviceMessageService.sendDeviceMessage(message, authInfo.getProductKey(), authInfo.getDeviceName(), - serverId); - log.info("[handleBusinessMessage][业务消息处理完成] 客户端 ID: {}, 消息 ID: {}, 设备 ID: {}, 方法: {}", - clientId, message.getId(), message.getDeviceId(), message.getMethod()); + // 发送到消息总线 + deviceMessageService.sendDeviceMessage(message, authInfo.getProductKey(), + authInfo.getDeviceName(), serverId); + } catch (Exception e) { - log.error("[handleBusinessMessage][处理业务消息失败] 客户端 ID: {}, 错误: {}", clientId, e.getMessage(), e); + log.error("[processBusinessMessage][业务消息处理失败,客户端 ID: {},消息 ID: {}]", + clientId, message.getId(), e); } } /** * 解码消息 */ - // TODO @haohao:是不是还是直接管理后台配置协议,然后直接使用就好啦。暂时不考虑动态解析哈。保持一致,降低理解成本哈。 private MessageInfo decodeMessage(Buffer buffer) { + if (buffer == null || buffer.length() == 0) { + return null; + } + + // 1. 快速检测消息格式类型 + String codecType = detectMessageFormat(buffer); + try { - String rawData = buffer.toString(); - String codecType = isJsonFormat(rawData) ? CODEC_TYPE_JSON : CODEC_TYPE_BINARY; + // 2. 使用检测到的格式进行解码 IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(buffer.getBytes(), codecType); - return message != null ? new MessageInfo(message, codecType) : null; + + if (message == null) { + return null; + } + + return new MessageInfo(message, codecType); + } catch (Exception e) { - log.debug("[decodeMessage][消息解码失败] 错误: {}", e.getMessage()); + log.warn("[decodeMessage][消息解码失败,格式: {},数据长度: {},错误: {}]", + codecType, buffer.length(), e.getMessage()); return null; } } /** - * 执行认证 + * 检测消息格式类型 + * 优化性能:避免不必要的字符串转换 */ - // TODO @haohao:下面的 1. 2. 可以合并下,本质也是校验哈。 - private boolean performAuthentication(AuthParams authParams, NetSocket socket, String requestId, String codecType) { - // 1. 执行认证 - if (!authenticateDevice(authParams)) { - sendError(socket, requestId, "认证失败", codecType); - return false; + private String detectMessageFormat(Buffer buffer) { + if (buffer.length() == 0) { + return CODEC_TYPE_JSON; // 默认使用 JSON } - // 2. 获取设备信息 - IotDeviceAuthUtils.DeviceInfo deviceInfo = deviceTokenService.parseUsername(authParams.username); - if (deviceInfo == null) { - sendError(socket, requestId, "解析设备信息失败", codecType); - return false; + // 1. 优先检测二进制格式(检查魔术字节 0x7E) + if (isBinaryFormat(buffer)) { + return CODEC_TYPE_BINARY; } - IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), - deviceInfo.getDeviceName()); - if (device == null) { - sendError(socket, requestId, "设备不存在", codecType); - return false; + // 2. 检测 JSON 格式(检查前几个有效字符) + if (isJsonFormat(buffer)) { + return CODEC_TYPE_JSON; } - // 3. 注册认证信息 - String token = deviceTokenService.createToken(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); - registerAuthInfo(socket, device, deviceInfo, token, authParams.clientId); - - // 4. 发送上线消息和成功响应 - IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); - deviceMessageService.sendDeviceMessage(onlineMessage, deviceInfo.getProductKey(), deviceInfo.getDeviceName(), - serverId); - sendSuccess(socket, requestId, "认证成功", codecType); - return true; + // 3. 默认尝试 JSON 格式 + return CODEC_TYPE_JSON; } /** - * 发送响应 + * 检测二进制格式 + * 通过检查魔术字节快速识别,避免完整字符串转换 + */ + private boolean isBinaryFormat(Buffer buffer) { + // 二进制协议最小长度检查 + if (buffer.length() < 8) { + return false; + } + + try { + // 检查魔术字节 0x7E(二进制协议的第一个字节) + byte firstByte = buffer.getByte(0); + return firstByte == (byte) 0x7E; + } catch (Exception e) { + return false; + } + } + + /** + * 检测 JSON 格式 + * 只检查前几个有效字符,避免完整字符串转换 + */ + private boolean isJsonFormat(Buffer buffer) { + try { + // 检查前 64 个字节或整个缓冲区(取较小值) + int checkLength = Math.min(buffer.length(), 64); + String prefix = buffer.getString(0, checkLength, StandardCharsets.UTF_8.name()); + + if (StrUtil.isBlank(prefix)) { + return false; + } + + String trimmed = prefix.trim(); + // JSON 格式必须以 { 或 [ 开头 + return trimmed.startsWith("{") || trimmed.startsWith("["); + + } catch (Exception e) { + return false; + } + } + + /** + * 注册连接信息 + */ + private void registerConnection(NetSocket socket, IotDeviceRespDTO device, + IotDeviceAuthUtils.DeviceInfo deviceInfo, String clientId) { + // 创建认证信息 + IotTcpConnectionManager.AuthInfo authInfo = new IotTcpConnectionManager.AuthInfo() + .setDeviceId(device.getId()) + .setProductKey(deviceInfo.getProductKey()) + .setDeviceName(deviceInfo.getDeviceName()) + .setClientId(clientId); + + // 注册连接 + connectionManager.registerConnection(socket, device.getId(), authInfo); + } + + /** + * 发送设备上线消息 + */ + private void sendOnlineMessage(IotDeviceAuthUtils.DeviceInfo deviceInfo) { + try { + IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); + deviceMessageService.sendDeviceMessage(onlineMessage, deviceInfo.getProductKey(), + deviceInfo.getDeviceName(), serverId); + } catch (Exception e) { + log.error("[sendOnlineMessage][发送上线消息失败,设备: {}]", deviceInfo.getDeviceName(), e); + } + } + + /** + * 清理连接 + */ + private void cleanupConnection(NetSocket socket) { + try { + // 发送离线消息(如果已认证) + IotTcpConnectionManager.AuthInfo authInfo = connectionManager.getAuthInfo(socket); + if (authInfo != null) { + IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); + deviceMessageService.sendDeviceMessage(offlineMessage, authInfo.getProductKey(), + authInfo.getDeviceName(), serverId); + } + + // 注销连接 + connectionManager.unregisterConnection(socket); + } catch (Exception e) { + log.error("[cleanupConnection][清理连接失败]", e); + } + } + + /** + * 发送响应消息 */ private void sendResponse(NetSocket socket, boolean success, String message, String requestId, String codecType) { try { - Object responseData = buildResponseData(success, message); + Object responseData = MapUtil.builder() + .put("success", success) + .put("message", message) + .build(); + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, AUTH_METHOD, responseData, success ? 0 : 401, message); + byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType); socket.write(Buffer.buffer(encodedData)); - log.debug("[sendResponse][发送响应] success: {}, message: {}, requestId: {}", success, message, requestId); + } catch (Exception e) { - log.error("[sendResponse][发送响应失败] requestId: {}", requestId, e); + log.error("[sendResponse][发送响应失败,requestId: {}]", requestId, e); } } - /** - * 构建响应数据(不返回 token) - */ - private Object buildResponseData(boolean success, String message) { - return MapUtil.builder() - .put("success", success) - .put("message", message) - .build(); - } + // ==================== 辅助方法 ==================== /** - * 清理会话 + * 判断是否为认证请求 */ - private void cleanupSession(NetSocket socket) { - // 如果已认证,发送离线消息 - IotTcpAuthManager.AuthInfo authInfo = authManager.getAuthInfo(socket); - if (authInfo != null) { - // 发送离线消息 - IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); - deviceMessageService.sendDeviceMessage(offlineMessage, authInfo.getProductKey(), authInfo.getDeviceName(), - serverId); - } - sessionManager.unregisterSession(socket); - authManager.unregisterAuth(socket); - } - - /** - * 判断是否为 JSON 格式 - */ - private boolean isJsonFormat(String data) { - if (StrUtil.isBlank(data)) { - return false; - } - String trimmed = data.trim(); - return (trimmed.startsWith("{") && trimmed.endsWith("}")) || (trimmed.startsWith("[") && trimmed.endsWith("]")); + private boolean isAuthRequest(IotDeviceMessage message) { + return AUTH_METHOD.equals(message.getMethod()); } /** @@ -273,38 +377,37 @@ public class IotTcpUpstreamHandler implements Handler { if (params == null) { return null; } - JSONObject paramsJson = params instanceof JSONObject ? (JSONObject) params - : JSONUtil.parseObj(params.toString()); - String clientId = paramsJson.getStr("clientId"); - String username = paramsJson.getStr("username"); - String password = paramsJson.getStr("password"); - return StrUtil.hasBlank(clientId, username, password) ? null : new AuthParams(clientId, username, password); + + try { + JSONObject paramsJson = params instanceof JSONObject ? (JSONObject) params + : JSONUtil.parseObj(params.toString()); + + String clientId = paramsJson.getStr("clientId"); + String username = paramsJson.getStr("username"); + String password = paramsJson.getStr("password"); + + return StrUtil.hasBlank(clientId, username, password) ? null + : new AuthParams(clientId, username, password); + } catch (Exception e) { + log.warn("[parseAuthParams][解析认证参数失败]", e); + return null; + } } /** * 认证设备 */ private boolean authenticateDevice(AuthParams authParams) { - CommonResult result = deviceApi.authDevice(new IotDeviceAuthReqDTO() - .setClientId(authParams.clientId).setUsername(authParams.username).setPassword(authParams.password)); - return result.isSuccess() && result.getData(); - } - - /** - * 注册认证信息 - */ - private void registerAuthInfo(NetSocket socket, IotDeviceRespDTO device, IotDeviceAuthUtils.DeviceInfo deviceInfo, - String token, String clientId) { - // TODO @haohao:可以链式调用; - IotTcpAuthManager.AuthInfo auth = new IotTcpAuthManager.AuthInfo(); - auth.setDeviceId(device.getId()); - auth.setProductKey(deviceInfo.getProductKey()); - auth.setDeviceName(deviceInfo.getDeviceName()); - auth.setToken(token); - auth.setClientId(clientId); - - authManager.registerAuth(socket, auth); - sessionManager.registerSession(device.getId(), socket); + try { + CommonResult result = deviceApi.authDevice(new IotDeviceAuthReqDTO() + .setClientId(authParams.clientId) + .setUsername(authParams.username) + .setPassword(authParams.password)); + return result.isSuccess() && Boolean.TRUE.equals(result.getData()); + } catch (Exception e) { + log.error("[authenticateDevice][设备认证异常,username: {}]", authParams.username, e); + return false; + } } /** @@ -315,24 +418,32 @@ public class IotTcpUpstreamHandler implements Handler { } /** - * 发送成功响应(不返回 token) + * 发送成功响应 */ private void sendSuccess(NetSocket socket, String requestId, String message, String codecType) { sendResponse(socket, true, message, requestId, codecType); } - // TODO @haohao:使用 lombok,方便 jdk8 兼容 + // ==================== 内部类 ==================== /** * 认证参数 */ - private record AuthParams(String clientId, String username, String password) { + @Data + @AllArgsConstructor + private static class AuthParams { + private final String clientId; + private final String username; + private final String password; } /** * 消息信息 */ - private record MessageInfo(IotDeviceMessage message, String codecType) { + @Data + @AllArgsConstructor + private static class MessageInfo { + private final IotDeviceMessage message; + private final String codecType; } - } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml index 26376b6669..b306f0588c 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml @@ -4,6 +4,15 @@ spring: profiles: active: local # 默认激活本地开发环境 + # Redis 配置 + data: + redis: + host: 127.0.0.1 # Redis 服务器地址 + port: 6379 # Redis 服务器端口 + database: 0 # Redis 数据库索引 + # password: # Redis 密码,如果有的话 + timeout: 30000ms # 连接超时时间 + --- #################### 消息队列相关 #################### # rocketmq 配置项,对应 RocketMQProperties 配置类 @@ -45,7 +54,7 @@ yudao: # 针对引入的 EMQX 组件的配置 # ==================================== emqx: - enabled: false + enabled: true http-port: 8090 # MQTT HTTP 服务端口 mqtt-host: 127.0.0.1 # MQTT Broker 地址 mqtt-port: 1883 # MQTT Broker 端口 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpBinaryDataPacketExamplesTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpBinaryDataPacketExamplesTest.java deleted file mode 100644 index 2e6fb41acc..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpBinaryDataPacketExamplesTest.java +++ /dev/null @@ -1,241 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.codec.tcp; - -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import lombok.extern.slf4j.Slf4j; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.util.HashMap; -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.*; - -/** - * TCP 二进制格式数据包单元测试 - * - * 测试二进制协议创建和解析 TCP 上报数据包和心跳包 - * - * 二进制协议格式: - * 包头(4 字节) | 功能码(2 字节) | 消息序号(2 字节) | 包体数据(变长) - * - * @author 芋道源码 - */ -@Slf4j -class TcpBinaryDataPacketExamplesTest { - - private IotTcpBinaryDeviceMessageCodec codec; - - @BeforeEach - void setUp() { - codec = new IotTcpBinaryDeviceMessageCodec(); - } - - @Test - void testDataReport() { - log.info("=== 二进制格式数据上报包测试 ==="); - - // 创建传感器数据 - Map sensorData = new HashMap<>(); - sensorData.put("temperature", 25.5); - sensorData.put("humidity", 60.2); - sensorData.put("pressure", 1013.25); - sensorData.put("battery", 85); - - // 创建设备消息 - IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", sensorData); - message.setDeviceId(123456L); - - // 编码 - byte[] packet = codec.encode(message); - log.info("编码后数据包长度: {} 字节", packet.length); - log.info("编码后数据包(HEX): {}", bytesToHex(packet)); - - // 解码验证 - IotDeviceMessage decoded = codec.decode(packet); - log.info("解码后消息 ID: {}", decoded.getId()); - log.info("解码后请求 ID: {}", decoded.getRequestId()); - log.info("解码后方法: {}", decoded.getMethod()); - log.info("解码后设备 ID: {}", decoded.getDeviceId()); - log.info("解码后服务 ID: {}", decoded.getServerId()); - log.info("解码后参数: {}", decoded.getParams()); - - // 断言验证 - assertNotNull(decoded.getId()); - assertEquals("thing.property.post", decoded.getMethod()); - assertNotNull(decoded.getParams()); - assertTrue(decoded.getParams() instanceof Map); - } - - @Test - void testHeartbeat() { - log.info("=== 二进制格式心跳包测试 ==="); - - // 创建心跳消息 - IotDeviceMessage heartbeat = IotDeviceMessage.requestOf("thing.state.online", null); - heartbeat.setDeviceId(123456L); - - // 编码 - byte[] packet = codec.encode(heartbeat); - log.info("心跳包长度: {} 字节", packet.length); - log.info("心跳包(HEX): {}", bytesToHex(packet)); - - // 解码验证 - IotDeviceMessage decoded = codec.decode(packet); - log.info("解码后消息 ID: {}", decoded.getId()); - log.info("解码后请求 ID: {}", decoded.getRequestId()); - log.info("解码后方法: {}", decoded.getMethod()); - log.info("解码后设备 ID: {}", decoded.getDeviceId()); - log.info("解码后服务 ID: {}", decoded.getServerId()); - log.info("解码后参数: {}", decoded.getParams()); - - // 断言验证 - assertNotNull(decoded.getId()); - assertEquals("thing.state.online", decoded.getMethod()); - } - - @Test - void testComplexDataReport() { - log.info("=== 二进制格式复杂数据上报测试 ==="); - - // 创建复杂设备数据 - Map deviceData = new HashMap<>(); - - // 环境数据 - Map environment = new HashMap<>(); - environment.put("temperature", 23.8); - environment.put("humidity", 55.0); - environment.put("co2", 420); - deviceData.put("environment", environment); - - // GPS 数据 - Map location = new HashMap<>(); - location.put("latitude", 39.9042); - location.put("longitude", 116.4074); - location.put("altitude", 43.5); - deviceData.put("location", location); - - // 设备状态 - Map status = new HashMap<>(); - status.put("battery", 78); - status.put("signal", -65); - status.put("online", true); - deviceData.put("status", status); - - // 创建设备消息 - IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", deviceData); - message.setDeviceId(789012L); - - // 编码 - byte[] packet = codec.encode(message); - log.info("复杂数据包长度: {} 字节", packet.length); - log.info("复杂数据包(HEX): {}", bytesToHex(packet)); - - // 解码验证 - IotDeviceMessage decoded = codec.decode(packet); - log.info("解码后消息 ID: {}", decoded.getId()); - log.info("解码后请求 ID: {}", decoded.getRequestId()); - log.info("解码后方法: {}", decoded.getMethod()); - log.info("解码后设备 ID: {}", decoded.getDeviceId()); - log.info("解码后服务 ID: {}", decoded.getServerId()); - log.info("解码后参数: {}", decoded.getParams()); - - // 断言验证 - assertNotNull(decoded.getId()); - assertEquals("thing.property.post", decoded.getMethod()); - assertNotNull(decoded.getParams()); - } - - @Test - void testPacketStructureAnalysis() { - log.info("=== 数据包结构分析测试 ==="); - - // 创建测试数据 - Map sensorData = new HashMap<>(); - sensorData.put("temperature", 25.5); - sensorData.put("humidity", 60.2); - - IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", sensorData); - message.setDeviceId(123456L); - - // 编码 - byte[] packet = codec.encode(message); - - // 分析数据包结构 - analyzePacketStructure(packet); - - // 断言验证 - assertTrue(packet.length >= 8, "数据包长度应该至少为 8 字节"); - } - - // ==================== 内部辅助方法 ==================== - - /** - * 字节数组转十六进制字符串 - * - * @param bytes 字节数组 - * @return 十六进制字符串 - */ - private static String bytesToHex(byte[] bytes) { - StringBuilder result = new StringBuilder(); - for (byte b : bytes) { - result.append(String.format("%02X ", b)); - } - return result.toString().trim(); - } - - /** - * 演示数据包结构分析 - * - * @param packet 数据包 - */ - private static void analyzePacketStructure(byte[] packet) { - if (packet.length < 8) { - log.error("数据包长度不足"); - return; - } - - int index = 0; - - // 解析包头(4 字节) - 后续数据长度 - int totalLength = ((packet[index] & 0xFF) << 24) | - ((packet[index + 1] & 0xFF) << 16) | - ((packet[index + 2] & 0xFF) << 8) | - (packet[index + 3] & 0xFF); - index += 4; - log.info("包头 - 后续数据长度: {} 字节", totalLength); - - // 解析功能码(2 字节) - int functionCode = ((packet[index] & 0xFF) << 8) | (packet[index + 1] & 0xFF); - index += 2; - log.info("功能码: {} ({})", functionCode, getFunctionCodeName(functionCode)); - - // 解析消息序号(2 字节) - int messageId = ((packet[index] & 0xFF) << 8) | (packet[index + 1] & 0xFF); - index += 2; - log.info("消息序号: {}", messageId); - - // 解析包体数据 - if (index < packet.length) { - String payload = new String(packet, index, packet.length - index); - log.info("包体数据: {}", payload); - } - } - - /** - * 获取功能码名称 - * - * @param code 功能码 - * @return 功能码名称 - */ - private static String getFunctionCodeName(int code) { - return switch (code) { - case 10 -> "设备注册"; - case 11 -> "注册回复"; - case 20 -> "心跳请求"; - case 21 -> "心跳回复"; - case 30 -> "消息上行"; - case 40 -> "消息下行"; - default -> "未知功能码"; - }; - } -} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpJsonDataPacketExamplesTest.java b/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpJsonDataPacketExamplesTest.java deleted file mode 100644 index 24258e0de2..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpJsonDataPacketExamplesTest.java +++ /dev/null @@ -1,185 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.codec.tcp; - -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import lombok.extern.slf4j.Slf4j; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.nio.charset.StandardCharsets; -import java.util.HashMap; -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.*; - -/** - * TCP JSON 格式数据包单元测试 - *

- * 测试 JSON 格式的 TCP 消息编解码功能 - * - * @author 芋道源码 - */ -@Slf4j -class TcpJsonDataPacketExamplesTest { - - private IotTcpJsonDeviceMessageCodec codec; - - @BeforeEach - void setUp() { - codec = new IotTcpJsonDeviceMessageCodec(); - } - - @Test - void testDataReport() { - log.info("=== JSON 格式数据上报测试 ==="); - - // 创建传感器数据 - Map sensorData = new HashMap<>(); - sensorData.put("temperature", 25.5); - sensorData.put("humidity", 60.2); - sensorData.put("pressure", 1013.25); - sensorData.put("battery", 85); - - // 创建设备消息 - IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", sensorData); - message.setDeviceId(123456L); - - // 编码 - byte[] packet = codec.encode(message); - String jsonString = new String(packet, StandardCharsets.UTF_8); - log.info("编码后 JSON: {}", jsonString); - log.info("数据包长度: {} 字节", packet.length); - - // 解码验证 - IotDeviceMessage decoded = codec.decode(packet); - log.info("解码后消息 ID: {}", decoded.getId()); - log.info("解码后方法: {}", decoded.getMethod()); - log.info("解码后设备 ID: {}", decoded.getDeviceId()); - log.info("解码后服务 ID: {}", decoded.getServerId()); - log.info("解码后参数: {}", decoded.getParams()); - - // 断言验证 - assertNotNull(decoded.getId()); - assertEquals("thing.property.post", decoded.getMethod()); - assertEquals(123456L, decoded.getDeviceId()); - assertNotNull(decoded.getParams()); - assertTrue(decoded.getParams() instanceof Map); - } - - @Test - void testHeartbeat() { - log.info("=== JSON 格式心跳测试 ==="); - - // 创建心跳消息 - IotDeviceMessage heartbeat = IotDeviceMessage.requestOf("thing.state.online", null); - heartbeat.setDeviceId(123456L); - - // 编码 - byte[] packet = codec.encode(heartbeat); - String jsonString = new String(packet, StandardCharsets.UTF_8); - log.info("编码后 JSON: {}", jsonString); - log.info("心跳包长度: {} 字节", packet.length); - - // 解码验证 - IotDeviceMessage decoded = codec.decode(packet); - log.info("解码后消息 ID: {}", decoded.getId()); - log.info("解码后方法: {}", decoded.getMethod()); - log.info("解码后设备 ID: {}", decoded.getDeviceId()); - log.info("解码后服务 ID: {}", decoded.getServerId()); - - // 断言验证 - assertNotNull(decoded.getId()); - assertEquals("thing.state.online", decoded.getMethod()); - assertEquals(123456L, decoded.getDeviceId()); - } - - @Test - void testEventReport() { - log.info("=== JSON 格式事件上报测试 ==="); - - // 创建事件数据 - Map eventData = new HashMap<>(); - eventData.put("eventType", "alarm"); - eventData.put("level", "warning"); - eventData.put("description", "温度过高"); - eventData.put("value", 45.8); - - // 创建事件消息 - IotDeviceMessage event = IotDeviceMessage.requestOf("thing.event.post", eventData); - event.setDeviceId(123456L); - - // 编码 - byte[] packet = codec.encode(event); - String jsonString = new String(packet, StandardCharsets.UTF_8); - log.info("编码后 JSON: {}", jsonString); - log.info("事件包长度: {} 字节", packet.length); - - // 解码验证 - IotDeviceMessage decoded = codec.decode(packet); - log.info("解码后消息 ID: {}", decoded.getId()); - log.info("解码后方法: {}", decoded.getMethod()); - log.info("解码后设备 ID: {}", decoded.getDeviceId()); - log.info("解码后参数: {}", decoded.getParams()); - - // 断言验证 - assertNotNull(decoded.getId()); - assertEquals("thing.event.post", decoded.getMethod()); - assertEquals(123456L, decoded.getDeviceId()); - assertNotNull(decoded.getParams()); - } - - @Test - void testComplexDataReport() { - log.info("=== JSON 格式复杂数据上报测试 ==="); - - // 创建复杂设备数据(类似 EMQX 格式) - Map deviceData = new HashMap<>(); - - // 环境数据 - Map environment = new HashMap<>(); - environment.put("temperature", 23.8); - environment.put("humidity", 55.0); - environment.put("co2", 420); - environment.put("pm25", 35); - deviceData.put("environment", environment); - - // GPS 数据 - Map location = new HashMap<>(); - location.put("latitude", 39.9042); - location.put("longitude", 116.4074); - location.put("altitude", 43.5); - location.put("speed", 0.0); - deviceData.put("location", location); - - // 设备状态 - Map status = new HashMap<>(); - status.put("battery", 78); - status.put("signal", -65); - status.put("online", true); - status.put("version", "1.2.3"); - deviceData.put("status", status); - - // 创建设备消息 - IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", deviceData); - message.setDeviceId(789012L); - - // 编码 - byte[] packet = codec.encode(message); - String jsonString = new String(packet, StandardCharsets.UTF_8); - log.info("编码后 JSON: {}", jsonString); - log.info("复杂数据包长度: {} 字节", packet.length); - - // 解码验证 - IotDeviceMessage decoded = codec.decode(packet); - log.info("解码后消息 ID: {}", decoded.getId()); - log.info("解码后方法: {}", decoded.getMethod()); - log.info("解码后设备 ID: {}", decoded.getDeviceId()); - log.info("解码后参数: {}", decoded.getParams()); - - // 断言验证 - assertNotNull(decoded.getId()); - assertEquals("thing.property.post", decoded.getMethod()); - assertEquals(789012L, decoded.getDeviceId()); - assertNotNull(decoded.getParams()); - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-binary-packet-examples.md b/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-binary-packet-examples.md index 4c2807276e..d85d347f70 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-binary-packet-examples.md +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-binary-packet-examples.md @@ -1,370 +1,198 @@ -# TCP 二进制协议数据包格式说明和示例 +# TCP 二进制协议数据包格式说明 -## 1. 二进制协议概述 +## 1. 协议概述 -TCP 二进制协议是一种高效的自定义协议格式,适用于对带宽和性能要求较高的场景。该协议采用紧凑的二进制格式,减少数据传输量,提高传输效率。 +TCP 二进制协议是一种高效的自定义协议格式,采用紧凑的二进制格式传输数据,适用于对带宽和性能要求较高的 IoT 场景。 -## 2. 数据包格式 +### 1.1 协议特点 + +- **高效传输**:完全二进制格式,减少数据传输量 +- **版本控制**:内置协议版本号,支持协议升级 +- **类型安全**:明确的消息类型标识 +- **扩展性**:预留标志位,支持未来功能扩展 +- **兼容性**:与现有 `IotDeviceMessage` 接口完全兼容 + +## 2. 协议格式 ### 2.1 整体结构 -根据代码实现,TCP 二进制协议的数据包格式为: - ``` -+----------+----------+----------+----------+ -| 包头 | 功能码 | 消息序号 | 包体数据 | -| 4字节 | 2字节 | 2字节 | 变长 | -+----------+----------+----------+----------+ ++--------+--------+--------+--------+--------+--------+--------+--------+ +| 魔术字 | 版本号 | 消息类型| 消息标志| 消息长度(4字节) | ++--------+--------+--------+--------+--------+--------+--------+--------+ +| 消息 ID 长度(2字节) | 消息 ID (变长字符串) | ++--------+--------+--------+--------+--------+--------+--------+--------+ +| 方法名长度(2字节) | 方法名(变长字符串) | ++--------+--------+--------+--------+--------+--------+--------+--------+ +| 消息体数据(变长) | ++--------+--------+--------+--------+--------+--------+--------+--------+ ``` -**注意**:与原始设计相比,实际实现中移除了设备地址字段,简化了协议结构。 +### 2.2 字段详细说明 -### 2.2 字段说明 +| 字段 | 长度 | 类型 | 说明 | +|------|------|------|------| +| 魔术字 | 1字节 | byte | `0x7E` - 协议识别标识,用于数据同步 | +| 版本号 | 1字节 | byte | `0x01` - 协议版本号,支持版本控制 | +| 消息类型 | 1字节 | byte | `0x01`=请求, `0x02`=响应 | +| 消息标志 | 1字节 | byte | 预留字段,用于未来扩展 | +| 消息长度 | 4字节 | int | 整个消息的总长度(包含头部) | +| 消息 ID 长度 | 2字节 | short | 消息 ID 字符串的字节长度 | +| 消息 ID | 变长 | string | 消息唯一标识符(UTF-8编码) | +| 方法名长度 | 2字节 | short | 方法名字符串的字节长度 | +| 方法名 | 变长 | string | 消息方法名(UTF-8编码) | +| 消息体 | 变长 | binary | 根据消息类型的不同数据格式 | -| 字段 | 长度 | 类型 | 说明 | -|------|-----|--------|-----------------| -| 包头 | 4字节 | int | 后续数据的总长度(不包含包头) | -| 功能码 | 2字节 | short | 消息类型标识 | -| 消息序号 | 2字节 | short | 消息唯一标识 | -| 包体数据 | 变长 | string | JSON 格式的消息内容 | +**⚠️ 重要说明**:deviceId 不包含在协议中,由服务器根据连接上下文自动设置 -### 2.3 功能码定义 - -根据代码实现,支持的功能码: - -| 功能码 | 名称 | 说明 | -|-----|------|--------------| -| 10 | 设备注册 | 设备首次连接时的注册请求 | -| 11 | 注册回复 | 服务器对注册请求的回复 | -| 20 | 心跳请求 | 设备发送的心跳包 | -| 21 | 心跳回复 | 服务器对心跳的回复 | -| 30 | 消息上行 | 设备向服务器发送的数据 | -| 40 | 消息下行 | 服务器向设备发送的指令 | - -**常量定义:** +### 2.3 协议常量定义 ```java -public static final short CODE_REGISTER = 10; -public static final short CODE_REGISTER_REPLY = 11; -public static final short CODE_HEARTBEAT = 20; -public static final short CODE_HEARTBEAT_REPLY = 21; -public static final short CODE_MESSAGE_UP = 30; -public static final short CODE_MESSAGE_DOWN = 40; -``` +// 协议标识 +private static final byte MAGIC_NUMBER = (byte) 0x7E; +private static final byte PROTOCOL_VERSION = (byte) 0x01; -## 3. 包体数据格式 - -### 3.1 JSON 负载结构 - -包体数据采用 JSON 格式,包含以下字段: - -```json -{ - "method": "消息方法", - "params": { - // 消息参数 - }, - "timestamp": 时间戳, - "requestId": "请求ID", - "msgId": "消息ID" +// 消息类型 +public static class MessageType { + public static final byte REQUEST = 0x01; // 请求消息 + public static final byte RESPONSE = 0x02; // 响应消息 } + +// 协议长度 +private static final int HEADER_FIXED_LENGTH = 8; // 固定头部长度 +private static final int MIN_MESSAGE_LENGTH = 12; // 最小消息长度 ``` -### 3.2 字段说明 +## 3. 消息类型和格式 -| 字段名 | 类型 | 必填 | 说明 | -|-----------|--------|----|------------------------------| -| method | String | 是 | 消息方法,如 `thing.property.post` | -| params | Object | 否 | 消息参数 | -| timestamp | Long | 是 | 时间戳(毫秒) | -| requestId | String | 否 | 请求唯一标识 | -| msgId | String | 否 | 消息唯一标识 | +### 3.1 请求消息 (REQUEST - 0x01) -**常量定义:** +请求消息用于设备向服务器发送数据或请求。 -```java -public static final String METHOD = "method"; -public static final String PARAMS = "params"; -public static final String TIMESTAMP = "timestamp"; -public static final String REQUEST_ID = "requestId"; -public static final String MESSAGE_ID = "msgId"; +#### 3.1.1 消息体格式 +``` +消息体 = params 数据(JSON格式) ``` -## 4. 消息类型 +#### 3.1.2 示例:设备认证请求 -### 4.1 数据上报 (thing.property.post) +**消息内容:** +- 消息 ID: `auth_1704067200000_123` +- 方法名: `auth` +- 参数: `{"clientId":"device_001","username":"productKey_deviceName","password":"device_password"}` -设备向服务器上报属性数据。 - -**功能码:** 30 (CODE_MESSAGE_UP) - -**包体数据示例:** - -```json -{ - "method": "thing.property.post", - "params": { - "temperature": 25.5, - "humidity": 60.2, - "pressure": 1013.25 - }, - "timestamp": 1642781234567, - "requestId": "req_001" -} +**二进制数据包结构:** +``` +7E // 魔术字 (0x7E) +01 // 版本号 (0x01) +01 // 消息类型 (REQUEST) +00 // 消息标志 (预留) +00 00 00 8A // 消息长度 (138字节) +00 19 // 消息 ID 长度 (25字节) +61 75 74 68 5F 31 37 30 34 30 // 消息 ID: "auth_1704067200000_123" +36 37 32 30 30 30 30 30 5F 31 +32 33 +00 04 // 方法名长度 (4字节) +61 75 74 68 // 方法名: "auth" +7B 22 63 6C 69 65 6E 74 49 64 // JSON参数数据 +22 3A 22 64 65 76 69 63 65 5F // {"clientId":"device_001", +30 30 31 22 2C 22 75 73 65 72 // "username":"productKey_deviceName", +6E 61 6D 65 22 3A 22 70 72 6F // "password":"device_password"} +64 75 63 74 4B 65 79 5F 64 65 +76 69 63 65 4E 61 6D 65 22 2C +22 70 61 73 73 77 6F 72 64 22 +3A 22 64 65 76 69 63 65 5F 70 +61 73 73 77 6F 72 64 22 7D ``` -### 4.2 心跳 (thing.state.online) +#### 3.1.3 示例:属性数据上报 -设备向服务器发送心跳保活。 +**消息内容:** +- 消息 ID: `property_1704067200000_456` +- 方法名: `thing.property.post` +- 参数: `{"temperature":25.5,"humidity":60.2,"pressure":1013.25}` -**功能码:** 20 (CODE_HEARTBEAT) +### 3.2 响应消息 (RESPONSE - 0x02) -**包体数据示例:** +响应消息用于服务器向设备回复请求结果。 -```json -{ - "method": "thing.state.online", - "params": {}, - "timestamp": 1642781234567, - "requestId": "req_002" -} +#### 3.2.1 消息体格式 +``` +消息体 = 响应码(4字节) + 响应消息长度(2字节) + 响应消息(UTF-8) + 响应数据(JSON) ``` -### 4.3 消息方法常量 +#### 3.2.2 字段说明 -```java -public static final String PROPERTY_POST = "thing.property.post"; // 数据上报 -public static final String STATE_ONLINE = "thing.state.online"; // 心跳 +| 字段 | 长度 | 类型 | 说明 | +|------|------|------|------| +| 响应码 | 4字节 | int | HTTP状态码风格,0=成功,其他=错误 | +| 响应消息长度 | 2字节 | short | 响应消息字符串的字节长度 | +| 响应消息 | 变长 | string | 响应提示信息(UTF-8编码) | +| 响应数据 | 变长 | binary | JSON格式的响应数据(可选) | + +#### 3.2.3 示例:认证成功响应 + +**消息内容:** +- 消息 ID: `auth_response_1704067200000_123` +- 方法名: `auth` +- 响应码: `0` +- 响应消息: `认证成功` +- 响应数据: `{"success":true,"message":"认证成功"}` + +**二进制数据包结构:** +``` +7E // 魔术字 (0x7E) +01 // 版本号 (0x01) +02 // 消息类型 (RESPONSE) +00 // 消息标志 (预留) +00 00 00 A5 // 消息长度 (165字节) +00 22 // 消息 ID 长度 (34字节) +61 75 74 68 5F 72 65 73 70 6F // 消息 ID: "auth_response_1704067200000_123" +6E 73 65 5F 31 37 30 34 30 36 +37 32 30 30 30 30 30 5F 31 32 +33 +00 04 // 方法名长度 (4字节) +61 75 74 68 // 方法名: "auth" +00 00 00 00 // 响应码 (0 = 成功) +00 0C // 响应消息长度 (12字节) +E8 AE A4 E8 AF 81 E6 88 90 E5 // 响应消息: "认证成功" (UTF-8) +8A 9F +7B 22 73 75 63 63 65 73 73 22 // JSON响应数据 +3A 74 72 75 65 2C 22 6D 65 73 // {"success":true,"message":"认证成功"} +73 61 67 65 22 3A 22 E8 AE A4 +E8 AF 81 E6 88 90 E5 8A 9F 22 +7D ``` -## 5. 数据包示例 - -### 5.1 温度传感器数据上报 - -**包体数据:** -```json -{ - "method": "thing.property.post", - "params": { - "temperature": 25.5, - "humidity": 60.2, - "pressure": 1013.25 - }, - "timestamp": 1642781234567, - "requestId": "req_001" -} -``` - -**数据包结构:** -``` -包头: 0x00000045 (69字节) -功能码: 0x001E (30 - 消息上行) -消息序号: 0x1234 (4660) -包体: JSON字符串 -``` - -**完整十六进制数据包:** -``` -00 00 00 45 00 1E 12 34 -7B 22 6D 65 74 68 6F 64 22 3A 22 74 68 69 6E 67 -2E 70 72 6F 70 65 72 74 79 2E 70 6F 73 74 22 2C -22 70 61 72 61 6D 73 22 3A 7B 22 74 65 6D 70 65 -72 61 74 75 72 65 22 3A 32 35 2E 35 2C 22 68 75 -6D 69 64 69 74 79 22 3A 36 30 2E 32 2C 22 70 72 -65 73 73 75 72 65 22 3A 31 30 31 33 2E 32 35 7D -2C 22 74 69 6D 65 73 74 61 6D 70 22 3A 31 36 34 -32 37 38 31 32 33 34 35 36 37 2C 22 72 65 71 75 -65 73 74 49 64 22 3A 22 72 65 71 5F 30 30 31 22 7D -``` - -### 5.2 心跳包示例 - -**包体数据:** -```json -{ - "method": "thing.state.online", - "params": {}, - "timestamp": 1642781234567, - "requestId": "req_002" -} -``` - -**数据包结构:** -``` -包头: 0x00000028 (40字节) -功能码: 0x0014 (20 - 心跳请求) -消息序号: 0x5678 (22136) -包体: JSON字符串 -``` - -**完整十六进制数据包:** -``` -00 00 00 28 00 14 56 78 -7B 22 6D 65 74 68 6F 64 22 3A 22 74 68 69 6E 67 -2E 73 74 61 74 65 2E 6F 6E 6C 69 6E 65 22 2C 22 -70 61 72 61 6D 73 22 3A 7B 7D 2C 22 74 69 6D 65 -73 74 61 6D 70 22 3A 31 36 34 32 37 38 31 32 33 -34 35 36 37 2C 22 72 65 71 75 65 73 74 49 64 22 -3A 22 72 65 71 5F 30 30 32 22 7D -``` - -## 6. 编解码器实现 - -### 6.1 编码器类型 +## 4. 编解码器标识 ```java public static final String TYPE = "TCP_BINARY"; ``` -### 6.2 编码过程 +## 5. 协议优势 -1. **参数验证**:检查消息和方法是否为空 -2. **确定功能码**: - - 心跳消息:使用 `CODE_HEARTBEAT` (20) - - 其他消息:使用 `CODE_MESSAGE_UP` (30) -3. **构建负载**:使用 `buildSimplePayload()` 构建 JSON 负载 -4. **生成消息序号**:基于当前时间戳生成 -5. **构建数据包**:创建 `TcpDataPackage` 对象 -6. **编码为字节流**:使用 `encodeTcpDataPackage()` 编码 +- **数据紧凑**:二进制格式,相比 JSON 减少 30-50% 的数据量 +- **解析高效**:直接二进制操作,减少字符串转换开销 +- **类型安全**:明确的消息类型和字段定义 +- **扩展性强**:预留标志位支持未来功能扩展 +- **版本控制**:内置版本号支持协议升级 -### 6.3 解码过程 +## 6. 与 JSON 协议对比 -1. **参数验证**:检查字节数组是否为空 -2. **解码数据包**:使用 `decodeTcpDataPackage()` 解码 -3. **确定消息方法**: - - 功能码 20:`thing.state.online` (心跳) - - 功能码 30:`thing.property.post` (数据上报) -4. **解析负载信息**:使用 `parsePayloadInfo()` 解析 JSON 负载 -5. **构建设备消息**:创建 `IotDeviceMessage` 对象 -6. **设置服务 ID**:使用 `generateServerId()` 生成 - -### 6.4 服务 ID 生成 - -```java -private String generateServerId(TcpDataPackage dataPackage) { - return String.format("tcp_binary_%d_%d", dataPackage.getCode(), dataPackage.getMid()); -} -``` - -## 7. 数据包解析步骤 - -### 7.1 解析流程 - -1. **读取包头(4字节)** - - 获取后续数据的总长度 - - 验证数据包完整性 - -2. **读取功能码(2字节)** - - 确定消息类型 - -3. **读取消息序号(2字节)** - - 获取消息唯一标识 - -4. **读取包体数据(变长)** - - 解析 JSON 格式的消息内容 - -### 7.2 Java 解析示例 - -```java -public TcpDataPackage parsePacket(byte[] packet) { - int index = 0; - - // 1. 解析包头 - int totalLength = ByteBuffer.wrap(packet, index, 4).getInt(); - index += 4; - - // 2. 解析功能码 - short functionCode = ByteBuffer.wrap(packet, index, 2).getShort(); - index += 2; - - // 3. 解析消息序号 - short messageId = ByteBuffer.wrap(packet, index, 2).getShort(); - index += 2; - - // 4. 解析包体数据 - String payload = new String(packet, index, packet.length - index); - - return new TcpDataPackage(functionCode, messageId, payload); -} -``` - -## 8. 使用示例 - -### 8.1 基本使用 - -```java -// 创建编解码器 -IotTcpBinaryDeviceMessageCodec codec = new IotTcpBinaryDeviceMessageCodec(); - -// 创建数据上报消息 -Map sensorData = Map.of( - "temperature", 25.5, - "humidity", 60.2 -); - -// 编码 -IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", sensorData); -byte[] data = codec.encode(message); - -// 解码 -IotDeviceMessage decoded = codec.decode(data); -``` - -### 8.2 错误处理 - -```java -try{ -byte[] data = codec.encode(message); -// 处理编码成功 -}catch( -IllegalArgumentException e){ - // 处理参数错误 - log. - -error("编码参数错误: {}",e.getMessage()); - }catch( -TcpCodecException e){ - // 处理编码失败 - log. - -error("编码失败: {}",e.getMessage()); - } -``` - -## 9. 注意事项 - -1. **字节序**:所有多字节数据使用大端序(Big-Endian) -2. **字符编码**:字符串数据使用 UTF-8 编码 -3. **JSON 格式**:包体数据必须是有效的 JSON 格式 -4. **长度限制**:单个数据包建议不超过 1MB -5. **错误处理**:解析失败时会抛出 `TcpCodecException` -6. **功能码映射**:目前只支持心跳和数据上报两种消息类型 - -## 10. 协议特点 - -### 10.1 优势 - -- **高效传输**:二进制格式,数据量小 -- **性能优化**:减少解析开销 -- **带宽节省**:相比 JSON 格式节省带宽 -- **实时性好**:适合高频数据传输 - -### 10.2 适用场景 +| 特性 | 二进制协议 | JSON协议 | +|------|------------|----------| +| 数据大小 | 小(节省30-50%) | 大 | +| 解析性能 | 高 | 中等 | +| 网络开销 | 低 | 高 | +| 可读性 | 差 | 优秀 | +| 调试难度 | 高 | 低 | +| 扩展性 | 良好(有预留位) | 优秀 | +**推荐场景**: - ✅ **高频数据传输**:传感器数据实时上报 - ✅ **带宽受限环境**:移动网络、卫星通信 -- ✅ **性能要求高**:需要低延迟的场景 -- ✅ **设备资源有限**:嵌入式设备、IoT 设备 - -### 10.3 与 JSON 协议对比 - -| 特性 | 二进制协议 | JSON 协议 | -|-------|-------|---------| -| 数据大小 | 小 | 稍大 | -| 解析性能 | 高 | 中等 | -| 可读性 | 差 | 优秀 | -| 调试难度 | 高 | 低 | -| 扩展性 | 差 | 优秀 | -| 实现复杂度 | 高 | 低 | - -这样就完成了 TCP 二进制协议的完整说明,与实际代码实现完全一致。 +- ✅ **性能要求高**:需要低延迟、高吞吐的场景 +- ✅ **设备资源有限**:嵌入式设备、低功耗设备 +- ❌ **开发调试阶段**:调试困难,建议使用 JSON 协议 +- ❌ **快速原型开发**:开发效率低 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-json-packet-examples.md b/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-json-packet-examples.md index 34251e7166..09ef50cfe5 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-json-packet-examples.md +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-json-packet-examples.md @@ -2,13 +2,13 @@ ## 1. 协议概述 -TCP JSON 格式协议采用纯 JSON 格式进行数据传输,参考了 EMQX 和 HTTP 模块的数据格式设计,具有以下优势: +TCP JSON 格式协议采用纯 JSON 格式进行数据传输,具有以下特点: - **标准化**:使用标准 JSON 格式,易于解析和处理 - **可读性**:人类可读,便于调试和维护 - **扩展性**:可以轻松添加新字段,向后兼容 -- **统一性**:与 HTTP 模块保持一致的数据格式 -- **简化性**:相比二进制协议,实现更简单,调试更容易 +- **跨平台**:JSON 格式支持所有主流编程语言 +- **安全优化**:移除冗余的 deviceId 字段,提高安全性 ## 2. 消息格式 @@ -18,294 +18,174 @@ TCP JSON 格式协议采用纯 JSON 格式进行数据传输,参考了 EMQX { "id": "消息唯一标识", "method": "消息方法", - "deviceId": 设备ID, "params": { - // 消息参数 + // 请求参数 + }, + "data": { + // 响应数据 }, - "timestamp": 时间戳, "code": 响应码, - "message": "响应消息" + "msg": "响应消息", + "timestamp": 时间戳 } ``` -### 2.2 字段说明 +**⚠️ 重要说明**: +- **不包含 deviceId 字段**:由服务器通过 TCP 连接上下文自动确定设备 ID +- **避免伪造攻击**:防止设备伪造其他设备的 ID 发送消息 -| 字段名 | 类型 | 必填 | 说明 | -|-----------|---------|----|-------------------------------------| -| id | String | 是 | 消息唯一标识,如果为空会自动生成 UUID | -| method | String | 是 | 消息方法,如 `auth`、`thing.property.post` | -| deviceId | Long | 否 | 设备 ID | -| params | Object | 否 | 消息参数,具体内容根据 method 而定 | -| timestamp | Long | 是 | 时间戳(毫秒),自动生成 | -| code | Integer | 否 | 响应码(下行消息使用) | -| message | String | 否 | 响应消息(下行消息使用) | +### 2.2 字段详细说明 -## 3. 消息类型 +| 字段名 | 类型 | 必填 | 用途 | 说明 | +|--------|------|------|------|------| +| id | String | 是 | 所有消息 | 消息唯一标识 | +| method | String | 是 | 所有消息 | 消息方法,如 `auth`、`thing.property.post` | +| params | Object | 否 | 请求消息 | 请求参数,具体内容根据method而定 | +| data | Object | 否 | 响应消息 | 响应数据,服务器返回的结果数据 | +| code | Integer | 否 | 响应消息 | 响应码,0=成功,其他=错误 | +| msg | String | 否 | 响应消息 | 响应提示信息 | +| timestamp | Long | 是 | 所有消息 | 时间戳(毫秒),编码时自动生成 | + +### 2.3 消息分类 + +#### 2.3.1 请求消息(上行) +- **特征**:包含 `params` 字段,不包含 `code`、`msg` 字段 +- **方向**:设备 → 服务器 +- **用途**:设备认证、数据上报、状态更新等 + +#### 2.3.2 响应消息(下行) +- **特征**:包含 `code`、`msg` 字段,可能包含 `data` 字段 +- **方向**:服务器 → 设备 +- **用途**:认证结果、指令响应、错误提示等 + +## 3. 消息示例 ### 3.1 设备认证 (auth) -设备连接后首先需要进行认证,认证成功后才能进行其他操作。 - -#### 3.1.1 认证请求格式 - -**示例:** +#### 认证请求格式 +**消息方向**:设备 → 服务器 ```json { - "id": "auth_8ac6a1db91e64aa9996143fdbac2cbfe", + "id": "auth_1704067200000_123", "method": "auth", "params": { "clientId": "device_001", "username": "productKey_deviceName", "password": "设备密码" }, - "timestamp": 1753111026437 + "timestamp": 1704067200000 } ``` -**字段说明:** +**认证参数说明:** + | 字段名 | 类型 | 必填 | 说明 | |--------|------|------|------| -| clientId | String | 是 | 客户端唯一标识 | +| clientId | String | 是 | 客户端唯一标识,用于连接管理 | | username | String | 是 | 设备用户名,格式为 `productKey_deviceName` | -| password | String | 是 | 设备密码 | +| password | String | 是 | 设备密码,在设备管理平台配置 | -#### 3.1.2 认证响应格式 +#### 认证响应格式 +**消息方向**:服务器 → 设备 **认证成功响应:** - ```json { - "id": "auth_response_8ac6a1db91e64aa9996143fdbac2cbfe", - "requestId": "auth_8ac6a1db91e64aa9996143fdbac2cbfe", + "id": "response_auth_1704067200000_123", "method": "auth", "data": { "success": true, "message": "认证成功" }, "code": 0, - "msg": "认证成功" + "msg": "认证成功", + "timestamp": 1704067200001 } ``` **认证失败响应:** - ```json { - "id": "auth_response_8ac6a1db91e64aa9996143fdbac2cbfe", - "requestId": "auth_8ac6a1db91e64aa9996143fdbac2cbfe", + "id": "response_auth_1704067200000_123", "method": "auth", "data": { "success": false, "message": "认证失败:用户名或密码错误" }, "code": 401, - "msg": "认证失败:用户名或密码错误" + "msg": "认证失败", + "timestamp": 1704067200001 } ``` -#### 3.1.3 认证流程 +### 3.2 属性数据上报 (thing.property.post) -1. **设备连接** → 建立 TCP 连接 -2. **发送认证请求** → 发送包含认证信息的 JSON 消息 -3. **服务器验证** → 验证 clientId、username、password -4. **生成 Token** → 认证成功后生成 JWT Token(内部使用) -5. **设备上线** → 发送设备上线消息到消息总线 -6. **返回响应** → 返回认证结果 -7. **会话注册** → 注册设备会话,允许后续业务操作 +**消息方向**:设备 → 服务器 -#### 3.1.4 认证错误码 - -| 错误码 | 说明 | 处理建议 | -|-----|-------|--------------| -| 401 | 认证失败 | 检查用户名、密码是否正确 | -| 400 | 参数错误 | 检查认证参数是否完整 | -| 404 | 设备不存在 | 检查设备是否已注册 | -| 500 | 服务器错误 | 联系管理员 | - -### 3.2 数据上报 (thing.property.post) - -设备向服务器上报属性数据。 - -**示例:** +**示例:温度传感器数据上报** ```json { - "id": "8ac6a1db91e64aa9996143fdbac2cbfe", + "id": "property_1704067200000_456", "method": "thing.property.post", - "deviceId": 8, "params": { "temperature": 25.5, "humidity": 60.2, "pressure": 1013.25, - "battery": 85 + "battery": 85, + "signal_strength": -65 }, - "timestamp": 1753111026437 + "timestamp": 1704067200000 } ``` -### 3.3 心跳 (thing.state.update) +### 3.3 设备状态更新 (thing.state.update) -设备向服务器发送心跳保活。 +**消息方向**:设备 → 服务器 -**示例:** +**示例:心跳请求** ```json { - "id": "7db8c4e6408b40f8b2549ddd94f6bb02", + "id": "heartbeat_1704067200000_321", "method": "thing.state.update", - "deviceId": 8, "params": { - "state": "1" + "state": "online", + "uptime": 86400, + "memory_usage": 65.2, + "cpu_usage": 12.8 }, - "timestamp": 1753111026467 + "timestamp": 1704067200000 } ``` -### 3.4 消息方法常量 +## 4. 编解码器标识 -支持的消息方法: +```java +public static final String TYPE = "TCP_JSON"; +``` -- `auth` - 设备认证 -- `thing.property.post` - 数据上报 -- `thing.state.update` - 心跳 +## 5. 协议优势 -## 4. 协议特点 - -### 4.1 优势 - -- **简单易用**:纯 JSON 格式,无需复杂的二进制解析 -- **调试友好**:可以直接查看消息内容 +- **开发效率高**:JSON 格式,开发和调试简单 +- **跨语言支持**:所有主流语言都支持 JSON +- **可读性优秀**:可以直接查看消息内容 - **扩展性强**:可以轻松添加新字段 -- **标准化**:与 EMQX 等主流平台格式兼容 -- **错误处理**:提供详细的错误信息和异常处理 -- **安全性**:支持设备认证机制 +- **安全性高**:移除 deviceId 字段,防止伪造攻击 -### 4.2 与二进制协议对比 +## 6. 与二进制协议对比 -| 特性 | 二进制协议 | JSON 协议 | -|-------|-------|----------| -| 可读性 | 差 | 优秀 | -| 调试难度 | 高 | 低 | -| 扩展性 | 差 | 优秀 | -| 解析复杂度 | 高 | 低 | -| 数据大小 | 小 | 稍大 | -| 标准化程度 | 低 | 高 | -| 实现复杂度 | 高 | 低 | -| 安全性 | 一般 | 优秀(支持认证) | +| 特性 | JSON协议 | 二进制协议 | +|------|----------|------------| +| 开发难度 | 低 | 高 | +| 调试难度 | 低 | 高 | +| 可读性 | 优秀 | 差 | +| 数据大小 | 中等 | 小(节省30-50%) | +| 解析性能 | 中等 | 高 | +| 学习成本 | 低 | 高 | -### 4.3 适用场景 - -- ✅ **开发调试**:JSON 格式便于查看和调试 -- ✅ **快速集成**:标准 JSON 格式,集成简单 -- ✅ **协议扩展**:可以轻松添加新字段 -- ✅ **多语言支持**:JSON 格式支持所有主流语言 -- ✅ **云平台对接**:与主流 IoT 云平台格式兼容 -- ✅ **安全要求**:支持设备认证和访问控制 - -## 5. 最佳实践 - -### 5.1 认证最佳实践 - -1. **连接即认证**:设备连接后立即进行认证 -2. **重连机制**:连接断开后重新认证 -3. **错误重试**:认证失败时适当重试 -4. **安全传输**:使用 TLS 加密传输敏感信息 - -### 5.2 消息设计 - -1. **保持简洁**:避免过深的嵌套结构 -2. **字段命名**:使用驼峰命名法,保持一致性 -3. **数据类型**:使用合适的数据类型,避免字符串表示数字 -4. **时间戳**:统一使用毫秒级时间戳 - -### 5.3 错误处理 - -1. **参数验证**:确保必要字段存在且有效 -2. **异常捕获**:正确处理编码解码异常 -3. **日志记录**:记录详细的调试信息 -4. **认证失败**:认证失败时及时关闭连接 - -### 5.4 性能优化 - -1. **批量上报**:可以在 params 中包含多个数据点 -2. **连接复用**:保持 TCP 连接,避免频繁建立连接 -3. **消息缓存**:客户端可以缓存消息,批量发送 -4. **心跳间隔**:合理设置心跳间隔,避免过于频繁 - -## 6. 配置说明 - -### 6.1 启用 JSON 协议 - -在配置文件中设置: - -```yaml -yudao: - iot: - gateway: - protocol: - tcp: - enabled: true - port: 8091 - default-protocol: "JSON" # 使用 JSON 协议 -``` - -### 6.2 认证配置 - -```yaml -yudao: - iot: - gateway: - token: - secret: "your-secret-key" # JWT 密钥 - expiration: "24h" # Token 过期时间 -``` - -## 7. 调试和监控 - -### 7.1 日志级别 - -```yaml -logging: - level: - cn.iocoder.yudao.module.iot.gateway.protocol.tcp: DEBUG -``` - -### 7.2 调试信息 - -编解码器会输出详细的调试日志: - -- 认证过程:显示认证请求和响应 -- 编码成功:显示方法、长度、内容 -- 解码过程:显示原始数据、解析结果 -- 错误信息:详细的异常堆栈 - -### 7.3 监控指标 - -- 认证成功率 -- 消息处理数量 -- 编解码成功率 -- 处理延迟 -- 错误率 -- 在线设备数量 - -## 8. 安全考虑 - -### 8.1 认证安全 - -1. **密码强度**:使用强密码策略 -2. **Token 过期**:设置合理的 Token 过期时间 -3. **连接限制**:限制单个设备的并发连接数 -4. **IP 白名单**:可选的 IP 访问控制 - -### 8.2 传输安全 - -1. **TLS 加密**:使用 TLS 1.2+ 加密传输 -2. **证书验证**:验证服务器证书 -3. **密钥管理**:安全存储和管理密钥 - -### 8.3 数据安全 - -1. **敏感信息**:不在日志中记录密码等敏感信息 -2. **数据验证**:验证所有输入数据 -3. **访问控制**:基于 Token 的访问控制 - -这样就完成了 TCP JSON 格式协议的完整说明,包括认证流程的详细说明,与实际代码实现完全一致。 +**推荐场景**: +- ✅ **开发调试阶段**:调试友好,开发效率高 +- ✅ **快速原型开发**:实现简单,快速迭代 +- ✅ **多语言集成**:广泛的语言支持 +- ❌ **高频数据传输**:建议使用二进制协议 +- ❌ **带宽受限环境**:建议使用二进制协议 \ No newline at end of file From ab7d9fdf72f96135efcdc21205d8eee1ba1a9149 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Tue, 29 Jul 2025 20:08:27 +0800 Subject: [PATCH 83/89] =?UTF-8?q?fix=EF=BC=9A=E3=80=90bpm=20=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E6=B5=81=E3=80=91=E5=A7=94=E6=B4=BE=E3=80=81=E8=BD=AC?= =?UTF-8?q?=E5=8A=9E=E6=97=B6=EF=BC=8Cowner=20=E4=B8=8D=E9=87=8D=E5=A4=8D?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/bpm/service/task/BpmTaskServiceImpl.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java index 1dbd0b17c0..8ea607a773 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java @@ -947,7 +947,10 @@ public class BpmTaskServiceImpl implements BpmTaskService { BpmCommentTypeEnum.DELEGATE_START.formatComment(currentUser.getNickname(), delegateUser.getNickname(), reqVO.getReason())); // 3.1 设置任务所有人 (owner) 为原任务的处理人 (assignee) - taskService.setOwner(taskId, task.getAssignee()); + // 特殊:如果已经被委派(owner 非空),则不需要更新 owner:https://gitee.com/zhijiantianya/yudao-cloud/issues/ICJ153 + if (StrUtil.isEmpty(task.getOwner())) { + taskService.setOwner(taskId, task.getAssignee()); + } // 3.2 执行委派,将任务委派给 delegateUser taskService.delegateTask(taskId, reqVO.getDelegateUserId().toString()); // 补充说明:委托不单独设置状态。如果需要,可通过 Task 的 DelegationState 字段,判断是否为 DelegationState.PENDING 委托中 @@ -973,7 +976,10 @@ public class BpmTaskServiceImpl implements BpmTaskService { BpmCommentTypeEnum.TRANSFER.formatComment(currentUser.getNickname(), assigneeUser.getNickname(), reqVO.getReason())); // 3.1 设置任务所有人 (owner) 为原任务的处理人 (assignee) - taskService.setOwner(taskId, task.getAssignee()); + // 特殊:如果已经被转派(owner 非空),则不需要更新 owner:https://gitee.com/zhijiantianya/yudao-cloud/issues/ICJ153 + if (StrUtil.isEmpty(task.getOwner())) { + taskService.setOwner(taskId, task.getAssignee()); + } // 3.2 执行转派(审批人),将任务转派给 assigneeUser // 委托( delegate)和转派(transfer)的差别,就在这块的调用!!!! taskService.setAssignee(taskId, reqVO.getAssigneeUserId().toString()); From 176e87141eec2ff13ff089eb10bec9f15102a817 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Tue, 29 Jul 2025 22:24:20 +0800 Subject: [PATCH 84/89] =?UTF-8?q?feat=EF=BC=9AGlobalExceptionHandler=20?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E6=9B=B4=E5=A4=9A=E8=AF=B7=E6=B1=82=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E4=B8=8D=E5=AF=B9=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/handler/GlobalExceptionHandler.java | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java index 2f103732cc..627a5ea784 100644 --- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java @@ -6,6 +6,8 @@ import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.servlet.JakartaServletUtil; +import cn.iocoder.yudao.framework.common.biz.infra.logger.ApiErrorLogCommonApi; +import cn.iocoder.yudao.framework.common.biz.infra.logger.dto.ApiErrorLogCreateReqDTO; import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil; import cn.iocoder.yudao.framework.common.pojo.CommonResult; @@ -14,8 +16,6 @@ import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils; import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; -import cn.iocoder.yudao.framework.common.biz.infra.logger.ApiErrorLogCommonApi; -import cn.iocoder.yudao.framework.common.biz.infra.logger.dto.ApiErrorLogCreateReqDTO; import com.fasterxml.jackson.databind.exc.InvalidFormatException; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.ConstraintViolation; @@ -29,6 +29,7 @@ import org.springframework.util.Assert; import org.springframework.validation.BindException; import org.springframework.validation.FieldError; import org.springframework.validation.ObjectError; +import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.MissingServletRequestParameterException; @@ -101,6 +102,9 @@ public class GlobalExceptionHandler { if (ex instanceof HttpRequestMethodNotSupportedException) { return httpRequestMethodNotSupportedExceptionHandler((HttpRequestMethodNotSupportedException) ex); } + if (ex instanceof HttpMediaTypeNotSupportedException) { + return httpMediaTypeNotSupportedExceptionHandler((HttpMediaTypeNotSupportedException) ex); + } if (ex instanceof ServiceException) { return serviceExceptionHandler((ServiceException) ex); } @@ -179,9 +183,11 @@ public class GlobalExceptionHandler { if (ex.getCause() instanceof InvalidFormatException) { InvalidFormatException invalidFormatException = (InvalidFormatException) ex.getCause(); return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数类型错误:%s", invalidFormatException.getValue())); - } else { - return defaultExceptionHandler(ServletUtils.getRequest(), ex); } + if (StrUtil.startWith(ex.getMessage(), "Required request body is missing")) { + return CommonResult.error(BAD_REQUEST.getCode(), "请求参数类型错误: request body 缺失"); + } + return defaultExceptionHandler(ServletUtils.getRequest(), ex); } /** @@ -237,6 +243,17 @@ public class GlobalExceptionHandler { return CommonResult.error(METHOD_NOT_ALLOWED.getCode(), String.format("请求方法不正确:%s", ex.getMessage())); } + /** + * 处理 SpringMVC 请求的 Content-Type 不正确 + * + * 例如说,A 接口的 Content-Type 为 application/json,结果请求的 Content-Type 为 application/octet-stream,导致不匹配 + */ + @ExceptionHandler(HttpMediaTypeNotSupportedException.class) + public CommonResult httpMediaTypeNotSupportedExceptionHandler(HttpMediaTypeNotSupportedException ex) { + log.warn("[httpMediaTypeNotSupportedExceptionHandler]", ex); + return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求类型不正确:%s", ex.getMessage())); + } + /** * 处理 Spring Security 权限不足的异常 * From f71ec384568d6291fef8216d0a6897c4395a9f70 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Wed, 30 Jul 2025 19:06:44 +0800 Subject: [PATCH 85/89] =?UTF-8?q?=E3=80=90=E5=90=8C=E6=AD=A5=E3=80=91BOOT?= =?UTF-8?q?=20=E5=92=8C=20CLOUD=20=E7=9A=84=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/bpm/enums/ErrorCodeConstants.java | 3 +- .../core/util/BpmHttpRequestUtils.java | 50 +++++++++++++++---- .../module/crm/util/CrmPermissionUtils.java | 2 +- 3 files changed, 41 insertions(+), 14 deletions(-) diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java index 4f7d7bf7ce..0e3b1b9208 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java @@ -40,7 +40,7 @@ public interface ErrorCodeConstants { ErrorCode PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_EXISTS = new ErrorCode(1_009_004_004, "任务({})的候选人({})不存在"); ErrorCode PROCESS_INSTANCE_START_USER_CAN_START = new ErrorCode(1_009_004_005, "发起流程失败,你没有权限发起该流程"); ErrorCode PROCESS_INSTANCE_CANCEL_FAIL_NOT_ALLOW = new ErrorCode(1_009_004_005, "流程取消失败,该流程不允许取消"); - ErrorCode PROCESS_INSTANCE_HTTP_TRIGGER_CALL_ERROR = new ErrorCode(1_009_004_006, "流程 Http 触发器请求调用失败"); + ErrorCode PROCESS_INSTANCE_HTTP_CALL_ERROR = new ErrorCode(1_009_004_006, "流程 Http 请求调用失败"); ErrorCode PROCESS_INSTANCE_APPROVE_USER_SELECT_ASSIGNEES_NOT_CONFIG = new ErrorCode(1_009_004_007, "下一个任务({})的审批人未配置"); ErrorCode PROCESS_INSTANCE_CANCEL_CHILD_FAIL_NOT_ALLOW = new ErrorCode(1_009_004_008, "子流程取消失败,子流程不允许取消"); @@ -58,7 +58,6 @@ public interface ErrorCodeConstants { ErrorCode TASK_SIGN_DELETE_NO_PARENT = new ErrorCode(1_009_005_012, "任务减签失败,被减签的任务必须是通过加签生成的任务"); ErrorCode TASK_TRANSFER_FAIL_USER_REPEAT = new ErrorCode(1_009_005_013, "任务转办失败,转办人和当前审批人为同一人"); ErrorCode TASK_TRANSFER_FAIL_USER_NOT_EXISTS = new ErrorCode(1_009_005_014, "任务转办失败,转办人不存在"); - ErrorCode TASK_CREATE_FAIL_NO_CANDIDATE_USER = new ErrorCode(1_009_006_003, "操作失败,原因:找不到任务的审批人!"); ErrorCode TASK_SIGNATURE_NOT_EXISTS = new ErrorCode(1_009_005_015, "签名不能为空!"); ErrorCode TASK_REASON_REQUIRE = new ErrorCode(1_009_005_016, "审批意见不能为空!"); diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmHttpRequestUtils.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmHttpRequestUtils.java index 014b5e3f0d..358ee9f4dc 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmHttpRequestUtils.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmHttpRequestUtils.java @@ -6,15 +6,15 @@ import cn.iocoder.yudao.framework.common.core.KeyValue; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.common.util.spring.SpringUtils; +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; +import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEvent; import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; import cn.iocoder.yudao.module.bpm.enums.definition.BpmHttpRequestParamTypeEnum; import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; import com.fasterxml.jackson.core.type.TypeReference; import lombok.extern.slf4j.Slf4j; import org.flowable.engine.runtime.ProcessInstance; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpMethod; -import org.springframework.http.ResponseEntity; +import org.springframework.http.*; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestClientException; @@ -26,7 +26,7 @@ import java.util.Map; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; -import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.PROCESS_INSTANCE_HTTP_TRIGGER_CALL_ERROR; +import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.PROCESS_INSTANCE_HTTP_CALL_ERROR; /** * 工作流发起 HTTP 请求工具类 @@ -42,7 +42,6 @@ public class BpmHttpRequestUtils { List bodyParams, Boolean handleResponse, List> response) { - RestTemplate restTemplate = SpringUtils.getBean(RestTemplate.class); BpmProcessInstanceService processInstanceService = SpringUtils.getBean(BpmProcessInstanceService.class); // 1.1 设置请求头 @@ -51,6 +50,7 @@ public class BpmHttpRequestUtils { MultiValueMap body = buildHttpBody(processInstance, bodyParams); // 2. 发起请求 + RestTemplate restTemplate = SpringUtils.getBean(RestTemplate.class); ResponseEntity responseEntity = sendHttpRequest(url, headers, body, restTemplate); // 3. 处理返回 @@ -78,27 +78,55 @@ public class BpmHttpRequestUtils { } } + public static void executeBpmHttpRequest(BpmProcessInstanceStatusEvent event, + String url) { + // 1.1 设置请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + if (TenantContextHolder.getTenantId() != null) { + headers.add(HEADER_TENANT_ID, String.valueOf(TenantContextHolder.getTenantId())); + } else { + BpmProcessInstanceService processInstanceService = SpringUtils.getBean(BpmProcessInstanceService.class); + ProcessInstance processInstance = processInstanceService.getProcessInstance(event.getId()); + if (processInstance != null) { + headers.add(HEADER_TENANT_ID, String.valueOf(TenantContextHolder.getTenantId())); + } + } + // 1.2 设置请求体 +// MultiValueMap body = new LinkedMultiValueMap<>(); +// body.add("id", event.getId()); +// body.add("processDefinitionKey", event.getProcessDefinitionKey()); +// body.add("status", event.getStatus().toString()); +// if (StrUtil.isNotEmpty(event.getBusinessKey())) { +// body.add("businessKey", event.getBusinessKey()); +// } + + // 2. 发起请求 + RestTemplate restTemplate = SpringUtils.getBean(RestTemplate.class); + sendHttpRequest(url, headers, event, restTemplate); + } + public static ResponseEntity sendHttpRequest(String url, MultiValueMap headers, - MultiValueMap body, + Object body, RestTemplate restTemplate) { - HttpEntity> requestEntity = new HttpEntity<>(body, headers); + HttpEntity requestEntity = new HttpEntity<>(body, headers); ResponseEntity responseEntity; try { responseEntity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class); - log.info("[sendHttpRequest][HTTP 触发器,请求头:{},请求体:{},响应结果:{}]", headers, body, responseEntity); + log.info("[sendHttpRequest][HTTP 请求,请求头:{},请求体:{},响应结果:{}]", headers, body, responseEntity); } catch (RestClientException e) { - log.error("[sendHttpRequest][HTTP 触发器,请求头:{},请求体:{},请求出错:{}]", headers, body, e.getMessage()); - throw exception(PROCESS_INSTANCE_HTTP_TRIGGER_CALL_ERROR); + log.error("[sendHttpRequest][HTTP 请求,请求头:{},请求体:{},请求出错:{}]", headers, body, e.getMessage()); + throw exception(PROCESS_INSTANCE_HTTP_CALL_ERROR); } return responseEntity; } public static MultiValueMap buildHttpHeaders(ProcessInstance processInstance, List headerSettings) { - Map processVariables = processInstance.getProcessVariables(); MultiValueMap headers = new LinkedMultiValueMap<>(); headers.add(HEADER_TENANT_ID, processInstance.getTenantId()); + Map processVariables = processInstance.getProcessVariables(); addHttpRequestParam(headers, headerSettings, processVariables); return headers; } diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/util/CrmPermissionUtils.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/util/CrmPermissionUtils.java index d2818cd072..f3a0b5792b 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/util/CrmPermissionUtils.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/util/CrmPermissionUtils.java @@ -32,7 +32,7 @@ public class CrmPermissionUtils { * @return 是/否 */ public static boolean isCrmAdmin() { - PermissionApi permissionApi = SpringUtil.getBean(PermissionApi.class); + PermissionCommonApi permissionApi = SpringUtil.getBean(PermissionCommonApi.class); return permissionApi.hasAnyRoles(getLoginUserId(), RoleCodeEnum.CRM_ADMIN.getCode()); } From aefaff2cad9fe78fb344ff94d2b7f888624c02cb Mon Sep 17 00:00:00 2001 From: YunaiV Date: Thu, 31 Jul 2025 09:56:34 +0800 Subject: [PATCH 86/89] =?UTF-8?q?fix=EF=BC=9A=E3=80=90bpm=20=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E6=B5=81=E3=80=91BpmCategoryServiceImplTest=20?= =?UTF-8?q?=E5=8D=95=E6=B5=8B=E4=B8=8D=E6=AD=A3=E7=A1=AE=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bpm/service/category/BpmCategoryServiceImplTest.java | 5 +++++ .../cn/iocoder/yudao/module/crm/util/CrmPermissionUtils.java | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/category/BpmCategoryServiceImplTest.java b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/category/BpmCategoryServiceImplTest.java index f71b8aae50..19199e5616 100644 --- a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/category/BpmCategoryServiceImplTest.java +++ b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/category/BpmCategoryServiceImplTest.java @@ -8,9 +8,11 @@ import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.category.BpmCa import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO; import cn.iocoder.yudao.module.bpm.dal.mysql.category.BpmCategoryMapper; import cn.iocoder.yudao.module.bpm.service.definition.BpmCategoryServiceImpl; +import cn.iocoder.yudao.module.bpm.service.definition.BpmModelService; import jakarta.annotation.Resource; import org.junit.jupiter.api.Test; import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildTime; @@ -32,6 +34,9 @@ public class BpmCategoryServiceImplTest extends BaseDbUnitTest { @Resource private BpmCategoryServiceImpl categoryService; + @MockitoBean + private BpmModelService modelService; + @Resource private BpmCategoryMapper categoryMapper; diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/util/CrmPermissionUtils.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/util/CrmPermissionUtils.java index f3a0b5792b..0563af0ad3 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/util/CrmPermissionUtils.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/util/CrmPermissionUtils.java @@ -2,11 +2,11 @@ package cn.iocoder.yudao.module.crm.util; import cn.hutool.core.collection.CollUtil; import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.biz.system.permission.PermissionCommonApi; import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO; import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum; import cn.iocoder.yudao.module.crm.enums.common.CrmSceneTypeEnum; import cn.iocoder.yudao.module.crm.enums.permission.CrmPermissionLevelEnum; -import cn.iocoder.yudao.module.system.api.permission.PermissionApi; import cn.iocoder.yudao.module.system.api.user.AdminUserApi; import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; import cn.iocoder.yudao.module.system.enums.permission.RoleCodeEnum; From 3a07bcc3c663f843ef130cff616df5a9ccb96902 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Thu, 31 Jul 2025 12:57:33 +0800 Subject: [PATCH 87/89] =?UTF-8?q?(=E3=80=83'=E2=96=BD'=E3=80=83)=20v2025.0?= =?UTF-8?q?8=20=E5=8F=91=E5=B8=83=EF=BC=9A=E5=AE=8C=E5=96=84=20vben5(antd)?= =?UTF-8?q?=20=E5=B7=A5=E4=BD=9C=E6=B5=81=E3=80=81=E8=BF=81=E7=A7=BB=20vbe?= =?UTF-8?q?n5(ep)=20=E5=95=86=E5=9F=8E=E3=80=81=E6=94=AF=E6=8C=81=20IoT=20?= =?UTF-8?q?TCP=20=E5=8D=8F=E8=AE=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 2 +- yudao-dependencies/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 48a6f0589d..e4770100e7 100644 --- a/pom.xml +++ b/pom.xml @@ -32,7 +32,7 @@ https://github.com/YunaiV/ruoyi-vue-pro - 2.6.1-SNAPSHOT + 2025.08-SNAPSHOT 17 ${java.version} diff --git a/yudao-dependencies/pom.xml b/yudao-dependencies/pom.xml index d3b23f62ca..668249863a 100644 --- a/yudao-dependencies/pom.xml +++ b/yudao-dependencies/pom.xml @@ -14,7 +14,7 @@ https://github.com/YunaiV/ruoyi-vue-pro - 2.6.1-SNAPSHOT + 2025.08-SNAPSHOT 1.6.0 3.4.5 From 0f9cf91899050d109105f8e5a8c53c39db35e6da Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 2 Aug 2025 10:48:22 +0800 Subject: [PATCH 88/89] =?UTF-8?q?review=EF=BC=9A=E3=80=90iot=20=E7=89=A9?= =?UTF-8?q?=E8=81=94=E7=BD=91=E3=80=91tcp=20=E5=8D=8F=E8=AE=AE=E7=9A=84?= =?UTF-8?q?=E6=8E=A5=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tcp/IotTcpBinaryDeviceMessageCodec.java | 75 +++++++++---------- .../tcp/IotTcpJsonDeviceMessageCodec.java | 14 ++-- .../protocol/tcp/IotTcpUpstreamProtocol.java | 6 +- .../tcp/manager/IotTcpConnectionManager.java | 13 ++-- .../tcp/router/IotTcpDownstreamHandler.java | 1 - .../tcp/router/IotTcpUpstreamHandler.java | 71 +++++++++--------- 6 files changed, 90 insertions(+), 90 deletions(-) diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java index 9ecaa8af6f..8279ca2471 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java @@ -20,11 +20,11 @@ import java.nio.charset.StandardCharsets; * *
  * +--------+--------+--------+--------+--------+--------+--------+--------+
- * | 魔术字 | 版本号 | 消息类型| 消息标志|         消息长度(4字节)          |
+ * | 魔术字 | 版本号 | 消息类型| 消息标志|         消息长度(4 字节)          |
  * +--------+--------+--------+--------+--------+--------+--------+--------+
- * |           消息 ID 长度(2字节)        |      消息 ID (变长字符串)         |
+ * |           消息 ID 长度(2 字节)        |      消息 ID (变长字符串)         |
  * +--------+--------+--------+--------+--------+--------+--------+--------+
- * |           方法名长度(2字节)        |      方法名(变长字符串)         |
+ * |           方法名长度(2 字节)        |      方法名(变长字符串)         |
  * +--------+--------+--------+--------+--------+--------+--------+--------+
  * |                        消息体数据(变长)                              |
  * +--------+--------+--------+--------+--------+--------+--------+--------+
@@ -56,12 +56,21 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec {
      */
     private static final byte PROTOCOL_VERSION = (byte) 0x01;
 
+    // TODO @haohao:这个要不直接静态枚举,不用 MessageType
     /**
      * 消息类型常量
      */
     public static class MessageType {
-        public static final byte REQUEST = 0x01; // 请求消息
-        public static final byte RESPONSE = 0x02; // 响应消息
+
+        /**
+         * 请求消息
+         */
+        public static final byte REQUEST = 0x01;
+        /**
+         * 响应消息
+         */
+        public static final byte RESPONSE = 0x02;
+
     }
 
     /**
@@ -83,17 +92,13 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec {
     public byte[] encode(IotDeviceMessage message) {
         Assert.notNull(message, "消息不能为空");
         Assert.notBlank(message.getMethod(), "消息方法不能为空");
-
         try {
             // 1. 确定消息类型
             byte messageType = determineMessageType(message);
-
             // 2. 构建消息体
             byte[] bodyData = buildMessageBody(message, messageType);
-
             // 3. 构建完整消息(不包含deviceId,由连接上下文管理)
             return buildCompleteMessage(message, messageType, bodyData);
-
         } catch (Exception e) {
             log.error("[encode][TCP 二进制消息编码失败,消息: {}]", message, e);
             throw new RuntimeException("TCP 二进制消息编码失败: " + e.getMessage(), e);
@@ -104,16 +109,12 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec {
     public IotDeviceMessage decode(byte[] bytes) {
         Assert.notNull(bytes, "待解码数据不能为空");
         Assert.isTrue(bytes.length >= MIN_MESSAGE_LENGTH, "数据包长度不足");
-
         try {
             Buffer buffer = Buffer.buffer(bytes);
-
             // 1. 解析协议头部
             ProtocolHeader header = parseProtocolHeader(buffer);
-
             // 2. 解析消息内容(不包含deviceId,由上层连接管理器设置)
             return parseMessageContent(buffer, header);
-
         } catch (Exception e) {
             log.error("[decode][TCP 二进制消息解码失败,数据长度: {}]", bytes.length, e);
             throw new RuntimeException("TCP 二进制消息解码失败: " + e.getMessage(), e);
@@ -128,6 +129,7 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec {
      */
     private byte determineMessageType(IotDeviceMessage message) {
         // 判断是否为响应消息:有响应码或响应消息时为响应
+        // TODO @haohao:感觉只判断 code 更稳妥点?msg 有可能空。。。
         if (message.getCode() != null || StrUtil.isNotBlank(message.getMsg())) {
             return MessageType.RESPONSE;
         }
@@ -140,27 +142,26 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec {
      */
     private byte[] buildMessageBody(IotDeviceMessage message, byte messageType) {
         Buffer bodyBuffer = Buffer.buffer();
-
         if (messageType == MessageType.RESPONSE) {
-            // 响应消息:code + msg长度 + msg + data
+            // code
             bodyBuffer.appendInt(message.getCode() != null ? message.getCode() : 0);
-
+            // msg
             String msg = message.getMsg() != null ? message.getMsg() : "";
             byte[] msgBytes = msg.getBytes(StandardCharsets.UTF_8);
             bodyBuffer.appendShort((short) msgBytes.length);
             bodyBuffer.appendBytes(msgBytes);
-
+            // data
             if (message.getData() != null) {
                 bodyBuffer.appendBytes(JsonUtils.toJsonByte(message.getData()));
             }
         } else {
-            // 请求消息:包含 params 或 data
+            // params
+            // TODO @haohao:请求是不是只处理 message.getParams() 哈?
             Object payload = message.getParams() != null ? message.getParams() : message.getData();
             if (payload != null) {
                 bodyBuffer.appendBytes(JsonUtils.toJsonByte(payload));
             }
         }
-
         return bodyBuffer.getBytes();
     }
 
@@ -169,35 +170,30 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec {
      */
     private byte[] buildCompleteMessage(IotDeviceMessage message, byte messageType, byte[] bodyData) {
         Buffer buffer = Buffer.buffer();
-
         // 1. 写入协议头部
         buffer.appendByte(MAGIC_NUMBER);
         buffer.appendByte(PROTOCOL_VERSION);
         buffer.appendByte(messageType);
-        buffer.appendByte((byte) 0x00); // 消息标志,预留字段
-
-        // 2. 预留消息长度位置
+        buffer.appendByte((byte) 0x00); // 消息标志,预留字段 TODO @haohao:这个标识的作用是啥呀?
+        // 2. 预留消息长度位置(在 6. 更新消息长度)
         int lengthPosition = buffer.length();
         buffer.appendInt(0);
-
-        // 3. 写入消息ID
+        // 3. 写入消息 ID
         String messageId = StrUtil.isNotBlank(message.getRequestId()) ? message.getRequestId()
+                // TODO @haohao:复用 IotDeviceMessageUtils 的 generateMessageId 哇?
                 : generateMessageId(message.getMethod());
+        // TODO @haohao:StrUtil.utf8Bytes()
         byte[] messageIdBytes = messageId.getBytes(StandardCharsets.UTF_8);
         buffer.appendShort((short) messageIdBytes.length);
         buffer.appendBytes(messageIdBytes);
-
         // 4. 写入方法名
         byte[] methodBytes = message.getMethod().getBytes(StandardCharsets.UTF_8);
         buffer.appendShort((short) methodBytes.length);
         buffer.appendBytes(methodBytes);
-
         // 5. 写入消息体
         buffer.appendBytes(bodyData);
-
         // 6. 更新消息长度
         buffer.setInt(lengthPosition, buffer.length());
-
         return buffer.getBytes();
     }
 
@@ -210,16 +206,15 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec {
 
     // ==================== 解码相关方法 ====================
 
+    // TODO @haohao:是不是把 parseProtocolHeader、parseMessageContent 合并?
     /**
      * 解析协议头部
      */
     private ProtocolHeader parseProtocolHeader(Buffer buffer) {
         int index = 0;
-
         // 1. 验证魔术字
         byte magic = buffer.getByte(index++);
         Assert.isTrue(magic == MAGIC_NUMBER, "无效的协议魔术字: " + magic);
-
         // 2. 验证版本号
         byte version = buffer.getByte(index++);
         Assert.isTrue(version == PROTOCOL_VERSION, "不支持的协议版本: " + version);
@@ -227,7 +222,6 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec {
         // 3. 读取消息类型
         byte messageType = buffer.getByte(index++);
         Assert.isTrue(isValidMessageType(messageType), "无效的消息类型: " + messageType);
-
         // 4. 读取消息标志(暂时跳过)
         byte messageFlags = buffer.getByte(index++);
 
@@ -235,7 +229,8 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec {
         int messageLength = buffer.getInt(index);
         index += 4;
 
-        Assert.isTrue(messageLength == buffer.length(), "消息长度不匹配,期望: " + messageLength + ", 实际: " + buffer.length());
+        Assert.isTrue(messageLength == buffer.length(),
+                "消息长度不匹配,期望: " + messageLength + ", 实际: " + buffer.length());
 
         return new ProtocolHeader(magic, version, messageType, messageFlags, messageLength, index);
     }
@@ -246,7 +241,7 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec {
     private IotDeviceMessage parseMessageContent(Buffer buffer, ProtocolHeader header) {
         int index = header.getNextIndex();
 
-        // 1. 读取消息ID
+        // 1. 读取消息 ID
         short messageIdLength = buffer.getShort(index);
         index += 2;
         String messageId = buffer.getString(index, index + messageIdLength, StandardCharsets.UTF_8.name());
@@ -314,12 +309,8 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec {
         if (startIndex >= endIndex) {
             return null;
         }
-
         try {
             String jsonStr = buffer.getString(startIndex, endIndex, StandardCharsets.UTF_8.name());
-            if (StrUtil.isBlank(jsonStr)) {
-                return null;
-            }
             return JsonUtils.parseObject(jsonStr, Object.class);
         } catch (Exception e) {
             log.warn("[parseJsonData][JSON 解析失败,返回原始字符串]", e);
@@ -329,6 +320,7 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec {
 
     // ==================== 辅助方法 ====================
 
+    // TODO @haohao:这个貌似只用一次,可以考虑不抽小方法哈;
     /**
      * 验证消息类型是否有效
      */
@@ -344,11 +336,16 @@ public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec {
     @Data
     @AllArgsConstructor
     private static class ProtocolHeader {
+
         private byte magic;
         private byte version;
         private byte messageType;
         private byte messageFlags;
         private int messageLength;
-        private int nextIndex; // 指向消息内容开始位置
+        /**
+         * 指向消息内容开始位置
+         */
+        private int nextIndex;
+
     }
 }
\ No newline at end of file
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java
index e4ff2f50bc..8f31305f17 100644
--- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java
@@ -14,13 +14,13 @@ import org.springframework.stereotype.Component;
  *
  * 采用纯 JSON 格式传输,格式如下:
  * {
- * "id": "消息 ID",
- * "method": "消息方法",
- * "params": {...}, // 请求参数
- * "data": {...}, // 响应结果
- * "code": 200, // 响应错误码
- * "msg": "success", // 响应提示
- * "timestamp": 时间戳
+ *     "id": "消息 ID",
+ *     "method": "消息方法",
+ *     "params": {...}, // 请求参数
+ *     "data": {...}, // 响应结果
+ *     "code": 200, // 响应错误码
+ *     "msg": "success", // 响应提示
+ *     "timestamp": 时间戳
  * }
  *
  * @author 芋道源码
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java
index 0d0cdd0f08..791c6cbfc2 100644
--- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java
@@ -39,10 +39,10 @@ public class IotTcpUpstreamProtocol {
     private NetServer tcpServer;
 
     public IotTcpUpstreamProtocol(IotGatewayProperties.TcpProperties tcpProperties,
-            IotDeviceService deviceService,
-            IotDeviceMessageService messageService,
+                                  IotDeviceService deviceService,
+                                  IotDeviceMessageService messageService,
                                   IotTcpConnectionManager connectionManager,
-            Vertx vertx) {
+                                  Vertx vertx) {
         this.tcpProperties = tcpProperties;
         this.deviceService = deviceService;
         this.messageService = messageService;
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java
index 3ab7470005..520861e51e 100644
--- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java
@@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager;
 
 import io.vertx.core.net.NetSocket;
 import lombok.Data;
-import lombok.experimental.Accessors;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Component;
 
@@ -62,9 +61,9 @@ public class IotTcpConnectionManager {
                 .setDeviceId(deviceId)
                 .setAuthInfo(authInfo)
                 .setAuthenticated(true);
-
         connectionMap.put(socket, connectionInfo);
         deviceSocketMap.put(deviceId, socket);
+        // TODO @haohao:socketDeviceMap 和 connectionMap 会重复哇?connectionMap.get(socket).getDeviceId
         socketDeviceMap.put(socket, deviceId);
 
         log.info("[registerConnection][注册设备连接,设备 ID: {},连接: {},product key: {},device name: {}]",
@@ -79,7 +78,6 @@ public class IotTcpConnectionManager {
     public void unregisterConnection(NetSocket socket) {
         ConnectionInfo connectionInfo = connectionMap.remove(socket);
         Long deviceId = socketDeviceMap.remove(socket);
-
         if (connectionInfo != null && deviceId != null) {
             deviceSocketMap.remove(deviceId);
             log.info("[unregisterConnection][注销设备连接,设备 ID: {},连接: {}]",
@@ -87,6 +85,7 @@ public class IotTcpConnectionManager {
         }
     }
 
+    // TODO @haohao:用不到,要不暂时清理哈。
     /**
      * 注销设备连接(通过设备 ID)
      *
@@ -160,26 +159,30 @@ public class IotTcpConnectionManager {
         }
     }
 
+    // TODO @haohao:ConnectionInfo 和 AuthInfo 是不是可以融合哈?
+
     /**
      * 连接信息
      */
     @Data
-    @Accessors(chain = true)
     public static class ConnectionInfo {
+
         private Long deviceId;
         private AuthInfo authInfo;
         private boolean authenticated;
+
     }
 
     /**
      * 认证信息
      */
     @Data
-    @Accessors(chain = true)
     public static class AuthInfo {
+
         private Long deviceId;
         private String productKey;
         private String deviceName;
         private String clientId;
+
     }
 }
\ No newline at end of file
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java
index fd352f3b44..3ee31d82e4 100644
--- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java
@@ -47,7 +47,6 @@ public class IotTcpDownstreamHandler {
             byte[] bytes = deviceMessageService.encodeDeviceMessage(message, deviceInfo.getProductKey(),
                     deviceInfo.getDeviceName());
             boolean success = connectionManager.sendToDevice(message.getDeviceId(), bytes);
-
             if (success) {
                 log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]",
                         message.getDeviceId(), message.getMethod(), message.getId(), bytes.length);
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java
index 29cda53228..627daad680 100644
--- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java
@@ -37,6 +37,7 @@ public class IotTcpUpstreamHandler implements Handler {
 
     private static final String CODEC_TYPE_JSON = IotTcpJsonDeviceMessageCodec.TYPE;
     private static final String CODEC_TYPE_BINARY = IotTcpBinaryDeviceMessageCodec.TYPE;
+
     private static final String AUTH_METHOD = "auth";
 
     private final IotDeviceMessageService deviceMessageService;
@@ -49,8 +50,10 @@ public class IotTcpUpstreamHandler implements Handler {
 
     private final String serverId;
 
-    public IotTcpUpstreamHandler(IotTcpUpstreamProtocol protocol, IotDeviceMessageService deviceMessageService,
-            IotDeviceService deviceService, IotTcpConnectionManager connectionManager) {
+    public IotTcpUpstreamHandler(IotTcpUpstreamProtocol protocol,
+                                 IotDeviceMessageService deviceMessageService,
+                                 IotDeviceService deviceService,
+                                 IotTcpConnectionManager connectionManager) {
         this.deviceMessageService = deviceMessageService;
         this.deviceService = deviceService;
         this.connectionManager = connectionManager;
@@ -68,12 +71,12 @@ public class IotTcpUpstreamHandler implements Handler {
             log.warn("[handle][连接异常,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress());
             cleanupConnection(socket);
         });
-
         socket.closeHandler(v -> {
             log.debug("[handle][连接关闭,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress());
             cleanupConnection(socket);
         });
 
+        // 设置消息处理器
         socket.handler(buffer -> processMessage(clientId, buffer, socket));
     }
 
@@ -82,26 +85,24 @@ public class IotTcpUpstreamHandler implements Handler {
      */
     private void processMessage(String clientId, Buffer buffer, NetSocket socket) {
         try {
-            // 1. 数据包基础检查
+            // 1.1 数据包基础检查
             if (buffer.length() == 0) {
                 return;
             }
-
-            // 2. 解码消息
+            // 1.2 解码消息
             MessageInfo messageInfo = decodeMessage(buffer);
             if (messageInfo == null) {
                 return;
             }
 
-            // 3. 根据消息类型路由处理
+            // 2. 根据消息类型路由处理
             if (isAuthRequest(messageInfo.message)) {
-                // 认证请求:无需检查认证状态
+                // 认证请求
                 handleAuthenticationRequest(clientId, messageInfo, socket);
             } else {
-                // 业务消息:需要检查认证状态
+                // 业务消息
                 handleBusinessRequest(clientId, messageInfo, socket);
             }
-
         } catch (Exception e) {
             log.error("[processMessage][处理消息失败,客户端 ID: {}]", clientId, e);
         }
@@ -112,16 +113,14 @@ public class IotTcpUpstreamHandler implements Handler {
      */
     private void handleAuthenticationRequest(String clientId, MessageInfo messageInfo, NetSocket socket) {
         try {
+            // 1.1 解析认证参数
             IotDeviceMessage message = messageInfo.message;
-
-            // 1. 解析认证参数
             AuthParams authParams = parseAuthParams(message.getParams());
             if (authParams == null) {
                 sendError(socket, message.getRequestId(), "认证参数不完整", messageInfo.codecType);
                 return;
             }
-
-            // 2. 执行认证
+            // 1.2 执行认证
             if (!authenticateDevice(authParams)) {
                 log.warn("[handleAuthenticationRequest][认证失败,客户端 ID: {},username: {}]",
                         clientId, authParams.username);
@@ -129,14 +128,13 @@ public class IotTcpUpstreamHandler implements Handler {
                 return;
             }
 
-            // 3. 解析设备信息
+            // 2.1 解析设备信息
             IotDeviceAuthUtils.DeviceInfo deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.username);
             if (deviceInfo == null) {
                 sendError(socket, message.getRequestId(), "解析设备信息失败", messageInfo.codecType);
                 return;
             }
-
-            // 4. 获取设备信息
+            // 2.2 获取设备信息
             IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(),
                     deviceInfo.getDeviceName());
             if (device == null) {
@@ -144,14 +142,12 @@ public class IotTcpUpstreamHandler implements Handler {
                 return;
             }
 
-            // 5. 注册连接并发送成功响应
+            // 3. 注册连接并发送成功响应
             registerConnection(socket, device, deviceInfo, authParams.clientId);
             sendOnlineMessage(deviceInfo);
             sendSuccess(socket, message.getRequestId(), "认证成功", messageInfo.codecType);
-
             log.info("[handleAuthenticationRequest][认证成功,设备 ID: {},设备名: {}]",
                     device.getId(), deviceInfo.getDeviceName());
-
         } catch (Exception e) {
             log.error("[handleAuthenticationRequest][认证处理异常,客户端 ID: {}]", clientId, e);
             sendError(socket, messageInfo.message.getRequestId(), "认证处理异常", messageInfo.codecType);
@@ -173,25 +169,23 @@ public class IotTcpUpstreamHandler implements Handler {
             // 2. 获取认证信息并处理业务消息
             IotTcpConnectionManager.AuthInfo authInfo = connectionManager.getAuthInfo(socket);
             processBusinessMessage(clientId, messageInfo.message, authInfo);
-
         } catch (Exception e) {
             log.error("[handleBusinessRequest][业务请求处理异常,客户端 ID: {}]", clientId, e);
         }
     }
 
+    // TODO @haohao:processBusinessMessage 这个小方法,直接融合到 handleBusinessRequest 里?读起来更聚集点
     /**
      * 处理业务消息
      */
     private void processBusinessMessage(String clientId, IotDeviceMessage message,
-            IotTcpConnectionManager.AuthInfo authInfo) {
+                                        IotTcpConnectionManager.AuthInfo authInfo) {
         try {
             message.setDeviceId(authInfo.getDeviceId());
             message.setServerId(serverId);
-
             // 发送到消息总线
             deviceMessageService.sendDeviceMessage(message, authInfo.getProductKey(),
                     authInfo.getDeviceName(), serverId);
-
         } catch (Exception e) {
             log.error("[processBusinessMessage][业务消息处理失败,客户端 ID: {},消息 ID: {}]",
                     clientId, message.getId(), e);
@@ -200,28 +194,27 @@ public class IotTcpUpstreamHandler implements Handler {
 
     /**
      * 解码消息
+     *
+     * @param buffer 消息
      */
     private MessageInfo decodeMessage(Buffer buffer) {
         if (buffer == null || buffer.length() == 0) {
             return null;
         }
-
         // 1. 快速检测消息格式类型
+        // TODO @haohao:是不是进一步优化?socket 建立认证后,那条消息已经定义了所有消息的格式哈?
         String codecType = detectMessageFormat(buffer);
-
         try {
             // 2. 使用检测到的格式进行解码
             IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(buffer.getBytes(), codecType);
-
             if (message == null) {
                 return null;
             }
-
             return new MessageInfo(message, codecType);
-
         } catch (Exception e) {
             log.warn("[decodeMessage][消息解码失败,格式: {},数据长度: {},错误: {}]",
                     codecType, buffer.length(), e.getMessage());
+            // TODO @haohao:一般消息格式不对,应该抛出异常,断开连接居多?
             return null;
         }
     }
@@ -231,8 +224,10 @@ public class IotTcpUpstreamHandler implements Handler {
      * 优化性能:避免不必要的字符串转换
      */
     private String detectMessageFormat(Buffer buffer) {
+        // TODO @haohao:是不是 IotTcpBinaryDeviceMessageCodec 提供一个 isBinaryFormat 方法哈?
+        // 默认使用 JSON
         if (buffer.length() == 0) {
-            return CODEC_TYPE_JSON; // 默认使用 JSON
+            return CODEC_TYPE_JSON;
         }
 
         // 1. 优先检测二进制格式(检查魔术字节 0x7E)
@@ -241,6 +236,7 @@ public class IotTcpUpstreamHandler implements Handler {
         }
 
         // 2. 检测 JSON 格式(检查前几个有效字符)
+        // TODO @haohao:这个检测去掉?直接 return CODEC_TYPE_JSON 更简洁一点。
         if (isJsonFormat(buffer)) {
             return CODEC_TYPE_JSON;
         }
@@ -295,14 +291,14 @@ public class IotTcpUpstreamHandler implements Handler {
      * 注册连接信息
      */
     private void registerConnection(NetSocket socket, IotDeviceRespDTO device,
-            IotDeviceAuthUtils.DeviceInfo deviceInfo, String clientId) {
+                                    IotDeviceAuthUtils.DeviceInfo deviceInfo, String clientId) {
+        // TODO @haohao:AuthInfo 的创建,放在 connectionManager 里构建貌似会更收敛一点?
         // 创建认证信息
         IotTcpConnectionManager.AuthInfo authInfo = new IotTcpConnectionManager.AuthInfo()
                 .setDeviceId(device.getId())
                 .setProductKey(deviceInfo.getProductKey())
                 .setDeviceName(deviceInfo.getDeviceName())
                 .setClientId(clientId);
-
         // 注册连接
         connectionManager.registerConnection(socket, device.getId(), authInfo);
     }
@@ -377,15 +373,12 @@ public class IotTcpUpstreamHandler implements Handler {
         if (params == null) {
             return null;
         }
-
         try {
             JSONObject paramsJson = params instanceof JSONObject ? (JSONObject) params
                     : JSONUtil.parseObj(params.toString());
-
             String clientId = paramsJson.getStr("clientId");
             String username = paramsJson.getStr("username");
             String password = paramsJson.getStr("password");
-
             return StrUtil.hasBlank(clientId, username, password) ? null
                     : new AuthParams(clientId, username, password);
         } catch (Exception e) {
@@ -410,6 +403,8 @@ public class IotTcpUpstreamHandler implements Handler {
         }
     }
 
+    // TODO @haohao:改成 sendErrorResponse sendSuccessResponse 更清晰点?
+
     /**
      * 发送错误响应
      */
@@ -426,15 +421,18 @@ public class IotTcpUpstreamHandler implements Handler {
 
     // ==================== 内部类 ====================
 
+    // TODO @haohao:IotDeviceAuthReqDTO 复用这个?
     /**
      * 认证参数
      */
     @Data
     @AllArgsConstructor
     private static class AuthParams {
+
         private final String clientId;
         private final String username;
         private final String password;
+
     }
 
     /**
@@ -443,7 +441,10 @@ public class IotTcpUpstreamHandler implements Handler {
     @Data
     @AllArgsConstructor
     private static class MessageInfo {
+
         private final IotDeviceMessage message;
+
         private final String codecType;
+
     }
 }
\ No newline at end of file

From da6b970a8de15f11ff730ebe782f515f5f6806e8 Mon Sep 17 00:00:00 2001
From: YunaiV 
Date: Sat, 2 Aug 2025 11:38:32 +0800
Subject: [PATCH 89/89] =?UTF-8?q?=E3=80=90=E5=90=8C=E6=AD=A5=E3=80=91BOOT?=
 =?UTF-8?q?=20=E5=92=8C=20CLOUD=20=E7=9A=84=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../controller/admin/device/vo/device/IotDeviceRespVO.java    | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceRespVO.java
index 7b4e498802..ecb8f81c45 100644
--- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceRespVO.java
+++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceRespVO.java
@@ -1,10 +1,10 @@
 package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device;
 
+import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
+import cn.idev.excel.annotation.ExcelProperty;
 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
 import cn.iocoder.yudao.module.iot.enums.DictTypeConstants;
-import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
-import com.alibaba.excel.annotation.ExcelProperty;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;