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<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.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<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.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<Map<String, Object>> before = new ArrayList<>();
|
List<Map<String, Object>> changes = new ArrayList<>();
|
List<Runnable> mutations = new ArrayList<>();
|
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("导入数据不能为空");
|
Set<String> identities = new HashSet<>();
|
for (Map<String, Object> row : rows)
|
{
|
String recordNo = row.get("recordNo") == null ? "" : row.get("recordNo").toString();
|
if (recordNo.isBlank() || !identities.add(recordNo)) throw new ServiceException("导入记录编号为空或重复");
|
Map<String, Object> values = importedRecord(action, row);
|
mutations.add(() -> mapper.insertImportedDataRecord(action.getDataType(), values));
|
changes.add(new LinkedHashMap<>(row));
|
}
|
action.setIdentitySummary(JSON.toJSONString(identities));
|
}
|
else
|
{
|
List<String> 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<String, DataRevisionItem> edits = new LinkedHashMap<>();
|
for (DataRevisionItem item : items == null ? List.<DataRevisionItem>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<String, Object> 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<String, Object> fields;
|
try { fields = JSON.parseObject(edits.get(identity).getFieldValues(), Map.class); }
|
catch (RuntimeException ex) { throw new ServiceException("修改字段格式无效"); }
|
Map<String, Object> 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<String, Object> 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<String, Object> 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<String, String> 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<String, Object> editableValues(String type, Map<String, Object> fields)
|
{
|
if (fields == null || fields.isEmpty()) throw new ServiceException("修改内容不能为空");
|
Map<String, Object> values = new LinkedHashMap<>();
|
Map<String, String> 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<String> STEP_EXTENSION_FIELDS = Set.of("stepDuration", "startVoltage", "chargePeakVoltage", "dischargeValleyVoltage");
|
|
private void mergeStepExtension(String type, Object previous, Map<String, Object> fields, Map<String, Object> values)
|
{
|
if (!"STEP".equals(type) || fields.keySet().stream().noneMatch(STEP_EXTENSION_FIELDS::contains)) return;
|
Map<String, Object> 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<String, Object> importedRecord(TTestDataRevisionAction action, Map<String, Object> row)
|
{
|
Map<String, Object> binding = mapper.selectImportBinding(action.getTestId(), String.valueOf(row.get("batteryNo")));
|
if (binding == null) throw new ServiceException("导入电池号不属于当前测试");
|
Map<String, Object> 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<String, Object> 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;
|
}
|
}
|