2026-05-21-00-43-44 修复ZIP STL导入反馈
This commit is contained in:
@@ -2595,6 +2595,12 @@ async function startServer() {
|
|||||||
writeState(state);
|
writeState(state);
|
||||||
res.json(project);
|
res.json(project);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('[import-assets] failed', {
|
||||||
|
projectId: req.params.projectId,
|
||||||
|
kind,
|
||||||
|
fileCount: multerFiles.length + legacyUploadedFiles.length,
|
||||||
|
message: error instanceof Error ? error.message : error,
|
||||||
|
});
|
||||||
res.status(422).json({ message: error instanceof Error ? error.message : '项目资产导入失败' });
|
res.status(422).json({ message: error instanceof Error ? error.message : '项目资产导入失败' });
|
||||||
} finally {
|
} finally {
|
||||||
cleanupUploadedTempFiles(multerFiles);
|
cleanupUploadedTempFiles(multerFiles);
|
||||||
|
|||||||
@@ -135,7 +135,8 @@ interface AssetImportProgressState {
|
|||||||
totalBytes: number;
|
totalBytes: number;
|
||||||
loadedBytes: number;
|
loadedBytes: number;
|
||||||
percent: number;
|
percent: number;
|
||||||
phase: 'uploading' | 'processing' | 'done';
|
phase: 'uploading' | 'processing' | 'done' | 'failed';
|
||||||
|
message?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatFileSize(value: number) {
|
function formatFileSize(value: number) {
|
||||||
@@ -953,8 +954,17 @@ export default function ProjectLibrary({
|
|||||||
setActionMessage(kind === 'dicom' ? `已导入 ${updated.dicomCount} 张 DICOM 影像` : `已导入 ${updated.modelCount ?? 0} 个 STL 模型`);
|
setActionMessage(kind === 'dicom' ? `已导入 ${updated.dicomCount} 张 DICOM 影像` : `已导入 ${updated.modelCount ?? 0} 个 STL 模型`);
|
||||||
window.setTimeout(() => setAssetImportProgress(null), 1800);
|
window.setTimeout(() => setAssetImportProgress(null), 1800);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setActionMessage(error instanceof Error ? error.message : '项目资产导入失败');
|
const message = error instanceof Error ? error.message : '项目资产导入失败';
|
||||||
window.setTimeout(() => setAssetImportProgress(null), 2400);
|
setAssetImportProgress((current) => ({
|
||||||
|
kind,
|
||||||
|
fileCount: files.length,
|
||||||
|
totalBytes: current?.totalBytes ?? totalBytes,
|
||||||
|
loadedBytes: current?.loadedBytes ?? 0,
|
||||||
|
percent: current?.percent ?? 0,
|
||||||
|
phase: 'failed',
|
||||||
|
message,
|
||||||
|
}));
|
||||||
|
setActionMessage(message);
|
||||||
} finally {
|
} finally {
|
||||||
setAssetImporting(false);
|
setAssetImporting(false);
|
||||||
}
|
}
|
||||||
@@ -1516,15 +1526,29 @@ export default function ProjectLibrary({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{assetImportProgress && (
|
{assetImportProgress && (
|
||||||
<div className="rounded-2xl border border-blue-100 bg-white px-5 py-3 shadow-sm">
|
<div className={`rounded-2xl border px-5 py-3 shadow-sm ${
|
||||||
|
assetImportProgress.phase === 'failed'
|
||||||
|
? 'border-rose-200 bg-rose-50'
|
||||||
|
: assetImportProgress.phase === 'done'
|
||||||
|
? 'border-emerald-100 bg-white'
|
||||||
|
: 'border-blue-100 bg-white'
|
||||||
|
}`}>
|
||||||
<div className="mb-2 flex items-center justify-between gap-4">
|
<div className="mb-2 flex items-center justify-between gap-4">
|
||||||
<div className="flex min-w-0 items-center gap-3">
|
<div className="flex min-w-0 items-center gap-3">
|
||||||
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-blue-50 text-blue-600">
|
<div className={`flex h-9 w-9 shrink-0 items-center justify-center rounded-xl ${
|
||||||
<FileArchive size={17} />
|
assetImportProgress.phase === 'failed'
|
||||||
|
? 'bg-rose-100 text-rose-600'
|
||||||
|
: assetImportProgress.phase === 'done'
|
||||||
|
? 'bg-emerald-50 text-emerald-600'
|
||||||
|
: 'bg-blue-50 text-blue-600'
|
||||||
|
}`}>
|
||||||
|
{assetImportProgress.phase === 'failed' ? <X size={17} /> : <FileArchive size={17} />}
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="truncate text-sm font-bold text-slate-800">
|
<p className="truncate text-sm font-bold text-slate-800">
|
||||||
{assetImportProgress.phase === 'done'
|
{assetImportProgress.phase === 'failed'
|
||||||
|
? `${describeImportKind(assetImportProgress.kind)}导入失败`
|
||||||
|
: assetImportProgress.phase === 'done'
|
||||||
? `${describeImportKind(assetImportProgress.kind)}导入完成`
|
? `${describeImportKind(assetImportProgress.kind)}导入完成`
|
||||||
: assetImportProgress.phase === 'processing'
|
: assetImportProgress.phase === 'processing'
|
||||||
? '上传完成,服务器正在解压与解析'
|
? '上传完成,服务器正在解压与解析'
|
||||||
@@ -1533,15 +1557,32 @@ export default function ProjectLibrary({
|
|||||||
<p className="mt-0.5 text-[11px] font-bold text-slate-400">
|
<p className="mt-0.5 text-[11px] font-bold text-slate-400">
|
||||||
{assetImportProgress.fileCount} 个文件 · {formatFileSize(assetImportProgress.loadedBytes)} / {formatFileSize(assetImportProgress.totalBytes)}
|
{assetImportProgress.fileCount} 个文件 · {formatFileSize(assetImportProgress.loadedBytes)} / {formatFileSize(assetImportProgress.totalBytes)}
|
||||||
</p>
|
</p>
|
||||||
|
{assetImportProgress.phase === 'failed' && assetImportProgress.message && (
|
||||||
|
<p className="mt-1 whitespace-normal text-[11px] font-bold leading-5 text-rose-700">
|
||||||
|
{assetImportProgress.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span className="shrink-0 font-mono text-sm font-black text-blue-600">
|
<span className={`shrink-0 font-mono text-sm font-black ${
|
||||||
|
assetImportProgress.phase === 'failed'
|
||||||
|
? 'text-rose-600'
|
||||||
|
: assetImportProgress.phase === 'done'
|
||||||
|
? 'text-emerald-600'
|
||||||
|
: 'text-blue-600'
|
||||||
|
}`}>
|
||||||
{assetImportProgress.percent}%
|
{assetImportProgress.percent}%
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-2 overflow-hidden rounded-full bg-slate-100">
|
<div className="h-2 overflow-hidden rounded-full bg-slate-100">
|
||||||
<div
|
<div
|
||||||
className={`h-full rounded-full transition-all duration-300 ${assetImportProgress.phase === 'done' ? 'bg-emerald-500' : 'bg-blue-600'}`}
|
className={`h-full rounded-full transition-all duration-300 ${
|
||||||
|
assetImportProgress.phase === 'failed'
|
||||||
|
? 'bg-rose-500'
|
||||||
|
: assetImportProgress.phase === 'done'
|
||||||
|
? 'bg-emerald-500'
|
||||||
|
: 'bg-blue-600'
|
||||||
|
}`}
|
||||||
style={{ width: `${assetImportProgress.percent}%` }}
|
style={{ width: `${assetImportProgress.percent}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
51
工程分析/实现方案-2026-05-21-00-43-44.md
Normal file
51
工程分析/实现方案-2026-05-21-00-43-44.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# 实现方案-2026-05-21-00-43-44
|
||||||
|
|
||||||
|
## 实现方案文档路径
|
||||||
|
|
||||||
|
`工程分析/实现方案-2026-05-21-00-43-44.md`
|
||||||
|
|
||||||
|
## 修改目标
|
||||||
|
|
||||||
|
- 复现 `3279-STL.zip` 导入时的 422 错误,获取具体失败原因。
|
||||||
|
- 修复 ZIP 内 STL 解压/筛选/写入逻辑,使该压缩包可导入。
|
||||||
|
- 优化导入失败 UI,将错误状态和原因显示在顶部导入进度区域。
|
||||||
|
- 补充测试记录和经验记录。
|
||||||
|
|
||||||
|
## 涉及路径
|
||||||
|
|
||||||
|
- `WebSite/server.ts`
|
||||||
|
- `WebSite/src/components/ProjectLibrary.tsx`
|
||||||
|
- `工程分析/经验记录.md`
|
||||||
|
|
||||||
|
## 技术路线
|
||||||
|
|
||||||
|
1. 使用 `curl -F files=@3279-STL.zip` 对现有接口复现错误,查看响应体。
|
||||||
|
2. 检查 `AdmZip` 解包结果、ZIP 条目大小和写入逻辑。
|
||||||
|
3. 如果问题来自大 ZIP 一次性 `entry.getData()` 导致内存或写入失败,则改为按条目流式/逐条写入目标目录,减少内存峰值。
|
||||||
|
4. 如果问题来自文件名扩展名、编码或路径处理,则修正筛选和安全文件名逻辑。
|
||||||
|
5. 前端导入状态增加 `failed` phase,错误时保留进度卡并展示错误文案。
|
||||||
|
|
||||||
|
## 执行步骤
|
||||||
|
|
||||||
|
1. 创建本次工程分析文档。
|
||||||
|
2. 本地复现 422,记录响应。
|
||||||
|
3. 修改后端 ZIP 导入逻辑和前端错误反馈。
|
||||||
|
4. 执行 `npm run lint`、`npm run build`。
|
||||||
|
5. 用 `3279-STL.zip` 对临时项目实际导入验证。
|
||||||
|
6. 追加经验记录,提交推送 Gitea,重新部署。
|
||||||
|
|
||||||
|
## 兼容性与回滚方案
|
||||||
|
|
||||||
|
- 若 `AdmZip` 对该 ZIP 仍不稳定,可增加系统 `unzip` fallback 或改用更适合流式解压的库。
|
||||||
|
- 前端错误卡只影响导入反馈,不影响其他页面。
|
||||||
|
- 导入失败时不更新项目状态,避免半成品资产进入项目库。
|
||||||
|
|
||||||
|
## 预计文件变更
|
||||||
|
|
||||||
|
- 后端 1 个、前端组件 1 个、工程分析文档 4 个。
|
||||||
|
|
||||||
|
## 提交与部署策略
|
||||||
|
|
||||||
|
- 不提交 `3279-STL.zip` 和解压资产。
|
||||||
|
- commit message 包含 `2026-05-21-00-43-44` 与“修复 ZIP STL 导入失败”。
|
||||||
|
- 部署后验证 `/api/health`、首页以及 ZIP 导入结果。
|
||||||
51
工程分析/测试方案-2026-05-21-00-43-44.md
Normal file
51
工程分析/测试方案-2026-05-21-00-43-44.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# 测试方案-2026-05-21-00-43-44
|
||||||
|
|
||||||
|
## 测试方案文档路径
|
||||||
|
|
||||||
|
`工程分析/测试方案-2026-05-21-00-43-44.md`
|
||||||
|
|
||||||
|
## 静态检查
|
||||||
|
|
||||||
|
- 执行 `cd WebSite && npm run lint`,确认 TypeScript 类型检查通过。
|
||||||
|
- 使用 `rg` 检查导入错误反馈和 ZIP 处理路径。
|
||||||
|
|
||||||
|
执行结果:`npm run lint` 已通过。
|
||||||
|
|
||||||
|
## 构建检查
|
||||||
|
|
||||||
|
- 执行 `cd WebSite && npm run build`,确认生产构建通过。
|
||||||
|
|
||||||
|
执行结果:`npm run build` 已通过;Vite 仅提示既有 chunk 体积超过 500 kB。
|
||||||
|
|
||||||
|
## 关键业务场景验证
|
||||||
|
|
||||||
|
- 使用 `3279-STL.zip` 调用项目导入接口,确认返回 200 且 `modelCount` 与 ZIP 内 STL 数量一致。
|
||||||
|
- 前端导入失败时顶部进度区域显示失败状态和错误原因。
|
||||||
|
- 前端导入成功时顶部进度区域显示完成状态。
|
||||||
|
|
||||||
|
执行结果:使用 `3279-STL.zip` 对项目 `project-mpea0n3e-akbpn` 导入成功,接口返回 200,`modelCount: 26`,`stlFiles` 已包含 ZIP 内 26 个 STL。
|
||||||
|
|
||||||
|
## 医学影像数据相关边界验证
|
||||||
|
|
||||||
|
- ZIP 中所有 `.stl` 文件写入项目级 `WebSite/data/uploads/<projectId>/STL`。
|
||||||
|
- 导入后 `project.stlFiles` 包含 ZIP 内模型文件名。
|
||||||
|
- 导入失败不会污染默认 `Head_CT_ReConstruct/`。
|
||||||
|
- 临时测试项目和上传目录验证后清理。
|
||||||
|
|
||||||
|
## 部署验证
|
||||||
|
|
||||||
|
- 重启 `tmux` 会话 `revoxelseg-dicom`。
|
||||||
|
- 验证:
|
||||||
|
- `curl http://127.0.0.1:4000/api/health`
|
||||||
|
- `curl -I http://127.0.0.1:4000/`
|
||||||
|
|
||||||
|
## Git/Gitea 备份验证
|
||||||
|
|
||||||
|
- `git status --short` 确认暂存范围不包含 `3279-STL.zip`、解压模型或无关历史删除。
|
||||||
|
- commit message 包含 `2026-05-21-00-43-44`。
|
||||||
|
- 推送 Gitea 成功。
|
||||||
|
|
||||||
|
## 风险与回归关注点
|
||||||
|
|
||||||
|
- ZIP 内大型 STL 不应同时全部进入内存。
|
||||||
|
- 前端错误信息不能只显示在左侧项目栏底部,应在导入进度卡上直接可见。
|
||||||
18
工程分析/经验记录.md
18
工程分析/经验记录.md
@@ -1423,3 +1423,21 @@ C. 解决问题方案
|
|||||||
D. 后续如何避免问题
|
D. 后续如何避免问题
|
||||||
|
|
||||||
凡是医学影像、STL、NIfTI、压缩包等大文件导入,都不要在浏览器内转 base64 或拼接巨型 JSON;默认使用 multipart 或分片上传,并把解压、筛选、校验放在服务端。导入 UI 应区分“上传进度”和“服务器处理阶段”,避免用户误以为进度到 100% 就已经完成解析。
|
凡是医学影像、STL、NIfTI、压缩包等大文件导入,都不要在浏览器内转 base64 或拼接巨型 JSON;默认使用 multipart 或分片上传,并把解压、筛选、校验放在服务端。导入 UI 应区分“上传进度”和“服务器处理阶段”,避免用户误以为进度到 100% 就已经完成解析。
|
||||||
|
|
||||||
|
## 2026-05-21-00-43-44 导入失败要在主操作区保留可见错误
|
||||||
|
|
||||||
|
A. 具体问题
|
||||||
|
|
||||||
|
用户导入 `3279-STL.zip` 后浏览器控制台出现 `422 Unprocessable Entity`,但页面主操作区没有明显反馈,用户感知为“没有反应”。后续用同一 ZIP 直接调用接口验证可成功导入 26 个 STL,说明压缩包本体可读,问题更集中在失败反馈不可见和缺少后端可追踪日志。
|
||||||
|
|
||||||
|
B. 产生问题原因
|
||||||
|
|
||||||
|
前端导入异常只写入左侧项目栏底部的小号 `actionMessage`,主内容区域的进度条会消失,用户视线集中在右侧模型区域时很难看到错误。后端 422 返回虽然包含 `message`,但服务端没有记录项目、导入类型和文件数量,排查需要重新手工复现。
|
||||||
|
|
||||||
|
C. 解决问题方案
|
||||||
|
|
||||||
|
将项目库导入状态扩展为 `uploading/processing/done/failed`,失败时在顶部进度卡保留红色错误状态、百分比和后端错误原因,不再自动消失。后端导入 catch 中增加结构化日志,记录 `projectId`、`kind`、上传文件数量和错误消息。并用用户提供的 `3279-STL.zip` 对项目 `123` 实际导入,确认项目更新为 26 个 STL。
|
||||||
|
|
||||||
|
D. 后续如何避免问题
|
||||||
|
|
||||||
|
所有文件导入、导出、保存这类长操作失败时,都要在主操作路径旁保留显眼错误卡,不能只写入边栏或短暂 toast。后端对 4xx/5xx 的可恢复业务错误应记录足够上下文,尤其是项目 ID、导入类型、文件数量和原始异常消息,方便从用户截图快速定位。
|
||||||
|
|||||||
46
工程分析/需求分析-2026-05-21-00-43-44.md
Normal file
46
工程分析/需求分析-2026-05-21-00-43-44.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# 需求分析-2026-05-21-00-43-44
|
||||||
|
|
||||||
|
## 开始时间
|
||||||
|
|
||||||
|
2026-05-21-00-43-44
|
||||||
|
|
||||||
|
## 原始需求摘要
|
||||||
|
|
||||||
|
1. 用户上传 `3279-STL.zip` 后项目库没有明显反应。
|
||||||
|
2. 浏览器控制台显示 `/api/projects/:projectId/import-assets` 返回 `422 Unprocessable Entity`。
|
||||||
|
3. 用户已将 `3279-STL.zip` 放在当前项目目录,需要定位压缩包导入失败原因并修复。
|
||||||
|
|
||||||
|
## 业务目标
|
||||||
|
|
||||||
|
- 让包含多个 STL 的 ZIP 压缩包可以稳定导入项目库 3D 模型。
|
||||||
|
- 导入失败时前端必须给出显眼、可读的错误反馈,而不是让用户感觉“没有反应”。
|
||||||
|
- 后端返回具体错误原因,便于定位压缩包、模型文件或服务器处理问题。
|
||||||
|
|
||||||
|
## 输入与输出
|
||||||
|
|
||||||
|
- 输入:项目根目录中的 `3279-STL.zip`,项目库 3D 模型导入动作。
|
||||||
|
- 输出:成功导入 ZIP 内 STL 模型并更新项目 `modelCount/stlFiles`,或在失败时展示明确原因。
|
||||||
|
|
||||||
|
## 影响范围
|
||||||
|
|
||||||
|
- `WebSite/server.ts`:ZIP 解压、文件名处理、STL 筛选、错误信息。
|
||||||
|
- `WebSite/src/components/ProjectLibrary.tsx`:导入错误提示可见性、导入进度失败状态。
|
||||||
|
- `WebSite/src/lib/api.ts`:错误消息解析。
|
||||||
|
- `工程分析/经验记录.md`:记录本次压缩包导入经验。
|
||||||
|
|
||||||
|
## 关键约束
|
||||||
|
|
||||||
|
- 不把 `3279-STL.zip` 或解压后的大模型资产纳入 Git 提交。
|
||||||
|
- 继续保证导入只写入项目级上传目录,不影响默认演示数据。
|
||||||
|
- 不回退到浏览器 base64 导入。
|
||||||
|
|
||||||
|
## 风险点
|
||||||
|
|
||||||
|
- ZIP 内 STL 总体积较大,服务端一次性解压到内存可能产生压力。
|
||||||
|
- ZIP 文件名或条目可能存在编码、路径、目录层级差异。
|
||||||
|
- 导入成功后项目库预览如果一次性加载过多大 STL,也可能造成浏览器压力,需要至少保证导入链路稳定。
|
||||||
|
|
||||||
|
## 默认假设
|
||||||
|
|
||||||
|
- 当前 422 由后端解压或写入阶段抛错造成,而不是项目不存在。
|
||||||
|
- 用户截图中的 ZIP 内条目均为 `.stl`,导入目标处于“3D 模型”页签。
|
||||||
Reference in New Issue
Block a user