From 561ff691ae9b58dc534ebe625d63471d040d3160 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期日, 06 九月 2026 00:17:28 +0800
Subject: [PATCH] 实现三类历史数据原子导入与共同修订批次

---
 battery-business/src/main/java/com/battery/business/service/impl/TestConsoleDataServiceImpl.java |  133 ++++++++++++++++++++++++++++++--------------
 1 files changed, 90 insertions(+), 43 deletions(-)

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 65c89dd..f57eb5f 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").contains(query.getDataType())) throw new ServiceException("数据类型无效");
+        if (query.getDataType() != null && !Set.of("REALTIME", "STEP", "CYCLE", "ALL").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);
     }
@@ -50,8 +50,9 @@
     public Long submitAction(TTestDataRevisionAction action, List<DataRevisionItem> items)
     {
         if (action == null || action.getTestId() == null) throw new ServiceException("测试ID不能为空");
-        if (action.getDataType() == null || !Set.of("REALTIME", "STEP", "CYCLE").contains(action.getDataType())) throw new ServiceException("数据类型无效");
+        if (action.getDataType() == null || !Set.of("REALTIME", "STEP", "CYCLE", "ALL").contains(action.getDataType())) throw new ServiceException("数据类型无效");
         if (action.getOperationType() == null || !Set.of("EDIT", "IMPORT", "DELETE").contains(action.getOperationType())) throw new ServiceException("操作类型无效");
+        if ("ALL".equals(action.getDataType()) && !"IMPORT".equals(action.getOperationType())) throw new ServiceException("三 Sheet 仅支持导入");
         if (action.getReason() == null || action.getReason().isBlank()) throw new ServiceException("数据操作原因不能为空");
         if (action.getScopeSummary() == null || action.getScopeSummary().isBlank()) throw new ServiceException("数据操作范围不能为空");
         TTest test = mapper.selectTestByIdForUpdate(action.getTestId());
@@ -64,45 +65,22 @@
         if ("IMPORT".equals(action.getOperationType()))
         {
             if (!"COMPLETED".equals(test.getTestStatus())) throw new ServiceException("测试完成后才可以导入数据");
-            List<Map> rows;
-            try { rows = JSON.parseArray(action.getChangesSummary(), Map.class); }
-            catch (RuntimeException ex) { throw new ServiceException("导入数据格式无效"); }
-            if (rows == null || rows.isEmpty()) throw new ServiceException("导入数据不能为空");
-            Map<String, Map<String, Object>> identities = new LinkedHashMap<>();
-            Map<String, Map<String, Object>> bindings = new LinkedHashMap<>();
-            for (Map<String, Object> existing : mapper.selectImportDataRecords(action.getTestId(), action.getDataType()))
-            {
-                identities.put(importIdentity(existing), existing);
-            }
+            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<>();
+            Map<String, Map<String, Object>> bindings = new LinkedHashMap<>();
+            Set<String> expectedBatteries = null;
             action.setImportedCount(0); action.setSkippedCount(0);
-            for (int index = 0; index < rows.size(); index++)
+            for (var sheet : sheets.entrySet())
             {
-                Map<String, Object> row = rows.get(index);
-                Object sourceRow = row == null ? index + 2 : row.getOrDefault("sourceRow", index + 2);
-                try
-                {
-                    if (row == null) throw new ServiceException("列「记录编号」:数据行不能为空");
-                    String batteryNo = importText(row, "batteryNo", "电池号", 100);
-                    Map<String, Object> binding = bindings.computeIfAbsent(batteryNo, key -> mapper.selectImportBinding(action.getTestId(), key));
-                    Map<String, Object> values = importedRecord(action, row, binding);
-                    String identity = importIdentity(values);
-                    Map<String, Object> previous = identities.get(identity);
-                    if (previous != null)
-                    {
-                        if (!sameImportContent(values, previous)) throw new ServiceException("列「记录编号」:相同来源身份和编号存在不同内容,冲突拒绝");
-                        action.setSkippedCount(action.getSkippedCount() + 1);
-                        continue;
-                    }
-                    identities.put(identity, values);
-                    addedIdentities.add(identity);
-                    mutations.add(() -> mapper.insertImportedDataRecord(action.getDataType(), values));
-                    changes.add(new LinkedHashMap<>(row));
-                }
-                catch (ServiceException ex) { errors.add("第" + sourceRow + "行," + ex.getMessage()); }
+                Set<String> batteries = prepareImport(action, sheet.getKey(), sheet.getValue(), workbook, bindings,
+                        errors, addedIdentities, changes, mutations);
+                if (workbook && expectedBatteries != null && !expectedBatteries.equals(batteries))
+                    errors.add(sheetLabel(sheet.getKey()) + "工作表,列「电池号」:三个 Sheet 的电池号集合不一致");
+                expectedBatteries = batteries;
             }
-            // 先完成整页校验,再写入修订与数据;测试行锁同时串行化重复提交和整体删除。
+            // 三类全量校验后才执行写入,共享测试行锁、事务和唯一修订编号;失败不留下部分数据或修订。
             if (!errors.isEmpty()) throw new ServiceException(String.join(";", errors));
             action.setImportedCount(changes.size());
             action.setIdentitySummary(JSON.toJSONString(addedIdentities));
@@ -153,6 +131,75 @@
         mutations.forEach(Runnable::run);
         return action.getActionId();
     }
+
+    private Map<String, List<Map<String, Object>>> importSheets(TTestDataRevisionAction action, boolean workbook)
+    {
+        Map<String, List<Map<String, Object>>> sheets = new LinkedHashMap<>();
+        try
+        {
+            Object parsed = JSON.parse(action.getChangesSummary());
+            if (workbook && (!(parsed instanceof Map<?, ?> map) || !map.keySet().equals(Set.of("REALTIME", "STEP", "CYCLE"))))
+                throw new ServiceException("三 Sheet 必须包含实时、工步、循环工作表");
+            for (String type : workbook ? List.of("REALTIME", "STEP", "CYCLE") : List.of(action.getDataType()))
+            {
+                Object value = workbook ? ((Map<?, ?>) parsed).get(type) : parsed;
+                if (!(value instanceof List<?> rows) || rows.isEmpty()) throw new ServiceException(sheetLabel(type) + "工作表:导入数据不能为空");
+                List<Map<String, Object>> records = new ArrayList<>();
+                for (Object row : rows)
+                {
+                    if (row != null && !(row instanceof Map)) throw new ServiceException(sheetLabel(type) + "工作表:数据行格式无效");
+                    records.add((Map<String, Object>) row);
+                }
+                sheets.put(type, records);
+            }
+        }
+        catch (ServiceException ex) { throw ex; }
+        catch (RuntimeException ex) { throw new ServiceException("导入数据格式无效"); }
+        return sheets;
+    }
+
+    private Set<String> prepareImport(TTestDataRevisionAction action, String type, List<Map<String, Object>> rows,
+            boolean workbook, Map<String, Map<String, Object>> bindings, List<String> errors,
+            List<String> addedIdentities, List<Map<String, Object>> changes, List<Runnable> mutations)
+    {
+        Map<String, Map<String, Object>> identities = new LinkedHashMap<>();
+        for (Map<String, Object> existing : mapper.selectImportDataRecords(action.getTestId(), type))
+            identities.put(importIdentity(existing), existing);
+        Set<String> batteries = new HashSet<>();
+        for (int index = 0; index < rows.size(); index++)
+        {
+            Map<String, Object> row = rows.get(index);
+            Object sourceRow = row == null ? index + 2 : row.getOrDefault("sourceRow", index + 2);
+            try
+            {
+                if (row == null) throw new ServiceException("列「记录编号」:数据行不能为空");
+                String batteryNo = importText(row, "batteryNo", "电池号", 100);
+                batteries.add(batteryNo);
+                Map<String, Object> binding = bindings.computeIfAbsent(batteryNo, key -> mapper.selectImportBinding(action.getTestId(), key));
+                Map<String, Object> values = importedRecord(action, type, row, binding);
+                String identity = importIdentity(values);
+                Map<String, Object> previous = identities.get(identity);
+                if (previous != null)
+                {
+                    if (!sameImportContent(values, previous)) throw new ServiceException("列「记录编号」:相同来源身份和编号存在不同内容,冲突拒绝");
+                    action.setSkippedCount(action.getSkippedCount() + 1);
+                    continue;
+                }
+                identities.put(identity, values);
+                String revisionIdentity = workbook ? type + ":" + identity : identity;
+                addedIdentities.add(revisionIdentity);
+                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()); }
+        }
+        return batteries;
+    }
+
+    private String sheetLabel(String type)
+    { return switch (type) { case "REALTIME" -> "实时"; case "STEP" -> "工步"; default -> "循环"; }; }
 
     private long recordId(String type, String identity)
     {
@@ -207,7 +254,7 @@
         values.put("extension_measurements_json", JSON.toJSONString(extension));
     }
 
