yeg
2026-09-09 90b6d6768ebeebc44863850289a4696203a0cda9
按三类数据分别保存工作簿导入修订

三页导入在同一事务中分别生成实时、工步、循环修订,操作范围按实际新增数量填写,完整重复不新增修订,返回全部修订编号。保留整批校验与失败回滚,查询类型仅接受三类。

原Excel三类7605、129、33条分别验证通过。全量290项测试中289通过、1项隔离MySQL测试跳过,打包通过。
8个文件已修改
140 ■■■■■ 已修改文件
battery-business/src/main/java/com/battery/business/domain/TTestDataRevisionAction.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
battery-business/src/main/java/com/battery/business/service/ITestConsoleDataService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
battery-business/src/main/java/com/battery/business/service/impl/TestConsoleDataServiceImpl.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
battery-business/src/main/resources/mapper/battery/TTestMapper.xml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/battery/business/web/TTestController.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/test/java/com/battery/business/service/impl/TestConsoleDataRevisionQueryServiceTest.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/test/java/com/battery/business/web/ConsoleSingleSheetImportDatabaseTest.java 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/test/java/com/battery/business/web/ConsoleWorkbookImportMysqlTest.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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; }
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);
}
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()); }
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>
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,
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");
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(
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());