From 90b6d6768ebeebc44863850289a4696203a0cda9 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期三, 09 九月 2026 12:25:33 +0800
Subject: [PATCH] 按三类数据分别保存工作簿导入修订

---
 battery-business/src/main/java/com/battery/business/service/impl/TestConsoleDataServiceImpl.java         |   37 ++++++++---
 battery-business/src/main/java/com/battery/business/service/ITestConsoleDataService.java                 |    2 
 ruoyi-admin/src/test/java/com/battery/business/web/ConsoleSingleSheetImportDatabaseTest.java             |   51 +++++++++++++++--
 ruoyi-admin/src/main/java/com/battery/business/web/TTestController.java                                  |    5 +
 ruoyi-admin/src/test/java/com/battery/business/service/impl/TestConsoleDataRevisionQueryServiceTest.java |    7 ++
 battery-business/src/main/java/com/battery/business/domain/TTestDataRevisionAction.java                  |    6 ++
 battery-business/src/main/resources/mapper/battery/TTestMapper.xml                                       |    2 
 ruoyi-admin/src/test/java/com/battery/business/web/ConsoleWorkbookImportMysqlTest.java                   |   30 +++++-----
 8 files changed, 104 insertions(+), 36 deletions(-)

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 c54404d..b4e72cd 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
@@ -38,6 +38,12 @@
     /** 本次提交结果,不持久化到修订事实。 */
     private int importedCount;
     private int skippedCount;
+    /** 本次提交生成的全部修订编号,仅用于提交响应,不持久化。 */
+    @JsonIgnore
+    private java.util.List<Long> actionIds = java.util.List.of();
+
+    public java.util.List<Long> getActionIds() { return actionIds; }
+    public void setActionIds(java.util.List<Long> value) { actionIds = value; }
 
     public int getImportedCount() { return importedCount; }
     public void setImportedCount(int value) { importedCount = value; }
diff --git a/battery-business/src/main/java/com/battery/business/service/ITestConsoleDataService.java b/battery-business/src/main/java/com/battery/business/service/ITestConsoleDataService.java
index 8fbfc33..7754946 100644
--- a/battery-business/src/main/java/com/battery/business/service/ITestConsoleDataService.java
+++ b/battery-business/src/main/java/com/battery/business/service/ITestConsoleDataService.java
@@ -15,6 +15,6 @@
 
 
 
-    /** 在测试行锁和事务内执行一次修订;ALL 导入共用修订编号作为三 Sheet 批次号。 */
+    /** 在测试行锁和事务内执行提交;三 Sheet 按类型各生成修订,全部编号写入 action。 */
     Long submitAction(TTestDataRevisionAction action, List<DataRevisionItem> overrides);
 }
diff --git a/battery-business/src/main/java/com/battery/business/service/impl/TestConsoleDataServiceImpl.java b/battery-business/src/main/java/com/battery/business/service/impl/TestConsoleDataServiceImpl.java
index 7bf81d0..547d62e 100644
--- a/battery-business/src/main/java/com/battery/business/service/impl/TestConsoleDataServiceImpl.java
+++ b/battery-business/src/main/java/com/battery/business/service/impl/TestConsoleDataServiceImpl.java
@@ -33,7 +33,7 @@
     @Override public List<TTestDataRevisionAction> selectActions(TTestDataRevisionAction query)
     {
         if (query == null) query = new TTestDataRevisionAction();
-        if (query.getDataType() != null && !Set.of("REALTIME", "STEP", "CYCLE", "ALL").contains(query.getDataType())) throw new ServiceException("数据类型无效");
+        if (query.getDataType() != null && !Set.of("REALTIME", "STEP", "CYCLE").contains(query.getDataType())) throw new ServiceException("数据类型无效");
         if (query.getOperationType() != null && !Set.of("EDIT", "IMPORT", "DELETE").contains(query.getOperationType())) throw new ServiceException("操作类型无效");
         return mapper.selectDataRevisionActions(query);
     }
@@ -44,7 +44,7 @@
 
     /**
      * 一次提交与接收、测试整体删除互斥;任一目标无效时整次动作回滚。导入仅允许检测完成的测试。
-     * @return 新增修订编号;导入全部重复时返回 null,不新增修订,跳过数量写入 action。
+     * @return 首条新增修订编号,全部编号写入 action;导入全部重复时返回 null,不新增修订。
      */
     @Override @Transactional(rollbackFor = Exception.class)
     public Long submitAction(TTestDataRevisionAction action, List<DataRevisionItem> items)
