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

128 lines
9.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:安全 + 静默失败(先做)
- [x] **① 补鉴权(修 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(无主数据同样拒绝)
- [x] **② 统一后台分派(修 F3,消 D5 一半)**
- 新建 `services/task_dispatcher.py`:Celery 可用走 `process_stp_task.delay`;否则 `asyncio.create_task` 并持有强引用(`_background_tasks` set + done_callback 回收)
- upload/batch 路由统一调用;`asyncio.Semaphore` 限制 API 进程内并发处理数
- [x] **③ 超时后重置 OCC executor(修 F2)**
- `asyncio.TimeoutError` 分支调用 `_reset_occ_executor()`:新建 executor、旧 executor `shutdown(wait=False)`
- 泄漏 1 个挂死线程远好于全队列堵死;生产环境确认 celery worker 必配(进程隔离天然免疫)
- [x] **④ 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 组装,语义正确)
- [x] **⑤ 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:性能与资源
- [x] **⑤ 任务视图 TTL 缓存(修 P1/P7 部分)**
- `TaskQueryService.get_task_view` 对 PG 路径(completed/failed)加进程内 TTL 缓存(60s)
- export-mold / cam 写参数后显式失效;删除重复的 HTMLFile 单独查询
- [x] **⑥ export_shapes_cache 改 LRU(修 P2)**
- OrderedDict LRU,`maxsize=32`,命中 `move_to_end`,满则逐出最旧(连原生 OCC shape 一起释放)
- [x] **⑦ 重启后 STEP->STL 现场转换(修 P6/F1 根因延伸)**
- 分析期已持久化各方案 cavity/core/分型面 STEP;重启后 cache miss 时下载已持久化的 STEP -> OCC 读取 -> 三角化 -> 写 STL
- `export-mold` 的 409 分支前新增此兜底,用户不再需要重新分析
- [x] **⑧ 收尾(修 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:架构清理
- [x] **⑨ 删死代码(修 D1/D2)**
- 删除:`services/storage_integration.py`、`storage/object_storage.py`、`services/storage_service.py`、`src/main.py`、根 `Dockerfile`
- 删前 `grep -r` 确认零引用(动态引用也排查)
- [x] **⑪ 配置收敛(修 D4 后半)**
- `settings` 改惰性校验:DB 配置缺失不在 import 时 raise,改为首次访问 `DATABASE_URL` 时报清晰错误
- 解锁 `import shared.*` 无 env 场景(测试环境)
- [x] **⑫ 测试建设(修 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` 停写后,历史行中的旧数据仍可读(列保留),仅新行不再写入