{
+ /**
+ * 查询最大的任务编号
+ * @return 最大任务编号
+ */
+ String selectMaxTaskCode();
+
}
diff --git a/java/src/main/java/com/yfd/platform/modules/experimentalData/service/ITsFilesService.java b/java/src/main/java/com/yfd/platform/modules/experimentalData/service/ITsFilesService.java
index b797e8e..4caddf6 100644
--- a/java/src/main/java/com/yfd/platform/modules/experimentalData/service/ITsFilesService.java
+++ b/java/src/main/java/com/yfd/platform/modules/experimentalData/service/ITsFilesService.java
@@ -8,10 +8,12 @@ import com.baomidou.mybatisplus.extension.service.IService;
import com.yfd.platform.modules.storage.model.result.FileItemResult;
import org.springframework.web.bind.annotation.RequestParam;
+import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.util.List;
+import java.util.Map;
/**
*
@@ -264,4 +266,18 @@ public interface ITsFilesService extends IService {
* 返回值说明: com.yfd.platform.config.ResponseResult 返回文件集合列表
***********************************/
Object listTsFilesById(String id, String taskId, String nodeId);
+
+ /**
+ * 根据任务ID获取文件列表
+ * @param taskId 任务ID
+ * @return 文件列表
+ */
+ List getByTaskId(String taskId);
+
+ Map splitFile(String id, String taskId, File jsonFile) throws Exception;
+
+ /**
+ * 批量计算并更新 parent_id
+ */
+ int updateParentId(String taskId, String nodeId);
}
diff --git a/java/src/main/java/com/yfd/platform/modules/experimentalData/service/ITsNodesService.java b/java/src/main/java/com/yfd/platform/modules/experimentalData/service/ITsNodesService.java
index 1e864a9..a77363c 100644
--- a/java/src/main/java/com/yfd/platform/modules/experimentalData/service/ITsNodesService.java
+++ b/java/src/main/java/com/yfd/platform/modules/experimentalData/service/ITsNodesService.java
@@ -65,4 +65,12 @@ public interface ITsNodesService extends IService {
* 返回值说明: com.yfd.platform.config.ResponseResult 返回删除成功或者失败
***********************************/
Object confirmDeleteNodes(String id);
+
+ /**
+ * 根据任务ID获取节点列表
+ * @param taskId 任务ID
+ * @return 节点列表
+ */
+ List getByTaskId(String taskId);
+
}
diff --git a/java/src/main/java/com/yfd/platform/modules/experimentalData/service/ITsTaskService.java b/java/src/main/java/com/yfd/platform/modules/experimentalData/service/ITsTaskService.java
index ed9a1ef..b3022dd 100644
--- a/java/src/main/java/com/yfd/platform/modules/experimentalData/service/ITsTaskService.java
+++ b/java/src/main/java/com/yfd/platform/modules/experimentalData/service/ITsTaskService.java
@@ -3,6 +3,7 @@ package com.yfd.platform.modules.experimentalData.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.yfd.platform.modules.experimentalData.domain.TsTask;
import com.baomidou.mybatisplus.extension.service.IService;
+import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.List;
@@ -70,4 +71,27 @@ public interface ITsTaskService extends IService {
List listTsTask();
+ /**
+ * 导出试验任务相关SQL数据为ZIP文件
+ * @param taskId 任务ID
+ * @return ZIP文件字节数组
+ */
+ byte[] exportTaskSqlAsZip(String taskId) throws IOException;
+
+ /**
+ * 从ZIP文件导入试验任务SQL数据
+ * @param file ZIP文件
+ * @param taskCode 任务编号
+ * @param localStorageId 本地存储空间标识
+ * @param backupStorageId 备份空间标识
+ * @return 是否导入成功
+ */
+ boolean importTaskSqlFromZip(MultipartFile file, String taskCode, int localStorageId, int backupStorageId) throws IOException;
+
+ /**
+ * 获取最大任务编号+1
+ * @return 最大任务编号+1
+ */
+ String getMaxTaskCode();
+
}
diff --git a/java/src/main/java/com/yfd/platform/modules/experimentalData/service/InsFileConvertNewService.java b/java/src/main/java/com/yfd/platform/modules/experimentalData/service/InsFileConvertNewService.java
new file mode 100644
index 0000000..ea56f87
--- /dev/null
+++ b/java/src/main/java/com/yfd/platform/modules/experimentalData/service/InsFileConvertNewService.java
@@ -0,0 +1,367 @@
+package com.yfd.platform.modules.experimentalData.service;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.yfd.platform.modules.common.exception.BizException;
+import com.yfd.platform.modules.experimentalData.config.DownsampleConfig;
+import com.yfd.platform.modules.experimentalData.config.InsConvertConfig;
+import com.yfd.platform.modules.experimentalData.config.OutputConfig;
+import com.yfd.platform.modules.experimentalData.config.RuleConfig;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.stereotype.Service;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+
+/**
+ * INS 文件转换服务
+ * - JSON 配置支持 INPUT / CONST 列
+ * - 模板文件决定输出列全集
+ * - 支持默认配置 + 客户自定义配置
+ */
+@Service
+@Slf4j
+public class InsFileConvertNewService {
+ @Value("${app.config-dir}")
+ private String configDir;
+ @Value("${app.templates-dir}")
+ private String templatesDir;
+
+ private static final String CONVERT_FILE_NAME = "ins-convert-default.json";
+ private static final Charset UTF8 = StandardCharsets.UTF_8;
+
+
+ /**
+ * 转换 INS 文件
+ * @param insFile INS 源文件
+ * @param jsonConfigFile 客户自定义 JSON 配置(可为 null,使用默认)
+ * @return Map<输出文件标识, 输出文件对象>
+ */
+ public Map convert(File insFile, File jsonConfigFile) throws Exception {
+
+ long start = System.currentTimeMillis();
+
+ if (!insFile.exists()) {
+ throw new IOException("INS 文件不存在:" + insFile.getAbsolutePath());
+ }
+
+ // 1. 加载配置
+ InsConvertConfig config = loadConfig(jsonConfigFile);
+
+ // 2. 读取 INS 表头
+ String insDelimiter;
+ Map insHeaderIndex;
+ try (BufferedReader insReader = Files.newBufferedReader(insFile.toPath(), UTF8)) {
+ String insHeaderLine = insReader.readLine();
+ if (insHeaderLine == null) {
+ throw new IllegalStateException("INS 文件为空");
+ }
+ //---------- 自动识别分隔符 ----------
+
+ if (insHeaderLine.contains("\t")) {
+ insDelimiter = "\t";
+ } else if (insHeaderLine.contains(",")) {
+ insDelimiter = ",";
+ } else {
+ throw new RuntimeException("未能识别源数据文件分隔符,请确认文件使用逗号(,)或制表符(\\t)分隔。");
+ }
+
+ // 构建列名索引
+ String[] insHeaders = insHeaderLine.split(insDelimiter, -1);
+ Map insIndex = new HashMap<>();
+ for (int i = 0; i < insHeaders.length; i++) {
+ insIndex.put(insHeaders[i].trim(), i);
+ }
+ insHeaderIndex = parseHeaderIndex(insHeaderLine,insDelimiter);
+ }
+
+ // 3. 校验 INS 必需字段
+ validateInsHeader(insHeaderIndex, config);
+
+ // 4. 初始化每个输出文件
+ Map outputs = initOutputs(config.getOutputs(), insFile.getParentFile());
+
+ // ===== 新增:初始化降采样 =====
+ initDownsample(outputs);
+
+ // 5. 流式读取 INS 正文
+ long rowCount = 0;
+ try (BufferedReader insReader = Files.newBufferedReader(insFile.toPath(), UTF8)) {
+ insReader.readLine(); // skip header
+ String line;
+ while ((line = insReader.readLine()) != null) {
+ rowCount++;
+ String[] insValues = line.split(insDelimiter, -1);
+
+ for (OutputContext ctx : outputs.values()) {
+ // ===== 新增:降采样判断 =====
+ if (ctx.sampleStep > 1 && rowCount % ctx.sampleStep != 0) {
+ continue;
+ }
+
+ String[] out = new String[ctx.headers.length];
+ Arrays.fill(out, "0");
+
+ for (Map.Entry e : ctx.rules.entrySet()) {
+ String outCol = e.getKey();
+ RuleConfig rule = e.getValue();
+ int outIdx = ctx.headerIndex.get(outCol);
+
+ if ("INPUT".equalsIgnoreCase(rule.getType())) {
+ int inIdx = insHeaderIndex.get(rule.getFrom());
+ out[outIdx] = inIdx < insValues.length ? insValues[inIdx] : "0";
+ } else if ("CONST".equalsIgnoreCase(rule.getType())) {
+ out[outIdx] = rule.getValue() != null ? rule.getValue() : "0";
+ } else {
+ out[outIdx] = "0"; // 未来扩展 FORMULA
+ }
+ }
+
+ ctx.writer.write(String.join(ctx.delimiter, out));
+ ctx.writer.newLine();
+ }
+ }
+ }
+
+ // 6. 关闭 writer
+ for (OutputContext ctx : outputs.values()) {
+ ctx.writer.flush();
+ ctx.writer.close();
+ }
+
+ log.info("INS 转换完成,行数={}, 耗时={}ms", rowCount, System.currentTimeMillis() - start);
+
+ Map result = new HashMap<>();
+ for (OutputContext ctx : outputs.values()) {
+ result.put(ctx.name, ctx.outFile.getAbsolutePath());
+ }
+ return result;
+ }
+
+ // =================== 内部方法 ===================
+
+ /** 加载 JSON 配置,客户自定义优先,否则默认 */
+ private InsConvertConfig loadConfig(File jsonConfigFile) throws IOException {
+ ObjectMapper mapper = new ObjectMapper();
+ // 1. 显式传入的配置文件(优先级最高)
+ if (jsonConfigFile != null && jsonConfigFile.exists()) {
+ log.info("加载客户自定义 JSON 配置:{}", jsonConfigFile.getAbsolutePath());
+ return mapper.readValue(jsonConfigFile, InsConvertConfig.class);
+ }
+
+ // 2. 尝试 配置文件获取 该方法兼容war部署
+ // 获取项目根目录下的data文件夹路径
+ Path dataDir = Paths.get(configDir);
+ Path filePath = dataDir.resolve(CONVERT_FILE_NAME);
+ if (Files.exists(filePath) && Files.isRegularFile(filePath)) {
+ log.info("加载 JSON 配置文件:{}", filePath.toAbsolutePath());
+ // 直接用 Jackson 从 Path 读取
+ return mapper.readValue(filePath.toFile(), InsConvertConfig.class);
+ }
+
+ // 3. 回退到 jar 内 classpath
+ log.info("加载内置默认 JSON 配置:classpath:config/ins-convert-default.json");
+ ClassPathResource res = new ClassPathResource("config/ins-convert-default.json");
+ try (InputStreamReader in = new InputStreamReader(res.getInputStream(), UTF8)) {
+ return mapper.readValue(in, InsConvertConfig.class);
+ }
+ }
+
+ /** 解析表头到 Map<列名,索引> */
+ private Map parseHeaderIndex(String headerLine,String delimiter) {
+ String[] headers = headerLine.split(delimiter, -1);
+ Map map = new HashMap<>();
+ for (int i = 0; i < headers.length; i++) {
+ map.put(headers[i].trim(), i);
+ }
+ return map;
+ }
+
+ /** 用","解析输出表头到 Map<列名,索引> */
+ private Map parseOutHeaderIndex(String headerLine,String delimiter) {
+ String[] headers = headerLine.split(delimiter, -1);
+ Map map = new HashMap<>();
+ for (int i = 0; i < headers.length; i++) {
+ map.put(headers[i].trim(), i);
+ }
+ return map;
+ }
+
+ /** 校验 INS 必需列 */
+ private void validateInsHeader(Map insHeader, InsConvertConfig config) {
+ Set required = new HashSet<>();
+ for (OutputConfig out : config.getOutputs()) {
+ for (RuleConfig rule : out.getRules().values()) {
+ if ("INPUT".equalsIgnoreCase(rule.getType())) {
+ required.add(rule.getFrom());
+ }
+ }
+ }
+
+ for (String col : required) {
+ if (!insHeader.containsKey(col)) {
+ throw new BizException("源数据文件 缺少必须字段:" + col);
+ }
+ }
+ }
+
+ /** 初始化输出文件流和上下文 */
+ private Map initOutputs(
+ List outputs, File parentDir) throws Exception {
+
+ Map map = new HashMap<>();
+ for (OutputConfig out : outputs) {
+
+ // 1.读取模板文件表头
+// ClassPathResource tpl = new ClassPathResource(out.getTemplateResource());
+// String headerLine;
+// try (BufferedReader r = new BufferedReader(new InputStreamReader(tpl.getInputStream(), UTF8))) {
+// headerLine = r.readLine();
+// }
+ String headerLine;
+ try (BufferedReader r = openTemplateReader(out.getTemplateResource())) {
+ headerLine = r.readLine();
+ }
+
+
+ if (headerLine == null || headerLine.isEmpty()) {
+ throw new RuntimeException("模板文件为空:" + out.getTemplateResource());
+ }
+
+ String delimiter = out.getDelimiter() != null ? out.getDelimiter() : ",";
+ String[] headers = headerLine.split(delimiter, -1);
+ Map headerIndex = parseOutHeaderIndex(headerLine,delimiter);
+
+ // ---------- 2. 输出规则列校验 ----------
+ for (String col : out.getRules().keySet()) {
+ if (!headerIndex.containsKey(col)) {
+ throw new BizException(
+ String.format(
+ "输出模板文件 [%s] 中未找到规则列 [%s]。",
+ out.getTemplateResource(),
+ col
+ ));
+ }
+ }
+
+ // --------- 3. 构造输出文件 ---------
+ String ts = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
+ String ext = out.getExtension() != null ? out.getExtension() : ".txt";
+ File outFile = new File(parentDir, out.getName() + "_" + ts + ext);
+
+ BufferedWriter writer = Files.newBufferedWriter(outFile.toPath(), UTF8);
+
+ // ------- 4. 写入模板表头 ----------
+ writer.write(headerLine);
+ writer.newLine();
+
+ // ---------- 5. 初始化上下文 ----------
+ OutputContext ctx = new OutputContext();
+ ctx.name = out.getName();
+ ctx.headers = headers;
+ ctx.headerIndex = headerIndex;
+ ctx.rules = out.getRules();
+ ctx.writer = writer;
+ ctx.delimiter = out.getDelimiter();
+ ctx.outFile = outFile;
+
+ // ===== 新增:绑定降采样配置 =====
+ ctx.downsample = out.getDownsample();
+
+ map.put(ctx.name, ctx);
+ }
+ return map;
+ }
+
+ private BufferedReader openTemplateReader(String templatePath) throws IOException {
+
+ // 1. jar 、war外部加载
+ // 01. 仅截取文件名(兼容 templates/xxx.txt、/templates/xxx.txt)
+ String fileName = Paths.get(templatePath).getFileName().toString();
+ // 02. 拼接外部 templatesDir
+ Path externalPath = Paths.get(templatesDir).resolve(fileName);
+ // 03. 校验并读取
+ if (Files.exists(externalPath) && Files.isRegularFile(externalPath)) {
+ log.info("加载外部模板文件:{}", externalPath.toAbsolutePath());
+ return Files.newBufferedReader(externalPath, UTF8);
+ }
+// File external = new File(templatePath);
+// if (external.exists()) {
+// log.info("加载 jar 同级模板文件:{}", external.getAbsolutePath());
+// return Files.newBufferedReader(external.toPath(), UTF8);
+// }
+
+ // 2. classpath 内模板
+ log.info("加载内置模板文件:classpath:{}", templatePath);
+ ClassPathResource res = new ClassPathResource(templatePath);
+ return new BufferedReader(new InputStreamReader(res.getInputStream(), UTF8));
+ }
+
+
+ // ===== 新增:初始化降采样步长 =====
+ private void initDownsample(Map outputs) {
+ for (OutputContext ctx : outputs.values()) {
+ DownsampleConfig ds = ctx.downsample;
+ if (ds != null && ds.isEnabled()) {
+
+ if (ds.getOutputInterval() <= 0 || ds.getInputDelta() <= 0) {
+ throw new BizException(
+ String.format("错误:输出 [%s] 的降采样(inputDelta/outputInterval)配置无效。", ctx.name)
+ );
+ }
+
+ double ratio = ds.getOutputInterval() / ds.getInputDelta();
+ int step = (int) Math.round(ratio);
+
+ ctx.sampleStep = step <= 1 ? 1 : step;
+ }
+ }
+ }
+
+
+ /** 输出上下文 */
+ private static class OutputContext {
+ String name;
+ String[] headers;
+ Map rules;
+ Map headerIndex;
+ BufferedWriter writer;
+ String delimiter;
+ File outFile;
+
+ // ===== 新增(最小)=====
+ DownsampleConfig downsample;
+ int sampleStep = 1; // 默认不降采样
+ }
+
+ // =================== 测试 main ===================
+ public static void main(String[] args) {
+ try {
+ InsFileConvertNewService service = new InsFileConvertNewService();
+
+ File insFile = new File("D:/data/test.txt"); // 修改为实际路径
+ File jsonFile = new File("D:/data/ins-convert-2.json");
+
+ // File jsonFile = new File("D:/data/customer-ins-convert.json"); // 客户自定义 JSON
+ Map result = service.convert(insFile,null); // 使用默认配置
+
+ result.forEach((k, f) -> System.out.println(k + " -> " + f));
+ System.out.println("转换完成!");
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/java/src/main/java/com/yfd/platform/modules/experimentalData/service/InsFileConvertService.java b/java/src/main/java/com/yfd/platform/modules/experimentalData/service/InsFileConvertService.java
new file mode 100644
index 0000000..a65753c
--- /dev/null
+++ b/java/src/main/java/com/yfd/platform/modules/experimentalData/service/InsFileConvertService.java
@@ -0,0 +1,228 @@
+package com.yfd.platform.modules.experimentalData.service;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.stereotype.Service;
+
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+
+@Service
+public class InsFileConvertService {
+
+ private static final Logger log =
+ LoggerFactory.getLogger(InsFileConvertService.class);
+
+ /**
+ * INS 转换通用方法
+ * INS 数据 → VINS/FVNS 文件
+ */
+ public Map convertByTemplate(File insFile) throws Exception {
+
+ long startTime = System.currentTimeMillis();
+
+ if (!insFile.exists()) {
+ throw new FileNotFoundException("INS 文件不存在:" + insFile.getAbsolutePath());
+ }
+
+ log.info("INS 转换开始,源文件:{}", insFile.getAbsolutePath());
+
+ // ---------- 1. 时间戳 ----------
+ String ts = LocalDateTime.now()
+ .format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
+
+ Path outDir = insFile.getParentFile().toPath();
+
+ File vinsOut = outDir.resolve("VINS_0_mode1_" + ts + ".txt").toFile();
+ File fvnsOut = outDir.resolve("FVNS_" + ts + ".csv").toFile();
+
+ // ---------- 2. 模板 ----------
+ ClassPathResource vinsTpl =
+ new ClassPathResource("templates/VINS_0_mode1.txt");
+ ClassPathResource fvnsTpl =
+ new ClassPathResource("templates/FVNS.csv");
+
+ // ---------- 3. 流 ----------
+ try (
+ BufferedReader insReader = new BufferedReader(
+ new InputStreamReader(new FileInputStream(insFile), StandardCharsets.UTF_8));
+
+ BufferedReader vinsTplReader = new BufferedReader(
+ new InputStreamReader(vinsTpl.getInputStream(), StandardCharsets.UTF_8));
+
+ BufferedReader fvnsTplReader = new BufferedReader(
+ new InputStreamReader(fvnsTpl.getInputStream(), StandardCharsets.UTF_8));
+
+ BufferedWriter vinsWriter = new BufferedWriter(
+ new OutputStreamWriter(new FileOutputStream(vinsOut), StandardCharsets.UTF_8));
+
+ BufferedWriter fvnsWriter = new BufferedWriter(
+ new OutputStreamWriter(new FileOutputStream(fvnsOut), StandardCharsets.UTF_8))
+ ) {
+
+ // ---------- 4. INS 表头 ----------
+ String insHeaderLine = insReader.readLine();
+ if (insHeaderLine == null) {
+ throw new RuntimeException("INS 文件为空");
+ }
+
+ Map insIndex = buildIndex(insHeaderLine);
+
+ // ---------- 5. 表头校验(关键增强) ----------
+ validateInsHeader(insIndex);
+
+ // ---------- 6. 模板表头 ----------
+ String vinsHeader = vinsTplReader.readLine();
+ String fvnsHeader = fvnsTplReader.readLine();
+
+ if (vinsHeader == null || fvnsHeader == null) {
+ throw new RuntimeException("模板文件缺少表头");
+ }
+
+ vinsWriter.write(vinsHeader);
+ vinsWriter.newLine();
+
+ fvnsWriter.write(fvnsHeader);
+ fvnsWriter.newLine();
+
+ String[] vinsCols = vinsHeader.split(",");
+ String[] fvnsCols = fvnsHeader.split(",");
+
+ // ---------- 7. 映射规则 ----------
+ Map vinsMapping = new HashMap<>();
+ vinsMapping.put("UTC", "utc");
+ vinsMapping.put("LatGps", "LatJG");
+ vinsMapping.put("LonGps", "LonJG");
+
+ Map fvnsMapping = new HashMap<>();
+ fvnsMapping.put("FvnsSts", "posx");
+ fvnsMapping.put("PosxFvns", "posy");
+ fvnsMapping.put("PosyFvns", "posz");
+
+
+ // ---------- 8. 流式处理 ----------
+ String line;
+ long rowCount = 0;
+
+ while ((line = insReader.readLine()) != null) {
+
+ String[] insRow = line.split("[,\t]", -1);
+
+ writeLine(vinsWriter, vinsCols, vinsMapping, insIndex, insRow, ",");
+ writeLine(fvnsWriter, fvnsCols, fvnsMapping, insIndex, insRow, ",");
+
+ rowCount++;
+ }
+
+ vinsWriter.flush();
+ fvnsWriter.flush();
+
+ long cost = System.currentTimeMillis() - startTime;
+
+ log.info(
+ "INS 转换完成,行数:{},耗时:{} ms,输出文件:{}, {}",
+ rowCount,
+ cost,
+ vinsOut.getName(),
+ fvnsOut.getName()
+ );
+ }
+ // 最后返回 Map:
+ Map out = new HashMap<>();
+ out.put("vins", vinsOut.getAbsolutePath());
+ out.put("fvns", fvnsOut.getAbsolutePath());
+ return out;
+ }
+
+ // ================== 校验与工具方法 ==================
+
+ /**
+ * INS 必须字段校验
+ */
+ private void validateInsHeader(Map insIndex) {
+
+ List required = Arrays.asList(
+ "utc",
+ "LatJG",
+ "LonJG",
+ "posx",
+ "posy",
+ "posz"
+ );
+
+
+ List missing = new ArrayList<>();
+
+ for (String field : required) {
+ if (!insIndex.containsKey(field)) {
+ missing.add(field);
+ }
+ }
+
+ if (!missing.isEmpty()) {
+ throw new RuntimeException(
+ "INS 文件缺少必要字段:" + String.join(", ", missing)
+ );
+ }
+ }
+
+ private Map buildIndex(String headerLine) {
+ String[] headers = headerLine.split("[,\t]");
+ Map map = new HashMap<>();
+ for (int i = 0; i < headers.length; i++) {
+ map.put(headers[i].trim(), i);
+ }
+ return map;
+ }
+
+ private void writeLine(BufferedWriter writer,
+ String[] targetCols,
+ Map mapping,
+ Map insIndex,
+ String[] insRow,
+ String delimiter) throws IOException {
+
+ String[] out = new String[targetCols.length];
+ Arrays.fill(out, "0");
+
+ for (int i = 0; i < targetCols.length; i++) {
+ String targetCol = targetCols[i].trim();
+ String insCol = mapping.get(targetCol);
+ if (insCol != null) {
+ Integer idx = insIndex.get(insCol);
+ if (idx != null && idx < insRow.length) {
+ out[i] = insRow[idx];
+ }
+ }
+ }
+
+ writer.write(String.join(delimiter, out));
+ writer.newLine();
+ }
+
+ public static void main(String[] args) {
+
+ try {
+ // ---------- 1. 构造待测 Service ----------
+ InsFileConvertService service = new InsFileConvertService();
+
+ // ---------- 2. 指定 INS 文件路径 ----------
+ File insFile = new File("D:/data/ins_frameSimu_1.txt");
+ // Linux 示例:
+ // File insFile = new File("/data/ins/ins_frameSimu_0.txt");
+
+ // ---------- 3. 执行转换 ----------
+ service.convertByTemplate(insFile);
+
+ System.out.println("转换完成,请检查输出文件。");
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/java/src/main/java/com/yfd/platform/modules/experimentalData/service/InsMergeService.java b/java/src/main/java/com/yfd/platform/modules/experimentalData/service/InsMergeService.java
new file mode 100644
index 0000000..1463eb3
--- /dev/null
+++ b/java/src/main/java/com/yfd/platform/modules/experimentalData/service/InsMergeService.java
@@ -0,0 +1,154 @@
+package com.yfd.platform.modules.experimentalData.service;
+
+import com.yfd.platform.utils.InterpCursor;
+import org.springframework.stereotype.Service;
+
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+
+@Service
+public class InsMergeService {
+
+ public File merge(File vinsFile, File fvnsFile, File templateFileOptional) throws Exception {
+
+ // ---------- 1. 模板表头 ----------
+ String[] headers = loadTemplateHeaders(templateFileOptional);
+// try (BufferedReader br = new BufferedReader(new FileReader(templateFile))) {
+// headers = br.readLine().split("\t");
+// }
+
+ // ---------- 2. 读取 FVNS(一次性,较小) ----------
+ List tList = new ArrayList<>();
+ List sList = new ArrayList<>();
+ List xList = new ArrayList<>();
+ List yList = new ArrayList<>();
+
+ try (BufferedReader br = new BufferedReader(new FileReader(fvnsFile))) {
+ String[] head = br.readLine().split(",", -1);
+ Map idx = indexMap(head);
+
+ String line;
+ while ((line = br.readLine()) != null) {
+ String[] a = line.split(",",-1);
+ tList.add(Double.parseDouble(a[idx.get("UTC")]));
+ sList.add(Double.parseDouble(a[idx.get("FvnsSts")]));
+ xList.add(Double.parseDouble(a[idx.get("PosxFvns")]));
+ yList.add(Double.parseDouble(a[idx.get("PosyFvns")]));
+ }
+ }
+ // 检查时间序列,在读 FVNS 后,只加一次校验即可
+ for (int i = 1; i < tList.size(); i++) {
+ if (tList.get(i) < tList.get(i - 1)) {
+ throw new IllegalStateException("FVNS UTC not sorted at line " + i);
+ }
+ }
+
+ InterpCursor cPosX = new InterpCursor(toArr(tList), toArr(sList));
+ InterpCursor cPosY = new InterpCursor(toArr(tList), toArr(xList));
+ InterpCursor cPosZ = new InterpCursor(toArr(tList), toArr(yList));
+
+ // ---------- 3. 输出文件 ----------
+ File out = File.createTempFile("ins_frameSimu_0_out", ".txt");
+
+ try (
+ BufferedReader br = new BufferedReader(new FileReader(vinsFile));
+ BufferedWriter bw = new BufferedWriter(
+ new OutputStreamWriter(new FileOutputStream(out), StandardCharsets.UTF_8), 1 << 20)
+ ) {
+ // VINS header
+ String[] vinsHead = br.readLine().split(",",-1);
+ Map vinsIdx = indexMap(vinsHead);
+ requireField(vinsIdx, "UTC");
+ requireField(vinsIdx, "LatGps");
+ requireField(vinsIdx, "LonGps");
+
+
+ // 写表头
+ bw.write(String.join("\t", headers));
+ bw.newLine();
+
+ String line;
+ while ((line = br.readLine()) != null) {
+ String[] a = line.split(",",-1);
+
+ double utc = Double.parseDouble(a[vinsIdx.get("UTC")]);
+ double lat = Double.parseDouble(a[vinsIdx.get("LatGps")]);
+ double lon = Double.parseDouble(a[vinsIdx.get("LonGps")]);
+
+ double posx = cPosX.valueAt(utc);
+ double posy = cPosY.valueAt(utc);
+ double posz = cPosZ.valueAt(utc);
+
+ // 按模板顺序输出
+ for (int i = 0; i < headers.length; i++) {
+ switch (headers[i]) {
+ case "utc": bw.write(fmt(utc)); break;
+ case "LatJG": bw.write(fmt(lat)); break;
+ case "LonJG": bw.write(fmt(lon)); break;
+ case "posx": bw.write(fmt(posx)); break;
+ case "posy": bw.write(fmt(posy)); break;
+ case "posz": bw.write(fmt(posz)); break;
+ default: bw.write("0"); break;
+ }
+ if (i < headers.length - 1) bw.write("\t");
+ }
+ bw.newLine();
+ }
+ }
+
+ return out;
+ }
+
+ // ---------- utils ----------
+
+ private Map indexMap(String[] head) {
+ Map m = new HashMap<>();
+ for (int i = 0; i < head.length; i++) {
+ m.put(head[i].trim(), i);
+ }
+ return m;
+ }
+
+ private void requireField(Map idx, String name) {
+ if (!idx.containsKey(name)) {
+ throw new IllegalArgumentException("Missing required field: " + name);
+ }
+ }
+
+
+ private double[] toArr(List list) {
+ double[] a = new double[list.size()];
+ for (int i = 0; i < a.length; i++) a[i] = list.get(i);
+ return a;
+ }
+
+ private String fmt(double v) {
+ return String.format(Locale.US, "%.6f", v);
+ }
+
+ private String[] loadTemplateHeaders(File templateFile) throws IOException {
+
+ // 1️⃣ 若用户上传模板
+ if (templateFile != null && templateFile.exists()) {
+ try (BufferedReader br = new BufferedReader(new FileReader(templateFile))) {
+ return br.readLine().split("\t");
+ }
+ }
+
+ // 2️⃣ 使用内置模板
+ try (InputStream is = getClass()
+ .getClassLoader()
+ .getResourceAsStream("templates/ins_frameSimu_0.txt")) {
+
+ if (is == null) {
+ throw new RuntimeException("Default template ins_frameSimu_0.txt not found");
+ }
+
+ try (BufferedReader br = new BufferedReader(new InputStreamReader(is))) {
+ return br.readLine().split("\t");
+ }
+ }
+ }
+
+}
diff --git a/java/src/main/java/com/yfd/platform/modules/experimentalData/service/impl/TsFilesServiceImpl.java b/java/src/main/java/com/yfd/platform/modules/experimentalData/service/impl/TsFilesServiceImpl.java
index 39c680e..f868593 100644
--- a/java/src/main/java/com/yfd/platform/modules/experimentalData/service/impl/TsFilesServiceImpl.java
+++ b/java/src/main/java/com/yfd/platform/modules/experimentalData/service/impl/TsFilesServiceImpl.java
@@ -35,6 +35,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.opencsv.CSVReader;
import com.opencsv.CSVWriter;
import com.opencsv.exceptions.CsvValidationException;
+import com.qiniu.storage.model.FileInfo;
import com.yfd.platform.component.ExtractTaskStatus;
import com.yfd.platform.component.ServerSendEventServer;
import com.yfd.platform.component.TaskStatusHolder;
@@ -45,6 +46,8 @@ import com.yfd.platform.modules.experimentalData.mapper.TsFilesMapper;
import com.yfd.platform.modules.experimentalData.mapper.TsTaskMapper;
import com.yfd.platform.modules.experimentalData.service.ITsFilesService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.yfd.platform.modules.experimentalData.service.InsFileConvertNewService;
+import com.yfd.platform.modules.experimentalData.service.InsFileConvertService;
import com.yfd.platform.modules.storage.context.StorageSourceContext;
import com.yfd.platform.modules.storage.mapper.StorageSourceConfigMapper;
import com.yfd.platform.modules.storage.mapper.StorageSourceMapper;
@@ -77,6 +80,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -138,10 +142,18 @@ public class TsFilesServiceImpl extends ServiceImpl impl
@Autowired
private ExtractTaskStatus extractTaskStatus;
+ @Autowired
+ private InsFileConvertService insFileConvertService;
+
+ @Autowired
+ private InsFileConvertNewService insFileConvertNewService;
+
@Autowired
private RedisTemplate redisTemplate;
private TaskMessage taskMessage;
+ private static final int BATCH_SIZE = 5000;
+
/**********************************
* 用途说明: 分页查询试验数据管理-文档内容
* 参数说明
@@ -253,6 +265,8 @@ public class TsFilesServiceImpl extends ServiceImpl impl
FileItemResult fileItemResult = fileService.getFileItem(path);
if (fileItemResult == null || fileItemResult.getName() == null) {
LOGGER.error("{}文件没有上传到工作空间,请重新选择上传", fileNameData);
+ // 跳过处理这个文件,避免空指针异常
+ continue;
}
tsFiles.setUrl(fileItemResult.getUrl());
//如果是压缩文件 类型就给zip
@@ -260,7 +274,12 @@ public class TsFilesServiceImpl extends ServiceImpl impl
if (isValid) {
tsFiles.setType("ZIP");
} else {
- tsFiles.setType(fileItemResult.getType().getValue());
+ // 添加空值检查,防止空指针异常
+ if (fileItemResult.getType() != null) {
+ tsFiles.setType(fileItemResult.getType().getValue());
+ } else {
+ tsFiles.setType("UNKNOWN");
+ }
}
if (tsFiles.getUpdateTime() == null) {
tsFiles.setUpdateTime(tsFiles.getUploadTime());
@@ -468,9 +487,23 @@ public class TsFilesServiceImpl extends ServiceImpl impl
List names = Arrays.asList(tsFiles.getFileName().split(","));
List sizes = Arrays.asList(tsFiles.getFileSize().split(","));
// 获取当前登录用户
- UsernamePasswordAuthenticationToken authentication =
- (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
- LoginUser loginuser = (LoginUser) authentication.getPrincipal();
+// UsernamePasswordAuthenticationToken authentication =
+// (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
+// LoginUser loginuser = (LoginUser) authentication.getPrincipal();
+
+ // ===== 安全获取当前登录用户 =====
+ Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+ LoginUser loginUser;
+ if (authentication instanceof UsernamePasswordAuthenticationToken) {
+ UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication;
+ loginUser = (LoginUser) token.getPrincipal();
+
+ // 这里继续你的业务逻辑
+ } else {
+ // 匿名访问处理:可以抛异常或记录默认值
+ throw new IllegalStateException("未登录用户无法新增试验数据");
+ }
+
// 数据校验
if (names.size() != sizes.size()) {
@@ -516,7 +549,7 @@ public class TsFilesServiceImpl extends ServiceImpl impl
files1.setKeywords(tsFiles.getKeywords());
files1.setDescription(tsFiles.getDescription());
files1.setUploadTime(tsFiles.getUploadTime());
- files1.setUploader(loginuser.getUsername());
+ files1.setUploader(loginUser.getUsername());
files1.setFileName(name);
files1.setFileSize("0.000".equals(sizeStr) ? "0.001" : sizeStr);
@@ -5422,20 +5455,40 @@ public class TsFilesServiceImpl extends ServiceImpl impl
//判断文件后缀是.txt还是.csv
String fileName = tsFiles.getFileName();
- TsFiles tsFilesData = tsFilesMapper.selectById(configId);
+// TsFiles tsFilesData = tsFilesMapper.selectById(configId);
+
+ // 根据文件后缀确定配置文件路径
+ String configResourcePath;
+ String Separator;
+ if (fileName.toLowerCase().endsWith(".csv")) {
+
+ configResourcePath = "config/trj_config.txt";
+ Separator = ",";
+ } else if (fileName.toLowerCase().endsWith(".txt")) {
+ configResourcePath = "config/trj_config_ins_img.txt";
+ Separator = "\t";
+ } else {
+ Separator = "";
+ throw new IllegalArgumentException("不支持的文件格式: " + fileName + ",仅支持.csv和.txt文件");
+ }
+
+
if (currentTaskFuture != null && !currentTaskFuture.isDone()) {
currentTaskFuture.cancel(true);
}
currentTaskFuture = executorService.submit(() -> {
try {
- // 1. 获取配置文件路径
- StorageSourceConfig config = getStorageSourceConfig("filePath", "local", tsTask.getLocalStorageId());
- Path basePath = Paths.get(config.getValue() + tsFilesData.getWorkPath());
+// // 1. 获取配置文件路径
+// StorageSourceConfig config = getStorageSourceConfig("filePath", "local", tsTask.getLocalStorageId());
+// Path basePath = Paths.get(config.getValue() + tsFilesData.getWorkPath());
+//
+// // 2. 读取配置文件(假设配置文件名为 trj_config.txt)
+// Path configPath = basePath.resolve(tsFilesData.getFileName());
+// Map columnMapping = parseConfigFile(configPath, token);
- // 2. 读取配置文件(假设配置文件名为 trj_config.txt)
- Path configPath = basePath.resolve(tsFilesData.getFileName());
- Map columnMapping = parseConfigFile(configPath, token);
+ // 1. 从资源文件读取配置文件
+ Map columnMapping = parseConfigFromResource(configResourcePath, token);
String timeColumn = columnMapping.get("time");
String lonColumn = columnMapping.get("lon");
@@ -5446,10 +5499,12 @@ public class TsFilesServiceImpl extends ServiceImpl impl
timeColumn, lonColumn, latColumn, hgtColumn);
// 3. 获取数据文件路径
+ StorageSourceConfig config = getStorageSourceConfig("filePath", "local", tsTask.getLocalStorageId());
+ Path basePath = Paths.get(config.getValue() + tsFiles.getWorkPath());
Path dataPath = basePath.resolve(tsFiles.getFileName());
// 4. 处理数据文件
- processDataFile(dataPath, samTimes, token, timeColumn, lonColumn, latColumn, hgtColumn);
+ processDataFile(dataPath, samTimes, token, timeColumn, lonColumn, latColumn, hgtColumn,Separator);
} catch (Exception e) {
LOGGER.error("任务执行失败: {}", e.getMessage(), e);
@@ -5465,6 +5520,64 @@ public class TsFilesServiceImpl extends ServiceImpl impl
}
}
+ /**
+ * 从资源文件读取配置文件并解析为列名映射
+ */
+ public Map parseConfigFromResource(String resourcePath, String token) throws IOException {
+ Map columnMapping = new HashMap<>();
+
+ // 使用ClassLoader读取资源文件
+ ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
+ try (InputStream inputStream = classLoader.getResourceAsStream(resourcePath);
+ BufferedReader reader = inputStream != null ?
+ new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)) : null) {
+
+ if (reader == null) {
+ throw new IOException("找不到配置文件: " + resourcePath);
+ }
+
+ String line;
+ while ((line = reader.readLine()) != null) {
+ line = line.trim();
+ if (line.isEmpty() || line.startsWith("#")) {
+ continue;
+ }
+
+ // 分割键值对,支持=和:分隔符
+ String[] parts = line.split("[=:]", 2);
+ if (parts.length < 2) {
+ LOGGER.warn("忽略无效行: {}", line);
+ continue;
+ }
+
+ String key = parts[0].trim().toLowerCase();
+ String value = parts[1].trim();
+
+ // 清理引号
+ if (value.startsWith("\"") && value.endsWith("\"")) {
+ value = value.substring(1, value.length() - 1);
+ }
+
+ // 只处理需要的键
+ if (key.equals("time") || key.equals("lon") ||
+ key.equals("lat") || key.equals("hgt")) {
+ columnMapping.put(key, value);
+ }
+ }
+ }
+
+ // 验证是否获取到所有必需的列
+ if (columnMapping.size() < 4) {
+ JSONObject jsonResponse = new JSONObject();
+ jsonResponse.put("message", "配置文件选择错误,请重新选择!");
+
+ ServerSendEventServer.sendMessageById(token, jsonResponse.toString());
+ throw new IOException("配置文件缺少必需列");
+ }
+
+ return columnMapping;
+ }
+
// // 封装发送数据的逻辑
// private void sendData(String token, String[] values, int lineCount) {
@@ -5557,7 +5670,7 @@ public class TsFilesServiceImpl extends ServiceImpl impl
private void processDataFile(Path dataPath, int samTimes, String token,
String timeColumn, String lonColumn,
- String latColumn, String hgtColumn)
+ String latColumn, String hgtColumn,String Separator)
throws IOException {
try (BufferedReader reader = Files.newBufferedReader(dataPath, StandardCharsets.UTF_8)) {
@@ -5641,32 +5754,50 @@ public class TsFilesServiceImpl extends ServiceImpl impl
int sendCount = 0;
int lineCountData = 0;
- float timeValue0 = 0;
- float timeValue1 = 0;
+ double timeValue0 = 0;
+ double timeValue1 = 0;
int result = 0; //
- while (!Thread.currentThread().isInterrupted() && (line = reader.readLine()) != null) {
- lineCountData++;
- if (lineCountData > 2) {
- break; // 跳出循环,停止读取数据
+ while (!Thread.currentThread().isInterrupted() ) {
+ String line1 = "";
+ String line2 = "";
+ if((line = reader.readLine()) != null)
+ line1 = line;
+ if ((line = reader.readLine()) != null) {
+ line2 = line;
}
+ System.out.println("正在处理数据行: {}" +line1);
+ System.out.println("正在处理数据行: {}" +line2);
// 使用逗号分隔数据行
- String[] values = line.split(","); // 改为逗号分隔
- if (lineCountData == 1) {
- // 读取第一行的时间值
- timeValue0 = Float.parseFloat(getValueSafely(values, timeIndex, "0.0")); // 将字符串转换为数字
- } else if (lineCountData == 2) {
- // 读取第二行的时间值
- timeValue1 = Float.parseFloat(getValueSafely(values, timeIndex, "0.0")); // 将字符串转换为数字
- // 计算 timeValue1 - timeValue0
- float data = timeValue1 - timeValue0;
- // 计算 1 / data
- if (data != 0) { // 确保避免除以零
- result = (int) Math.floor(1 / data);
- } else {
- result = 1;
- }
+ String[] values = line1.split(Separator); // 改为逗号分隔
+ timeValue0 = Float.parseFloat(values[timeIndex]);//Float.parseFloat(getValueSafely(values, timeIndex, "0.0"));
+ String[] values2 = line2.split(Separator); // 改为逗号分隔
+ timeValue1 = Float.parseFloat(values2[timeIndex]);//Float.parseFloat(getValueSafely(values2, timeIndex, "0.0"));
+
+ double data = timeValue1 - timeValue0;
+ // 计算 1 / data
+ if (Math.abs(data) > 0.0000001) {
+ // 四舍五入,而不是向下取整
+ result = (int) Math.round(1.0 / data);
+ break;
+ }else {
+ result = 1;
}
+// if (lineCountData == 1) {
+// // 读取第一行的时间值
+// timeValue0 = Float.parseFloat(getValueSafely(values, timeIndex, "0.0")); // 将字符串转换为数字
+// } else if (lineCountData == 2) {
+// // 读取第二行的时间值
+// timeValue1 = Float.parseFloat(getValueSafely(values, timeIndex, "0.0")); // 将字符串转换为数字
+// // 计算 timeValue1 - timeValue0
+// float data = timeValue1 - timeValue0;
+// // 计算 1 / data
+// if (data != 0) { // 确保避免除以零
+// result = (int) Math.floor(1 / data);
+// } else {
+// result = 1;
+// }
+// }
}
@@ -6275,6 +6406,105 @@ public class TsFilesServiceImpl extends ServiceImpl impl
return String.format("%s,%s,%s,%s", parts[0], parts[1], parts[2], parts[3]);
}
+ @Override
+ public List getByTaskId(String taskId) {
+ LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
+ queryWrapper.eq(TsFiles::getTaskId, taskId);
+ return this.list(queryWrapper);
+ }
+
+ /**
+ * 拆分文件接口
+ * @param id
+ */
+ @Override
+ public Map splitFile(String id,String taskId,File jsonFile) throws Exception {
+ //1:动态表名 以及通过ID查询tsfiles 然后根据id path taskId nodeid 等等条件查询所欲的集合
+ Map resultMap = new HashMap<>();
+ try {
+ TsTask tsTask = tsTaskMapper.selectById(taskId);
+ TableNameContextHolder.setTaskCode(tsTask.getTaskCode());
+ StorageSource storageSource = getStorageConfig(tsTask.getLocalStorageId());
+ TsFiles tsFile = tsFilesMapper.selectById(id);
+ if (tsFile == null) {
+ throw new RuntimeException("文件不存在: " + id);
+ }
+ StorageSourceConfig config = getStorageSourceConfig("filePath", "local", tsTask.getLocalStorageId());
+
+
+ // 2. 构建源文件路径
+ Path sourcePath = Paths.get(config.getValue(), tsFile.getWorkPath(), tsFile.getFileName()).normalize();
+ File sourceFile = sourcePath.toFile();
+ if (!sourceFile.exists()) {
+ throw new FileNotFoundException("文件不存在: " + sourcePath);
+ }
+
+ // 3. 根据规则拆分文件
+ Map outputPaths = insFileConvertNewService.convert(sourceFile, jsonFile);
+
+ // 3. 生成TsFiles对象集合
+
+ LocalDateTime now = LocalDateTime.now();
+
+ // 遍历所有输出文件
+ for (Map.Entry entry : outputPaths.entrySet()) {
+ String outputName = entry.getKey(); // 如 vins、fvns
+ String path = entry.getValue();
+ File outFile = new File(path);
+
+ TsFiles ts = new TsFiles();
+ ts.setNodeId(tsFile.getNodeId());
+ ts.setTaskId(tsFile.getTaskId());
+ ts.setIsFile("FILE");
+ ts.setParentId(tsFile.getParentId());
+ ts.setFileName(outFile.getName());
+ ts.setFileSize(String.valueOf(outFile.length() / (1024.0 * 1024.0))); // 转 M
+ ts.setWorkPath(tsFile.getWorkPath());
+ this.addTsFiles(ts);
+
+ resultMap.put(outputName, ts);
+ }
+ } finally {
+ TableNameContextHolder.clear(); // 确保清理资源
+ }
+
+ return resultMap;
+
+ }
+
+ /**
+ * 批量计算并更新 parent_id
+ */
+ public int updateParentId(String taskId, String nodeId) {
+ // 1. 查询所有文件和文件夹
+ List allFiles = tsFilesMapper.selectByTaskAndNode(taskId, nodeId);
+
+ // 2. 构建父路径 -> id 映射
+ Map pathToId = allFiles.stream()
+ .collect(Collectors.toMap(
+ f -> f.getWorkPath() + f.getFileName() + "/", // 父路径
+ TsFiles::getId,
+ (existing, replacement) -> existing // 避免重复 key
+ ));
+
+ // 3. 设置 parent_id
+ for (TsFiles f : allFiles) {
+ String parentId = pathToId.getOrDefault(f.getWorkPath(), "00"); // 顶级为 00
+ f.setParentId(parentId);
+ }
+
+
+ // 4. 分批更新并累加返回值
+ int totalUpdated = 0;
+ for (int i = 0; i < allFiles.size(); i += BATCH_SIZE) {
+ List subList = allFiles.subList(i, Math.min(i + BATCH_SIZE, allFiles.size()));
+ int updated = tsFilesMapper.updateParentIdBatch(subList);
+ totalUpdated += updated;
+
+ }
+ // 5. 返回总更新行数
+ return totalUpdated;
+ }
}
diff --git a/java/src/main/java/com/yfd/platform/modules/experimentalData/service/impl/TsNodesServiceImpl.java b/java/src/main/java/com/yfd/platform/modules/experimentalData/service/impl/TsNodesServiceImpl.java
index 87a7fa4..be54179 100644
--- a/java/src/main/java/com/yfd/platform/modules/experimentalData/service/impl/TsNodesServiceImpl.java
+++ b/java/src/main/java/com/yfd/platform/modules/experimentalData/service/impl/TsNodesServiceImpl.java
@@ -38,6 +38,7 @@ import com.yfd.platform.system.domain.LoginUser;
import com.yfd.platform.utils.StringUtils;
import com.yfd.platform.utils.TableNameContextHolder;
import io.netty.channel.ChannelInboundHandlerAdapter;
+import lombok.val;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -46,6 +47,7 @@ import org.springframework.scheduling.annotation.Async;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
@@ -70,6 +72,8 @@ public class TsNodesServiceImpl extends ServiceImpl impl
private static final Logger LOGGER = LoggerFactory.getLogger(ChannelInboundHandlerAdapter.class);
+ private static final int BATCH_SIZE = 5000;
+
//试验任务节点表 Mapper
@Resource
private TsNodesMapper tsNodesMapper;
@@ -109,53 +113,182 @@ public class TsNodesServiceImpl extends ServiceImpl impl
@Override
public List