@@ -59,6 +59,7 @@
         if (test == null) throw new ServiceException("测试不存在");
         action.setCreatedAtUtc(new Date());
         action.setReason(action.getReason().trim());
+        action.setActionIds(List.of());
         List<Map<String, Object>> before = new ArrayList<>();
         List<Map<String, Object>> changes = new ArrayList<>();
         List<Runnable> mutations = new ArrayList<>();
@@ -68,23 +69,38 @@
             boolean workbook = "ALL".equals(action.getDataType());
             Map<String, List<Map<String, Object>>> sheets = importSheets(action, workbook);
             List<String> errors = new ArrayList<>();
-            List<String> addedIdentities = new ArrayList<>();
+            List<TTestDataRevisionAction> revisions = new ArrayList<>();
             Map<String, Map<String, Object>> bindings = new LinkedHashMap<>();
             Set<String> expectedBatteries = null;
             action.setImportedCount(0); action.setSkippedCount(0);
             for (var sheet : sheets.entrySet())
             {
+                List<String> addedIdentities = new ArrayList<>();
+                List<Map<String, Object>> sheetChanges = new ArrayList<>();
                 Set<String> batteries = prepareImport(action, sheet.getKey(), sheet.getValue(), workbook, bindings,
-                        errors, addedIdentities, changes, mutations);
+                        errors, addedIdentities, sheetChanges, mutations);
                 if (workbook && expectedBatteries != null && !expectedBatteries.equals(batteries))
                     errors.add(sheetLabel(sheet.getKey()) + "工作表,列「电池号」:三个 Sheet 的电池号集合不一致");
                 expectedBatteries = batteries;
+                var revision = new TTestDataRevisionAction();
+                revision.setTestId(action.getTestId()); revision.setDataType(sheet.getKey()); revision.setOperationType("IMPORT");
+                revision.setScopeSummary("导入" + sheetChanges.size() + "条" + sheetLabel(sheet.getKey()) + "数据");
+                revision.setIdentitySummary(JSON.toJSONString(addedIdentities));
+                revision.setChangesSummary(JSON.toJSONString(sheetChanges)); revision.setBeforeContentJson("[]");
+                revision.setOperatorId(action.getOperatorId()); revision.setOperatorName(action.getOperatorName());
+                revision.setReason(action.getReason()); revision.setCreatedAtUtc(action.getCreatedAtUtc());
+                revisions.add(revision);
+                action.setImportedCount(action.getImportedCount() + sheetChanges.size());
             }
-            // 三类全量校验后才执行写入,共享测试行锁、事务和唯一修订编号;失败不留下部分数据或修订。
+            // ALL 仅为请求调度标记;三类全量校验后按类型写修订,共享测试行锁和事务。
+            // 整批全重复不留修订;部分重复仍分别记录各类实际新增数量,失败全部回滚。
             if (!errors.isEmpty()) throw new ServiceException(String.join(";", errors));
-            action.setImportedCount(changes.size());
-            action.setIdentitySummary(JSON.toJSONString(addedIdentities));
-            if (changes.isEmpty()) return null;
+            if (action.getImportedCount() == 0) return null;
+            revisions.forEach(mapper::insertDataRevisionAction);
+            mutations.forEach(Runnable::run);
+            action.setActionIds(revisions.stream().map(TTestDataRevisionAction::getActionId).toList());
+            action.setActionId(action.getActionIds().get(0));
+            return action.getActionId();
         }
         else
         {
@@ -129,6 +145,7 @@
         action.setChangesSummary(JSON.toJSONString("DELETE".equals(action.getOperationType()) ? before : changes));
         mapper.insertDataRevisionAction(action);
         mutations.forEach(Runnable::run);
+        action.setActionIds(List.of(action.getActionId()));
         return action.getActionId();
     }
 
@@ -186,11 +203,9 @@
                     continue;
                 }
                 identities.put(identity, values);
-                String revisionIdentity = workbook ? type + ":" + identity : identity;
-                addedIdentities.add(revisionIdentity);
+                addedIdentities.add(identity);
                 mutations.add(() -> mapper.insertImportedDataRecord(type, values));
                 Map<String, Object> change = new LinkedHashMap<>(row);
-                if (workbook) { change.put("dataType", type); change.put("identity", revisionIdentity); }
                 changes.add(change);
             }
             catch (ServiceException ex) { errors.add((workbook ? sheetLabel(type) + "工作表," : "") + "第" + sourceRow + "行," + ex.getMessage()); }
