package com.ruoyi.system.service.impl; import java.util.Date; import java.util.List; import java.util.Set; import java.util.Map; import java.util.HashSet; import com.alibaba.fastjson2.JSON; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.system.domain.TTestDataOverride; import com.ruoyi.system.domain.TTestDataRevisionAction; import com.ruoyi.system.domain.TTestConsoleImportRecord; import com.ruoyi.system.mapper.TTestMapper; import com.ruoyi.system.service.ITestConsoleDataService; /** 控制台数据操作的边界校验和事务协调。 */ @Service public class TestConsoleDataServiceImpl implements ITestConsoleDataService { private static final Set DATA_TYPES = Set.of("REALTIME", "STEP", "CYCLE"); private static final Set OPERATIONS = Set.of("EDIT", "IMPORT", "DELETE"); @Autowired private TTestMapper mapper; @Override public List selectActions(TTestDataRevisionAction query) { if (query == null) query = new TTestDataRevisionAction(); if (query.getDataType() != null && !query.getDataType().isBlank() && !DATA_TYPES.contains(query.getDataType())) throw new ServiceException("控制台数据类型无效"); if (query.getOperationType() != null && !query.getOperationType().isBlank() && !OPERATIONS.contains(query.getOperationType())) throw new ServiceException("数据操作类型无效"); return mapper.selectDataRevisionActions(query); } @Override public TTestDataRevisionAction selectActionById(Long actionId) { if (actionId == null || actionId < 1) throw new ServiceException("修订行为ID无效"); return mapper.selectDataRevisionActionById(actionId); } @Override public List selectOverrides(Long testId, String dataType) { validateType(testId, dataType); return mapper.selectDataOverrides(testId, dataType); } @Override @Transactional(rollbackFor = Exception.class) public Long submitAction(TTestDataRevisionAction action, List overrides, List btsSampleIds, List collectorSampleIds) { if (action == null || action.getTestId() == null) throw new ServiceException("测试ID不能为空"); validateType(action.getTestId(), action.getDataType()); if (!OPERATIONS.contains(action.getOperationType())) throw new ServiceException("数据操作类型无效"); if ("IMPORT".equals(action.getOperationType())) { com.ruoyi.system.domain.TTest test = mapper.selectTestById(action.getTestId()); if (test == null || !"ENDED".equals(test.getReceptionStatus()) || !"COMPLETED".equals(test.getTestStatus())) throw new ServiceException("测试完成后才可以导入数据"); } if ("EDIT".equals(action.getOperationType())) validateEditableFields(action); if (action.getReason() == null || action.getReason().trim().isEmpty()) throw new ServiceException("数据操作原因不能为空"); validateActionSummaries(action, overrides); if (action.getOperatorName() == null || action.getOperatorName().trim().isEmpty()) action.setOperatorName("未知用户"); action.setReason(action.getReason().trim()); action.setCreatedAtUtc(new Date()); if ("DELETE".equals(action.getOperationType())) { applyPhysicalDelete(action); return action.getActionId(); } mapper.insertDataRevisionAction(action); if ("IMPORT".equals(action.getOperationType()) && action.getChangesSummary() != null) { try { @SuppressWarnings("unchecked") List> rows = (List>) (List) JSON.parseArray(action.getChangesSummary(), Map.class); Set identities = new HashSet<>(); for (Map row : rows) { Object identity = row.get("recordNo"); if (identity == null || !identities.add(String.valueOf(identity))) throw new ServiceException("导入数据存在重复稳定身份"); Object batteryNo = row.get("batteryNo"); if (batteryNo != null && !String.valueOf(batteryNo).isBlank() && mapper.countTestBatteryNo(action.getTestId(), String.valueOf(batteryNo)) == 0) throw new ServiceException("导入数据包含不属于当前测试的电池号: " + batteryNo); TTestConsoleImportRecord record = new TTestConsoleImportRecord(); record.setTestId(action.getTestId()); record.setDataType(action.getDataType()); record.setStableIdentity(String.valueOf(identity)); record.setRowValues(JSON.toJSONString(row)); record.setActionId(action.getActionId()); record.setCreatedAtUtc(action.getCreatedAtUtc()); mapper.insertConsoleImportRecord(record); } } catch (ServiceException exception) { throw exception; } catch (Exception exception) { throw new ServiceException("导入数据格式无效"); } } if (overrides != null) { Set identities = new HashSet<>(); for (TTestDataOverride item : overrides) { if (item == null || item.getStableIdentity() == null || item.getStableIdentity().trim().isEmpty()) throw new ServiceException("数据稳定身份不能为空"); item.setStableIdentity(item.getStableIdentity().trim()); if (!identities.add(item.getStableIdentity())) throw new ServiceException("同一批数据存在重复稳定身份"); validateOverride(item, action.getOperationType(), action.getDataType()); item.setTestId(action.getTestId()); item.setDataType(action.getDataType()); item.setActionId(action.getActionId()); item.setCreatedAtUtc(action.getCreatedAtUtc()); mapper.upsertDataOverride(item); } } return action.getActionId(); } /** 删除前先把完整当前记录写入修订摘要,再删除事实行。 */ private void applyPhysicalDelete(TTestDataRevisionAction action) { List> rows = new java.util.ArrayList<>(); List> source = switch (action.getDataType()) { case "REALTIME" -> mapper.selectPersistedFinalRealtimeRecords(action.getTestId()); case "STEP" -> mapper.selectPersistedFinalStepRecords(action.getTestId()); case "CYCLE" -> mapper.selectPersistedFinalCycleRecords(action.getTestId()); default -> List.of(); }; Set wanted = new HashSet<>(); try { for (Object value : JSON.parseArray(action.getIdentitySummary())) wanted.add(String.valueOf(value)); } catch (Exception e) { throw new ServiceException("身份摘要格式无效"); } for (Map row : source) if (wanted.contains(String.valueOf(row.get("stableIdentity")))) rows.add(new java.util.LinkedHashMap<>(row)); action.setChangesSummary(JSON.toJSONString(rows)); mapper.insertDataRevisionAction(action); List ids = new java.util.ArrayList<>(); for (String identity : wanted) { String[] p = identity.split(":", -1); try { if ("REALTIME".equals(action.getDataType()) && p.length == 2) ids.add(Long.valueOf(p[1])); else if ("STEP".equals(action.getDataType()) && p.length == 2) ids.add(Long.valueOf(p[1])); else if ("CYCLE".equals(action.getDataType()) && p.length == 2) ids.add(Long.valueOf(p[1])); } catch (NumberFormatException ignored) { } } if (ids.isEmpty()) return; if ("REALTIME".equals(action.getDataType())) mapper.deleteRealtimeRecordsByIds(action.getTestId(), ids); if ("STEP".equals(action.getDataType())) mapper.deleteStepRecordsByIds(action.getTestId(), ids); if ("CYCLE".equals(action.getDataType())) mapper.deleteCycleRecordsByIds(action.getTestId(), ids); } private void validateType(Long testId, String dataType) { if (testId == null || testId < 1) throw new ServiceException("测试ID无效"); if (!DATA_TYPES.contains(dataType)) throw new ServiceException("控制台数据类型无效"); } private void validateEditableFields(TTestDataRevisionAction action) { if (action.getChangesSummary() == null || action.getChangesSummary().isBlank()) throw new ServiceException("修改内容不能为空"); try { @SuppressWarnings("unchecked") List> changes = (List>) (List) JSON.parseArray(action.getChangesSummary(), Map.class); Set editable = switch (action.getDataType()) { case "REALTIME" -> Set.of("voltage", "current", "capacity", "energy", "metricValue", "temperature", "positivePotential", "negativePotential"); case "STEP" -> Set.of("stepDuration", "capacity", "energy", "startVoltage", "endVoltage", "chargePeakVoltage", "dischargeValleyVoltage"); case "CYCLE" -> Set.of("chargeCapacity", "dischargeCapacity", "efficiency", "chargeEnergy", "dischargeEnergy"); default -> Set.of(); }; Set immutable = Set.of("recordNo", "batteryNo", "cycleNo", "stepNo", "stepName", "sequenceId", "recordedAt", "absoluteTime"); for (Map item : changes) { Map before = objectMap(item.get("before")); Map after = objectMap(item.get("after")); for (String key : immutable) if (!java.util.Objects.equals(before.get(key), after.get(key))) throw new ServiceException("记录身份字段不可修改: " + key); for (String key : after.keySet()) if (!java.util.Objects.equals(before.get(key), after.get(key)) && !editable.contains(key)) throw new ServiceException("字段不可修改: " + key); } } catch (ServiceException exception) { throw exception; } catch (Exception exception) { throw new ServiceException("修改内容格式无效"); } } private void validateOverride(TTestDataOverride item, String operation, String dataType) { if ("DELETE".equals(operation) && parseDeleteIdentity(item.getStableIdentity(), dataType) == null) throw new ServiceException("删除数据稳定身份无效"); if (!"EDIT".equals(operation) || item.getFieldValues() == null || item.getFieldValues().isBlank()) return; try { Map values = JSON.parseObject(item.getFieldValues(), Map.class); for (String key : values.keySet()) if (!editableFields(dataType).contains(key)) throw new ServiceException("字段不可修改: " + key); } catch (ServiceException exception) { throw exception; } catch (Exception exception) { throw new ServiceException("修改字段格式无效"); } } /** 防止客户端提交与实际覆盖身份不一致的伪行为摘要。 */ private void validateActionSummaries(TTestDataRevisionAction action, List overrides) { if (action.getScopeSummary() == null || action.getScopeSummary().trim().isEmpty()) throw new ServiceException("数据操作范围不能为空"); Set summaryIdentities = new HashSet<>(); try { Object parsed = JSON.parse(action.getIdentitySummary()); if (!(parsed instanceof List values)) throw new ServiceException("身份摘要格式无效"); for (Object value : values) if (value == null || !summaryIdentities.add(String.valueOf(value))) throw new ServiceException("身份摘要存在重复或空值"); } catch (ServiceException exception) { throw exception; } catch (Exception exception) { throw new ServiceException("身份摘要格式无效"); } if (overrides == null) return; Set overrideIdentities = new HashSet<>(); for (TTestDataOverride item : overrides) if (item == null || !overrideIdentities.add(item.getStableIdentity()) || !summaryIdentities.contains(item.getStableIdentity())) throw new ServiceException("身份摘要与数据覆盖明细不一致"); if (!summaryIdentities.equals(overrideIdentities)) throw new ServiceException("身份摘要与数据覆盖明细不一致"); if ("EDIT".equals(action.getOperationType())) { try { Object parsed = JSON.parse(action.getChangesSummary()); if (!(parsed instanceof List changes) || changes.size() < overrides.size()) throw new ServiceException("字段变化摘要与数据覆盖明细不一致"); } catch (ServiceException exception) { throw exception; } catch (Exception exception) { throw new ServiceException("字段变化摘要格式无效"); } } } private Set editableFields(String dataType) { return switch (dataType) { case "REALTIME" -> Set.of("voltage", "current", "capacity", "energy", "metricValue", "temperature", "positivePotential", "negativePotential"); case "STEP" -> Set.of("stepDuration", "capacity", "energy", "startVoltage", "endVoltage", "chargePeakVoltage", "dischargeValleyVoltage"); case "CYCLE" -> Set.of("chargeCapacity", "dischargeCapacity", "efficiency", "chargeEnergy", "dischargeEnergy"); default -> Set.of(); }; } private Long[] parseDeleteIdentity(String value, String dataType) { String[] parts = value.split(":", -1); if ("REALTIME".equals(dataType) && parts.length == 5 && "collector".equals(parts[0]) && !parts[2].isBlank() && !parts[4].isBlank()) { try { return new Long[] { Long.valueOf(parts[1]), Long.valueOf(parts[3]) }; } catch (NumberFormatException exception) { return null; } } if ("REALTIME".equals(dataType) && parts.length == 2 && "snapshot".equals(parts[0])) { try { return new Long[] { Long.valueOf(parts[1]) }; } catch (NumberFormatException exception) { return null; } } int expectedLength = "STEP".equals(dataType) ? 8 : "CYCLE".equals(dataType) ? 7 : 6; int legacyLength = "STEP".equals(dataType) ? 5 : "CYCLE".equals(dataType) ? 4 : 3; if (parts.length != expectedLength && parts.length != legacyLength) return null; int offset = "REALTIME".equals(dataType) ? 0 : 1; try { if (parts.length == legacyLength && "STEP".equals(dataType)) return new Long[] { Long.valueOf(parts[1]), Long.valueOf(parts[2]), Long.valueOf(parts[3]), Long.valueOf(parts[4]) }; if (parts.length == legacyLength) return new Long[] { Long.valueOf(parts[0]), Long.valueOf(parts[1]), Long.valueOf(parts[2]) }; if ("STEP".equals(dataType)) return new Long[] { Long.valueOf(parts[offset]), Long.valueOf(parts[offset + 1]), Long.valueOf(parts[offset + 2]), Long.valueOf(parts[offset + 3]), Long.valueOf(parts[offset + 4]), Long.valueOf(parts[offset + 5]), Long.valueOf(parts[offset + 6]) }; return new Long[] { Long.valueOf(parts[offset]), Long.valueOf(parts[offset + 1]), Long.valueOf(parts[offset + 2]), Long.valueOf(parts[offset + 3]), Long.valueOf(parts[offset + 4]), Long.valueOf(parts[offset + 5]) }; } catch (NumberFormatException exception) { return null; } } @SuppressWarnings("unchecked") private Map objectMap(Object value) { return value instanceof Map ? (Map) value : Map.of(); } }