Files
geMoldInsight/docs/MOLDINSIGHT_TECH_DEBT_PLAN.md
T
2026-08-31 18:01:34 +08:00

9.9 KiB
Raw Blame History

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.id
    • debug_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_tasks set + done_callback 回收)
    • upload/batch 路由统一调用;asyncio.Semaphore 限制 API 进程内并发处理数
  • ③ 超时后重置 OCC executor(修 F2)

    • asyncio.TimeoutError 分支调用 _reset_occ_executor():新建 executor、旧 executor shutdown(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 一起释放)
  • ⑦ 重启后 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" 并前端标注


三、验证方式

  1. python -m pytest tests/ -x(inventory 既有测试不回归 + 新增单测通过)
  2. python -c "import ..." 冒烟:dispatcher / shape_loader / redis_task_manager / task_query_service 可导入
  3. 部署面: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 停写后,历史行中的旧数据仍可读(列保留),仅新行不再写入