-    private Map<String, Object> importedRecord(TTestDataRevisionAction action, Map<String, Object> row, Map<String, Object> binding)
+    private Map<String, Object> importedRecord(TTestDataRevisionAction action, String type, Map<String, Object> row, Map<String, Object> binding)
     {
         if (binding == null || List.of("test_battery_id", "binding_id", "adapter_code", "devtype", "devid", "subdevid", "chlid")
                 .stream().anyMatch(key -> binding.get(key) == null))
@@ -233,20 +280,20 @@
         values.put("server_received_at_utc", action.getCreatedAtUtc());
         values.put("cycle_no", importInteger(row, "cycleNo", "循环序号", Integer.MAX_VALUE));
         Map<String, Object> metrics = new LinkedHashMap<>();
-        columns(action.getDataType()).keySet().forEach(key -> metrics.put(key, importMetric(row, key)));
-        values.putAll(editableValues(action.getDataType(), metrics));
-        if ("STEP".equals(action.getDataType()))
+        columns(type).keySet().forEach(key -> metrics.put(key, importMetric(row, key)));
+        values.putAll(editableValues(type, metrics));
+        if ("STEP".equals(type))
         {
             Map<String, Object> extension = new TreeMap<>();
             STEP_EXTENSION_FIELDS.forEach(key -> extension.put(key, importMetric(row, key)));
-            mergeStepExtension(action.getDataType(), null, extension, values);
+            mergeStepExtension(type, null, extension, values);
         }
-        if (!"CYCLE".equals(action.getDataType()))
+        if (!"CYCLE".equals(type))
         {
             values.put("step_no", importInteger(row, "stepNo", "工步序号", Integer.MAX_VALUE));
             values.put("step_name", importText(row, "stepName", "工步名称", 191));
         }
-        if ("REALTIME".equals(action.getDataType()))
+        if ("REALTIME".equals(type))
         {
             values.put("cabinet_sequence_id", importInteger(row, "sequenceId", "记录序号", 9007199254740991L));
             for (String field : List.of("devtype", "devid", "subdevid", "chlid")) values.put("cabinet_" + field, binding.get(field));

--
Gitblit v1.7.1