From b78295e8da96c3e5e76befeef9582ce5273dbb74 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期二, 29 九月 2026 13:10:23 +0800
Subject: [PATCH] 按代码规范检查整改实体注释、Service Javadoc 与分层边界
---
battery-business/src/main/java/com/battery/business/domain/TBatteryGroup.java | 21 +
battery-business/src/main/java/com/battery/business/domain/TTestConsoleDetail.java | 4
ruoyi-admin/src/main/java/com/battery/business/web/TTestController.java | 7
battery-business/src/main/java/com/battery/business/domain/vo/DiscoveredCabinetVo.java | 10
battery-business/src/main/java/com/battery/business/domain/vo/TestBatteryOptionVo.java | 5
battery-business/src/main/java/com/battery/business/service/TodoService.java | 14
ruoyi-admin/src/main/java/com/battery/business/web/TodoController.java | 6
battery-business/src/main/resources/mapper/battery/TTestMapper.xml | 1
battery-device-api/src/main/java/com/battery/device/api/model/DeviceIdentity.java | 1
battery-business/src/main/java/com/battery/business/service/impl/RoutingTestDataObservationAdapter.java | 61 +++
battery-device-bts/src/main/java/com/battery/device/bts/BtsTransportException.java | 1
battery-business/src/main/resources/mapper/battery/TDeviceIngestRecordMapper.xml | 10
battery-device-api/src/main/java/com/battery/device/api/model/DeviceWorkStatus.java | 17
battery-business/src/main/java/com/battery/business/service/impl/TestDataReceptionServiceImpl.java | 73 +++
battery-business/src/main/java/com/battery/business/domain/vo/TDeviceIngestChannel.java | 5
ruoyi-admin/src/main/java/com/battery/business/runtime/RedisBtsSimulatorStateStore.java | 5
battery-business/src/main/java/com/battery/business/domain/vo/OfflineTestSource.java | 7
battery-business/src/main/java/com/battery/business/battery/dto/BatteryGroupDto.java | 1
battery-device-bts/src/main/java/com/battery/device/bts/BtsXmlCodec.java | 4
ruoyi-admin/src/main/java/com/battery/business/runtime/DeviceAdapterRuntimeConfig.java | 1
battery-business/src/main/java/com/battery/business/domain/TTestStepRecord.java | 53 ++
battery-business/src/main/java/com/battery/business/domain/TDeviceIngestRecord.java | 25 +
battery-device-api/src/main/java/com/battery/device/api/registry/DeviceAdapterRegistry.java | 2
battery-business/src/main/java/com/battery/business/domain/TDetectionCabinet.java | 4
ruoyi-admin/src/main/java/com/battery/business/web/TBatteryGroupController.java | 7
ruoyi-admin/src/main/java/com/battery/business/web/TestLiveStream.java | 18
battery-business/src/main/java/com/battery/business/domain/TTestLogicStepMapping.java | 4
ruoyi-admin/src/main/java/com/battery/business/runtime/FlywayMigrationConfig.java | 1
battery-device-api/src/main/java/com/battery/device/api/model/ChannelIdentity.java | 1
battery-business/src/main/java/com/battery/business/domain/TBatteryGroupRelationField.java | 10
battery-business/src/main/java/com/battery/business/domain/TCollector.java | 2
ruoyi-admin/src/main/java/com/battery/business/runtime/RedisDeviceDiscoveryCandidateStore.java | 8
ruoyi-admin/src/main/java/com/battery/business/web/TTestWarningExceptionController.java | 22
battery-business/src/main/java/com/battery/business/domain/vo/TestChannelOptionVo.java | 10
ruoyi-admin/src/test/java/com/battery/business/service/TodoServiceTest.java | 10
battery-business/src/main/java/com/battery/business/service/impl/DeviceAdapterRuntimeCoordinator.java | 2
ruoyi-admin/src/main/java/com/battery/business/runtime/DevelopmentSimulatorConfig.java | 6
battery-business/src/main/java/com/battery/business/service/impl/DeviceIngestServiceImpl.java | 16
battery-business/src/main/java/com/battery/business/service/impl/PushIngestAdapter.java | 9
battery-device-bts/src/main/java/com/battery/device/bts/BtsTcpTransport.java | 6
battery-device-neware/src/main/java/com/battery/device/neware/NewareStepDirectionResolver.java | 1
battery-device-bts/src/main/java/com/battery/device/bts/BtsDeviceAdapter.java | 5
battery-device-neware/src/main/java/com/battery/device/neware/NewareMysqlAdapterFactory.java | 2
battery-device-api/src/main/java/com/battery/device/api/model/DetectedDeviceMetadata.java | 1
ruoyi-admin/src/main/java/com/battery/business/web/TTestDataRevisionController.java | 2
battery-business/src/main/java/com/battery/business/domain/TTest.java | 38 +
ruoyi-admin/src/main/java/com/battery/business/runtime/BtsDevelopmentSimulatorWebConfig.java | 1
battery-device-api/src/main/java/com/battery/device/api/model/DeviceObservation.java | 1
battery-device-bts/src/main/java/com/battery/device/bts/BtsProtocolException.java | 1
battery-business/src/main/java/com/battery/business/domain/TTestWarningThreshold.java | 10
battery-business/src/main/java/com/battery/business/service/impl/TTestServiceImpl.java | 157 +++++++
battery-business/src/main/java/com/battery/business/domain/TTestWarningException.java | 25 +
battery-device-neware/src/main/java/com/battery/device/neware/SingleConnectionVendorDataSource.java | 5
battery-device-simulator/src/main/java/com/battery/device/simulator/BtsDevelopmentSimulator.java | 4
ruoyi-admin/src/main/java/com/battery/business/runtime/TestDataReceptionScheduler.java | 1
battery-business/src/main/java/com/battery/business/domain/TBatteryGroupBattery.java | 7
battery-business/src/main/java/com/battery/business/domain/vo/TestUserOptionVo.java | 3
ruoyi-admin/src/test/java/com/battery/business/service/impl/TestWarningExceptionServiceTest.java | 14
battery-device-bts/src/main/java/com/battery/device/bts/BtsDeviceAdapterFactory.java | 2
battery-business/src/main/java/com/battery/business/domain/TBatteryGroupField.java | 2
battery-business/src/main/java/com/battery/business/domain/TTestChannelBinding.java | 21 +
battery-business/src/main/java/com/battery/business/domain/TTestRealtimeRecord.java | 42 ++
battery-business/src/main/java/com/battery/business/domain/vo/AdapterDiscoveryResult.java | 8
battery-business/src/main/java/com/battery/business/domain/DataRevisionItem.java | 2
battery-business/src/main/java/com/battery/business/domain/TTestDataRevisionAction.java | 20
battery-device-api/src/main/java/com/battery/device/api/model/AdapterCapabilities.java | 1
battery-business/src/main/java/com/battery/business/domain/TDeviceAdapter.java | 18
battery-business/src/main/java/com/battery/business/domain/vo/DeviceRelatedTestVo.java | 4
battery-business/src/main/java/com/battery/business/domain/vo/TestConfigurationOptionsVo.java | 7
battery-device-api/src/main/java/com/battery/device/api/discovery/InMemoryDeviceDiscoveryCandidateStore.java | 4
battery-device-neware/src/main/java/com/battery/device/neware/ReadOnlyVendorDataSource.java | 6
battery-business/src/main/java/com/battery/business/domain/TDetectionChannel.java | 18
battery-business/src/main/java/com/battery/business/service/impl/PushIngestAdapterFactory.java | 7
battery-business/src/main/java/com/battery/business/domain/CurrentBatteryField.java | 7
battery-business/src/main/java/com/battery/business/domain/vo/TestCollectorOptionVo.java | 4
battery-business/src/main/java/com/battery/business/domain/TTestBattery.java | 13
battery-business/src/main/java/com/battery/business/service/impl/TestWarningExceptionService.java | 78 +++
battery-device-simulator/src/main/java/com/battery/device/simulator/ScriptedBtsTcpSimulator.java | 27 +
battery-business/src/main/java/com/battery/business/domain/TTestCollectorChannel.java | 9
battery-device-neware/src/main/java/com/battery/device/neware/NewareMysqlAdapter.java | 3
battery-device-simulator/src/main/java/com/battery/device/simulator/InMemoryBtsSimulatorStateStore.java | 5
battery-business/src/main/java/com/battery/business/domain/TTestCycleRecord.java | 50 ++
battery-business/src/main/java/com/battery/business/service/impl/TBatteryGroupTemplateServiceImpl.java | 36 +
battery-business/src/main/java/com/battery/business/test/dto/TestBatteryBindingDto.java | 2
battery-business/src/main/java/com/battery/business/domain/vo/TestTypeOptionVo.java | 2
battery-device-bts/src/test/java/com/battery/device/bts/ScriptedBtsTcpSimulator.java | 19
86 files changed, 1,080 insertions(+), 85 deletions(-)
diff --git a/battery-business/src/main/java/com/battery/business/battery/dto/BatteryGroupDto.java b/battery-business/src/main/java/com/battery/business/battery/dto/BatteryGroupDto.java
index 6713cff..1f15fd4 100644
--- a/battery-business/src/main/java/com/battery/business/battery/dto/BatteryGroupDto.java
+++ b/battery-business/src/main/java/com/battery/business/battery/dto/BatteryGroupDto.java
@@ -19,6 +19,7 @@
List<String> batteryNos, List<FieldValueDto> fields) {
this(groupId, groupName, model, purpose, status, batteryNos, fields, null, null, null, null, null, null, null, null);
}
+ /** 电池组当前字段值;兼容只传模板字段关系 templateFieldRelations 的调用方。 */
public List<FieldValueDto> currentFields() { return fields != null ? fields : templateFieldRelations; }
public record FieldValueDto(Long fieldId, String value, Long batteryGroupFieldId, String fieldValue) {
public FieldValueDto(Long fieldId, String value) { this(fieldId, value, null, null); }
diff --git a/battery-business/src/main/java/com/battery/business/domain/CurrentBatteryField.java b/battery-business/src/main/java/com/battery/business/domain/CurrentBatteryField.java
index cb2b19f..12f0da9 100644
--- a/battery-business/src/main/java/com/battery/business/domain/CurrentBatteryField.java
+++ b/battery-business/src/main/java/com/battery/business/domain/CurrentBatteryField.java
@@ -9,15 +9,22 @@
{
private static final long serialVersionUID = 1L;
+ /** 字段主键,创建测试时与 fieldDefinitionId 同值写入。 */
private Long fieldId;
+ /** 旧页面入参别名;本对象以 fieldId 为准。 */
private Long relationId;
+ /** 字段定义主键,指向电池组的业务自定义字段定义。 */
private Long fieldDefinitionId;
+ /** 字段类型:text 文本、number 数字、time 时间、remark 备注;决定前端输入控件形态。 */
private String fieldType;
+ /** 扩展项名称。 */
@NotBlank(message = "扩展项名称不能为空")
@Size(max = 100, message = "扩展项名称长度不能超过100个字符")
private String fieldName;
+ /** 扩展项值,电池组的当前值;测试直接读取,不另存快照。 */
@Size(max = 500, message = "扩展项值长度不能超过500个字符")
private String fieldValue;
+ /** 逻辑删除标识:0存在,其他值表示已删除。 */
private String delFlag;
public Long getFieldId() { return fieldId; }
diff --git a/battery-business/src/main/java/com/battery/business/domain/DataRevisionItem.java b/battery-business/src/main/java/com/battery/business/domain/DataRevisionItem.java
index e4a2c4e..bf4b215 100644
--- a/battery-business/src/main/java/com/battery/business/domain/DataRevisionItem.java
+++ b/battery-business/src/main/java/com/battery/business/domain/DataRevisionItem.java
@@ -3,7 +3,9 @@
/** 一次修订提交的目标和修改值,不是持久化覆盖记录。 */
public class DataRevisionItem
{
+ /** 目标记录的稳定身份,用于在测试数据表中定位同一条记录。 */
private String stableIdentity;
+ /** 本次要写入的字段与值的 JSON。 */
private String fieldValues;
public String getStableIdentity() { return stableIdentity; }
public void setStableIdentity(String value) { stableIdentity = value; }
diff --git a/battery-business/src/main/java/com/battery/business/domain/TBatteryGroup.java b/battery-business/src/main/java/com/battery/business/domain/TBatteryGroup.java
index 97dd171..5885385 100644
--- a/battery-business/src/main/java/com/battery/business/domain/TBatteryGroup.java
+++ b/battery-business/src/main/java/com/battery/business/domain/TBatteryGroup.java
@@ -8,30 +8,49 @@
import jakarta.validation.constraints.Size;
import com.ruoyi.common.core.domain.BaseEntity;
-/** 电池组主档对象。 */
+/** 电池组主档对象,对应表 t_battery_group;电池号不能脱离所属电池组参与测试。 */
public class TBatteryGroup extends BaseEntity
{
private static final long serialVersionUID = 1L;
+ /** 电池组主键。 */
private Long groupId;
+ /** 电池组名称。 */
@NotBlank(message = "电池组名称不能为空") @Size(max = 100, message = "电池组名称长度不能超过100个字符")
private String groupName;
+ /** 电池组型号。 */
@NotBlank(message = "电池组型号不能为空") @Size(max = 100, message = "电池组型号长度不能超过100个字符")
private String model;
+ /** 电池组用途。 */
@Size(max = 500, message = "用途长度不能超过500个字符") private String purpose;
+ /** 化成报表公式用参数 Z。 */
@Size(max = 100, message = "z字段长度不能超过100个字符") private String z;
+ /** 化成报表公式用参数 H。 */
@Size(max = 100, message = "h字段长度不能超过100个字符") private String h;
+ /** 化成报表公式用参数 F,全部报表版本共用同一份定义。 */
@Size(max = 100, message = "f字段长度不能超过100个字符") private String f;
+ /** 化成报表公式用参数 A。 */
@Size(max = 100, message = "a字段长度不能超过100个字符") private String a;
+ /** 化成报表公式用参数 B。 */
@Size(max = 100, message = "b字段长度不能超过100个字符") private String b;
+ /** 启用状态:0正常,1停用;已被测试引用的电池组不能删除。 */
private String status;
+ /** 逻辑删除标识:0存在,其他值表示已删除。 */
private String delFlag;
+ /** 查询条件:创建时间起始值。 */
private Date beginCreateTime;
+ /** 查询条件:创建时间结束值。 */
private Date endCreateTime;
+ /** 本电池组的电池号列表;新增或编辑时整体替换。 */
@NotEmpty(message = "电池号不能为空") private List<String> batteryNos;
+ /** 所选字段模板主键。 */
private Long templateId;
+ /** 模板字段关系,落库为电池组自己的字段值。 */
private List<TBatteryGroupRelationField> templateFieldRelations;
+ /** 电池号关系列表,含排序号。 */
private List<TBatteryGroupBattery> batteries;
+ /** 查询结果:电池号数量,由查询汇总得出。 */
private Integer batteryCount;
+ /** 电池组字段值;templateFields 为现有页面兼容名称。 */
private List<TBatteryGroupRelationField> templateFields;
/** 电池组当前字段值;templateFields 为现有页面兼容名称。 */
private List<TBatteryGroupRelationField> fields;
diff --git a/battery-business/src/main/java/com/battery/business/domain/TBatteryGroupBattery.java b/battery-business/src/main/java/com/battery/business/domain/TBatteryGroupBattery.java
index 1677c8c..596cb44 100644
--- a/battery-business/src/main/java/com/battery/business/domain/TBatteryGroupBattery.java
+++ b/battery-business/src/main/java/com/battery/business/domain/TBatteryGroupBattery.java
@@ -2,14 +2,19 @@
import java.io.Serializable;
-/** 电池组与电池号关系对象。 */
+/** 电池组与电池号关系对象,对应表 t_battery_group_battery;电池号在组内业务唯一。 */
public class TBatteryGroupBattery implements Serializable
{
private static final long serialVersionUID = 1L;
+ /** 电池号关系主键。 */
private Long relationId;
+ /** 所属电池组主键。 */
private Long groupId;
+ /** 电池号,在所属电池组内业务唯一。 */
private String batteryNo;
+ /** 组内展示顺序,从 1 起。 */
private Integer sortOrder;
+ /** 逻辑删除标识:0存在,其他值表示已删除。 */
private String delFlag;
public Long getRelationId() { return relationId; } public void setRelationId(Long v) { relationId = v; }
public Long getGroupId() { return groupId; } public void setGroupId(Long v) { groupId = v; }
diff --git a/battery-business/src/main/java/com/battery/business/domain/TBatteryGroupField.java b/battery-business/src/main/java/com/battery/business/domain/TBatteryGroupField.java
index c8879af..b440541 100644
--- a/battery-business/src/main/java/com/battery/business/domain/TBatteryGroupField.java
+++ b/battery-business/src/main/java/com/battery/business/domain/TBatteryGroupField.java
@@ -23,7 +23,7 @@
@Size(max = 100, message = "字段名称长度不能超过100个字符")
private String fieldName;
- /** 字段类型编码 */
+ /** 字段类型编码:text 文本、number 数字、time 时间、remark 备注 */
@NotBlank(message = "字段类型不能为空")
private String fieldType;
diff --git a/battery-business/src/main/java/com/battery/business/domain/TBatteryGroupRelationField.java b/battery-business/src/main/java/com/battery/business/domain/TBatteryGroupRelationField.java
index 36e552a..76394b4 100644
--- a/battery-business/src/main/java/com/battery/business/domain/TBatteryGroupRelationField.java
+++ b/battery-business/src/main/java/com/battery/business/domain/TBatteryGroupRelationField.java
@@ -2,17 +2,25 @@
import java.io.Serializable;
-/** 电池组与模板字段关系对象。 */
+/** 电池组与模板字段关系对象,对应表 t_battery_group_relation_field;保存该电池组自己的字段值。 */
public class TBatteryGroupRelationField implements Serializable
{
private static final long serialVersionUID = 1L;
+ /** 关系主键。 */
private Long relationId;
+ /** 所属电池组主键。 */
private Long groupId;
+ /** 来源字段模板主键。 */
private Long templateId;
+ /** 引用的业务自定义字段定义主键,创建后不再变更。 */
private Long batteryGroupFieldId;
+ /** 字段名称,取自字段定义,测试与报表直接读取。 */
private String fieldName;
+ /** 字段类型:text 文本、number 数字、time 时间、remark 备注;决定前端输入控件形态。 */
private String fieldType;
+ /** 该电池组的字段值;只保存当前值,不形成测试字段快照。 */
private String fieldValue;
+ /** 逻辑删除标识:0存在,其他值表示已删除。 */
private String delFlag;
public Long getRelationId() { return relationId; } public void setRelationId(Long v) { relationId = v; }
public Long getGroupId() { return groupId; } public void setGroupId(Long v) { groupId = v; }
diff --git a/battery-business/src/main/java/com/battery/business/domain/TCollector.java b/battery-business/src/main/java/com/battery/business/domain/TCollector.java
index e610f70..4196760 100644
--- a/battery-business/src/main/java/com/battery/business/domain/TCollector.java
+++ b/battery-business/src/main/java/com/battery/business/domain/TCollector.java
@@ -73,7 +73,7 @@
@Max(value = 10000, message = "通道数不能大于10000")
private Integer channelCount;
- /** 在线状态:0离线、1在线 */
+ /** 接口在线状态:ONLINE 在线、OFFLINE 离线;数据库约束只允许这两个值。 */
private String onlineStatus;
/** 进行中测试关联标记,供启禁用和删除校验使用,不写入采集器表。 */
diff --git a/battery-business/src/main/java/com/battery/business/domain/TDetectionCabinet.java b/battery-business/src/main/java/com/battery/business/domain/TDetectionCabinet.java
index f784ed2..3467f8b 100644
--- a/battery-business/src/main/java/com/battery/business/domain/TDetectionCabinet.java
+++ b/battery-business/src/main/java/com/battery/business/domain/TDetectionCabinet.java
@@ -28,13 +28,17 @@
/** 所属设备适配器实例。 */
private Long adapterId;
+ /** 所属设备适配器实例编码,与厂商字段共同构成通道身份。 */
private String adapterCode;
/** 数据来源名称,仅供展示,用于区分不同机器上的同名柜号;不落库。 */
private String adapterName;
+ /** 厂商设备类型。 */
private Integer vendorDeviceType;
+ /** 厂商设备号。 */
private Integer vendorDeviceId;
+ /** 最近一次观测到该设备的时间,以 UTC 保存。 */
private Instant lastSeenAtUtc;
/** 登记状态:新记录只使用 REGISTERED;DISCOVERED 仅兼容有业务引用的历史记录。 */
diff --git a/battery-business/src/main/java/com/battery/business/domain/TDetectionChannel.java b/battery-business/src/main/java/com/battery/business/domain/TDetectionChannel.java
index c8e598d..b1fef64 100644
--- a/battery-business/src/main/java/com/battery/business/domain/TDetectionChannel.java
+++ b/battery-business/src/main/java/com/battery/business/domain/TDetectionChannel.java
@@ -3,23 +3,39 @@
import java.time.Instant;
import com.ruoyi.common.core.domain.BaseEntity;
-/** 检测柜通道对象 t_detection_channel。 */
+/** 检测柜通道对象 t_detection_channel,是测试数据归属的最小来源单位。 */
public class TDetectionChannel extends BaseEntity
{
private static final long serialVersionUID = 1L;
+ /** 通道主键。 */
private Long channelId;
+ /** 所属设备适配器实例主键。 */
private Long adapterId;
+ /** 所属设备适配器实例编码。 */
private String adapterCode;
+ /** 所属检测柜主键。 */
private Long cabinetId;
+ /** 厂商设备类型;与下方三个字段共同构成通道身份。 */
private Integer devtype;
+ /** 厂商设备号。 */
private Integer devid;
+ /** 厂商单元号。 */
private Integer subdevid;
+ /** 厂商通道号。 */
private Integer chlid;
+ /** 通道展示编号,仅供界面展示,不能代替通道身份。 */
private String displayNo;
+ /** 设备在线状态:ONLINE 在线、OFFLINE 离线;由设备来源可达性观测,不能被测试动作改写。 */
private String onlineStatus;
+ /**
+ * 通道工作状态:RUNNING 运行、PAUSED 暂停、ABNORMAL 异常、ENDED 结束、UNOBSERVABLE 无法观测。
+ * 读库路线下设备不提供可用状态,离线或读取失败时一律为 UNOBSERVABLE,不沿用上一次状态,也不由数据行推断。
+ */
private String workStatus;
+ /** 最近一次观测到该通道的时间,以 UTC 保存。 */
private Instant lastSeenAtUtc;
+ /** 启用状态:0启用,1停用;开始接收时通道必须存在且启用。 */
private String status;
public Long getChannelId() { return channelId; }
diff --git a/battery-business/src/main/java/com/battery/business/domain/TDeviceAdapter.java b/battery-business/src/main/java/com/battery/business/domain/TDeviceAdapter.java
index ed700db..b2286a0 100644
--- a/battery-business/src/main/java/com/battery/business/domain/TDeviceAdapter.java
+++ b/battery-business/src/main/java/com/battery/business/domain/TDeviceAdapter.java
@@ -9,57 +9,73 @@
import jakarta.validation.constraints.Size;
import com.ruoyi.common.core.domain.BaseEntity;
-/** 设备适配器实例对象 t_device_adapter。 */
+/** 设备适配器实例对象 t_device_adapter;实例按「机器 + 上位机版本」划分,不能按厂商划分。 */
public class TDeviceAdapter extends BaseEntity
{
private static final long serialVersionUID = 1L;
+ /** 适配器实例主键。 */
private Long adapterId;
+ /** 适配器实例编码,与厂商通道字段共同构成通道身份。 */
@NotBlank(message = "适配器编码不能为空")
@Size(max = 64, message = "适配器编码长度不能超过64个字符")
private String adapterCode;
+ /** 适配器实例名称,用于区分不同机器上的同名设备,仅供展示。 */
@NotBlank(message = "适配器名称不能为空")
@Size(max = 100, message = "适配器名称长度不能超过100个字符")
private String adapterName;
+ /** 适配器类型,决定使用哪一套来源实现:BTS 协议客户端、NEWARE_MYSQL 新威读库、PUSH_INGEST 读落地表。 */
@NotBlank(message = "适配器类型不能为空")
private String adapterType;
+ /** 来源主机地址。 */
@NotBlank(message = "主机不能为空")
@Size(max = 255, message = "主机长度不能超过255个字符")
private String host;
+ /** 来源端口。 */
@NotNull(message = "端口不能为空")
@Min(value = 1, message = "端口必须在1到65535之间")
@Max(value = 65535, message = "端口必须在1到65535之间")
private Integer port;
+ /** 轮询周期(毫秒);设备来源约 60 秒才产生一行数据,取数不必更快。 */
@NotNull(message = "轮询周期不能为空")
@Min(value = 1000, message = "轮询周期不能小于1000毫秒")
private Integer pollIntervalMs;
+ /** 适配器时区标识,用于把设备时间解释为 UTC。 */
@NotBlank(message = "时区不能为空")
@Size(max = 64, message = "时区长度不能超过64个字符")
private String timezoneId;
+ /** 只读能力标记,只允许为真:第一版只观测设备,不下发控制命令。 */
@NotNull(message = "只读能力不能为空")
@AssertTrue(message = "当前里程碑只允许只读设备适配器")
private Boolean readOnly;
+ /** 适配器声明的能力标记,运行时控制能力默认为不支持。 */
private String capabilities;
/** 来源形态配置(JSON),承载各存储形态互不相交的非敏感字段;连接口令不入库。 */
private String sourceConfig;
+ /** 启用状态:0启用,1停用。 */
private String status;
/** 适配器连接状态,仅允许 ONLINE/OFFLINE。 */
private String connectionStatus;
+ /** 最近一次连接失败的错误码,供现场排查使用。 */
private String lastErrorCode;
+ /** 最近一次连接失败的错误说明,供现场排查使用。 */
private String lastErrorMessage;
+ /** 最近一次尝试连接的时间,以 UTC 保存。 */
private Instant lastAttemptAtUtc;
+ /** 最近一次成功观测到设备事实的时间,以 UTC 保存。 */
private Instant lastObservedAtUtc;
+ /** 逻辑删除标识:0存在,其他值表示已删除。 */
private String delFlag;
public Long getAdapterId() { return adapterId; }
diff --git a/battery-business/src/main/java/com/battery/business/domain/TDeviceIngestRecord.java b/battery-business/src/main/java/com/battery/business/domain/TDeviceIngestRecord.java
index a01b650..b770c53 100644
--- a/battery-business/src/main/java/com/battery/business/domain/TDeviceIngestRecord.java
+++ b/battery-business/src/main/java/com/battery/business/domain/TDeviceIngestRecord.java
@@ -20,30 +20,55 @@
/** 载荷类别:存活心跳,只刷新"取数程序还在推"这一条事实。 */
public static final String KIND_HEARTBEAT = "HEARTBEAT";
+ /** 落地行主键。 */
private Long ingestId;
+ /** 推送来源所属的适配器实例编码。 */
private String adapterCode;
+ /** 载荷类别:TOPOLOGY 拓扑上报、SAMPLE 逐点数据行、HEARTBEAT 存活心跳。 */
private String payloadKind;
+ /** 厂商设备类型。 */
private Integer devtype;
+ /** 厂商设备号。 */
private Integer devid;
+ /** 厂商单元号。 */
private Integer subdevid;
+ /** 厂商通道号。 */
private Integer chlid;
+ /** 来源协议序号,工步切换点可在同一绝对秒内出现多条。 */
private Long recordSequence;
+ /** 来源行身份,重复推送由它幂等忽略。 */
private String recordKey;
+ /** 来源侧引用标识,如测试或来源记录编号,用于现场核对。 */
private String sourceRef;
+ /** 设备时间,按来源时区解释后以 UTC 保存。 */
private Date deviceTimeUtc;
+ /** 设备时间原始文本,保留厂商原值。 */
private String deviceTimeRaw;
+ /** 解释设备时间所用的时区标识。 */
private String timezoneId;
+ /** 设备实际循环号。 */
private Integer cycleNo;
+ /** 设备实际工步号。 */
private Integer stepNo;
+ /** 工步内相对时长,单位秒;由成功观测提供,不以接收时间推算。 */
private BigDecimal relativeTimeSeconds;
+ /** 电压,业务单位 V。 */
private BigDecimal voltageV;
+ /** 电流,业务单位 A。 */
private BigDecimal currentA;
+ /** 充电容量,业务单位 Ah。 */
private BigDecimal chargeCapacityAh;
+ /** 放电容量,业务单位 Ah。 */
private BigDecimal dischargeCapacityAh;
+ /** 充电能量,业务单位 Wh。 */
private BigDecimal chargeEnergyWh;
+ /** 放电能量,业务单位 Wh。 */
private BigDecimal dischargeEnergyWh;
+ /** 厂商原始行内容,按收到的键值原样保存,便于回溯来源事实。 */
private String payloadJson;
+ /** 记录内容摘要,用于重复推送的幂等与冲突判定。 */
private byte[] contentSha256;
+ /** 服务器收到该行的时间,以 UTC 保存。 */
private Date receivedAtUtc;
public Long getIngestId() { return ingestId; } public void setIngestId(Long v) { ingestId = v; }
diff --git a/battery-business/src/main/java/com/battery/business/domain/TTest.java b/battery-business/src/main/java/com/battery/business/domain/TTest.java
index c558192..ed4901d 100644
--- a/battery-business/src/main/java/com/battery/business/domain/TTest.java
+++ b/battery-business/src/main/java/com/battery/business/domain/TTest.java
@@ -12,56 +12,92 @@
import jakarta.validation.constraints.Size;
import com.ruoyi.common.core.domain.BaseEntity;
-/** 测试配置及其独立业务状态。 */
+/**
+ * 测试配置对象 t_test,含测试状态与接收状态两个并列的业务状态。
+ *
+ * <p>测试状态描述测试流程,接收状态描述系统对该测试的数据归属;两者都不得反向改写设备事实。</p>
+ */
public class TTest extends BaseEntity
{
private static final long serialVersionUID = 1L;
+ /** 测试主键。 */
private Long testId;
+ /** 测试名称,由操作员填写,用于在同一电池组内区分测试。 */
@NotBlank(message = "测试名称不能为空")
@Size(max = 100, message = "测试名称长度不能超过100个字符")
private String testName;
+ /** 创建人用户 ID,创建时取当前登录用户,后续修改不可变更。 */
private Long creatorId;
+ /** 创建人显示名称,仅供列表展示,不作为身份判断依据。 */
private String creatorName;
+ /** 当前操作人用户 ID,仅由服务入参带入用于校验,不序列化也不落库。 */
@JSONField(serialize = false)
private transient Long operatorId;
+ /** 测试负责人用户 ID,负责关闭测试预警异常和停止测试。 */
@NotNull(message = "测试负责人不能为空")
private Long principalId;
+ /** 测试负责人显示名称,仅供列表展示,不作为身份判断依据。 */
private String principalName;
+ /** 测试类型主键,引用已启用的测试类型。 */
@NotNull(message = "测试类型不能为空")
private Long testTypeId;
+ /** 测试类型显示名称,仅供列表展示。 */
private String testTypeName;
+ /** 测试目的,由操作员填写。 */
@NotBlank(message = "测试目的不能为空")
private String testPurpose;
+ /**
+ * 测试状态:WAITING 待测试、RUNNING 检测中、PAUSED 暂停、COMPLETED 检测完成。
+ * 历史数据还可能出现 RUNNING_ABNORMAL(检测中(异常))、COMPLETION_PENDING(完成确认中)、
+ * TERMINATED(已终止),只作兼容展示,新流程不再写入。
+ */
private String testStatus;
+ /**
+ * 接收状态:NOT_STARTED 未开始接收、RECEIVING 接收中、PAUSED 暂停接收、ENDED 已结束接收。
+ * 它表示系统对本次测试的数据归属状态,与设备状态并列,不改变设备事实。
+ */
private String receptionStatus;
+ /** 监测超时分钟数,创建测试时缺省 30;控制台按该值展示,不参与设备控制。 */
private Integer monitorTimeoutMinutes;
+ /** 首次开始接收动作建立的服务器时间;暂停后继续接收不再改写。 */
@JsonFormat(shape = JsonFormat.Shape.NUMBER)
private Date startedAtUtc;
/** 最近一次开始或继续接收动作建立的服务器时间边界。 */
@JsonFormat(shape = JsonFormat.Shape.NUMBER)
private Date receptionBoundaryAtUtc;
+ /** 停止测试(检测完成)动作的服务器时间;暂停不写入。 */
@JsonFormat(shape = JsonFormat.Shape.NUMBER)
private Date endedAtUtc;
/** 是否从已运行设备的后续观测边界接入。 */
private Boolean runtimeAttach;
+ /** 逻辑删除标识:0存在,1删除;测试整体删除按 ADR-0047 不写审计、修订或墓碑。 */
private String delFlag;
+ /** 查询条件:按绑定电池号过滤;不落库。 */
private String batteryNo;
+ /** 查询条件:创建时间起始值。 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date beginCreateTime;
+ /** 查询条件:创建时间结束值。 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date endCreateTime;
+ /** 查询结果:绑定电池号数量,由列表查询汇总得出,不落库。 */
private Integer batteryCount;
+ /** 测试绑定的电池号及其通道配置;开始接收后配置整体只读。 */
@Valid
@NotEmpty(message = "测试至少绑定一个电池号")
private List<TTestBattery> batteries;
+ /** 测试阈值规则,接收中按当前成功观测判定超限,不参与设备侧保护。 */
@Valid
@NotEmpty(message = "阈值配置不能为空")
private List<TTestWarningThreshold> thresholds;
+ /** 逻辑工步映射的落库 JSON 原文。 */
private String logicStepMappingsJson;
+ /** 逻辑工步映射对象,由 JSON 解析或查询组装,不单独落库。 */
private List<TTestLogicStepMapping> logicStepMappings;
/** 工步图表中的测试补充详情,仅保存当前最新版。 */
private String consoleDetailJson;
+ /** 测试补充详情对象,由上述 JSON 解析或查询组装,不单独落库。 */
private TTestConsoleDetail consoleDetail;
/** 测试业务时间输出毫秒,平台实体仍保留原有时间格式。 */
diff --git a/battery-business/src/main/java/com/battery/business/domain/TTestBattery.java b/battery-business/src/main/java/com/battery/business/domain/TTestBattery.java
index c94cab2..ac73871 100644
--- a/battery-business/src/main/java/com/battery/business/domain/TTestBattery.java
+++ b/battery-business/src/main/java/com/battery/business/domain/TTestBattery.java
@@ -6,28 +6,39 @@
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
-/** 测试内的电池号、必填采集器与设备通道配置。 */
+/** 测试内的电池号、必填采集器与设备通道配置,对应表 t_test_battery。 */
public class TTestBattery implements Serializable
{
private static final long serialVersionUID = 1L;
+ /** 旧页面入参别名,仅用于兼容;数据库列以 testBatteryId 为准。 */
private Long relationId;
/** 最终数据库主键;relationId 仅作为旧页面入参别名保留。 */
private Long testBatteryId;
+ /** 所属测试主键。 */
private Long testId;
+ /** 电池号所属电池组主键。 */
@NotNull(message = "电池组不能为空")
private Long groupId;
+ /** 电池组显示名称,仅供列表展示,可能来自电池组当前配置。 */
private String groupName;
+ /** 电池号,在所属电池组内业务唯一。 */
@NotBlank(message = "电池号不能为空")
private String batteryNo;
+ /** 本电池号绑定的采集器主键;开始接收前校验其存在且已启用。 */
private Long collectorId;
+ /** 采集器显示名称,仅供列表展示。 */
private String collectorName;
+ /** 逻辑删除标识:0存在,1删除。 */
private String delFlag;
+ /** 主要检测柜通道绑定,开始接收的通道唯一性以此为判据。 */
@Valid
@NotNull(message = "主要检测柜通道不能为空")
private TTestChannelBinding binding;
+ /** 采集器演示通道的测量映射,用于接收期间的温度与电位归属。 */
@Valid
private List<TTestCollectorChannel> collectorChannels;
+ /** 电池组当前字段投影,测试直接读取当前值,不保存测试字段快照。 */
private List<CurrentBatteryField> fields;
public Long getRelationId() { return relationId; }
diff --git a/battery-business/src/main/java/com/battery/business/domain/TTestChannelBinding.java b/battery-business/src/main/java/com/battery/business/domain/TTestChannelBinding.java
index 17013e3..190ee05 100644
--- a/battery-business/src/main/java/com/battery/business/domain/TTestChannelBinding.java
+++ b/battery-business/src/main/java/com/battery/business/domain/TTestChannelBinding.java
@@ -4,30 +4,49 @@
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
-/** 测试电池号与主要检测柜通道的绑定。 */
+/** 测试电池号与主要检测柜通道的绑定,对应表 t_test_channel_binding。 */
public class TTestChannelBinding implements Serializable
{
private static final long serialVersionUID = 1L;
+ /** 通道绑定主键。 */
private Long bindingId;
+ /** 所属测试主键。 */
private Long testId;
+ /** 旧页面入参别名,仅用于兼容;数据库列以 testBatteryId 为准。 */
private Long batteryRelationId;
/** 最终数据库列名对应的测试电池主键。 */
private Long testBatteryId;
+ /** 绑定的检测柜通道主键,同一次开始接收不允许重复绑定同一通道。 */
@NotNull(message = "检测柜通道不能为空")
private Long channelId;
+ /** 操作员填写的预期条码,用于人工核对,不参与数据归属判定。 */
@Size(max = 100, message = "预期条码长度不能超过100个字符")
private String expectedBarcode;
+ /**
+ * 通道认领状态,数据库约束只允许四个取值:PENDING 待认领、CLAIMED 已认领、
+ * RELEASED 已释放、CONFLICT 冲突;数据归属只按绑定关系判定,不依赖该值。
+ */
private String claimStatus;
+ /** 通道所属检测柜主键,由通道查询带出。 */
private Long cabinetId;
+ /** 检测柜编码,仅供列表展示。 */
private String cabinetCode;
+ /** 检测柜名称,仅供列表展示。 */
private String cabinetName;
+ /** 设备适配器实例编码,与下方厂商字段共同构成通道身份。 */
private String adapterCode;
+ /** 厂商设备类型。 */
private Integer devtype;
+ /** 厂商设备号。 */
private Integer devid;
+ /** 厂商单元号。 */
private Integer subdevid;
+ /** 厂商通道号。 */
private Integer chlid;
+ /** 通道展示编号,仅供界面展示,不能代替通道身份。 */
private String displayNo;
+ /** 适配器时区标识,用于解释设备时间;缺省按 Asia/Hong_Kong 处理。 */
private String adapterTimezoneId;
public Long getBindingId() { return bindingId; }
diff --git a/battery-business/src/main/java/com/battery/business/domain/TTestCollectorChannel.java b/battery-business/src/main/java/com/battery/business/domain/TTestCollectorChannel.java
index 623cf4d..f8ee948 100644
--- a/battery-business/src/main/java/com/battery/business/domain/TTestCollectorChannel.java
+++ b/battery-business/src/main/java/com/battery/business/domain/TTestCollectorChannel.java
@@ -3,21 +3,28 @@
import java.io.Serializable;
import jakarta.validation.constraints.NotBlank;
-/** 测试电池号与采集器演示通道的测量映射。 */
+/** 测试电池号与采集器演示通道的测量映射,对应表 t_test_collector_channel。 */
public class TTestCollectorChannel implements Serializable
{
private static final long serialVersionUID = 1L;
+ /** 测量映射主键。 */
private Long mappingId;
+ /** 所属测试主键。 */
private Long testId;
+ /** 旧页面入参别名,仅用于兼容;数据库列以 testBatteryId 为准。 */
private Long batteryRelationId;
/** 最终数据库列名对应的测试电池主键。 */
private Long testBatteryId;
+ /** 采集器主键。 */
private Long collectorId;
+ /** 绑定角色编码,如主要温度、正电位、负电位;用于区分主测量与辅助测量。 */
@NotBlank(message = "采集器测量项编码不能为空")
private String measurementCode;
+ /** 绑定角色显示名称,仅供界面展示。 */
@NotBlank(message = "采集器测量项名称不能为空")
private String measurementName;
+ /** 采集器通道编码,须与设备返回的通道编码一致才归属本绑定。 */
@NotBlank(message = "采集器通道不能为空")
private String channelCode;
diff --git a/battery-business/src/main/java/com/battery/business/domain/TTestConsoleDetail.java b/battery-business/src/main/java/com/battery/business/domain/TTestConsoleDetail.java
index cf2db09..cfeadf2 100644
--- a/battery-business/src/main/java/com/battery/business/domain/TTestConsoleDetail.java
+++ b/battery-business/src/main/java/com/battery/business/domain/TTestConsoleDetail.java
@@ -7,9 +7,13 @@
/** 工步图表中的测试补充详情,仅保留当前测试的一份记录。 */
public class TTestConsoleDetail
{
+ /** 实验名称,控制台详情专用;三类报表详情不含该字段。 */
private String experimentName;
+ /** 从电池组带入的字段名称有序列表,用于决定详情表格的列顺序。 */
private List<String> batteryFields = new java.util.ArrayList<>();
+ /** 测试内自行添加的特殊字段名称有序列表,不进入跨业务共享的字段定义。 */
private List<String> specialFields = new java.util.ArrayList<>();
+ /** 人工填写值,按「电池号 → 字段名 → 值」组织;只保存当前最新版,不形成测试字段快照。 */
private Map<String, Map<String, String>> fieldValues = new LinkedHashMap<>();
public String getExperimentName() { return experimentName; }
diff --git a/battery-business/src/main/java/com/battery/business/domain/TTestCycleRecord.java b/battery-business/src/main/java/com/battery/business/domain/TTestCycleRecord.java
index 49ff31e..4a7ca21 100644
--- a/battery-business/src/main/java/com/battery/business/domain/TTestCycleRecord.java
+++ b/battery-business/src/main/java/com/battery/business/domain/TTestCycleRecord.java
@@ -4,15 +4,55 @@
import java.math.BigDecimal;
import java.util.Date;
-/** 一条循环统计记录;设备没有循环汇总行时由工步层按循环聚合,来源类别随之保存。 */
+/**
+ * 一条循环统计记录,对应表 t_test_cycle_record;设备没有循环汇总行时由工步层按循环聚合,来源类别随之保存。
+ */
public class TTestCycleRecord implements Serializable
{
- private Long cycleRecordId, testId, testBatteryId, bindingId;
- private String adapterCode, sourceIdentity, sourceRecordId, sourceKind, adapterTimezoneId, dedupeKey, extensionMeasurementsJson;
- private Date deviceTimeUtc, serverReceivedAtUtc;
+ /** 循环记录主键。 */
+ private Long cycleRecordId;
+ /** 所属测试主键。 */
+ private Long testId;
+ /** 所属测试电池主键。 */
+ private Long testBatteryId;
+ /** 本次观测使用的通道绑定主键。 */
+ private Long bindingId;
+ /** 设备适配器实例编码。 */
+ private String adapterCode;
+ /** 来源身份,由适配器实例与厂商通道字段拼接。 */
+ private String sourceIdentity;
+ /** 来源记录编号,设备未提供时为空并回落到时间组合去重。 */
+ private String sourceRecordId;
+ /**
+ * 循环统计来源类别:DEVICE 设备提供、AGGREGATED 工步层聚合、IMPORTED 文件导入。
+ * 推导值必须能与设备事实区分,不能只靠界面文案说明。
+ */
+ private String sourceKind;
+ /** 解释设备时间所用的适配器时区标识。 */
+ private String adapterTimezoneId;
+ /** 幂等键:优先取来源记录编号,缺失时按设备结束时间与循环号组合。 */
+ private String dedupeKey;
+ /** 扩展测量记录 JSON。 */
+ private String extensionMeasurementsJson;
+ /** 循环结束时间,按适配器时区解释后以 UTC 保存。 */
+ private Date deviceTimeUtc;
+ /** 服务器接收时间。 */
+ private Date serverReceivedAtUtc;
+ /** 设备时间原始文本,保留厂家原值供核对。 */
private String deviceTimeRaw;
+ /** 设备循环号。 */
private Integer cycleNo;
- private BigDecimal chargeCapacityAh, dischargeCapacityAh, chargeEnergyWh, dischargeEnergyWh, efficiency;
+ /** 充电容量,业务单位 Ah。 */
+ private BigDecimal chargeCapacityAh;
+ /** 放电容量,业务单位 Ah。 */
+ private BigDecimal dischargeCapacityAh;
+ /** 充电能量,业务单位 Wh。 */
+ private BigDecimal chargeEnergyWh;
+ /** 放电能量,业务单位 Wh。 */
+ private BigDecimal dischargeEnergyWh;
+ /** 循环效率,业务单位 %,为放电容量 ÷ 充电容量;没有可用充电容量时为空,未知比率不用零代替。 */
+ private BigDecimal efficiency;
+ /** 记录内容摘要,用于重复观测的幂等与冲突判定。 */
private byte[] contentSha256;
public Long getCycleRecordId(){return cycleRecordId;} public void setCycleRecordId(Long v){cycleRecordId=v;}
public String getSourceKind(){return sourceKind;} public void setSourceKind(String v){sourceKind=v;} public Long getTestId(){return testId;} public void setTestId(Long v){testId=v;}
diff --git a/battery-business/src/main/java/com/battery/business/domain/TTestDataRevisionAction.java b/battery-business/src/main/java/com/battery/business/domain/TTestDataRevisionAction.java
index b4e72cd..16965f5 100644
--- a/battery-business/src/main/java/com/battery/business/domain/TTestDataRevisionAction.java
+++ b/battery-business/src/main/java/com/battery/business/domain/TTestDataRevisionAction.java
@@ -6,37 +6,55 @@
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonFormat;
-/** 一次控制台数据编辑、导入或删除行为。 */
+/** 一次控制台数据编辑、导入或删除行为,对应表 t_test_data_revision_action;只追加,不提供修改或删除能力。 */
public class TTestDataRevisionAction implements Serializable
{
private static final long serialVersionUID = 1L;
+ /** 修订主键,一次用户行为对应一条修订。 */
private Long actionId;
+ /** 被修订数据所属测试主键。 */
private Long testId;
+ /**
+ * 修订的数据类型:REALTIME 实时、STEP 工步、CYCLE 循环;
+ * 三 Sheet 一键导入在提交入参中使用 ALL,落库时按各 Sheet 分别记为三类中的一种。
+ */
private String dataType;
+ /** 操作类型:EDIT 编辑、IMPORT 导入、DELETE 物理删除。 */
private String operationType;
+ /** 操作范围摘要,例如“导入 3 条实时数据”。 */
private String scopeSummary;
+ /** 目标记录身份数组的 JSON,用于定位本次行为涉及的记录。 */
private String identitySummary;
+ /** 变化内容摘要的 JSON;删除行为保存删除前内容,不额外记录改动项。 */
private String changesSummary;
+ /** 操作前完整内容的 JSON,用于追溯已物理删除的数据。 */
private String beforeContentJson;
/** 详情展示数量由持久化层统计,非编辑无需加载完整身份数组;不写入审计事实。 */
private Long affectedRecordCount;
+ /** 操作人填写的修订原因,必填。 */
private String reason;
+ /** 操作人用户 ID,取当前登录会话。 */
private Long operatorId;
+ /** 操作人显示名称,随修订事实一起保存,不随用户改名变化。 */
private String operatorName;
+ /** 修订发生时间,以 UTC 保存。 */
@JsonFormat(shape = JsonFormat.Shape.NUMBER)
private Date createdAtUtc;
/** 查询展示字段:来源测试名称。 */
private String testName;
/** 返回测试控制台时使用的后端接收状态。 */
private String testReceptionStatus;
+ /** 查询条件:修订时间起始值。 */
@JsonIgnore
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
private Date beginCreatedAt;
+ /** 查询条件:修订时间结束值。 */
@JsonIgnore
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
private Date endCreatedAt;
/** 本次提交结果,不持久化到修订事实。 */
private int importedCount;
+ /** 本次提交跳过的重复记录数,不持久化到修订事实。 */
private int skippedCount;
/** 本次提交生成的全部修订编号,仅用于提交响应,不持久化。 */
@JsonIgnore
diff --git a/battery-business/src/main/java/com/battery/business/domain/TTestLogicStepMapping.java b/battery-business/src/main/java/com/battery/business/domain/TTestLogicStepMapping.java
index fc0d581..6f30b55 100644
--- a/battery-business/src/main/java/com/battery/business/domain/TTestLogicStepMapping.java
+++ b/battery-business/src/main/java/com/battery/business/domain/TTestLogicStepMapping.java
@@ -7,9 +7,13 @@
/** 当前测试循环内工步位置到逻辑工步的映射;只影响展示和统计,不改写原始检测柜数据。 */
public class TTestLogicStepMapping implements Serializable {
private static final long serialVersionUID = 1L;
+ /** 逻辑工步编号,按循环内顺序从 1 连续编号。 */
@Min(value = 1, message = "逻辑工步编号必须大于0") private Integer logicalNo;
+ /** 操作员填写的逻辑工步名称。 */
@NotBlank(message = "逻辑工步名称不能为空") private String logicalName;
+ /** 本逻辑工步覆盖的循环内起始实际工步位置,含该位置。 */
@Min(value = 1, message = "循环内起始位置必须大于0") private Integer startStepNo;
+ /** 本逻辑工步覆盖的循环内结束实际工步位置,含该位置。 */
@Min(value = 1, message = "循环内结束位置必须大于0") private Integer endStepNo;
public Integer getLogicalNo() { return logicalNo; }
public void setLogicalNo(Integer value) { logicalNo = value; }
diff --git a/battery-business/src/main/java/com/battery/business/domain/TTestRealtimeRecord.java b/battery-business/src/main/java/com/battery/business/domain/TTestRealtimeRecord.java
index aa7683b..93c53de 100644
--- a/battery-business/src/main/java/com/battery/business/domain/TTestRealtimeRecord.java
+++ b/battery-business/src/main/java/com/battery/business/domain/TTestRealtimeRecord.java
@@ -4,44 +4,84 @@
import java.math.BigDecimal;
import java.util.Date;
-/** 一次测试电池轮询槽位的检测柜与采集器组合观测。 */
+/**
+ * 一次测试电池轮询槽位的检测柜与采集器组合观测,对应表 t_test_realtime_record。
+ *
+ * <p>槽位为空的记录来自授权历史导入或柜端逐行接收,同一秒可以存在多条不同来源编号的记录。</p>
+ */
public class TTestRealtimeRecord implements Serializable
{
+ /** 实时记录主键。 */
private Long recordId;
+ /** 所属测试主键。 */
private Long testId;
+ /** 所属测试电池主键,数据按电池号归属。 */
private Long testBatteryId;
+ /** 本次观测使用的通道绑定主键。 */
private Long bindingId;
+ /** 设备适配器实例编码。 */
private String adapterCode;
+ /** 来源身份,由适配器实例与厂商通道字段拼接,是幂等与冲突判定的组成部分。 */
private String sourceIdentity;
+ /** 来源记录编号:柜端为协议序号或来源主键,采集器为采集端序号;配合来源身份执行幂等。 */
private String sourceRecordId;
+ /** 柜端厂商设备类型,采集器来源或导入行为空。 */
private Integer cabinetDevtype;
+ /** 柜端厂商设备号。 */
private Integer cabinetDevid;
+ /** 柜端厂商单元号。 */
private Integer cabinetSubdevid;
+ /** 柜端厂商通道号。 */
private Integer cabinetChlid;
+ /** 柜端协议序号,工步切换点在同一绝对秒内可写入多条记录,靠该值区分。 */
private Long cabinetSequenceId;
+ /** 柜端设备时间,按适配器时区解释后以 UTC 保存。 */
private Date cabinetDeviceTimeUtc;
+ /** 柜端设备时间原始文本,保留厂家原值供核对。 */
private String cabinetDeviceTimeRaw;
+ /** 解释柜端设备时间所用的适配器时区标识。 */
private String cabinetAdapterTimezoneId;
+ /** 采集器主键,柜端单来源记录为空。 */
private Long collectorId;
+ /** 采集器通道编码。 */
private String collectorChannelCode;
+ /** 采集端记录序号。 */
private Long collectorSequenceId;
+ /** 采集器设备时间,按适配器时区解释后以 UTC 保存。 */
private Date collectorDeviceTimeUtc;
+ /** 采集器设备时间原始文本。 */
private String collectorDeviceTimeRaw;
+ /** 解释采集器设备时间所用的适配器时区标识。 */
private String collectorAdapterTimezoneId;
+ /** 系统轮询槽位;柜端逐行接收和授权历史导入的记录槽位为空,同秒多行因此各自成立。 */
private Date captureSlotUtc;
+ /** 服务器接收时间,仅用于展示接收节奏,不参与重复观测的内容比较。 */
private Date serverReceivedAtUtc;
+ /** 设备实际循环号。 */
private Integer cycleNo;
+ /** 设备实际工步号。 */
private Integer stepNo;
+ /** 设备工步类型,未知取值原样保留,不按电流或容量猜测方向。 */
private String stepType;
+ /** 设备工步名称。 */
private String stepName;
+ /** 电压,业务单位 V。 */
private BigDecimal voltageV;
+ /** 电流,业务单位 A。 */
private BigDecimal currentA;
+ /** 容量,业务单位 Ah。 */
private BigDecimal capacityAh;
+ /** 能量,业务单位 Wh。 */
private BigDecimal energyWh;
+ /** 主温度,业务单位 ℃;多路同类测量按绑定角色区分,辅助温度不覆盖主温度。 */
private BigDecimal temperature;
+ /** 正电位,业务单位 V。 */
private BigDecimal positivePotential;
+ /** 负电位,业务单位 V。 */
private BigDecimal negativePotential;
+ /** 扩展测量记录 JSON,按指标编码保存动态辅助通道或 DBC 指标及单位。 */
private String extensionMeasurementsJson;
+ /** 记录内容摘要,用于重复观测的幂等与冲突判定;比较时排除服务器接收时间。 */
private byte[] contentSha256;
public Long getRecordId() { return recordId; } public void setRecordId(Long v) { recordId = v; }
diff --git a/battery-business/src/main/java/com/battery/business/domain/TTestStepRecord.java b/battery-business/src/main/java/com/battery/business/domain/TTestStepRecord.java
index 627fe93..317d5af 100644
--- a/battery-business/src/main/java/com/battery/business/domain/TTestStepRecord.java
+++ b/battery-business/src/main/java/com/battery/business/domain/TTestStepRecord.java
@@ -4,15 +4,56 @@
import java.math.BigDecimal;
import java.util.Date;
-/** 检测柜直接返回的一条实际工步统计记录。 */
+/**
+ * 检测柜直接返回的一条实际工步统计记录,对应表 t_test_step_record。
+ *
+ * <p>只保存设备给出的实际工步,逻辑工步合并仅用于派生视图,不回写本表。</p>
+ */
public class TTestStepRecord implements Serializable
{
- private Long stepRecordId, testId, testBatteryId, bindingId;
- private String adapterCode, sourceIdentity, sourceRecordId, adapterTimezoneId, dedupeKey, stepType, stepName, extensionMeasurementsJson;
- private Date deviceTimeUtc, serverReceivedAtUtc;
+ /** 工步记录主键。 */
+ private Long stepRecordId;
+ /** 所属测试主键。 */
+ private Long testId;
+ /** 所属测试电池主键。 */
+ private Long testBatteryId;
+ /** 本次观测使用的通道绑定主键。 */
+ private Long bindingId;
+ /** 设备适配器实例编码。 */
+ private String adapterCode;
+ /** 来源身份,由适配器实例与厂商通道字段拼接。 */
+ private String sourceIdentity;
+ /** 来源记录编号,设备未提供时为空并回落到时间组合去重。 */
+ private String sourceRecordId;
+ /** 解释设备时间所用的适配器时区标识。 */
+ private String adapterTimezoneId;
+ /** 幂等键:优先取来源记录编号,缺失时按结束时间、循环号、工步号组合。 */
+ private String dedupeKey;
+ /** 设备工步类型,未知取值原样保留,不按电流或容量猜测方向。 */
+ private String stepType;
+ /** 设备工步名称。 */
+ private String stepName;
+ /** 扩展测量记录 JSON,保存设备提供的充电/放电中值电压等额外指标。 */
+ private String extensionMeasurementsJson;
+ /** 工步结束时间,按适配器时区解释后以 UTC 保存。 */
+ private Date deviceTimeUtc;
+ /** 服务器接收时间。 */
+ private Date serverReceivedAtUtc;
+ /** 设备时间原始文本,保留厂家原值供核对。 */
private String deviceTimeRaw;
- private Integer cycleNo, stepNo;
- private BigDecimal voltageV, currentA, capacityAh, energyWh;
+ /** 设备实际循环号。 */
+ private Integer cycleNo;
+ /** 设备实际工步号。 */
+ private Integer stepNo;
+ /** 工步结束电压,业务单位 V。 */
+ private BigDecimal voltageV;
+ /** 工步结束电流,业务单位 A。 */
+ private BigDecimal currentA;
+ /** 工步容量,业务单位 Ah;设备未提供时为空,不写成零。 */
+ private BigDecimal capacityAh;
+ /** 工步能量,业务单位 Wh;设备未提供时为空,不写成零。 */
+ private BigDecimal energyWh;
+ /** 记录内容摘要,用于重复观测的幂等与冲突判定。 */
private byte[] contentSha256;
public Long getStepRecordId(){return stepRecordId;} public void setStepRecordId(Long v){stepRecordId=v;}
public Long getTestId(){return testId;} public void setTestId(Long v){testId=v;} public Long getTestBatteryId(){return testBatteryId;} public void setTestBatteryId(Long v){testBatteryId=v;}
diff --git a/battery-business/src/main/java/com/battery/business/domain/TTestWarningException.java b/battery-business/src/main/java/com/battery/business/domain/TTestWarningException.java
index d152392..9ad9348 100644
--- a/battery-business/src/main/java/com/battery/business/domain/TTestWarningException.java
+++ b/battery-business/src/main/java/com/battery/business/domain/TTestWarningException.java
@@ -4,27 +4,50 @@
import java.math.BigDecimal;
import java.util.Date;
-/** 测试阈值超限或接收期间设备离线的异常事实。 */
+/**
+ * 测试阈值超限或接收期间设备离线的异常事实,对应表 t_test_warning_exception。
+ *
+ * <p>它只是本系统的观测提醒,不能表述或实现成设备安全保护的替代品。</p>
+ */
public class TTestWarningException implements Serializable {
private static final long serialVersionUID = 1L;
+ /** 异常主键。 */
private Long exceptionId;
+ /** 所属测试主键。 */
private Long testId;
+ /** 异常类型:THRESHOLD 阈值超限、DEVICE_OFFLINE 接收期间设备离线。 */
private String exceptionType = "THRESHOLD";
+ /** 所属测试名称,查询时带出,仅供展示。 */
private String testName;
+ /** 来源类型:CABINET 检测柜通道、CABINET_CHANNEL 绑定来源离线、COLLECTOR 采集器。 */
private String sourceType;
+ /** 来源身份:检测柜按适配器实例与厂商通道字段拼接,采集器按设备 ID。 */
private String sourceIdentity;
+ /** 指标编码;离线异常固定为 DEVICE_ONLINE。 */
private String metricCode;
+ /** 触发异常时的测量值;离线异常不伪造测量值,保持为空。 */
private BigDecimal abnormalValue;
+ /** 测量值单位;离线异常为空字符串。 */
private String unit;
+ /** 触发时阈值规则的内容快照 JSON,规则后续修改不回写历史异常。 */
private String ruleSnapshot;
+ /** 严重程度:WARNING 警告;离线异常固定为 WARNING。 */
private String severity;
+ /** 异常状态:OPEN 开放、CLOSED 已关闭;同一聚合键人工关闭前只保留一条开放异常。 */
private String exceptionStatus;
+ /** 首次触发时间,以 UTC 保存。 */
private Date firstOccurredAtUtc;
+ /** 最近一次触发时间,以 UTC 保存;正常观测不会自动关闭异常。 */
private Date lastOccurredAtUtc;
+ /** 累计触发次数。 */
private Long occurrenceCount;
+ /** 处理人用户 ID,由测试负责人关闭时写入。 */
private Long handledBy;
+ /** 处理人显示名称,随异常事实保存。 */
private String handledByName;
+ /** 关闭时间,以 UTC 保存。 */
private Date handledAtUtc;
+ /** 负责人填写的处理结果,关闭时必填。 */
private String handlingNote;
public Long getExceptionId(){return exceptionId;} public void setExceptionId(Long v){exceptionId=v;}
public Long getTestId(){return testId;} public void setTestId(Long v){testId=v;}
diff --git a/battery-business/src/main/java/com/battery/business/domain/TTestWarningThreshold.java b/battery-business/src/main/java/com/battery/business/domain/TTestWarningThreshold.java
index fdd214e..fcfbdbc 100644
--- a/battery-business/src/main/java/com/battery/business/domain/TTestWarningThreshold.java
+++ b/battery-business/src/main/java/com/battery/business/domain/TTestWarningThreshold.java
@@ -5,21 +5,29 @@
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
-/** 使用 SI 业务单位保存的测试告警阈值。 */
+/** 使用 SI 业务单位保存的测试告警阈值,对应表 t_test_warning_threshold;供接收中的实时超限判定使用。 */
public class TTestWarningThreshold implements Serializable
{
private static final long serialVersionUID = 1L;
+ /** 阈值主键。 */
private Long thresholdId;
+ /** 所属测试主键。 */
private Long testId;
+ /** 指标编码,须与观测指标匹配,如电压、电流、容量、能量、温度;支持主要测量前缀形式。 */
@NotBlank(message = "指标编码不能为空")
@Size(max = 100, message = "指标编码长度不能超过100个字符")
private String metricCode;
+ /** 下限,为空表示不判下限;单位与该指标的业务单位一致。 */
private BigDecimal minValue;
+ /** 上限,为空表示不判上限;单位与该指标的业务单位一致。 */
private BigDecimal maxValue;
+ /** 阈值单位,只允许 A、V、Ah、Wh、℃。 */
@NotBlank(message = "阈值单位不能为空")
private String unit;
+ /** 严重程度:WARNING 警告;随异常事实保存为触发时的规则快照。 */
private String severity;
+ /** 逻辑删除标识:0存在,其他值表示已删除。 */
private String delFlag;
public Long getThresholdId() { return thresholdId; }
diff --git a/battery-business/src/main/java/com/battery/business/domain/vo/AdapterDiscoveryResult.java b/battery-business/src/main/java/com/battery/business/domain/vo/AdapterDiscoveryResult.java
index 847de4b..6462cbf 100644
--- a/battery-business/src/main/java/com/battery/business/domain/vo/AdapterDiscoveryResult.java
+++ b/battery-business/src/main/java/com/battery/business/domain/vo/AdapterDiscoveryResult.java
@@ -2,7 +2,13 @@
import java.time.Instant;
-/** 单次设备发现写入 Redis 候选后的结果。 */
+/**
+ * 单次设备发现写入 Redis 候选后的结果。
+ *
+ * @param cabinetCount 本次写入的检测柜候选数量
+ * @param channelCount 本次写入的通道候选数量
+ * @param observedAtUtc 本次观测时间,以 UTC 保存
+ */
public record AdapterDiscoveryResult(int cabinetCount, int channelCount, Instant observedAtUtc)
{
}
diff --git a/battery-business/src/main/java/com/battery/business/domain/vo/DeviceRelatedTestVo.java b/battery-business/src/main/java/com/battery/business/domain/vo/DeviceRelatedTestVo.java
index 7a3b30b..9d36341 100644
--- a/battery-business/src/main/java/com/battery/business/domain/vo/DeviceRelatedTestVo.java
+++ b/battery-business/src/main/java/com/battery/business/domain/vo/DeviceRelatedTestVo.java
@@ -6,9 +6,13 @@
/** 设备关联的测试及该测试绑定的本设备通道,不写入设备表。 */
public class DeviceRelatedTestVo
{
+ /** 测试主键,设备内按该值去重。 */
private Long testId;
+ /** 测试名称,界面上作为测试详情链接。 */
private String testName;
+ /** 测试自己的测试状态,不表示设备状态。 */
private String testStatus;
+ /** 该测试绑定到本设备的通道展示编号。 */
private List<String> channels = new ArrayList<>();
public Long getTestId() { return testId; }
diff --git a/battery-business/src/main/java/com/battery/business/domain/vo/DiscoveredCabinetVo.java b/battery-business/src/main/java/com/battery/business/domain/vo/DiscoveredCabinetVo.java
index ff8346e..d24bcd5 100644
--- a/battery-business/src/main/java/com/battery/business/domain/vo/DiscoveredCabinetVo.java
+++ b/battery-business/src/main/java/com/battery/business/domain/vo/DiscoveredCabinetVo.java
@@ -6,6 +6,16 @@
* 检测柜选择弹窗使用的设备摘要。
* {@code lastDataAt} 是该设备最近一次采样行的设备时间(来源里没有采样行时为空),
* 由操作员据此判断哪几台柜子现在真的在产出数据;登记与否仍由操作员决定。
+ *
+ * @param deviceCode 发现候选的设备编码,由适配器按来源规则生成,供操作员核对
+ * @param ip 设备网络地址
+ * @param mac 设备 MAC 地址
+ * @param name 设备显示名称
+ * @param manufacturer 设备制造商
+ * @param model 设备型号
+ * @param channelCount 该设备在来源里登记且发现的通道数量
+ * @param onlineStatus 界面展示用的在线文案:在线 / 离线
+ * @param lastDataAt 最近一次采样行的设备时间;只作为观测事实,不判定设备是否在用
*/
public record DiscoveredCabinetVo(
String deviceCode,
diff --git a/battery-business/src/main/java/com/battery/business/domain/vo/OfflineTestSource.java b/battery-business/src/main/java/com/battery/business/domain/vo/OfflineTestSource.java
index 142d848..ae0a80d 100644
--- a/battery-business/src/main/java/com/battery/business/domain/vo/OfflineTestSource.java
+++ b/battery-business/src/main/java/com/battery/business/domain/vo/OfflineTestSource.java
@@ -1,4 +1,9 @@
package com.battery.business.domain.vo;
-/** 测试绑定来源的持久化离线事实;检测柜使用完整协议通道身份。 */
+/**
+ * 测试绑定来源的持久化离线事实;检测柜使用完整协议通道身份。
+ *
+ * @param sourceType 来源类型:CABINET_CHANNEL 检测柜通道、COLLECTOR 采集器
+ * @param sourceIdentity 来源身份:检测柜按适配器实例与厂商通道字段拼接,采集器按设备 ID
+ */
public record OfflineTestSource(String sourceType, String sourceIdentity) {}
diff --git a/battery-business/src/main/java/com/battery/business/domain/vo/TDeviceIngestChannel.java b/battery-business/src/main/java/com/battery/business/domain/vo/TDeviceIngestChannel.java
index 2f19310..27d0f04 100644
--- a/battery-business/src/main/java/com/battery/business/domain/vo/TDeviceIngestChannel.java
+++ b/battery-business/src/main/java/com/battery/business/domain/vo/TDeviceIngestChannel.java
@@ -9,10 +9,15 @@
*/
public class TDeviceIngestChannel implements Serializable
{
+ /** 厂商设备类型。 */
private Integer devtype;
+ /** 厂商设备号。 */
private Integer devid;
+ /** 厂商单元号。 */
private Integer subdevid;
+ /** 厂商通道号。 */
private Integer chlid;
+ /** 该通道在落地表里最近一条采样行的时间;只作为观测事实,不判定通道是否在用。 */
private Date lastDataAt;
public Integer getDevtype() { return devtype; } public void setDevtype(Integer v) { devtype = v; }
diff --git a/battery-business/src/main/java/com/battery/business/domain/vo/TestBatteryOptionVo.java b/battery-business/src/main/java/com/battery/business/domain/vo/TestBatteryOptionVo.java
index 9c022a1..a751344 100644
--- a/battery-business/src/main/java/com/battery/business/domain/vo/TestBatteryOptionVo.java
+++ b/battery-business/src/main/java/com/battery/business/domain/vo/TestBatteryOptionVo.java
@@ -3,10 +3,15 @@
/** 启用电池组内可选的电池号。 */
public class TestBatteryOptionVo
{
+ /** 电池号关系主键,旧页面入参别名。 */
private Long relationId;
+ /** 最终数据库主键,与 relationId 同值。 */
private Long testBatteryId;
+ /** 电池号所属电池组主键。 */
private Long groupId;
+ /** 电池组名称,供下拉分组展示。 */
private String groupName;
+ /** 电池号。 */
private String batteryNo;
public Long getRelationId() { return relationId; }
diff --git a/battery-business/src/main/java/com/battery/business/domain/vo/TestChannelOptionVo.java b/battery-business/src/main/java/com/battery/business/domain/vo/TestChannelOptionVo.java
index a493651..6c71b18 100644
--- a/battery-business/src/main/java/com/battery/business/domain/vo/TestChannelOptionVo.java
+++ b/battery-business/src/main/java/com/battery/business/domain/vo/TestChannelOptionVo.java
@@ -3,15 +3,25 @@
/** 已正式登记检测柜的稳定通道选项。 */
public class TestChannelOptionVo
{
+ /** 通道主键,绑定测试时提交该值。 */
private Long channelId;
+ /** 通道所属检测柜主键。 */
private Long cabinetId;
+ /** 检测柜编码,供下拉展示。 */
private String cabinetCode;
+ /** 检测柜名称,供下拉展示。 */
private String cabinetName;
+ /** 设备适配器实例编码。 */
private String adapterCode;
+ /** 厂商设备类型。 */
private Integer devtype;
+ /** 厂商设备号。 */
private Integer devid;
+ /** 厂商单元号。 */
private Integer subdevid;
+ /** 厂商通道号。 */
private Integer chlid;
+ /** 通道展示编号,仅供界面展示,不能代替通道身份。 */
private String displayNo;
public Long getChannelId() { return channelId; }
diff --git a/battery-business/src/main/java/com/battery/business/domain/vo/TestCollectorOptionVo.java b/battery-business/src/main/java/com/battery/business/domain/vo/TestCollectorOptionVo.java
index 600c55c..dda43f2 100644
--- a/battery-business/src/main/java/com/battery/business/domain/vo/TestCollectorOptionVo.java
+++ b/battery-business/src/main/java/com/battery/business/domain/vo/TestCollectorOptionVo.java
@@ -3,9 +3,13 @@
/** 可选的启用采集器。 */
public class TestCollectorOptionVo
{
+ /** 采集器主键,绑定测试电池时提交该值。 */
private Long collectorId;
+ /** 采集器编码,供下拉展示。 */
private String collectorCode;
+ /** 采集器名称,供下拉展示。 */
private String collectorName;
+ /** 已登记的采集器通道数量,用于校验绑定映射是否完整。 */
private Integer channelCount;
public Long getCollectorId() { return collectorId; }
diff --git a/battery-business/src/main/java/com/battery/business/domain/vo/TestConfigurationOptionsVo.java b/battery-business/src/main/java/com/battery/business/domain/vo/TestConfigurationOptionsVo.java
index f3b8db9..8bb1418 100644
--- a/battery-business/src/main/java/com/battery/business/domain/vo/TestConfigurationOptionsVo.java
+++ b/battery-business/src/main/java/com/battery/business/domain/vo/TestConfigurationOptionsVo.java
@@ -2,13 +2,18 @@
import java.util.List;
-/** 创建或编辑测试时由后端提供的有效选项。 */
+/** 创建或编辑测试时由后端提供的有效选项:只返回启用且已正式登记的记录。 */
public class TestConfigurationOptionsVo
{
+ /** 可选的测试负责人,取启用用户。 */
private List<TestUserOptionVo> users;
+ /** 可选的测试类型,取启用类型。 */
private List<TestTypeOptionVo> testTypes;
+ /** 可选的电池号,取启用电池组内的电池号。 */
private List<TestBatteryOptionVo> batteries;
+ /** 可选的检测柜通道,取已正式登记的通道。 */
private List<TestChannelOptionVo> channels;
+ /** 可选的采集器,取启用并已登记的采集器。 */
private List<TestCollectorOptionVo> collectors;
public List<TestUserOptionVo> getUsers() { return users; }
diff --git a/battery-business/src/main/java/com/battery/business/domain/vo/TestTypeOptionVo.java b/battery-business/src/main/java/com/battery/business/domain/vo/TestTypeOptionVo.java
index a41ca58..d84aa1b 100644
--- a/battery-business/src/main/java/com/battery/business/domain/vo/TestTypeOptionVo.java
+++ b/battery-business/src/main/java/com/battery/business/domain/vo/TestTypeOptionVo.java
@@ -3,7 +3,9 @@
/** 启用的测试类型选项。 */
public class TestTypeOptionVo
{
+ /** 测试类型主键。 */
private Long testTypeId;
+ /** 测试类型名称。 */
private String testTypeName;
public Long getTestTypeId() { return testTypeId; }
diff --git a/battery-business/src/main/java/com/battery/business/domain/vo/TestUserOptionVo.java b/battery-business/src/main/java/com/battery/business/domain/vo/TestUserOptionVo.java
index 9f8a883..cc4dde6 100644
--- a/battery-business/src/main/java/com/battery/business/domain/vo/TestUserOptionVo.java
+++ b/battery-business/src/main/java/com/battery/business/domain/vo/TestUserOptionVo.java
@@ -3,8 +3,11 @@
/** 可担任测试负责人的启用用户选项。 */
public class TestUserOptionVo
{
+ /** 用户主键,提交测试时作为负责人 ID。 */
private Long userId;
+ /** 登录账号,账号为空时不展示。 */
private String userName;
+ /** 用户昵称,优先作为负责人的展示名称。 */
private String nickName;
public Long getUserId() { return userId; }
diff --git a/battery-business/src/main/java/com/battery/business/service/TodoService.java b/battery-business/src/main/java/com/battery/business/service/TodoService.java
index b7d36e6..5912fe5 100644
--- a/battery-business/src/main/java/com/battery/business/service/TodoService.java
+++ b/battery-business/src/main/java/com/battery/business/service/TodoService.java
@@ -52,8 +52,18 @@
return key.getKey().longValue();
}
- /** 负责人只能将自己的待处理事项完成一次;写入条件同时校验身份和当前状态。 */
- public void handle(long id, long ownerId, String remark) {
+ /**
+ * 负责人把自己的待处理事项标记为已处理,并保存处理说明。
+ *
+ * <p>前置条件:操作人就是该待办的负责人,处理说明非空且不超过 500 个字符。
+ * 写入条件同时校验身份和当前状态,已处理的事项不能重复处理。</p>
+ *
+ * @param id 待办主键
+ * @param ownerId 操作人用户 ID
+ * @param remark 处理说明
+ * @throws ServiceException 处理说明为空或超长、待办不存在、已处理或不属于该负责人
+ */
+ public void completeTodo(long id, long ownerId, String remark) {
String value = required(remark, 500, "处理说明");
if (jdbc.update("update t_todo set status='已处理',handle_remark=?,operated_at_utc=? where todo_id=? and owner_id=? and status='待处理'",
value, new Timestamp(System.currentTimeMillis()), id, ownerId) != 1)
diff --git a/battery-business/src/main/java/com/battery/business/service/impl/DeviceAdapterRuntimeCoordinator.java b/battery-business/src/main/java/com/battery/business/service/impl/DeviceAdapterRuntimeCoordinator.java
index 60d1746..39d45a4 100644
--- a/battery-business/src/main/java/com/battery/business/service/impl/DeviceAdapterRuntimeCoordinator.java
+++ b/battery-business/src/main/java/com/battery/business/service/impl/DeviceAdapterRuntimeCoordinator.java
@@ -17,9 +17,11 @@
public DeviceAdapterRuntimeCoordinator(IDeviceAdapterService service) { this.service = service; }
+ /** 应用就绪时恢复已启用的适配器实例;不限定 profile,真实来源也要在非开发 profile 下运行。 */
@EventListener(ApplicationReadyEvent.class)
public void restoreEnabledAdapters() { service.synchronizeEnabledAdapters(); }
+ /** 每约 1 秒检查一次到期实例并执行只读发现,实际取数节奏由各实例自己的轮询周期决定。 */
@Scheduled(fixedDelay = 1000)
public void pollDueAdapters() { service.pollDueAdapters(); }
}
diff --git a/battery-business/src/main/java/com/battery/business/service/impl/DeviceIngestServiceImpl.java b/battery-business/src/main/java/com/battery/business/service/impl/DeviceIngestServiceImpl.java
index 09f2ff8..bb44aeb 100644
--- a/battery-business/src/main/java/com/battery/business/service/impl/DeviceIngestServiceImpl.java
+++ b/battery-business/src/main/java/com/battery/business/service/impl/DeviceIngestServiceImpl.java
@@ -83,12 +83,27 @@
this.credentials = credentials;
}
+ /**
+ * 接收推式来源程序的一批载荷:校验来源与令牌,按载荷类别写入落地表,并刷新该来源的心跳。
+ *
+ * <p>前置条件:来源已登记为启用中的推式实例,端点令牌与本地凭据一致,载荷为合法 JSON 且类别已知。
+ * 整批在同一事务内落库,任一行失败则整批回滚;重复推送按来源行身份幂等忽略,内容不一致的行记为冲突
+ * 而不覆盖既有事实。心跳、拓扑与采样都会刷新心跳时间。</p>
+ *
+ * @param adapterCode 来源实例编码,取自接收端点路径
+ * @param token 端点令牌,取自请求头
+ * @param body 载荷正文,含 kind 与对应的行数据
+ * @return 本批的接收计数:成功写入、幂等忽略、内容冲突的行数
+ * @throws ServiceException 来源不是启用的推式实例、令牌不匹配、载荷格式非法或行数超出单批上限
+ */
@Override
@Transactional(rollbackFor = Exception.class)
public IngestResult ingest(String adapterCode, String token, String body)
{
+ // STEP 1: 校验来源是启用中的推式实例,并校验端点令牌。
TDeviceAdapter adapter = requirePushSource(adapterCode);
requireToken(adapter, token);
+ // STEP 2: 解析载荷并按 kind 分流;未知类别直接拒绝,不按猜测处理。
JSONObject payload = parse(body);
String kind = payload.getString("kind");
Instant receivedAt = clock.instant();
@@ -109,6 +124,7 @@
{
throw payloadInvalid("未知的载荷类别 kind:" + kind);
}
+ // STEP 3: 刷新心跳时间;每一批成功接收都是"程序还在推"的证据。
// 每一批成功接收都刷新心跳:收到采样行同样是"程序还在推"的证据,缺一次心跳不会误判离线。
touchHeartbeat(adapterCode, receivedAt);
return new IngestResult(counters.accepted, counters.ignored, counters.conflicts);
diff --git a/battery-business/src/main/java/com/battery/business/service/impl/PushIngestAdapter.java b/battery-business/src/main/java/com/battery/business/service/impl/PushIngestAdapter.java
index ac014a2..7645d61 100644
--- a/battery-business/src/main/java/com/battery/business/service/impl/PushIngestAdapter.java
+++ b/battery-business/src/main/java/com/battery/business/service/impl/PushIngestAdapter.java
@@ -83,12 +83,14 @@
this.clock = clock;
}
+ /** 返回本实例登记的适配器编码;推送端点与通道身份都用它定位来源。 */
@Override
public String adapterCode()
{
return config.adapterCode();
}
+ /** 返回本实例登记的能力标记;推式来源同样只观测,不提供控制能力。 */
@Override
public AdapterCapabilities capabilities()
{
@@ -135,6 +137,12 @@
return Map.copyOf(result);
}
+ /**
+ * 返回该通道在落地表里最近一条采样行投影成的实时观测。
+ *
+ * @param channel 检测柜通道身份
+ * @return 最近一条采样行的实时观测;该通道还没有采样行时为空
+ */
@Override
public CabinetRealtime latestCabinet(ChannelIdentity channel)
{
@@ -169,6 +177,7 @@
return List.of();
}
+ /** 返回推式来源固定的持久化槽位间隔;采样行按来源自己的落库节奏到达。 */
@Override
public long persistenceIntervalMillis(ChannelIdentity channel)
{
diff --git a/battery-business/src/main/java/com/battery/business/service/impl/PushIngestAdapterFactory.java b/battery-business/src/main/java/com/battery/business/service/impl/PushIngestAdapterFactory.java
index f1bc3f3..892cbe1 100644
--- a/battery-business/src/main/java/com/battery/business/service/impl/PushIngestAdapterFactory.java
+++ b/battery-business/src/main/java/com/battery/business/service/impl/PushIngestAdapterFactory.java
@@ -20,12 +20,19 @@
this.records = records;
}
+ /** 返回本工厂负责的存储形态编码 PUSH_INGEST。 */
@Override
public String adapterType()
{
return PushIngestAdapter.ADAPTER_TYPE;
}
+ /**
+ * 按登记的实例配置构造读落地表的适配器。
+ *
+ * @param config 实例配置,含编码、时区、轮询周期与来源形态配置
+ * @return 读落地表的推式来源适配器
+ */
@Override
public DeviceAdapter create(AdapterInstanceConfig config)
{
diff --git a/battery-business/src/main/java/com/battery/business/service/impl/RoutingTestDataObservationAdapter.java b/battery-business/src/main/java/com/battery/business/service/impl/RoutingTestDataObservationAdapter.java
index 067c86b..99adf2a 100644
--- a/battery-business/src/main/java/com/battery/business/service/impl/RoutingTestDataObservationAdapter.java
+++ b/battery-business/src/main/java/com/battery/business/service/impl/RoutingTestDataObservationAdapter.java
@@ -10,6 +10,7 @@
import com.battery.device.api.model.ChannelIdentity;
import com.battery.device.api.model.DetectedChannel;
import com.battery.device.api.model.DeviceObservation;
+import com.battery.device.api.registry.AdapterRegistryException;
import com.battery.device.api.registry.DeviceAdapterRegistry;
/**
@@ -36,12 +37,14 @@
this.registry = registry;
}
+ /** 返回聚合路由的固定编码,仅用于说明"按通道身份路由",不是任何来源实例的编码。 */
@Override
public String adapterCode()
{
return ADAPTER_CODE;
}
+ /** 返回聚合适配器声明的能力:只观测,不提供控制能力。 */
@Override
public AdapterCapabilities capabilities()
{
@@ -77,12 +80,27 @@
return grouped;
}
+ /**
+ * 按通道所属来源实例请求该通道最近一次实时观测。
+ *
+ * @param channel 检测柜通道身份,其中的适配器编码决定请求送到哪个实例
+ * @return 最近一次实时观测;没有成功观测时为空
+ * @throws AdapterRegistryException 该通道所属的实例未启用,或实例未提供测试数据观测契约
+ */
@Override
public CabinetRealtime latestCabinet(ChannelIdentity channel)
{
return registry.executeTestData(channel.adapterCode(), adapter -> adapter.latestCabinet(channel));
}
+ /**
+ * 按来源实例请求采集器的辅助测量快照。
+ *
+ * @param adapterCode 来源实例编码;为空表示来源未知,直接返回空值而不猜也不回退
+ * @param collectorCode 采集器编码
+ * @return 采集器实时快照;来源未知时为空
+ * @throws AdapterRegistryException 来源实例未启用,或实例未提供测试数据观测契约
+ */
@Override
public CollectorRealtime latestCollector(String adapterCode, String collectorCode)
{
@@ -92,36 +110,79 @@
return registry.executeTestData(adapterCode, adapter -> adapter.latestCollector(adapterCode, collectorCode));
}
+ /**
+ * 按通道所属来源实例请求设备直接提供的工步统计。
+ *
+ * @param channel 检测柜通道身份
+ * @return 该通道的设备工步统计;设备未提供时为空列表
+ * @throws AdapterRegistryException 该通道所属的实例未启用,或实例未提供测试数据观测契约
+ */
@Override
public List<StepRecord> stepRecords(ChannelIdentity channel)
{
return registry.executeTestData(channel.adapterCode(), adapter -> adapter.stepRecords(channel));
}
+ /**
+ * 按通道所属来源实例请求循环统计,来源类别由实例写在记录上。
+ *
+ * @param channel 检测柜通道身份
+ * @return 该通道的循环统计;设备未提供且无法聚合时为空列表
+ * @throws AdapterRegistryException 该通道所属的实例未启用,或实例未提供测试数据观测契约
+ */
@Override
public List<CycleRecord> cycleRecords(ChannelIdentity channel)
{
return registry.executeTestData(channel.adapterCode(), adapter -> adapter.cycleRecords(channel));
}
+ /**
+ * 按通道所属来源实例取该来源定义的实时持久化槽位间隔。
+ *
+ * @param channel 检测柜通道身份
+ * @return 持久化槽位间隔,单位毫秒
+ * @throws AdapterRegistryException 该通道所属的实例未启用,或实例未提供测试数据观测契约
+ */
@Override
public long persistenceIntervalMillis(ChannelIdentity channel)
{
return registry.executeTestData(channel.adapterCode(), adapter -> adapter.persistenceIntervalMillis(channel));
}
+ /**
+ * 按通道所属来源实例取设备计划方案,只用于实时展示。
+ *
+ * @param channel 检测柜通道身份
+ * @return 设备计划方案;设备未提供时为空
+ * @throws AdapterRegistryException 该通道所属的实例未启用,或实例未提供测试数据观测契约
+ */
@Override
public TestPlan testPlan(ChannelIdentity channel)
{
return registry.executeTestData(channel.adapterCode(), adapter -> adapter.testPlan(channel));
}
+ /**
+ * 按通道所属来源实例读取接收水位,用于把开始接收与继续接收切在这一时刻之后。
+ *
+ * @param channel 检测柜通道身份
+ * @return 通道当前接收水位;来源不按设备数据行接收时为空
+ * @throws AdapterRegistryException 该通道所属的实例未启用,或实例未提供测试数据观测契约
+ */
@Override
public SampleWatermark currentWatermark(ChannelIdentity channel)
{
return registry.executeTestData(channel.adapterCode(), adapter -> adapter.currentWatermark(channel));
}
+ /**
+ * 按通道所属来源实例读取严格晚于水位的数据行,同秒多行各自成立、不按秒合并。
+ *
+ * @param channel 检测柜通道身份
+ * @param watermark 上次取得的水位;为空表示尚未取水位,此时返回空列表而不并入设备历史
+ * @return 水位之后的设备数据行,按设备时间与协议序号升序
+ * @throws AdapterRegistryException 该通道所属的实例未启用,或实例未提供测试数据观测契约
+ */
@Override
public List<CabinetRealtime> samplesSince(ChannelIdentity channel, SampleWatermark watermark)
{
diff --git a/battery-business/src/main/java/com/battery/business/service/impl/TBatteryGroupTemplateServiceImpl.java b/battery-business/src/main/java/com/battery/business/service/impl/TBatteryGroupTemplateServiceImpl.java
index 114e6fe..492ac43 100644
--- a/battery-business/src/main/java/com/battery/business/service/impl/TBatteryGroupTemplateServiceImpl.java
+++ b/battery-business/src/main/java/com/battery/business/service/impl/TBatteryGroupTemplateServiceImpl.java
@@ -29,6 +29,13 @@
@Autowired
private TBatteryGroupTemplateMapper batteryGroupTemplateMapper;
+ /**
+ * 按主键读取电池组模板,并装配模板的字段关系与显示顺序。
+ *
+ * @param templateId 模板主键
+ * @return 模板及其字段关系
+ * @throws ServiceException 模板ID为空或模板不存在
+ */
@Override
public TBatteryGroupTemplate selectBatteryGroupTemplateById(Long templateId)
{
@@ -45,12 +52,27 @@
return template;
}
+ /**
+ * 返回正常状态的模板选项,供电池组选择字段模板。
+ *
+ * @return 模板选项列表;没有可用模板时为空列表
+ */
@Override
public List<TBatteryGroupTemplate> selectBatteryGroupTemplateOptions()
{
return batteryGroupTemplateMapper.selectBatteryGroupTemplateOptions();
}
+ /**
+ * 新增电池组模板,在同一事务内写入模板主记录与字段关系。
+ *
+ * <p>前置条件:模板名称在当前数据中唯一,字段不能为空且不能重复,字段须启用,
+ * 排序数量与字段数量一致。字段关系整体按提交的字段与顺序写入。</p>
+ *
+ * @param template 模板配置,含字段主键与可选的显示顺序
+ * @return 写入的模板主记录行数
+ * @throws ServiceException 模板信息为空、名称重复、字段为空或重复、字段不存在或已禁用、排序不合法
+ */
@Override
@Transactional(rollbackFor = Exception.class)
public int insertBatteryGroupTemplate(TBatteryGroupTemplate template)
@@ -62,11 +84,23 @@
template.setUpdateBy(template.getCreateBy());
}
template.setCreateTime(new Date());
+ // STEP 1: 写入模板主记录,字段关系依赖本次生成的主键。
int rows = batteryGroupTemplateMapper.insertBatteryGroupTemplate(template);
+ // STEP 2: 在同一事务内整体写入字段关系与显示顺序。
replaceFields(template);
return rows;
}
+ /**
+ * 修改电池组模板,字段关系按提交内容整体替换。
+ *
+ * <p>前置条件:模板存在,模板名称不与其它模板重复(允许与自身同名),
+ * 字段与排序的校验要求同新增。已有电池组引用的只是字段定义,模板变更不改写电池组当前字段值。</p>
+ *
+ * @param template 模板配置,须带模板主键
+ * @return 更新的模板主记录行数
+ * @throws ServiceException 模板ID为空、模板不存在、名称重复或字段与排序校验不通过
+ */
@Override
@Transactional(rollbackFor = Exception.class)
public int updateBatteryGroupTemplate(TBatteryGroupTemplate template)
@@ -82,7 +116,9 @@
validateTemplate(template, true);
template.setTemplateName(trimToNull(template.getTemplateName()));
template.setUpdateTime(new Date());
+ // STEP 1: 更新模板主记录。
int rows = batteryGroupTemplateMapper.updateBatteryGroupTemplate(template);
+ // STEP 2: 字段关系整体替换:先清理旧关系再按提交顺序写入。
replaceFields(template);
return rows;
}
diff --git a/battery-business/src/main/java/com/battery/business/service/impl/TTestServiceImpl.java b/battery-business/src/main/java/com/battery/business/service/impl/TTestServiceImpl.java
index 690bcb5..e7f29ad 100644
--- a/battery-business/src/main/java/com/battery/business/service/impl/TTestServiceImpl.java
+++ b/battery-business/src/main/java/com/battery/business/service/impl/TTestServiceImpl.java
@@ -45,6 +45,13 @@
@Autowired
private TTestMapper testMapper;
+ /**
+ * 按测试主键读取测试配置,同时装配电池号、阈值、通道绑定等配置关系。
+ *
+ * @param testId 测试主键
+ * @return 测试配置及其业务状态
+ * @throws ServiceException 测试ID为空或测试不存在
+ */
@Override
public TTest selectTestById(Long testId)
{
@@ -55,6 +62,15 @@
return test;
}
+ /**
+ * 按正式测试状态、接收状态及配置条件查询测试列表。
+ *
+ * <p>查询条件中的测试状态与接收状态都须是现行取值,历史兼容状态不参与筛选。</p>
+ *
+ * @param test 查询条件;为空时按无条件查询处理
+ * @return 测试列表,含各测试绑定的电池号
+ * @throws ServiceException 传入的测试状态或接收状态不是现行取值
+ */
@Override
public List<TTest> selectTestList(TTest test)
{
@@ -68,6 +84,11 @@
return tests;
}
+ /**
+ * 返回创建或编辑测试时可选的来源:启用用户、启用测试类型、启用电池组的电池号、已正式登记的通道与采集器。
+ *
+ * @return 测试配置选项,五类选项都可能为空列表
+ */
@Override
public TestConfigurationOptionsVo selectConfigurationOptions()
{
@@ -80,36 +101,62 @@
return options;
}
+ /**
+ * 创建一条待测试记录,在同一事务内写入测试配置和电池号、阈值、通道绑定关系。
+ *
+ * <p>前置条件:调用方已从登录会话取得操作人,测试配置通过基本信息与配置关系校验。
+ * 创建人取当前操作人,监测超时缺省 30 分钟,初始状态固定为“待测试 + 未开始接收”。</p>
+ *
+ * @param test 测试配置,含电池号、阈值、检测柜通道绑定与采集器映射
+ * @return 新建测试主键
+ * @throws ServiceException 测试信息为空、操作人缺失或配置校验不通过
+ */
@Override
@Transactional(rollbackFor = Exception.class)
public Long createTest(TTest test)
{
if (test == null) throw new ServiceException("测试信息不能为空");
+ // STEP 1: 规范化系统赋值字段,创建人取当前操作人,监测超时缺省 30 分钟。
test.setCreatorId(test.getOperatorId());
if (test.getMonitorTimeoutMinutes() == null) test.setMonitorTimeoutMinutes(30);
+ // STEP 2: 校验基本信息与电池号、阈值、通道绑定等配置关系。
validateBase(test, false);
validateConfiguration(test);
+ // STEP 3: 初始化为“待测试 + 未开始接收”,测试状态只在开始接收时流转。
test.setTestStatus(WAITING);
test.setReceptionStatus(NOT_STARTED);
test.setDelFlag("0");
test.setCreateTime(new Date());
testMapper.insertTest(test);
+ // STEP 4: 在同一事务内写入电池号、阈值、通道绑定与采集器映射。
saveConfiguration(test);
// 最终基线移除了 t_test_audit;Controller 的 @Log 负责记录创建操作,业务表只保存配置事实。
return test.getTestId();
}
+ /**
+ * 仅在开始接收之前整体替换测试配置,测试状态与接收状态保持不变。
+ *
+ * <p>前置条件:测试处于“待测试 + 未开始接收”。开始接收后设备数据已按配置归属,
+ * 配置不再允许修改;替换范围包括电池号、阈值和通道绑定,在同一事务内完成。</p>
+ *
+ * @param test 新的测试配置,须带测试主键
+ * @return 更新行数,成功固定为 1
+ * @throws ServiceException 测试ID为空、测试不存在、已开始接收或配置校验不通过
+ */
@Override
@Transactional(rollbackFor = Exception.class)
public int updateTest(TTest test)
{
if (test == null || test.getTestId() == null) throw new ServiceException("测试ID不能为空");
+ // STEP 1: 取测试行锁并确认仍处于“待测试 + 未开始接收”。
TTest current = testMapper.selectTestByIdForUpdate(test.getTestId());
if (current == null) throw new ServiceException("测试不存在");
loadConfiguration(current);
if (!stateMatches(current, WAITING, NOT_STARTED))
throw new ServiceException("开始接收后不能修改测试配置");
+ // STEP 2: 保留创建人与两个状态字段,只接受配置类字段的修改。
test.setCreatorId(current.getCreatorId());
test.setTestStatus(current.getTestStatus());
test.setReceptionStatus(current.getReceptionStatus());
@@ -121,7 +168,7 @@
int rows = testMapper.updateTest(test);
if (rows != 1) throw new ServiceException("测试配置已变更,请刷新后重试");
- // 开始接收前没有数据归属事实,配置关系可在同一事务内整体替换;操作审计由 Controller @Log 追加。
+ // STEP 3: 开始接收前没有数据归属事实,配置关系可在同一事务内整体替换;操作审计由 Controller @Log 追加。
testMapper.deleteTestCollectorChannelsByTestId(test.getTestId());
testMapper.deleteTestBindingsByTestId(test.getTestId());
testMapper.deleteTestBatteriesByTestId(test.getTestId());
@@ -131,7 +178,17 @@
}
/**
- * 建立测试接收边界;来源离线不阻断状态流转,只有成功观测数据才会写入测试事实。
+ * 开始接收:校验全部绑定来源后建立接收边界,边界之后的成功观测才归属本测试。
+ *
+ * <p>前置条件:测试处于“待测试 + 未开始接收”,每个电池号都绑定启用的检测柜通道与采集器,
+ * 且同一检测柜通道在本测试内不重复。来源离线不阻断状态流转,允许从已运行设备中途接入;
+ * 本动作只改变系统数据请求和测试状态,不向设备发送控制命令。</p>
+ *
+ * @param testId 测试主键
+ * @param operatorId 操作人用户 ID
+ * @param updateBy 更新人账号,写入测试行的 update_by
+ * @return 更新行数,成功固定为 1
+ * @throws ServiceException 入参为空、测试不存在、状态不匹配、来源校验失败或数据来源已被其他测试占用
*/
@Override
@Transactional(rollbackFor = Exception.class)
@@ -139,6 +196,7 @@
{
if (testId == null) throw new ServiceException("测试ID不能为空");
if (operatorId == null) throw new ServiceException("操作人不能为空");
+ // STEP 1: 取测试行锁,确认状态为“待测试 + 未开始接收”并加载绑定配置。
TTest current = testMapper.selectTestByIdForUpdate(testId);
if (current == null) throw new ServiceException("测试不存在");
if (!stateMatches(current, WAITING, NOT_STARTED))
@@ -146,6 +204,7 @@
loadConfiguration(current);
if (current.getBatteries().isEmpty()) throw new ServiceException("测试未配置电池号");
+ // STEP 2: 逐电池号校验检测柜通道唯一、通道启用、采集器存在且映射完整。
Date actionAt = new Date();
Set<Long> channelIds = new HashSet<>();
Set<String> collectorChannels = new HashSet<>();
@@ -171,6 +230,7 @@
}
+ // STEP 3: 在测试行锁内推进为“检测中 + 接收中”,并把此刻写为接收边界。
try
{
if (testMapper.updateReceptionStarted(testId, actionAt, runtimeAttach, updateBy) != 1)
@@ -179,21 +239,36 @@
}
catch (DuplicateKeyException exception)
{
+ // 检测柜通道已被其他接收中的测试占用,唯一索引保证不会同时归属两个测试。
throw new ServiceException("数据来源已被其他测试占用,请刷新后重试");
}
}
+ /**
+ * 暂停接收:停止为本次测试请求实时数据,保留已持久化的历史事实。
+ *
+ * <p>前置条件:测试处于“检测中 + 接收中”。设备不接收暂停命令,
+ * 暂停期间停止请求数据且不补采,暂停空档不视为数据缺口。</p>
+ *
+ * @param testId 测试主键
+ * @param operatorId 操作人用户 ID
+ * @param updateBy 更新人账号,写入测试行的 update_by
+ * @return 更新行数,成功固定为 1
+ * @throws ServiceException 入参为空、测试不存在或当前不是接收中状态
+ */
@Override
@Transactional(rollbackFor = Exception.class)
public int pauseReception(Long testId, Long operatorId, String updateBy)
{
if (testId == null) throw new ServiceException("测试ID不能为空");
if (operatorId == null) throw new ServiceException("操作人不能为空");
+ // STEP 1: 取测试行锁,确认状态为“检测中 + 接收中”。
TTest current = testMapper.selectTestByIdForUpdate(testId);
if (current == null) throw new ServiceException("测试不存在");
if (!stateMatches(current, "RUNNING", "RECEIVING"))
throw new ServiceException("当前测试不是接收中状态");
+ // STEP 2: 推进为“暂停 + 暂停接收”;不记录接收边界,恢复时再重置边界。
Date actionAt = new Date();
if (testMapper.updateReceptionPaused(testId, updateBy, actionAt) != 1)
throw new ServiceException("测试状态已变更,请刷新后重试");
@@ -201,35 +276,63 @@
return 1;
}
+ /**
+ * 继续接收:为全部绑定来源恢复实时数据请求,只追加新的成功接收事实。
+ *
+ * <p>前置条件:测试处于“暂停 + 暂停接收”,且绑定来源仍可用。
+ * 继续接收会刷新接收边界,暂停空档不补采;设备已经结束时不得继续接收。</p>
+ *
+ * @param testId 测试主键
+ * @param operatorId 操作人用户 ID
+ * @param updateBy 更新人账号,写入测试行的 update_by
+ * @return 更新行数,成功固定为 1
+ * @throws ServiceException 入参为空、测试不存在或当前不是暂停接收状态
+ */
@Override
@Transactional(rollbackFor = Exception.class)
public int resumeReception(Long testId, Long operatorId, String updateBy)
{
if (testId == null) throw new ServiceException("测试ID不能为空");
if (operatorId == null) throw new ServiceException("操作人不能为空");
+ // STEP 1: 取测试行锁,确认状态为“暂停 + 暂停接收”。
TTest current = testMapper.selectTestByIdForUpdate(testId);
if (current == null) throw new ServiceException("测试不存在");
if (!stateMatches(current, "PAUSED", "PAUSED"))
throw new ServiceException("当前测试不是暂停接收状态");
- // 加锁查询只返回测试主表,继续接收前重新加载绑定配置以校验来源仍可用。
+ // STEP 2: 加锁查询只返回测试主表,继续接收前重新加载绑定配置以校验来源仍可用。
loadConfiguration(current);
+ // STEP 3: 推进为“检测中 + 接收中”,并把此刻刷新为新的接收边界。
Date actionAt = new Date();
if (testMapper.updateReceptionResumed(testId, updateBy, actionAt) != 1)
throw new ServiceException("测试状态已变更,请刷新后重试");
return 1;
}
+ /**
+ * 停止测试:停止数据请求并进入“检测完成 + 已结束接收”的不可恢复终态。
+ *
+ * <p>前置条件:测试处于“检测中 + 接收中”或“暂停 + 暂停接收”。数据缺失、序号跳跃和
+ * 采样间隔异常不阻止完成;不向检测柜或采集器发送停止命令,设备仍按现场事实独立运行。</p>
+ *
+ * @param testId 测试主键
+ * @param operatorId 操作人用户 ID
+ * @param updateBy 更新人账号,写入测试行的 update_by
+ * @return 更新行数,成功固定为 1
+ * @throws ServiceException 入参为空、测试不存在或当前状态不允许停止
+ */
@Override
@Transactional(rollbackFor = Exception.class)
public int stopTest(Long testId, Long operatorId, String updateBy)
{
if (testId == null) throw new ServiceException("测试ID不能为空");
if (operatorId == null) throw new ServiceException("操作人不能为空");
+ // STEP 1: 取测试行锁,确认处于可停止的两种状态之一。
TTest current = testMapper.selectTestByIdForUpdate(testId);
if (current == null) throw new ServiceException("测试不存在");
if (!(stateMatches(current, "RUNNING", "RECEIVING")
|| stateMatches(current, "PAUSED", "PAUSED")))
throw new ServiceException("当前测试不能停止");
+ // STEP 2: 推进为“检测完成 + 已结束接收”并记录结束时间,不执行完整性门禁。
Date actionAt = new Date();
if (testMapper.updateReceptionStopped(testId, updateBy, actionAt) != 1)
throw new ServiceException("测试状态已变更,请刷新后重试");
@@ -248,6 +351,19 @@
&& expectedTestStatus.equals(test.getTestStatus());
}
+ /**
+ * 在任意生命周期整体删除测试及其全部子数据,删除后不可恢复。
+ *
+ * <p>前置条件:调用方已确认操作人身份。逐条测试加测试行锁后清理绑定、阈值、三类数据、
+ * 逻辑工步、预警异常和修订记录,最后删除测试主记录;按 ADR-0047 不写操作日志、修订记录或墓碑,
+ * 也不删除电池组、设备登记和独立保存的报表。</p>
+ *
+ * @param testIds 待删除的测试主键数组
+ * @param operatorId 操作人用户 ID
+ * @param updateBy 更新人账号
+ * @return 实际删除的测试数量
+ * @throws ServiceException 未选择测试或测试不存在
+ */
@Override
@Transactional(rollbackFor = Exception.class)
public int deleteTestByIds(Long[] testIds, Long operatorId, String updateBy)
@@ -256,9 +372,10 @@
int rows = 0;
for (Long testId : testIds)
{
+ // STEP 1: 加测试行锁,与接收事务互斥;删除提交后不能再向该测试写入数据。
TTest current = testMapper.selectTestByIdForUpdate(testId);
if (current == null) throw new ServiceException("测试不存在");
- // 与接收事务使用同一测试行锁,删除提交后不能再向该测试写入数据;整体删除不留审计或墓碑。
+ // STEP 2: 在同一事务内清理全部测试子数据,整体删除不留审计或墓碑。
testMapper.deleteTestRealtimeRecordsByTestId(testId);
testMapper.deleteTestStepRecordsByTestId(testId);
testMapper.deleteTestCycleRecordsByTestId(testId);
@@ -269,6 +386,7 @@
testMapper.deleteTestBindingsByTestId(testId);
testMapper.deleteTestThresholdRowsByTestId(testId);
testMapper.deleteTestBatteriesByTestId(testId);
+ // STEP 3: 最后删除测试主记录,行数不为 1 说明配置已被并发改动。
int deleted = testMapper.deleteTestById(testId, updateBy);
if (deleted != 1) throw new ServiceException("测试配置已变更,请刷新后重试");
rows += deleted;
@@ -276,6 +394,19 @@
return rows;
}
+ /**
+ * 保存当前测试的逻辑工步合并规则,整体替换原有映射。
+ *
+ * <p>前置条件:映射须按循环内连续位置升序、互不重叠、编号从 1 起,且每项都有名称。
+ * 逻辑工步只用于统计图表和报表等派生视图,不改写原始采样记录。</p>
+ *
+ * @param testId 测试主键
+ * @param mappings 逻辑工步映射列表,为空表示清空
+ * @param operatorId 操作人用户 ID
+ * @param updateBy 更新人账号
+ * @return 更新行数,成功固定为 1
+ * @throws ServiceException 测试或操作人为空、测试不存在或映射不满足连续升序且互不重叠
+ */
@Override
@Transactional(rollbackFor = Exception.class)
public int saveLogicStepMappings(Long testId, List<TTestLogicStepMapping> mappings, Long operatorId, String updateBy)
@@ -283,6 +414,7 @@
if (testId == null || operatorId == null) throw new ServiceException("测试和操作人不能为空");
TTest current = testMapper.selectTestByIdForUpdate(testId);
if (current == null) throw new ServiceException("测试不存在");
+ // STEP 1: 校验映射为连续的循环内位置区间,编号从 1 起且互不重叠。
List<TTestLogicStepMapping> normalized = mappings == null ? List.of() : mappings;
int previousEnd = 0;
int expectedNo = 1;
@@ -297,25 +429,42 @@
previousEnd = mapping.getEndStepNo();
expectedNo++;
}
+ // STEP 2: 整体替换映射 JSON,作用于该测试的全部循环。
String newJson = JSON.toJSONString(normalized);
int rows = testMapper.updateLogicStepMappings(testId, newJson, updateBy, new Date());
if (rows != 1) throw new ServiceException("工步设置已变更,请刷新后重试");
return rows;
}
+ /**
+ * 保存测试详情当前最新版,允许测试处于任意状态。
+ *
+ * <p>前置条件:实验名称不超过 200 个字符,字段名称与字段值通过校验。
+ * 详情只保留当前最新版,直接读取电池组当前字段值,不创建测试字段快照,也不产生修订记录。</p>
+ *
+ * @param testId 测试主键
+ * @param detail 测试补充详情,含实验名称、字段名称与人工填写值
+ * @param operatorId 操作人用户 ID
+ * @param updateBy 更新人账号
+ * @return 更新行数,成功固定为 1
+ * @throws ServiceException 测试或操作人为空、测试不存在、实验名称或字段校验不通过
+ */
@Override
@Transactional(rollbackFor = Exception.class)
public int saveConsoleDetail(Long testId, TTestConsoleDetail detail, Long operatorId, String updateBy)
{
if (testId == null || operatorId == null || detail == null) throw new ServiceException("测试详情不能为空");
+ // STEP 1: 加测试行锁并确认测试存在。
TTest current = testMapper.selectTestByIdForUpdate(testId);
if (current == null) throw new ServiceException("测试不存在");
+ // STEP 2: 规范化并校验实验名称、字段名称与字段值。
String experimentName = trimToNull(detail.getExperimentName());
if (experimentName != null && experimentName.length() > 200) throw new ServiceException("实验名称长度不能超过200个字符");
detail.setExperimentName(experimentName);
validateConsoleFields(detail.getBatteryFields());
validateConsoleFields(detail.getSpecialFields());
validateConsoleValues(testId, detail);
+ // STEP 3: 覆盖保存最新版详情,不追加修订记录。
int rows = testMapper.updateConsoleDetail(testId, JSON.toJSONString(detail), updateBy, new Date());
if (rows != 1) throw new ServiceException("测试详情保存失败,请刷新后重试");
return rows;
diff --git a/battery-business/src/main/java/com/battery/business/service/impl/TestDataReceptionServiceImpl.java b/battery-business/src/main/java/com/battery/business/service/impl/TestDataReceptionServiceImpl.java
index a5a484e..5a77983 100644
--- a/battery-business/src/main/java/com/battery/business/service/impl/TestDataReceptionServiceImpl.java
+++ b/battery-business/src/main/java/com/battery/business/service/impl/TestDataReceptionServiceImpl.java
@@ -70,19 +70,28 @@
TestWarningExceptionService warningExceptions, Clock clock)
{ this.mapper = mapper; this.tests = tests; this.adapter = adapter; this.warningExceptions = warningExceptions; this.clock = clock; }
- /** 轮询接收中测试,判定实时超限和来源离线异常,并按独立周期保存成功观测。 */
+ /**
+ * 接收调度入口:对全部接收中的测试判定实时超限与来源离线异常,并按各自节奏保存成功观测。
+ *
+ * <p>前置条件:测试处于“检测中 + 接收中”。预警判定按约 5 秒节奏,柜端逐行接收和工步、
+ * 循环汇总按来源写库节奏限流;只有成功观测且可归属本测试的数据才写入事实表,不执行历史补采。</p>
+ *
+ * @throws org.springframework.dao.DataAccessException 任一写入失败时整个接收事务回滚,本轮不保存任何测试的数据
+ */
@Override @Transactional(rollbackFor = Exception.class)
public void captureReceivedData()
{
for (TTest test : receivingTests()) {
Instant now = clock.instant();
Instant previous = lastRealtimeCheckAt.get(test.getTestId());
+ // STEP 1: 按约 5 秒节奏判定当前来源离线事实与实时超限,预警不受采样入库周期限制。
// 实时预警独立于持久化槽位,并与暂停、停止和删除共享测试事务锁。
if (previous == null || !now.isBefore(previous.plusSeconds(5))) {
warningExceptions.recordOfflineSources(test, Date.from(now));
for (TTestBattery battery : batteries(test)) saveRealtime(test, battery);
lastRealtimeCheckAt.put(test.getTestId(), now);
}
+ // STEP 2: 单独按来源写库节奏读取工步与循环汇总,避免每秒重查厂商库。
for (TTestBattery battery : batteries(test)) saveDeviceAggregates(test, battery);
}
}
@@ -99,12 +108,21 @@
}
private List<TTestBattery> batteries(TTest test) { return test.getBatteries() == null ? List.of() : test.getBatteries(); }
+ /**
+ * 保存单个测试电池的一次实时观测:来源支持设备数据行时逐行落库,否则回落到轮询槽位。
+ *
+ * <p>前置条件:测试正在接收且绑定通道完整。柜端与采集器分别判断成功,失败来源的字段保持为空。</p>
+ *
+ * @param test 接收中的测试
+ * @param battery 测试内的一个电池号及其通道绑定
+ */
private void saveRealtime(TTest test, TTestBattery battery)
{
if (adapter == null) return;
TTestChannelBinding binding = battery.getBinding(); if (binding == null || binding.getDevid() == null || binding.getChlid() == null) return;
- // 来源以设备数据行为接收单位时,柜端事实逐行落库(同秒多行都保留),槽位只留给采集器端。
+ // STEP 1: 优先按来源水位逐行接收柜端事实(同秒多行都保留),槽位只留给采集器端。
boolean rowBased = persistCabinetSamples(test, battery, binding);
+ // STEP 2: 来源不按设备数据行接收时,落到轮询槽位,保持原来的组合观测语义。
saveRealtimeSlot(test, battery, binding, !rowBased);
}
@@ -112,6 +130,12 @@
* 按来源水位逐行接收柜端事实。水位是"开始接收"或"继续接收"这一刻来源里的最新一行,
* 因此水位之前的数据行即使仍在来源里也不进本测试,暂停期间的空档也不会被补回来。
*
+ * <p>前置条件:调用方持有该测试电池的接收游标。读取节奏按来源写库周期限流;
+ * 实例缺失、来源不可达或读取失败时明确失败并留痕,不回退到其他实例。</p>
+ *
+ * @param test 接收中的测试
+ * @param battery 测试内的一个电池号
+ * @param binding 该电池号绑定的检测柜通道
* @return 该来源是否按设备数据行接收;来源不支持或被拒绝时返回 false,调用方回落到轮询槽位
*/
private boolean persistCabinetSamples(TTest test, TTestBattery battery, TTestChannelBinding binding)
@@ -121,7 +145,7 @@
SampleCursor cursor = sampleCursors.computeIfAbsent(key, ignored -> new SampleCursor());
synchronized (cursor)
{
- // 开始接收与继续接收都会推进接收边界:边界变化即重新取水位,暂停期间的数据不补采。
+ // STEP 1: 开始接收与继续接收都会推进接收边界,边界变化即重新取水位,暂停期间的数据不补采。
if (!java.util.Objects.equals(cursor.boundary, boundary))
{
cursor.boundary = boundary; cursor.watermark = null; cursor.rowBased = null; cursor.sampledAt = null;
@@ -135,6 +159,7 @@
return Boolean.TRUE.equals(cursor.rowBased);
try
{
+ // STEP 2: 首次接触该边界时只记录水位,不把水位之前的历史行带入本测试。
if (cursor.watermark == null)
{
cursor.sampledAt = now;
@@ -149,6 +174,7 @@
cursor.rowBased = true;
return true;
}
+ // STEP 3: 逐行写入水位之后的设备事实,每行成功后推进水位。
List<CabinetRealtime> rows = adapter.samplesSince(channel, cursor.watermark);
cursor.sampledAt = now;
for (CabinetRealtime row : rows)
@@ -173,15 +199,22 @@
* 把一行设备事实写成一条实时记录;来源行身份保证重复接收幂等、同身份不同内容报冲突。
* 归属边界由水位在设备时间里判定(见 {@link #persistCabinetSamples}),
* 这里不用服务器取数时间反推设备时间。
+ *
+ * @param test 接收中的测试
+ * @param battery 测试内的一个电池号
+ * @param binding 该电池号绑定的检测柜通道
+ * @param row 来源返回的一行柜端事实;设备时间或服务器接收时间缺失时丢弃不写
*/
private void insertCabinetSample(TTest test, TTestBattery battery, TTestChannelBinding binding, CabinetRealtime row)
{
if (row.deviceTime() == null || row.serverReceivedAt() == null) return;
+ // STEP 1: 先用本次成功观测判定实时超限,异常不依赖采样入库周期。
Date observedAt = Date.from(row.serverReceivedAt());
evaluateCabinet(test, binding, "VOLTAGE", BigDecimal.valueOf(row.voltageV()), "V", observedAt);
evaluateCabinet(test, binding, "CURRENT", BigDecimal.valueOf(row.currentA()), "A", observedAt);
evaluateCabinet(test, binding, "CAPACITY", BigDecimal.valueOf(row.capacityAh()), "Ah", observedAt);
evaluateCabinet(test, binding, "ENERGY", BigDecimal.valueOf(row.energyWh()), "Wh", observedAt);
+ // STEP 2: 按来源身份组装记录,槽位留空以允许同秒多行各自成立。
String identity = sourceIdentity(binding);
String recordId = row.sourceRecordId() == null || row.sourceRecordId().isBlank()
? "cabinet:" + row.sequenceId() : row.sourceRecordId();
@@ -205,7 +238,7 @@
value.setExtensionMeasurementsJson(JSON.toJSONString(extension));
value.setSourceIdentity(identity); value.setSourceRecordId(recordId);
value.setContentSha256(hash(value));
- // 重复接收的同内容记录幂等忽略;同身份但内容不同时保留原事实,不覆盖。
+ // STEP 3: 按来源行身份幂等;同内容重复接收忽略,同身份但内容不同时保留原事实并告警,不覆盖。
String existing = mapper.selectRealtimeRecordHashBySourceRecord(battery.getTestBatteryId(), identity, recordId);
if (existing != null)
{
@@ -217,16 +250,27 @@
mapper.insertRealtimeRecord(value);
}
+ /**
+ * 向轮询槽位写入一条实时采样记录,柜端与采集器端的成功观测在同一行合并保存。
+ *
+ * <p>前置条件:调用方已判定该来源不按设备数据行接收。槽位按持久化周期对齐,同一槽位只写一行;
+ * 柜端和采集器端分别判断成功,失败来源的身份、时间和指标保持为空。</p>
+ *
+ * @param test 接收中的测试
+ * @param battery 测试内的一个电池号
+ * @param binding 该电池号绑定的检测柜通道
+ * @param includeCabinet 本次是否把柜端事实合并进该槽位
+ */
private void saveRealtimeSlot(TTest test, TTestBattery battery, TTestChannelBinding binding, boolean includeCabinet)
{
- // 柜端和采集器端分别判断成功;同一槽位只写一行,失败来源的身份、时间和指标保持为空。
+ // STEP 1: 柜端和采集器端分别判断成功,各取最近一次成功观测;同一槽位只写一行,失败来源的身份、时间和指标保持为空。
Instant now = clock.instant(); long interval = persistenceInterval(binding); String key = test.getTestId() + ":" + battery.getTestBatteryId();
CabinetRealtime cabinet = includeCabinet ? latestCabinet(binding) : null;
if (cabinet != null && !accepted(test, cabinet.deviceTime(), cabinet.serverReceivedAt())) cabinet = null;
CollectorRealtime collector = latestCollector(battery);
Map<String, CollectorMeasurement> mapped = mappedCollectorMetrics(test, battery, collector);
if (cabinet == null && (collector == null || !collector.online() || mapped.isEmpty())) return;
- // 当前成功测量先判定预警;槽位尚未到期或历史行被删除,都不能阻断实时告警。
+ // STEP 2: 槽位尚未到期或历史行被删除都不能阻断实时告警,当前成功测量先判定预警。
if (cabinet != null) {
Date observedAt = Date.from(cabinet.serverReceivedAt());
evaluateCabinet(test, binding, "VOLTAGE", BigDecimal.valueOf(cabinet.voltageV()), "V", observedAt);
@@ -238,11 +282,12 @@
warningExceptions.evaluate(test, "COLLECTOR", battery.getCollectorId() + ":" + measurement.channelCode(),
measurement.metricCode(), measurement.value(), measurement.unit(), Date.from(measurement.serverReceivedAt()));
}
+ // STEP 3: 按持久化周期限流并对齐槽位;删除修订随数据库保留,重启后同一槽位也不能重新出现。
Instant previous = lastSnapshotAt.get(key); if (previous != null && now.isBefore(previous.plusMillis(interval))) return;
Instant collectorTime = mapped.values().stream().map(CollectorMeasurement::deviceTime).max(Instant::compareTo).orElse(null);
Instant slot = Instant.ofEpochMilli((now.toEpochMilli() / interval) * interval);
- // 删除修订随数据库保留;重启后同一槽位也不能重新出现。
if (mapper.wasRealtimeSlotDeleted(test.getTestId(), battery.getTestBatteryId(), slot.toEpochMilli())) return;
+ // STEP 4: 组装组合观测行,只写成功来源的字段。
TTestRealtimeRecord row = new TTestRealtimeRecord(); row.setTestId(test.getTestId()); row.setTestBatteryId(battery.getTestBatteryId()); row.setBindingId(binding.getBindingId());
row.setAdapterCode(binding.getAdapterCode()); row.setCaptureSlotUtc(Date.from(slot)); row.setServerReceivedAtUtc(Date.from(now));
if (cabinet != null) { row.setCabinetDevtype(binding.getDevtype()); row.setCabinetDevid(binding.getDevid()); row.setCabinetSubdevid(binding.getSubdevid()); row.setCabinetChlid(binding.getChlid()); row.setCabinetAdapterTimezoneId(cabinet.timezoneId()); }
@@ -261,6 +306,7 @@
row.setExtensionMeasurementsJson(JSON.toJSONString(extension)); row.setSourceIdentity(sourceIdentity(binding));
row.setSourceRecordId(sourceRecordId(cabinet, mapped));
row.setContentSha256(hash(row));
+ // STEP 5: 同一槽位内容不相同报冲突并保留原事实;插入被忽略时再核对一次并发写入。
String old = mapper.selectRealtimeRecordContentHash(test.getTestId(), battery.getTestBatteryId(), row.getCaptureSlotUtc());
if (old != null) { if (!old.equalsIgnoreCase(HexFormat.of().formatHex(row.getContentSha256()))) log.warn("实时采样内容冲突,拒绝覆盖 testId={}, testBatteryId={}, captureSlotUtc={}", test.getTestId(), battery.getTestBatteryId(), slot); return; }
int inserted = mapper.insertRealtimeRecord(row);
@@ -278,8 +324,18 @@
if (value != null) warningExceptions.evaluate(test, "CABINET", sourceIdentity(binding), metricCode, value, unit, occurredAt);
}
+ /**
+ * 保存单个测试电池的工步与循环汇总事实,来源编号或明确的时间组合保证重复观测幂等。
+ *
+ * <p>前置条件:测试正在接收且绑定通道完整。只接受来源返回的统计记录,不从实时采样推算;
+ * 已按授权物理删除的汇总不会因为重复观测被写回。</p>
+ *
+ * @param test 接收中的测试
+ * @param battery 测试内的一个电池号及其通道绑定
+ */
private void saveDeviceAggregates(TTest test, TTestBattery battery)
{
+ // STEP 1: 按来源写库节奏限流,避免接收调度每秒重查厂商库。
// 工步、循环只接受来源返回的统计记录,并以来源编号或明确的时间组合执行幂等。
if (adapter == null) return; TTestChannelBinding binding = battery.getBinding(); if (binding == null || binding.getDevid() == null || binding.getChlid() == null) return;
String key = test.getTestId() + ":" + battery.getTestBatteryId();
@@ -287,9 +343,11 @@
Instant now = clock.instant(); Instant previous = lastAggregateAt.get(key);
if (previous != null && now.isBefore(previous.plusMillis(persistenceInterval(binding)))) return;
lastAggregateAt.put(key, now);
+ // STEP 2: 读取来源的工步与循环汇总;来源不可达时本轮放弃,下一轮重试。
ChannelIdentity channel = new ChannelIdentity(binding.getAdapterCode(), binding.getDevtype(), binding.getDevid(), binding.getSubdevid(), binding.getChlid());
List<StepRecord> steps; List<CycleRecord> cycles; try { steps = adapter.stepRecords(channel); cycles = adapter.cycleRecords(channel); } catch (RuntimeException ex) { return; }
String identity = sourceIdentity(binding);
+ // STEP 3: 逐条写入可归属的实际工步,已删除的来源行跳过,内容不一致时保留原事实。
for (StepRecord source : steps == null ? List.<StepRecord>of() : steps)
{
if (!accepted(test, source.endedAt(), source.serverReceivedAt())) continue;
@@ -305,6 +363,7 @@
log.warn("工步记录内容冲突,拒绝覆盖 testId={}, dedupeKey={}", test.getTestId(), row.getDedupeKey());
}
}
+ // STEP 4: 逐条写入可归属的循环汇总,来源类别随记录保存以便区分推导值与设备事实。
for (CycleRecord source : cycles == null ? List.<CycleRecord>of() : cycles)
{
if (!accepted(test, source.endedAt(), source.serverReceivedAt())) continue;
diff --git a/battery-business/src/main/java/com/battery/business/service/impl/TestWarningExceptionService.java b/battery-business/src/main/java/com/battery/business/service/impl/TestWarningExceptionService.java
index bd6c174..29d0a6c 100644
--- a/battery-business/src/main/java/com/battery/business/service/impl/TestWarningExceptionService.java
+++ b/battery-business/src/main/java/com/battery/business/service/impl/TestWarningExceptionService.java
@@ -7,6 +7,7 @@
import com.alibaba.fastjson2.JSON;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
+import com.ruoyi.common.exception.ServiceException;
import com.battery.business.domain.TTest;
import com.battery.business.domain.TTestWarningException;
import com.battery.business.domain.TTestWarningThreshold;
@@ -17,17 +18,43 @@
public class TestWarningExceptionService {
private final TTestMapper mapper;
public TestWarningExceptionService(TTestMapper mapper) { this.mapper = mapper; }
- /** 调用方须持有测试事务锁;仅在接收中记录当前绑定来源的离线事实。 */
+
+ /**
+ * 记录接收中测试的绑定来源离线事实。
+ *
+ * <p>前置条件:调用方持有该测试的事务锁。只有测试处于“检测中 + 接收中”才判定离线,
+ * 离线不伪造测量值;暂停、停止、未开始接收时不新增或累加。</p>
+ *
+ * @param test 接收中的测试
+ * @param occurredAt 离线事实发生时间
+ */
public void recordOfflineSources(TTest test, Date occurredAt) {
+ // STEP 1: 只有“检测中 + 接收中”才判定离线,其他状态不新增也不累加。
if (test == null || !"RUNNING".equals(test.getTestStatus()) || !"RECEIVING".equals(test.getReceptionStatus())) return;
+ // STEP 2: 按测试和来源身份聚合离线异常,不写入测量值或阈值。
for (var source : mapper.selectOfflineTestSources(test.getTestId())) {
mapper.upsertOfflineException(test.getTestId(), source.sourceType(), source.sourceIdentity(), occurredAt);
}
}
- /** 在持有测试事务锁时,对接收中的成功实时测量聚合开放异常,不受采样入库周期限制。 */
+ /**
+ * 对接收中的一次成功实时测量判定超限并按聚合键写入开放异常。
+ *
+ * <p>前置条件:调用方持有该测试的事务锁。判定只依据成功观测值,不受采样入库周期限制,
+ * 正常观测不会自动关闭已有异常。</p>
+ *
+ * @param test 接收中的测试,须已加载阈值规则
+ * @param sourceType 来源类型:CABINET 检测柜、COLLECTOR 采集器
+ * @param sourceIdentity 来源身份
+ * @param metricCode 指标编码
+ * @param value 本次成功观测值;为空时不判定
+ * @param unit 观测值单位
+ * @param occurredAt 观测时间,为空时取当前时间
+ */
public void evaluate(TTest test, String sourceType, String sourceIdentity, String metricCode, BigDecimal value, String unit, Date occurredAt) {
+ // STEP 1: 按阈值规则筛出被本次成功观测超限的规则,缺失测量不参与判定。
for (TTestWarningThreshold rule : exceededRules(test, metricCode, value)) {
+ // STEP 2: 以测试、指标和规则为聚合键原子累加开放异常,并保存触发时的规则快照。
mapper.upsertWarningException(test.getTestId(), rule.getThresholdId(), sourceType, sourceIdentity, metricCode, value, unit,
JSON.toJSONString(rule), rule.getSeverity(), occurredAt == null ? new Date() : occurredAt);
}
@@ -41,14 +68,47 @@
&& outside(value, rule.getMinValue(), rule.getMaxValue())).toList();
}
- /** 由测试负责人关闭开放异常,处理内容保存在异常事实中。 */
- @Transactional(rollbackFor = Exception.class)
- public int close(Long exceptionId, Long actorId, String actorName, String handlingNote) {
- TTestWarningException exception = mapper.selectWarningExceptionById(exceptionId);
- if (exception == null) return 0;
- int rows = mapper.closeWarningException(exceptionId, actorId, actorName, new Date(), handlingNote);
- return rows;
+ /**
+ * 按测试和异常状态查询已持久化的阈值与离线异常事实。
+ *
+ * @param testId 测试主键,为空表示不限测试
+ * @param status 异常状态:OPEN 开放、CLOSED 已关闭;为空表示不限状态
+ * @return 按最近触发时间倒序排列的异常事实
+ */
+ public List<TTestWarningException> list(Long testId, String status) {
+ return mapper.selectWarningExceptions(testId, status);
}
+
+ /**
+ * 测试负责人关闭开放异常:校验归属与处理结果后写入,返回关闭后的异常事实。
+ *
+ * <p>前置条件:操作人必须是该异常所属测试的测试负责人,且处理结果必填。
+ * 只有开放异常可以关闭;关闭只改变处理状态,原始异常事实与累计触发次数仍保留。</p>
+ *
+ * @param exceptionId 异常主键
+ * @param actorId 操作人用户 ID,同时作为处理人写入
+ * @param actorName 操作人显示名称,随异常事实保存
+ * @param handlingNote 处理结果,必填
+ * @return 关闭后的异常事实
+ * @throws ServiceException 处理结果为空、异常不存在、操作人不是测试负责人或异常已关闭
+ */
+ @Transactional(rollbackFor = Exception.class)
+ public TTestWarningException closeByPrincipal(Long exceptionId, Long actorId, String actorName, String handlingNote) {
+ // STEP 1: 处理结果为必填,先拒绝空提交。
+ String note = handlingNote == null ? null : handlingNote.trim();
+ if (note == null || note.isEmpty()) throw new ServiceException("处理结果不能为空");
+ // STEP 2: 异常与所属测试必须存在,且操作人就是该测试的测试负责人。
+ TTestWarningException exception = mapper.selectWarningExceptionById(exceptionId);
+ if (exception == null) throw new ServiceException("异常不存在");
+ TTest test = mapper.selectTestById(exception.getTestId());
+ if (test == null || actorId == null || !actorId.equals(test.getPrincipalId()))
+ throw new ServiceException("仅测试负责人可以关闭预警异常");
+ // STEP 3: 仅开放异常可关闭,行数不为 1 说明已被并发关闭。
+ if (mapper.closeWarningException(exceptionId, actorId, actorName, new Date(), note) != 1)
+ throw new ServiceException("异常已关闭或不存在");
+ return mapper.selectWarningExceptionById(exceptionId);
+ }
+
private static boolean outside(BigDecimal value, BigDecimal min, BigDecimal max) { return (min != null && value.compareTo(min) < 0) || (max != null && value.compareTo(max) > 0); }
static boolean matchesCollectorMetric(String configured, String actual) {
if (configured == null || actual == null) return false;
diff --git a/battery-business/src/main/java/com/battery/business/test/dto/TestBatteryBindingDto.java b/battery-business/src/main/java/com/battery/business/test/dto/TestBatteryBindingDto.java
index 651e416..c799500 100644
--- a/battery-business/src/main/java/com/battery/business/test/dto/TestBatteryBindingDto.java
+++ b/battery-business/src/main/java/com/battery/business/test/dto/TestBatteryBindingDto.java
@@ -16,7 +16,9 @@
CabinetChannelDto cabinetChannel, List<CollectorChannelDto> collectorChannels) {
this(testBatteryId, null, groupId, batteryNo, collectorId, cabinetChannel, null, collectorChannels);
}
+ /** 测试电池主键;兼容只传旧页面入参别名 relationId 的调用方。 */
public Long resolvedTestBatteryId() { return testBatteryId != null ? testBatteryId : relationId; }
+ /** 检测柜通道绑定;兼容只传旧页面入参别名 binding 的调用方。 */
public CabinetChannelDto resolvedCabinetChannel() { return cabinetChannel != null ? cabinetChannel : binding; }
public record CabinetChannelDto(Long channelId, String expectedBarcode) { }
public record CollectorChannelDto(String measurementCode, String measurementName, String channelCode) { }
diff --git a/battery-business/src/main/resources/mapper/battery/TDeviceIngestRecordMapper.xml b/battery-business/src/main/resources/mapper/battery/TDeviceIngestRecordMapper.xml
index c7eeca2..b33e572 100644
--- a/battery-business/src/main/resources/mapper/battery/TDeviceIngestRecordMapper.xml
+++ b/battery-business/src/main/resources/mapper/battery/TDeviceIngestRecordMapper.xml
@@ -30,6 +30,12 @@
<result property="receivedAtUtc" column="received_at_utc"/>
</resultMap>
+ <!-- 落地行的显式列名,顺序与建表脚本一致;列集合不随运行期表结构变化。 -->
+ <sql id="ingestColumns">ingest_id, adapter_code, payload_kind, devtype, devid, subdevid, chlid, record_sequence, record_key,
+ source_ref, device_time_utc, device_time_raw, timezone_id, cycle_no, step_no, relative_time_seconds,
+ voltage_v, current_a, charge_capacity_ah, discharge_capacity_ah, charge_energy_wh, discharge_energy_wh,
+ payload_json, content_sha256, received_at_utc</sql>
+
<!-- 驼峰映射未开启(见 mybatis-config.xml),列名带下划线的查询一律显式映射。 -->
<resultMap id="TDeviceIngestChannelResultMap" type="com.battery.business.domain.vo.TDeviceIngestChannel">
<result property="devtype" column="devtype"/>
@@ -91,7 +97,7 @@
</select>
<select id="selectLatestSample" resultMap="TDeviceIngestRecordResultMap">
- select * from t_device_ingest_record
+ select <include refid="ingestColumns"/> from t_device_ingest_record
where adapter_code = #{adapterCode} and payload_kind = 'SAMPLE'
and devtype = #{devtype} and devid = #{devid} and subdevid = #{subdevid} and chlid = #{chlid}
order by device_time_utc desc, record_sequence desc
@@ -100,7 +106,7 @@
<!-- 只取严格晚于水位的行;同秒多行按协议序号各自成立,不按秒合并。 -->
<select id="selectSamplesSince" resultMap="TDeviceIngestRecordResultMap">
- select * from t_device_ingest_record
+ select <include refid="ingestColumns"/> from t_device_ingest_record
where adapter_code = #{adapterCode} and payload_kind = 'SAMPLE'
and devtype = #{devtype} and devid = #{devid} and subdevid = #{subdevid} and chlid = #{chlid}
<if test="watermarkTime != null">
diff --git a/battery-business/src/main/resources/mapper/battery/TTestMapper.xml b/battery-business/src/main/resources/mapper/battery/TTestMapper.xml
index b9dc5dc..2daa454 100644
--- a/battery-business/src/main/resources/mapper/battery/TTestMapper.xml
+++ b/battery-business/src/main/resources/mapper/battery/TTestMapper.xml
@@ -657,6 +657,7 @@
'capture_slot_utc', #{captureSlotMillis})))
</select>
<sql id="dataId"><choose><when test="dataType == 'REALTIME'">record_id</when><when test="dataType == 'STEP'">step_record_id</when><otherwise>cycle_record_id</otherwise></choose></sql>
+ <!-- 三类数据表按 dataType 动态取表名,resultMap 映射全列:显式列名需随每次迁移同步,故保留 SELECT *。 -->
<select id="selectDataRecord" resultType="map">
select * from <include refid="dataTable"/> where test_id=#{testId} and <include refid="dataId"/>=#{recordId} for update
</select>
diff --git a/battery-device-api/src/main/java/com/battery/device/api/discovery/InMemoryDeviceDiscoveryCandidateStore.java b/battery-device-api/src/main/java/com/battery/device/api/discovery/InMemoryDeviceDiscoveryCandidateStore.java
index 37eea0f..2426a24 100644
--- a/battery-device-api/src/main/java/com/battery/device/api/discovery/InMemoryDeviceDiscoveryCandidateStore.java
+++ b/battery-device-api/src/main/java/com/battery/device/api/discovery/InMemoryDeviceDiscoveryCandidateStore.java
@@ -11,12 +11,14 @@
private final Map<String, CabinetCandidate> cabinets = new LinkedHashMap<>();
private final Map<String, CollectorCandidate> collectors = new LinkedHashMap<>();
+ /** 按设备编码 upsert 本轮检测柜观测;本轮未返回的候选保留到逻辑过期。 */
@Override
public synchronized void recordCabinets(List<CabinetCandidate> candidates)
{
candidates.forEach(item -> cabinets.put(item.deviceCode(), item));
}
+ /** 整体替换指定适配器的采集器候选,使该适配器下已不存在的采集器立即消失。 */
@Override
public synchronized void replaceCollectors(String adapterCode, List<CollectorCandidate> candidates)
{
@@ -24,6 +26,7 @@
candidates.forEach(item -> collectors.put(item.deviceCode(), item));
}
+ /** 删除指定适配器名下的检测柜与采集器候选;仅供人工重置或联调清理。 */
@Override
public synchronized void removeAdapter(String adapterCode)
{
@@ -31,6 +34,7 @@
collectors.values().removeIf(item -> item.adapterCode().equals(adapterCode));
}
+ /** 返回指定时刻尚未过期的候选,并清理已过期条目。 */
@Override
public synchronized CandidateSnapshot snapshot(Instant now)
{
diff --git a/battery-device-api/src/main/java/com/battery/device/api/model/AdapterCapabilities.java b/battery-device-api/src/main/java/com/battery/device/api/model/AdapterCapabilities.java
index 9752a71..515ace8 100644
--- a/battery-device-api/src/main/java/com/battery/device/api/model/AdapterCapabilities.java
+++ b/battery-device-api/src/main/java/com/battery/device/api/model/AdapterCapabilities.java
@@ -6,6 +6,7 @@
boolean historyDownload,
boolean control)
{
+ /** 只观测的能力组合:具备发现与实时观测,不提供历史下载与控制。 */
public static AdapterCapabilities observeOnly()
{
return new AdapterCapabilities(true, true, false, false);
diff --git a/battery-device-api/src/main/java/com/battery/device/api/model/ChannelIdentity.java b/battery-device-api/src/main/java/com/battery/device/api/model/ChannelIdentity.java
index 812bb8d..c9d5fd2 100644
--- a/battery-device-api/src/main/java/com/battery/device/api/model/ChannelIdentity.java
+++ b/battery-device-api/src/main/java/com/battery/device/api/model/ChannelIdentity.java
@@ -17,6 +17,7 @@
throw new IllegalArgumentException("BTS channel identity values must be non-negative");
}
+ /** 返回不含适配器实例的厂商通道键,由设备类型、设备号、单元号和通道号拼成。 */
public String vendorKey()
{
return devtype + "-" + devid + "-" + subdevid + "-" + chlid;
diff --git a/battery-device-api/src/main/java/com/battery/device/api/model/DetectedDeviceMetadata.java b/battery-device-api/src/main/java/com/battery/device/api/model/DetectedDeviceMetadata.java
index 8bec7a3..da7817f 100644
--- a/battery-device-api/src/main/java/com/battery/device/api/model/DetectedDeviceMetadata.java
+++ b/battery-device-api/src/main/java/com/battery/device/api/model/DetectedDeviceMetadata.java
@@ -3,6 +3,7 @@
/** 设备发现响应可选携带的展示元数据;空值表示协议未提供。 */
public record DetectedDeviceMetadata(String name, String manufacturer, String model, String macAddress)
{
+ /** 返回各字段均为空、表示协议未提供元数据的取值。 */
public static DetectedDeviceMetadata empty()
{
return new DetectedDeviceMetadata(null, null, null, null);
diff --git a/battery-device-api/src/main/java/com/battery/device/api/model/DeviceIdentity.java b/battery-device-api/src/main/java/com/battery/device/api/model/DeviceIdentity.java
index 538c790..f2e2b62 100644
--- a/battery-device-api/src/main/java/com/battery/device/api/model/DeviceIdentity.java
+++ b/battery-device-api/src/main/java/com/battery/device/api/model/DeviceIdentity.java
@@ -13,6 +13,7 @@
throw new IllegalArgumentException("device identity values must be non-negative");
}
+ /** 取通道身份中的设备部分,丢弃通道号。 */
public static DeviceIdentity from(ChannelIdentity channel)
{
Objects.requireNonNull(channel, "channel");
diff --git a/battery-device-api/src/main/java/com/battery/device/api/model/DeviceObservation.java b/battery-device-api/src/main/java/com/battery/device/api/model/DeviceObservation.java
index 749e1b9..1e8d540 100644
--- a/battery-device-api/src/main/java/com/battery/device/api/model/DeviceObservation.java
+++ b/battery-device-api/src/main/java/com/battery/device/api/model/DeviceObservation.java
@@ -78,6 +78,7 @@
reason == null ? ObservationFailureReason.OFFLINE : reason);
}
+ /** 按失败分类创建不携带过期设备事实的离线观测;通道工作状态一律为 UNOBSERVABLE。 */
public static DeviceObservation offline(ChannelIdentity channel, ObservationFailureReason failureReason)
{
Objects.requireNonNull(channel, "channel");
diff --git a/battery-device-api/src/main/java/com/battery/device/api/model/DeviceWorkStatus.java b/battery-device-api/src/main/java/com/battery/device/api/model/DeviceWorkStatus.java
index 615d83e..5c24b1c 100644
--- a/battery-device-api/src/main/java/com/battery/device/api/model/DeviceWorkStatus.java
+++ b/battery-device-api/src/main/java/com/battery/device/api/model/DeviceWorkStatus.java
@@ -2,22 +2,39 @@
import java.util.Locale;
+/**
+ * 设备侧观测到的通道工作状态,是设备事实,独立于设备在线状态和测试接收状态。
+ *
+ * <p>落库前会归并为 RUNNING、PAUSED、ABNORMAL、ENDED、UNOBSERVABLE 五种,见业务层的通道状态映射。</p>
+ */
public enum DeviceWorkStatus
{
+ /** 设备运行中。*/
WORKING,
+ /** 设备通道已结束。*/
FINISHED,
+ /** 设备通道已停止。*/
STOPPED,
+ /** 设备保护动作触发。*/
PROTECTED,
+ /** 设备通道暂停。*/
PAUSED,
+ /** 设备同步控制中。*/
SYNCHRONIZING,
+ /** 设备指示灯状态。*/
LIT,
+ /** 设备等待超时。*/
WAITING_TIMEOUT,
+ /** 设备等待开始。*/
WAITING_START,
+ /** 设备通道锁定。*/
LOCKED,
/** 设备离线或请求失败时不得沿用上一次通道状态。 */
UNOBSERVABLE,
+ /** 协议返回无法识别的状态值。 */
UNKNOWN;
+ /** 将厂商协议状态文本映射为通道工作状态;无法识别的取值和 null 归为 UNKNOWN。 */
public static DeviceWorkStatus fromBts(String value)
{
if (value == null) return UNKNOWN;
diff --git a/battery-device-api/src/main/java/com/battery/device/api/registry/DeviceAdapterRegistry.java b/battery-device-api/src/main/java/com/battery/device/api/registry/DeviceAdapterRegistry.java
index f2c51c5..e6fb6d3 100644
--- a/battery-device-api/src/main/java/com/battery/device/api/registry/DeviceAdapterRegistry.java
+++ b/battery-device-api/src/main/java/com/battery/device/api/registry/DeviceAdapterRegistry.java
@@ -119,11 +119,13 @@
return registered;
}
+ /** 返回指定编码的适配器实例当前是否已注册。 */
public synchronized boolean contains(String adapterCode)
{
return adapters.containsKey(adapterCode);
}
+ /** 关闭全部已注册实例并清空实例表;逐个等待在途操作结束。 */
@Override
public void close()
{
diff --git a/battery-device-bts/src/main/java/com/battery/device/bts/BtsDeviceAdapter.java b/battery-device-bts/src/main/java/com/battery/device/bts/BtsDeviceAdapter.java
index cb1ed00..f18583f 100644
--- a/battery-device-bts/src/main/java/com/battery/device/bts/BtsDeviceAdapter.java
+++ b/battery-device-bts/src/main/java/com/battery/device/bts/BtsDeviceAdapter.java
@@ -31,24 +31,28 @@
this.capabilities = capabilities;
}
+ /** 返回本实例登记的适配器编码。 */
@Override
public String adapterCode()
{
return adapterCode;
}
+ /** 返回本实例登记的能力标记;BTS 协议路线按只观测装配。 */
@Override
public AdapterCapabilities capabilities()
{
return capabilities;
}
+ /** 通过设备信息命令读取通道登记事实,作为发现候选返回。 */
@Override
public List<DetectedChannel> discoverChannels()
{
return codec.parseDeviceInfo(transport.exchange(codec.getDeviceInfoRequest()));
}
+ /** 按给定通道集合请求一次实时状态;通道为空时不发请求。 */
@Override
public Map<ChannelIdentity, RealtimeSnapshot> inquire(Set<ChannelIdentity> channels)
{
@@ -58,6 +62,7 @@
+ /** 关闭底层传输连接。 */
@Override
public void close()
{
diff --git a/battery-device-bts/src/main/java/com/battery/device/bts/BtsDeviceAdapterFactory.java b/battery-device-bts/src/main/java/com/battery/device/bts/BtsDeviceAdapterFactory.java
index 58de5d7..c5399ea 100644
--- a/battery-device-bts/src/main/java/com/battery/device/bts/BtsDeviceAdapterFactory.java
+++ b/battery-device-bts/src/main/java/com/battery/device/bts/BtsDeviceAdapterFactory.java
@@ -12,12 +12,14 @@
private static final Duration CONNECT_TIMEOUT = Duration.ofSeconds(3);
private static final Duration RESPONSE_TIMEOUT = Duration.ofSeconds(30);
+ /** 返回本工厂的适配器类型标识;注册表按它匹配实例配置。 */
@Override
public String adapterType()
{
return "BTS";
}
+ /** 按实例配置装配 TCP 传输与报文编解码器;连接与响应超时取本类的固定常量。 */
@Override
public DeviceAdapter create(AdapterInstanceConfig config)
{
diff --git a/battery-device-bts/src/main/java/com/battery/device/bts/BtsProtocolException.java b/battery-device-bts/src/main/java/com/battery/device/bts/BtsProtocolException.java
index 9311a74..0b0dc56 100644
--- a/battery-device-bts/src/main/java/com/battery/device/bts/BtsProtocolException.java
+++ b/battery-device-bts/src/main/java/com/battery/device/bts/BtsProtocolException.java
@@ -39,6 +39,7 @@
this.reason = reason;
}
+ /** 返回协议失败分类;构造时未指定分类的取 {@code INVALID_RESPONSE}。 */
public Reason reason()
{
return reason;
diff --git a/battery-device-bts/src/main/java/com/battery/device/bts/BtsTcpTransport.java b/battery-device-bts/src/main/java/com/battery/device/bts/BtsTcpTransport.java
index b0bbc22..8bf78fb 100644
--- a/battery-device-bts/src/main/java/com/battery/device/bts/BtsTcpTransport.java
+++ b/battery-device-bts/src/main/java/com/battery/device/bts/BtsTcpTransport.java
@@ -51,6 +51,11 @@
if (maxResponseBytes < 1) throw new IllegalArgumentException("maxResponseBytes must be positive");
}
+ /**
+ * 在长连接上串行完成一次「单请求、单响应」交换:请求后追加报文结束符 {@code #\r\n},
+ * 读到同一结束符即视为响应完整。并发调用由锁串行化;任何传输失败都作废当前连接,
+ * 并把失败分类为 {@link BtsTransportException} 抛出,下一次调用重新建连。
+ */
@Override
public String exchange(String request)
{
@@ -137,6 +142,7 @@
}
}
+ /** 关闭当前连接;与交换共用同一把锁,下一次交换按需重新建连。 */
@Override
public void close()
{
diff --git a/battery-device-bts/src/main/java/com/battery/device/bts/BtsTransportException.java b/battery-device-bts/src/main/java/com/battery/device/bts/BtsTransportException.java
index 0e56279..2146284 100644
--- a/battery-device-bts/src/main/java/com/battery/device/bts/BtsTransportException.java
+++ b/battery-device-bts/src/main/java/com/battery/device/bts/BtsTransportException.java
@@ -27,6 +27,7 @@
this.reason = reason;
}
+ /** 返回本次传输失败的分类。 */
public Reason reason()
{
return reason;
diff --git a/battery-device-bts/src/main/java/com/battery/device/bts/BtsXmlCodec.java b/battery-device-bts/src/main/java/com/battery/device/bts/BtsXmlCodec.java
index 65225b8..16c5b49 100644
--- a/battery-device-bts/src/main/java/com/battery/device/bts/BtsXmlCodec.java
+++ b/battery-device-bts/src/main/java/com/battery/device/bts/BtsXmlCodec.java
@@ -50,11 +50,13 @@
this.deviceZone = deviceZone;
}
+ /** 构造通道发现的请求报文({@code getdevinfo})。 */
public String getDeviceInfoRequest()
{
return envelope("<cmd>getdevinfo</cmd>");
}
+ /** 构造按通道批量查询实时快照的请求报文({@code inquire});只接受属于本实例的通道身份。 */
public String inquireRequest(Set<ChannelIdentity> channels, boolean includeBarcode)
{
StringBuilder list = new StringBuilder("<cmd>inquire</cmd><list count=\"").append(channels.size()).append("\">");
@@ -72,6 +74,7 @@
+ /** 解析 {@code getdevinfo_resp},按本实例的适配器编码生成检测柜通道清单。 */
public List<DetectedChannel> parseDeviceInfo(String xml)
{
Document document = parse(xml, "getdevinfo_resp");
@@ -92,6 +95,7 @@
return List.copyOf(channels);
}
+ /** 解析 {@code inquire_resp} 并按通道身份归集实时快照;{@code receivedAt} 记为服务器接收时间。 */
public Map<ChannelIdentity, RealtimeSnapshot> parseSnapshots(String xml, Instant receivedAt)
{
Document document = parse(xml, "inquire_resp");
diff --git a/battery-device-bts/src/test/java/com/battery/device/bts/ScriptedBtsTcpSimulator.java b/battery-device-bts/src/test/java/com/battery/device/bts/ScriptedBtsTcpSimulator.java
index 4683702..3376a76 100644
--- a/battery-device-bts/src/test/java/com/battery/device/bts/ScriptedBtsTcpSimulator.java
+++ b/battery-device-bts/src/test/java/com/battery/device/bts/ScriptedBtsTcpSimulator.java
@@ -13,9 +13,11 @@
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
/**
@@ -27,7 +29,9 @@
private final ServerSocket serverSocket;
private final int port;
- private final ExecutorService executor = Executors.newSingleThreadExecutor();
+ /** 连接服务线程显式创建并命名。该池只提交一个常驻服务任务,队列容量 1 只是声明不再接受更多任务。 */
+ private final ExecutorService executor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
+ new LinkedBlockingQueue<>(1), simulatorThreadFactory());
private final BlockingQueue<ScriptedAction> actions = new LinkedBlockingQueue<>();
private final List<String> requests = new CopyOnWriteArrayList<>();
private final Function<String, String> responseProvider;
@@ -145,6 +149,17 @@
executor.awaitTermination(2, TimeUnit.SECONDS);
}
+ /** 模拟器连接服务线程统一命名并设为守护线程,避免测试退出时残留。 */
+ private static ThreadFactory simulatorThreadFactory()
+ {
+ AtomicInteger sequence = new AtomicInteger();
+ return runnable -> {
+ Thread thread = new Thread(runnable, "bts-simulator-pool-" + sequence.incrementAndGet());
+ thread.setDaemon(true);
+ return thread;
+ };
+ }
+
private record ScriptedAction(Duration delay, byte[] payload, boolean disconnect)
{
}
diff --git a/battery-device-neware/src/main/java/com/battery/device/neware/NewareMysqlAdapter.java b/battery-device-neware/src/main/java/com/battery/device/neware/NewareMysqlAdapter.java
index 475afe0..d122581 100644
--- a/battery-device-neware/src/main/java/com/battery/device/neware/NewareMysqlAdapter.java
+++ b/battery-device-neware/src/main/java/com/battery/device/neware/NewareMysqlAdapter.java
@@ -114,12 +114,14 @@
source::invalidate, Clock.systemUTC(), SAMPLE_INTERVAL_MS);
}
+ /** 返回本实例登记的适配器编码。 */
@Override
public String adapterCode()
{
return config.adapterCode();
}
+ /** 返回实例配置登记的能力标记。 */
@Override
public AdapterCapabilities capabilities()
{
@@ -166,6 +168,7 @@
return Map.copyOf(result);
}
+ /** 取该通道最新测试的最新一行采样作为柜端实时观测;没有测试或没有采样行时返回空值。 */
@Override
public CabinetRealtime latestCabinet(ChannelIdentity channel)
{
diff --git a/battery-device-neware/src/main/java/com/battery/device/neware/NewareMysqlAdapterFactory.java b/battery-device-neware/src/main/java/com/battery/device/neware/NewareMysqlAdapterFactory.java
index 8526d37..6792927 100644
--- a/battery-device-neware/src/main/java/com/battery/device/neware/NewareMysqlAdapterFactory.java
+++ b/battery-device-neware/src/main/java/com/battery/device/neware/NewareMysqlAdapterFactory.java
@@ -31,12 +31,14 @@
/** 凭据文件相对进程工作目录定位,使开发(仓库根)与交付(应用目录)用同一个位置。 */
static final Path CREDENTIALS_FILE = LocalCredentials.DEFAULT_FILE;
+ /** 返回本形态的适配器类型标识 {@code NEWARE_MYSQL}。 */
@Override
public String adapterType()
{
return NewareMysqlAdapter.ADAPTER_TYPE;
}
+ /** 从实例配置取库名、只读账号与口令来源,装配单连接厂商数据源和只读适配器。 */
@Override
public DeviceAdapter create(AdapterInstanceConfig config)
{
diff --git a/battery-device-neware/src/main/java/com/battery/device/neware/NewareStepDirectionResolver.java b/battery-device-neware/src/main/java/com/battery/device/neware/NewareStepDirectionResolver.java
index 05ed408..6b14118 100644
--- a/battery-device-neware/src/main/java/com/battery/device/neware/NewareStepDirectionResolver.java
+++ b/battery-device-neware/src/main/java/com/battery/device/neware/NewareStepDirectionResolver.java
@@ -10,6 +10,7 @@
*/
public final class NewareStepDirectionResolver implements StepDirectionResolver
{
+ /** 只按已确认的协议取值给出方向;其余取值与空值一律保持未知。 */
@Override
public Direction resolve(String stepType)
{
diff --git a/battery-device-neware/src/main/java/com/battery/device/neware/ReadOnlyVendorDataSource.java b/battery-device-neware/src/main/java/com/battery/device/neware/ReadOnlyVendorDataSource.java
index 8562e73..f682c0d 100644
--- a/battery-device-neware/src/main/java/com/battery/device/neware/ReadOnlyVendorDataSource.java
+++ b/battery-device-neware/src/main/java/com/battery/device/neware/ReadOnlyVendorDataSource.java
@@ -25,6 +25,10 @@
*
* <p>防线是结构性的,不是数学上的:任何绕过本数据源另开连接的代码仍能写。
* 服务端只读账号(见现场对接清单第七节)是第一道防线,本层是第二道。
+ *
+ * <p>{@link DataSource} 接口中与只读边界无关的 JDK 样板方法(日志写出器、登录超时、
+ * 父日志器、{@code unwrap} / {@code isWrapperFor} / {@code toString})直接转发或拒绝驱动对象,
+ * 不承载业务语义,因此不单独加方法注释。
*/
public final class ReadOnlyVendorDataSource implements DataSource
{
@@ -47,12 +51,14 @@
return audit;
}
+ /** 取一条受只读约束的连接;语句白名单在返回的连接代理上生效。 */
@Override
public Connection getConnection() throws SQLException
{
return guard(delegate.getConnection());
}
+ /** 同上,按给定账号取连接;账号仍受只读白名单约束。 */
@Override
public Connection getConnection(String username, String password) throws SQLException
{
diff --git a/battery-device-neware/src/main/java/com/battery/device/neware/SingleConnectionVendorDataSource.java b/battery-device-neware/src/main/java/com/battery/device/neware/SingleConnectionVendorDataSource.java
index 9c03559..7613f32 100644
--- a/battery-device-neware/src/main/java/com/battery/device/neware/SingleConnectionVendorDataSource.java
+++ b/battery-device-neware/src/main/java/com/battery/device/neware/SingleConnectionVendorDataSource.java
@@ -15,6 +15,9 @@
*
* <p>连接复用一条并在查询失败时作废,避免厂商服务端重启后长期持有失效连接;
* 厂商库实测按固定 60 秒写库,取数频率远低于连接复用带来的收益门槛。
+ *
+ * <p>{@link DataSource} 接口中与连接获取无关的 JDK 样板方法(日志写出器、登录超时、父日志器、
+ * {@code unwrap} / {@code isWrapperFor})只转发驱动对象,不承载业务语义,因此不单独加方法注释。
*/
public final class SingleConnectionVendorDataSource implements DataSource
{
@@ -31,6 +34,7 @@
this.credentials.setProperty("password", password == null ? "" : password);
}
+ /** 返回复用的单条连接,必要时按登记凭据新建;厂商库只读,连接不做写事务。 */
@Override
public synchronized Connection getConnection() throws SQLException
{
@@ -41,6 +45,7 @@
return connection;
}
+ /** 按给定账号临时新建一条连接,不复用也不替换已缓存的那条。 */
@Override
public Connection getConnection(String username, String password) throws SQLException
{
diff --git a/battery-device-simulator/src/main/java/com/battery/device/simulator/BtsDevelopmentSimulator.java b/battery-device-simulator/src/main/java/com/battery/device/simulator/BtsDevelopmentSimulator.java
index 5fa4690..1dea589 100644
--- a/battery-device-simulator/src/main/java/com/battery/device/simulator/BtsDevelopmentSimulator.java
+++ b/battery-device-simulator/src/main/java/com/battery/device/simulator/BtsDevelopmentSimulator.java
@@ -68,12 +68,14 @@
private static final ZoneId DEVICE_ZONE = ZoneId.of("Asia/Hong_Kong");
private static final DateTimeFormatter DEVICE_TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
+ /** 返回固定的适配器实例代码,用于把模拟观测归属到 dev-sim 这一实例。 */
@Override
public String adapterCode()
{
return ADAPTER_CODE;
}
+ /** 返回只观测能力:支持发现与实时观测,不支持历史下载,也不支持向设备下发控制命令。 */
@Override
public AdapterCapabilities capabilities()
{
@@ -172,6 +174,7 @@
simulator = ScriptedBtsTcpSimulator.responding(port, this::respond);
}
+ /** 返回模拟器 TCP 端点实际监听的端口。 */
public int port()
{
return simulator.port();
@@ -890,6 +893,7 @@
throw new IllegalStateException("Invalid BTS simulator numbering or tombstone state in Redis");
}
+ /** 停止模拟器:标记为不再运行并释放 TCP 端点;释放失败时以运行时异常报告。 */
@Override
public synchronized void close()
{
diff --git a/battery-device-simulator/src/main/java/com/battery/device/simulator/InMemoryBtsSimulatorStateStore.java b/battery-device-simulator/src/main/java/com/battery/device/simulator/InMemoryBtsSimulatorStateStore.java
index 722b799..f7903f7 100644
--- a/battery-device-simulator/src/main/java/com/battery/device/simulator/InMemoryBtsSimulatorStateStore.java
+++ b/battery-device-simulator/src/main/java/com/battery/device/simulator/InMemoryBtsSimulatorStateStore.java
@@ -11,6 +11,7 @@
public InMemoryBtsSimulatorStateStore() { }
public InMemoryBtsSimulatorStateStore(SimulatorState state) { this.state = state; }
+ /** 返回已有状态;尚未保存过任何状态时以 defaults 初始化并复用同一实例。 */
@Override
public synchronized SimulatorState loadOrInitialize(SimulatorState defaults)
{
@@ -18,12 +19,16 @@
return state;
}
+ /** 保存状态引用,不做拷贝、校验或版本迁移。 */
@Override
public synchronized void save(SimulatorState state) { this.state = state; }
+ /** 返回最近请求列表;尚未保存过时为空列表。 */
@Override
public synchronized List<RecentRequest> loadRecentRequests() { return recentRequests; }
+ /** 保存最近请求的不可变副本,调用方后续修改原列表不影响这里。 */
@Override
public synchronized void saveRecentRequests(List<RecentRequest> requests) { recentRequests = List.copyOf(requests); }
+ /** 用 defaults 覆盖主状态并清空最近请求。 */
@Override
public synchronized void reset(SimulatorState defaults)
{
diff --git a/battery-device-simulator/src/main/java/com/battery/device/simulator/ScriptedBtsTcpSimulator.java b/battery-device-simulator/src/main/java/com/battery/device/simulator/ScriptedBtsTcpSimulator.java
index 2f7a75a..762ff2c 100644
--- a/battery-device-simulator/src/main/java/com/battery/device/simulator/ScriptedBtsTcpSimulator.java
+++ b/battery-device-simulator/src/main/java/com/battery/device/simulator/ScriptedBtsTcpSimulator.java
@@ -13,9 +13,11 @@
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
/**
@@ -27,7 +29,9 @@
private final ServerSocket serverSocket;
private final int port;
- private final ExecutorService executor = Executors.newSingleThreadExecutor();
+ /** 连接服务线程显式创建并命名。该池只提交一个常驻服务任务,队列容量 1 只是声明不再接受更多任务。 */
+ private final ExecutorService executor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
+ new LinkedBlockingQueue<>(1), simulatorThreadFactory());
private final BlockingQueue<ScriptedAction> actions = new LinkedBlockingQueue<>();
private final List<String> requests = new CopyOnWriteArrayList<>();
private final Function<String, String> responseProvider;
@@ -51,6 +55,7 @@
serverSocket.close();
}
+ /** 创建一启动就关闭监听、任何连接都会被拒绝的模拟器。 */
public static ScriptedBtsTcpSimulator refusingConnections() throws IOException
{
return new ScriptedBtsTcpSimulator(0, false, null);
@@ -64,32 +69,38 @@
return new ScriptedBtsTcpSimulator(port, true, responseProvider);
}
+ /** 返回该模拟器绑定的本机端口;构造时传入 0 时返回的是系统分配端口而非 0。 */
public int port()
{
return port;
}
+ /** 向脚本队列追加一条零延迟应答,按 UTF-8 编码,由下一次请求按顺序取用。 */
public void enqueue(String response)
{
actions.add(new ScriptedAction(Duration.ZERO, response.getBytes(StandardCharsets.UTF_8), false));
}
+ /** 追加一条延迟应答:连接服务线程收到请求后先等待 delay 再写出;delay 不得为负。 */
public void enqueueDelayed(Duration delay, String response)
{
if (delay.isNegative()) throw new IllegalArgumentException("delay must not be negative");
actions.add(new ScriptedAction(delay, response.getBytes(StandardCharsets.UTF_8), false));
}
+ /** 追加一条零延迟的原始字节应答:字节原样写出后追加帧结束符,不做字符编码转换。 */
public void enqueueRaw(byte[] response)
{
actions.add(new ScriptedAction(Duration.ZERO, response.clone(), false));
}
+ /** 追加一条断开动作:处理到该条时结束当前连接,且不写出任何应答。 */
public void enqueueDisconnect()
{
actions.add(new ScriptedAction(Duration.ZERO, null, true));
}
+ /** 返回已接收请求帧的只读快照;只有脚本模式(未配置 responseProvider)才记录。 */
public List<String> requests()
{
return List.copyOf(requests);
@@ -166,6 +177,7 @@
}
}
+ /** 关闭监听并终止连接服务线程,最多等待 2 秒,避免测试退出时残留线程。 */
@Override
public void close() throws Exception
{
@@ -174,6 +186,17 @@
executor.awaitTermination(2, TimeUnit.SECONDS);
}
+ /** 模拟器连接服务线程统一命名并设为守护线程,避免测试退出时残留。 */
+ private static ThreadFactory simulatorThreadFactory()
+ {
+ AtomicInteger sequence = new AtomicInteger();
+ return runnable -> {
+ Thread thread = new Thread(runnable, "bts-simulator-pool-" + sequence.incrementAndGet());
+ thread.setDaemon(true);
+ return thread;
+ };
+ }
+
private record ScriptedAction(Duration delay, byte[] payload, boolean disconnect)
{
}
diff --git a/ruoyi-admin/src/main/java/com/battery/business/runtime/BtsDevelopmentSimulatorWebConfig.java b/ruoyi-admin/src/main/java/com/battery/business/runtime/BtsDevelopmentSimulatorWebConfig.java
index 988e48d..092b525 100644
--- a/ruoyi-admin/src/main/java/com/battery/business/runtime/BtsDevelopmentSimulatorWebConfig.java
+++ b/ruoyi-admin/src/main/java/com/battery/business/runtime/BtsDevelopmentSimulatorWebConfig.java
@@ -12,6 +12,7 @@
@Configuration
public class BtsDevelopmentSimulatorWebConfig implements WebMvcConfigurer
{
+ /** 只为模拟器页面和控制 API 挂本机拦截器,其余路径不受影响。 */
@Override
public void addInterceptors(InterceptorRegistry registry)
{
diff --git a/ruoyi-admin/src/main/java/com/battery/business/runtime/DevelopmentSimulatorConfig.java b/ruoyi-admin/src/main/java/com/battery/business/runtime/DevelopmentSimulatorConfig.java
index 53c5461..d1aa1f0 100644
--- a/ruoyi-admin/src/main/java/com/battery/business/runtime/DevelopmentSimulatorConfig.java
+++ b/ruoyi-admin/src/main/java/com/battery/business/runtime/DevelopmentSimulatorConfig.java
@@ -10,6 +10,7 @@
@Configuration
@Profile("dev-simulator")
public class DevelopmentSimulatorConfig {
+ /** 按配置的监听端口装配开发模拟器;仅 dev-simulator 生效,容器关闭时释放其端口。 */
@Bean(destroyMethod = "close")
@Profile("dev-simulator")
public BtsDevelopmentSimulator btsDevelopmentSimulator(BtsSimulatorStateStore stateStore,
@@ -18,6 +19,11 @@
return new BtsDevelopmentSimulator(simulatorPort, stateStore);
}
+ /**
+ * 将固定开发适配器按代码装配注册进运行时实例表,不写 t_device_adapter。
+ *
+ * <p>其运行时能力只有发现与实时观测,控制能力为不支持,不向设备发送控制命令。</p>
+ */
@Bean
@Profile("dev-simulator")
public ApplicationRunner registerDevelopmentSimulatorAdapter(DeviceAdapterRegistry registry,
diff --git a/ruoyi-admin/src/main/java/com/battery/business/runtime/DeviceAdapterRuntimeConfig.java b/ruoyi-admin/src/main/java/com/battery/business/runtime/DeviceAdapterRuntimeConfig.java
index 5f42435..ffb819d 100644
--- a/ruoyi-admin/src/main/java/com/battery/business/runtime/DeviceAdapterRuntimeConfig.java
+++ b/ruoyi-admin/src/main/java/com/battery/business/runtime/DeviceAdapterRuntimeConfig.java
@@ -24,6 +24,7 @@
@EnableScheduling
public class DeviceAdapterRuntimeConfig
{
+ /** 装配持有全部可用工厂的适配器注册表;推式来源工厂使用业务落地表 Mapper,实例由配置或代码另行注册。 */
@Bean(destroyMethod = "close")
public DeviceAdapterRegistry deviceAdapterRegistry(TDeviceIngestRecordMapper ingestRecords)
{
diff --git a/ruoyi-admin/src/main/java/com/battery/business/runtime/FlywayMigrationConfig.java b/ruoyi-admin/src/main/java/com/battery/business/runtime/FlywayMigrationConfig.java
index a0555ae..8bca0cd 100644
--- a/ruoyi-admin/src/main/java/com/battery/business/runtime/FlywayMigrationConfig.java
+++ b/ruoyi-admin/src/main/java/com/battery/business/runtime/FlywayMigrationConfig.java
@@ -16,6 +16,7 @@
@Configuration
public class FlywayMigrationConfig
{
+ /** 只取主数据源的连接参数执行业务库迁移;厂商库只读,不迁移也不建表。 */
@Bean(name = "flywayMigrationInitializer", initMethod = "migrate")
public FlywayMigrationInitializer flywayMigrationInitializer(@Qualifier("masterDataSource") DataSource dataSource)
{
diff --git a/ruoyi-admin/src/main/java/com/battery/business/runtime/RedisBtsSimulatorStateStore.java b/ruoyi-admin/src/main/java/com/battery/business/runtime/RedisBtsSimulatorStateStore.java
index 16c34e8..61dba9d 100644
--- a/ruoyi-admin/src/main/java/com/battery/business/runtime/RedisBtsSimulatorStateStore.java
+++ b/ruoyi-admin/src/main/java/com/battery/business/runtime/RedisBtsSimulatorStateStore.java
@@ -32,6 +32,7 @@
this.redisTemplate = redisTemplate;
}
+ /** 读取模拟器主状态,不存在时原子写入默认拓扑;并发初始化时以 Redis 中先写入的那份为准。 */
@Override
public SimulatorState loadOrInitialize(SimulatorState defaultState)
{
@@ -44,12 +45,14 @@
return restoreState(stored);
}
+ /** 整体覆盖模拟器主状态;该键不设普通 TTL。 */
@Override
public void save(SimulatorState state)
{
redisTemplate.opsForValue().set(STATE_KEY, JSON.toJSONString(state));
}
+ /** 读取短期保留的最近请求;键不存在时返回空列表。 */
@Override
public List<RecentRequest> loadRecentRequests()
{
@@ -58,6 +61,7 @@
return List.copyOf(JSON.parseArray(stored.toString(), RecentRequest.class));
}
+ /** 覆盖保存最近请求,整键按小时级 TTL 自动过期。 */
@Override
public void saveRecentRequests(List<RecentRequest> requests)
{
@@ -65,6 +69,7 @@
RECENT_REQUESTS_TTL_HOURS, TimeUnit.HOURS);
}
+ /** 人工重置:删除最近请求并写回默认主状态。 */
@Override
public void reset(SimulatorState defaultState)
{
diff --git a/ruoyi-admin/src/main/java/com/battery/business/runtime/RedisDeviceDiscoveryCandidateStore.java b/ruoyi-admin/src/main/java/com/battery/business/runtime/RedisDeviceDiscoveryCandidateStore.java
index a21b4b2..d6b3231 100644
--- a/ruoyi-admin/src/main/java/com/battery/business/runtime/RedisDeviceDiscoveryCandidateStore.java
+++ b/ruoyi-admin/src/main/java/com/battery/business/runtime/RedisDeviceDiscoveryCandidateStore.java
@@ -28,6 +28,7 @@
this.redisTemplate = redisTemplate;
}
+ /** 按设备编码 upsert 本轮检测柜候选,本轮未返回的候选保留至逻辑过期。 */
@Override
public synchronized void recordCabinets(List<CabinetCandidate> candidates)
{
@@ -38,6 +39,7 @@
save(new StoredState(CURRENT_VERSION, List.copyOf(merged.values()), current.collectors()));
}
+ /** 替换指定设备适配器实例的全部采集器候选,其它实例的候选保持不变。 */
@Override
public synchronized void replaceCollectors(String adapterCode, List<CollectorCandidate> candidates)
{
@@ -49,6 +51,7 @@
save(new StoredState(CURRENT_VERSION, current.cabinets(), merged));
}
+ /** 删除指定设备适配器实例的检测柜与采集器候选;仅供人工重置或实机联调清理。 */
@Override
public synchronized void removeAdapter(String adapterCode)
{
@@ -60,6 +63,11 @@
save(new StoredState(CURRENT_VERSION, cabinets, collectors));
}
+ /**
+ * 返回指定时刻尚未过期的发现候选,并顺带清掉已逻辑过期的条目。
+ *
+ * <p>是否“在用”由操作员判断,适配器不自动判定。</p>
+ */
@Override
public synchronized CandidateSnapshot snapshot(Instant now)
{
diff --git a/ruoyi-admin/src/main/java/com/battery/business/runtime/TestDataReceptionScheduler.java b/ruoyi-admin/src/main/java/com/battery/business/runtime/TestDataReceptionScheduler.java
index f0766d2..3f7e7d5 100644
--- a/ruoyi-admin/src/main/java/com/battery/business/runtime/TestDataReceptionScheduler.java
+++ b/ruoyi-admin/src/main/java/com/battery/business/runtime/TestDataReceptionScheduler.java
@@ -17,6 +17,7 @@
public TestDataReceptionScheduler(ITestDataReceptionService reception) { this.reception = reception; }
+ /** 每秒触发一次数据接收,由业务服务保存成功观测;只推进请求和保存,不向设备发送控制命令。 */
@Scheduled(fixedDelay = 1000)
public void capture() { reception.captureReceivedData(); }
}
diff --git a/ruoyi-admin/src/main/java/com/battery/business/web/TBatteryGroupController.java b/ruoyi-admin/src/main/java/com/battery/business/web/TBatteryGroupController.java
index 73e35f3..2b8311f 100644
--- a/ruoyi-admin/src/main/java/com/battery/business/web/TBatteryGroupController.java
+++ b/ruoyi-admin/src/main/java/com/battery/business/web/TBatteryGroupController.java
@@ -29,24 +29,31 @@
private final ITBatteryGroupService batteryGroupService;
@Autowired public TBatteryGroupController(ITBatteryGroupService batteryGroupService) { this.batteryGroupService = batteryGroupService; }
+ /** 分页查询电池组列表。 */
@PreAuthorize("@ss.hasPermi('system:batteryGroup:list')") @GetMapping("/list")
public TableDataInfo list(TBatteryGroup group) { startPage(); List<TBatteryGroup> list = batteryGroupService.selectBatteryGroupList(group); return getDataTable(list); }
+ /** 返回可作为创建人的启用用户选项。 */
@PreAuthorize("@ss.hasPermi('system:batteryGroup:list')") @GetMapping("/creatorOptions")
public AjaxResult creatorOptions() { return success(batteryGroupService.selectBatteryGroupCreatorOptions()); }
+ /** 按电池组主键查询电池组及其当前字段值。 */
@PreAuthorize("@ss.hasPermi('system:batteryGroup:query')") @GetMapping("/{groupId}")
public AjaxResult getInfo(@PathVariable Long groupId) { return success(batteryGroupService.selectBatteryGroupById(groupId)); }
+ /** 新增电池组及电池号与字段值。 */
@PreAuthorize("@ss.hasPermi('system:batteryGroup:add')") @Log(title = "电池组管理", businessType = BusinessType.INSERT) @PostMapping
public AjaxResult add(@Validated @RequestBody BatteryGroupDto request) { TBatteryGroup group = toDomain(request); group.setCreateBy(getUsername()); return toAjax(batteryGroupService.insertBatteryGroup(group)); }
+ /** 修改电池组;字段值按提交内容整体替换为当前值。 */
@PreAuthorize("@ss.hasPermi('system:batteryGroup:edit')") @Log(title = "电池组管理", businessType = BusinessType.UPDATE) @PutMapping
public AjaxResult edit(@Validated @RequestBody BatteryGroupDto request) { TBatteryGroup group = toDomain(request); group.setUpdateBy(getUsername()); return toAjax(batteryGroupService.updateBatteryGroup(group)); }
+ /** 启用或停用电池组;已被测试引用的电池组不能删除,但可以停用。 */
@PreAuthorize("@ss.hasPermi('system:batteryGroup:changeStatus')") @Log(title = "电池组管理", businessType = BusinessType.UPDATE) @PutMapping("/changeStatus")
public AjaxResult changeStatus(@RequestBody TBatteryGroup group) { group.setUpdateBy(getUsername()); return toAjax(batteryGroupService.updateBatteryGroupStatus(group)); }
+ /** 删除电池组;电池号与字段关系一并清理,被测试引用的电池组由服务层拒绝。 */
@PreAuthorize("@ss.hasPermi('system:batteryGroup:remove')") @Log(title = "电池组管理", businessType = BusinessType.DELETE) @DeleteMapping("/{groupIds}")
public AjaxResult remove(@PathVariable Long[] groupIds) { return toAjax(batteryGroupService.deleteBatteryGroupByIds(groupIds, getUsername())); }
diff --git a/ruoyi-admin/src/main/java/com/battery/business/web/TTestController.java b/ruoyi-admin/src/main/java/com/battery/business/web/TTestController.java
index e57caa6..5a8cc59 100644
--- a/ruoyi-admin/src/main/java/com/battery/business/web/TTestController.java
+++ b/ruoyi-admin/src/main/java/com/battery/business/web/TTestController.java
@@ -48,6 +48,7 @@
@Autowired
private ITestDataQueryService testDataQueryService;
+ /** 创建或编辑测试时可选的来源选项。 */
@PreAuthorize("@ss.hasAnyPermi('system:test:add,system:test:edit')")
@GetMapping("/options")
public AjaxResult options()
@@ -55,6 +56,7 @@
return success(testService.selectConfigurationOptions());
}
+ /** 按测试状态、接收状态及配置条件分页查询测试列表。 */
@PreAuthorize("@ss.hasPermi('system:test:list')")
@GetMapping("/list")
public TableDataInfo list(TTest test)
@@ -64,6 +66,7 @@
return getDataTable(list);
}
+ /** 按测试主键查询测试配置及其业务状态。 */
@PreAuthorize("@ss.hasPermi('system:test:query')")
@GetMapping("/{testId}")
public AjaxResult getInfo(@PathVariable Long testId)
@@ -71,6 +74,7 @@
return success(testService.selectTestById(testId));
}
+ /** 创建待测试记录;创建人取当前登录用户,初始为未开始接收。 */
@PreAuthorize("@ss.hasPermi('system:test:add')")
@Log(title = "测试管理", businessType = BusinessType.INSERT)
@PostMapping
@@ -84,6 +88,7 @@
return success(testService.selectTestById(testId));
}
+ /** 修改尚未开始接收的测试配置;开始接收后由服务层拒绝。 */
@PreAuthorize("@ss.hasPermi('system:test:edit')")
@Log(title = "测试管理", businessType = BusinessType.UPDATE)
@PutMapping
@@ -96,6 +101,7 @@
return success(testService.selectTestById(test.getTestId()));
}
+ /** 开始接收动作;只改变系统数据归属,不向设备发送控制命令。 */
@PreAuthorize("@ss.hasPermi('system:test:start')")
@Log(title = "测试管理", businessType = BusinessType.UPDATE)
@PutMapping("/start")
@@ -220,6 +226,7 @@
public record ConsoleDataActionRequest(String dataType, String operationType, String scopeSummary,
String identitySummary, String changesSummary, String reason, List<DataRevisionItem> overrides) { }
+ /** 整体删除测试及其全部子数据;不可恢复,不写操作日志、修订或墓碑。 */
@PreAuthorize("@ss.hasPermi('system:test:remove')")
@DeleteMapping("/{testIds}")
public AjaxResult remove(@PathVariable Long[] testIds)
diff --git a/ruoyi-admin/src/main/java/com/battery/business/web/TTestDataRevisionController.java b/ruoyi-admin/src/main/java/com/battery/business/web/TTestDataRevisionController.java
index 076058d..3e9bb3b 100644
--- a/ruoyi-admin/src/main/java/com/battery/business/web/TTestDataRevisionController.java
+++ b/ruoyi-admin/src/main/java/com/battery/business/web/TTestDataRevisionController.java
@@ -21,6 +21,7 @@
@Autowired
private ITestConsoleDataService service;
+ /** 按测试、数据类型、操作类型和时间范围分页查询修订行为。 */
@PreAuthorize("@ss.hasPermi('system:dataRevision:list')")
@GetMapping("/list")
public TableDataInfo list(TTestDataRevisionAction query)
@@ -30,6 +31,7 @@
return getDataTable(rows);
}
+ /** 按修订主键返回一次用户行为的详情,含操作前完整内容。 */
@PreAuthorize("@ss.hasPermi('system:dataRevision:query')")
@GetMapping("/{actionId}")
public AjaxResult detail(@PathVariable Long actionId)
diff --git a/ruoyi-admin/src/main/java/com/battery/business/web/TTestWarningExceptionController.java b/ruoyi-admin/src/main/java/com/battery/business/web/TTestWarningExceptionController.java
index 54c4b98..4552fda 100644
--- a/ruoyi-admin/src/main/java/com/battery/business/web/TTestWarningExceptionController.java
+++ b/ruoyi-admin/src/main/java/com/battery/business/web/TTestWarningExceptionController.java
@@ -10,38 +10,28 @@
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.battery.business.domain.TTestWarningException;
-import com.battery.business.mapper.TTestMapper;
-import com.battery.business.service.ITTestService;
import com.battery.business.service.impl.TestWarningExceptionService;
-/** 测试预警异常查询与人工关闭接口。 */
+/** 测试预警异常查询与人工关闭接口;归属校验和关闭规则由服务层负责。 */
@RestController
@RequestMapping("/system/test-warning-exceptions")
public class TTestWarningExceptionController extends BaseController {
- @Autowired private TTestMapper mapper;
- @Autowired private ITTestService testService;
@Autowired private TestWarningExceptionService warningExceptionService;
/** 按测试和状态查询后端异常事实。 */
@PreAuthorize("@ss.hasPermi('system:exception:query')")
@GetMapping("/list")
public TableDataInfo list(@RequestParam(required=false) Long testId, @RequestParam(required=false) String status) {
startPage();
- List<TTestWarningException> rows = mapper.selectWarningExceptions(testId, status);
+ List<TTestWarningException> rows = warningExceptionService.list(testId, status);
return getDataTable(rows);
}
- /** 校验测试负责人身份并关闭异常,通过应用操作日志记录处理动作。 */
+ /** 关闭异常:处理结果必填且只有测试负责人可操作,通过应用操作日志记录处理动作。 */
@PreAuthorize("@ss.hasPermi('system:exception:handle')")
@Log(title = "测试预警异常", businessType = BusinessType.UPDATE)
@PutMapping("/{exceptionId}/close")
- public AjaxResult close(@PathVariable Long exceptionId, @RequestBody CloseRequest request) {
- if (request == null || request.handlingNote() == null || request.handlingNote().trim().isEmpty()) return error("处理结果不能为空");
- TTestWarningException exception = mapper.selectWarningExceptionById(exceptionId);
- if (exception == null) return error("异常不存在");
- var test = testService.selectTestById(exception.getTestId());
- if (test == null || !getUserId().equals(test.getPrincipalId()))
- return error("仅测试负责人可以关闭预警异常");
- int rows = warningExceptionService.close(exceptionId, getUserId(), getUsername(), request.handlingNote().trim());
- return rows == 1 ? success(mapper.selectWarningExceptionById(exceptionId)) : error("异常已关闭或不存在");
+ public AjaxResult close(@PathVariable Long exceptionId, @RequestBody(required=false) CloseRequest request) {
+ return success(warningExceptionService.closeByPrincipal(exceptionId, getUserId(), getUsername(),
+ request == null ? null : request.handlingNote()));
}
public record CloseRequest(String handlingNote) {}
}
diff --git a/ruoyi-admin/src/main/java/com/battery/business/web/TestLiveStream.java b/ruoyi-admin/src/main/java/com/battery/business/web/TestLiveStream.java
index 9420d2d..3e8dd81 100644
--- a/ruoyi-admin/src/main/java/com/battery/business/web/TestLiveStream.java
+++ b/ruoyi-admin/src/main/java/com/battery/business/web/TestLiveStream.java
@@ -3,6 +3,7 @@
import java.util.Map;
import java.util.concurrent.*;
import jakarta.annotation.PreDestroy;
+import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import com.battery.business.service.ITestLiveService;
@@ -11,8 +12,23 @@
@Component
public class TestLiveStream {
private final ITestLiveService live;
- private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
+ /**
+ * SSE 推送线程池显式创建并命名,便于按连接排查推送任务。
+ *
+ * <p>连接关闭后立即移除已取消的定点任务,避免取消任务在延迟队列里滞留到下一个周期。</p>
+ */
+ private final ScheduledThreadPoolExecutor scheduler = scheduler();
public TestLiveStream(ITestLiveService live) { this.live = live; }
+
+ private static ScheduledThreadPoolExecutor scheduler()
+ {
+ ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(2,
+ BasicThreadFactory.builder().namingPattern("sse-live-pool-%d").daemon(true).build());
+ executor.setRemoveOnCancelPolicy(true);
+ return executor;
+ }
+
+ /** 建立指定测试的 SSE 连接,先推一次当前状态,随后每 3 秒推一次;连接完成、超时或出错时取消该推送任务。 */
public SseEmitter stream(Long testId)
{
SseEmitter emitter = createEmitter();
diff --git a/ruoyi-admin/src/main/java/com/battery/business/web/TodoController.java b/ruoyi-admin/src/main/java/com/battery/business/web/TodoController.java
index 339f6b0..1dbd993 100644
--- a/ruoyi-admin/src/main/java/com/battery/business/web/TodoController.java
+++ b/ruoyi-admin/src/main/java/com/battery/business/web/TodoController.java
@@ -24,10 +24,10 @@
if (request == null || request.ownerId() == null) return error("负责人不能为空");
return success(service.create(request.content(), request.ownerId(), getUserId(), getUsername()));
}
- /** 仅负责人可完成自己的待办,已处理不能再次修改。 */
+ /** 仅负责人可处理自己的待办,已处理不能再次修改。 */
@PutMapping("/{id}/handle") @PreAuthorize("@ss.hasPermi('todo:list:handle')")
- public AjaxResult handle(@PathVariable long id, @RequestBody HandleRequest request) {
- service.handle(id, getUserId(), request == null ? null : request.remark()); return success();
+ public AjaxResult complete(@PathVariable long id, @RequestBody(required = false) HandleRequest request) {
+ service.completeTodo(id, getUserId(), request == null ? null : request.remark()); return success();
}
/** 删除只作用于当前负责人自己的记录。 */
@DeleteMapping("/{id}") @PreAuthorize("@ss.hasPermi('todo:list:remove')")
diff --git a/ruoyi-admin/src/test/java/com/battery/business/service/TodoServiceTest.java b/ruoyi-admin/src/test/java/com/battery/business/service/TodoServiceTest.java
index 594f717..b17339f 100644
--- a/ruoyi-admin/src/test/java/com/battery/business/service/TodoServiceTest.java
+++ b/ruoyi-admin/src/test/java/com/battery/business/service/TodoServiceTest.java
@@ -10,7 +10,7 @@
class TodoServiceTest {
@Test
- void onlyOwnerCanReadHandleAndRemoveEvenWhenNamesMatch() throws Exception {
+ void onlyOwnerCanReadCompleteAndRemoveEvenWhenNamesMatch() throws Exception {
var database = new JdbcDataSource();
database.setURL("jdbc:h2:mem:todo_" + UUID.randomUUID() + ";MODE=MySQL;DB_CLOSE_DELAY=-1");
var jdbc = new JdbcTemplate(database);
@@ -25,14 +25,14 @@
assertEquals(id, service.list(2).get(0).id());
assertEquals(2, service.owners().size());
assertThrows(ServiceException.class, () -> service.create("事项", 3, 1, "创建人"));
- assertThrows(ServiceException.class, () -> service.handle(id, 1, "管理员或创建人不可越权"));
+ assertThrows(ServiceException.class, () -> service.completeTodo(id, 1, "管理员或创建人不可越权"));
assertThrows(ServiceException.class, () -> service.remove(id, 1));
- assertThrows(ServiceException.class, () -> service.handle(id, 2, " "));
- service.handle(id, 2, "已完成");
+ assertThrows(ServiceException.class, () -> service.completeTodo(id, 2, " "));
+ service.completeTodo(id, 2, "已完成");
var reopened = new TodoService(database).list(2).get(0);
assertEquals("已处理", reopened.status());
assertEquals("已完成", reopened.handleRemark());
- assertThrows(ServiceException.class, () -> service.handle(id, 2, "再次处理"));
+ assertThrows(ServiceException.class, () -> service.completeTodo(id, 2, "再次处理"));
service.remove(id, 2);
assertTrue(service.list(2).isEmpty());
}
diff --git a/ruoyi-admin/src/test/java/com/battery/business/service/impl/TestWarningExceptionServiceTest.java b/ruoyi-admin/src/test/java/com/battery/business/service/impl/TestWarningExceptionServiceTest.java
index 9291f0d..6760c0c 100644
--- a/ruoyi-admin/src/test/java/com/battery/business/service/impl/TestWarningExceptionServiceTest.java
+++ b/ruoyi-admin/src/test/java/com/battery/business/service/impl/TestWarningExceptionServiceTest.java
@@ -3,11 +3,13 @@
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import org.junit.jupiter.api.Test;
+import com.ruoyi.common.exception.ServiceException;
import com.battery.business.domain.TTest;
import com.battery.business.domain.TTestWarningException;
import com.battery.business.domain.TTestWarningThreshold;
@@ -68,18 +70,24 @@
}
@Test
- void closesOpenExceptionAndAppendsAuditInServiceOperation() {
+ void closesOpenExceptionOnlyForTestPrincipalWithHandlingNote() {
TTestMapper mapper = mock(TTestMapper.class);
TestWarningExceptionService service = new TestWarningExceptionService(mapper);
TTestWarningException exception = new TTestWarningException();
exception.setExceptionId(7L);
exception.setTestId(1L);
+ TTest test = new TTest();
+ test.setTestId(1L);
+ test.setPrincipalId(22L);
when(mapper.selectWarningExceptionById(7L)).thenReturn(exception);
+ when(mapper.selectTestById(1L)).thenReturn(test);
when(mapper.closeWarningException(eq(7L), eq(22L), eq("负责人"), any(Date.class), eq("已核实"))).thenReturn(1);
- int rows = service.close(7L, 22L, "负责人", "已核实");
+ assertEquals(exception, service.closeByPrincipal(7L, 22L, "负责人", "已核实"));
- assertEquals(1, rows);
+ // 归属校验与处理结果必填都由服务层负责,Controller 不再判断业务规则。
+ assertThrows(ServiceException.class, () -> service.closeByPrincipal(7L, 23L, "其他人", "已核实"));
+ assertThrows(ServiceException.class, () -> service.closeByPrincipal(7L, 22L, "负责人", " "));
}
private static TTest testWithThreshold(String metric, BigDecimal min, BigDecimal max) {
--
Gitblit v1.7.1