package com.battery.business.service.impl; import java.math.BigDecimal; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.time.Instant; import java.util.ArrayList; import java.util.Date; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import com.alibaba.fastjson2.JSON; import com.battery.business.domain.DataRevisionItem; import com.battery.business.domain.TTest; import com.battery.business.domain.TTestDataRevisionAction; import com.battery.business.mapper.TTestMapper; import com.battery.business.service.ITestConsoleDataService; import com.ruoyi.common.exception.ServiceException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; /** 在测试行锁内保存修改前完整事实,再编辑、导入或物理删除三类记录。 */ @Service public class TestConsoleDataServiceImpl implements ITestConsoleDataService { private final TTestMapper mapper; public TestConsoleDataServiceImpl(TTestMapper mapper) { this.mapper = mapper; } /** 查询只追加的修订动作。 */ @Override public List 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.getOperationType() != null && !Set.of("EDIT", "IMPORT", "DELETE").contains(query.getOperationType())) throw new ServiceException("操作类型无效"); return mapper.selectDataRevisionActions(query); } /** 查询修订动作详情及修改前内容。 */ @Override public TTestDataRevisionAction selectActionById(Long actionId) { return mapper.selectDataRevisionActionById(actionId); } /** 一次提交与接收、测试整体删除互斥;任一目标无效时整次动作回滚。 */ @Override @Transactional(rollbackFor = Exception.class) public Long submitAction(TTestDataRevisionAction action, List 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.getOperationType() == null || !Set.of("EDIT", "IMPORT", "DELETE").contains(action.getOperationType())) throw new ServiceException("操作类型无效"); 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 (test == null) throw new ServiceException("测试不存在"); action.setCreatedAtUtc(new Date()); action.setReason(action.getReason().trim()); List> before = new ArrayList<>(); List> changes = new ArrayList<>(); List mutations = new ArrayList<>(); if ("IMPORT".equals(action.getOperationType())) { if (!"COMPLETED".equals(test.getTestStatus())) throw new ServiceException("测试完成后才可以导入数据"); List rows; try { rows = JSON.parseArray(action.getChangesSummary(), Map.class); } catch (RuntimeException ex) { throw new ServiceException("导入数据格式无效"); } if (rows == null || rows.isEmpty()) throw new ServiceException("导入数据不能为空"); Set identities = new HashSet<>(); for (Map row : rows) { String recordNo = row.get("recordNo") == null ? "" : row.get("recordNo").toString(); if (recordNo.isBlank() || !identities.add(recordNo)) throw new ServiceException("导入记录编号为空或重复"); Map values = importedRecord(action, row); mutations.add(() -> mapper.insertImportedDataRecord(action.getDataType(), values)); changes.add(new LinkedHashMap<>(row)); } action.setIdentitySummary(JSON.toJSONString(identities)); } else { List identities; try { identities = JSON.parseArray(action.getIdentitySummary(), String.class); } catch (RuntimeException ex) { throw new ServiceException("身份摘要格式无效"); } if (identities == null || identities.isEmpty() || new HashSet<>(identities).size() != identities.size()) throw new ServiceException("请选择不重复的数据记录"); Map edits = new LinkedHashMap<>(); for (DataRevisionItem item : items == null ? List.of() : items) if (item == null || edits.put(item.getStableIdentity(), item) != null) throw new ServiceException("修改目标无效或重复"); if ("EDIT".equals(action.getOperationType()) && !edits.keySet().equals(new HashSet<>(identities))) throw new ServiceException("修改目标与身份摘要不一致"); for (String identity : identities) { long id = recordId(action.getDataType(), identity); Map row = mapper.selectDataRecord(action.getTestId(), action.getDataType(), id); if (row == null || row.isEmpty()) throw new ServiceException("数据不存在或不属于当前测试"); before.add(new LinkedHashMap<>(row)); if ("DELETE".equals(action.getOperationType())) mutations.add(() -> mapper.deleteDataRecord(action.getTestId(), action.getDataType(), id)); else { Map fields; try { fields = JSON.parseObject(edits.get(identity).getFieldValues(), Map.class); } catch (RuntimeException ex) { throw new ServiceException("修改字段格式无效"); } Map values = editableValues(action.getDataType(), fields); mergeStepExtension(action.getDataType(), row.get("extension_measurements_json"), fields, values); mutations.add(() -> mapper.updateDataRecord(action.getTestId(), action.getDataType(), id, values)); Map displayBefore = new LinkedHashMap<>(); columns(action.getDataType()).forEach((field, column) -> displayBefore.put(field, row.get(column))); if ("STEP".equals(action.getDataType()) && row.get("extension_measurements_json") != null) { Map extensionBefore = JSON.parseObject(row.get("extension_measurements_json").toString()); STEP_EXTENSION_FIELDS.forEach(field -> displayBefore.put(field, extensionBefore.get(field))); } changes.add(Map.of("identity", identity, "before", displayBefore, "after", fields)); } } } // 修改前内容从数据库读取,不信任浏览器提交的 before;事务失败不会留下孤立修订。 action.setBeforeContentJson(JSON.toJSONString(before)); action.setChangesSummary(JSON.toJSONString("DELETE".equals(action.getOperationType()) ? before : changes)); mapper.insertDataRevisionAction(action); mutations.forEach(Runnable::run); return action.getActionId(); } private long recordId(String type, String identity) { String prefix = switch (type) { case "REALTIME" -> "realtime:"; case "STEP" -> "step:"; default -> "cycle:"; }; try { if (identity == null || !identity.startsWith(prefix)) throw new NumberFormatException(); long id = Long.parseLong(identity.substring(prefix.length())); if (id < 1) throw new NumberFormatException(); return id; } catch (NumberFormatException ex) { throw new ServiceException("数据稳定身份无效"); } } private Map columns(String type) { return switch (type) { case "REALTIME" -> Map.of("voltage", "voltage_v", "current", "current_a", "capacity", "capacity_ah", "energy", "energy_wh", "temperature", "temperature", "positivePotential", "positive_potential", "negativePotential", "negative_potential"); case "STEP" -> Map.of("endVoltage", "voltage_v", "capacity", "capacity_ah", "energy", "energy_wh"); default -> Map.of("chargeCapacity", "charge_capacity_ah", "dischargeCapacity", "discharge_capacity_ah", "chargeEnergy", "charge_energy_wh", "dischargeEnergy", "discharge_energy_wh", "efficiency", "efficiency"); }; } private Map editableValues(String type, Map fields) { if (fields == null || fields.isEmpty()) throw new ServiceException("修改内容不能为空"); Map values = new LinkedHashMap<>(); Map columns = columns(type); fields.forEach((key, value) -> { if ("STEP".equals(type) && STEP_EXTENSION_FIELDS.contains(key)) return; String column = columns.get(key); if (column == null) throw new ServiceException("字段不可修改: " + key); try { values.put(column, value == null ? null : new BigDecimal(value.toString())); } catch (RuntimeException ex) { throw new ServiceException("测量值必须为数值或空值: " + key); } }); return values; } private static final Set STEP_EXTENSION_FIELDS = Set.of("stepDuration", "startVoltage", "chargePeakVoltage", "dischargeValleyVoltage"); private void mergeStepExtension(String type, Object previous, Map fields, Map values) { if (!"STEP".equals(type) || fields.keySet().stream().noneMatch(STEP_EXTENSION_FIELDS::contains)) return; Map extension = previous == null ? new LinkedHashMap<>() : JSON.parseObject(previous.toString()); for (String field : STEP_EXTENSION_FIELDS) { if (!fields.containsKey(field)) continue; Object value = fields.get(field); try { extension.put(field, value == null ? null : new BigDecimal(value.toString())); } catch (RuntimeException ex) { throw new ServiceException("测量值必须为数值或空值: " + field); } } values.put("extension_measurements_json", JSON.toJSONString(extension)); } private Map importedRecord(TTestDataRevisionAction action, Map row) { Map binding = mapper.selectImportBinding(action.getTestId(), String.valueOf(row.get("batteryNo"))); if (binding == null) throw new ServiceException("导入电池号不属于当前测试"); Map values = new LinkedHashMap<>(binding); values.put("test_id", action.getTestId()); values.put("source_identity", "IMPORT"); values.put("source_record_id", String.valueOf(row.get("recordNo"))); Date time; try { Object raw = row.get("recordedAt"); if (raw == null) raw = row.get("deviceTime"); if (!(raw instanceof Number number)) throw new IllegalArgumentException(); time = new Date(number.longValue()); } catch (RuntimeException ex) { throw new ServiceException("导入设备时间必须为毫秒时间戳"); } values.put("server_received_at_utc", action.getCreatedAtUtc()); values.put("cycle_no", row.get("cycleNo")); Map metrics = new LinkedHashMap<>(); columns(action.getDataType()).keySet().forEach(key -> { if (row.containsKey(key)) metrics.put(key, row.get(key)); }); if (!metrics.isEmpty()) values.putAll(editableValues(action.getDataType(), metrics)); mergeStepExtension(action.getDataType(), null, row, values); if (!"CYCLE".equals(action.getDataType())) { values.put("step_no", row.get("stepNo")); values.put("step_name", row.get("stepName")); } if ("REALTIME".equals(action.getDataType())) { values.put("cabinet_sequence_id", row.get("sequenceId")); values.put("capture_slot_utc", time); values.put("cabinet_device_time_utc", time); values.put("cabinet_device_time_raw", time.toInstant().toString()); values.put("cabinet_adapter_timezone_id", "UTC"); } else { values.put("dedupe_key", "import:" + UUID.randomUUID()); values.put("device_time_utc", time); values.put("device_time_raw", time.toInstant().toString()); values.put("adapter_timezone_id", "UTC"); } try { values.put("content_sha256", MessageDigest.getInstance("SHA-256").digest(JSON.toJSONBytes(row))); } catch (Exception ex) { throw new IllegalStateException(ex); } return values; } }