This commit is contained in:
2026-09-18 18:21:01 +08:00
parent e728dcd226
commit c51e6b793a
10 changed files with 71 additions and 53 deletions
+4 -4
View File
@@ -34,13 +34,13 @@
``` ```
src/ src/
entrypoints/ # 独立部署入口(均为 create_app 组装,含 sys.path 修正) entrypoints/ # 独立部署入口(纯组装:sys.path 修正 + create_app + startup_hooks/register_routers)
moldinsight.py # moldinsight-only 入口:/api 前缀挂 moldinsight router,端口 8000 moldinsight.py # moldinsight-only 入口:/api 前缀挂 moldinsight router,端口 8000
inventory.py # inventory-only 入口:inventory_router,端口 8001 inventory.py # inventory-only 入口:inventory_router,端口 8001
unified.py # 双模块统一入口:/api 挂 moldinsight + inventory,当前推荐后端 unified.py # 双模块统一入口:/api 挂 moldinsight + inventory,当前推荐后端
moldinsight/ # 【模具分析模块】 moldinsight/ # 【模具分析模块】
api/ api/
__init__.py # router 聚合:ROUTE_MODULES 清单 + _safe_include 挂载,失败登记 route_registry(/api/health 呈现 degraded,DEBUG 下 fail fast);debug_router 仅 settings.DEBUG 挂载 __init__.py # router 聚合:ROUTE_MODULES 清单 + _safe_include 挂载,失败登记 route_registry(/api/health 呈现 degraded,DEBUG 下 fail fast);register_moldinsight_routers 入口单点调用(/api 聚合 + HTML 报告根路径挂载);debug_router 仅 settings.DEBUG 挂载
route_registry.py # 路由装载注册表(loaded / failed / disabled,health_router 引用) route_registry.py # 路由装载注册表(loaded / failed / disabled,health_router 引用)
health_router.py # /api/health 模块健康检查(含真实 pythonocc 探测与路由装载状态) health_router.py # /api/health 模块健康检查(含真实 pythonocc 探测与路由装载状态)
upload_router.py # /api/upload STEP/STP 上传 upload_router.py # /api/upload STEP/STP 上传
@@ -93,7 +93,7 @@ src/
models/ # moldinsight 域 ORM(stp_analysis.py:stp_files 及各阶段产物 + processing_tasks,共 9 表) models/ # moldinsight 域 ORM(stp_analysis.py:stp_files 及各阶段产物 + processing_tasks,共 9 表)
storage/ storage/
rustfs_storage.py # RustFS/MinIO 客户端封装 rustfs_storage.py # RustFS/MinIO 客户端封装
init_storage.py # 存储初始化 init_storage.py # RustFS 连接初始化 + rustfs_startup_hook(入口经 startup_hooks 注入)
inventory/ # 【进销存模块】 inventory/ # 【进销存模块】
api/ # 每域一个 routes 文件:product / supplier / customer / warehouse / inventory / stock_movement / purchase_order / sales_order / purchase_demand / finance / dashboard / material api/ # 每域一个 routes 文件:product / supplier / customer / warehouse / inventory / stock_movement / purchase_order / sales_order / purchase_demand / finance / dashboard / material
schemas/ # 每域一个 Pydantic schema 文件(与 api 一一对应) schemas/ # 每域一个 Pydantic schema 文件(与 api 一一对应)
@@ -101,7 +101,7 @@ src/
models/ # inventory 域 ORM(catalog / warehouse / trading / finance 四文件,共 15 表) models/ # inventory 域 ORM(catalog / warehouse / trading / finance 四文件,共 15 表)
utils.py utils.py
shared/ # 【共享平台层:只放真正跨模块复用的基础能力,勿堆业务】 shared/ # 【共享平台层:只放真正跨模块复用的基础能力,勿堆业务】
app_factory.py # create_app:request_id 日志中间件 / auth_router / /health / SPA fallback / connect_rustfs 开关(D11 后 /html 由 moldinsight 代理路由提供,不再挂本地 StaticFiles) app_factory.py # create_app:纯平台引导(CORS / 请求日志 / /health / SPA fallback / db+Redis 启动);模块专属接线经 startup_hooks 注入(D3 收敛,原 connect_rustfs 已移除)
config/settings.py # Settings 单例:dotenv + os.getenv;DB_*/SECRET_KEY 惰性校验无默认 config/settings.py # Settings 单例:dotenv + os.getenv;DB_*/SECRET_KEY 惰性校验无默认
database/database.py # async engine / session / get_db_session database/database.py # async engine / session / get_db_session
database/init_db.py # 建表与管理员种子 database/init_db.py # 建表与管理员种子
+10 -4
View File
@@ -75,8 +75,8 @@ geMoldInsight 的目标架构不是微服务,也不是继续维持历史单体
- 应用工厂与共用中间件 - 应用工厂与共用中间件
说明: 说明:
- `shared` 当前仍是“共享平台层 + 历史耦合区”的混合体 - `shared` 的定位已明确为**平台层(跨模块基础能力)**;模块专属接线(RustFS 启动 / HTML 报告挂载 / 路由聚合)已收敛回模块层(见 §6.2),当前仓库结构仍以 `shared` 为事实名称
- 后续会继续向更清晰的 platform 语义收敛,但当前仓库结构仍以 `shared` 为事实名称 - 剩余语义收敛(identity 平台表 vs 模块表的命名与注释口径)随实际重构继续推进
--- ---
@@ -191,9 +191,15 @@ geMoldInsight/
跨模块只允许裸 FK(规则见 §5.1);全量模型注册点收敛为 `migrations/env.py` 与 `tests/conftest.py`;归属边界由 [tests/test_model_ownership.py](../tests/test_model_ownership.py) 锁定(含单模块独立 mapper 配置与旧模块无 facade 断言)。 跨模块只允许裸 FK(规则见 §5.1);全量模型注册点收敛为 `migrations/env.py` 与 `tests/conftest.py`;归属边界由 [tests/test_model_ownership.py](../tests/test_model_ownership.py) 锁定(含单模块独立 mapper 配置与旧模块无 facade 断言)。
### 6.2 app factory 组合职责偏重 ### 6.2 app factory 组合职责 —— 已收敛(2026-09-18,D3 剩余)
当前 [src/shared/app_factory.py](../src/shared/app_factory.py) 仍承担较多平台与模块组合职责,是后续平台层收敛的重点。 平台层与模块专属接线的边界已明确(`shared` = 跨模块基础能力,模块专属接线归属模块层):
- **平台工厂 [src/shared/app_factory.py](../src/shared/app_factory.py) 只做纯平台引导**:CORS / 请求日志 / 目录准备 / 静态托管 / 数据库与 Redis 启动 / auth 路由 / /health / SPA fallback。`startup_hooks` 参数承载模块专属启动接线——原 `connect_rustfs` 参数(平台工厂持有 moldinsight 依赖)已移除。
- **moldinsight 专属接线收敛回 moldinsight 层**:
- RustFS 启动 → [init_storage.py](../src/moldinsight/storage/init_storage.py) 的 `rustfs_startup_hook`(moldinsight/unified 入口经 `startup_hooks` 注入)
- /api 路由聚合 + HTML 报告根路径挂载 → [moldinsight/api/__init__.py](../src/moldinsight/api/__init__.py) 的 `register_moldinsight_routers`(入口只做单点调用,不再重复 include html_report)
- **入口 [src/entrypoints/](../src/entrypoints/) 退化为纯组装**:sys.path 修正 + 调 create_app + 传 startup_hooks / register_routers。
### 6.3 文档与结构尚未完全同步 ### 6.3 文档与结构尚未完全同步
+2
View File
@@ -3,6 +3,8 @@
> 文档定位:**唯一的「现在到哪了」**。README / AGENTS / 各主文档只链接到这里,不复制状态内容。 > 文档定位:**唯一的「现在到哪了」**。README / AGENTS / 各主文档只链接到这里,不复制状态内容。
> 维护规则:每完整完成一个需求,**倒序在本文顶部加一条**(日期 + 主题 + 关键事实);其余主文档(架构 / 规划 / 技术债 / 部署)维护各自的"当前有效说法",本文只记录"什么时候做到了哪一步"。维护规则出处见根目录 [AGENTS.md](../AGENTS.md)。 > 维护规则:每完整完成一个需求,**倒序在本文顶部加一条**(日期 + 主题 + 关键事实);其余主文档(架构 / 规划 / 技术债 / 部署)维护各自的"当前有效说法",本文只记录"什么时候做到了哪一步"。维护规则出处见根目录 [AGENTS.md](../AGENTS.md)。
> 2026-09-18(**D3 剩余收敛:app_factory 平台层去模块化 + 路由单点聚合**:① 平台工厂 [app_factory.py](../src/shared/app_factory.py) 只做纯平台引导——`connect_rustfs` 参数移除,模块专属启动接线改经 `startup_hooks` 注入;RustFS 连接收敛为 [init_storage.py](../src/moldinsight/storage/init_storage.py) 的 `rustfs_startup_hook`(moldinsight/unified 入口注入)。② 路由挂载收敛——[moldinsight/api/__init__.py](../src/moldinsight/api/__init__.py) 新增 `register_moldinsight_routers` 单点聚合(/api 聚合路由 + HTML 报告根路径挂载),moldinsight/unified 入口不再各自重复 include html_report,退化为纯组装。③ 边界语义入文档:ARCHITECTURE §6.2/§2 说明更新(shared = 平台层,模块专属接线归属模块层);TECH_DEBT D3 剩余仅 identity/platform 注释口径。**接口面零变化**(openapi 无漂移已验)。**测试基线**:**144 passed, 0 skipped**(新增 [test_html_report_router.py](../tests/test_html_report_router.py) 单点聚合注册 1 项)。)
> 2026-09-18(**批次 4 后续专项五项完成:D11 清偿 + 部署参数 + D2 诚实标注 + CI 门禁 + OCC 方案 B 实施**:① **D11 清偿**(TECH_DEBT P2)——可视化报告 RustFS 单源化:写侧 HTMLGenerator 每任务写临时目录,`.html`/`_summary.json`/`_data.json` 三件统一裸传 RustFS 报告键 `html/reports/{filename}`(文件名寻址),`/html` StaticFiles 本地挂载删除,新增 [html_report_router.py](../src/moldinsight/api/html_report_router.py) 根路径代理(报告键直取 → 遗留 `html/{hash}.json` JSON 包装解析 → 本地卷存量兜底 → 404;URL 形状 `/html/{filename}` 不变,持久化 cavity JSON 与前端 iframe 引用零迁移);celery 服务摘除 `html_data` 卷,Dockerfile.moldinsight 删除 `COPY html_output/`(构建机陈旧报告不再进镜像);已知约束:报告路由不做认证(iframe 无法携带 Authorization 头,沿用 StaticFiles 时代既定姿态,文档已声明);顺带删除 `get_stp_file_with_data` 的死数据块(`html_content` 组装零消费方,此前每次完成任务查询白下载数 MB 正文)。② **OCC 方案 A 参数落地**——`CELERY_CONCURRENCY`/`CELERY_MAX_TASKS_PER_CHILD` 进 [Dockerfile.celery](../deploy/Dockerfile.celery) ENV + compose 透传 + `.env.example`。③ **D2 清偿**——铝价响应带 `source: "simulated"`,[HomeView.vue](../frontend/src/modules/home/HomeView.vue) 按来源渲染"模拟数据 · 参考走势"标注(原硬编码"上海期货交易所"属虚假声明),死代码 `getAluminumPrice` 删除。④ **CI 门禁**——[.gitea/workflows/ci.yml](.gitea/workflows/ci.yml) 三 job:pytest 全量 / 前端构建(含 vue-tsc)/ openapi 漂移检测(conda pythonocc 环境重导出比对;已实测 pytest 与导出均不依赖 .env)。⑤ **OCC 方案 B 实施**(TECH_DEBT D10 清偿)——`run_occ(fn, *args)` → `run_occ(op_name, payload)`,执行器由线程池替换为常驻工作进程池 [occ_process_pool.py](../src/moldinsight/services/occ_process_pool.py) + 操作注册表 [occ_worker.py](../src/moldinsight/core/occ_worker.py):超时/崩溃 terminate 换新补位、任务级超时 recover 整体重建,**残留线程泄漏根治**(进程边界回收 C++ 栈);TopoDS 形状不跨进程(`generate_cavity` 分模 + 方案 STEP 持久化导出全在子进程内,返回 export_manifest);调用点全量迁移(解析/网格/型腔/分析/倒扣/STEP 转换),删除内存形状缓存链(`_cache_export_shapes`/`get_export_shapes`/`_persist_step_exports`)、`CADExporter.export_mold_results`(零调用方)、shape_loader(→ [stp_materializer.py](../src/moldinsight/services/stp_materializer.py));回归测试 [test_occ_process_pool.py](../tests/test_occ_process_pool.py)(OCC-gated,6 例含真实盒体 STP 解析/分模端到端)。**接口变更三件套随批完成**:openapi.json 重导出(76→77 paths,新增 `/html/{filename}`)+ 前端 `gen:api` 再生 + 前端构建通过(方案 B 接口面零变化,无路由/schema 变更)。**测试基线**:**143 passed, 0 skipped**(D11 8 项 + 铝价 2 项 + OCC 进程池 6 项;基线 129 中原 2 个 skip 已随本地环境补齐 celery/alembic 转为执行)。**下一步**:回到 §3 主线 P2/P3 长期方向——D3 剩余收敛(app_factory 参数收敛、identity/platform 语义)、inventory 服务下沉、D13 pip 锁文件随下次镜像构建补齐,见 [ROADMAP.md](ROADMAP.md) §3。) > 2026-09-18(**批次 4 后续专项五项完成:D11 清偿 + 部署参数 + D2 诚实标注 + CI 门禁 + OCC 方案 B 实施**:① **D11 清偿**(TECH_DEBT P2)——可视化报告 RustFS 单源化:写侧 HTMLGenerator 每任务写临时目录,`.html`/`_summary.json`/`_data.json` 三件统一裸传 RustFS 报告键 `html/reports/{filename}`(文件名寻址),`/html` StaticFiles 本地挂载删除,新增 [html_report_router.py](../src/moldinsight/api/html_report_router.py) 根路径代理(报告键直取 → 遗留 `html/{hash}.json` JSON 包装解析 → 本地卷存量兜底 → 404;URL 形状 `/html/{filename}` 不变,持久化 cavity JSON 与前端 iframe 引用零迁移);celery 服务摘除 `html_data` 卷,Dockerfile.moldinsight 删除 `COPY html_output/`(构建机陈旧报告不再进镜像);已知约束:报告路由不做认证(iframe 无法携带 Authorization 头,沿用 StaticFiles 时代既定姿态,文档已声明);顺带删除 `get_stp_file_with_data` 的死数据块(`html_content` 组装零消费方,此前每次完成任务查询白下载数 MB 正文)。② **OCC 方案 A 参数落地**——`CELERY_CONCURRENCY`/`CELERY_MAX_TASKS_PER_CHILD` 进 [Dockerfile.celery](../deploy/Dockerfile.celery) ENV + compose 透传 + `.env.example`。③ **D2 清偿**——铝价响应带 `source: "simulated"`,[HomeView.vue](../frontend/src/modules/home/HomeView.vue) 按来源渲染"模拟数据 · 参考走势"标注(原硬编码"上海期货交易所"属虚假声明),死代码 `getAluminumPrice` 删除。④ **CI 门禁**——[.gitea/workflows/ci.yml](.gitea/workflows/ci.yml) 三 job:pytest 全量 / 前端构建(含 vue-tsc)/ openapi 漂移检测(conda pythonocc 环境重导出比对;已实测 pytest 与导出均不依赖 .env)。⑤ **OCC 方案 B 实施**(TECH_DEBT D10 清偿)——`run_occ(fn, *args)` → `run_occ(op_name, payload)`,执行器由线程池替换为常驻工作进程池 [occ_process_pool.py](../src/moldinsight/services/occ_process_pool.py) + 操作注册表 [occ_worker.py](../src/moldinsight/core/occ_worker.py):超时/崩溃 terminate 换新补位、任务级超时 recover 整体重建,**残留线程泄漏根治**(进程边界回收 C++ 栈);TopoDS 形状不跨进程(`generate_cavity` 分模 + 方案 STEP 持久化导出全在子进程内,返回 export_manifest);调用点全量迁移(解析/网格/型腔/分析/倒扣/STEP 转换),删除内存形状缓存链(`_cache_export_shapes`/`get_export_shapes`/`_persist_step_exports`)、`CADExporter.export_mold_results`(零调用方)、shape_loader(→ [stp_materializer.py](../src/moldinsight/services/stp_materializer.py));回归测试 [test_occ_process_pool.py](../tests/test_occ_process_pool.py)(OCC-gated,6 例含真实盒体 STP 解析/分模端到端)。**接口变更三件套随批完成**:openapi.json 重导出(76→77 paths,新增 `/html/{filename}`)+ 前端 `gen:api` 再生 + 前端构建通过(方案 B 接口面零变化,无路由/schema 变更)。**测试基线**:**143 passed, 0 skipped**(D11 8 项 + 铝价 2 项 + OCC 进程池 6 项;基线 129 中原 2 个 skip 已随本地环境补齐 celery/alembic 转为执行)。**下一步**:回到 §3 主线 P2/P3 长期方向——D3 剩余收敛(app_factory 参数收敛、identity/platform 语义)、inventory 服务下沉、D13 pip 锁文件随下次镜像构建补齐,见 [ROADMAP.md](ROADMAP.md) §3。)
> 2026-09-17(**批次 4(架构演进)完成,§3.1 治理批次全部执行完毕**:① D3 主体清偿——891 行的旧 `shared/models/database.py`(31 模型类三类同居,已删除)按归属拆为 [shared/models/base.py](../src/shared/models/base.py)(唯一 Base)+ [shared/models/identity.py](../src/shared/models/identity.py)(身份权限 7 表)+ [moldinsight/models/](../src/moldinsight/models/)(分析域 9 表)+ [inventory/models/](../src/inventory/models/)(进销存 15 表,catalog/warehouse/trading/finance 四文件);**三条跨模块 ORM relationship(`User.stp_files` / `STPFile.user` / `STPFile.product`)经全仓核实零使用,直接删除**——跨模块桥接收敛为裸 FK 硬规则([ARCHITECTURE.md](ARCHITECTURE.md) §5.1),单模块部署 mapper 可独立配置;约 45 处 import 全量改写(含 migrations/env.py 全量注册、scripts/ 两个一次性脚本),旧模块物理删除无兼容 facade;零调用方死方法 `db_manager.create_tables` 一并删除(拆分后会静默建残缺 schema);② D10 治理——`_reset_occ_executor` 补 `cancel_futures=True`(旧实现下"慢恢复"的旧线程会继续消化旧队列,与新 executor **并发操作非线程安全的 OCC**,属数据竞争而非单纯泄漏);吞吐方案设计先行定稿 [topics/performance/OCC_THROUGHPUT.md](topics/performance/OCC_THROUGHPUT.md)(短期 A:celery `--concurrency` 伸缩 + `--max-tasks-per-child` 兜底,启动参数已记 [OPERATIONS.md](OPERATIONS.md) §3;中期 B:`run_occ` 契约进程化 + kill-on-timeout,待独立排期);③ 新增 [tests/test_model_ownership.py](../tests/test_model_ownership.py) 锁定归属边界(31 表全量注册 / 单模块独立 mapper 配置 / 旧模块无 facade);④ 顺手清偿 D15——[vite.config.ts](../frontend/vite.config.ts) 删除未用的 `mode` 参数,`vue-tsc -b` 恢复通过,前端生产构建链路解除阻断。**接口面零变化**(无路由与 schema 变更,openapi.json 不触发重导出)。**测试基线**:**125 passed, 2 skipped**(基线 122 + 新增归属测试 3 项)。**下一步**:治理批次收尾后回到主线方向;遗留项 D11(HTML RustFS 单源)、D13(pip 锁文件)、OCC 方案 B 独立批次。) > 2026-09-17(**批次 4(架构演进)完成,§3.1 治理批次全部执行完毕**:① D3 主体清偿——891 行的旧 `shared/models/database.py`(31 模型类三类同居,已删除)按归属拆为 [shared/models/base.py](../src/shared/models/base.py)(唯一 Base)+ [shared/models/identity.py](../src/shared/models/identity.py)(身份权限 7 表)+ [moldinsight/models/](../src/moldinsight/models/)(分析域 9 表)+ [inventory/models/](../src/inventory/models/)(进销存 15 表,catalog/warehouse/trading/finance 四文件);**三条跨模块 ORM relationship(`User.stp_files` / `STPFile.user` / `STPFile.product`)经全仓核实零使用,直接删除**——跨模块桥接收敛为裸 FK 硬规则([ARCHITECTURE.md](ARCHITECTURE.md) §5.1),单模块部署 mapper 可独立配置;约 45 处 import 全量改写(含 migrations/env.py 全量注册、scripts/ 两个一次性脚本),旧模块物理删除无兼容 facade;零调用方死方法 `db_manager.create_tables` 一并删除(拆分后会静默建残缺 schema);② D10 治理——`_reset_occ_executor` 补 `cancel_futures=True`(旧实现下"慢恢复"的旧线程会继续消化旧队列,与新 executor **并发操作非线程安全的 OCC**,属数据竞争而非单纯泄漏);吞吐方案设计先行定稿 [topics/performance/OCC_THROUGHPUT.md](topics/performance/OCC_THROUGHPUT.md)(短期 A:celery `--concurrency` 伸缩 + `--max-tasks-per-child` 兜底,启动参数已记 [OPERATIONS.md](OPERATIONS.md) §3;中期 B:`run_occ` 契约进程化 + kill-on-timeout,待独立排期);③ 新增 [tests/test_model_ownership.py](../tests/test_model_ownership.py) 锁定归属边界(31 表全量注册 / 单模块独立 mapper 配置 / 旧模块无 facade);④ 顺手清偿 D15——[vite.config.ts](../frontend/vite.config.ts) 删除未用的 `mode` 参数,`vue-tsc -b` 恢复通过,前端生产构建链路解除阻断。**接口面零变化**(无路由与 schema 变更,openapi.json 不触发重导出)。**测试基线**:**125 passed, 2 skipped**(基线 122 + 新增归属测试 3 项)。**下一步**:治理批次收尾后回到主线方向;遗留项 D11(HTML RustFS 单源)、D13(pip 锁文件)、OCC 方案 B 独立批次。)
+4 -4
View File
@@ -99,17 +99,17 @@
优先级:**P2** 优先级:**P2**
### D3. shared/platform 边界仍需继续收敛 —— ORM 归属已清偿(2026-09-17,批次 4) ### D3. shared/platform 边界仍需继续收敛 —— 主体已清偿(2026-09-17 批次 4 + 2026-09-18 后续)
已完成部分: 已完成部分:
- 共享 ORM(原最强耦合点)按模块拆分:base / identity(shared)+ moldinsight/models + inventory/models;跨模块只允许裸 FK,单模块部署 mapper 可独立配置(详见 §2.8 与 [ARCHITECTURE.md](ARCHITECTURE.md) §6.1) - 共享 ORM(原最强耦合点)按模块拆分:base / identity(shared)+ moldinsight/models + inventory/models;跨模块只允许裸 FK,单模块部署 mapper 可独立配置(详见 §2.8 与 [ARCHITECTURE.md](ARCHITECTURE.md) §6.1)
- 旧 `shared/models/database.py` 物理删除,无兼容 facade;归属边界由 [tests/test_model_ownership.py](../tests/test_model_ownership.py) 锁定 - 旧 `shared/models/database.py` 物理删除,无兼容 facade;归属边界由 [tests/test_model_ownership.py](../tests/test_model_ownership.py) 锁定
- **app_factory 组合职责收敛**(2026-09-18):平台工厂只做纯平台引导,`connect_rustfs` 参数移除;moldinsight 专属接线(RustFS 启动钩子 [init_storage.py](../src/moldinsight/storage/init_storage.py) `rustfs_startup_hook`、路由单点聚合 [moldinsight/api/__init__.py](../src/moldinsight/api/__init__.py) `register_moldinsight_routers`)收敛回模块层,入口退化为纯组装(ARCHITECTURE §6.2)
仍保留的收敛方向(低优先级,随实际重构推进): 仍保留的收敛方向(低优先级,随实际重构推进):
- [app_factory.py](../src/shared/app_factory.py) 组合职责偏重(ARCHITECTURE §6.2) - identity / platform 的边界语义(ROADMAP §2.1):平台表与模块表的命名/注释口径随实际重构推进
- identity / platform 的边界语义(ROADMAP §2.1)
优先级:**P3**(剩余部分) 优先级:**P3**(仅剩 identity/platform 语义注释口径)
### D4. 文档现状 / 规划 / 历史混放 ### D4. 文档现状 / 规划 / 历史混放
+5 -11
View File
@@ -10,28 +10,22 @@ sys.path.insert(0, str(src_root))
os.chdir(Path(__file__).parent.parent.parent) os.chdir(Path(__file__).parent.parent.parent)
from shared.app_factory import create_app from shared.app_factory import create_app
from moldinsight.storage.init_storage import rustfs_startup_hook
def _register_routers(app): def _register_routers(app):
"""注册 moldinsight 业务路由""" """注册 moldinsight 业务路由(/api 聚合 + HTML 报告代理,收敛于 moldinsight.api)"""
try: try:
from moldinsight.api import router as moldinsight_router from moldinsight.api import register_moldinsight_routers
app.include_router(moldinsight_router, prefix="/api") register_moldinsight_routers(app)
except Exception as e: except Exception as e:
print(f"[WARN] MoldInsight 路由: {e}") print(f"[WARN] MoldInsight 路由: {e}")
# D11:HTML 报告代理挂根路径,URL 形状与原 StaticFiles 保持一致(/html/{filename})
try:
from moldinsight.api.html_report_router import include_into as include_html_report
include_html_report(app)
except Exception as e:
print(f"[WARN] HTML 报告路由: {e}")
app = create_app( app = create_app(
title="Gemold - 模具分析引擎", title="Gemold - 模具分析引擎",
service_name="moldinsight", service_name="moldinsight",
connect_rustfs=True,
serve_frontend_static=False, serve_frontend_static=False,
startup_hooks=[rustfs_startup_hook],
register_routers=_register_routers, register_routers=_register_routers,
) )
+5 -11
View File
@@ -10,23 +10,17 @@ sys.path.insert(0, str(src_root))
os.chdir(Path(__file__).parent.parent.parent) os.chdir(Path(__file__).parent.parent.parent)
from shared.app_factory import create_app from shared.app_factory import create_app
from moldinsight.storage.init_storage import rustfs_startup_hook
def _register_routers(app): def _register_routers(app):
"""注册 unified 业务路由""" """注册 unified 业务路由(moldinsight 单点聚合 + inventory)"""
try: try:
from moldinsight.api import router as moldinsight_router from moldinsight.api import register_moldinsight_routers
app.include_router(moldinsight_router, prefix="/api") register_moldinsight_routers(app)
except Exception as e: except Exception as e:
print(f"[WARN] MoldInsight 路由: {e}") print(f"[WARN] MoldInsight 路由: {e}")
# D11:HTML 报告代理挂根路径,URL 形状与原 StaticFiles 保持一致(/html/{filename})
try:
from moldinsight.api.html_report_router import include_into as include_html_report
include_html_report(app)
except Exception as e:
print(f"[WARN] HTML 报告路由: {e}")
try: try:
from inventory.api import inventory_router from inventory.api import inventory_router
app.include_router(inventory_router) app.include_router(inventory_router)
@@ -37,7 +31,7 @@ def _register_routers(app):
app = create_app( app = create_app(
title="Gemold - Unified Backend", title="Gemold - Unified Backend",
service_name="unified", service_name="unified",
connect_rustfs=True,
serve_frontend_static=False, serve_frontend_static=False,
startup_hooks=[rustfs_startup_hook],
register_routers=_register_routers, register_routers=_register_routers,
) )
+13
View File
@@ -4,6 +4,7 @@ import importlib
from shared.config.settings import settings from shared.config.settings import settings
from shared.utils.logger import get_logger from shared.utils.logger import get_logger
from moldinsight.api.route_registry import route_load_status from moldinsight.api.route_registry import route_load_status
from moldinsight.api.html_report_router import include_into as include_html_report
logger = get_logger(__name__) logger = get_logger(__name__)
@@ -54,3 +55,15 @@ def _safe_include(label: str, module_path: str, debug_only: bool = False):
for _label, _module_path, _debug_only in ROUTE_MODULES: for _label, _module_path, _debug_only in ROUTE_MODULES:
_safe_include(_label, _module_path, _debug_only) _safe_include(_label, _module_path, _debug_only)
def register_moldinsight_routers(app):
"""注册 moldinsight 全部业务路由到 app(D3 收敛:入口侧单点调用)。
- /api 聚合路由:各子路由模块装载失败经 ROUTE_MODULES/route_load_status 呈现
(/api/health degraded,DEBUG fail-fast),见 _safe_include
- HTML 报告代理挂根路径 /html/{filename}(URL 形状与原 StaticFiles 一致),
失败同样登记 route_load_status(html_report_router.include_into)
"""
app.include_router(router, prefix="/api")
include_html_report(app)
+9
View File
@@ -36,6 +36,15 @@ async def init_rustfs_storage():
return False return False
async def rustfs_startup_hook():
"""app_factory startup_hooks 注入点(D3 收敛):moldinsight/unified 入口把
本钩子传入 create_app(startup_hooks=[rustfs_startup_hook]),RustFS 连接接线
归属 moldinsight 层,平台工厂不再持有模块专属依赖(原 connect_rustfs 已移除)。
"""
ok = await init_rustfs_storage()
print(f"[{'OK' if ok else 'WARN'}] RustFS")
async def test_storage(): async def test_storage():
"""测试 RustFS 对象存储功能""" """测试 RustFS 对象存储功能"""
try: try:
+3 -19
View File
@@ -27,7 +27,6 @@ def create_app(
title: str, title: str,
service_name: str, service_name: str,
version: str = "4.0.0", version: str = "4.0.0",
connect_rustfs: bool = False,
serve_frontend_static: bool = False, serve_frontend_static: bool = False,
startup_hooks: Optional[List[Callable[[], Awaitable[None]]]] = None, startup_hooks: Optional[List[Callable[[], Awaitable[None]]]] = None,
register_routers: Optional[Callable[[FastAPI], None]] = None, register_routers: Optional[Callable[[FastAPI], None]] = None,
@@ -38,11 +37,10 @@ def create_app(
title: 应用标题 title: 应用标题
service_name: 服务名(用于 /health 响应) service_name: 服务名(用于 /health 响应)
version: 版本号 version: 版本号
connect_rustfs: 是否连接 RustFS 对象存储(moldinsight 需要)。
D11:/html 报告读取由 moldinsight.api.html_report_router 代理
(RustFS 报告键直取 + 遗留对象/本地卷兜底),不再挂本地 StaticFiles。
serve_frontend_static: 是否由后端托管 /static 与 SPA fallback(默认关闭,前端独立部署) serve_frontend_static: 是否由后端托管 /static 与 SPA fallback(默认关闭,前端独立部署)
startup_hooks: 额外的 startup 钩子列表(在数据库/RustFS/Redis 初始化后执行) startup_hooks: 额外的 startup 钩子列表(在数据库/Redis 初始化后执行)。
模块专属的启动接线(如 moldinsight 的 RustFS 连接)经此参数注入——
D3 收敛:平台工厂不再持有模块专属依赖(原 connect_rustfs 已移除)。
register_routers: 回调函数,用于注册业务路由 register_routers: 回调函数,用于注册业务路由
""" """
app = FastAPI(title=title, version=version) app = FastAPI(title=title, version=version)
@@ -115,20 +113,6 @@ def create_app(
success = await init_database(keep_connected=True) success = await init_database(keep_connected=True)
print(f"[{'OK' if success else 'FAIL'}] 数据库初始化") print(f"[{'OK' if success else 'FAIL'}] 数据库初始化")
# RustFS(仅 moldinsight 需要)
if connect_rustfs:
try:
from moldinsight.storage.rustfs_storage import rustfs_manager
await rustfs_manager.connect(
endpoint=settings.RUSTFS_ENDPOINT,
access_key=settings.RUSTFS_ACCESS_KEY,
secret_key=settings.RUSTFS_SECRET_KEY,
timeout=settings.RUSTFS_TIMEOUT,
)
print("[OK] RustFS连接成功")
except Exception as e:
print(f"[WARN] RustFS连接失败: {e}")
# Redis # Redis
try: try:
from shared.services.redis_task_manager import redis_task_manager from shared.services.redis_task_manager import redis_task_manager
+16
View File
@@ -158,6 +158,22 @@ async def test_json_media_type_from_extension(client, tmp_path):
assert "application/json" in resp.headers["content-type"] assert "application/json" in resp.headers["content-type"]
# ── D3 收敛:单点聚合注册 ───────────────────────────────────────
def test_register_moldinsight_routers_mounts_api_and_html():
"""register_moldinsight_routers 一次挂载 /api 聚合路由 + 根路径 /html 报告代理
(入口侧不再重复 include_html_report)。"""
from moldinsight.api import register_moldinsight_routers
application = FastAPI()
register_moldinsight_routers(application)
paths = {route.path for route in application.routes}
assert "/api/upload" in paths
# Starlette route.path 保留 :path 转换器(OpenAPI 路径才显示 /html/{filename})
assert any(p.startswith("/html/{") for p in paths)
# ── 未命中与防护 ──────────────────────────────────────────────── # ── 未命中与防护 ────────────────────────────────────────────────