This commit is contained in:
2026-08-31 18:01:34 +08:00
parent 3ea59551db
commit bee439cf34
46 changed files with 1884 additions and 1898 deletions
+315
View File
@@ -0,0 +1,315 @@
# 前端独立部署 + 统一后端入口实施计划
> 目标:在已经切换到“前端独立部署 + 同域反代”的基础上,进一步取消前端 Nginx 对 `/api` 的路径级分流,改为反代到一个真正的 **unified backend**,一次性解决长期维护成本。
---
## 1. 背景
当前项目已经完成了两项关键演进:
1. 前端从历史 `static/` 托管模式中抽离,开始走独立构建与独立部署
2. 前端通过同域 Nginx 反代访问后端 API 与分析产物
但当前 Nginx 仍然承担了“后端路由所有权判断”的职责:
- 一部分 `/api/...` 被转发到 `moldinsight`
- 另一部分 `/api/...` 被转发到 `inventory`
这虽然能跑通当前功能,但长期存在明显问题:
- 每新增一个 gemold API,Nginx 都要同步改配置
- Nginx 配置承担了业务边界知识,维护成本高
- `/health` 只能代表某一套后端,而不是统一入口
- 与“unified / gemold-only / inventory-only”三种部署模式的目标不完全一致
因此,本轮改造的目标是:
> 把前端入口反代逻辑从“按路径分流到两套后端”升级为“统一反代到一个 unified backend”。
---
## 2. 目标状态
### 浏览器视角
浏览器始终只访问一个同域入口:
- `/` → 前端静态页面
- `/api/*` → unified backend
- `/health` → unified backend
- `/html/*` → unified backend(由 unified backend 再提供 gemold 产物访问)
### Nginx 视角
Nginx 不再负责理解 gemold / inventory 的业务边界。
它只做两件事:
1. 提供前端静态文件与 SPA fallback
2. 把 `/api`、`/health`、`/html` 统一转发给一个 backend upstream
### 后端视角
后端新增一个统一入口,负责组合:
- auth
- moldinsight routes
- inventory routes
- `/health`
- `/html`
同时继续保留:
- `moldinsight-only`
- `inventory-only`
以满足模块独立部署场景。
---
## 3. 设计决策
### 3.1 为什么要引入 unified backend
因为前端与网关层最适合面对的是一个统一后端,而不是两套需要网关手工分流的内部模块。
收益:
- Nginx 配置显著简化
- 新增 API 不需要修改网关规则
- 文档和运维认知更简单
- 前端保持统一 `/api` 契约
- 更符合模块化蓝图中对 `unified` 模式的定义
### 3.2 为什么不直接把 split 模式删掉
因为:
- `gemold-only` 和 `inventory-only` 仍然有独立部署价值
- 当前仓库已经形成了清晰模块边界
- 统一入口应该成为**前端同域反代的默认方案**,而不是抹掉模块部署模式
所以最终保留三类入口:
- `src/entrypoints/unified.py`
- `src/entrypoints/moldinsight.py`
- `src/entrypoints/inventory.py`
---
## 4. 需要改动的核心文件
## 4.1 新增 unified 入口
新增:
- `src/entrypoints/unified.py`
职责:
- 基于 `shared.app_factory.create_app()` 创建应用
- 统一挂载:
- `moldinsight.api.router`(prefix=`/api`)
- `inventory.api.inventory_router`
- 使用:
- `mount_html=True`
- `serve_frontend_static=False`
- 不额外挂载 auth(交给 `app_factory`)
- 不手工重复定义 `/health`
## 4.2 简化前端 Nginx
修改:
- `deploy/nginx/frontend.conf`
从当前:
- 双 upstream:`moldinsight` / `inventory`
- 多个 `location /api/...` 手工分流
改成:
- 单 upstream:例如 `gemold_backend_upstream`
- 统一转发:
- `/api/` → unified backend
- `/health` → unified backend
- `/html/` → unified backend
保留:
- `/` 的 SPA fallback
- `/assets/` 的静态缓存策略
## 4.3 调整 Compose
修改:
- `docker-compose.yml`
目标:
- 增加 unified backend 服务
- `frontend` 只依赖 unified backend
- 保留 `moldinsight-celery`
- 按需保留 split backend 入口作为独立 profile
建议最终 profile 语义:
- `full`:frontend + unified + celery
- `frontend`:仅前端入口
- `moldinsight`:仅 gemold-only
- `inventory`:仅 inventory-only
- (可选)`unified`:仅 unified backend
## 4.4 视实现需要调整 Dockerfile
可能新增:
- `deploy/Dockerfile.unified`
或复用已有:
- `deploy/Dockerfile.moldinsight`
取决于是否希望 unified backend 使用单独镜像名。
统一要求:
- unified backend 镜像必须包含:
- `src/moldinsight/`
- `src/inventory/`
- `src/shared/`
- `src/entrypoints/unified.py`
## 4.5 文档同步
需要更新:
- `README.md`
- `frontend/README.md`
- `docs/deployment/LINUX_SETUP.md`
- `docs/deployment/DEPLOY_PORT.md`
- `docs/deployment/PORT_CONFIG.md`
重点改动:
- 当前推荐部署方式改为“frontend + unified backend + celery”
- 说明 split 模式仍保留,但不再是前端同域反代默认方式
- 端口说明中要区分:
- 前端入口端口
- unified backend 内部/对外端口
- gemold-only / inventory-only 模块端口
---
## 5. 路由与冲突评估
根据当前代码结构,unified 模式可行,主要原因:
- inventory 所有业务路由都挂在 `/api` 下,并且以独立业务前缀区分
- moldinsight 业务路由同样挂在 `/api` 下,但使用不同子路径
- auth 路由使用 `/api/auth`
- top-level `/health` 由 `app_factory` 提供
- moldinsight 内部还有 `/api/health`,与 top-level `/health` 不冲突
- `/html` 只有 moldinsight 需要
关键约束:
1. unified 入口中不要重复 include auth
2. unified 入口中不要手工再定义 top-level `/health`
3. unified 入口必须 `mount_html=True`
---
## 6. 风险与控制
### 风险 1:统一入口与现有 split 入口行为不一致
**控制:**
- 保留现有 `moldinsight.py` 与 `inventory.py`
- 只把 unified 作为前端默认 upstream
### 风险 2:`/health` 语义变化
当前前端只请求一个 `/health`,但 split 时代它实际上只代表某个后端。
**控制:**
- unified 上的 `/health` 明确作为“前端默认 backend 健康入口”
- 文档中明确其语义
### 风险 3:`/html` 丢失或不可达
**控制:**
- unified backend 继续 `mount_html=True`
- 前端 Nginx 保留 `/html/` 反代
### 风险 4:Compose、Nginx、文档不同步
**控制:**
- 先写本计划文档
- 再改 unified 入口、Nginx、Compose
- 最后统一 README 与 deployment docs
---
## 7. 验证方案
## 7.1 路由验证
unified backend 启动后应验证:
- `/api/auth/login`
- `/api/auth/me`
- `/api/upload`
- `/api/status/{task_id}`
- `/api/history`
- `/api/cost-estimate`
- `/api/products`
- `/api/inventory`
- `/api/dashboard`
- `/api/finance/*`
- `/health`
- `/html/...`
## 7.2 前端验证
前端同域访问应验证:
- `/login`
- `/moldinsight`
- `/inventory`
- `/moldinsight/result/:taskId`
关键交互:
- 登录
- 模具上传
- 任务轮询
- 成本估算
- 产品/库存/订单页面加载
- `/html` 分析结果页访问
## 7.3 Compose 验证
完整系统:
```bash
docker compose --profile full up -d
```
应满足:
- `frontend` 正常提供页面
- `frontend` 只反代一个 unified backend
- `moldinsight-celery` 正常运行
- 不再依赖 Nginx 路径级业务分流
---
## 8. 实施顺序
1. 新增 `docs/FRONTEND_UNIFIED_DEPLOYMENT_PLAN.md`
2. 新增 `src/entrypoints/unified.py`
3. 修改 `deploy/nginx/frontend.conf`
4. 修改 `docker-compose.yml`
5. 按需要修改 Dockerfile / 构建脚本
6. 更新 README 与 deployment docs
7. 做一致性验证
---
## 9. 最终预期
完成后,系统对外部署形态将变成:
- 前端:独立 Nginx 静态站点
- 网关:同域同入口
- 后端:一个 unified backend 作为前端默认 upstream
- worker:保留 gemold Celery 异步处理
- split 模式:继续作为模块独立部署能力保留
这能一次性解决当前“前端入口依赖 Nginx 路径级业务分流”的长期维护问题。
+127
View File
@@ -0,0 +1,127 @@
# 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` 停写后,历史行中的旧数据仍可读(列保留),仅新行不再写入
+10 -6
View File
@@ -4,9 +4,9 @@
当前推荐部署对象:
- gemold API
- frontend(Nginx,同域入口)
- unified backend
- gemold Celery worker(无 HTTP 端口)
- inventory API
以下基础设施默认由服务器现有服务提供,不在本项目 compose 中重复部署:
@@ -20,8 +20,10 @@
| 组件 | 默认端口 | 说明 |
|---|---:|---|
| gemold API | 8000 | 模具分析后端 |
| inventory API | 8001 | 进销存后端 |
| frontend | 80 | 前端 Nginx,同域入口 |
| unified backend | 8000 | 当前推荐统一后端 |
| gemold API | 8000 | 模具分析独立部署时使用 |
| inventory API | 8001 | 进销存独立部署时使用 |
| PostgreSQL | 5432 | 共享数据库 |
| Redis | 6379 | 共享队列/缓存 |
| MinIO API | 9000 | 对象存储接口 |
@@ -67,8 +69,10 @@
关键端口映射:
- `MOLDINSIGHT_PORT` → gemold API 外部端口
- `INVENTORY_PORT` → inventory API 外部端口
- `FRONTEND_PORT` → frontend Nginx 外部端口
- `BACKEND_PORT` → unified backend 外部端口
- `MOLDINSIGHT_PORT` → gemold-only 独立部署端口
- `INVENTORY_PORT` → inventory-only 独立部署端口
示例:
+29 -6
View File
@@ -4,7 +4,7 @@
当前项目支持三种部署模式:
- **unified**:gemold + inventory 统一部署
- **unified**:frontend + unified backend + celery,统一对外部署(当前推荐)
- **gemold-only**:仅部署模具分析后端
- **inventory-only**:仅部署进销存后端
@@ -140,6 +140,19 @@ RUSTFS_SECRET_KEY=minioadmin
## 6. 启动方式
## 6.0 frontend(同域反代入口)
当前推荐把前端作为独立静态站点部署,并通过同域 Nginx 反代到 unified backend:
- `/` → 前端静态资源与 SPA 路由
- `/api` → unified backend
- `/health` → unified backend
- `/html` → unified backend(内部再提供 gemold 分析产物)
如果使用根目录 [docker-compose.yml](../../docker-compose.yml) 的 `frontend` 服务,则该入口已经内置在前端 Nginx 镜像中。
---
## 6.1 inventory-only
```bash
@@ -285,7 +298,16 @@ WantedBy=multi-user.target
---
## 8. Nginx 反向代理示例
## 8. Nginx / 前端同域反代示例
当前仓库已提供前端 Nginx 配置:
- [deploy/nginx/frontend.conf](../../deploy/nginx/frontend.conf)
如果不使用仓库内 `frontend` 容器,也应遵循同样原则:
- `/` 提供前端静态资源与 SPA fallback
- `/api/` 反代后端
- `/health` 反代后端
- `/html/` 反代 gemold
### 8.1 inventory-only
@@ -373,11 +395,12 @@ curl http://127.0.0.1:8000/health
## 11. Docker Compose 说明
当前 [docker-compose.yml](../../docker-compose.yml) 仅启动:
当前 [docker-compose.yml](../../docker-compose.yml) 会启动:
- `moldinsight`
- `frontend`
- `backend`
- `moldinsight-celery`
- `inventory`
- 可选:`moldinsight` / `inventory`(独立模块模式)
它**不会**再拉起:
@@ -385,7 +408,7 @@ curl http://127.0.0.1:8000/health
- Redis
- MinIO
这些基础设施应由服务器现有服务提供,并通过 `.env` 传入连接信息。
这些基础设施应由服务器现有服务提供,并通过 `.env` 传入连接信息;前端则由 `frontend` 容器独立提供,并通过同域反代转发到后端。
示例:
+6 -2
View File
@@ -25,6 +25,8 @@
核心环境变量:
```env
FRONTEND_PORT=80
BACKEND_PORT=8000
MOLDINSIGHT_PORT=8000
INVENTORY_PORT=8001
```
@@ -48,8 +50,10 @@ RUSTFS_ENDPOINT=http://localhost:9000
| 变量 / 端口 | 用途 |
|---|---|
| `MOLDINSIGHT_PORT` | gemold API 宿主机暴露端口 |
| `INVENTORY_PORT` | inventory API 宿主机暴露端口 |
| `FRONTEND_PORT` | 前端 Nginx 宿主机暴露端口 |
| `BACKEND_PORT` | unified backend 宿主机暴露端口 |
| `MOLDINSIGHT_PORT` | gemold-only 独立部署端口 |
| `INVENTORY_PORT` | inventory-only 独立部署端口 |
| `DB_PORT` | PostgreSQL 端口 |
| `REDIS_PORT` | Redis 端口 |
| `9000` | MinIO/RustFS S3 兼容 API |