|
@@ -151,16 +151,17 @@ public class DmService {
|
|
|
* {@code doClean}({@code CleanTask})与「重新清洗」都需要按 {@code filePath}
|
|
* {@code doClean}({@code CleanTask})与「重新清洗」都需要按 {@code filePath}
|
|
|
* 重新打开源文件,落到 tmp 会导致清洗后源文件丢失。
|
|
* 重新打开源文件,落到 tmp 会导致清洗后源文件丢失。
|
|
|
*
|
|
*
|
|
|
- * <p><b>非 xls / xlsx / csv 的文件不再被拒收</b>:以前这里直接抛
|
|
|
|
|
|
|
+ * <p><b>非 xls / xlsx / csv 的文件不再被拒收,但也不落盘</b>:以前这里直接抛
|
|
|
* 「不支持的文件类型」把用户挡在门外,用户既看不到自己传了什么,也看不到为什么没解析。
|
|
* 「不支持的文件类型」把用户挡在门外,用户既看不到自己传了什么,也看不到为什么没解析。
|
|
|
- * 现在改为<b>照常落盘 + 登记一条 {@link FileSheetStateEnum#FILE_UNSUPPORTED_FAIL} 记录</b>,
|
|
|
|
|
- * 与表格文件共用同一张 {@code file_info} 表、同一个批次,前端右侧列表会直接显示失败状态。
|
|
|
|
|
|
|
+ * 现在改为<b>跳过上传、只在 {@code file_info} 登记一条
|
|
|
|
|
+ * {@link FileSheetStateEnum#FILE_UNSUPPORTED_FAIL}(不支持解析)记录</b>,
|
|
|
|
|
+ * 与表格文件共用同一张表、同一个批次,前端右侧列表会直接显示失败状态。
|
|
|
*
|
|
*
|
|
|
- * @param file 上传的单个文件;仅 xls / xlsx / csv 会被解析,其余格式登记为「文件格式不正确」
|
|
|
|
|
|
|
+ * @param file 上传的单个文件;仅 xls / xlsx / csv 会被保存并解析,其余后缀只登记不落盘
|
|
|
* @param batchId 批次ID;为 null 时自动生成,同批文件应由前端回传首次的 batchId
|
|
* @param batchId 批次ID;为 null 时自动生成,同批文件应由前端回传首次的 batchId
|
|
|
* @param reClean 1=重新清洗(跳过 MD5 去重)
|
|
* @param reClean 1=重新清洗(跳过 MD5 去重)
|
|
|
* @return 该文件的树形结构:文件根节点(pid=0)内嵌 children sheet 列表;
|
|
* @return 该文件的树形结构:文件根节点(pid=0)内嵌 children sheet 列表;
|
|
|
- * 非表格文件返回无 children 的失败节点;重复文件返回 null
|
|
|
|
|
|
|
+ * 不支持解析的后缀返回无 children 的失败节点;重复文件返回 null
|
|
|
*/
|
|
*/
|
|
|
public FileInfo preProcessUploadedFile(MultipartFile file, Long batchId, Integer reClean) {
|
|
public FileInfo preProcessUploadedFile(MultipartFile file, Long batchId, Integer reClean) {
|
|
|
if (file == null || file.isEmpty()) {
|
|
if (file == null || file.isEmpty()) {
|
|
@@ -169,31 +170,21 @@ public class DmService {
|
|
|
String originalName = StrUtil.blankToDefault(file.getOriginalFilename(), "未命名文件");
|
|
String originalName = StrUtil.blankToDefault(file.getOriginalFilename(), "未命名文件");
|
|
|
String suffix = FileUtil.getSuffix(originalName);
|
|
String suffix = FileUtil.getSuffix(originalName);
|
|
|
String lowerSuffix = suffix == null ? null : suffix.toLowerCase();
|
|
String lowerSuffix = suffix == null ? null : suffix.toLowerCase();
|
|
|
|
|
+ Long currentBatchId = batchId != null ? batchId : IdUtil.getSnowflakeNextId();
|
|
|
|
|
|
|
|
- // 第一级:可上传白名单。不在清单里的直接拒收 —— 前端已按同一清单过滤,
|
|
|
|
|
- // 这里防的是绕过前端直接调接口(拖拽、脚本)把无关文件塞进来。
|
|
|
|
|
- // 必须放在落盘**之前**,否则会留下一个永远没人认领的垃圾文件。
|
|
|
|
|
- if (lowerSuffix == null || !GlobalCache.uploadSuffixList.contains(lowerSuffix)) {
|
|
|
|
|
- throw ServerException.spe("不支持的文件类型:" + originalName
|
|
|
|
|
- + "(仅支持 " + String.join(" / ", GlobalCache.uploadSuffixList) + ")");
|
|
|
|
|
|
|
+ // 后缀校验:只有 csv / xls / xlsx 才真正上传(落盘 + 解析)。
|
|
|
|
|
+ // 其余后缀一律**跳过上传**(不落盘、不解析),但仍在 file_info 留一条「不支持解析」的记录 ——
|
|
|
|
|
+ // 这样用户在文件列表里能看到「传过这个文件、以及为什么没解析」,而不是像以前那样
|
|
|
|
|
+ // 被一句「不支持的文件类型」弹窗打发掉、列表里一点痕迹都没有。
|
|
|
|
|
+ if (lowerSuffix == null || !GlobalCache.suffixList.contains(lowerSuffix)) {
|
|
|
|
|
+ FileInfo unsupported = buildUnsupportedFileInfo(originalName, suffix, file.getSize(), currentBatchId);
|
|
|
|
|
+ saveParsedFileInfos(unsupported);
|
|
|
|
|
+ log.info("上传文件后缀不支持解析,已登记(未落盘): name={}, batchId={}", originalName, currentBatchId);
|
|
|
|
|
+ return unsupported;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- Long currentBatchId = batchId != null ? batchId : IdUtil.getSnowflakeNextId();
|
|
|
|
|
Path saved = saveUploadedFile(file, currentBatchId, originalName);
|
|
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,否则会被全部过滤)
|
|
// 重新清洗时跳过 MD5 去重(历史文件 md5 已存在于 file_info,否则会被全部过滤)
|
|
|
if (reClean == null || reClean != 1) {
|
|
if (reClean == null || reClean != 1) {
|
|
|
List<Path> kept = filterExistsMd5Files(List.of(saved));
|
|
List<Path> kept = filterExistsMd5Files(List.of(saved));
|
|
@@ -233,29 +224,33 @@ public class DmService {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 构造「无法读取」的文件节点:文件已落盘,但格式不在可解析白名单内。
|
|
|
|
|
|
|
+ * 构造「不支持解析」的文件节点:文件**未落盘**,只在 {@code file_info} 留一条记录。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>后缀不是 csv / xls / xlsx 时,上传会被跳过(不保存文件、不解析、不建 DuckDB 预览表),
|
|
|
|
|
+ * 但仍登记这条记录,让用户在文件列表里能看到「传过这个文件、以及为什么没解析」。
|
|
|
|
|
+ * {@code fileType} 记真实后缀(如 {@code docx}),前端「名称」列会显示成 {@code 报告.docx · docx}。
|
|
|
*
|
|
*
|
|
|
- * <p>不解析、不建 DuckDB 预览表,只留一条 {@code file_info} 记录,让用户在文件列表里
|
|
|
|
|
- * 能看到「传过这个文件、以及为什么没解析」。{@code fileType} 记真实后缀(如 {@code docx}),
|
|
|
|
|
- * 前端「名称」列会显示成 {@code 报告.docx · docx}。
|
|
|
|
|
|
|
+ * <p>{@code filePath} 刻意留空 —— 文件根本没落盘,留空能让
|
|
|
|
|
+ * {@code SystemService#resolveDownloadFile} 明确拒绝下载(「该记录没有可下载的源文件!」),
|
|
|
|
|
+ * 而不是指向一个根本不存在的路径。
|
|
|
*
|
|
*
|
|
|
* <p>字段对齐 {@code PreTask#initDefaultFileInfo}:计数一律 0、{@code pid=0}(文件根节点)、
|
|
* <p>字段对齐 {@code PreTask#initDefaultFileInfo}:计数一律 0、{@code pid=0}(文件根节点)、
|
|
|
- * 无 children,因此前端 {@code buildPreviewTree} 之外无需特殊处理,直接当普通根节点渲染。
|
|
|
|
|
|
|
+ * 无 children,因此前端无需特殊处理,直接当普通根节点渲染。
|
|
|
*
|
|
*
|
|
|
* @param originalName 原始文件名
|
|
* @param originalName 原始文件名
|
|
|
- * @param suffix 小写文件后缀(不含点),调用方保证非空
|
|
|
|
|
- * @param saved 落盘后的绝对路径
|
|
|
|
|
|
|
+ * @param suffix 原始后缀(不含点),可为 null(无后缀文件)
|
|
|
|
|
+ * @param fileSize 上传的字节数;文件未落盘,这里记的是浏览器实际传上来的大小
|
|
|
* @param batchId 批次ID
|
|
* @param batchId 批次ID
|
|
|
* @return 失败状态的文件根节点
|
|
* @return 失败状态的文件根节点
|
|
|
*/
|
|
*/
|
|
|
- private FileInfo buildUnreadableFileInfo(String originalName, String suffix, Path saved, Long batchId) {
|
|
|
|
|
|
|
+ private FileInfo buildUnsupportedFileInfo(String originalName, String suffix, long fileSize, Long batchId) {
|
|
|
FileInfo info = new FileInfo();
|
|
FileInfo info = new FileInfo();
|
|
|
info.setId(IdUtil.getSnowflakeNextId());
|
|
info.setId(IdUtil.getSnowflakeNextId());
|
|
|
info.setPid(0L);
|
|
info.setPid(0L);
|
|
|
info.setFileName(originalName);
|
|
info.setFileName(originalName);
|
|
|
- info.setFilePath(saved.toString());
|
|
|
|
|
- info.setFileType(suffix);
|
|
|
|
|
|
|
+ info.setFileType(StrUtil.isBlank(suffix) ? "unknown" : suffix.toLowerCase());
|
|
|
info.setFileStatus(FileSheetStateEnum.FILE_UNSUPPORTED_FAIL);
|
|
info.setFileStatus(FileSheetStateEnum.FILE_UNSUPPORTED_FAIL);
|
|
|
|
|
+ info.setFileSize(fileSize);
|
|
|
info.setDataNum(0);
|
|
info.setDataNum(0);
|
|
|
info.setSuccessNum(0);
|
|
info.setSuccessNum(0);
|
|
|
info.setFailNum(0);
|
|
info.setFailNum(0);
|
|
@@ -264,7 +259,6 @@ public class DmService {
|
|
|
// 非表格文件没有分隔符概念,写 COMMA 只是为了满足列约束(与 PreTask 的默认值一致)
|
|
// 非表格文件没有分隔符概念,写 COMMA 只是为了满足列约束(与 PreTask 的默认值一致)
|
|
|
info.setDelimiter(DelimiterEnum.COMMA);
|
|
info.setDelimiter(DelimiterEnum.COMMA);
|
|
|
info.setCreateDateTime(LocalDateTime.now());
|
|
info.setCreateDateTime(LocalDateTime.now());
|
|
|
- backfillFileSize(info, saved);
|
|
|
|
|
return info;
|
|
return info;
|
|
|
}
|
|
}
|
|
|
|
|
|