diff --git a/.env.example b/.env.example index f5b1f2f..c37db1a 100644 --- a/.env.example +++ b/.env.example @@ -4,9 +4,13 @@ HOST=0.0.0.0 # ================================ # 模块 API 对外端口 # ================================ -# gemold(moldinsight)API 对外端口 +# 前端 Nginx 对外端口 +FRONTEND_PORT=80 +# unified backend 对外端口 +BACKEND_PORT=8000 +# gemold(moldinsight)API 对外端口(独立部署时使用) MOLDINSIGHT_PORT=8000 -# inventory API 对外端口 +# inventory API 对外端口(独立部署时使用) INVENTORY_PORT=8001 # ================================ diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 6350274..0000000 --- a/Dockerfile +++ /dev/null @@ -1,28 +0,0 @@ -# Legacy Dockerfile for geMoldInsight -# -# 说明: -# 该文件保留为历史/兼容用途,仍按旧的单体入口 `src/main.py` 组织。 -# 当前模块化部署的主入口应优先使用: -# - deploy/Dockerfile.moldinsight -# - deploy/Dockerfile.inventory -# - deploy/Dockerfile.celery -# 以及 deploy/docker-compose.yml - -FROM continuumio/miniconda3:latest - -WORKDIR /app -COPY . . - -RUN conda update -n base -c defaults conda -y && \ - conda create -n moldinsight python=3.11 pythonocc-core=7.9.0 -c conda-forge -y - -RUN . /opt/conda/etc/profile.d/conda.sh && \ - conda activate moldinsight && \ - pip install -r requirements.txt - -RUN mkdir -p uploads html_output logs -RUN chmod +x start.sh - -EXPOSE 8000 - -CMD ["/bin/bash", "-c", "source /opt/conda/etc/profile.d/conda.sh && conda activate moldinsight && python src/main.py"] diff --git a/README.md b/README.md index 2520c5e..8bd6e91 100644 --- a/README.md +++ b/README.md @@ -115,11 +115,13 @@ geMoldInsight/ │ └── package.json │ ├── alembic/ # Alembic migrations -├── deploy/ # 镜像构建与部署辅助文件 +├── deploy/ # 镜像构建、Nginx 配置与部署辅助文件 │ ├── Dockerfile.base │ ├── Dockerfile.moldinsight │ ├── Dockerfile.inventory -│ └── Dockerfile.celery +│ ├── Dockerfile.celery +│ ├── Dockerfile.frontend +│ └── nginx/ │ ├── docs/ ├── tests/ @@ -192,13 +194,14 @@ geMoldInsight/ 当前项目设计上支持三种模式: -### 1. unified -一个统一后端同时挂载 gemold + inventory。 +### 1. unified(当前推荐) +一个统一后端同时挂载 gemold + inventory,并作为前端同域反代的默认 backend。 适合: - 本地开发 - 集成环境 - 小团队统一部署 +- 前端独立部署 + 单 upstream 反代 ### 2. gemold-only 只部署模具分析后端。 @@ -300,15 +303,15 @@ RUSTFS_SECRET_KEY=your-secret-key 当前唯一 Compose 入口: - [docker-compose.yml](docker-compose.yml) -该 compose 文件**只启动项目自身容器**: -- `moldinsight` +该 compose 文件会启动: +- `frontend`(独立前端 Nginx 静态站点) +- `backend`(unified backend,当前推荐) - `moldinsight-celery` -- `inventory` +- 可选保留:`moldinsight` / `inventory`(模块独立部署 profile) -并通过 `.env` 连接服务器上**已经存在**的: -- PostgreSQL -- Redis -- RustFS / MinIO 兼容对象存储 +其中: +- 前端通过同域反代把 `/api`、`/health`、`/html` 转发给 unified backend +- PostgreSQL / Redis / RustFS / MinIO 兼容对象存储仍由服务器现有服务提供 示例: @@ -318,10 +321,12 @@ docker compose --profile full up -d 可选 profile: - `full` +- `frontend` +- `unified` - `moldinsight` - `inventory` -> 说明:`docker-compose.yml` 不再重复部署 postgres / redis / minio,而是复用服务器现有基础设施。 +> 说明:根目录 `docker-compose.yml` 是当前唯一 Compose 入口;前端已独立部署,并默认反代到 unified backend。 ### 方式 B:直接启动后端入口 @@ -337,7 +342,7 @@ inventory-only: uvicorn src.entrypoints.inventory:app --reload --host 0.0.0.0 --port 8001 ``` -### 方式 C:启动前端 +### 方式 C:单独启动前端开发服务器 ```bash cd frontend diff --git a/deploy/.env.example b/deploy/.env.example index 621c668..85c74e3 100644 --- a/deploy/.env.example +++ b/deploy/.env.example @@ -3,7 +3,9 @@ # ============================================ # 复制为 .env 并按服务器实际服务地址修改 -# API 对外端口 +# API / 前端对外端口 +FRONTEND_PORT=80 +BACKEND_PORT=8000 MOLDINSIGHT_PORT=8000 INVENTORY_PORT=8001 diff --git a/deploy/Dockerfile.frontend b/deploy/Dockerfile.frontend new file mode 100644 index 0000000..4fecb13 --- /dev/null +++ b/deploy/Dockerfile.frontend @@ -0,0 +1,13 @@ +FROM node:20-alpine AS build + +WORKDIR /app/frontend +COPY frontend/package*.json ./ +RUN npm install +COPY frontend/ ./ +RUN npm run build + +FROM nginx:1.27-alpine +COPY deploy/nginx/frontend.conf /etc/nginx/conf.d/default.conf +COPY --from=build /app/frontend/dist /usr/share/nginx/html + +EXPOSE 80 diff --git a/deploy/Dockerfile.inventory b/deploy/Dockerfile.inventory index 0f014b9..05d2b8e 100644 --- a/deploy/Dockerfile.inventory +++ b/deploy/Dockerfile.inventory @@ -2,8 +2,6 @@ FROM gemold-base:latest COPY src/inventory/ /app/src/inventory/ COPY src/entrypoints/ /app/src/entrypoints/ -COPY static/ /app/static/ - RUN mkdir -p /app/logs EXPOSE 8001 diff --git a/deploy/Dockerfile.moldinsight b/deploy/Dockerfile.moldinsight index 228029c..581eb16 100644 --- a/deploy/Dockerfile.moldinsight +++ b/deploy/Dockerfile.moldinsight @@ -17,7 +17,6 @@ COPY src/inventory/ /app/src/inventory/ COPY src/entrypoints/ /app/src/entrypoints/ COPY src/celery_app.py src/celery_tasks.py /app/src/ COPY uploads/ /app/uploads/ -COPY static/ /app/static/ COPY html_output/ /app/html_output/ RUN mkdir -p /app/logs diff --git a/deploy/build.bat b/deploy/build.bat index 0e54ead..7a64ed1 100644 --- a/deploy/build.bat +++ b/deploy/build.bat @@ -8,25 +8,32 @@ echo === 构建基础镜像 === docker build -t gemold-base:latest -f deploy\Dockerfile.base . echo. -echo === 构建 MoldInsight 镜像 (含 PythonOCC) === -docker build -t gemold-moldinsight:latest -f deploy\Dockerfile.moldinsight . - -echo. -echo === 构建 Inventory 镜像 === -docker build -t gemold-inventory:latest -f deploy\Dockerfile.inventory . +echo === 构建统一后端镜像 === +docker build -t gemold-backend:latest -f deploy\Dockerfile.moldinsight . echo. echo === 构建 Celery Worker 镜像 === docker build -t gemold-celery:latest -f deploy\Dockerfile.celery . +echo. +echo. +echo === 构建前端镜像 (Nginx 静态站点) === +docker build -t gemold-frontend:latest -f deploy\Dockerfile.frontend . + echo. echo === 全部构建完成 === echo. -echo 启动完整系统: -echo cd deploy ^&^& docker compose --profile full up -d +echo 启动完整系统(前端 + unified backend + celery): +echo docker compose --profile full up -d +echo. +echo 仅启动 unified backend: +echo docker compose --profile unified up -d +echo. +echo 仅启动前端入口: +echo docker compose --profile frontend up -d echo. echo 仅启动进销存: -echo cd deploy ^&^& docker compose --profile inventory up -d +echo docker compose --profile inventory up -d echo. echo 仅启动模具分析: -echo cd deploy ^&^& docker compose --profile moldinsight up -d +echo docker compose --profile moldinsight up -d diff --git a/deploy/build.sh b/deploy/build.sh index 21a9ec3..77109f7 100644 --- a/deploy/build.sh +++ b/deploy/build.sh @@ -10,25 +10,32 @@ echo "=== 构建基础镜像 ===" docker build -t gemold-base:latest -f deploy/Dockerfile.base . echo "" -echo "=== 构建 MoldInsight 镜像 (含 PythonOCC) ===" -docker build -t gemold-moldinsight:latest -f deploy/Dockerfile.moldinsight . - -echo "" -echo "=== 构建 Inventory 镜像 ===" -docker build -t gemold-inventory:latest -f deploy/Dockerfile.inventory . +echo "=== 构建统一后端镜像 ===" +docker build -t gemold-backend:latest -f deploy/Dockerfile.moldinsight . echo "" echo "=== 构建 Celery Worker 镜像 ===" docker build -t gemold-celery:latest -f deploy/Dockerfile.celery . +echo "" +echo "" +echo "=== 构建前端镜像 (Nginx 静态站点) ===" +docker build -t gemold-frontend:latest -f deploy/Dockerfile.frontend . + echo "" echo "=== 全部构建完成 ===" echo "" -echo "启动完整系统:" -echo " cd deploy && docker compose --profile full up -d" +echo "启动完整系统(前端 + unified backend + celery):" +echo " docker compose --profile full up -d" +echo "" +echo "仅启动 unified backend:" +echo " docker compose --profile unified up -d" +echo "" +echo "仅启动前端入口:" +echo " docker compose --profile frontend up -d" echo "" echo "仅启动进销存:" -echo " cd deploy && docker compose --profile inventory up -d" +echo " docker compose --profile inventory up -d" echo "" echo "仅启动模具分析:" -echo " cd deploy && docker compose --profile moldinsight up -d" +echo " docker compose --profile moldinsight up -d" diff --git a/deploy/nginx/frontend.conf b/deploy/nginx/frontend.conf new file mode 100644 index 0000000..0aa1f51 --- /dev/null +++ b/deploy/nginx/frontend.conf @@ -0,0 +1,46 @@ +upstream gemold_backend_upstream { + server backend:8000; +} + +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + location /assets/ { + try_files $uri =404; + access_log off; + expires 30d; + add_header Cache-Control "public, max-age=2592000, immutable"; + } + + location /api/ { + proxy_pass http://gemold_backend_upstream; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /health { + proxy_pass http://gemold_backend_upstream; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /html/ { + proxy_pass http://gemold_backend_upstream; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/docker-compose.yml b/docker-compose.yml index a2187d8..e7b63f0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,124 @@ services: + frontend: + build: + context: . + dockerfile: deploy/Dockerfile.frontend + image: gemold-frontend:latest + container_name: gemold_frontend + ports: + - "${FRONTEND_PORT:-80}:80" + depends_on: + - backend + restart: unless-stopped + profiles: + - full + - frontend + networks: + - gemold_network + + backend: + build: + context: . + dockerfile: deploy/Dockerfile.moldinsight + image: gemold-backend:latest + container_name: gemold_backend + command: ["python", "-m", "uvicorn", "entrypoints.unified:app", "--host", "0.0.0.0", "--port", "8000"] + ports: + - "${BACKEND_PORT:-8000}:8000" + environment: + HOST: 0.0.0.0 + PORT: "8000" + DB_HOST: ${DB_HOST} + DB_PORT: ${DB_PORT:-5432} + DB_NAME: ${DB_NAME:-moldinsight} + DB_USER: ${DB_USER} + DB_PASSWORD: ${DB_PASSWORD} + REDIS_HOST: ${REDIS_HOST} + REDIS_PORT: ${REDIS_PORT:-6379} + REDIS_PASSWORD: ${REDIS_PASSWORD:-} + RUSTFS_ENDPOINT: ${RUSTFS_ENDPOINT} + RUSTFS_ACCESS_KEY: ${RUSTFS_ACCESS_KEY} + RUSTFS_SECRET_KEY: ${RUSTFS_SECRET_KEY} + RUSTFS_TIMEOUT: ${RUSTFS_TIMEOUT:-30} + SECRET_KEY: ${SECRET_KEY:-change-me-in-production} + ADMIN_USERNAME: ${ADMIN_USERNAME:-admin} + ADMIN_PASSWORD: ${ADMIN_PASSWORD:-admin123} + ADMIN_EMAIL: ${ADMIN_EMAIL:-admin@gemold.com} + ADMIN_FULL_NAME: ${ADMIN_FULL_NAME:-系统管理员} + ALGORITHM: ${ALGORITHM:-HS256} + ACCESS_TOKEN_EXPIRE_MINUTES: ${ACCESS_TOKEN_EXPIRE_MINUTES:-1440} + DEBUG: ${DEBUG:-false} + SERVE_FRONTEND_STATIC: ${SERVE_FRONTEND_STATIC:-false} + UPLOAD_DIR: ${UPLOAD_DIR:-./uploads} + MAX_FILE_SIZE: ${MAX_FILE_SIZE:-104857600} + ALLOWED_EXTENSIONS: ${ALLOWED_EXTENSIONS:-.stp,.step,.stp.gz} + POINTCLOUD_SAMPLE_COUNT: ${POINTCLOUD_SAMPLE_COUNT:-10000} + MESH_QUALITY: ${MESH_QUALITY:-high} + PARALLEL_PROCESSING: ${PARALLEL_PROCESSING:-true} + RUSTFS_PRESIGNED_URL_EXPIRES: ${RUSTFS_PRESIGNED_URL_EXPIRES:-3600} + ENABLE_FREECAD_VERIFICATION: ${ENABLE_FREECAD_VERIFICATION:-false} + FREECAD_VERIFICATION_TIMEOUT: ${FREECAD_VERIFICATION_TIMEOUT:-120} + LLM_ENABLED: ${LLM_ENABLED:-false} + LLM_API_URL: ${LLM_API_URL:-https://api.openai.com/v1} + LLM_API_KEY: ${LLM_API_KEY:-} + LLM_MODEL: ${LLM_MODEL:-gpt-4o-mini} + LLM_TIMEOUT: ${LLM_TIMEOUT:-60} + LLM_MAX_TOKENS: ${LLM_MAX_TOKENS:-2000} + restart: unless-stopped + profiles: + - full + - unified + networks: + - gemold_network + + moldinsight-celery: + build: + context: . + dockerfile: deploy/Dockerfile.celery + container_name: gemold_celery + environment: + DB_HOST: ${DB_HOST} + DB_PORT: ${DB_PORT:-5432} + DB_NAME: ${DB_NAME:-moldinsight} + DB_USER: ${DB_USER} + DB_PASSWORD: ${DB_PASSWORD} + REDIS_HOST: ${REDIS_HOST} + REDIS_PORT: ${REDIS_PORT:-6379} + REDIS_PASSWORD: ${REDIS_PASSWORD:-} + RUSTFS_ENDPOINT: ${RUSTFS_ENDPOINT} + RUSTFS_ACCESS_KEY: ${RUSTFS_ACCESS_KEY} + RUSTFS_SECRET_KEY: ${RUSTFS_SECRET_KEY} + RUSTFS_TIMEOUT: ${RUSTFS_TIMEOUT:-30} + SECRET_KEY: ${SECRET_KEY:-change-me-in-production} + ALGORITHM: ${ALGORITHM:-HS256} + ACCESS_TOKEN_EXPIRE_MINUTES: ${ACCESS_TOKEN_EXPIRE_MINUTES:-1440} + DEBUG: ${DEBUG:-false} + SERVE_FRONTEND_STATIC: ${SERVE_FRONTEND_STATIC:-false} + UPLOAD_DIR: ${UPLOAD_DIR:-./uploads} + MAX_FILE_SIZE: ${MAX_FILE_SIZE:-104857600} + ALLOWED_EXTENSIONS: ${ALLOWED_EXTENSIONS:-.stp,.step,.stp.gz} + POINTCLOUD_SAMPLE_COUNT: ${POINTCLOUD_SAMPLE_COUNT:-10000} + MESH_QUALITY: ${MESH_QUALITY:-high} + PARALLEL_PROCESSING: ${PARALLEL_PROCESSING:-true} + RUSTFS_PRESIGNED_URL_EXPIRES: ${RUSTFS_PRESIGNED_URL_EXPIRES:-3600} + ENABLE_FREECAD_VERIFICATION: ${ENABLE_FREECAD_VERIFICATION:-false} + FREECAD_VERIFICATION_TIMEOUT: ${FREECAD_VERIFICATION_TIMEOUT:-120} + LLM_ENABLED: ${LLM_ENABLED:-false} + LLM_API_URL: ${LLM_API_URL:-https://api.openai.com/v1} + LLM_API_KEY: ${LLM_API_KEY:-} + LLM_MODEL: ${LLM_MODEL:-gpt-4o-mini} + LLM_TIMEOUT: ${LLM_TIMEOUT:-60} + LLM_MAX_TOKENS: ${LLM_MAX_TOKENS:-2000} + depends_on: + - backend + restart: unless-stopped + profiles: + - full + - unified + - moldinsight + networks: + - gemold_network + moldinsight: build: context: . @@ -30,6 +150,7 @@ services: ALGORITHM: ${ALGORITHM:-HS256} ACCESS_TOKEN_EXPIRE_MINUTES: ${ACCESS_TOKEN_EXPIRE_MINUTES:-1440} DEBUG: ${DEBUG:-false} + SERVE_FRONTEND_STATIC: ${SERVE_FRONTEND_STATIC:-false} UPLOAD_DIR: ${UPLOAD_DIR:-./uploads} MAX_FILE_SIZE: ${MAX_FILE_SIZE:-104857600} ALLOWED_EXTENSIONS: ${ALLOWED_EXTENSIONS:-.stp,.step,.stp.gz} @@ -47,53 +168,6 @@ services: LLM_MAX_TOKENS: ${LLM_MAX_TOKENS:-2000} restart: unless-stopped profiles: - - full - - moldinsight - networks: - - gemold_network - - moldinsight-celery: - build: - context: . - dockerfile: deploy/Dockerfile.celery - container_name: gemold_celery - environment: - DB_HOST: ${DB_HOST} - DB_PORT: ${DB_PORT:-5432} - DB_NAME: ${DB_NAME:-moldinsight} - DB_USER: ${DB_USER} - DB_PASSWORD: ${DB_PASSWORD} - REDIS_HOST: ${REDIS_HOST} - REDIS_PORT: ${REDIS_PORT:-6379} - REDIS_PASSWORD: ${REDIS_PASSWORD:-} - RUSTFS_ENDPOINT: ${RUSTFS_ENDPOINT} - RUSTFS_ACCESS_KEY: ${RUSTFS_ACCESS_KEY} - RUSTFS_SECRET_KEY: ${RUSTFS_SECRET_KEY} - RUSTFS_TIMEOUT: ${RUSTFS_TIMEOUT:-30} - SECRET_KEY: ${SECRET_KEY:-change-me-in-production} - ALGORITHM: ${ALGORITHM:-HS256} - ACCESS_TOKEN_EXPIRE_MINUTES: ${ACCESS_TOKEN_EXPIRE_MINUTES:-1440} - DEBUG: ${DEBUG:-false} - UPLOAD_DIR: ${UPLOAD_DIR:-./uploads} - MAX_FILE_SIZE: ${MAX_FILE_SIZE:-104857600} - ALLOWED_EXTENSIONS: ${ALLOWED_EXTENSIONS:-.stp,.step,.stp.gz} - POINTCLOUD_SAMPLE_COUNT: ${POINTCLOUD_SAMPLE_COUNT:-10000} - MESH_QUALITY: ${MESH_QUALITY:-high} - PARALLEL_PROCESSING: ${PARALLEL_PROCESSING:-true} - RUSTFS_PRESIGNED_URL_EXPIRES: ${RUSTFS_PRESIGNED_URL_EXPIRES:-3600} - ENABLE_FREECAD_VERIFICATION: ${ENABLE_FREECAD_VERIFICATION:-false} - FREECAD_VERIFICATION_TIMEOUT: ${FREECAD_VERIFICATION_TIMEOUT:-120} - LLM_ENABLED: ${LLM_ENABLED:-false} - LLM_API_URL: ${LLM_API_URL:-https://api.openai.com/v1} - LLM_API_KEY: ${LLM_API_KEY:-} - LLM_MODEL: ${LLM_MODEL:-gpt-4o-mini} - LLM_TIMEOUT: ${LLM_TIMEOUT:-60} - LLM_MAX_TOKENS: ${LLM_MAX_TOKENS:-2000} - depends_on: - - moldinsight - restart: unless-stopped - profiles: - - full - moldinsight networks: - gemold_network @@ -125,9 +199,9 @@ services: ALGORITHM: ${ALGORITHM:-HS256} ACCESS_TOKEN_EXPIRE_MINUTES: ${ACCESS_TOKEN_EXPIRE_MINUTES:-1440} DEBUG: ${DEBUG:-false} + SERVE_FRONTEND_STATIC: ${SERVE_FRONTEND_STATIC:-false} restart: unless-stopped profiles: - - full - inventory networks: - gemold_network diff --git a/docs/FRONTEND_UNIFIED_DEPLOYMENT_PLAN.md b/docs/FRONTEND_UNIFIED_DEPLOYMENT_PLAN.md new file mode 100644 index 0000000..af96892 --- /dev/null +++ b/docs/FRONTEND_UNIFIED_DEPLOYMENT_PLAN.md @@ -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 路径级业务分流”的长期维护问题。 \ No newline at end of file diff --git a/docs/MOLDINSIGHT_TECH_DEBT_PLAN.md b/docs/MOLDINSIGHT_TECH_DEBT_PLAN.md new file mode 100644 index 0000000..942a28d --- /dev/null +++ b/docs/MOLDINSIGHT_TECH_DEBT_PLAN.md @@ -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` 停写后,历史行中的旧数据仍可读(列保留),仅新行不再写入 diff --git a/docs/deployment/DEPLOY_PORT.md b/docs/deployment/DEPLOY_PORT.md index 37dea57..fe0a223 100644 --- a/docs/deployment/DEPLOY_PORT.md +++ b/docs/deployment/DEPLOY_PORT.md @@ -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 独立部署端口 示例: diff --git a/docs/deployment/LINUX_SETUP.md b/docs/deployment/LINUX_SETUP.md index e69b953..8eec9d0 100644 --- a/docs/deployment/LINUX_SETUP.md +++ b/docs/deployment/LINUX_SETUP.md @@ -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` 容器独立提供,并通过同域反代转发到后端。 示例: diff --git a/docs/deployment/PORT_CONFIG.md b/docs/deployment/PORT_CONFIG.md index b6bef78..b4cf4ab 100644 --- a/docs/deployment/PORT_CONFIG.md +++ b/docs/deployment/PORT_CONFIG.md @@ -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 | diff --git a/frontend/README.md b/frontend/README.md index 33895ab..1b0eb3a 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -1,5 +1,74 @@ -# Vue 3 + TypeScript + Vite +# geMoldInsight Frontend -This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `