按三类数据分别保存工作簿导入修订
三页导入在同一事务中分别生成实时、工步、循环修订,操作范围按实际新增数量填写,完整重复不新增修订,返回全部修订编号。保留整批校验与失败回滚,查询类型仅接受三类。
原Excel三类7605、129、33条分别验证通过。全量290项测试中289通过、1项隔离MySQL测试跳过,打包通过。
| | |
| | | /** 本次提交结果,不持久化到修订事实。 */ |
| | | 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; } |
| | |
| | | |
| | | |
| | | |
| | | /** 在测试行锁和事务内执行一次修订;ALL 导入共用修订编号作为三 Sheet 批次号。 */ |
| | | /** 在测试行锁和事务内执行提交;三 Sheet 按类型各生成修订,全部编号写入 action。 */ |
| | | Long submitAction(TTestDataRevisionAction action, List<DataRevisionItem> overrides); |
| | | } |
| | |
| | | @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); |
| | | } |
| | |
| | | |
| | | /** |
| | | * 一次提交与接收、测试整体删除互斥;任一目标无效时整次动作回滚。导入仅允许检测完成的测试。 |
| | | * @return 新增修订编号;导入全部重复时返回 null,不新增修订,跳过数量写入 action。 |
| | | * @return 首条新增修订编号,全部编号写入 action;导入全部重复时返回 null,不新增修订。 |
| | | */ |
| | | @Override @Transactional(rollbackFor = Exception.class) |
| | | public Long submitAction(TTestDataRevisionAction action, List<DataRevisionItem> items) |
| | |
| | | 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<>(); |
| | |
| | | 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 |
| | | { |
| | |
| | | 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(); |
| | | } |
| | | |
| | |
| | | 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()); } |
| | |
| | | <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 >= #{beginCreatedAt}</if> |
| | | <if test="endCreatedAt != null">and a.created_at_utc < #{endCreatedAt}</if> |
| | |
| | | return success(testDataQueryService.cycles(testId)); |
| | | } |
| | | |
| | | /** 一次提交编辑、导入或删除;服务端只追加一条修订行为。 */ |
| | | /** 一次提交编辑、导入或删除;三 Sheet 按数据类型分别追加修订,共享同一事务。 */ |
| | | @PreAuthorize("@ss.hasPermi('system:test:edit')") |
| | | @Log(title = "测试控制台数据修订", businessType = BusinessType.UPDATE) |
| | | @PostMapping("/{testId}/console-data/actions") |
| | |
| | | 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, |
| | |
| | | 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"); |
| | |
| | | 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); |
| | |
| | | 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) |
| | |
| | | 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() |
| | |
| | | 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( |
| | |
| | | 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()); |
| | |
| | | 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()); |