| | |
| | | @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); |
| | | } |
| | |
| | | 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()); |
| | |
| | | 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)); |
| | |
| | | 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) |
| | | { |
| | |
| | | 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)) |
| | |
| | | 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)); |