diff --git a/battery-business/src/main/resources/mapper/battery/TTestMapper.xml b/battery-business/src/main/resources/mapper/battery/TTestMapper.xml
index 0f82dbb..00e2240 100644
--- a/battery-business/src/main/resources/mapper/battery/TTestMapper.xml
+++ b/battery-business/src/main/resources/mapper/battery/TTestMapper.xml
@@ -201,7 +201,7 @@
         <where>
             <if test="testId != null">and a.test_id = #{testId}</if>
             <if test="testName != null and testName != ''">and t.test_name like concat('%', #{testName}, '%')</if>
-            <if test="dataType != null and dataType != ''">and (a.data_type = #{dataType} or a.data_type = 'ALL')</if>
+            <if test="dataType != null and dataType != ''">and a.data_type = #{dataType}</if>
             <if test="operationType != null and operationType != ''">and a.operation_type = #{operationType}</if>
             <if test="beginCreatedAt != null">and a.created_at_utc &gt;= #{beginCreatedAt}</if>
             <if test="endCreatedAt != null">and a.created_at_utc &lt; #{endCreatedAt}</if>
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 909c96a..e57caa6 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
@@ -199,7 +199,7 @@
         return success(testDataQueryService.cycles(testId));
     }
 
-    /** 一次提交编辑、导入或删除;服务端只追加一条修订行为。 */
+    /** 一次提交编辑、导入或删除;三 Sheet 按数据类型分别追加修订,共享同一事务。 */
     @PreAuthorize("@ss.hasPermi('system:test:edit')")
     @Log(title = "测试控制台数据修订", businessType = BusinessType.UPDATE)
     @PostMapping("/{testId}/console-data/actions")
@@ -213,7 +213,8 @@
         action.setOperatorId(getUserId()); action.setOperatorName(getUsername());
         Long actionId = consoleDataService.submitAction(action,
                 request.overrides() == null ? java.util.Collections.emptyList() : request.overrides());
-        return success().put("actionId", actionId).put("importedCount", action.getImportedCount()).put("skippedCount", action.getSkippedCount());
+        return success().put("actionId", actionId).put("actionIds", action.getActionIds())
+                .put("importedCount", action.getImportedCount()).put("skippedCount", action.getSkippedCount());
     }
 
     public record ConsoleDataActionRequest(String dataType, String operationType, String scopeSummary,
diff --git a/ruoyi-admin/src/test/java/com/battery/business/service/impl/TestConsoleDataRevisionQueryServiceTest.java b/ruoyi-admin/src/test/java/com/battery/business/service/impl/TestConsoleDataRevisionQueryServiceTest.java
index 181e2da..8791830 100644
--- a/ruoyi-admin/src/test/java/com/battery/business/service/impl/TestConsoleDataRevisionQueryServiceTest.java
+++ b/ruoyi-admin/src/test/java/com/battery/business/service/impl/TestConsoleDataRevisionQueryServiceTest.java
@@ -18,6 +18,13 @@
     private final TTestMapper mapper = mock(TTestMapper.class);
     private final TestConsoleDataServiceImpl service = new TestConsoleDataServiceImpl(mapper);
 
+    @org.junit.jupiter.api.BeforeEach void generatedRevisionId()
+    {
+        when(mapper.insertDataRevisionAction(any())).thenAnswer(call -> {
+            ((TTestDataRevisionAction) call.getArgument(0)).setActionId(1L); return 1;
+        });
+    }
+
     @Test void savesDatabaseBeforeContentInsteadOfClientClaims()
     {
         var action = action("EDIT", "realtime:7");
diff --git a/ruoyi-admin/src/test/java/com/battery/business/web/ConsoleSingleSheetImportDatabaseTest.java b/ruoyi-admin/src/test/java/com/battery/business/web/ConsoleSingleSheetImportDatabaseTest.java
index bc52da0..9290b0e 100644
--- a/ruoyi-admin/src/test/java/com/battery/business/web/ConsoleSingleSheetImportDatabaseTest.java
+++ b/ruoyi-admin/src/test/java/com/battery/business/web/ConsoleSingleSheetImportDatabaseTest.java
@@ -177,7 +177,7 @@
         assertEquals(1, service.selectActions(new TTestDataRevisionAction()).size());
     }
 
-    @Test void workbookImportsThreeSheetsWithOneRevisionAndSkipsRetry()
+    @Test void workbookImportsThreeSheetsWithSeparateRevisionsAndSkipsRetry()
     {
         var sheets = Map.of("REALTIME", List.of(row(), row()), "STEP", List.of(row()), "CYCLE", List.of(row()));
         var result = submitWorkbook(sheets);
@@ -191,14 +191,22 @@
             assertEquals(1, service.selectActions(query).size());
         }
         var actions = service.selectActions(new TTestDataRevisionAction());
-        assertEquals(1, actions.size());
-        assertEquals("ALL", actions.get(0).getDataType());
+        assertEquals(3, actions.size());
+        assertEquals(3, ((List<?>) result.get("actionIds")).size());
+        assertEquals(java.util.Set.of("REALTIME", "STEP", "CYCLE"), actions.stream().map(TTestDataRevisionAction::getDataType).collect(java.util.stream.Collectors.toSet()));
+        for (var revision : actions)
+        {
+            String label = switch (revision.getDataType()) { case "REALTIME" -> "实时"; case "STEP" -> "工步"; default -> "循环"; };
+            assertEquals("导入1条" + label + "数据", revision.getScopeSummary());
+            assertEquals(1L, service.selectActionById(revision.getActionId()).getAffectedRecordCount());
+        }
         assertEquals("三页验收", actions.get(0).getReason());
         var retry = submitWorkbook(sheets);
         assertEquals(0, retry.get("importedCount"));
         assertEquals(4, retry.get("skippedCount"));
         assertNull(retry.get("actionId"));
-        assertEquals(1, service.selectActions(new TTestDataRevisionAction()).size());
+        assertEquals(List.of(), retry.get("actionIds"));
+        assertEquals(3, service.selectActions(new TTestDataRevisionAction()).size());
     }
 
     private com.ruoyi.common.core.domain.AjaxResult submitWorkbook(Map<String, List<Map<String, Object>>> sheets)
