From c2dc2817c4d6ba533b0fa0c746880d3727de3115 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期一, 31 八月 2026 10:36:53 +0800
Subject: [PATCH] 实现检测结果历史查询闭环

---
 ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TTestServiceImpl.java                 |    8 +
 ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/TTestController.java                 |   10 ++
 ruoyi-system/src/main/java/com/ruoyi/system/domain/TTestResultQuery.java                       |   38 +++++++
 ruoyi-system/src/main/java/com/ruoyi/system/domain/TTestResultRecord.java                      |   99 +++++++++++++++++++
 ruoyi-system/src/main/resources/mapper/system/TTestMapper.xml                                  |   83 ++++++++++++++++
 ruoyi-admin/src/test/java/com/ruoyi/web/controller/system/Ticket15ResultQueryContractTest.java |   32 ++++++
 ruoyi-system/src/main/java/com/ruoyi/system/service/ITTestService.java                         |    5 +
 ruoyi-system/src/main/java/com/ruoyi/system/mapper/TTestMapper.java                            |    3 
 8 files changed, 278 insertions(+), 0 deletions(-)

diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/TTestController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/TTestController.java
index 0387c7b..6075c77 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/TTestController.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/TTestController.java
@@ -20,6 +20,7 @@
 import com.ruoyi.common.enums.BusinessType;
 import com.ruoyi.system.domain.TTest;
 import com.ruoyi.system.domain.TTestResult;
+import com.ruoyi.system.domain.TTestResultQuery;
 import com.ruoyi.system.service.ITTestService;
 
 /** 测试管理 Controller。 */
@@ -151,4 +152,13 @@
         startPage();
         return getDataTable(testService.selectTestResults(result));
     }
+
+    /** 统一历史结果查询;仅返回检测完成测试的区间原始采样。 */
+    @PreAuthorize("@ss.hasPermi('system:test:query')")
+    @GetMapping("/results")
+    public TableDataInfo historicalResults(TTestResultQuery query)
+    {
+        startPage();
+        return getDataTable(testService.selectHistoricalResultRecords(query));
+    }
 }
