9.9 KiB
9.9 KiB
moldinsight 模块技术债务分析与重构计划
日期:2026-08-31 · 基线 commit:
3ea5955(模块拆分 init) 状态标记:[ ]待办 /[x]已完成 /[~]部分完成
一、问题清单(按严重程度)
A. 安全漏洞(P0)
| # | 问题 | 位置 | 影响 |
|---|---|---|---|
| S1 | /api/debug/tasks 无鉴权,全量 dump 所有用户任务(含 geometry_data、analysis_result、文件名、LLM 报告)及 Redis 拓扑信息 |
api/debug_router.py:9-20 |
跨用户数据泄露 |
| S2 | /api/history 与 /api/history/{filename} 无鉴权,且 get_all_file_groups() 未传 user_id(参数形同虚设) |
api/history_router.py:25-40、services/storage_integration_rustfs.py:698-704 |
跨用户文件清单泄露 |
| S3 | _ensure_task_access 对 owner_id is None 的无主数据直接放行 |
api/advanced_router.py:87-89 |
任意登录用户可下载历史无主任务的导出文件 |
B. 静默失败(P0)
| # | 问题 | 位置 | 影响 |
|---|---|---|---|
| F1 | /api/detect-undercuts 传 shape=None,OCC 异常被兜底 except 吞掉,永远返回"无倒扣"的假 DFM 结论 |
api/advanced_router.py:293-302、core/side_action_designer.py:205-218 |
功能性错误,用户拿到 200 + 错误工程结论 |
| F2 | asyncio.wait_for 超时无法杀死 OCC 线程;_occ_executor 为 max_workers=1,一个病态文件可永久堵死全部分析队列直到重启 |
services/processing_service.py:44-45,74-82 |
服务级可用性风险 |
| F3 | asyncio.create_task(...) 未持有引用(GC 可回收任务)且无并发上限 |
api/upload_router.py:99-108、api/batch_router.py:114-121 |
后台任务静默消失 / 内存失控 |
C. 性能与资源(P1)
| # | 问题 | 位置 | 影响 |
|---|---|---|---|
| P1 | 已完成任务每次状态轮询都从 RustFS 全量拉取 geometry + 多方案型腔 JSON + 网格 + 完整 HTML,无缓存 | services/task_query_service.py:54-58 |
轮询 5s 一次 = 每次几十 MB 对象存储流量 |
| P2 | _export_shapes_cache 缓存 OCC TopoDS_Shape(C++ 原生内存),按 task_id 无上限增长,无 LRU/TTL |
services/processing_service.py:43 |
原生内存泄漏 |
| P3 | save_html_file 双写:完整 HTML 既入 RustFS 又塞 PG 行(html_content) |
services/storage_integration_rustfs.py:448-462 |
PG 表膨胀 + 双份数据一致性负担 |
| P4 | get_all_file_groups 每文件组单独一次 count 查询(N+1) |
services/storage_integration_rustfs.py:745-751 |
history 接口放大 100 倍查询 |
| P5 | update_task 为 get->merge->set 三步非原子,后台流程与 export-mold 端点并发写同一任务会丢更新;且每次进度 tick 全量重写整个 blob |
shared/services/redis_task_manager.py:138-146 |
竞态丢数据 + 写放大 |
| P6 | 服务重启后 _export_shapes_cache 清空,STL 等格式的重导出直接 409 |
api/advanced_router.py:477-481 |
用户体验缺陷 |
| P7 | get_task_view 已 joinedload html_file 后又单独查询 HTMLFile;llm_service._chat 每次新建 httpx client 且无重试 |
services/task_query_service.py:76-81、services/llm_service.py:517-531 |
小浪费 × 高频 |
D. 架构与死代码(P2)
| # | 问题 | 位置 | 影响 |
|---|---|---|---|
| D1 | ~1000 行死代码:storage_integration.py(MinIO版,376行,零引用)、storage/object_storage.py(361行,仅被死文件引用)、storage_service.py(295行,零引用,仍用已弃用列)、src/main.py(废弃单体,~230行) |
详见各文件 | 认知负担 + 误用风险 |
| D2 | 根 Dockerfile 仍在运行旧单体 python src/main.py,在仓库根目录 docker build . 会部署出错误服务 |
Dockerfile:28 |
部署陷阱 |
| D3 | planner 调用 generator 13 个 _ 前缀私有方法,私有方法成为事实契约;公共 API generate_mold_cavities 反而无人使用 |
core/multi_scheme_planner.py:38,102-165 |
core 边界糊化,重构即炸 |
| D4 | REDIS_HOST 两处读取两个默认值,其一为硬编码个人主机名 szcjw;settings 在 import 时因缺 DB 配置直接 raise |
shared/services/redis_task_manager.py:38、shared/config/settings.py:50-51 |
配置漂移 + 模块不可导入即不可测 |
| D5 | upload/batch 约 50 行复制粘贴(参数归一化 + Celery/asyncio 分派);process_file_with_storage 与 process_file_core 异常处理两份拷贝 |
api/upload_router.py:43-49 vs api/batch_router.py:62-68 |
漂移风险 |
| D6 | moldinsight 测试覆盖为零;唯一测试 temp_test_injection_p0.py 因无 test_ 前缀不被收集,且用黑加载规避 settings 导入期失败 |
tests/ |
回归无保障 |
| D7 | 铝价服务返回模拟数据但未在任何层面标注 | services/aluminum_price_service.py |
产品诚信问题 |
二、实施方案
P0:安全 + 静默失败(先做)
-
① 补鉴权(修 S1/S2/S3)
history_router两个端点加get_current_active_user依赖,显式传user_id=current_user.iddebug_router加鉴权,且仅在settings.DEBUG下注册_ensure_task_access改为owner_id != user_id即 403(无主数据同样拒绝)
-
② 统一后台分派(修 F3,消 D5 一半)
- 新建
services/task_dispatcher.py:Celery 可用走process_stp_task.delay;否则asyncio.create_task并持有强引用(_background_tasksset + done_callback 回收) - upload/batch 路由统一调用;
asyncio.Semaphore限制 API 进程内并发处理数
- 新建
-
③ 超时后重置 OCC executor(修 F2)
asyncio.TimeoutError分支调用_reset_occ_executor():新建 executor、旧 executorshutdown(wait=False)- 泄漏 1 个挂死线程远好于全队列堵死;生产环境确认 celery worker 必配(进程隔离天然免疫)
-
④ shape_loader 重建几何(修 F1)
- 新建
services/shape_loader.py:task_id -> PG 查 object_key -> RustFS 下载 STP -> 临时文件 -> occ executor 内stp_parser.load_step_file /detect-undercuts用真实 shape 调analyze_and_design,补_ensure_task_access/cost-estimate的任务数据源从 Redis 直读迁移到TaskQueryService.get_task_view(完成态走 PG+RustFS 组装,语义正确)
- 新建
-
⑤ Redis 哈希原子更新 + 配置收敛 + 完成态瘦身(修 P5/D4 部分)
redis_task_manager改为 Hash 存储:HSET task:{id} field value字段级原子更新,无读改写竞态,进度 tick 不再全量重写 blob- 兼容读旧 string 格式(过渡期);
redis_client属性保留供 batch_router 使用 - 连接参数统一读
settings.*,删除硬编码szcjw - 完成态任务 Redis 只存摘要字段(去掉 geometry_data/analysis_result 大对象,完成态视图本就由 PG+RustFS 组装)
P1:性能与资源
-
⑤ 任务视图 TTL 缓存(修 P1/P7 部分)
TaskQueryService.get_task_view对 PG 路径(completed/failed)加进程内 TTL 缓存(60s)- export-mold / cam 写参数后显式失效;删除重复的 HTMLFile 单独查询
-
⑥ export_shapes_cache 改 LRU(修 P2)
- OrderedDict LRU,
maxsize=32,命中move_to_end,满则逐出最旧(连原生 OCC shape 一起释放)
- OrderedDict LRU,
-
⑦ 重启后 STEP->STL 现场转换(修 P6/F1 根因延伸)
- 分析期已持久化各方案 cavity/core/分型面 STEP;重启后 cache miss 时下载已持久化的 STEP -> OCC 读取 -> 三角化 -> 写 STL
export-mold的 409 分支前新增此兜底,用户不再需要重新分析
-
⑧ 收尾(修 P3/P4/P7)
save_html_file停止向 PG 写html_content(RustFS 为准,PG 只存 key 与文件名)get_all_file_groups的 N+1 count 改为单条GROUP BY聚合llm_service._chat加一次瞬态错误重试(保持 per-call client:celery 每任务新循环,模块级 AsyncClient 会跨循环失效,与 redis 同理)
P2:架构清理
-
⑨ 删死代码(修 D1/D2)
- 删除:
services/storage_integration.py、storage/object_storage.py、services/storage_service.py、src/main.py、根Dockerfile - 删前
grep -r确认零引用(动态引用也排查)
- 删除:
-
⑪ 配置收敛(修 D4 后半)
settings改惰性校验:DB 配置缺失不在 import 时 raise,改为首次访问DATABASE_URL时报清晰错误- 解锁
import shared.*无 env 场景(测试环境)
-
⑫ 测试建设(修 D6,本阶段做低风险部分)
temp_test_injection_p0.py->test_injection_p0.py,改包路径导入- 补纯逻辑单测:PartingSchemeScorer / PartingCandidateGenerator / MaterialService / cost_estimate_service /
_determine_mold_structure/ redis_task_manager 序列化
-
⑩ Generator 公共接口提取(修 D3) —— 13 个
_方法提为公共 API,需排期单独做(纯机械重命名,但触及 core 三个文件,建议独立 PR + 集成测试保护) -
⑬ 顺手项(修 D7 等) —— advanced_router 拆分 + Pydantic 模型;铝价响应加
"source": "simulated"并前端标注
三、验证方式
python -m pytest tests/ -x(inventory 既有测试不回归 + 新增单测通过)python -c "import ..."冒烟:dispatcher / shape_loader / redis_task_manager / task_query_service 可导入- 部署面:
docker-compose.yml仅引用deploy/Dockerfile.*,根 Dockerfile 删除后无引用(grep 验证)
四、风险与回滚
- Redis Hash 改造保留旧 string 读取兼容:升级期间在途任务可读;新写入一律 Hash。回滚版本读到 Hash 会
get_task返回 None -> 走 PG 组装路径(TaskQueryService 兜底),不会 500 _ensure_task_access收紧 owner=None 后,如确有管理员查看无主历史数据的需求,后续走 admin 角色专用端点,而非放开普通用户html_content停写后,历史行中的旧数据仍可读(列保留),仅新行不再写入