@@ -213,6 +221,29 @@
         jdbc.execute("alter table t_test_cycle_record add check (discharge_energy_wh <> 999)");
         assertThrows(RuntimeException.class, () -> submitWorkbook(Map.of("REALTIME", List.of(row()), "STEP", List.of(row()), "CYCLE", List.of(cycle))));
         assertWorkbookEmpty();
+    }
+
+    @Test void workbookRevisionFailureRollsBackEarlierRevisionsAndAllData()
+    {
+        jdbc.execute("alter table t_test_data_revision_action add check (data_type <> 'CYCLE')");
+        assertThrows(RuntimeException.class, () -> submitWorkbook(Map.of("REALTIME", List.of(row()), "STEP", List.of(row()), "CYCLE", List.of(row()))));
+        assertWorkbookEmpty();
+    }
+
+    @Test void partiallyRepeatedWorkbookRecordsActualPerTypeCounts()
+    {
+        submit("REALTIME", List.of(row()));
+        var result = submitWorkbook(Map.of("REALTIME", List.of(row()), "STEP", List.of(row()), "CYCLE", List.of(row())));
+        assertEquals(2, result.get("importedCount"));
+        assertEquals(1, result.get("skippedCount"));
+        assertEquals(3, ((List<?>) result.get("actionIds")).size());
+        var realtime = new TTestDataRevisionAction(); realtime.setDataType("REALTIME");
+        var revisions = service.selectActions(realtime);
+        assertEquals(2, revisions.size());
+        assertEquals("导入0条实时数据", revisions.get(0).getScopeSummary());
+        assertEquals(0L, service.selectActionById(revisions.get(0).getActionId()).getAffectedRecordCount());
+        var invalid = new TTestDataRevisionAction(); invalid.setDataType("ALL");
+        assertThrows(RuntimeException.class, () -> service.selectActions(invalid));
     }
 
     @Test void workbookMissingEmptyInvalidOrConflictingSheetNeverWritesAnySheet()
