Commit 0caf6464028614824ab4dc7c491815e5b45897d7

Authored by YunaiV
1 parent 50adebd1

fix(iot): 使用复合主键修复设备消息日志同毫秒覆盖问题

- 将 device_message 改为 ts + id 复合主键,保留同毫秒的不同消息
- 消息 id 为空时生成 UUID,并补充兜底单测
- 查询按 ts、id 倒序排序,保证同毫秒数据分页稳定

验证:消息服务 14 个单测通过,真实 TDengine 3.3.8.4 集成验证通过
注意:已有消息表需单独迁移,服务端需支持 VARCHAR 复合键

相关讨论:https://t.zsxq.com/YP2ao
相关 PR:https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/1598
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageServiceImpl.java
@@ -95,6 +95,9 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { @@ -95,6 +95,9 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
95 if (messageDO.getData() != null) { 95 if (messageDO.getData() != null) {
96 messageDO.setData(JsonUtils.toJsonString(messageDO.getData())); 96 messageDO.setData(JsonUtils.toJsonString(messageDO.getData()));
97 } 97 }
  98 + if (StrUtil.isEmpty(messageDO.getId())) {
  99 + messageDO.setId(IotDeviceMessageUtils.generateMessageId());
  100 + }
98 if (messageDO.getTs() == null) { 101 if (messageDO.getTs() == null) {
99 messageDO.setTs(System.currentTimeMillis()); 102 messageDO.setTs(System.currentTimeMillis());
100 } 103 }
yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/device/IotDeviceMessageMapper.xml
@@ -7,7 +7,7 @@ @@ -7,7 +7,7 @@
7 <update id="createSTable"> 7 <update id="createSTable">
8 CREATE STABLE IF NOT EXISTS device_message ( 8 CREATE STABLE IF NOT EXISTS device_message (
9 ts TIMESTAMP, 9 ts TIMESTAMP,
10 - id NCHAR(50), 10 + id VARCHAR(50) COMPOSITE KEY,
11 report_time TIMESTAMP, 11 report_time TIMESTAMP,
12 tenant_id BIGINT, 12 tenant_id BIGINT,
13 server_id NCHAR(50), 13 server_id NCHAR(50),
@@ -63,7 +63,7 @@ @@ -63,7 +63,7 @@
63 AND identifier = #{reqVO.identifier} 63 AND identifier = #{reqVO.identifier}
64 </if> 64 </if>
65 </where> 65 </where>
66 - ORDER BY ts DESC 66 + ORDER BY ts DESC, id DESC
67 </select> 67 </select>
68 68
69 <select id="selectListByRequestIdsAndReply" resultType="cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceMessageDO"> 69 <select id="selectListByRequestIdsAndReply" resultType="cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceMessageDO">
@@ -76,7 +76,7 @@ @@ -76,7 +76,7 @@
76 <foreach collection="requestIds" item="requestId" open="(" close=")" separator=","> 76 <foreach collection="requestIds" item="requestId" open="(" close=")" separator=",">
77 #{requestId} 77 #{requestId}
78 </foreach> 78 </foreach>
79 - ORDER BY ts DESC 79 + ORDER BY ts DESC, id DESC
80 </select> 80 </select>
81 81
82 <select id="selectCountByCreateTime" resultType="Long"> 82 <select id="selectCountByCreateTime" resultType="Long">
@@ -106,4 +106,4 @@ @@ -106,4 +106,4 @@
106 GROUP BY TIMETRUNCATE(ts, 1h) 106 GROUP BY TIMETRUNCATE(ts, 1h)
107 </select> 107 </select>
108 108
109 -</mapper>  
110 \ No newline at end of file 109 \ No newline at end of file
  110 +</mapper>
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageServiceImplTest.java
@@ -104,6 +104,21 @@ public class IotDeviceMessageServiceImplTest extends BaseMockitoUnitTest { @@ -104,6 +104,21 @@ public class IotDeviceMessageServiceImplTest extends BaseMockitoUnitTest {
104 } 104 }
105 105
106 @Test 106 @Test
  107 + public void testCreateDeviceLogAsync_idFallback_whenBlank() {
  108 + // 准备:构造一条 id 为空的消息,复合主键字段不能为 null
  109 + IotDeviceMessage message = buildMessage(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod())
  110 + .setId(null);
  111 +
  112 + // 调用
  113 + service.createDeviceLogAsync(message);
  114 +
  115 + // 断言:mapper.insert 接收到的 messageDO 已补充 id
  116 + ArgumentCaptor<IotDeviceMessageDO> captor = ArgumentCaptor.forClass(IotDeviceMessageDO.class);
  117 + verify(deviceMessageMapper).insert(captor.capture());
  118 + assertNotNull(captor.getValue().getId(), "id 不应为空");
  119 + }
  120 +
  121 + @Test
107 public void testCreateDeviceLogAsync_swallowMapperException() { 122 public void testCreateDeviceLogAsync_swallowMapperException() {
108 // 准备:mapper.insert 抛异常,验证 @Async 方法内部 try/catch 兜底,不向上抛 123 // 准备:mapper.insert 抛异常,验证 @Async 方法内部 try/catch 兜底,不向上抛
109 IotDeviceMessage message = buildMessage(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod()); 124 IotDeviceMessage message = buildMessage(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod());