|
|
@@ -14,6 +14,7 @@ import com.zsjz.ai.common.cache.GlobalCache;
|
|
|
import com.zsjz.ai.common.constants.PathConst;
|
|
|
import com.zsjz.ai.common.constants.StrConsts;
|
|
|
import com.zsjz.ai.common.context.CaseContextHolder;
|
|
|
+import com.zsjz.ai.common.enums.DelimiterEnum;
|
|
|
import com.zsjz.ai.common.enums.FileSheetStateEnum;
|
|
|
import com.zsjz.ai.common.enums.HasInnerEnum;
|
|
|
import com.zsjz.ai.common.exception.ServerException;
|
|
|
@@ -48,6 +49,7 @@ import org.springframework.web.multipart.MultipartFile;
|
|
|
import java.io.File;
|
|
|
import java.io.IOException;
|
|
|
import java.io.InputStream;
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
import java.nio.file.Files;
|
|
|
import java.nio.file.Path;
|
|
|
import java.nio.file.StandardCopyOption;
|
|
|
@@ -149,10 +151,16 @@ public class DmService {
|
|
|
* {@code doClean}({@code CleanTask})与「重新清洗」都需要按 {@code filePath}
|
|
|
* 重新打开源文件,落到 tmp 会导致清洗后源文件丢失。
|
|
|
*
|
|
|
- * @param file 上传的单个文件(xls / xlsx / csv)
|
|
|
+ * <p><b>非 xls / xlsx / csv 的文件不再被拒收</b>:以前这里直接抛
|
|
|
+ * 「不支持的文件类型」把用户挡在门外,用户既看不到自己传了什么,也看不到为什么没解析。
|
|
|
+ * 现在改为<b>照常落盘 + 登记一条 {@link FileSheetStateEnum#FILE_UNSUPPORTED_FAIL} 记录</b>,
|
|
|
+ * 与表格文件共用同一张 {@code file_info} 表、同一个批次,前端右侧列表会直接显示失败状态。
|
|
|
+ *
|
|
|
+ * @param file 上传的单个文件;仅 xls / xlsx / csv 会被解析,其余格式登记为「文件格式不正确」
|
|
|
* @param batchId 批次ID;为 null 时自动生成,同批文件应由前端回传首次的 batchId
|
|
|
* @param reClean 1=重新清洗(跳过 MD5 去重)
|
|
|
- * @return 该文件的树形结构:文件根节点(pid=0)内嵌 children sheet 列表;重复文件返回 null
|
|
|
+ * @return 该文件的树形结构:文件根节点(pid=0)内嵌 children sheet 列表;
|
|
|
+ * 非表格文件返回无 children 的失败节点;重复文件返回 null
|
|
|
*/
|
|
|
public FileInfo preProcessUploadedFile(MultipartFile file, Long batchId, Integer reClean) {
|
|
|
if (file == null || file.isEmpty()) {
|
|
|
@@ -160,13 +168,32 @@ public class DmService {
|
|
|
}
|
|
|
String originalName = StrUtil.blankToDefault(file.getOriginalFilename(), "未命名文件");
|
|
|
String suffix = FileUtil.getSuffix(originalName);
|
|
|
- if (suffix == null || !GlobalCache.suffixList.contains(suffix.toLowerCase())) {
|
|
|
- throw ServerException.spe("不支持的文件类型:" + originalName + "(仅支持 xls / xlsx / csv)");
|
|
|
+ String lowerSuffix = suffix == null ? null : suffix.toLowerCase();
|
|
|
+
|
|
|
+ // 第一级:可上传白名单。不在清单里的直接拒收 —— 前端已按同一清单过滤,
|
|
|
+ // 这里防的是绕过前端直接调接口(拖拽、脚本)把无关文件塞进来。
|
|
|
+ // 必须放在落盘**之前**,否则会留下一个永远没人认领的垃圾文件。
|
|
|
+ if (lowerSuffix == null || !GlobalCache.uploadSuffixList.contains(lowerSuffix)) {
|
|
|
+ throw ServerException.spe("不支持的文件类型:" + originalName
|
|
|
+ + "(仅支持 " + String.join(" / ", GlobalCache.uploadSuffixList) + ")");
|
|
|
}
|
|
|
|
|
|
Long currentBatchId = batchId != null ? batchId : IdUtil.getSnowflakeNextId();
|
|
|
Path saved = saveUploadedFile(file, currentBatchId, originalName);
|
|
|
|
|
|
+ // 第二级:可解析白名单。可上传但不可解析(txt / md / doc / docx / pdf)不解析,
|
|
|
+ // 但同样落盘 + 登记到 file_info,状态记为「文件格式不正确」,让用户在文件列表里
|
|
|
+ // 能看到「传过这个文件、以及为什么没解析」—— 这正是以前直接抛「不支持的文件类型」
|
|
|
+ // 所丢掉的信息。
|
|
|
+ // 这类文件刻意**不做 MD5 去重**:去重的意义是避免重复解析浪费算力,而这些文件根本
|
|
|
+ // 不解析,去重反而会让用户第二次上传时在列表里什么都看不到,与「让用户看得见」相悖。
|
|
|
+ if (!GlobalCache.suffixList.contains(lowerSuffix)) {
|
|
|
+ FileInfo unreadable = buildUnreadableFileInfo(originalName, lowerSuffix, saved, currentBatchId);
|
|
|
+ saveParsedFileInfos(unreadable);
|
|
|
+ log.info("上传文件不可解析,已登记为无法读取: name={}, batchId={}", originalName, currentBatchId);
|
|
|
+ return unreadable;
|
|
|
+ }
|
|
|
+
|
|
|
// 重新清洗时跳过 MD5 去重(历史文件 md5 已存在于 file_info,否则会被全部过滤)
|
|
|
if (reClean == null || reClean != 1) {
|
|
|
List<Path> kept = filterExistsMd5Files(List.of(saved));
|
|
|
@@ -205,6 +232,42 @@ public class DmService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 构造「无法读取」的文件节点:文件已落盘,但格式不在可解析白名单内。
|
|
|
+ *
|
|
|
+ * <p>不解析、不建 DuckDB 预览表,只留一条 {@code file_info} 记录,让用户在文件列表里
|
|
|
+ * 能看到「传过这个文件、以及为什么没解析」。{@code fileType} 记真实后缀(如 {@code docx}),
|
|
|
+ * 前端「名称」列会显示成 {@code 报告.docx · docx}。
|
|
|
+ *
|
|
|
+ * <p>字段对齐 {@code PreTask#initDefaultFileInfo}:计数一律 0、{@code pid=0}(文件根节点)、
|
|
|
+ * 无 children,因此前端 {@code buildPreviewTree} 之外无需特殊处理,直接当普通根节点渲染。
|
|
|
+ *
|
|
|
+ * @param originalName 原始文件名
|
|
|
+ * @param suffix 小写文件后缀(不含点),调用方保证非空
|
|
|
+ * @param saved 落盘后的绝对路径
|
|
|
+ * @param batchId 批次ID
|
|
|
+ * @return 失败状态的文件根节点
|
|
|
+ */
|
|
|
+ private FileInfo buildUnreadableFileInfo(String originalName, String suffix, Path saved, Long batchId) {
|
|
|
+ FileInfo info = new FileInfo();
|
|
|
+ info.setId(IdUtil.getSnowflakeNextId());
|
|
|
+ info.setPid(0L);
|
|
|
+ info.setFileName(originalName);
|
|
|
+ info.setFilePath(saved.toString());
|
|
|
+ info.setFileType(suffix);
|
|
|
+ info.setFileStatus(FileSheetStateEnum.FILE_UNSUPPORTED_FAIL);
|
|
|
+ info.setDataNum(0);
|
|
|
+ info.setSuccessNum(0);
|
|
|
+ info.setFailNum(0);
|
|
|
+ info.setBatchId(batchId);
|
|
|
+ info.setCharSet(StandardCharsets.UTF_8.displayName());
|
|
|
+ // 非表格文件没有分隔符概念,写 COMMA 只是为了满足列约束(与 PreTask 的默认值一致)
|
|
|
+ info.setDelimiter(DelimiterEnum.COMMA);
|
|
|
+ info.setCreateDateTime(LocalDateTime.now());
|
|
|
+ backfillFileSize(info, saved);
|
|
|
+ return info;
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 查询 sheet 原始数据预览
|
|
|
* 从预览阶段动态创建的 DuckDB 表 raw_{sheetId} 读取数据,替代原 Electron 端 readPreviewFile
|