@@ -302,13 +333,21 @@
     void suppliedPartyWorkbookImportsAllRowsAndSkipsEveryRetry() throws Exception
     {
         var sample = JSON.parseObject(java.nio.file.Files.readString(java.nio.file.Path.of(System.getenv("TICKET03_SAMPLE_JSON"))));
+        Map<String, List<Map<String, Object>>> sheets = new LinkedHashMap<>();
+        for (String type : List.of("REALTIME", "STEP", "CYCLE")) sheets.put(type, (List) sample.getJSONArray(type).toJavaList(Map.class));
+        assertEquals(7767, submitWorkbook(sheets).get("importedCount"));
+        assertEquals(7767, submitWorkbook(sheets).get("skippedCount"));
         for (String type : List.of("REALTIME", "STEP", "CYCLE"))
         {
             List<Map<String, Object>> rows = (List) sample.getJSONArray(type).toJavaList(Map.class);
             int count = switch (type) { case "REALTIME" -> 7605; case "STEP" -> 129; default -> 33; };
-            assertEquals(count, submit(type, rows).get("importedCount"));
             assertEquals(count, mapper.selectImportDataRecords(1L, type).size());
-            assertEquals(count, submit(type, rows).get("skippedCount"));
+            var query = new TTestDataRevisionAction(); query.setDataType(type);
+            var actions = service.selectActions(query);
+            assertEquals(1, actions.size());
+            assertEquals((long) count, service.selectActionById(actions.get(0).getActionId()).getAffectedRecordCount());
+            String label = switch (type) { case "REALTIME" -> "实时"; case "STEP" -> "工步"; default -> "循环"; };
+            assertEquals("导入" + count + "条" + label + "数据", actions.get(0).getScopeSummary());
             if ("STEP".equals(type))
             {
                 var persisted = mapper.selectImportDataRecords(1L, type).stream().collect(java.util.stream.Collectors.toMap(
diff --git a/ruoyi-admin/src/test/java/com/battery/business/web/ConsoleWorkbookImportMysqlTest.java b/ruoyi-admin/src/test/java/com/battery/business/web/ConsoleWorkbookImportMysqlTest.java
index 08efa77..99ebdc8 100644
--- a/ruoyi-admin/src/test/java/com/battery/business/web/ConsoleWorkbookImportMysqlTest.java
+++ b/ruoyi-admin/src/test/java/com/battery/business/web/ConsoleWorkbookImportMysqlTest.java
@@ -102,20 +102,20 @@
                 var firstRecord = json.readTree(json.writeValueAsString(controller.persistedRecords(1L))).get("data").get(7604);
                 assertEquals(1683517331000L, firstRecord.get("deviceTime").longValue());
                 var actions = service.selectActions(new TTestDataRevisionAction());
-                assertEquals(1, actions.size());
-                var revision = service.selectActionById(actions.get(0).getActionId());
-                assertEquals(imported.get("actionId"), revision.getActionId());
-                assertEquals("三 Sheet 共用原因", revision.getReason());
-                assertEquals(7767L, revision.getAffectedRecordCount());
-                assertNull(revision.getIdentitySummary());
-                assertNull(revision.getChangesSummary());
-                assertNull(revision.getBeforeContentJson());
-                // 页面仅返回摘要;仍从数据库原文验证三类审计完整保存。
-                var changes = JSON.parseArray(jdbc.queryForObject("select cast(changes_summary as char) from t_test_data_revision_action where action_id=?", String.class, revision.getActionId()));
-                assertEquals(7767, changes.size());
-                assertEquals("REALTIME", changes.getJSONObject(0).getString("dataType"));
-                assertEquals("STEP", changes.getJSONObject(7605).getString("dataType"));
-                assertEquals("CYCLE", changes.getJSONObject(7734).getString("dataType"));
+                assertEquals(3, actions.size());
+                assertEquals(3, ((List<?>) imported.get("actionIds")).size());
+                for (var item : actions) {
+                    var revision = service.selectActionById(item.getActionId());
+                    assertEquals("三 Sheet 共用原因", revision.getReason());
+                    int expected = switch (revision.getDataType()) { case "REALTIME" -> 7605; case "STEP" -> 129; default -> 33; };
+                    assertEquals((long) expected, revision.getAffectedRecordCount());
+                    assertNull(revision.getIdentitySummary());
+                    assertNull(revision.getChangesSummary());
+                    assertNull(revision.getBeforeContentJson());
+                    // 页面只返回各类摘要,数据库仍保留该类全部导入明细。
+                    var changes = JSON.parseArray(jdbc.queryForObject("select cast(changes_summary as char) from t_test_data_revision_action where action_id=?", String.class, revision.getActionId()));
+                    assertEquals(expected, changes.size());
+                }
                 for (String type : sheets.keySet()) {
                     var query = new TTestDataRevisionAction(); query.setTestId(1L); query.setDataType(type);
                     assertEquals(1, service.selectActions(query).size());
@@ -124,7 +124,7 @@
                 assertEquals(0, retry.get("importedCount")); assertEquals(7767, retry.get("skippedCount")); assertNull(retry.get("actionId"));
                 sheets.get("CYCLE").get(32).put("dischargeCapacity", 99);
                 assertThrows(RuntimeException.class, () -> submit(controller, sheets));
-                assertEquals(1, service.selectActions(new TTestDataRevisionAction()).size());
+                assertEquals(3, service.selectActions(new TTestDataRevisionAction()).size());
                 assertTrue(mapper.selectWarningExceptions(1L, null).isEmpty());
                 assertEquals("COMPLETED", mapper.selectTestById(1L).getTestStatus());
                 assertEquals("ENDED", mapper.selectTestById(1L).getReceptionStatus());

--
Gitblit v1.7.1