diff --git a/ruoyi-admin/src/test/java/com/ruoyi/web/controller/system/Ticket15ResultQueryContractTest.java b/ruoyi-admin/src/test/java/com/ruoyi/web/controller/system/Ticket15ResultQueryContractTest.java
new file mode 100644
index 0000000..596c8dd
--- /dev/null
+++ b/ruoyi-admin/src/test/java/com/ruoyi/web/controller/system/Ticket15ResultQueryContractTest.java
@@ -0,0 +1,32 @@
+package com.ruoyi.web.controller.system;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.junit.jupiter.api.Test;
+
+/** Ticket15:结果查询必须从区间历史表读取,并排除实时和未完成测试。 */
+class Ticket15ResultQueryContractTest
+{
+    @Test
+    void historicalResultQueryUsesCompletedIntervalHistory()
+    {
+        Path mapper = Path.of("../ruoyi-system/src/main/resources/mapper/system/TTestMapper.xml");
+        String xml;
+        try
+        {
+            xml = Files.readString(mapper, StandardCharsets.UTF_8);
+        }
+        catch (Exception exception)
+        {
+            throw new AssertionError(exception);
+        }
+        assertTrue(xml.contains("id=\"selectHistoricalResultRecords\""));
+        assertTrue(xml.contains("t_test_sample s"));
+        assertTrue(xml.contains("t_test_collector_sample cs"));
+        assertTrue(xml.contains("t.test_status = 'COMPLETED'"));
+        assertTrue(xml.contains("i.interval_status = 'CLOSED'"));
+        assertTrue(xml.contains("union all"));
+    }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/TTestResultQuery.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/TTestResultQuery.java
new file mode 100644
index 0000000..2d01467
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/TTestResultQuery.java
@@ -0,0 +1,38 @@
+package com.ruoyi.system.domain;
+
+import java.util.Date;
+
+/** 结果查询条件;仅用于历史结果筛选,不承载实时快照。 */
+public class TTestResultQuery
+{
+    private Long testId;
+    private String testName;
+    private String batteryNo;
+    private String sourceType;
+    private Long sourceId;
+    private String sourceChannelCode;
+    private Date beginDeviceTime;
+    private Date endDeviceTime;
+    private Integer cycleNo;
+    private Integer stepNo;
+    public Long getTestId() { return testId; }
+    public void setTestId(Long value) { testId = value; }
+    public String getTestName() { return testName; }
+    public void setTestName(String value) { testName = value; }
+    public String getBatteryNo() { return batteryNo; }
+    public void setBatteryNo(String value) { batteryNo = value; }
+    public String getSourceType() { return sourceType; }
+    public void setSourceType(String value) { sourceType = value; }
+    public Long getSourceId() { return sourceId; }
+    public void setSourceId(Long value) { sourceId = value; }
+    public String getSourceChannelCode() { return sourceChannelCode; }
+    public void setSourceChannelCode(String value) { sourceChannelCode = value; }
+    public Date getBeginDeviceTime() { return beginDeviceTime; }
+    public void setBeginDeviceTime(Date value) { beginDeviceTime = value; }
+    public Date getEndDeviceTime() { return endDeviceTime; }
+    public void setEndDeviceTime(Date value) { endDeviceTime = value; }
+    public Integer getCycleNo() { return cycleNo; }
+    public void setCycleNo(Integer value) { cycleNo = value; }
+    public Integer getStepNo() { return stepNo; }
+    public void setStepNo(Integer value) { stepNo = value; }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/TTestResultRecord.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/TTestResultRecord.java
new file mode 100644
index 0000000..d3f5718
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/TTestResultRecord.java
@@ -0,0 +1,99 @@
+package com.ruoyi.system.domain;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.Date;
+
+/** 结果查询统一记录;检测柜核心采样和采集器扩展测量使用同一分页契约。 */
+public class TTestResultRecord implements Serializable
+{
+    private static final long serialVersionUID = 1L;
+    private Long resultId;
+    private String sourceType;
+    private Long testId;
+    private String testName;
+    private String testStatus;
+    private String receptionStatus;
+    private Long relationId;
+    private String batteryNo;
+    private Long sourceId;
+    private String sourceIdentity;
+    private Long intervalId;
+    private Date intervalStartAtUtc;
+    private Date intervalEndAtUtc;
+    private Integer cycleNo;
+    private Integer stepNo;
+    private String stepType;
+    private String workStatus;
+    private BigDecimal voltageV;
+    private BigDecimal currentA;
+    private BigDecimal capacityAh;
+    private BigDecimal energyWh;
+    private String metricCode;
+    private BigDecimal metricValue;
+    private String unit;
+    private String unitMetadata;
+    private String integrityConclusion;
+    private Date deviceTimeUtc;
+    private String deviceTimeRaw;
+    private Date serverReceivedAtUtc;
+
+    public Long getResultId() { return resultId; }
+    public void setResultId(Long value) { resultId = value; }
+    public String getSourceType() { return sourceType; }
+    public void setSourceType(String value) { sourceType = value; }
+    public Long getTestId() { return testId; }
+    public void setTestId(Long value) { testId = value; }
+    public String getTestName() { return testName; }
+    public void setTestName(String value) { testName = value; }
+    public String getTestStatus() { return testStatus; }
+    public void setTestStatus(String value) { testStatus = value; }
+    public String getReceptionStatus() { return receptionStatus; }
+    public void setReceptionStatus(String value) { receptionStatus = value; }
+    public Long getRelationId() { return relationId; }
+    public void setRelationId(Long value) { relationId = value; }
+    public String getBatteryNo() { return batteryNo; }
+    public void setBatteryNo(String value) { batteryNo = value; }
+    public Long getSourceId() { return sourceId; }
+    public void setSourceId(Long value) { sourceId = value; }
+    public String getSourceIdentity() { return sourceIdentity; }
+    public void setSourceIdentity(String value) { sourceIdentity = value; }
+    public Long getIntervalId() { return intervalId; }
+    public void setIntervalId(Long value) { intervalId = value; }
+    public Date getIntervalStartAtUtc() { return intervalStartAtUtc; }
+    public void setIntervalStartAtUtc(Date value) { intervalStartAtUtc = value; }
+    public Date getIntervalEndAtUtc() { return intervalEndAtUtc; }
+    public void setIntervalEndAtUtc(Date value) { intervalEndAtUtc = value; }
+    public Integer getCycleNo() { return cycleNo; }
+    public void setCycleNo(Integer value) { cycleNo = value; }
+    public Integer getStepNo() { return stepNo; }
+    public void setStepNo(Integer value) { stepNo = value; }
+    public String getStepType() { return stepType; }
+    public void setStepType(String value) { stepType = value; }
+    public String getWorkStatus() { return workStatus; }
+    public void setWorkStatus(String value) { workStatus = value; }
+    public BigDecimal getVoltageV() { return voltageV; }
+    public void setVoltageV(BigDecimal value) { voltageV = value; }
+    public BigDecimal getCurrentA() { return currentA; }
+    public void setCurrentA(BigDecimal value) { currentA = value; }
+    public BigDecimal getCapacityAh() { return capacityAh; }
+    public void setCapacityAh(BigDecimal value) { capacityAh = value; }
+    public BigDecimal getEnergyWh() { return energyWh; }
+    public void setEnergyWh(BigDecimal value) { energyWh = value; }
+    public String getMetricCode() { return metricCode; }
+    public void setMetricCode(String value) { metricCode = value; }
+    public BigDecimal getMetricValue() { return metricValue; }
+    public void setMetricValue(BigDecimal value) { metricValue = value; }
+    public String getUnit() { return unit; }
+    public void setUnit(String value) { unit = value; }
+    public String getUnitMetadata() { return unitMetadata; }
+    public void setUnitMetadata(String value) { unitMetadata = value; }
+    public String getIntegrityConclusion() { return integrityConclusion; }
+    public void setIntegrityConclusion(String value) { integrityConclusion = value; }
+    public Date getDeviceTimeUtc() { return deviceTimeUtc; }
+    public void setDeviceTimeUtc(Date value) { deviceTimeUtc = value; }
+    public String getDeviceTimeRaw() { return deviceTimeRaw; }
+    public void setDeviceTimeRaw(String value) { deviceTimeRaw = value; }
+    public Date getServerReceivedAtUtc() { return serverReceivedAtUtc; }
+    public void setServerReceivedAtUtc(Date value) { serverReceivedAtUtc = value; }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/TTestMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/TTestMapper.java
index b567608..d03a568 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/TTestMapper.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/TTestMapper.java
@@ -10,6 +10,8 @@
 import com.ruoyi.system.domain.TTestCollectorChannel;
 import com.ruoyi.system.domain.TTestPlanStep;
 import com.ruoyi.system.domain.TTestResult;
+import com.ruoyi.system.domain.TTestResultQuery;
+import com.ruoyi.system.domain.TTestResultRecord;
 import com.ruoyi.system.domain.TTestWarningThreshold;
 import com.ruoyi.system.domain.TTestCollectorSource;
 import com.ruoyi.system.domain.TTestCollectorExceptionInterval;
@@ -55,6 +57,7 @@
     /** 按来源和起始边界顺序返回测试接收区间。 */
     List<TTestReceptionInterval> selectTestReceptionIntervals(Long testId);
     List<TTestResult> selectTestResults(TTestResult result);
+    List<TTestResultRecord> selectHistoricalResultRecords(TTestResultQuery query);
     int countTestTypeEnabled(Long testTypeId);
     int countUserEnabled(Long userId);
     int countBatteryInEnabledGroup(@Param("groupId") Long groupId, @Param("batteryNo") String batteryNo);
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ITTestService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ITTestService.java
index fa24351..7dda6e6 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/ITTestService.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ITTestService.java
@@ -3,6 +3,8 @@
 import java.util.List;
 import com.ruoyi.system.domain.TTest;
 import com.ruoyi.system.domain.TTestResult;
+import com.ruoyi.system.domain.TTestResultQuery;
+import com.ruoyi.system.domain.TTestResultRecord;
 import com.ruoyi.system.domain.vo.TestConfigurationOptionsVo;
 
 /** 测试配置业务接口。 */
@@ -43,4 +45,7 @@
 
     /** 查询指定测试已持久化的结果记录。 */
     List<TTestResult> selectTestResults(TTestResult result);
+
+    /** 查询检测完成测试的区间历史结果,实时快照和暂停数据不会返回。 */
+    List<TTestResultRecord> selectHistoricalResultRecords(TTestResultQuery query);
 }
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TTestServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TTestServiceImpl.java
index 3bfb6fd..037afc0 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TTestServiceImpl.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TTestServiceImpl.java
@@ -23,6 +23,8 @@
 import com.ruoyi.system.domain.TTestCollectorChannel;
 import com.ruoyi.system.domain.TTestPlanStep;
 import com.ruoyi.system.domain.TTestResult;
+import com.ruoyi.system.domain.TTestResultQuery;
+import com.ruoyi.system.domain.TTestResultRecord;
 import com.ruoyi.system.domain.TTestFieldSnapshot;
 import com.ruoyi.system.domain.TTestWarningThreshold;
 import com.ruoyi.system.domain.TDetectionChannel;
@@ -333,6 +335,12 @@
         return testMapper.selectTestResults(result);
     }
 
+    @Override
+    public List<TTestResultRecord> selectHistoricalResultRecords(TTestResultQuery query)
+    {
+        return testMapper.selectHistoricalResultRecords(query == null ? new TTestResultQuery() : query);
+    }
+
     private void loadConfiguration(TTest test)
     {
         Long testId = test.getTestId();
diff --git a/ruoyi-system/src/main/resources/mapper/system/TTestMapper.xml b/ruoyi-system/src/main/resources/mapper/system/TTestMapper.xml
index 70a5d79..60ba5bc 100644
--- a/ruoyi-system/src/main/resources/mapper/system/TTestMapper.xml
+++ b/ruoyi-system/src/main/resources/mapper/system/TTestMapper.xml
@@ -97,6 +97,8 @@
         <result property="minValue" column="min_value" />
         <result property="maxValue" column="max_value" />
         <result property="unit" column="unit" />
+        <result property="unitMetadata" column="unit_metadata" />
+        <result property="integrityConclusion" column="integrity_conclusion" />
         <result property="severity" column="severity" />
         <result property="delFlag" column="del_flag" />
     </resultMap>
@@ -110,6 +112,35 @@
         <result property="resultTime" column="result_time" />
         <result property="exceptionInfo" column="exception_info" />
         <result property="delFlag" column="del_flag" />
+    </resultMap>
+    <resultMap id="TTestResultRecordMap" type="TTestResultRecord">
+        <id property="resultId" column="result_id" />
+        <result property="sourceType" column="source_type" />
+        <result property="testId" column="test_id" />
+        <result property="testName" column="test_name" />
+        <result property="testStatus" column="test_status" />
+        <result property="receptionStatus" column="reception_status" />
+        <result property="relationId" column="relation_id" />
+        <result property="batteryNo" column="battery_no" />
+        <result property="sourceId" column="source_id" />
+        <result property="sourceIdentity" column="source_identity" />
+        <result property="intervalId" column="interval_id" />
+        <result property="intervalStartAtUtc" column="interval_start_at_utc" />
+        <result property="intervalEndAtUtc" column="interval_end_at_utc" />
+        <result property="cycleNo" column="cycle_no" />
+        <result property="stepNo" column="step_no" />
+        <result property="stepType" column="step_type" />
+        <result property="workStatus" column="work_status" />
+        <result property="voltageV" column="voltage_v" />
+        <result property="currentA" column="current_a" />
+        <result property="capacityAh" column="capacity_ah" />
+        <result property="energyWh" column="energy_wh" />
+        <result property="metricCode" column="metric_code" />
+        <result property="metricValue" column="metric_value" />
+        <result property="unit" column="unit" />
+        <result property="deviceTimeUtc" column="device_time_utc" />
+        <result property="deviceTimeRaw" column="device_time_raw" />
+        <result property="serverReceivedAtUtc" column="server_received_at_utc" />
     </resultMap>
     <resultMap id="TTestCollectorExceptionIntervalResultMap" type="TTestCollectorExceptionInterval">
         <id property="intervalId" column="interval_id" />
@@ -351,6 +382,58 @@
         order by result.result_time, result.result_id
     </select>
 
+    <select id="selectHistoricalResultRecords" parameterType="TTestResultQuery" resultMap="TTestResultRecordMap">
+        select * from (
+            select s.sample_id result_id, 'CABINET_CHANNEL' source_type, t.test_id, t.test_name,
+                   t.test_status, t.reception_status, b.relation_id, b.battery_no,
+                   s.channel_id source_id,
+                   concat('CABINET:', coalesce(cab.adapter_code, ''), ':', coalesce(cab.devtype, ''), ':',
+                          coalesce(cab.devid, ''), ':', coalesce(cab.subdevid, ''), ':', coalesce(cab.chlid, '')) source_identity,
+                   s.reception_interval_id interval_id, i.start_boundary_at_utc interval_start_at_utc,
+                   i.end_boundary_at_utc interval_end_at_utc, s.cycle_no, s.step_no, s.step_type,
+                   s.step_type work_status, s.voltage_v, s.current_a, s.capacity_ah, s.energy_wh,
+                   null metric_code, null metric_value, 'V/A/Ah/Wh' unit, 'voltage=V,current=A,capacity=Ah,energy=Wh' unit_metadata,
+                   s.device_time_utc, s.device_time_raw, s.received_at_utc server_received_at_utc,
+                   case when exists (select 1 from t_test_integrity_issue x where x.test_id = t.test_id and x.issue_status = 'OPEN') then '完成确认中' else '完整性已通过' end integrity_conclusion
+            from t_test_sample s
+            join t_test t on t.test_id = s.test_id and t.test_status = 'COMPLETED' and t.del_flag = 0
+            join t_test_channel_binding tb on tb.binding_id = s.binding_id
+            join t_test_battery b on b.relation_id = tb.battery_relation_id and b.del_flag = 0
+            join t_test_reception_interval i on i.interval_id = s.reception_interval_id and i.interval_status = 'CLOSED'
+            join t_detection_channel cab on cab.channel_id = s.channel_id
+            union all
+            select cs.sample_id result_id, 'COLLECTOR_CHANNEL' source_type, t.test_id, t.test_name,
+                   t.test_status, t.reception_status, b.relation_id, b.battery_no,
+                   cs.collector_id source_id,
+                   concat('COLLECTOR:', coalesce(col.collector_code, cs.collector_id), ':', cs.collector_channel_code) source_identity,
+                   cs.reception_interval_id interval_id, i.start_boundary_at_utc interval_start_at_utc,
+                   i.end_boundary_at_utc interval_end_at_utc, null cycle_no, null step_no, null step_type,
+                   null work_status, null voltage_v, null current_a, null capacity_ah, null energy_wh,
+                   cs.metric_code, cs.metric_value, cs.unit, concat('metric=', cs.metric_code, ',unit=', cs.unit) unit_metadata,
+                   cs.device_time_utc, cs.device_time_raw, cs.server_received_at_utc,
+                   case when exists (select 1 from t_test_integrity_issue x where x.test_id = t.test_id and x.issue_status = 'OPEN') then '完成确认中' else '完整性已通过' end integrity_conclusion
+            from t_test_collector_sample cs
+            join t_test t on t.test_id = cs.test_id and t.test_status = 'COMPLETED' and t.del_flag = 0
+            join t_test_channel_binding tb on tb.binding_id = cs.binding_id
+            join t_test_battery b on b.relation_id = tb.battery_relation_id and b.del_flag = 0
+            join t_collector col on col.collector_id = cs.collector_id
+            join t_test_reception_interval i on i.interval_id = cs.reception_interval_id and i.interval_status = 'CLOSED'
+        ) result
+        <where>
+            <if test="testId != null">and result.test_id = #{testId}</if>
+            <if test="testName != null and testName != ''">and result.test_name like concat('%', #{testName}, '%')</if>
+            <if test="batteryNo != null and batteryNo != ''">and result.battery_no like concat('%', #{batteryNo}, '%')</if>
+            <if test="sourceType != null and sourceType != ''">and result.source_type = #{sourceType}</if>
+            <if test="sourceId != null">and result.source_id = #{sourceId}</if>
+            <if test="sourceChannelCode != null and sourceChannelCode != ''">and result.source_identity like concat('%:', #{sourceChannelCode})</if>
+            <if test="beginDeviceTime != null">and result.device_time_utc &gt;= #{beginDeviceTime}</if>
+            <if test="endDeviceTime != null">and result.device_time_utc &lt;= #{endDeviceTime}</if>
+            <if test="cycleNo != null">and result.cycle_no = #{cycleNo}</if>
+            <if test="stepNo != null">and result.step_no = #{stepNo}</if>
+        </where>
+        order by result.device_time_utc, result.source_type, result.result_id
+    </select>
+
     <select id="countTestTypeEnabled" resultType="int">
         select count(1) from t_test_type where test_type_id = #{testTypeId} and status = '0' and del_flag = 0
     </select>

--
Gitblit v1.7.1